RAG 很容易做出 Demo。
准备一个向量数据库,把用户问题转成向量,检索几个相关片段,再塞给大模型:
Question
↓
Retrieve
↓
Generate
↓
Answer
代码甚至可以简单到只有两个节点:
const graph <span>=</span> <span>new</span> StateGraph(GraphState)
.addNode("retrieve", retrieveNode)
.addNode("generate", generateNode)
.addEdge(<span>START</span>, "retrieve")
.addEdge("retrieve", "generate")
.addEdge("generate", <span>END</span>)
.compile();
这已经是一个完整的 RAG。
但问题也恰恰出在这里:
它太听话了。
无论用户问什么,它都老老实实执行:
检索 → 生成
它不会想:
这个问题真的需要查知识库吗?
这就是普通 RAG 和 Agentic RAG 之间一个非常重要的分界点。
一、Naive RAG 的问题不是不能检索,而是只会检索
假设我们做的是一个《天龙八部》知识库。
用户问:
阿朱的结局是什么?
这个问题显然适合走知识库。
因为它涉及小说中的具体情节:
问题
↓
Milvus
↓
找到阿朱相关章节
↓
LLM 根据原文回答
没有问题。
但如果用户问:
什么是向量数据库?
我们的 Naive RAG 还是会执行:
什么是向量数据库?
<span> ↓
搜索《天龙八部》
↓
返回几个莫名其妙的小说片段
↓
交给 LLM
</span>
这就很荒谬了。
真正的问题不在 Milvus,也不在 Embedding。
而在于:
我们的程序没有决策能力。
它只是一个固定的数据处理流水线。
二、真正的升级:让 LLM 参与控制流
所以 Agentic RAG 的第一步,并不是突然增加十几个 Agent。
也不是给模型塞一堆 Tool。
而是做一件很小、但非常关键的事情:
让模型判断下一步该怎么走。
原来的流程:
<span>START</span>
↓
retrieve
↓
generate
↓
<span>END</span>
现在变成:
┌→ direct_answer → <span>END</span>
│
<span>START</span> → router ──┤
│
└→ retrieve → rag_generate → <span>END</span>
程序第一次出现了真正意义上的分支。
模型不再只是最后负责回答问题。
它开始参与:
下一步执行哪个节点?
这才是这一阶段最值得理解的地方。
三、先定义 Graph State
我们的 Graph 需要维护几份数据:
<span>const</span> <span>GraphState</span> = Annotation.<span>Root</span>({
<span>question</span>: Annotation,
<span>k</span>: Annotation,
<span>strategy</span>: Annotation,
<span>routeReason</span>: Annotation,
<span>documents</span>: Annotation,
<span>generation</span>: Annotation,
});
可以把它理解成整个工作流共享的一块状态:
GraphState
question
用户问题
k
检索数量
strategy
路由结果
routeReason
为什么这么路由
documents
检索出来的文档
generation
最终回答
节点执行过程中,不需要自己把整个 State 从头维护一遍。
例如路由节点只负责产生:
<span>return</span> {
strategy: route.strategy,
routeReason: route.reason,
};
LangGraph 会把这些字段更新回 Graph State。
所以理解 LangGraph 时,可以先记住一句话:
节点读取当前 State,返回自己想修改的部分。
这样看代码会清晰很多。
四、让模型只负责一件事:分类
接下来定义路由结果:
<span>const</span> RouteSchema = z.<span>object</span>({
strategy: z.<span>enum</span>([<span>"simple"</span>, <span>"complex"</span>]),
reason: z.<span>string</span>(),
});
我们不要让模型自由发挥:
我觉得这个问题可能需要检索……
而是要求它严格输出:
{
strategy: <span>"complex"</span>,
reason: <span>"涉及《天龙八部》具体人物情节"</span>
}
这里使用:
model.withStructuredOutput(RouteSchema)<span>;</span>
得到一个受 Zod Schema 约束的模型。
const <span>router</span> = model.withStructuredOutput(RouteSchema)<span>;</span>
然后让它完成分类:
const <span>routeQuestionNode</span> = async (state) => {
const <span>router</span> = model.withStructuredOutput(RouteSchema)<span>;</span>
const <span>route</span> = await router.invoke(`
你是问答路由器,请判断用户问题是否需要外部检索。
规则:
simple:
常识问答、简短定义,
无需特定小说细节即可回答。
complex:
需要《天龙八部》具体情节、
人物关系、章节事实、
原文细节或证据支持。
用户问题:
${state.question}
`)<span>;</span>
return {
strategy: route.strategy,
routeReason: route.reason,
}<span>;</span>
}<span>;</span>
这里的大模型已经不是“回答模型”了。
它承担的是另一种角色:
Router
输入:
阿朱的结局是什么?
可能得到:
{
strategy: <span>"complex"</span>,
reason: <span>"涉及小说具体人物结局,需要小说内容支持"</span>
}
而输入:
什么是向量数据库?
可能得到:
{
strategy: <span>"simple"</span>,
reason: <span>"属于通用技术概念,无需查询小说知识库"</span>
}
这时候模型还没有回答用户。
它只是完成了:
分类
五、真正决定程序怎么走的是 Edge
这是我认为 LangGraph 最值得理解的一点。
很多人第一次看 Agent Workflow,会把所有注意力放在 Node 上:
这个 Node 调了哪个模型?
这个 Node Prompt 怎么写?
但实际上:
Node 决定“做什么”,Edge 决定“接下来做什么”。
定义一个条件函数:
const <span>decideNext</span> = (state) => {
return <span>state.strategy</span> === <span>"simple"</span>
? "direct_answer"
: "retrieve"<span>;</span>
}<span>;</span>
然后交给:
<span>.addConditionalEdges</span>()
.<span>addConditionalEdges</span>(
<span>"route_question"</span>,
decideNext,
{
<span>direct_answer</span>: <span>"directAnswer"</span>,
<span>retrieve</span>: <span>"retrieve"</span>,
}
)
于是:
routeQuestionNode
↓
strategy
↓
<span>decideNext</span>()
如果:
<span>strategy</span> === <span>"simple"</span>
走:
directAnswer
如果:
<span>strategy</span> === <span>"complex"</span>
走:
retrieve
这已经不是传统的:
await <span>fn1</span>();
await <span>fn2</span>();
await <span>fn3</span>();
而是在描述:
一个状态驱动的执行图。
六、两条路线终于分开了
完整 Graph:
<span>const</span> <span>graph</span> = <span>new</span> <span>StateGraph</span>(GraphState)
.<span>addNode</span>(<span>"route_question"</span>, routeQuestionNode)
.<span>addNode</span>(<span>"directAnswer"</span>, directAnswerNode)
.<span>addNode</span>(<span>"retrieve"</span>, retrieveNode)
.<span>addNode</span>(<span>"rag_generate"</span>, generateNode)
.<span>addEdge</span>(START, <span>"route_question"</span>)
.<span>addConditionalEdges</span>(
<span>"route_question"</span>,
decideNext,
{
<span>direct_answer</span>: <span>"directAnswer"</span>,
<span>retrieve</span>: <span>"retrieve"</span>,
}
)
.<span>addEdge</span>(<span>"directAnswer"</span>, END)
.<span>addEdge</span>(<span>"retrieve"</span>, <span>"rag_generate"</span>)
.<span>addEdge</span>(<span>"rag_generate"</span>, END)
.<span>compile</span>();
现在两个问题的执行轨迹已经完全不同。
普通问题
什么是向量数据库?
<span>START</span>
↓
route_question
↓
simple
↓
directAnswer
↓
<span>END</span>
根本不会访问 Milvus。
小说知识问题
阿朱的结局是什么?
<span>START</span>
↓
route_question
↓
complex
↓
retrieve
↓
Milvus
↓
rag_generate
↓
<span>END</span>
只有真正需要小说知识的时候才进行检索。
七、这已经算 Agentic RAG 了吗?
严格来说,它还非常初级。
但它已经出现了一个关键变化。
Naive RAG:
程序员决定流程
<span>retrieve</span>();
<span>generate</span>();
现在:
LLM 的判断
↓
影响 Graph 的执行路径
也就是说:
LLM 开始参与系统决策,而不只是生成文本。
这正是 Agentic 系统非常核心的思想。
所以我更愿意把现在这个版本称为:
Agentic RAG 的第一步
而不是已经完成了一个多么智能的 Agent。
因为现在还有一个非常明显的问题没有解决。
八、检索出来的内容就一定靠谱吗?
现在我们的复杂问题会走:
question
↓
retrieve
↓
documents
↓
generate
但这里偷偷做了一个非常危险的假设:
向量数据库返回的 Top K 文档都是有用的。
实际上并不是。
向量搜索只能告诉我们:
这些文本在向量空间里比较相似。
它并不能保证:
这些内容真的能够回答用户的问题。
例如用户问:
阿朱为什么替父亲赴死?
向量库可能搜到:
阿朱和乔峰相处的片段
语义确实相关。
但未必包含:
为什么赴死
如果直接把这些内容交给模型生成答案,RAG 仍然可能产生幻觉。
所以现在的 Graph:
route
↓
retrieve
↓
generate
还缺一个非常关键的节点:
grade_documents
下一步,Graph 会继续进化成:
<span> ┌───────────────┐
│ ↓
retrieve → grade_documents → generate
│
└→ 不相关
</span>
但“不相关”之后怎么办?
重新搜索?
修改问题?
扩大 Top K?
还是换一种检索策略?
到了这里,RAG 才真正开始从一条固定流水线,变成一个:
会判断执行结果,并根据结果调整下一步动作的系统。
这也是下一篇要解决的问题。
觅优短评:价值在于用 LangGraph 把“是否检索”变成模型可决策的分支,适合想从 Demo 迈向 Agentic RAG 的开发者。检索质量评估是下一道坎。