Skip to content

检索增强智能体

前三篇的 RAG 都是"固定流程":人提前画好图,模型只能按图走。Agentic RAG(检索增强智能体)换个思路——把检索、计算等都做成工具,交给 ReAct 智能体自己决定:要不要检索、检索几次、信息够不够、要不要再算一下。流程由模型动态决定,而不是写死。

一、固定流程 RAG 的局限

回头看 基础 RAG自适应 RAGCRAG,它们的流程都是开发者预设的:

  • 检索几次、什么时候停,都是图里写死的。
  • 想换数据源(比如先查本地再查 web),要重新改图加节点。
  • 用户问"比较 A 和 B"这种需要多次检索的问题,固定流程很难优雅处理。

而智能体的思路是:把检索当工具,让 LLM 自己 plan + act。它会判断"先查 A,再查 B,然后对比"——这种灵活度是固定图做不到的。

二、Agentic RAG 的核心:检索即工具

用 LangGraph 的 create_react_agent,把检索函数注册成 @tool,智能体就能像调用计算器一样调用检索。

mermaid
flowchart TD
    U([用户问题]) --> A[Agent LLM]
    A -- 需要资料 --> T1[retrieve_kb 检索工具]
    A -- 需要算 --> T2[calculate 计算工具]
    T1 --> A
    T2 --> A
    A -- 信息够了 --> R([最终回答])

智能体在一个循环里反复"思考 → 调工具 → 看结果 → 再思考",直到它觉得能回答为止。这正是 ReAct 智能体 的核心循环,只不过这里把"检索"作为工具之一。

对 Java/LangGraph4j 同学:可以理解为把"硬编码的流程图"换成"由 LLM 在运行时动态选择工具的循环",类似把固定 BPML 换成 Agent 自主决策。

三、与固定流程 RAG 对比

维度固定流程 RAGAgentic RAG
流程开发者画死LLM 动态决定
检索次数固定(通常 1 次,CRAG 最多 2 次)按需,0 次到多次
多数据源改图加节点加一个工具即可
复杂问题难(如对比类需多次检索)自然支持
成本可控性高(流程固定)低(智能体可能滥用工具)
调试难度中(看图就行)高(要看每步思考)

简单说:问题简单且流程固定用 RAG,问题开放需要多步探索用 Agentic RAG

四、多检索源作为不同工具

Agentic RAG 的一大优势是"多数据源即插即用"。常见工具组合:

  • retrieve_kb:本地向量库检索(事实性知识)
  • query_sql:查数据库(结构化数据,如订单、库存)
  • web_search:网络搜索(时效性、本地库覆盖不到的内容)
  • calculate:精确计算(避免 LLM 算错)

智能体根据问题性质自动选工具。本篇示例用 retrieve_kb + calculate 两个工具演示,扩展到多检索源只需照葫芦画瓢加 @tool 函数。

五、状态与工具定义

create_react_agent 默认用 MessagesState(只含消息列表),我们直接复用,不必自定义状态。

python
from langchain_core.tools import tool
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document

# ---- 本地知识库 ----
docs = [Document(page_content=t) for t in [
    "LangGraph 用图建模流程,节点是普通函数。",
    "条件边可以根据状态选择下一个节点,实现分支。",
    "Reducer 决定同一字段多次更新时如何合并,如追加或覆盖。",
    "MemorySaver 是 LangGraph 自带的内存检查点,用于多轮记忆。",
]]
vs = InMemoryVectorStore(OpenAIEmbeddings(model="text-embedding-3-small"))
vs.add_documents(docs)
retriever = vs.as_retriever(search_kwargs={"k": 2})

@tool
def retrieve_kb(query: str) -> str:
    """检索本地知识库,返回与问题相关的资料片段。当需要查询 LangGraph 相关概念、用法时使用。"""
    docs = retriever.invoke(query)
    if not docs:
        return "未检索到相关资料。"
    return "\n\n".join(d.page_content for d in docs)

@tool
def calculate(expression: str) -> str:
    """精确计算一个数学表达式,例如 '1+2*3'。当需要准确数值计算时使用,避免估算出错。"""
    try:
        # 注意:真实项目里 eval 有安全风险,这里仅 demo
        # 生产建议用 ast.literal_eval 或 sympy
        result = eval(expression, {"__builtins__": {}}, {})
        return f"{expression} = {result}"
    except Exception as e:
        return f"计算失败:{e}"

注意 @tool 装饰器里的文档字符串非常重要——它就是给智能体看的"工具说明书",模型根据这段描述决定何时调用。写清楚"什么时候该用这个工具"能显著减少误调用。

六、组装智能体

create_react_agent 一行搞定,传入模型和工具列表。

python
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

agent = create_react_agent(
    model=llm,
    tools=[retrieve_kb, calculate],
    # 系统提示,塑造智能体行为
    prompt=(
        "你是一个严谨的助手。回答事实性问题前先用 retrieve_kb 查资料,"
        "遇到数值计算用 calculate。如果资料不足就直说不知道,不要编造。"
    ),
)

create_react_agent 内部其实构建了一张 agent ↔ tools 的循环图:agent 节点决定调哪个工具,tools 节点执行工具后把结果回传给 agent,直到 agent 不再调工具直接给最终答案。详见 ReAct 智能体

七、完整可运行示例

python
import os
from langchain_core.tools import tool
from langchain_core.documents import Document
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langgraph.prebuilt import create_react_agent

# ---- 知识库 ----
docs = [Document(page_content=t) for t in [
    "LangGraph 用图建模流程,节点是普通函数。",
    "条件边可以根据状态选择下一个节点,实现分支。",
    "Reducer 决定同字段多次更新如何合并,如追加或覆盖。",
    "MemorySaver 是 LangGraph 自带的内存检查点,用于多轮记忆。",
]]
vs = InMemoryVectorStore(OpenAIEmbeddings(model="text-embedding-3-small"))
vs.add_documents(docs)
retriever = vs.as_retriever(search_kwargs={"k": 2})

# ---- 工具 ----
@tool
def retrieve_kb(query: str) -> str:
    """检索本地知识库,返回与 query 相关的 LangGraph 资料片段。需要查概念/用法时用。"""
    docs = retriever.invoke(query)
    return "\n\n".join(d.page_content for d in docs) if docs else "未检索到相关资料。"

@tool
def calculate(expression: str) -> str:
    """精确计算数学表达式,如 '1+2*3'。需要准确数值时用。"""
    try:
        return f"{expression} = {eval(expression, {'__builtins__': {}}, {})}"
    except Exception as e:
        return f"计算失败:{e}"

# ---- 智能体 ----
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
agent = create_react_agent(
    model=llm,
    tools=[retrieve_kb, calculate],
    prompt="你是严谨助手。事实问题先 retrieve_kb,数值计算用 calculate,资料不足就说不知道。",
)

if __name__ == "__main__":
    # 测试 1:纯知识问答 → 智能体应调用 retrieve_kb
    r = agent.invoke({"messages": [("user", "条件边和 Reducer 分别是做什么的?")]})
    print("=== 测试1 ===")
    print(r["messages"][-1].content)

    # 测试 2:知识 + 计算 → 智能体应先检索再计算
    r = agent.invoke({"messages": [("user", "LangGraph 里有几个核心概念?把核心概念数量乘以 100 算给我看。")]})
    print("\n=== 测试2 ===")
    for m in r["messages"]:
        print(type(m).__name__, ":", str(m.content)[:120])

    # 测试 3:纯计算 → 智能体应只调用 calculate,不检索
    r = agent.invoke({"messages": [("user", "123 * 456 等于多少?")]})
    print("\n=== 测试3 ===")
    print(r["messages"][-1].content)

测试 2 很有意思:智能体会先调 retrieve_kb 查"核心概念",得到几条资料后数一下条数,再调 calculate 算乘法,最后给答案。这种"查资料 → 推理 → 算"的串联是固定流程 RAG 难以表达的。

八、用 stream 看清智能体的思考过程

智能体黑盒感强,调试时建议用 stream 看每一步:

python
for chunk in agent.stream(
    {"messages": [("user", "MemorySaver 是什么?能存几条?")]},
    stream_mode="updates",
):
    for node, update in chunk.items():
        print(f"[{node}]")
        for m in update.get("messages", []):
            print("  ", type(m).__name__, ":", str(m.content)[:100])

你会看到 agent 节点输出一个 tool_calltools 节点输出一个 ToolMessage,然后 agent 再输出最终答案。这种可见性对排查"为什么智能体乱调工具"非常关键。

九、常见踩坑

1. 智能体滥用检索(调用太多次)

表现:一个简单问题,智能体反复 retrieve_kb 五六次,token 飙升。原因往往是:

  • 工具描述太模糊:模型不确定工具干啥,就反复试。把描述写具体("当需要 X 时用")能大幅缓解。
  • 系统 prompt 没给停止信号:加一句"信息足够后直接回答,不要重复检索"。
  • 模型太弱:小模型容易陷在工具循环里出不来,换更强模型或加 recursion_limit

recursion_limit 是硬刹车:agent.invoke({...}, {"recursion_limit": 10}) 限制图最多走 10 步,超了直接报错,避免无限循环烧钱。

2. 成本控制

Agentic RAG 的成本不可预测——同一问题可能调 1 次工具,也可能调 5 次。生产建议:

  • recursion_limit:强制上限。
  • 缓存工具结果:相同 query 的检索结果缓存,省 embedding 和 LLM 费用。
  • 监控 token:把每次 invoke 的 token 用量落日志,设单次请求预算告警。
  • 分级模型:简单判断用小模型,复杂推理用大模型。

3. 工具描述写不好导致误调用

@tool 的 docstring 是智能体选工具的唯一依据。反面教材:

python
@tool
def search(q: str) -> str:
    """搜索"""          # 太模糊,模型不知道啥时候用
    ...

正面教材:

python
@tool
def retrieve_kb(query: str) -> str:
    """检索本地 LangGraph 知识库,返回相关资料。需要查询 LangGraph 概念/用法/配置时使用;闲聊或时效性问题不要用。"""
    ...

把"什么时候用"和"什么时候不用"都写上,模型选对率会高很多。

4. 多工具之间结果冲突

retrieve_kbweb_search 都返回结果且互相矛盾时,智能体可能无所适从。建议:

  • 在系统 prompt 里给优先级:"本地资料优先,本地没有再查 web,矛盾时以本地为准并说明。"
  • 工具返回里带来源标记(如 [kb] / [web]),方便智能体在答案里溯源。

十、小结

  • Agentic RAG = 把检索作为工具交给 ReAct 智能体自主调用,流程由模型动态决定。
  • create_react_agent(model, tools=[retrieve_kb, ...]) 一行组装,@tool 的 docstring 是工具说明书,要写清"何时用"。
  • 相比固定流程 RAG,更灵活、支持多步探索和多数据源,但成本更不可控。
  • 必加 recursion_limit 防止智能体陷入工具循环,生产要监控 token 和缓存工具结果。

本模块 RAG 部分到此结束。接下来进入 实战项目,把这些 RAG 能力塞进真实业务场景里。