Agent 工程体系 · 第 31/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。
Agent 知识引用:证据片段、来源映射、冲突和可验证回答
Agent 的回答如果只返回一段自然语言,用户无法判断其中哪些内容来自检索结果,哪些是模型推断,哪些只是模型凭经验补全。知识引用要解决的不是“在回答末尾附几个链接”,而是建立一条可以回溯、检查和复现的证据链:
用户问题
↓
检索与查询分解
↓
证据片段
↓
来源映射
↓
事实主张
↓
冲突检测与决策
↓
带引用的回答
↓
验证、拒答或升级
本文把知识引用视为 Agent 的一个状态化子系统,重点讨论四个对象:
- 证据片段:回答所依据的最小信息单元;
- 来源映射:主张与证据、文档、版本、位置之间的关系;
- 冲突:多个证据对同一主张给出不一致结论时如何处理;
- 可验证回答:用户能够沿着引用重新定位原文,并判断回答是否超出了证据支持范围。
这里的“引用”不是语言格式问题,而是一个从检索结果到最终回答的可追溯性约束。
一、先区分四种内容:事实、证据、推断和回答
Agent 生成回答时,至少存在四个不同层次。
1. 事实主张
事实主张是回答中可以被判断为真或假的命题。例如:
LangGraph 的 checkpointer 按 thread 保存图状态快照。
这句话包含了一个明确命题:
主体:LangGraph checkpointer
关系:保存
客体:图状态快照
作用域:单个 thread
工程上应把回答拆成主张,而不是把整段回答视为一个不可分割的字符串。
设回答包含主张集合:
其中每个 都应能独立判断:
- 是否有证据支持;
- 证据支持的是全部主张还是部分主张;
- 是否存在相互冲突的证据;
- 是否因时间、版本、权限或数据域而改变。
2. 证据片段
证据片段是从某个来源中截取出来、足以支持一个或多个主张的最小内容单元。
例如,文档中有一段:
Checkpointers persist a thread’s graph state as checkpoints. Stores persist application-defined data outside the graph state.
可以拆成两个证据片段:
E1:
checkpointer → thread-scoped graph state snapshots
E2:
store → application-defined data outside graph state
证据片段不等于搜索结果标题,也不等于整个文档。搜索结果只能说明“可能相关”,不能自动证明命题成立。
3. 来源
来源是证据片段的载体,例如:
- 官方文档;
- API 参考;
- 数据库记录;
- 工单;
- 代码提交;
- 业务系统返回结果;
- 用户明确提供的信息。
来源通常需要包含:
source_id
source_type
canonical_locator
title
version
published_at
retrieved_at
authority
content_hash
access_scope
其中:
canonical_locator:稳定定位信息,例如 URL、文档 ID、文件路径;version:文档或业务数据版本;published_at:来源发布或生效时间;retrieved_at:Agent 获取该来源的时间;authority:来源权威等级;content_hash:内容指纹,用于判断引用时内容是否已经变化;access_scope:用户或 Agent 是否有权访问该来源。
4. 推断
推断是 Agent 根据一个或多个证据片段得到的结论,但该结论未必在原文中直接出现。
例如:
E1:checkpointer 保存单个 thread 的图状态快照
E2:长对话会积累 checkpoint
推断:如果不设置保留策略,长会话可能增加存储成本
“可能增加存储成本”是合理推断,但不是 E1 单独直接陈述的事实。
因此,引用系统必须区分:
direct_support # 原文直接支持
derived_support # 由多个证据推导
unsupported # 没有可接受证据
contradicted # 被证据反驳
最常见的错误是把推断写成事实,把相关证据伪装成直接证据。
二、证据片段不是“相关文本”,而是可验证的最小单元
2.1 证据片段的四个必要条件
一个可用于引用的证据片段,至少应满足四个条件。
条件一:定位稳定
用户必须能够重新找到它。
只保存:
{
"text": "checkpointers persist state"
}
是不够的,因为这段文本可能出现在多个文档、多个版本甚至不同上下文中。
更可靠的表示是:
{
"source_id": "langgraph-persistence",
"locator": {
"url": "https://docs.langchain.com/oss/python/langgraph/persistence",
"section": "Checkpointer vs. store",
"line_start": 64,
"line_end": 72
},
"content_hash": "sha256:..."
}
行号不是所有来源都具备,因此可以使用多级定位:
URL
→ 文档版本
→ 章节标题
→ 页面内锚点
→ 段落序号
→ 字符范围
→ 内容哈希
条件二:语义完整
片段必须保留足够上下文,避免断章取义。
错误示例:
“does not persist”
这句话没有主语和作用域。它可能指:
- 内存保存器;
- 某个 response;
- 某种数据;
- 某一类会话。
更好的片段应包括完整命题:
“InMemorySaver stores checkpoints in RAM. When the process restarts,
all checkpoints are lost.”
条件三:时间可解释
知识不是永恒不变的。一个配置项在 2025 年有效,不代表在 2026 年仍有效。
因此,证据至少要记录两个时间:
published_at # 来源声明的发布时间或生效时间
retrieved_at # Agent 实际检索时间
当用户问“当前版本如何配置”时,retrieved_at 说明 Agent 何时看到它;当用户问“2025 年当时的行为”时,published_at 或历史版本更重要。
条件四:权限可解释
证据可能来自用户无权查看的内部系统。Agent 可以内部使用它,但不能把敏感原文直接暴露给用户。
因此引用对象要区分:
evidence_visibility:
internal_only
user_visible
redacted
aggregate_only
“有引用”不等于“可以把原文全部展示出来”。生产系统通常需要把内部证据映射为:
来源名称 + 脱敏摘要 + 可授权链接
三、推荐的数据模型:把回答从字符串变成带证据的对象
3.1 来源、片段和主张三层模型
一个实用的数据模型如下:
{
"sources": [
{
"source_id": "s1",
"type": "official_documentation",
"title": "Persistence",
"locator": {
"url": "https://docs.langchain.com/oss/python/langgraph/persistence",
"section": "Checkpointer vs. store"
},
"version": "2026-09",
"retrieved_at": "2026-09-01T10:00:00+08:00",
"authority": 0.95,
"content_hash": "sha256:..."
}
],
"evidence": [
{
"evidence_id": "e1",
"source_id": "s1",
"text": "Checkpointers persist graph state snapshots...",
"span": {
"line_start": 64,
"line_end": 72
},
"retrieval_score": 0.88,
"support_type": "direct"
}
],
"claims": [
{
"claim_id": "c1",
"text": "checkpointer 用于保存单个 thread 的图状态",
"evidence_ids": ["e1"],
"status": "supported",
"confidence": 0.93
}
]
}
这里有三个不能混淆的分数:
retrieval_score:片段与查询的相关性;authority:来源的权威程度;confidence:该主张在当前证据下的可信程度。
高相关性不代表高权威。例如,一篇博客可能非常贴合问题,但不能压过同版本官方 API 文档。
3.2 主张—证据映射是有向图
可以把引用关系表示为图:
graph LR
Q[用户问题] --> C1[主张 c1]
Q --> C2[主张 c2]
S1[来源 s1: 官方文档] --> E1[证据 e1]
S2[来源 s2: 运行日志] --> E2[证据 e2]
S3[来源 s3: 旧版本文档] --> E3[证据 e3]
E1 --> C1
E2 --> C2
E3 --> C1
C1 --> A[回答]
C2 --> A
关键点是:
- 一个来源可以包含多个证据片段;
- 一个主张可以由多个片段共同支持;
- 一个片段可以支持多个主张;
- 来源之间可能处于冲突关系;
- 回答只是主张集合的语言化结果,不是证据本身。
如果系统只保存最终回答和几个 URL,就无法知道某个句子究竟由哪一段内容支持。
四、来源映射:引用必须回答“哪句话由什么证据支持”
4.1 映射的基本关系
定义来源映射:
其中:
- 是主张集合;
- 是证据片段集合;
- 表示证据 支持主张 。
但单纯的二元关系仍然不够。还需要记录支持类型和覆盖范围:
例如:
{
"claim_id": "c1",
"evidence_id": "e1",
"support": "direct",
"coverage": 1.0,
"scope": "LangGraph Python OSS",
"valid_at": "2026-09"
}
4.2 覆盖率不等于可信度
假设主张为:
LangGraph 的 checkpointer 保存单个 thread 的状态,并且生产环境重启后不会丢失这些状态。
第一半可能由官方文档支持,第二半却与内存实现有关。即使一个片段覆盖了主句的大部分内容,也不能把整个句子标记为已验证。
可定义主张覆盖率:
若把该句拆成两个语义单元:
u1:checkpointer 保存单个 thread 的状态
u2:生产环境重启后状态不会丢失
而证据只支持 u1,则:
此时正确做法不是降低语气后继续输出整句话,而是拆句:
Checkpointer 用于保存单个 thread 的图状态。是否能跨进程重启保留状态,取决于具体实现;内存保存器不会持久化到进程外。
官方 LangGraph 文档明确区分了 checkpointer 与 store:前者保存单个 thread 的图状态快照,后者保存跨 thread 的应用数据;同时,InMemorySaver 和 MemorySaver 的数据位于内存中,进程重启后会丢失。(docs.langchain.com)
4.3 引用粒度要与主张粒度一致
以下引用方式不可靠:
LangGraph 有短期记忆、长期记忆、故障恢复、时间旅行、跨线程共享知识等能力。[引用一个 URL]
因为这句话包含多个独立主张,应该拆成:
c1:checkpointer 提供 thread 级短期状态
c2:store 提供跨 thread 的长期数据
c3:checkpointer 可用于故障恢复
c4:checkpointer 支持 time travel
每一个主张都要有独立证据,或者明确标记为“由文档能力组合推导”。
五、从检索结果到引用:Agent 的完整数据流
知识引用通常位于 Agentic RAG 的后半段,但它会反向约束前半段的检索设计。
5.1 检索结果必须保留元数据
普通检索接口经常只返回:
[
"一段文本",
"另一段文本"
]
这样的结果无法可靠引用。至少应该返回:
[
{
"document_id": "doc-001",
"chunk_id": "doc-001#p12",
"text": "...",
"score": 0.91,
"source_url": "...",
"section": "...",
"version": "...",
"retrieved_at": "...",
"access_scope": "user-visible"
}
]
chunk_id 是内部稳定标识,source_url 是用户可访问定位,二者不应混为一谈。
5.2 查询分解会改变引用结构
用户问题可能同时包含多个子问题:
OpenAI 的会话状态如何跨轮次保存?LangGraph 的 checkpointer 和 store 有什么区别?内存保存器重启后会怎样?
可以分解为:
q1:OpenAI 如何传递上一轮上下文?
q2:OpenAI Conversations API 的作用域是什么?
q3:LangGraph checkpointer 保存什么?
q4:LangGraph store 保存什么?
q5:内存保存器是否跨重启持久化?
如果不分解,检索器可能找到一段“看起来相关”的材料,然后让模型用它回答全部问题。这会制造证据越权:一段证据只支持 q1,却被用于 q2~q5。
5.3 重排不能替代证据判断
重排模型只负责判断“这段材料是否更相关”,不能直接判断:
这段材料是否真的蕴含该主张?
因此应至少有两个阶段:
相关性排序:
query ↔ chunk
证据验证:
claim ↔ chunk
形式化表示为:
其中:
relevance表示证据是否与问题主题相关;entailed表示证据是否蕴含主张;partially_entailed表示只支持主张的一部分;unknown表示无法判断;contradicted表示证据与主张相反。
只有 relevance 高且 V(c,e) 为 entailed 或可接受的 partially_entailed,片段才适合作为引用依据。
六、冲突不是异常数据,而是知识系统的正常状态
6.1 什么是冲突
两个证据发生冲突,不是指它们文字不同,而是它们对同一个语义命题给出了不可同时成立的结论。
例如:
证据 e1:内存保存器的数据在进程重启后丢失
证据 e2:内存保存器的数据在进程重启后保留
二者对命题:
P:内存保存器可以跨进程重启持久化
分别给出:
e1:¬P
e2:P
但下面两段不一定冲突:
e1:Responses response 默认保存 30 天
e2:Conversation 对象中的项目不受 30 天 TTL 限制
它们作用于不同对象:
response object ≠ conversation item
OpenAI 的会话状态文档同时说明,Response 对象默认保存 30 天,而附加到 Conversation 的项目不受该 30 天 TTL 限制;如果忽略对象类型,就会错误地把两条规则判定为冲突。(developers.openai.com)
6.2 冲突检测的五步
对每个候选冲突,按以下顺序处理。
第一步:规范化主张
把自然语言转成带作用域的结构:
subject:response object
predicate:retained_for
object:30 days
scope:default
和:
subject:conversation item
predicate:retained_for
object:unlimited by 30-day TTL
scope:attached to conversation
对象不同,因此不冲突。
第二步:对齐时间
两个证据可能分别适用于:
v1.0, 2025-01
v2.0, 2026-09
这时不是简单选择“最新文本”,而是回答:
- 用户问哪个版本;
- 哪个版本在目标环境中实际运行;
- 旧行为是否仍影响历史数据;
- 是否存在迁移期或兼容模式。
第三步:对齐作用域
需要检查:
产品
API
模型
语言
部署模式
租户
用户权限
数据中心
例如,“默认行为”与“配置后行为”不能直接比较;“Cloud Agent Server 自动持久化”也不能推导出“本地内存保存器自动持久化”。
LangGraph 文档指出,使用 Agent Server 时,持久化基础设施由服务器处理;而在直接编译图时,需要显式配置 checkpointer 或 store。两种运行形态不同,不能把一个环境中的保证扩展到另一个环境。(docs.langchain.com)
第四步:判定证据等级
可采用一个明确但可配置的优先级:
同版本官方规范 / API 文档
>
官方源码与测试
>
运行时观测结果
>
官方示例
>
经过认证的内部文档
>
社区文章
>
模型记忆
但“官方文档优先”不是绝对规则。对于实时业务数据,数据库当前读数可能比静态文档更能说明当前状态;对于已部署系统,运行时观测可能揭示文档未覆盖的配置行为。
第五步:保留未解决冲突
如果冲突无法消解,不应强行选一条。输出应明确:
来源 A 在版本 v1 中说明 X;
来源 B 在版本 v2 中说明 Y;
当前无法仅凭公开材料确定你的部署使用哪种行为。
这比生成一个看似确定但不可验证的答案更安全。
6.3 冲突决策函数
可以把证据优先级写成一个评分模型:
其中:
- :来源权威度;
- :时间匹配度;
- :作用域匹配度;
- :对主张的直接支持程度;
- :冲突风险;
- :业务配置的权重。
直觉是:一条证据即使很新,如果版本和作用域不匹配,也不应该压过一条适用范围明确的旧证据。
七、可验证回答:不是“有链接”,而是“可复核”
7.1 可验证回答的定义
一个回答 是可验证的,至少满足:
也就是说,每个可核查主张都必须有:
- 可定位的证据;
- 足以支持主张的内容;
- 匹配的版本、时间和作用域。
这不要求每句话都附引用。寒暄、格式说明和纯粹的组织性文字不需要引用;但以下内容通常需要:
- API 行为;
- 版本差异;
- 存储、权限、计费和安全保证;
- 业务数据;
- “默认”“始终”“不会”“保证”等绝对化表述。
7.2 引用位置
引用应尽量紧邻主张:
Responses API 可以通过 `previous_response_id` 将响应串成线程式上下文。([developers.openai.com](https://developers.openai.com/api/docs/guides/conversation-state))
而不是把多个段落的所有证据集中到末尾。OpenAI 文档把 previous_response_id 定义为用于链接响应、创建 threaded conversation 的参数;它与 Conversations API 的持久化会话对象是两种不同的状态管理方式。(developers.openai.com)
7.3 “引用存在”仍可能不可验证
以下情况都会导致伪可验证:
1. 引用指向搜索结果而非原文;
2. 链接需要用户没有的权限;
3. 来源内容已经变化但没有版本或哈希;
4. 片段被截断,缺失否定词或作用域;
5. 引用只支持句子的前半部分;
6. 回答使用了证据中没有的数字、因果或绝对结论;
7. 引用对象与回答对象不一致。
例如:
证据:系统支持持久化会话状态
回答:系统永远不会丢失任何状态
这里从“支持持久化”跳到了“永远不会丢失”,中间缺少:
- 故障模型;
- 事务边界;
- 写入确认;
- 备份策略;
- 恢复策略;
- 数据保留策略。
因此,回答即使附了正确链接,仍然不可验证。
八、一个最小可运行的引用决策器
下面的示例不依赖模型和向量数据库,用 Python 标准库演示三个核心动作:
- 保存证据片段及其来源;
- 对主张进行直接支持、部分支持和冲突标记;
- 只把达到阈值的主张写入回答。
from dataclasses import dataclass
from typing import Literal
Support = Literal["supports", "contradicts", "unknown"]
@dataclass
class Source:
source_id: str
title: str
locator: str
authority: float
version: str
@dataclass
class Evidence:
evidence_id: str
source_id: str
text: str
support: Support
scope: str
version: str
@dataclass
class Claim:
claim_id: str
text: str
evidence_ids: list[str]
sources = {
"s1": Source(
source_id="s1",
title="LangGraph Persistence",
locator="official-docs#checkpointer-vs-store",
authority=0.95,
version="2026-09",
)
}
evidence = {
"e1": Evidence(
evidence_id="e1",
source_id="s1",
text="Checkpointers persist a thread's graph state as checkpoints.",
support="supports",
scope="LangGraph OSS Python",
version="2026-09",
),
"e2": Evidence(
evidence_id="e2",
source_id="s1",
text="InMemorySaver stores checkpoints in RAM; after restart they are lost.",
support="contradicts",
scope="InMemorySaver",
version="2026-09",
),
}
def evaluate_claim(claim: Claim, evidence_map: dict[str, Evidence]) -> dict:
matched = [evidence_map[eid] for eid in claim.evidence_ids]
supporting = [
e for e in matched
if e.support == "supports"
]
contradicting = [
e for e in matched
if e.support == "contradicts"
]
if not supporting:
status = "unsupported"
elif contradicting:
status = "conflict"
else:
status = "supported"
# 这里的分数只是示例规则,不是通用概率。
score = 0.0
if supporting:
score = max(
sources[e.source_id].authority
for e in supporting
)
return {
"claim_id": claim.claim_id,
"text": claim.text,
"status": status,
"score": round(score, 2),
"evidence_ids": claim.evidence_ids,
}
claims = [
Claim(
claim_id="c1",
text="checkpointer 用于保存单个 thread 的图状态",
evidence_ids=["e1"],
),
Claim(
claim_id="c2",
text="所有 checkpointer 都能跨进程重启保存状态",
evidence_ids=["e1", "e2"],
),
]
for claim in claims:
print(evaluate_claim(claim, evidence))
预期输出类似:
{
'claim_id': 'c1',
'text': 'checkpointer 用于保存单个 thread 的图状态',
'status': 'supported',
'score': 0.95,
'evidence_ids': ['e1']
}
{
'claim_id': 'c2',
'text': '所有 checkpointer 都能跨进程重启保存状态',
'status': 'conflict',
'score': 0.95,
'evidence_ids': ['e1', 'e2']
}
这个示例有一个重要限制:它把证据的 support 作为输入,实际系统不能依赖人工预填。真实流程应由证据验证器根据主张和片段计算:
claim + evidence
↓
蕴含判断
↓
scope/version 判断
↓
支持、部分支持、未知或冲突
示例中的 score 也不是概率。它只是来源权威度的简单演示,不能直接解释为“95% 正确”。
九、回答生成应采用“先验证、后表述”
不可靠的流程是:
检索 → 直接把片段塞进 prompt → 生成答案
更可控的流程是:
检索
↓
候选证据归一化
↓
生成原子主张
↓
验证主张—证据关系
↓
冲突分类
↓
决定回答、限定回答或拒答
↓
渲染引用
9.1 主张状态机
可以把每个主张建模为状态机:
stateDiagram-v2
[*] --> Candidate
Candidate --> Supported: 有直接证据且作用域匹配
Candidate --> PartiallySupported: 仅支持部分语义
Candidate --> Conflicted: 存在未消解矛盾
Candidate --> Unknown: 证据相关但不能蕴含
Candidate --> Unsupported: 没有可接受证据
PartiallySupported --> Supported: 拆分主张或补充证据
Conflicted --> Supported: 完成版本/作用域消歧
Conflicted --> Escalated: 无法消歧
Unknown --> RetrievedMore: 继续检索
RetrievedMore --> Supported
RetrievedMore --> Escalated
每次状态变化都应记录原因:
{
"claim_id": "c2",
"from": "candidate",
"to": "conflicted",
"reason": "同一作用域下存在相反证据",
"evidence_ids": ["e1", "e2"],
"timestamp": "2026-09-01T10:03:00+08:00"
}
这样在生产事故中,工程师可以回答:
- Agent 为什么相信这句话;
- 哪个证据触发了拒答;
- 是检索失败、版本误判,还是冲突策略失败;
- 重新运行时是否得到相同结论。
9.2 三种回答形态
证据充分
结论:可以这样配置。
依据:
- 证据 e1 明确说明……
- 证据 e2 明确说明……
限制:
- 该结论仅适用于版本 2026-09。
证据部分充分
已确认:checkpointer 保存 thread 级图状态。
尚未确认:你的部署是否能跨进程重启保留状态,因为这取决于保存器实现。
证据冲突
当前存在冲突:
- 文档 A 对版本 v1 的描述是 X;
- 文档 B 对版本 v2 的描述是 Y。
请先确认实际运行版本或提供部署配置;在此之前不应把 X 或 Y 当作无条件结论。
拒答不是“我不知道”的同义改写,而是一个有证据原因的状态:
拒答原因 = 证据不足 ∨ 未解决冲突 ∨ 权限不足 ∨ 作用域不匹配
十、与会话状态和 Agent 持久化的关系
知识引用属于上下文与记忆模块,但它不应简单等同于聊天历史。
10.1 会话历史不是来源证明
OpenAI 文档指出,单次文本生成请求本身是独立且无状态的;应用可以通过手动传入历史消息来实现多轮对话。(developers.openai.com)
这意味着:
聊天历史 = 模型输入上下文
来源映射 = 证据审计关系
把引用信息仅放在聊天消息中,会产生几个问题:
- 压缩上下文时引用关系可能被丢弃;
- 同一来源被重复复制;
- 后续 Agent 无法知道引用对应哪个版本;
- 用户编辑或删除消息后,审计链断裂;
- 工具结果和最终回答之间没有结构化关系。
因此,推荐把引用作为独立状态:
state = {
"messages": [...],
"claims": [...],
"evidence": [...],
"source_map": [...],
"conflicts": [...],
"answer_status": "supported",
}
10.2 previous_response_id 与 Conversation 的区别
OpenAI 的 Responses API 支持使用 previous_response_id 链接前后响应,形成线程式对话;也支持创建具有持久标识符的 Conversation 对象,并在后续响应中传入该 Conversation。(developers.openai.com)
从引用系统角度看,二者都只解决“上下文如何继续传递”的问题,不自动解决:
哪些事实被引用;
引用来自哪个版本;
某条主张是否与新证据冲突;
引用是否仍对当前问题有效。
此外,官方文档说明,即使使用 previous_response_id,链中之前的输入 token 仍会作为输入计费;上下文窗口也包含输入、输出和推理 token。(developers.openai.com)
因此,长时间运行的 Agent 不应把完整检索片段无限追加到会话历史。更合理的做法是:
会话消息:
保存用户意图和必要上下文
引用状态:
保存 claim/evidence/source 图
回答摘要:
保存已经验证的结论和失效条件
10.3 LangGraph 的 checkpointer 与 store
LangGraph 将持久化分为两个互补层次:
- checkpointer:保存单个 thread 的图状态快照,用于短期、线程级状态;
- store:保存图状态之外的应用定义数据,用于跨 thread 的长期记忆。(docs.langchain.com)
可以这样划分引用数据:
checkpointer:
当前任务的 claims
本轮检索结果
未解决冲突
当前回答草稿
人工审核暂停点
store:
文档目录
来源版本索引
用户可授权的来源映射
跨会话的知识实体
已确认的业务规则
最小配置示例:
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
checkpointer = InMemorySaver()
store = InMemoryStore()
graph = builder.compile(
checkpointer=checkpointer,
store=store,
)
result = graph.invoke(
{
"messages": [
{"role": "user", "content": "检查这个结论是否有证据"}
],
"claims": [],
"evidence": [],
"conflicts": [],
},
{
"configurable": {
"thread_id": "citation-review-001"
}
},
)
官方示例要求在调用图时通过 configurable.thread_id 指定线程;文档还说明,内存保存器位于 RAM,进程重启后 checkpoint 会丢失,生产环境应使用持久化保存器。(docs.langchain.com)
这里要注意一个边界:
保存引用状态 ≠ 保证引用内容永久可访问
如果来源是外部网页,网页可能更新或删除;如果来源是业务数据库,记录可能被修改;如果来源是权限控制系统,用户之后可能失去访问权。因此应保存:
source_id
source_version
content_hash
retrieved_at
引用片段快照
用户可访问性
十一、并发和故障路径:引用状态必须可合并
Agent 常常并行执行多个检索器:
查询分支 A:官方文档
查询分支 B:代码仓库
查询分支 C:运行时配置
三个分支可能同时写入:
evidence[]
claims[]
conflicts[]
如果直接覆盖状态,会出现:
分支 A 写入 e1
分支 B 写入 e2
分支 B 的旧快照覆盖分支 A
最终只剩 e2
11.1 使用追加集合而不是覆盖字段
适合并发合并的数据结构是集合:
state["evidence_by_id"][evidence_id] = evidence
state["claims_by_id"][claim_id] = claim
合并规则:
同 evidence_id + 相同 content_hash → 幂等合并
同 evidence_id + 不同 content_hash → 版本冲突
同 claim_id + 新证据增加 → 重新评估
不要使用:
state["evidence"] = latest_branch_result
除非图框架明确保证单写者或事务串行化。
11.2 检索失败与验证失败要分开
两种失败具有不同含义:
检索失败:
没有得到足够候选证据
验证失败:
得到相关材料,但材料不蕴含主张
来源不可访问:
证据可能存在,但当前凭证无法读取
冲突失败:
多条高质量证据无法同时成立
对应的回退动作不同:
| 失败类型 | 合理动作 |
|---|---|
| 检索失败 | 查询改写、查询分解、扩大来源范围 |
| 验证失败 | 降低主张范围、拆句或拒答 |
| 来源不可访问 | 请求授权、使用公开摘要或标记不可验证 |
| 冲突失败 | 版本消歧、请求环境信息或升级人工 |
不能把所有失败都处理成“再检索一次”。如果问题是作用域不明确,重复检索只会增加更多相互矛盾的文本。
十二、常见错误及其失败机制
错误一:引用整个文档
回答:系统支持持久化、回滚和跨会话记忆。[引用整篇文档]
失败原因是文档中可能分别描述多个组件,而回答没有说明每个能力由什么组件提供。正确做法是把能力拆为主张,并分别映射到证据片段。
错误二:把检索排名当作真实性
top_k[0] = 相关性最高
因此 top_k[0] = 事实依据
相关性只是检索排序信号。广告页、过期文档和错误配置示例都可能与查询高度相关。
错误三:忽略否定词和限定词
以下词语会改变命题:
默认
通常
仅在……时
不包括
除非
实验性
单个 thread
跨 thread
进程重启后
切片时如果把“在默认配置下不会……”截成“不会……”,引用会反转原意。
错误四:把相关来源混成同一来源
用户问的是运行时行为,Agent 引用了设计文档;用户问的是当前版本,Agent 引用了旧版本;用户问的是企业租户,Agent 引用了公共环境。
这不是“引用不够多”,而是来源映射的作用域错误。
错误五:只存 URL,不存证据快照
URL 可以继续访问,但内容已经变化。此时用户打开链接看到的新内容可能不再支持原回答。
至少要保存:
抓取时间
版本
片段文本
内容哈希
章节定位
错误六:把模型自检当作事实验证
让同一个模型生成回答后再问“这句话是否有依据”,只能提高格式质量,不能构成独立验证。更可靠的组合是:
生成主张
→ 独立检索
→ 证据蕴含判断
→ 规则检查
→ 必要时人工审核
所谓“独立”不一定意味着使用不同模型,但至少应避免验证器直接复用生成器未经审查的结论。
十三、生产诊断:从错误回答反查证据链
当用户指出回答错误时,不要只记录最终文本。应按以下顺序检查。
1. 检查主张拆分
错误是否来自一句话包含多个未拆分命题?
“支持持久化并且不会丢数据”
可能实际包含:
支持写入
支持跨重启
支持故障恢复
支持零数据丢失
2. 检查证据覆盖范围
确认每个主张是否有:
direct support
partial support
derived support
如果只有“相关”,不能标记为“支持”。
3. 检查版本和作用域
记录:
回答目标版本
证据版本
运行时版本
部署模式
租户和权限
4. 检查冲突是否被吞掉
日志中应能看到:
{
"claim_id": "c7",
"candidate_evidence": ["e10", "e11"],
"conflict_detected": true,
"resolution": "latest_version",
"resolution_reason": "用户指定 v2.1"
}
如果只看到最终引用,看不到被放弃的冲突证据,后续无法解释 Agent 的决策。
5. 检查回答是否超出证据
可以做一个简单的后处理:
回答句子 → 原子主张
原子主张 → 证据映射
无映射的事实句 → 阻止发布或标记未验证
特别需要拦截:
所有、始终、保证、绝不会、唯一、零风险
这些词通常意味着回答引入了证据中不存在的强断言。
十四、引用质量的评估指标
引用评估不能只测“回答是否正确”,还要测“回答是否能被证明”。
14.1 证据覆盖率
它衡量回答中有多少需要验证的主张具备证据。
14.2 引用精确率
引用很多但大部分只“看起来相关”,精确率仍然很低。
14.3 主张完整性
它区别于覆盖率:只支持句子一半,不应算完整支持。
14.4 冲突召回率
构造带版本、时间和作用域冲突的测试集,测量:
如果冲突召回率低,系统会倾向于给出过度确定的答案。
14.5 可定位率
一个 URL 能打开,但无法定位到支持句,也不应被视为完整可验证。
十五、设计结论
Agent 知识引用的核心不是把链接附加到答案后面,而是让回答中的每个重要主张都拥有明确的证据路径:
主张
→ 证据片段
→ 来源
→ 版本、时间、作用域
→ 支持或冲突判断
→ 最终回答中的引用
其中最重要的工程边界有四个:
- 相关性不是支持性:检索排名不能代替证据验证;
- 引用不是来源列表:必须映射到具体主张和片段;
- 冲突不是噪声:无法消解时应保留冲突并限制回答;
- 持久化不是可验证性:会话状态可以被保存,但来源内容、版本和权限仍需单独管理。
OpenAI 的会话状态能力可以帮助 Agent 跨轮次传递上下文,Conversation 对象可以提供具有持久标识的会话状态;LangGraph 则通过 checkpointer 和 store 区分 thread 级图状态与跨 thread 的长期数据。(developers.openai.com)
但无论采用哪种框架,真正可审计的知识 Agent 都必须额外维护:
claims
evidence
sources
source_map
conflicts
validity_scope
verification_status
当证据足够时,回答应清晰地说明结论;当证据只支持一部分时,应缩小主张;当来源冲突或无法定位时,应拒答、请求更多上下文或升级人工。可验证回答的价值不在于让 Agent 显得更确定,而在于让用户能够判断:这句话为什么成立,它在什么条件下成立,以及什么时候不再成立。
系列导航与关联阅读
- 系列入口:Agent 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:Agentic RAG:检索决策、查询分解、重排、迭代和停止
- 下一篇:MCP 架构深解:Host、Client、Server、能力协商和生命周期
- 延伸:Agent 不确定性与拒答:证据不足、置信边界、升级和回退
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论