第1章 从回答演示走向检索契约
假设差旅制度规定酒店每晚五百元,系统也准确回答了五百元。这个成功仍不完整。如果回答引用的是另一租户的制度,或者该制度已经废止,文字再准确也没有用。检索工程的第一项工作,是说明“正确”依赖哪些状态,而不是只看答案中的数字。
一条完整的证据链
随书实现把链路拆为源文档、规范化投影、片段、召回结果和回答证据。源文档说明原始内容从哪里来;投影规定不同输入格式如何成为统一文本;片段保存投影中的起止位置与摘要;召回结果携带文档版本和权限版本;回答阶段重新读取当前数据库,确认这条链仍然成立。
例如 acme/travel 的第一版内容经过 NFC 与换行规范化后,产生一个文档摘要。片段记录 [start,end) 和自己的 SHA-256。查询中的 Context("acme", "alice") 决定允许参与检索的集合。摘录时,代码再次比对文档版本、文档摘要、ACL 版本和片段内容。任何一步失效,都不能继续沿用旧证据。
这些字段各自解决不同的问题。摘要标识内容,不能代表授权;文档版本说明更新顺序,不能替代内容校验;ACL 版本说明策略变化,不能替代用户身份认证。把三个概念揉成一个更新时间字段,会使重放事件、撤权与内容变更难以区分。
把承诺写成可失败的行为
本书的核心承诺可以逐条运行:没有可信查询上下文时拒绝检索;不同租户的同名文档不能串用;更新退款政策后旧引用失效;Alice 被撤权后,旧命中不能继续组装回答;删除文档后,旧内容事件不能让它自动复活。
这些承诺比“安全可靠”更窄,也更有检查价值。tests/test_retrieval.py 中每条测试都构造一个具体状态变化,再观察真实 SQLite 数据或返回值。例如撤权测试先保留搜索结果,再修改 ACL,然后把保留结果交给回答函数。它覆盖了两次调用之间的状态变化,而非只检查一个静态权限函数。
工程上还应分开两类错误。相关性错误是可见文档中没有找到好证据;权限错误是不可见文档进入结果。扩大候选池可能改善前者,却不能接受后者。评估报告因此单列拒答与权限违规,不把所有失败压缩成一个平均分。
小实现的责任边界
实验提供 Index 类和可信上下文对象,没有 HTTP 服务、登录系统或身份提供方。调用方负责把已经认证的身份转换为上下文,不能直接相信浏览器提交的租户和用户名。status() 是维护接口,会返回文档内部状态,也不能直接公开给终端用户。
向量检索扫描当前可见片段,适合观察行为,不是大规模近邻索引。全文检索使用共享 FTS5 表,虽然结果受到租户和 ACL 限制,底层词频统计仍然共享。书中验证结果隔离,没有审计严格的统计非干扰或时间侧信道。
读完这一章,应能为自己的系统画出一条证据链,并给每一段写一个能实际失败的断言。下一章先固定输入数据,因为没有稳定来源,后面的版本与评估都无法解释。