RAG 遇上 Agent,不只是"给 LLM 接个搜索框"
很多人第一次接触 RAG,都是这个用法:用户问一个问题 → 检索知识库 → 把结果塞进 Prompt → LLM 生成回答。
这个模式叫 Pipeline RAG 。它有效,但有个根本问题------它不思考。
Pipeline RAG 对每一个问题都执行检索,不管这个问题是问"WonderBot 订阅多少钱"(确实需要查知识库),还是问"Python 列表怎么求平均值"(LLM 自己就知道)。它像一个只会一招的工人:不管手头的活是什么,都先去仓库跑一趟。
Agentic RAG 解决的是这个问题:让 Agent 自己判断何时检索、检索什么、检索完是否够用。
本篇聚焦三个核心能力:
- 检索决策:这个问题需要查知识库吗?
- 多知识库路由:需要查,但查哪一个?
- 质量门控 + Fallback:查完了,够用吗?不够怎么办?
Pipeline RAG vs Agentic RAG:架构层面的本质区别
先看两种架构的对比:
markdown
Pipeline RAG(每次必检索):
用户问题
↓
向量检索(无论问题类型)
↓
结果注入 Prompt
↓
LLM 生成
Agentic RAG(智能决策):
用户问题
↓
[决策节点] 这个问题需要检索吗?
├─ 不需要 → LLM 直接回答(常识/数学/通用编程)
└─ 需要 → 查哪个知识库?
├─ product_kb(产品功能/价格)
├─ ops_kb(部署/运维/监控)
└─ faq_kb(账号/退款/发票)
↓
检索质量够用吗?
├─ 够 → LLM 生成
└─ 不够 → 重写查询 → 重试(最多 2 次)→ LLM 生成
关键差别:LLM 是控制中心,不是下游的文本生成器。
Demo 1: Pipeline RAG vs Agentic RAG 核心差异
用 5 个问题对比两种模式:3 个需要查知识库,2 个不需要(通用常识、数学计算)。
Pipeline RAG 实现
python
def pipeline_rag(question: str) -> dict:
"""Pipeline RAG:检索→注入→生成,永远不跳过检索步骤"""
docs = unified_retriever.invoke(question)
context = "\n".join(d.page_content for d in docs)
answer = _ask(
f"根据以下参考资料回答问题,若资料无关请基于资料内容作答。\n参考:{context}",
question,
)
return {"answer": answer, "retrieved": True, "docs": len(docs)}
注意那个 若资料无关请基于资料内容作答------当知识库内容和问题完全不相关时,这个 Prompt 会让 LLM 产生奇怪的行为:要么强行把不相关内容和答案混在一起,要么输出"根据参考资料,无法回答您的问题"。
Agentic RAG 实现
python
def agentic_rag(question: str) -> dict:
"""Agentic RAG:先决策,再(选择性)检索"""
# Step 1:Agent 决定是否需要检索
decision = _ask(
"判断以下问题是否需要查询知识库才能回答。\n"
"需要检索的场景:产品定价/功能、运维操作、用户服务政策\n"
"不需要检索的场景:常识问题、数学计算、通用编程知识\n"
"只输出 yes 或 no",
f"问题:{question}",
).strip().lower()
if "yes" not in decision:
answer = _ask("你是一个知识丰富的助手,请直接回答问题。", question)
return {"answer": answer, "retrieved": False, "docs": 0}
else:
docs = unified_retriever.invoke(question)
context = "\n".join(d.page_content for d in docs)
answer = _ask(f"根据以下参考资料回答问题。\n参考:{context}", question)
return {"answer": answer, "retrieved": True, "docs": len(docs)}
实测对比结果
针对 5 个问题的实际运行:
scss
问题类型 | Pipeline 检索 | Agentic 检索 | 问题
─────────────────────────────────────────────────────────────────
产品功能 | ✓ (3条) | ✓ (3条) | WonderBot Pro 基础版每月能调用多少次 API?
运维操作 | ✓ (3条) | ✓ (3条) | 部署 WonderBot 服务最低需要多少内存?
用户服务 | ✓ (3条) | ✓ (3条) | 购买 30 天后还能退款吗?
通用常识 | ✓ (3条) | ✗ 跳过 | Python 中如何计算列表的平均值?
数学计算 | ✓ (3条) | ✗ 跳过 | 1024 除以 32 等于多少?
Pipeline RAG 对全部 5 个问题都执行了检索(包括"1024 除以 32 等于多少"这种拿到知识库内容也毫无帮助的问题)。Agentic RAG 正确识别了通用常识和数学题,跳过了检索。
不是所有问题都值得"跑一趟仓库"。
Demo 2: 多知识库路由
真实的企业场景通常有多个知识库:产品文档、运维手册、用户 FAQ......不同问题需要查不同的知识库。
三个知识库
python
PRODUCT_DOCS = [
Document(page_content="WonderBot Pro 订阅价格:基础版 ¥99/月,专业版 ¥299/月,企业版按需报价。"),
Document(page_content="API 调用限额:基础版 10K次/月,专业版 100K次/月,超出按 ¥0.01/次计费。"),
Document(page_content="WonderBot Pro 支持 GPT-4、Claude 3、Gemini Pro、GLM-4,可在控制台自由切换。"),
Document(page_content="数据安全:对话数据存储在中国区服务器,符合等保三级认证,支持数据加密导出。"),
]
OPS_DOCS = [
Document(page_content="部署要求:Docker 20+,内存 ≥ 8GB,CPU ≥ 4核,推荐 docker-compose up --build。"),
Document(page_content="故障排查:服务无响应→检查 docker ps;API 超时→检查 LLM 连通性;内存溢出→调高内存 limit。"),
Document(page_content="备份策略:每日凌晨 2 点自动备份,保留 30 天,用 restore.sh 脚本恢复。"),
Document(page_content="监控告警:CPU > 80% 持续 5 分钟告警;内存 > 90% 告警;API 错误率 > 5% 告警。"),
]
FAQ_DOCS = [
Document(page_content="重置密码:登录页点击'忘记密码'→输入注册邮箱→查收重置邮件→设置新密码。"),
Document(page_content="退款政策:7 天内全额退款,7-30 天按比例,30 天后不退。"),
Document(page_content="申请发票:在'账单中心'点击'申请发票',3-5 工作日开出电子发票并发送邮箱。"),
Document(page_content="API Key 管理:在'开发者设置'中创建/撤销,每账号最多 5 个。"),
]
LangGraph 路由实现
python
class RoutingState(TypedDict):
question: str
kb_choice: str # "product" | "ops" | "faq"
context: str
answer: str
path: list
def route_node(state: RoutingState) -> RoutingState:
"""Step 1:LLM 判断应查哪个知识库"""
decision = _ask(
"根据问题内容,判断应该查询哪个知识库,只输出知识库名称:\n"
"product - 涉及产品功能、价格、技术规格、支持的模型\n"
"ops - 涉及部署、运维、故障排查、监控告警、备份恢复\n"
"faq - 涉及账号密码、退款、发票、API Key 等用户服务",
f"问题:{state['question']}",
).strip().lower()
...
图结构非常简单:
route → retrieve → generate
route_node 的输出决定 retrieve_node 使用哪个 retriever。
实测路由准确率
针对 6 个问题(每个知识库 2 个),真实运行结果:
perl
预期KB | 实际路由 | 匹配 | 问题
─────────────────────────────────────────────────────────────
应查 product | product | ✓ | 专业版订阅每月多少钱?支持哪些大模型?
应查 product | ops | ✗ | 数据存储在哪里,符合什么安全认证?
应查 ops | ops | ✓ | 服务 API 超时了怎么排查?
应查 ops | ops | ✓ | 监控到 CPU 超过 80% 会触发什么告警?
应查 faq | faq | ✓ | 我买了 15 天,还能退款多少?
应查 faq | ops | ✗ | 怎么给公司开增值税发票?
路由准确率:4/6 = 67%
两个错误值得关注:
- "数据存储在哪里" → 路由到 ops(应该是 product):LLM 认为"数据存储"更偏向运维范畴,这种歧义在单句路由判断中容易出错
- "怎么给公司开增值税发票" → 路由到 ops(应该是 faq):发票里有"公司",LLM 把它跟企业运维关联了
这 67% 的准确率说明了一个重要问题:用 LLM 做路由判断是可行的,但对歧义问题需要额外增强。生产中常见的改进手段:
python
# 改进方案:在路由 Prompt 中加入更多示例
route_prompt = """
判断应该查询哪个知识库:
product:产品价格/功能/模型支持/数据安全认证
ops:服务部署/故障排查/监控/备份恢复
faq:账号密码/退款/发票/API Key/用户账单
示例:
"支持哪些大模型" → product
"开发票" → faq ← 发票类问题属于用户服务
"数据存储安全" → product ← 数据安全是产品特性
问题:{question}
"""
完整示例的实际回答
问题:"API 超时了怎么排查?" → 路由到 ops,检索后生成:
路由到:ops_kb
回答:API 超时了,可以按照以下步骤排查:
1. 检查 LLM(语言学习模型)的连通性,确保网络连接正常。
2. 查看 Docker 的运行状态,使用 docker ps 命令检查服务是否正常。
3. 如果是内存溢出导致的,可以尝试调高 Docker 的内存限制。
知识库命中正确,回答直接引用了 ops 文档中的排查步骤。
Demo 3: 质量门控 + 查询重写 Fallback
当检索结果质量不足时,与其直接用低质量内容生成,不如先修改问题再重试。
核心思路
retrieve → evaluate_quality
├─ 质量 ≥ 0.6 → generate
└─ 质量 < 0.6 且重试次数 < 2 → rewrite_query → retrieve(重新循环)
LangGraph 实现
python
QUALITY_THRESHOLD = 0.6
MAX_RETRIES = 2
class QualityGateState(TypedDict):
question: str
rewritten_q: str # 重写后的查询(初始等于原始问题)
context: str
quality_score: float
answer: str
attempts: int
path: list
def qg_evaluate_node(state: QualityGateState) -> QualityGateState:
"""让 LLM 评估检索内容与问题的相关度"""
score = _score_quality(state["question"], state["context"])
return {**state, "quality_score": score, ...}
def qg_rewrite_node(state: QualityGateState) -> QualityGateState:
"""把模糊问题改写为更具体的检索查询"""
rewritten = _ask(
"将以下模糊问题改写为更具体的检索查询,保留原意但增加关键词,只输出改写后的问题:",
state["question"],
).strip()
return {**state, "rewritten_q": rewritten, "attempts": state["attempts"] + 1}
def should_rewrite(state: QualityGateState) -> str:
if state["quality_score"] >= QUALITY_THRESHOLD:
return "generate" # 质量够了,直接生成
if state["attempts"] >= MAX_RETRIES:
return "generate" # 重试次数到上限,兜底生成
return "rewrite" # 质量不够,重写查询
实测结果
用三个极度模糊的问题测试:
scss
原始问题 | 重试次数 | 最终质量 | 执行路径
─────────────────────────────────────────────────────────────────────
价钱怎么样 | 2 | 0.00 | retrieve → evaluate(0.50) → rewrite → retrieve → evaluate(0.00) → rewrite → retrieve → evaluate(0.00) → generate
出问题了怎么办 | 2 | 0.50 | retrieve → evaluate(0.50) → rewrite → retrieve → evaluate(0.50) → rewrite → retrieve → evaluate(0.50) → generate
钱的事 | 2 | 0.50 | retrieve → evaluate(0.50) → rewrite → retrieve → evaluate(0.50) → rewrite → retrieve → evaluate(0.50) → generate
详细追踪"价钱怎么样"这个问题:
arduino
原始问题: "价钱怎么样"
↓ retrieve → 检索到备份/部署/退款相关内容(无关)
↓ evaluate → 质量分 0.50(LLM 认为略微相关)
↓ rewrite → "商品价格范围查询"(重写后反而更泛化了)
↓ retrieve → 检索结果质量下降
↓ evaluate → 质量分 0.00
↓ rewrite → "商品价格范围查询"(重写无进展)
↓ generate → 生成兜底回答
最终回答:根据您提供的参考资料,关于价格的信息并不包含在内。
如果您需要了解价格信息,建议直接联系服务提供商或访问官方网站。
这个结果很有教学价值:查询重写不是万能的。"价钱怎么样"这类极度模糊的问题,LLM 重写后给出"商品价格范围查询",反而丢失了特定产品的上下文,质量没有提升。
更好的处理方式是在质量评估之前加一个意图澄清环节:
python
# 改进方案:质量持续低时,要求用户补充上下文
if state["attempts"] >= MAX_RETRIES and state["quality_score"] < 0.3:
return "clarify" # 新增节点:向用户追问"您想了解哪个产品/服务的价格?"
这就是 Agentic RAG 的真实挑战------检索质量低不一定是检索策略的问题,有时是问题本身信息不足。
Agentic RAG 设计清单
设计一套 Agentic RAG 系统需要考虑的核心决策点:
检索决策层
- 明确什么类型的问题需要检索(业务知识 vs 通用知识)
- 给 LLM 的判断 Prompt 中提供具体的边界示例,减少歧义
- 设置
skip_retrieval类型:纯数学/代码语法/常识问题直接走 LLM
知识库路由层
- 为每个知识库写清晰的描述(类型 + 典型问题 + 边界案例)
- 路由准确率低于 80% 时,考虑加 Few-shot 示例或使用专用分类模型
- 支持跨知识库检索(当问题涉及多个领域时,合并结果)
质量门控层
- 设置合理的阈值(0.6 是合理起点,可根据业务调整)
- 限制最大重试次数(建议 2 次,避免无限循环)
- 记录每次重写的查询和质量分(用于后续数据优化)
- 质量持续低时触发澄清(向用户追问),而不是硬生成
生产优化
- 在 LangGraph 的路由 Prompt 中加入领域示例
- 考虑用专门的 embedding 模型 + BM25 混合检索提升基础质量
- 记录哪些问题被跳过检索、哪些触发了重写,用于评估和迭代
本篇小结
几个核心结论:
- Pipeline RAG 的问题不是检索,是不思考:对所有问题一刀切地执行检索,既浪费资源,又可能让不相关内容干扰回答
- Agentic RAG 的本质是 LLM 做调度:检索、路由、评估都是 LLM 的决策,不是固定流程
- 多知识库路由的真实准确率不完美:67% 的路由准确率说明用一句 Prompt 做路由有限制,生产中需要 Few-shot 或专用分类模型加持
- 质量门控 + 重写不是银弹:极度模糊的问题,重写有时会让查询更泛化,根本解法是追问用户
- LangGraph 的图结构让 Agentic RAG 易于扩展:加新知识库只需加一个节点,改路由 Prompt,不需要改整体架构
下一篇:上下文工程------Token 预算管理、动态上下文组装、以及如何在 128K 上下文窗口里把每一个 Token 用在刀刃上。
参考资料
- LangGraph Agentic RAG 示例
- LangChain RAG Conceptual Guide
- CRAG 论文(Corrective RAG)
- 本系列完整代码:agent-06-agentic-rag
欢迎来我的个人主页找到更多有用的知识和有趣的产品