Agent 工程体系 · 第 30/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。
Agentic RAG:检索决策、查询分解、重排、迭代和停止
RAG(Retrieval-Augmented Generation,检索增强生成)把“生成答案”拆成两个基本动作:先从外部知识源取得证据,再让模型基于证据生成回答。传统 RAG 通常是固定流水线:
这种流程适合“一个问题对应一组稳定文档”的场景,但它默认了几个并不总成立的前提:
- 用户问题可以直接作为检索查询;
- 一次检索就能找全需要的证据;
- 向量相似度足以决定证据顺序;
- 返回的文档都可以直接放进上下文;
- 模型能够自行判断证据是否足够;
- 检索应该对所有问题一视同仁。
Agentic RAG 的核心变化,是让 Agent 参与检索过程中的决策。它不再把检索当作固定工具调用,而是把检索视为一个具有状态、分支、反馈和停止条件的决策过程:
这里的 Agent 不等于“让大模型自由发挥”。一个可控的 Agentic RAG 必须明确:
- 什么情况下需要检索;
- 检索什么内容;
- 一个复杂问题如何拆分;
- 多个查询如何并发执行;
- 检索结果如何重排;
- 什么证据已经足够;
- 什么情况必须继续检索;
- 什么时候应该停止并承认信息不足。
一、先区分三个对象:问题、查询和证据
1. 用户问题不等于检索查询
用户问题是面向人的表达,通常包含背景、目标、约束和隐含意图。例如:
我们计划把订单服务从单体应用拆成微服务,数据库应该怎么迁移,如何保证迁移期间不丢数据?
这是一个完整的工程问题,但它不是一个理想的检索查询。它至少包含以下子问题:
- 单体拆分时数据库迁移有哪些模式;
- 如何进行双写或变更数据捕获;
- 如何校验新旧库的一致性;
- 如何处理回滚;
- 哪些方案适合订单这种强一致性业务。
查询是为了从某个知识源中找到相关内容而构造的检索表达。它应当尽可能包含:
- 明确主题;
- 关键实体;
- 约束条件;
- 所需证据类型;
- 时间范围或版本范围;
- 来源范围。
例如,可以把原问题改写成:
订单服务 单体拆分 数据库迁移 双写 一致性校验 回滚
或者拆成多个更窄的查询:
单体拆分到微服务 数据库迁移模式
订单系统 双写 数据一致性校验
微服务数据库迁移 失败回滚策略
2. 查询不等于证据
查询只是寻找证据的请求,检索结果通常是候选文档或候选片段。证据则必须满足更严格的条件:
- 能够支持某个具体主张;
- 来源身份可识别;
- 内容边界明确;
- 与问题中的实体和约束一致;
- 没有被截断到失去语义;
- 在需要时具备时间和版本信息。
因此,一条高相似度的文本不一定是证据。例如,查询“数据库迁移 双写一致性”可能找到一篇介绍缓存双写的文章。它在词面上相关,但并不能直接支持订单数据库迁移的结论。
可以把一个证据片段表示为:
其中:
- :文档标识;
- :文档中的位置;
- :片段文本;
- :标题、作者、版本、时间、权限等元数据;
- :来源映射,例如 URL、文件路径、数据库记录或网页抓取任务。
Agentic RAG 的后续判断,应该围绕“证据是否支持所需主张”进行,而不是只围绕“搜索结果是否相似”进行。
二、Agentic RAG 的基本决策模型
设用户问题为 ,可用知识源为 ,当前已经收集的证据集合为 ,剩余预算为 。Agent 在每一步选择一个动作:
对应含义如下:
answer:基于当前证据生成回答;retrieve:执行一次或一组检索;decompose:把问题拆成多个子问题;ask_clarification:问题存在无法安全猜测的歧义;abstain:证据不足或来源冲突无法解决,明确说明限制。
Agent 的目标不是让检索次数最大化,而是在准确性、覆盖率、延迟和成本之间取得平衡。可以用一个简化目标表示:
其中:
- :证据对问题的支持效用;
- :模型调用、检索和抓取成本;
- :延迟;
- :错误、冲突或不确定性风险;
- :业务对成本、延迟和风险的权重。
一次额外检索是否值得,取决于它的期望收益:
当预期新增收益低于检索成本和风险时,就不应继续。
这个公式不是要求在线计算精确数值,而是说明一个重要原则:“继续搜索”必须是有条件的动作,而不是失败后的默认重试。
三、检索决策:什么时候检索,检索哪一个源
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 和双写迁移,哪一种更适合订单服务从单体拆分,要求支持灰度发布和失败回滚。
可以分解为:
- logical replication 的数据同步语义是什么;
- 双写迁移的一致性风险是什么;
- 两者对灰度发布的支持方式是什么;
- 两者的失败回滚路径是什么;
- 订单服务对一致性和顺序性的要求是什么;
- 在这些约束下,比较结论是什么。
最后一个问题“哪一种更适合”不能直接检索得到,它需要建立在前五个事实之上。
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 需要花更多时间判断哪些结果真正有用。
因此可以给每个子问题分配查询预算:
其中 是错误风险, 是术语或意图歧义。高风险、高歧义问题可以生成更多查询,但低风险问题不必使用同样的数量。
六、检索执行:并发、权限和故障路径
查询分解后,可以并发执行相互独立的子查询:
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
这段代码表达的是一种故障隔离原则:一个子查询失败,不应自动丢弃所有已经获得的证据。
但并发并不意味着可以无条件并发。以下任务通常需要串行:
- 后一个查询依赖前一个查询发现的实体;
- 第一次检索确定了版本,第二次检索需要使用该版本;
- 第一次结果发现来源冲突,第二次检索针对冲突进行核查;
- 查询包含权限敏感条件,需要根据授权结果调整范围。
检索层还必须处理四类故障:
- 超时:返回部分结果,并把超时记录为检索事件;
- 限流:根据来源分别退避,不能让所有来源同时重试;
- 权限拒绝:不能通过改写查询绕过权限;
- 结果为空:区分“没有匹配结果”和“检索系统失败”。
如果所有故障最后都表现为“空结果”,Agent 会错误地把基础设施故障判断成知识不存在。
七、重排:为什么第一次检索顺序不够可靠
1. 召回和排序解决不同问题
检索通常分为两个阶段:
召回阶段追求“不漏掉可能相关的文档”,允许噪声较多;重排阶段追求“把最能支持当前问题的证据放在前面”。
向量相似度通常只衡量语义接近程度:
它无法充分表达:
- 文档是否来自可信来源;
- 文档是否匹配目标版本;
- 片段是否真的回答了问题;
- 片段是否包含必要条件;
- 片段是否与其他证据重复;
- 片段是否与当前用户权限匹配。
因此,重排评分可以组合多个维度:
其中:
- :语义相关性;
- :关键词、实体和术语匹配;
- :来源可信度;
- :时间或版本新鲜度;
- :对未解决主张的覆盖程度;
- :与已选证据的重复程度;
- :与高可信证据冲突的程度。
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):
第一项鼓励相关,第二项惩罚与已选片段重复。
例如,回答“某配置项的默认值、适用版本和限制”时,理想证据可能分别来自:
- 官方参数说明;
- 版本变更记录;
- 限制或异常处理章节。
三个片段的主题不同,但共同覆盖一个问题。只选择最高相似度片段,可能只得到三段参数定义,遗漏版本和限制。
八、证据充分性:Agent 为什么知道该继续查
Agent 需要把“回答质量”转换成可检查的中间结构。最实用的方式是维护主张集合。
设问题需要回答的主张为:
每个证据片段 能支持其中一部分主张。定义支持关系:
分别表示:
0:不支持;0.5:部分支持或需要结合其他证据;1:直接支持。
证据覆盖率可以定义为:
其中 covered(c_i) 可以取该主张目前的最大支持程度。
例如,问题是:
某 API 参数在 v3 中是否支持流式模式?如果支持,有哪些限制?
主张集合可能是:
c1:v3 支持该参数
c2:该参数可以用于流式模式
c3:流式模式存在限制 A
c4:流式模式存在限制 B
如果证据只支持 c1 和 c2,Agent 不能因为“已经找到官方文档”就直接回答完整限制。它应把 c3 和 c4 保留为未解决主张,继续检索或明确说明未找到限制信息。
反例:相似度阈值不是充分性判断
假设系统设置:
如果最高相似度 > 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_queries、execute_queries 和 assess_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 文档把 checkpointer 和 store 区分为两种不同持久化机制:前者保存单个线程的图状态快照,适合对话连续性、人工介入、时间回溯和容错;后者保存跨线程的应用数据,适合用户偏好、事实和共享知识。(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:限制适用的版本和条件
第二步:选择来源
优先级:
- v4 官方 API 文档;
- v4 发布说明;
- 官方 SDK 类型定义或示例;
- 官方错误码和限制说明。
社区文章只能作为发现术语的辅助来源,不能独立支持最终结论。
第三步:生成查询
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"
}
诊断时按以下顺序检查:
- 决策错误:本来需要检索,却被判断为无需检索;
- 分解错误:遗漏了关键子问题;
- 查询错误:术语、版本或实体错误;
- 召回错误:正确文档没有进入候选集;
- 重排错误:低质量片段排在高质量片段前;
- 评估错误:片段并未支持主张,却被标记为已覆盖;
- 停止错误:存在关键缺口或冲突时仍然回答;
- 引用错误:最终句子与证据片段无法建立映射。
一个实用的评估单位不是“答案是否听起来合理”,而是主张级别的记录:
claim_id | claim_text | evidence_ids | support_level | source_quality | status
这样可以区分:
- 检索失败;
- 证据存在但评估失败;
- 证据和回答都正确,但引用映射失败。
十六、实现时应保持的核心不变量
一个 Agentic RAG 实现至少应保持以下不变量:
不变量一:每个重要结论都能追溯到证据
如果回答中的句子无法映射到证据片段、结构化记录或用户输入,就不能把它标记为已验证事实。
不变量二:每轮检索都必须改变信息状态
新增证据、解决主张、消除冲突或澄清约束,至少发生一项。否则就是无进展循环。
不变量三:查询预算必须显式消耗
查询、抓取、重排和模型调用都应进入预算,而不是只限制 Agent 循环次数。
不变量四:版本和时间属于证据的一部分
“内容正确”不代表“在当前版本中适用”。版本、发布时间、抓取时间和适用条件应参与重排和停止判断。
不变量五:权限过滤发生在检索边界
不能先把无权访问的内容交给模型,再依赖提示词要求模型忽略它。权限过滤应在召回或数据访问层完成。
不变量六:停止原因必须可解释
evidence_sufficient、budget_exhausted、no_progress、unresolved_conflict 和 clarification_required 表示完全不同的系统状态,不能统一显示为“完成”。
Agentic RAG 的本质,不是给普通 RAG 增加一个会调用搜索工具的模型,而是把检索变成一个可规划、可验证、可恢复的状态机。查询分解决定问题如何展开,检索决策决定向哪里寻找信息,重排决定哪些候选证据进入上下文,迭代决定如何根据缺口继续寻找,停止条件决定何时可以回答、降级回答或承认不足。
当这些环节都围绕“主张—证据—来源—状态”组织时,RAG 才从一次性的相似度搜索,变成能够处理复杂问题、版本变化、来源冲突和有限预算的 Agent 工程组件。
系列导航与关联阅读
- 系列入口:Agent 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:Agent 记忆隐私与删除:同意、保留期、可追溯删除和备份
- 下一篇:Agent 知识引用:证据片段、来源映射、冲突和可验证回答
- 延伸:联网搜索 Agent:查询规划、来源选择、抓取、引用和时效性
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论