第 10 / 21 篇

第9章 把引用变成一次当前状态复核

搜索结果中的原文并不天然可信。它可能来自很早以前的缓存,也可能被调用者改写;即使原文没变,当前用户也可能已经失去权限。因此,引用的关键动作发生在组装回答时,而不是只在检索成功时加一个来源链接。

命中只是待核查对象

随书命中携带租户、用户、文档编号、内容版本、ACL 版本、文档摘要、片段主键、片段摘要与坐标。answer() 首先验证可信上下文,再要求命中的租户和用户与本次调用一致。随后重新读取文档与 ACL,拒绝删除、换版、撤权或摘要变化的对象。

代码继续从当前片段表读取内容,检查片段属于该文档、摘要符合命中记录,并确认片段文本与当前正文切片相同。最终使用的是数据库片段文本,不使用调用者传来的 hit["text"]。这避免把“搜索结果里有原文”误当成可以直接转发原文的授权。

hits = index.search("退款政策", Context("acme", "alice"))
# 两次调用之间,文档或权限都可能改变。
result = index.answer(hits, Context("acme", "alice"))

这些检查在一个读事务中完成,使同一次摘录看到一致的数据库状态。它没有跨越应用外部的网络发送过程:读取完成以后发生的撤权,不能收回已经交付给用户的内容,也不能自动擦除日志或屏幕截图。

摘录式回答的真实能力

有可用证据时,函数返回 state="extractive"、non_llm=True,以及原文组成的答案和结构化证据。无可用证据时返回 state="abstain",提示“没有当前权限下可核查的证据”。这里没有调用语言模型,也没有把不同片段改写成一个新结论。

这个实现核查的是证据完整性和当前可见性,不是任意语义蕴含。原文确实存在,也可能不回答用户的全部问题;标题碰巧相近的片段,也可能在业务上无关。如果要生成综合回答,仍需要单独检查每项断言由哪段证据支持,以及引用是否真的支撑它。

第一次演示检索住宿报销,得到文档 travel 第一个片段,坐标零到六十三。摘录同时包含住宿五百元与餐费八十元,其 SHA-256 为 6c0b230598d5e8b3bf87b66874730c931bf32b7584dff3f0d1a4d6590885b808。读者可以直接对报告中的 quote 重新编码计算,或在规范正文上重建相同切片。

一条过期证据不等于全部无证据

回答函数会跳过失效命中,继续检查后面的可用候选。这意味着某篇文档删除后,若列表里还有其他当前可见证据,整体回答仍可能是摘录状态。不能把“目标文档不可引用”写成“任何查询都应完全拒答”。

删除演示因此先保留目标文档的命中,再单独验证这些命中全部失效,同时检查新搜索不再返回该文档。这样的断言精确对应删除承诺,避免因为另一个合法候选存在而误判系统错误。

引用对象应当是一张等待复核的凭据,不能成为绕过权限的通行证。接下来的三章分别改变正文、删除状态和 ACL,观察同一条凭据如何失效。