Agent系列(七):知识库集成——Agent 调用 RAG 的正确姿势

RAG 遇上 Agent,不只是"给 LLM 接个搜索框"

很多人第一次接触 RAG,都是这个用法:用户问一个问题 → 检索知识库 → 把结果塞进 Prompt → LLM 生成回答。

这个模式叫 Pipeline RAG 。它有效,但有个根本问题------它不思考

Pipeline RAG 对每一个问题都执行检索,不管这个问题是问"WonderBot 订阅多少钱"(确实需要查知识库),还是问"Python 列表怎么求平均值"(LLM 自己就知道)。它像一个只会一招的工人:不管手头的活是什么,都先去仓库跑一趟。

Agentic RAG 解决的是这个问题:让 Agent 自己判断何时检索、检索什么、检索完是否够用

本篇聚焦三个核心能力:

  1. 检索决策:这个问题需要查知识库吗?
  2. 多知识库路由:需要查,但查哪一个?
  3. 质量门控 + 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 混合检索提升基础质量
  • 记录哪些问题被跳过检索、哪些触发了重写,用于评估和迭代

本篇小结

几个核心结论:

  1. Pipeline RAG 的问题不是检索,是不思考:对所有问题一刀切地执行检索,既浪费资源,又可能让不相关内容干扰回答
  2. Agentic RAG 的本质是 LLM 做调度:检索、路由、评估都是 LLM 的决策,不是固定流程
  3. 多知识库路由的真实准确率不完美:67% 的路由准确率说明用一句 Prompt 做路由有限制,生产中需要 Few-shot 或专用分类模型加持
  4. 质量门控 + 重写不是银弹:极度模糊的问题,重写有时会让查询更泛化,根本解法是追问用户
  5. LangGraph 的图结构让 Agentic RAG 易于扩展:加新知识库只需加一个节点,改路由 Prompt,不需要改整体架构

下一篇:上下文工程------Token 预算管理、动态上下文组装、以及如何在 128K 上下文窗口里把每一个 Token 用在刀刃上。


参考资料


欢迎来我的个人主页找到更多有用的知识和有趣的产品

相关推荐
ITyunwei09871 小时前
主流 SaaS 工单系统对比
运维·服务器·人工智能
青风971 小时前
SDDGR:基于稳定扩散的深度生成重放,用于类增量对象检测(CVPR 2024)
网络·人工智能·深度学习·神经网络·计算机视觉
冬奇Lab2 小时前
一天一个开源项目(第114篇):stop-slop - 一个教 AI 消除自身写作口癖的 Skill 文件
人工智能
天青色等烟雨..2 小时前
R+VIC模型融合实践技术应用及未来气候变化模型预测
大数据·人工智能·arcgis·语言模型·数据分析
云栖梦泽在2 小时前
AI安全实战:AI系统应急响应的实战演练案例
大数据·人工智能·安全
wanzehongsheng2 小时前
户外追日光伏技术对比:双轴太阳花与三轴智能太阳花场景适配分析
人工智能·能源·光伏·光伏支架·光伏太阳花
程序员小假2 小时前
我们来说说 Agent 记忆压缩通常有哪些方法?
agent
北辰alk2 小时前
AI Agent 记忆系统架构设计:OpenClaw、Claude Code、Hermes Agent 深度对比
人工智能
忆~遂愿2 小时前
《大模型驱动软件测试》| 软件工程3.0时代,大模型驱动测试实战指南
人工智能·深度学习·神经网络·机器学习·自然语言处理·软件工程·知识图谱