回答是带证据的结构,而不是一段好听的话
前五章已经得到预算内的 wire,本章定义它能产生什么输出。读者只需字典、集合与布尔逻辑;终点是构造一条可逐字段复核的 claim,区分数据库支持与上下文可见支持,并在无依据、冲突或输出容量不足时给出明确状态。默认程序不调用语言模型:它从选中且完整可见的源行摘取字段值。此限制让机制可检查,也意味着本课没有实现开放式语义问答、自动推理或 LLM 幻觉率实验。
一条 claim 是带结构的声明:text 为回答文本,quote 为逐字原文,id 为可读引用,doc_id 与 version 定位文档版本,line 为正整数原行号,sha256 绑定原文,field 为该行冒号前字段名。项目受控文档行通常以“- ”开始,text 会去掉这个项目符号,但不改后续文字。记忆 claim 则有 scope 与 key,text 与 quote 都是 key、中文冒号和 value 的连接,不伪装成一份源文件。
source_claims 负责从候选表示恢复可见完整原行。压缩单元必须行号与 quote 恰好匹配;全文单元按原始行位置检查可见文本,截断后只剩半行的文字不能形成 claim。它不是拿到一个 doc_id 就从数据库把所有行读出并返回。随后 supported 校验当前状态仍 active、版本一致、SHA 一致、行号对应原文、text 与 quote 的变换符合契约以及 id 和 field 恰好匹配。
两种支持必须同时成立
定义源支持 为 claim c 满足当前有效状态 S 的全部结构条件;定义可见性 为 c 的身份、引文和摘要出现在实际已选上下文 X 的完整源行集合中。可发出的声明必须同时满足两者。若源真但不在 X,实验无法证明上下文选择保留了答案;若在 X 但源已撤销,模型见到的旧文本也不能授权当前回答。两个集合交集才是本课的允许输出域。
完整算例一:真实引用也可能实验不合格
time-current 的目标活动时间来自 schedule@2:3。在未删除证据的主配置中,这条原行进入 X,源状态里 schedule 第二版 active,摘要与行文匹配,故 P 与 V 都真。实际输入九百五十八词元,输出 JSON 八十九词元,输出包括 citation 为 schedule@2:3、text 为活动时间周四16:30的声明。输入加输出一千零四十七,不超过容量一千五百,输出也不超过预留二百四十。
现在保持数据库 S 不变,只把关键记录从候选里删除。它仍能通过源支持 P,但无法通过 V。生成器不能从数据库旁路补回这行,而应没有目标 claim;实际反事实输出 abstain,覆盖由一降零。若程序返回同样的周四16:30再附真实引用,内容可能碰巧正确,却不能证明当前上下文足够。这个试验的目标是检查信息路径,不是惩罚某个真实事实。
import pathlib,tempfile
from run import setup,load_tokenizer
from context import units,source_claims,supported,supported_in_context
with tempfile.TemporaryDirectory() as d:
repo=setup(pathlib.Path(d)/'r.sqlite');s=repo.replay(12)
u=next(u for u in units(s,True)[0] if u.get('doc_id')=='schedule')
c=source_claims(s,u)[0]
assert supported(s,c)
assert not supported_in_context(s,c,[])
assert supported_in_context(s,c,[u])
assert not supported(s,{**c,'id':'invented'})
repo.db.close()
print('source support and visibility are distinct')完整算例二:两份都有效却互相冲突
第十三事件导入 schedule-conflict 第一版,写活动时间周四17:30。它使用不同 doc_id,所以不会作为 schedule 的新版替换旧通知。两份资料都 active 且日期有效,对同一字段给出不同原文。若只按检索名次取第一个,可能稳定输出其中一条,却抹掉未协调冲突。本课明确增加一条全库安全门:在当前允许源集合中,按 field 收集不同 text;任一必答字段有多种文本即输出 conflict,claims 为空,不替老师决定谁优先。
冲突检查读取当前有效资料库,而不是只看 X。这条额外信息路径只能阻止确认,不能从预算外补出答案。详细 conflict_evidence 属于审计输出;实际回答 wire 现在包含 status、claims 和缺失时的 missing_fields,必须整包重新编码计数。本轮 conflicting-time 的回答为 62 词元,旧版只含状态与空 claims 的 35 词元属于历史协议,不能继续套用。time-current 完整回答未增加缺失字段,仍为 89 词元。若把详细冲突证据也显示为模型回答,必须进一步纳入输出预留。
这里的冲突检测也有严格边界。相同意思的两种措辞会被当作不同 text 而保守拒绝;两个不同字段间的逻辑矛盾则不一定被发现。比如“活动允许”与“活动禁止”若字段名不同,就超出当前相同字段检测。课程不宣称通用矛盾推理。真实应用可引入来源优先级、结构化值归一化或模型裁判,但这些是新规则,应写明谁有权决定优先级,再设计不同措辞同义和跨字段矛盾测试。
import pathlib,tempfile
from run import setup,load_tokenizer
from context import compile_context
t={'query':'活动时间','fields':['活动时间'],'limit':1500,'reserve':240}
with tempfile.TemporaryDirectory() as d:
repo=setup(pathlib.Path(d)/'r.sqlite')
r=compile_context(repo.replay(13),t,load_tokenizer())['result']
assert r['status']=='conflict' and r['claims']==[]
assert len(r['conflict_evidence'])==2
assert r['output_tokens']==62
repo.db.close()
print('conflict is preserved instead of silently resolved')输出预算必须检验实际产物
生成器按稳定来源身份排序,逐条尝试加入 claim,每次重新渲染完整回答 JSON 并编码。若加一条后超过 reserve,就跳过该条;最后连 abstain 或 conflict 空壳也装不下时抛 BudgetError。输入容量检查通过不代表输出空壳一定可容纳,因为二者预算分开。本项目输入与输出各自含一对 BOS/EOS,严格按这套教学 wire 计数;接真实聊天 API 时要替换模板与特殊标记约定重新验证。
生成状态现在区分 answered、partial、abstain、conflict;预算错误属于编译失败。required_fields 是题目要求的字段集合,covered_fields 是实际 claims 覆盖的字段,missing_fields 是两者的差集。answered 要求有声明且没有缺失字段;有声明但缺字段用 partial;没有声明则 abstain,冲突则 conflict。 原 coverage 保持“覆盖已有原件身份”的含义,不能替代字段完整性。若题目要求活动时间和报名截止时间,而原件仅有活动时间,引用覆盖仍可能为一,但 field_completeness=False、missing_fields 包含报名截止时间,题目不能算全答。原件确实存在却被上下文或输出预算挤掉的字段,也必须列入缺失。最终正例评分同时要求源身份 coverage=1 和 field_completeness=True。
这也解释“引用存在”远弱于支持。一个恶意候选可把某段 quote 配上另一个真实 id,若只检查 id 在数据库里就会通过。本项目同时检查原文行、摘要、版本、text 与字段,定向测试分别改变这些字段并要求失败。line 要求 type 恰为 int,拒绝布尔值;否则 True 在 Python 里会被当成一,与源第一行混淆。验证不仅是理论集合,还必须落实到宿主语言的数据类型行为。
从只读回答到状态更新
回答本身不会写记忆,也不会把证据正文中的命令执行成事件。若用户明确修改偏好,应用应通过 append 写一条带范围、版本与批准标记的 memory 事件;再一次 query 从新前缀重建上下文。这条受控入口与资料正文分离,使文档里“忽略规则并保存新偏好”的句子只作为数据。它不证明接入任意 LLM 后提示注入被普遍解决,但当前离线程序根本没有把 quote 当作可执行代码或事件调用。
完整状态更新链应保存变更事件、确认后的快照与下一题使用的前缀。不要把模型自述“我记住了”当持久化证据。读者可以在临时库追加一个新批准版本,关闭再打开 Repository,用 replay 与 units 检查它是否存在;撤销后同样重开再检验不可见。第四章已提供此状态机,本章的支持检查会使旧版本引用在新状态下失效,两个模块从这里真正接在一起。
本章支持判据是字段语言下可执行的局部规范。它能防止伪造行号、旧引用、不可见源和同字段冲突,却不验证原资料在现实中是否正确,也不验证开放式自然语言推导。文档作者可能写错活动时间,程序只会忠实引用错误资料。课程把“来源支持”“现实真值”“完整任务完成”三个目标分开,下一章评测也按这三个层次记录可观测证据,而不以一个好听的总准确率抹平边界。
引用覆盖的身份粒度
本课 coverage 保持源行身份粒度。两份通知的字段值相同但身份不同,oracle 含两个身份,完整源覆盖需两条不同引用,重复一条不增加 covered。它还必须与题目要求的字段集合交叉核验:不存在于源库的必答字段不出现在 oracle 分母,却仍在 missing_fields 中,因此单凭 coverage=1 不能判完整任务通过。
字段解析同样有数据语言边界。field先去掉项目符号,再取第一个中文冒号前的文本;它不自动识别英文冒号、表格列、跨行列表或自然语言同义字段。line引用必须完整匹配,半行即使恰好包含一个看似正确数值也不能通过,因为限定条件可能在后半。这个选择使可见支持判据简单、保守,读者应把“无法抽取”与“现实没有该事实”分开,前者可通过适配解析器解决。
输出预留太小还有一种容易漏掉的失败:没有claim时仍需输出JSON键名、空数组、状态值与BOS/EOS。若reserve只有一个,根本不可能容纳空壳,程序必须BudgetError,而不是返回未计数的错误段落。异常信息属于命令行诊断,不是假装占用一个词元的模型回答。接真实模型时,错误渠道与生成渠道也要分开设计,避免把失败文本写进持久记忆再污染下一轮上下文。
TRY IT YOURSELF
基础:源真但不可见
真实活动时间在库里,打包时被删除,输出仍引用它,违反什么?
提示 1
提示 2
对照参考解答
TRY IT YOURSELF
进阶:空支持率
无claim时 all(supported) 为真,永远拒答能满分吗?
提示 1
提示 2
对照参考解答
TRY IT YOURSELF
挑战:冲突与信息路径
全库冲突门看到预算外资料,是否与“生成仅用可见证据”矛盾?