# 一条回答怎样证明有出处

Python 3.11+、标准库、完全离线。这里真实实现BM25、RRF、版本/删除/ACL及指标；dense、rerank和选择策略是确定性教学替身，不能据此报告真实embedding、LLM或生产质量。课文在 `/learn/rag-systems/case.html`。

## 下载后第一次运行

解压ZIP，进入包含raglab.py的目录：

```bash
cd production-rag-lab
python3 -B raglab.py qualify
python3 -B -m unittest discover -s tests -v
```

Windows可用`py -3`替代`python3`。不需要pip或账号。输出写入`artifacts/`；如需隔离：

```bash
python3 -B raglab.py --artifacts ./my-run qualify
```

首次应看到`pass: true`，四组实验均通过；18条测试通过。结果只证明这份合成数据下的机制。先写下预测：合法引用ID下，把45天改成900天，哪项指标应下降？

## 按知识顺序运行

```bash
python3 -B raglab.py ingest
python3 -B raglab.py retrieve
python3 -B raglab.py ground
python3 -B raglab.py agent
```

1. `01-ingestion/document-lineage.csv`与`delete-reconciliation.json`：追踪旧版→新版、重复事件、乱序ACL与删除。普通员工不能得到安全管理员日志；删除资源不再active。
2. `02-retrieval/bm25-results.jsonl`：先找E42的实际候选。再读`ablation-summary.json`。当前12题中10题有相关集；BM25 Recall@5=1、MRR=0.95、nDCG@5约0.963093。不要把“都找到了”误读成“排序同样好”。dense fixture只是同义词/token cosine。
3. `03-grounding/claim-citation-audit.jsonl`：逐claim查看实际span。当前有12项required facts，citation_precision与claim_coverage均为1；`mutation-results.json`的10个反例全部caught。数字变900、条件反转、错span、遗漏required claim各使对应指标下降；一个错误的precision为11/12≈0.916667。
4. `04-agentic/agent-runs.jsonl`：读停止原因与检索次数。所有策略同样使用top_k=3、最大4次预算；单次策略只使用一次预算。当前小集三种策略成功率均1，iterative平均调用约1.166667，另外两种为1。这个结果支持保留简单方案，不能宣传循环一定更好。

## 有限schema如何验事实

`FACT_PATTERNS`从真实文档文本抽取限定事实及字符span：退款天数、日志天数、境外审批、住宿上限、E42含义等。运行时从用户词句解析requested_fields，在授权context中寻找事实；没有事实则拒答。未支持的提问要扩展输入/事实合同或交回澄清，不能靠猜测过关。

候选输出不需要supports。grader重新解析claim.text，与引用文档的实际数值/条件/span比对，核对当前版本、主体权限及独立context manifest。它只接受这里明确列出的句式和少量空白/标点差异，不等于通用语义蕴含裁判。任意同义改写需要单独的标注和经过校准的语义评分器；本实验会拒绝无法识别的句式。

`evaluate_answers(..., context_manifest=...)`接收单独保存的可信检索记录，不信任答案自报context_ids。默认评测会按冻结检索合同重放；接真实适配器时必须保存并传入本次实际context。

qrels与expected_fields只供结束后的评分。`runtime_request`剔除slice、refusal_expected、expected_fields；`agent_run`没有qrels参数。requested_fields是从用户问题解析出的服务合同，不是gold evidence。`evaluate_agent_run`在运行结束后计算gold coverage/success。改变gold可能改变评分，但不能改变工具调用或停止原因。

## 故障开关与红→绿

`ground`自动运行10个mutations。再在自己副本里：

1. 暂时把`evaluate_answers`中“parsed值等于source值”核对移除，运行数字篡改测试。
2. 测试应变红；解释合法ID为什么不等于支持关系。恢复核对，再复跑全部测试。
3. 暂时删除独立context membership检查。保留合法来源却给grader空context manifest，测试应变红。

```bash
python3 -B -m unittest tests.test_raglab.ProductionRagLabTests.test_candidate_self_reported_support_cannot_validate_changed_number -v
```

若模块式导入在你的环境找不到tests包，使用通用discover命令。不要改测试期望让故障变绿。

## 陌生数据任务与验收

在自己副本把ACME当前退款文档v4的45改成52，并记录新的hash；query不变。先预测：标准运行应读到52，旧snapshot不能支持当前回答；再运行qualify。然后把候选答案改成45，必须判unsupported。保留改动前后manifest、两条claim audit和数字mutation。

第二题：同tenant、同epoch、同query，主管与员工权限不同。画出缓存key并写一条失败测试；key必须包含principal_scope，命中后仍检查ACL/tombstone。删除时仅换index alias不能自动消掉答案缓存。这个包实现检索前ACL，但没有共享答案缓存；缓存题交测试设计，不能写成运行通过。

参考答案：来源从45变52会产生新snapshot/hash；生成器从实际span抽到52。旧答案45即使沿用合法当前ID，也与source value不等，precision/coverage下降。gold标签只在离线评分端；清空qrels.relevant不得改变agent_run返回的events或stop_reason。缓存需要主体授权范围，而不是只增一个租户epoch。

## 排错和进一步阅读

- SyntaxError：先核对Python 3.11+与解压文件完整性。
- 数据文件找不到：确认data目录与raglab.py同级；文件路径从脚本位置计算。
- 基础与mutation都100%且caught=false：检查grader是否只验ID或信任candidate.supports。
- 想接真实模型：替换adapter，保留身份、snapshot、实际context、独立qrels与逐claim评测。不能把fixture表现写成客户结果。

[Elastic RRF官方公式](https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion) · [Azure用户/组安全过滤](https://learn.microsoft.com/en-us/azure/search/search-security-trimming-for-azure-search)。先手算一组排名，再对照代码；先写出同租户不同主体，再读过滤文档。
