Agent 工程体系 · 第 30/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。

Agentic RAG:检索决策、查询分解、重排、迭代和停止

RAG(Retrieval-Augmented Generation,检索增强生成)把“生成答案”拆成两个基本动作:先从外部知识源取得证据,再让模型基于证据生成回答。传统 RAG 通常是固定流水线:

问题向量检索拼接片段生成答案\text{问题} \rightarrow \text{向量检索} \rightarrow \text{拼接片段} \rightarrow \text{生成答案}

这种流程适合“一个问题对应一组稳定文档”的场景,但它默认了几个并不总成立的前提:

  1. 用户问题可以直接作为检索查询;
  2. 一次检索就能找全需要的证据;
  3. 向量相似度足以决定证据顺序;
  4. 返回的文档都可以直接放进上下文;
  5. 模型能够自行判断证据是否足够;
  6. 检索应该对所有问题一视同仁。

Agentic RAG 的核心变化,是让 Agent 参与检索过程中的决策。它不再把检索当作固定工具调用,而是把检索视为一个具有状态、分支、反馈和停止条件的决策过程:

问题判断是否检索规划查询检索重排与验证判断是否继续回答或再次检索\text{问题} \rightarrow \text{判断是否检索} \rightarrow \text{规划查询} \rightarrow \text{检索} \rightarrow \text{重排与验证} \rightarrow \text{判断是否继续} \rightarrow \text{回答或再次检索}

这里的 Agent 不等于“让大模型自由发挥”。一个可控的 Agentic RAG 必须明确:

  • 什么情况下需要检索;
  • 检索什么内容;
  • 一个复杂问题如何拆分;
  • 多个查询如何并发执行;
  • 检索结果如何重排;
  • 什么证据已经足够;
  • 什么情况必须继续检索;
  • 什么时候应该停止并承认信息不足。

一、先区分三个对象:问题、查询和证据

1. 用户问题不等于检索查询

用户问题是面向人的表达,通常包含背景、目标、约束和隐含意图。例如:

我们计划把订单服务从单体应用拆成微服务,数据库应该怎么迁移,如何保证迁移期间不丢数据?

这是一个完整的工程问题,但它不是一个理想的检索查询。它至少包含以下子问题:

  • 单体拆分时数据库迁移有哪些模式;
  • 如何进行双写或变更数据捕获;
  • 如何校验新旧库的一致性;
  • 如何处理回滚;
  • 哪些方案适合订单这种强一致性业务。

查询是为了从某个知识源中找到相关内容而构造的检索表达。它应当尽可能包含:

  • 明确主题;
  • 关键实体;
  • 约束条件;
  • 所需证据类型;
  • 时间范围或版本范围;
  • 来源范围。

例如,可以把原问题改写成:

订单服务 单体拆分 数据库迁移 双写 一致性校验 回滚

或者拆成多个更窄的查询:

单体拆分到微服务 数据库迁移模式
订单系统 双写 数据一致性校验
微服务数据库迁移 失败回滚策略

2. 查询不等于证据

查询只是寻找证据的请求,检索结果通常是候选文档或候选片段。证据则必须满足更严格的条件:

  • 能够支持某个具体主张;
  • 来源身份可识别;
  • 内容边界明确;
  • 与问题中的实体和约束一致;
  • 没有被截断到失去语义;
  • 在需要时具备时间和版本信息。

因此,一条高相似度的文本不一定是证据。例如,查询“数据库迁移 双写一致性”可能找到一篇介绍缓存双写的文章。它在词面上相关,但并不能直接支持订单数据库迁移的结论。

可以把一个证据片段表示为:

e=(doc_id,span,text,metadata,provenance)e = (doc\_id, span, text, metadata, provenance)

其中:

  • doc_iddoc\_id:文档标识;
  • spanspan:文档中的位置;
  • texttext:片段文本;
  • metadatametadata:标题、作者、版本、时间、权限等元数据;
  • provenanceprovenance:来源映射,例如 URL、文件路径、数据库记录或网页抓取任务。

Agentic RAG 的后续判断,应该围绕“证据是否支持所需主张”进行,而不是只围绕“搜索结果是否相似”进行。


二、Agentic RAG 的基本决策模型

设用户问题为 qq,可用知识源为 SS,当前已经收集的证据集合为 EE,剩余预算为 BB。Agent 在每一步选择一个动作:

at{answer,retrieve,decompose,ask_clarification,abstain}a_t \in \{ \text{answer}, \text{retrieve}, \text{decompose}, \text{ask\_clarification}, \text{abstain} \}

对应含义如下:

  • answer:基于当前证据生成回答;
  • retrieve:执行一次或一组检索;
  • decompose:把问题拆成多个子问题;
  • ask_clarification:问题存在无法安全猜测的歧义;
  • abstain:证据不足或来源冲突无法解决,明确说明限制。

Agent 的目标不是让检索次数最大化,而是在准确性、覆盖率、延迟和成本之间取得平衡。可以用一个简化目标表示:

J=U(E,q)λcCλlLλrRJ = U(E, q) - \lambda_c C - \lambda_l L - \lambda_r R

其中:

  • U(E,q)U(E, q):证据对问题的支持效用;
  • CC:模型调用、检索和抓取成本;
  • LL:延迟;
  • RR:错误、冲突或不确定性风险;
  • λc,λl,λr\lambda_c,\lambda_l,\lambda_r:业务对成本、延迟和风险的权重。

一次额外检索是否值得,取决于它的期望收益:

ΔU=U(EEnew,q)U(E,q)\Delta U = U(E \cup E_{\text{new}}, q) - U(E, q)

当预期新增收益低于检索成本和风险时,就不应继续。

这个公式不是要求在线计算精确数值,而是说明一个重要原则:“继续搜索”必须是有条件的动作,而不是失败后的默认重试。


三、检索决策:什么时候检索,检索哪一个源

1. 是否需要检索

Agent 首先需要判断问题是否依赖外部信息。以下问题通常应检索:

  • 涉及企业内部知识;
  • 涉及当前版本、当前价格、当前政策或当前状态;
  • 用户要求引用来源;
  • 问题要求精确数字、条款或配置;
  • 模型自身知识不足以可靠回答;
  • 不同来源可能存在冲突。

以下问题可能不需要检索:

  • 纯粹的代码语法解释;
  • 用户已经提供了完整材料,只要求总结或改写;
  • 不依赖外部事实的头脑风暴;
  • 简单的数学推导。

但“是否检索”不能只由问题类型决定,还要考虑风险。一个“看起来简单”的问题,如果错误代价很高,也应优先检索。例如:

这个生产数据库参数能否直接修改?

即使问题只有一句话,也可能需要根据数据库版本、部署方式和官方文档进行确认。

2. 检索决策的输入

一个较完整的检索决策状态可以包含:

from dataclasses import dataclass, field
from typing import Literal

@dataclass
class RetrievalState:
    user_question: str
    intent: str | None = None
    freshness_required: bool = False
    citation_required: bool = False
    risk_level: Literal["low", "medium", "high"] = "medium"
    candidate_sources: list[str] = field(default_factory=list)
    subquestions: list[str] = field(default_factory=list)
    evidence: list[dict] = field(default_factory=list)
    unresolved_claims: list[str] = field(default_factory=list)
    conflicts: list[dict] = field(default_factory=list)
    iteration: int = 0
    budget_remaining: int = 5
    stop_reason: str | None = None

这里的 evidence 不应只保存字符串。至少要保存文档 ID、片段位置、来源、检索查询和评分,以便后续重排、引用和诊断。

3. 检索源选择

不同知识源解决的问题不同:

知识源 适合解决的问题 主要风险
结构化数据库 事实、状态、统计数据 字段语义错误、权限泄漏
全文搜索 精确术语、错误信息、条款 词面匹配导致噪声
向量索引 语义相近、自然语言问法 相似但不支持结论
官方文档 API、规范、版本行为 版本不匹配
网页搜索 时效信息、公开资料 来源质量不一致
代码仓库 实际实现、配置和调用方式 分支、版本和上下文不明
工单和日志 真实故障与历史经验 隐私、过时方案和样本偏差

“选择来源”不是简单地把所有源一起搜索。不同源的检索语义、权限模型、时效性和返回结构不同。更合理的做法是先识别所需证据类型,再选择源。

例如:

问题:某 API 的参数在 2026 年版本中是否仍然支持?
证据类型:版本化规范 + 官方 API 文档
首选来源:官方文档和版本变更记录
不应首选:论坛帖子、未经确认的代码示例

四、查询分解:把复杂问题变成可验证的子问题

1. 查询分解的目的

查询分解(query decomposition)是把一个复杂问题转换成多个较小、可独立检索和验证的子问题。

它不是机械地按逗号切分句子,而是识别问题中的不同关系:

  • 实体:讨论谁或什么;
  • 属性:要查什么属性;
  • 时间:哪个时间点或版本;
  • 条件:在什么约束下;
  • 比较:需要比较哪些对象;
  • 因果:要解释原因还是只要事实;
  • 决策:需要选择方案还是描述现状。

例如:

比较 PostgreSQL logical replication 和双写迁移,哪一种更适合订单服务从单体拆分,要求支持灰度发布和失败回滚。

可以分解为:

  1. logical replication 的数据同步语义是什么;
  2. 双写迁移的一致性风险是什么;
  3. 两者对灰度发布的支持方式是什么;
  4. 两者的失败回滚路径是什么;
  5. 订单服务对一致性和顺序性的要求是什么;
  6. 在这些约束下,比较结论是什么。

最后一个问题“哪一种更适合”不能直接检索得到,它需要建立在前五个事实之上。

2. 分解的必要条件

一个合格的子问题应满足三个条件:

可检索

子问题应能转化为一个具体查询。例如:

PostgreSQL logical replication conflict handling rollback

而不是:

它有什么问题?

可验证

子问题的答案应能被一个或多个证据片段支持。例如:

logical replication 是否保证跨表事务原子性?

这比“哪个方案更好”更容易验证。

可组合

多个子问题的结论应能重新组合为原问题的回答。如果分解后的问题彼此无关,或者遗漏了关键约束,组合时仍然无法得出结论。

3. 分解的依赖关系

子问题并不总是独立的。可以用有向无环图表示依赖:

graph TD
    Q[原始问题] --> A[识别业务约束]
    Q --> B[查询方案一]
    Q --> C[查询方案二]
    A --> D[定义比较维度]
    B --> D
    C --> D
    D --> E[生成比较结论]
    E --> F[证据覆盖检查]
    F --> G[回答或继续检索]

例如,如果不知道业务是否要求跨表事务一致性,就无法正确判断两种迁移方案。此时“查询方案优缺点”可能不是第一步,应该先确认业务约束。

4. 分解的反例

下面这种分解看似详细,实际上不可用:

1. 什么是数据库?
2. 什么是微服务?
3. 什么是迁移?
4. 什么是双写?
5. 什么是回滚?

它的问题是:

  • 过度拆成定义题;
  • 没有围绕用户决策;
  • 没有表达对象之间的关系;
  • 检索结果无法直接组合成方案选择。

更好的分解应保留原问题的约束:

1. 订单服务拆分时,哪些数据必须保持强一致?
2. 双写方案如何处理部分成功?
3. logical replication 如何处理增量同步和冲突?
4. 灰度期间读流量如何切换?
5. 失败时能否回退到旧服务和旧数据库?

五、查询生成:从子问题构造多个检索表达

同一个子问题通常需要多个查询表达,因为单一查询可能受到术语差异、语言差异和文档组织方式的影响。

例如,子问题是:

如何验证新旧数据库之间的数据一致性?

可生成:

数据库迁移 新旧库 一致性校验 行数 checksum
online migration source target consistency validation
dual write migration reconciliation strategy

多查询的价值不是简单扩大召回数量,而是覆盖不同表达空间:

  • 用户术语;
  • 官方术语;
  • 实现术语;
  • 错误术语;
  • 约束术语。

查询生成可以分为两种模式。

1. 改写

改写保留原问题语义,只改变表达方式:

原问题:如何验证数据库迁移后数据一致?
改写一:数据库迁移 数据一致性校验方法
改写二:source target database migration reconciliation

适合用户问题本身较完整,但术语不规范的情况。

2. 视角扩展

视角扩展从不同角度寻找证据:

方案视角:database migration dual write patterns
风险视角:dual write inconsistency failure modes
验证视角:database migration reconciliation checks
回滚视角:database migration rollback after cutover

适合需要同时覆盖机制、限制、故障和恢复的问题。

查询生成不能无限扩张。查询数量增加后,会带来:

  • 检索成本增加;
  • 重复文档增多;
  • 不同查询产生相互矛盾的片段;
  • Agent 需要花更多时间判断哪些结果真正有用。

因此可以给每个子问题分配查询预算:

ni=min(nmax,αriski+βambiguityi)n_i = \min(n_{\max}, \alpha \cdot risk_i + \beta \cdot ambiguity_i)

其中 riskirisk_i 是错误风险,ambiguityiambiguity_i 是术语或意图歧义。高风险、高歧义问题可以生成更多查询,但低风险问题不必使用同样的数量。


六、检索执行:并发、权限和故障路径

查询分解后,可以并发执行相互独立的子查询:

import asyncio
from dataclasses import dataclass

@dataclass
class SearchHit:
    query: str
    doc_id: str
    title: str
    text: str
    score: float
    source: str

async def search_one(query: str) -> list[SearchHit]:
    # 这里替换为实际的全文、向量或网页搜索客户端
    await asyncio.sleep(0.01)
    return []

async def retrieve_all(queries: list[str]) -> list[SearchHit]:
    results = await asyncio.gather(
        *(search_one(query) for query in queries),
        return_exceptions=True,
    )

    hits: list[SearchHit] = []
    for query, result in zip(queries, results):
        if isinstance(result, Exception):
            # 记录单个查询失败,但不让一个源阻断全部任务
            print(f"search failed: {query}: {result}")
            continue
        hits.extend(result)
    return hits

这段代码表达的是一种故障隔离原则:一个子查询失败,不应自动丢弃所有已经获得的证据。

但并发并不意味着可以无条件并发。以下任务通常需要串行:

  • 后一个查询依赖前一个查询发现的实体;
  • 第一次检索确定了版本,第二次检索需要使用该版本;
  • 第一次结果发现来源冲突,第二次检索针对冲突进行核查;
  • 查询包含权限敏感条件,需要根据授权结果调整范围。

检索层还必须处理四类故障:

  1. 超时:返回部分结果,并把超时记录为检索事件;
  2. 限流:根据来源分别退避,不能让所有来源同时重试;
  3. 权限拒绝:不能通过改写查询绕过权限;
  4. 结果为空:区分“没有匹配结果”和“检索系统失败”。

如果所有故障最后都表现为“空结果”,Agent 会错误地把基础设施故障判断成知识不存在。


七、重排:为什么第一次检索顺序不够可靠

1. 召回和排序解决不同问题

检索通常分为两个阶段:

候选召回候选重排\text{候选召回} \rightarrow \text{候选重排}

召回阶段追求“不漏掉可能相关的文档”,允许噪声较多;重排阶段追求“把最能支持当前问题的证据放在前面”。

向量相似度通常只衡量语义接近程度:

sdense(q,d)=cos(embedding(q),embedding(d))s_{\text{dense}}(q,d) = \cos(\text{embedding}(q), \text{embedding}(d))

它无法充分表达:

  • 文档是否来自可信来源;
  • 文档是否匹配目标版本;
  • 片段是否真的回答了问题;
  • 片段是否包含必要条件;
  • 片段是否与其他证据重复;
  • 片段是否与当前用户权限匹配。

因此,重排评分可以组合多个维度:

S(dq)=w1ssemantic+w2slexical+w3sauthority+w4sfreshness+w5scoveragew6sduplicationw7sconflictS(d \mid q) = w_1s_{\text{semantic}} +w_2s_{\text{lexical}} +w_3s_{\text{authority}} +w_4s_{\text{freshness}} +w_5s_{\text{coverage}} -w_6s_{\text{duplication}} -w_7s_{\text{conflict}}

其中:

  • ssemantics_{\text{semantic}}:语义相关性;
  • slexicals_{\text{lexical}}:关键词、实体和术语匹配;
  • sauthoritys_{\text{authority}}:来源可信度;
  • sfreshnesss_{\text{freshness}}:时间或版本新鲜度;
  • scoverages_{\text{coverage}}:对未解决主张的覆盖程度;
  • sduplications_{\text{duplication}}:与已选证据的重复程度;
  • sconflicts_{\text{conflict}}:与高可信证据冲突的程度。

2. 一个可执行的简化重排示例

下面使用简单的词项重叠模拟重排逻辑。它不是生产级排序器,但可以清楚展示每一步如何发生。

import re
from collections import Counter

STOPWORDS = {"如何", "什么", "是否", "以及", "一个", "这个", "方案"}

def tokenize(text: str) -> list[str]:
    # 示例实现:实际系统应使用适合中文和英文混合文本的分词器
    terms = re.findall(r"[A-Za-z0-9_]+|[\u4e00-\u9fff]", text.lower())
    return [x for x in terms if x not in STOPWORDS]

def lexical_score(query: str, text: str) -> float:
    q = Counter(tokenize(query))
    d = Counter(tokenize(text))
    common = sum((q & d).values())
    return common / max(sum(q.values()), 1)

def rerank(query: str, hits: list[SearchHit]) -> list[SearchHit]:
    scored = []
    for hit in hits:
        lexical = lexical_score(query, hit.text)

        # 来源权重只是示例,真实系统应由治理规则配置
        authority = {
            "official": 1.0,
            "internal": 0.95,
            "academic": 0.9,
            "community": 0.6,
        }.get(hit.source, 0.5)

        final_score = 0.6 * hit.score + 0.25 * lexical + 0.15 * authority
        scored.append((final_score, hit))

    scored.sort(key=lambda x: x[0], reverse=True)
    return [hit for _, hit in scored]

输入是候选片段及其初始检索分数,输出是重新排序后的片段。这里的权重不具有通用正确性,重点在于:重排应当使用当前问题所需的证据属性,而不是盲目追求向量分数。

3. 重排与多样性

只按相关性排序,可能返回同一文档的十个相似片段。它们看起来都很相关,却没有增加事实覆盖率。

可以引入最大边际相关性(MMR):

MMR(d)=λRel(d,q)(1λ)maxdEselectedSim(d,d)\operatorname{MMR}(d) = \lambda \cdot \operatorname{Rel}(d,q) - (1-\lambda)\cdot \max_{d'\in E_{\text{selected}}} \operatorname{Sim}(d,d')

第一项鼓励相关,第二项惩罚与已选片段重复。

例如,回答“某配置项的默认值、适用版本和限制”时,理想证据可能分别来自:

  • 官方参数说明;
  • 版本变更记录;
  • 限制或异常处理章节。

三个片段的主题不同,但共同覆盖一个问题。只选择最高相似度片段,可能只得到三段参数定义,遗漏版本和限制。


八、证据充分性:Agent 为什么知道该继续查

Agent 需要把“回答质量”转换成可检查的中间结构。最实用的方式是维护主张集合。

设问题需要回答的主张为:

C={c1,c2,,cn}C = \{c_1,c_2,\ldots,c_n\}

每个证据片段 eje_j 能支持其中一部分主张。定义支持关系:

support(ej,ci){0,0.5,1}support(e_j,c_i)\in\{0, 0.5, 1\}

分别表示:

  • 0:不支持;
  • 0.5:部分支持或需要结合其他证据;
  • 1:直接支持。

证据覆盖率可以定义为:

Coverage(E,C)=iimportance(ci)covered(ci)iimportance(ci)Coverage(E,C) = \frac{\sum_i importance(c_i)\cdot covered(c_i)} {\sum_i importance(c_i)}

其中 covered(c_i) 可以取该主张目前的最大支持程度。

例如,问题是:

某 API 参数在 v3 中是否支持流式模式?如果支持,有哪些限制?

主张集合可能是:

c1:v3 支持该参数
c2:该参数可以用于流式模式
c3:流式模式存在限制 A
c4:流式模式存在限制 B

如果证据只支持 c1c2,Agent 不能因为“已经找到官方文档”就直接回答完整限制。它应把 c3c4 保留为未解决主张,继续检索或明确说明未找到限制信息。

反例:相似度阈值不是充分性判断

假设系统设置:

如果最高相似度 > 0.82,则停止检索

这会产生明显错误:

  • 文档可能高度相似,但没有回答问题;
  • 复杂问题只被部分覆盖;
  • 旧版本文档可能比新版本文档更相似;
  • 一个文档中的免责声明可能被截断;
  • 冲突证据仍然存在。

相似度只能作为候选信号,不能单独作为停止依据。


九、迭代检索:把检索结果当作下一步的反馈

迭代检索是指 Agent 根据当前结果调整下一轮查询,而不是重复执行相同查询。

一次迭代通常包含以下状态变化:

stateDiagram-v2
    [*] --> NeedDecision
    NeedDecision --> DirectAnswer: 不需要外部证据
    NeedDecision --> Plan: 需要检索
    Plan --> Retrieve
    Retrieve --> Rank
    Rank --> Assess
    Assess --> Answer: 主张已覆盖且无关键冲突
    Assess --> RefineQuery: 有未解决主张
    Assess --> ResolveConflict: 存在关键冲突
    Assess --> Clarify: 关键约束缺失
    RefineQuery --> Retrieve
    ResolveConflict --> Retrieve
    Clarify --> [*]
    Answer --> [*]

1. 典型迭代策略

查询收窄

第一次查询太宽:

微服务数据库迁移

结果可能包括架构介绍、工具广告和泛化文章。下一轮应增加约束:

订单服务 单体拆分 数据库迁移 双写 一致性 回滚

查询扩展

第一次查询太窄:

PostgreSQL logical replication transaction rollback

如果没有结果,可以扩展术语:

PostgreSQL logical replication failover recovery consistency

实体补全

第一次结果发现产品名或版本号:

某平台 Data Sync v3

下一轮可以使用准确实体:

"Data Sync v3" conflict handling

针对缺口检索

如果当前证据已经解释机制,但没有解释限制,查询应改变目标:

机制查询:logical replication how it works
缺口查询:logical replication limitations cross-table transaction

针对冲突检索

如果两个来源对默认值给出不同结论,不能继续搜索原问题,而应明确冲突:

产品名 v3 default timeout official release notes

必要时还要比较文档发布时间和版本范围。

2. 迭代不是简单重试

以下代码展示了一个带预算和进展检测的控制循环:

def should_stop(state: RetrievalState) -> tuple[bool, str]:
    if not state.unresolved_claims and not state.conflicts:
        return True, "evidence_sufficient"

    if state.iteration >= 4:
        return True, "max_iterations"

    if state.budget_remaining <= 0:
        return True, "budget_exhausted"

    return False, ""

def has_progress(before: RetrievalState, after: RetrievalState) -> bool:
    old = set(before.unresolved_claims)
    new = set(after.unresolved_claims)

    resolved_claims = len(old - new)
    new_evidence = len(after.evidence) - len(before.evidence)

    return resolved_claims > 0 or new_evidence > 0

def agentic_retrieve(state: RetrievalState):
    while True:
        stop, reason = should_stop(state)
        if stop:
            state.stop_reason = reason
            return state

        state.iteration += 1
        before = RetrievalState(
            user_question=state.user_question,
            unresolved_claims=list(state.unresolved_claims),
            evidence=list(state.evidence),
            conflicts=list(state.conflicts),
            iteration=state.iteration,
            budget_remaining=state.budget_remaining,
        )

        queries = build_next_queries(state)
        hits = execute_queries(queries)
        ranked = rerank(state.user_question, hits)

        state.evidence.extend(select_diverse(ranked))
        state.budget_remaining -= len(queries)
        assess_evidence(state)

        if not has_progress(before, state):
            state.stop_reason = "no_progress"
            return state

这个示例省略了实际的 build_next_queriesexecute_queriesassess_evidence,但保留了关键控制关系:

  • 有明确的最大迭代次数;
  • 有明确的查询预算;
  • 每轮都要重新评估证据;
  • 如果连续迭代没有进展,应停止;
  • 停止原因必须可观测。

如果没有 no_progress 检测,Agent 可能在查询改写后不断得到同一批文档,最终耗尽成本。


十、停止:停止不是“模型说够了”

停止条件是 Agentic RAG 最容易被低估的部分。一个可靠的停止决策至少应检查五个维度。

1. 覆盖率

所有高重要性主张都有证据支持,或者已经被标记为无法确认。

主张覆盖:
- 核心结论:已覆盖
- 关键限制:已覆盖
- 版本条件:已覆盖
- 例外情况:未覆盖

此时不能把回答包装成无条件结论,而应降低表达强度。

2. 来源质量

证据来源是否达到任务要求。如果用户要求“官方依据”,社区文章不能作为唯一证据。

3. 冲突状态

冲突分为三种:

  • 表面冲突:其实是版本或条件不同;
  • 可解释冲突:不同来源描述不同层次;
  • 未解决冲突:同一版本、同一条件下结论不同。

只有前两种可以继续回答。第三种应继续检索,或者显式报告冲突。

4. 时效性

涉及“当前”“最新”“截至某日期”的问题,必须把时间作为证据属性,而不是检索后的附加说明。旧文档即使内容正确,也可能不适用于当前版本。

5. 预算和边际收益

当新一轮检索没有带来新的主张覆盖、没有解决冲突,也没有发现更高质量来源时,继续检索通常没有价值。

一个实际的停止策略可以写成:

停止并回答:
- 核心主张覆盖率达到阈值;
- 高风险主张都有合格来源;
- 没有未解决的关键冲突;
- 当前结果相对上一轮有新增信息。

停止但降级回答:
- 仍有低重要性主张未覆盖;
- 预算或时间已耗尽;
- 需要明确列出未确认部分。

停止并拒答或请求澄清:
- 关键实体不明确;
- 用户权限不足;
- 来源冲突无法解决;
- 问题依赖缺失的业务约束。

反例:固定轮数停止

for _ in range(3):
    retrieve()
answer()

固定三轮可能在简单问题上浪费资源,在复杂问题上又不够。迭代次数可以作为安全上限,但不能作为唯一停止依据。


十一、把 Agent 状态和检索状态分开

Agentic RAG 不应把全部信息都塞进一串聊天消息。至少需要区分三类状态。

1. 线程状态

线程状态是当前任务的短期工作记忆,例如:

{
  "question": "...",
  "subquestions": ["...", "..."],
  "queries": ["...", "..."],
  "evidence_ids": ["doc-17#p3", "doc-42#p8"],
  "unresolved_claims": ["..."],
  "iteration": 2,
  "budget_remaining": 3,
  "stop_reason": null
}

它决定 Agent 当前执行到哪一步。

2. 证据状态

证据状态保存可复用的检索产物:

{
  "evidence_id": "doc-17#p3",
  "document_id": "doc-17",
  "source": "official",
  "query": "database migration rollback",
  "text": "...",
  "retrieved_at": "2026-09-01T10:30:00+08:00",
  "document_version": "v3",
  "support": [
    {
      "claim_id": "c4",
      "level": 1.0
    }
  ]
}

证据必须绑定来源和版本,否则后续回答无法建立可靠的引用映射。

3. 跨任务记忆

用户偏好、组织知识和已确认事实可能跨越多个任务,但不能把一次回答中的推测直接写成长期事实。应区分:

  • 已验证事实;
  • 用户明确提供的事实;
  • Agent 推断;
  • 暂时性任务状态;
  • 过期或待复核信息。

LangGraph 文档把 checkpointerstore 区分为两种不同持久化机制:前者保存单个线程的图状态快照,适合对话连续性、人工介入、时间回溯和容错;后者保存跨线程的应用数据,适合用户偏好、事实和共享知识。(docs.langchain.com)


十二、持久化与恢复:检索循环不能只存在于内存

Agentic RAG 经常会遇到长任务、人工审批、网页抓取延迟或外部 API 暂时不可用的情况。此时需要在关键节点保存状态:

计划生成后
查询生成后
每个检索任务完成后
重排完成后
证据评估完成后
准备回答前

持久化的最小恢复单元不应只是“上一次模型输出”,而应包括:

  • 当前状态版本;
  • 已执行的查询;
  • 每个查询的结果;
  • 已选证据;
  • 未解决主张;
  • 冲突记录;
  • 预算消耗;
  • 外部调用的幂等键;
  • 下一步动作。

否则任务恢复后可能重复抓取、重复扣费或重复写入。

LangGraph 的 thread_id 用于定位线程范围内的状态;内存型 saver 适合开发,但进程重启后检查点会丢失,生产环境需要使用持久化 checkpointer。官方文档还特别指出,长期运行时检查点会持续增长,应设置保留或清理策略。(docs.langchain.com)

在 OpenAI 的对话状态模型中,可以手动传递历史,也可以使用 Responses API 的 previous_response_id 连接响应,或使用 Conversations API 保存具有持久标识符的会话对象;会话对象可以保存消息、工具调用和工具输出等项目。(developers.openai.com)

这类对话状态能力解决的是“模型交互上下文如何延续”,并不自动等价于“检索证据如何治理”。检索查询、证据版本、来源权限和主张覆盖仍应由应用层显式管理。


十三、一个完整算例:回答版本化技术问题

问题:

在 2026 年 9 月,某 SDK v4 是否支持批量工具调用?如果支持,单次请求有什么限制?

第一步:识别任务属性

需要外部检索:是
需要当前版本:是
需要官方来源:是
核心实体:某 SDK、v4、批量工具调用
核心主张:
  c1:v4 是否支持该能力
  c2:能力的准确名称和调用方式
  c3:单次请求限制
  c4:限制适用的版本和条件

第二步:选择来源

优先级:

  1. v4 官方 API 文档;
  2. v4 发布说明;
  3. 官方 SDK 类型定义或示例;
  4. 官方错误码和限制说明。

社区文章只能作为发现术语的辅助来源,不能独立支持最终结论。

第三步:生成查询

SDK v4 batch tool calls official documentation
SDK v4 release notes batch tool calling
SDK v4 request limits tool calls

第四步:第一次检索

假设结果为:

doc-a:v4 API reference,说明存在 tool calls
doc-b:v4 release notes,提到 tool execution improvements
doc-c:社区文章,声称“单次最多 128 个工具调用”

此时:

c1:部分支持
c2:部分支持
c3:未覆盖
c4:未覆盖

不能直接引用社区文章中的 128 作为结论,因为它没有满足官方来源和版本条件。

第五步:针对缺口检索

site:official.example SDK v4 maximum tool calls request limit
site:official.example SDK v4 errors too many tool calls

如果官方文档发现限制是“每个响应最多 64 个工具调用”,而社区文章说 128,那么形成冲突:

冲突:
- doc-a / doc-d:64
- doc-c:128
来源差异:官方 vs 社区
版本差异:社区文章未标注版本

此时应优先采用版本明确、来源权威的官方文档,同时在回答中说明社区资料与官方资料不一致,不能把 128 作为 v4 的确定限制。

第六步:停止

如果官方文档已经支持:

c1:已覆盖
c2:已覆盖
c3:已覆盖
c4:已覆盖
冲突:已解释为来源和版本标注不同

则停止检索,生成带来源映射的回答。

如果官方文档只确认“支持批量调用”,没有给出数量限制,正确答案应是:

官方资料确认 v4 支持批量工具调用,但在当前检索到的 v4 文档中未找到单次请求数量上限。社区资料提到 128,但未标注适用版本,不能作为确定结论。

这比强行给出一个数字更可靠。


十四、真实边界:Agentic RAG 不能解决所有知识问题

1. 检索不到不等于事实不存在

可能的原因包括:

  • 查询术语错误;
  • 文档未被索引;
  • 用户无权限;
  • 内容在图片、表格或附件中;
  • 来源暂时不可用;
  • 版本信息被隐藏;
  • 查询范围过窄。

因此,no result 至少要区分:

NO_MATCH        没有匹配内容
SOURCE_ERROR    来源调用失败
ACCESS_DENIED   没有权限
UNSUPPORTED     当前连接器不支持该内容类型
STALE_INDEX     索引可能过期

2. 更多检索不一定更准确

当来源质量差、问题定义不清或存在重复传播时,增加文档数量会放大噪声。尤其是网页搜索中,同一个错误结论可能被多个网站复制。此时“出现次数多”不等于“证据更强”。

3. 重排不能替代事实核验

排序模型可以判断“哪些片段更可能有用”,但不能保证片段真实、未过期或适用条件正确。涉及安全、法律、财务、生产变更和合规的问题,仍需要来源治理、人工审批或专门校验器。

4. 分解可能引入新的错误

模型拆分问题时可能:

  • 遗漏关键约束;
  • 生成不存在的实体;
  • 把相关问题错误地当成因果问题;
  • 把用户的比较问题拆成无关定义题;
  • 生成带有错误前提的查询。

因此,查询分解结果也应作为可观测状态,而不是不可追踪的隐藏提示词。

5. 停止过早和停止过晚都危险

停止过早会导致:

  • 只覆盖问题的一部分;
  • 忽略版本限制;
  • 把相关片段误当作直接证据;
  • 在冲突未解决时给出确定结论。

停止过晚会导致:

  • 延迟和成本失控;
  • 重复检索;
  • 更多低质量来源进入上下文;
  • 模型被相互矛盾的材料干扰。

可靠的系统不追求“搜得最多”,而是追求“在可解释的证据条件下停止”。


十五、生产诊断:从最终答案反查检索过程

Agentic RAG 出现错误时,不能只查看最终回答。至少要记录以下事件:

{
  "run_id": "run-123",
  "iteration": 2,
  "action": "retrieve",
  "subquestion": "v4 单次请求限制",
  "queries": [
    "SDK v4 request limits tool calls"
  ],
  "sources": ["official-docs"],
  "result_count": 8,
  "selected_evidence": ["doc-d#limits"],
  "unresolved_claims_before": ["c3", "c4"],
  "unresolved_claims_after": [],
  "stop_reason": "evidence_sufficient"
}

诊断时按以下顺序检查:

  1. 决策错误:本来需要检索,却被判断为无需检索;
  2. 分解错误:遗漏了关键子问题;
  3. 查询错误:术语、版本或实体错误;
  4. 召回错误:正确文档没有进入候选集;
  5. 重排错误:低质量片段排在高质量片段前;
  6. 评估错误:片段并未支持主张,却被标记为已覆盖;
  7. 停止错误:存在关键缺口或冲突时仍然回答;
  8. 引用错误:最终句子与证据片段无法建立映射。

一个实用的评估单位不是“答案是否听起来合理”,而是主张级别的记录:

claim_id | claim_text | evidence_ids | support_level | source_quality | status

这样可以区分:

  • 检索失败;
  • 证据存在但评估失败;
  • 证据和回答都正确,但引用映射失败。

十六、实现时应保持的核心不变量

一个 Agentic RAG 实现至少应保持以下不变量:

不变量一:每个重要结论都能追溯到证据

如果回答中的句子无法映射到证据片段、结构化记录或用户输入,就不能把它标记为已验证事实。

不变量二:每轮检索都必须改变信息状态

新增证据、解决主张、消除冲突或澄清约束,至少发生一项。否则就是无进展循环。

不变量三:查询预算必须显式消耗

查询、抓取、重排和模型调用都应进入预算,而不是只限制 Agent 循环次数。

不变量四:版本和时间属于证据的一部分

“内容正确”不代表“在当前版本中适用”。版本、发布时间、抓取时间和适用条件应参与重排和停止判断。

不变量五:权限过滤发生在检索边界

不能先把无权访问的内容交给模型,再依赖提示词要求模型忽略它。权限过滤应在召回或数据访问层完成。

不变量六:停止原因必须可解释

evidence_sufficientbudget_exhaustedno_progressunresolved_conflictclarification_required 表示完全不同的系统状态,不能统一显示为“完成”。


Agentic RAG 的本质,不是给普通 RAG 增加一个会调用搜索工具的模型,而是把检索变成一个可规划、可验证、可恢复的状态机。查询分解决定问题如何展开,检索决策决定向哪里寻找信息,重排决定哪些候选证据进入上下文,迭代决定如何根据缺口继续寻找,停止条件决定何时可以回答、降级回答或承认不足。

当这些环节都围绕“主张—证据—来源—状态”组织时,RAG 才从一次性的相似度搜索,变成能够处理复杂问题、版本变化、来源冲突和有限预算的 Agent 工程组件。


系列导航与关联阅读

官方资料

本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。