项目:离线可引用问答
项目目标
运行一条完全离线、可追踪、可故障注入的教学管线:
稳定 query fixture → 可信执行上下文 → tenant/ACL/半开有效期过滤 → 字符召回
→ topic 规则重排 → canonical 去重与预算
→ 抽取式 claim → citation/revision 校验 → 公共响应
↘ 受保护审计轨迹 → 分层 eval artifact项目解决的是“RAG 的每层契约如何被验证”,不是“如何用字符规则做高质量语义搜索”。它故意不使用 Embedding、ANN、LLM、网络或 API key。
项目文件
| 文件 | 内容 |
|---|---|
| rag-fixture.json | 10 篇版本化文档、8 个 query、风险切片与 eval policy |
| offline_cited_qa.py | 严格 JSON、公共/审计双 schema、证据管线、评测报告与 CLI |
| test_offline_cited_qa.py | 73 项 schema、UTF-8/数值资源边界、非干扰、权限、审计绑定、故障、grounding、评测与 CLI 测试 |
环境
- Windows 11 与 PowerShell 7;
- Python 3.11 已验证,代码只依赖标准库;
- 不需要安装依赖或提供密钥;
- 通过
-B和PYTHONDONTWRITEBYTECODE避免生成__pycache__。
若你需要隔离 Python,可把 venv 建在 vault 外,例如 $env:TEMP\rag-lab-venv;不要把 .venv、缓存或真实凭据放进知识库。
第一步:运行全部案例
从项目根目录(同时包含 docs/ 与 .website/)执行:
$env:PYTHONDONTWRITEBYTECODE = '1' # 禁止写入 __pycache__,使本地教学项目保持干净。
$env:PYTHONIOENCODING = 'utf-8' # 强制终端 JSON 使用 UTF-8,避免中文结果显示异常。
$script = '.\docs\RAG\examples\offline_cited_qa.py' # 保存主脚本路径,后续阶段复用同一可执行文件。
$fixture = '.\docs\RAG\examples\rag-fixture.json' # 保存被严格加载和哈希绑定的教学 fixture 路径。
python -B -W error $script --fixture $fixture demo # 一次运行全部内置场景,观察不同业务状态与引用边界。预期业务状态:
| Query ID | 场景 | 状态 |
|---|---|---|
Q-refund | 当前退款规则,旧版已过期 | answered |
Q-duplicate | 重复扣款处理 | answered |
Q-phone-guest | 普通用户询问内部电话 | insufficient_evidence,且不泄露 S3 |
Q-phone-oncall | 有 oncall 组权限 | answered |
Q-conflict | 两个当前住宿标准冲突 | conflict |
Q-mars | 语料无答案 | insufficient_evidence |
Q-untrusted-content | 有权读取但含伪指令的外部内容 | answered,控制字段不随正文改变 |
Q-order-live | 实时订单状态 | tool_required |
第二步:比较公共响应与受保护审计轨迹
公共命令:
python -B -W error $script --fixture $fixture ask --query-id Q-refund # 输出退款规则的公共 answered 投影。公共 CLI 的教学 envelope 只包含 mode 和 public result:后者只含稳定状态、由 claims 渲染的答案、授权后引用和教学 trace_id,不包含候选、过滤计数、内部版本或调用方传入的 fixture 本机路径。本例 ID 由 pipeline revision 与 query ID 确定性计算,只为测试复算,不是生产随机 opaque ID,也不能用作身份或授权。需要本地教学诊断时显式运行:
python -B -W error $script --fixture $fixture inspect --query-id Q-refund --operator-view # 在本地教学中显式请求同一案例的受保护审计轨迹。--operator-view 只是防止误把内部结构当公共响应,不提供真实身份认证。检查受保护轨迹:
- 本项目的固定 enum 是
visibility=privileged_audit,中文统一称“受保护审计”;它与第 9 课的protected_audit是两个不同 schema; - 轨迹绑定
authorization_revision; filter_summary、候选和 dropped reason 只存在于内部轨迹;retrieved、reranked、selected分开保存;- filter summary、rank/score、selected chars/context chars、阶段转换及 degraded/fallback 会按可信输入重算;
dropped能看到 canonical duplicate 或预算原因;citations同时记录 document、fact 和 source revision,claim 中每条引用都必须逐字支持该 claim;trace_id和公共响应会从可信 runtime 输入及各阶段结果重新计算,不能在 audit envelope 中任意替换;- runtime validator 不读取离线
expected_*oracle。
字符/bigram 分数只在本 query 内排序,不是概率,也不能与真实向量分数比较。
第三步:比较权限
python -B $script --fixture $fixture ask --query-id Q-phone-guest # 用普通 public 组执行,同文本不应泄露内部电话。
python -B $script --fixture $fixture ask --query-id Q-phone-oncall # 用包含 oncall 的可信 fixture 身份执行,允许得到授权事实。两次 query 文本相同,可信身份层解析出的 subject_groups 不同;公共访问也显式携带 public 组,不以空组或 magic bypass 表示。观察普通用户公共结果中:
- 没有内部电话;
- 没有
S3; - 没有私有标题;
- 没有任何过滤原因或计数。
内部 inspect 可以区分原因;公共响应对新增未授权文档保持逐字节不变,避免通过计数探测私有资源。
第四步:观察冲突与工具路由
python -B $script --fixture $fixture ask --query-id Q-conflict # 查看有效来源互相矛盾时的 conflict 状态。
python -B $script --fixture $fixture ask --query-id Q-order-live # 查看实时问题被路由为 tool_required,而非伪装成知识答案。Q-conflict 保留两个互相矛盾的有效 fact,不取平均、不凭 authority 随意选一个。Q-order-live 在检索前短路,因为知识快照不能回答当前订单状态。
第五步:模拟依赖故障
python -B $script --fixture $fixture inspect --query-id Q-refund --failure retrieval_error --operator-view # 注入检索失败,观察 dependency_unavailable 与审计原因。
python -B $script --fixture $fixture inspect --query-id Q-refund --failure reranker_error --operator-view # 注入重排失败,观察是否复用相同安全候选集。
python -B $script --fixture $fixture inspect --query-id Q-refund --failure generation_error --operator-view # 注入生成失败,观察是否拒绝输出未经验证 claim。| 故障 | 行为 |
|---|---|
| retrieval_error | 停止并返回 dependency_unavailable |
| reranker_error | 在相同安全候选集上保留 retrieval 顺序,标记 degraded |
| generation_error | 保留检索 trace,但不输出未经验证 claim |
第六步:运行四种解释器模式回归
$env:PYTHONDONTWRITEBYTECODE = '1' # 不让四轮测试留下字节码缓存。
$env:PYTHONIOENCODING = 'utf-8' # 让失败信息和 JSON 断言的中文输出保持 UTF-8。
$tests = '.\docs\RAG\examples' # 保存 unittest discovery 的测试目录。
python -B -m unittest discover -s $tests -p 'test_offline_cited_qa.py' -v # 正常模式详细运行 73 项离线问答测试。
python -O -B -m unittest discover -s $tests -p 'test_offline_cited_qa.py' # 优化模式复跑,检验不依赖裸 assert。
python -B -W error -m unittest discover -s $tests -p 'test_offline_cited_qa.py' # 严格 warning 模式复跑,避免忽略运行期告警。
python -O -B -W error -m unittest discover -s $tests -p 'test_offline_cited_qa.py' # 同时覆盖优化与严格 warning 的组合模式。-O 会移除 Python 的裸 assert。四种模式都通过,说明关键校验没有依赖会被优化掉的断言,组合模式也没有忽略 warning;它仍不等于证明所有生产风险已覆盖。
第七步:生成分层评测证据
python -B -W error $script --fixture $fixture evaluate # 生成正常路径的分层评测 artifact,预期 PASS。
python -B -W error $script --fixture $fixture evaluate --failure retrieval_error # 注入检索故障,预期得到阻断发布的 BLOCK artifact。正常报告为 PASS、退出码 0;故障注入为 BLOCK、退出码 1。artifact 同时报告 case/critical pass rate、status accuracy、retrieval/context/citation fact recall、non-disclosure 违规数、pipeline revisions、fixture/evidence 完整 SHA-256。当前按 fact 计数的统计与严格审计验证由 offline-rag-harness-v3 标识;改变指标或验证语义时必须继续升版。case 只输出非敏感 failure code,不回显私有 canary。
Fixture 契约
严格加载器会在解析前限制 fixture 为 2,000,000 UTF-8 bytes、JSON 容器深度为 64,并限制文档、query、每文档 fact、列表、阶段候选与上下文字符预算。JSON 转义出的孤立 surrogate 会在进入 hash/检索前拒绝;需要转换为有限数值的超大整数也会成为 schema 错误,而不是泄漏 OverflowError。读取失败只报告错误类型,不回显本机路径。这些是教学防资源耗尽边界,不是吞吐压测结论;生产系统仍需在入口、队列、解析器和各检索阶段分别设置预算。
文档
每篇文档包含:
- stable/canonical ID、title、text;
- tenant、ACL、status、有效期;
- source revision 与教学 authority;
- 一个或多个 fact:topic、逐字 statement、value 与 unit。
statement 必须逐字存在于 text,让引用支持可确定验证。真实系统通常使用 source span,而不是预先结构化 fact。
Query
每个 query case 包含两类字段:
- 运行时字段:文本、tenant、可信 groups、
authorization_revision、as_of、route 与稳定 topic; - 离线 oracle:slice、critical、expected status/facts、forbidden document IDs 与私有正文 canary。
代码用 _runtime_query() 白名单投影运行时字段,期望字段不传给回答器。effective_from 含起点、effective_to 不含终点,即 [from, to);若业务字段是“结束日期当天有效”,应在摄取时转换为下一日 exclusive date。
两个字段的真实边界
本项目把
subject_groups视为 host 已可信解析的执行上下文;authorization_revision只是随 trace 绑定的 opaque 教学标签,代码没有调用 authorization resolver,也不验证该 revision 对 groups、tenant 或资源策略的真实含义。任意非空标签仍可能通过 fixture 校验,因此本项目不能证明身份/策略快照兼容性。Citation 也只绑定 fixture 内手写的
document_id + fact_id + source_revision,并验证 fact statement 位于当前document.text;它没有 raw/canonical hash、精确 source span、parse/chunk/index generation 或删除墓碑链。要验证这些上游合同,继续完成项目:从来源到引用的离线证据链。
实践任务
任务 A:增加“归档/非发布文档”
新增一个 status=archived 的高相似文档和 query。要求:
- Fixture schema 仍通过;
- 文档不进入 retrieved;
- 公共响应及引用不出现其 document ID;
- 新增测试覆盖正常与
-O。
这只证明 lifecycle hard filter,不证明删除。真正的删除要在 版本、删除与权限 中验证 delete event、source sequence、tombstone、投影/缓存传播、防旧快照复活和保留/物理清除证据。
任务 B:预算压缩
把 max_context_chars 调小,观察 dropped.reason。回答被拒绝时,判断是 retrieval miss 还是 context budget 导致。
任务 C:从规则组件替换为真实组件
按顺序替换:
- 字符召回 → BM25,并保留旧版对照;
- 再增加 Embedding/dense 通道与融合;
- topic rule → reranker;
- 抽取生成 → LLM 结构化输出;
- 逐步增加模型 judge,但保留确定性权限和 citation ID 检查。
- 接入第 9 课的 source-span/generation manifest;不要把手写
source_revision标签当作内容绑定证明。
每次只替换一层,锁定 revision 并运行同一测试集。
任务 D:输入与公共输出边界红队
复制 fixture 后分别做两项最小篡改:在任意字符串中写入 JSON 转义的孤立 surrogate,以及将一个 min_* 阈值改为无法转换为有限浮点数的超大整数。两次 CLI 都应以退出码 2 返回受控契约错误,而不是 traceback。随后用绝对 --fixture 路径运行 ask,确认 public stdout 不回显该路径、候选或审计字段。这个练习只验证本地输入/投影边界,不替代网关限流、日志脱敏或真实身份认证。
生产化迁移清单
- 身份、tenant 与 group 来自可信认证层;
- Parser/chunk/embedding/index 都有 revision;
- 删除、过期和权限变更传播到缓存与副本;
- Sparse/dense 分开观测,融合有 qrels 实验;
- Reranker 输出验证未知、重复、缺失与非有限值;
- Token 预算使用真实 tokenizer;
- Evidence 使用 source/span/revision;
- LLM 输出有 schema、超时、限流和失败状态;
- Claim support 有人工校准,关键字段有确定性检查;
- 外部文档视为不可信数据,工具最小权限;
- 日志脱敏、限权、有保留期;
- 离线、影子、小流量与回滚门禁齐全。
项目验收
- 八个 query 的正常状态符合 fixture;
- 旧版、跨 tenant 与 ACL 文档先过滤后评分;
- Canonical 镜像不重复占用上下文;
- 冲突证据不被合成为第三个答案;
- 实时问题不走知识检索;
- Answer 只能由已验证 claims 渲染,claim/citation 能回溯到 selected context;
- 公共响应不含 filter/candidate trace,未授权语料变化不改变公共输出;
- 三类依赖故障有确定、安全、可观测行为;
- 分层 eval 正常
PASS、故障注入BLOCK,完整 SHA-256 可复算; - 73 项测试在正常、
-O、warnings-as-errors 与组合模式通过; - 没有网络、密钥、缓存、模型或大型数据依赖。
自测
- 为什么 fixture 中的 expected 字段不能作为线上回答输入?
- 为什么本项目能验证 citation provenance,却不能证明真实 LLM 的所有语义忠实性?
- 为什么公共响应连聚合过滤计数也不应显示?
- Reranker fallback 为什么不能重新扩大候选权限范围?
- 把字符检索换成向量检索后,哪些契约不应改变?
返回 RAG 目录。
参考资料
- Lewis et al., Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks
- Es et al., RAGAS
- OWASP GenAI Security Project: LLM01:2025 Prompt Injection
- OWASP GenAI Security Project: LLM08:2025 Vector and Embedding Weaknesses
- OWASP Cheat Sheet Series: RAG Security
来源获取日期:2026-07-22。