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 用在刀刃上。


参考资料


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

相关推荐
七夜zippoe4 分钟前
为什么 2026 年每个 Java 团队都该懂 AI Agent
java·开发语言·人工智能
举个栗子。6 分钟前
SwarmForge:AI 智能体协同编程框架,让多个 Agent 在隔离工作区并行协作
人工智能·开源·ai编程
AIGC小尼17 分钟前
Windows 本地 AI 漫剧全自动生产线部署完整教程(零基础、全指令、带源码、模型配置、排错方案)
人工智能·windows·ai漫剧
合米AI SOP系统22 分钟前
传统产线如何快速上马落地 AI 防错?合米科技 AI SOP 7天即可上线。
大数据·人工智能·科技
思录Echo22 分钟前
什么决定具身智能的最终走向?多技术路线与落地现实辨析
大数据·人工智能
ShallWeL40 分钟前
Orin 上多模型常驻与显存预算
人工智能·嵌入式硬件·nvidia·orin
xiaohaiAIgeo42 分钟前
【2026年】AI监控加行为分析守护实验室安全
大数据·人工智能·科普知识
IT_陈寒43 分钟前
Python的多线程就是个假把式,我算是体验到了
前端·人工智能·后端
集和诚JHCTECH1 小时前
案例分享 | BRAV-7721助力印刷电路板(PCB)智能缺陷复判系统
人工智能·嵌入式硬件·边缘计算