从 RAG 到 Agentic RAG:第三步,本地知识不够就去网络搜索

前两步,我们已经让 RAG 学会判断"要不要检索",也能把复杂问题拆开做多轮检索。但还有一个很现实的问题:如果答案压根不在本地知识库里怎么办?这一篇继续改造 LangGraph:检索后先评估上下文是否足够,不够就生成 Web Query,自动进入网络搜索节点补充信息,再交给模型生成最终回答。

前两篇,我们已经让 RAG 做了两件事。

第一步:

复制代码
这个问题
要不要查知识库?

第二步:

复制代码
这个复杂问题
一次检索够不够?

于是流程已经从最开始的:

复制代码
Question
   ↓
Retrieve
   ↓
Generate

慢慢变成了一个会判断、会循环的 Graph。

但这里还有一个绕不过去的问题:

如果答案根本不在本地知识库里呢?

这不是"多查几次"能解决的。


一、多检索几次,也搜不到不存在的东西

我们的本地知识库还是《天龙八部》。

现在用户问:

复制代码
《天龙八部》中虚竹最后继承了哪些身份?
另外,第一部《天龙八部》电视剧是什么时候上映的?

第一个问题:

复制代码
虚竹最后继承了哪些身份?

答案就在小说里。

Milvus 可以从本地知识库中检索相关片段。

但第二个问题:

复制代码
第一部《天龙八部》电视剧是什么时候上映的?

就不一样了。

我们的知识库里存的是小说内容,并没有电视剧的上映信息。

这时候即使:

ini 复制代码
Top K = 5

改成:

ini 复制代码
Top K = 20

甚至重新检索很多次,也解决不了。

因为问题不是:

没搜准。

而是:

本地根本没有。

这两个问题要分开。


二、传统 RAG 的问题:检索完就直接回答

普通 RAG 往往是:

复制代码
Question
   ↓
Retrieve
   ↓
Generate

也就是说:

markdown 复制代码
检索到了什么
      ↓
直接塞给 LLM
      ↓
生成回答

这里其实少了一步:

这些资料真的够回答用户的问题吗?

比如刚才的问题,本地知识库可能已经找到了虚竹相关的小说片段。

但关于:

复制代码
第一部《天龙八部》电视剧什么时候上映

本地完全没有信息。

如果这时候直接把检索结果交给模型回答,模型可能会依赖自己的参数记忆把后半部分补出来。

于是就容易出现:

diff 复制代码
上下文里没有
+
模型自己补
=
幻觉

所以不能再让:

复制代码
Retrieve → Generate

直接连在一起。

中间需要增加一个节点:

复制代码
Evaluate

变成:

复制代码
Retrieve
   ↓
Evaluate
   ↓
信息够吗?

这个节点才是这一篇真正的核心。


三、先让模型判断:现有信息够不够

先给 Graph 增加几个状态:

php 复制代码
const GraphState = Annotation.Root({
  question: Annotation,

  retrievedDocs: Annotation,
  localContext: Annotation,

  webContext: Annotation,
  evaluation: Annotation,

  generation: Annotation,
});

这里最值得关注的是:

复制代码
localContext
webContext
evaluation

分别代表:

复制代码
localContext
→ 本地知识库找到的内容

webContext
→ 后面网络搜索补充的内容

evaluation
→ 模型对当前信息是否足够的判断

接下来还是先走本地检索。


四、本地检索节点只负责找资料

这一层其实没什么新东西。

还是从 Milvus 中检索相关文档:

ini 复制代码
const retrieveLocalNode = async (state) => {
  const retrievedDocs = await retrieveRelevantContent(
    state.question,
    state.k
  );

  const localContext = retrievedDocs
    .map((doc) => doc.content)
    .join("\n\n");

  return {
    retrievedDocs,
    localContext,
  };
};

流程就是:

css 复制代码
Question
   ↓
Embedding
   ↓
Milvus
   ↓
Top K Documents
   ↓
localContext

但这里有一个职责划分值得注意:

Retrieve 只负责找资料,不负责判断这些资料够不够。

信息是否充分,交给下一个节点。


五、Evaluate 才是这一轮真正的决策节点

我们希望模型返回一个结构化结果:

css 复制代码
const EvaluateSchema = z.object({
  enough: z.boolean(),
  missing: z.array(z.string()).max(6),
  reason: z.string(),
  web_query: z.string().optional(),
});

几个字段分别解决不同问题。

enough

当前上下文够不够回答:

arduino 复制代码
true
→ 可以回答

false
→ 信息还不够

missing

如果不够,具体缺什么。

比如刚才的问题可能得到:

复制代码
缺少第一部《天龙八部》电视剧上映时间的信息

reason

为什么认为当前上下文足够或不足。

web_query

如果本地信息不够:

下一步应该去网上搜什么?

于是 Evaluate 节点做的事情就很清楚了:

markdown 复制代码
用户问题
+
本地检索结果
        ↓
       LLM
        ↓
信息够不够?

六、为什么不直接拿用户原问题去搜索?

这里有一个很关键的细节。

用户原问题是:

复制代码
《天龙八部》中虚竹最后继承了哪些身份?
另外,第一部《天龙八部》电视剧是什么时候上映的?

经过本地检索以后:

复制代码
虚竹最后继承了哪些身份?

这部分已经有答案了。

真正缺的是:

复制代码
第一部《天龙八部》电视剧是什么时候上映的?

如果直接把原问题整个扔进搜索引擎,搜索目标反而不够集中。

更合理的是:

markdown 复制代码
先判断缺什么
      ↓
只针对缺失信息生成 Web Query
      ↓
再去搜索

比如生成:

复制代码
第一部《天龙八部》电视剧上映时间

所以 web_query 并不是一个随手加进去的字段。

它解决的是:

已经从本地拿到的信息不用重复找,只搜索真正缺失的部分。

这个思路和上一篇的子问题拆解其实很像。

本质上都是在做:

让 Query 更适合下一次检索。


七、现在 Graph 第一次开始切换数据源

Evaluate 有了结果以后,就可以决定下一步去哪里。

逻辑很简单:

javascript 复制代码
function afterEvaluateLocal(state) {
  const parsed = JSON.parse(state.evaluation || "{}");

  return parsed.enough === true
    ? "generate"
    : "web_search";
}

如果信息已经足够:

复制代码
Evaluate
   ↓
Generate

如果还不够:

sql 复制代码
Evaluate
   ↓
Web Search

Graph 就变成:

sql 复制代码
Local Retrieve
      ↓
   Evaluate
      ↓
   够了吗?
   ↙    ↘
 够      不够
 ↓        ↓
Generate Web Search

这里其实发生了一个很重要的变化。

以前:

复制代码
数据源是固定的。

现在:

复制代码
数据源开始根据当前信息动态切换。

这已经非常接近 Agentic RAG 的核心了。


这里可以接任意 Web Search API。

具体使用哪一家搜索服务,并不是这一节的重点。

对于整个 Graph 来说,Web Search 节点只需要完成三件事:

markdown 复制代码
1. 从 State 拿到 web_query

2. 调用网络搜索服务

3. 把搜索结果写回 webContext

核心逻辑可以压缩成:

ini 复制代码
const webSearchNode = async (state) => {
  const parsed = JSON.parse(state.evaluation || "{}");

  const query =
    parsed.web_query?.trim() ||
    state.question;

  const webContext = await webSearch(query);

  return {
    webContext,
  };
};

注意:

sql 复制代码
Web Search

本身并不负责回答用户。

它只是:

再找回来一批资料。

这一点和 Milvus 的 Retrieve 节点其实很像。

只是数据源不一样:

sql 复制代码
Local Retrieve
→ 本地向量数据库

Web Search
→ 互联网

九、网络搜索结果本质上也只是 Context

网络搜索拿回来以后,可以整理成类似:

makefile 复制代码
引用: 1
标题: ...
URL: ...
摘要: ...

引用: 2
标题: ...
URL: ...
摘要: ...

然后写进:

复制代码
webContext

这时候 State 里就有了两部分上下文:

diff 复制代码
localContext
+
webContext

最后生成回答的时候,把它们合起来:

sql 复制代码
const context = [
  state.localContext,
  state.webContext,
]
  .filter(Boolean)
  .join("\n\n");

于是 LLM 最终看到的是:

diff 复制代码
小说知识库中的相关内容
+
网络搜索补充的信息
+
用户问题

对于刚才的问题:

复制代码
《天龙八部》中虚竹最后继承了哪些身份?
另外,第一部《天龙八部》电视剧是什么时候上映的?

最终答案的依据就来自两个地方:

复制代码
虚竹的身份
→ 本地小说知识库

电视剧上映时间
→ 网络搜索

这就是多数据源 RAG 最直观的形态。


这里是整个流程里很值得注意的一点。

我们没有直接写:

sql 复制代码
Web Search
   ↓
Generate

而是:

sql 复制代码
Web Search
   ↓
Evaluate

原因很简单:

搜过网络,不代表搜回来的信息一定够。

搜索结果也可能:

复制代码
没有命中
结果跑偏
信息不完整
无法确认关键事实

所以更合理的职责划分应该是:

sql 复制代码
Search
→ 负责找

Evaluate
→ 负责判断

整个过程变成:

复制代码
本地检索
   ↓
评估
   ↓
不够
   ↓
网络搜索
   ↓
再评估

这时候整个检索流程才真正形成一个闭环。


十一、Agentic 不代表无限循环

不过这里还有一个很现实的问题。

如果写成:

sql 复制代码
Evaluate
   ↓
Web Search
   ↓
Evaluate
   ↓
Web Search
   ↓
......

理论上可能一直循环。

这显然不是我们想要的。

所以实际系统一定要加边界。

当前这个简单 Demo 可以规定:

markdown 复制代码
本地检索
    ↓
第一次 Evaluate
    ↓
   不够
    ↓
Web Search
    ↓
第二次 Evaluate
    ↓
Generate

如果网络补充以后仍然无法确认答案,就明确告诉用户:

复制代码
当前上下文仍不足以确认

而不是继续编。

这也是实现 Agent 时非常重要的一点:

自主决策不等于无限自主。

系统仍然需要明确的退出条件和执行边界。


十二、把这一篇的 Graph 串起来

现在完整流程大概是:

markdown 复制代码
                  ┌→ direct_answer → END
                  │
Question → Router
                  │
                  └→ local_retrieve
                           ↓
                        evaluate
                           ↓
                       信息够吗?
                       ↙      ↘
                     够        不够
                     ↓          ↓
                  generate   web_search
                     ↑          ↓
                     └──── evaluate

对应到 LangGraph:

php 复制代码
const graph = new StateGraph(GraphState)
  .addNode("route_question", routeQuestionNode)
  .addNode("direct_answer", directAnswerNode)

  .addNode("local_retrieve", retrieveLocalNode)
  .addNode("evaluate_local", evaluateNode)
  .addNode("web_search", webSearchNode)

  .addNode("generate", generateNode)

  .addEdge(START, "route_question")

  .addConditionalEdges("route_question", afterRoute, {
    direct_answer: "direct_answer",
    local_retrieve: "local_retrieve",
  })

  .addEdge("local_retrieve", "evaluate_local")

  .addConditionalEdges(
    "evaluate_local",
    afterEvaluateLocal,
    {
      generate: "generate",
      web_search: "web_search",
    }
  )

  .addEdge("web_search", "evaluate_local")

  .addEdge("direct_answer", END)
  .addEdge("generate", END)

  .compile();

单看代码,只是增加了:

sql 复制代码
Evaluate
Web Search

两个节点。

但系统能力其实已经发生了很明显的变化。


十三、从"检索流程"走向"检索决策"

回头看最开始的 RAG:

复制代码
Question
   ↓
Retrieve
   ↓
Generate

所有步骤都是固定的。

后来加入 Router:

复制代码
要不要检索?

再加入多跳检索:

复制代码
一次检索够不够?

现在又加入 Evaluate + Web Search:

复制代码
本地信息够不够?

如果不够,
要不要换一个数据源继续找?

所以 Agentic RAG 真正重要的并不是:

复制代码
用了多少个 Agent

也不是:

复制代码
Graph 画得有多复杂

真正发生变化的是:

LLM 开始参与检索流程本身的决策。


十四、到这里,Agentic RAG 的主线已经基本完整

现在这套流程已经能够处理传统 RAG 中几个很典型的问题:

sql 复制代码
简单问题
→ 不检索,直接回答

知识库问题
→ 查询 Milvus

复杂问题
→ 拆成多个子问题,多轮检索

信息不足
→ 判断当前缺什么

本地没有
→ 自动切换到 Web Search

串起来就是:

复制代码
Question
   ↓
判断要不要查
   ↓
判断应该怎么查
   ↓
检索
   ↓
判断查到的够不够
   ↓
不够就继续查 / 换数据源
   ↓
Generate

这时候再看 Agentic RAG,就不应该简单理解成:

RAG 加几个 Agent。

更准确一点:

让 LLM 成为检索流程里的决策中枢,根据当前问题和已有证据,动态决定下一步应该做什么。


十五、但还有一种检索能力没有补上

现在我们已经有:

复制代码
Milvus
→ 语义检索

也有:

sql 复制代码
Web Search
→ 网络信息补充

但还有一类数据:

复制代码
专业术语
精确实体
专有名词

这些内容有时候并不需要:

复制代码
意思差不多

而是需要:

复制代码
关键词准确命中

这时候纯向量检索并不一定合适。

所以接下来还需要补上一种能力:

复制代码
关键词检索

这就要进入 Elasticsearch 了。

在继续做混合检索之前,先把 Elasticsearch 最核心的三个东西讲清楚:

复制代码
IK 分词器
→ 词怎么切

倒排索引
→ 怎么快速找到文档

BM25
→ 找到以后怎么排序

这会是下一篇。

相关推荐
桃西西呀1 小时前
别被"秒回"骗了:推理模型背后那只"吞金兽",吃的是你看不见的预算
人工智能·llm·ai编程
龙亘川1 小时前
AI + 人社新范式:智慧人社系统如何为民生治理数字化难题提供帮助
人工智能·智慧城市·数据可视化·政务
水如烟1 小时前
孤能子视角:蓝星文明篇·市——交换机制的运行化:从偶发交换到日常运行的制度化
人工智能
技灵AI2 小时前
Wan 3.0 API怎么做多参考商品视频?从图片、视频、音频分工到30秒交付
人工智能·prompt·aigc·音视频·wan 3.0
武子康2 小时前
CLAUDE.md 引用 AGENTS.md 后,两边真的读到同一套规则吗?
人工智能·llm·agent
动恰客流统计2 小时前
线下零售数字化浪潮下,客流统计的3个核心发展趋势
大数据·前端·人工智能
johnsong2 小时前
效率的边界:当推理突破遇见语言革命
人工智能·语言模型
染指11102 小时前
119.Agent-LangChain核心组件-Runtime运行时
人工智能·langchain·agent
DevNo2 小时前
英文会议音视频转中文纪要:我近期的几款工具使用记录
人工智能