# RAG 评测实战（四）：平均分涨了，为什么仍然不能发布？

这是一个零依赖、确定性的离线发布门禁演示。接续前三篇的证据、切块和排序问题，本篇把视角移到“同一批问题，候选版本究竟改善了哪里，又损坏了哪里”。

**全部文档、回答、模拟评审分数、参考标签、权限元数据、数据角色和候选对照均为手工合成。脚本真正执行校验、阈值选择、配对比较和门禁；没有运行检索器、大模型、真实评审员、身份认证、ACL 服务或生产部署。** `referenceLabel` 是作者指定的布尔参考标签，不是专家真值；`simulatedJudgeScore` 不是模型实际打分。本例不能证明真实评审校准、统计显著性、泛化效果或安全性。

`clean-contrast` 与 `unsafe-uplift` 同时编写，供机制对照。它不是看完留出结果后修复出来的版本。`holdout-role` 只表示流程里的数据角色，不表示未见过的独立测试集。

## 下载与运行

把 `evaluate.mjs` 与 `fixture.json` 下载到同一目录，先审阅代码，用 Node.js 18+ 运行：

```sh
node evaluate.mjs fixture.json
node evaluate.mjs fixture.json > my-results.json
```

再下载 `test.mjs` 和发布的 `results.json`，四个文件同目录：

```sh
node test.mjs
```

仓库内使用同一套公开测试：

```sh
node scripts/tests/test-rag-release.mjs
```

85 项测试包含独立手算断言、坏输入、配对/角色泄漏检查、权限元数据反例、阈值边界、输出快照，以及复制四个文件到临时目录后执行 CLI 和完整测试。测试不依赖仓库包、网络、密钥或隐藏模块，也不覆盖重写 `results.json`。

### 报告模式与门禁模式的退出码不同

默认命令是**生成整份教学报告**，合法输入返回 0，即使报告里包含被拦下的候选。需要判断一个候选时，必须显式指定：

```sh
node evaluate.mjs fixture.json --candidate unsafe-uplift
# 输出该候选的报告，退出码 2：BLOCK

node evaluate.mjs fixture.json --candidate clean-contrast
# 输出该候选的报告，退出码 0：PASS_DEMO_POLICY
```

- 0：报告成功；指定候选时表示它通过本例策略
- 2：指定候选被门禁拦下
- 1：参数、JSON、版本、数据结构或配对无效；stderr 解释错误，stdout 不输出可用报告

`PASS_DEMO_POLICY` 只是这个教学策略的结果，不是部署许可。请勿把默认报告命令的退出码当作发布门禁，也不要使用忽略退出码的 shell 组合。`--candidate` 仍然先验证整份 fixture；其他候选存在坏记录时也会失败关闭。

## 一个完整的发布故事

同一个合成服务知识库有 8 道发布比较题。6 道放在已知故障回放角色，2 道放在留出角色。三组回答和分数已经写入文件；不是程序生成或调用模型获得的。

| 题目 | 角色 | 关键题 | 基线 | unsafe-uplift | clean-contrast |
| --- | --- | --- | ---: | ---: | ---: |
| r-exact-code | replay | 是 | 90 | 60 | 95 |
| r-two-facts | replay | 否 | 40 | 95 | 90 |
| r-current-version | replay | 否 | 50 | 95 | 90 |
| r-no-answer | replay | 是 | 90 | 85 | 95 |
| r-tenant-boundary | replay | 是 | 90 | 98 | 92 |
| r-synonym | replay | 否 | 50 | 95 | 90 |
| h-backup-window | holdout-role | 否 | 60 | 90 | 90 |
| h-cache-boundary | holdout-role | 是 | 80 | 85 | 90 |
| 合计 | 8 题 |  | 550 | 703 | 732 |
| 等权题均值 | 分母 8 |  | 68.75 | 87.875 | 91.5 |

这里的分数是 0–100 的合成回答质量分，不是前几篇的 Evidence Recall，不能跨篇直接比较。8 题都进入本篇均值，包括需拒答题和权限边界题；它们同样有模拟回答评分。每题权重相同，不按流量或事实数量加权。表中的变化单位是“分”，不是百分比收益。

`unsafe-uplift` 提升 19.125 分，6 题上升、2 题下降，但出现三个阻断信号：

1. `r-exact-code` 从 90 掉到 60；回答把“仅重试幂等请求”变成“任何请求都重试”。`r-no-answer` 从 90 掉到 85。这两道事先标记的关键题发生逐题回退
2. `r-tenant-boundary` 的模拟质量分反而从 90 涨到 98，但实际记录的上下文含 `tenant-b-private`，不在该题的允许列表里。回答仅引用 `tenant-a-public`，因此只查引用会漏掉这条上下文违例
3. `r-no-answer` 的题目元数据要求 `abstain`，候选却声明 `responseMode: answer`，并给出材料中不存在的“90 天”

独立的 `clean-contrast` 提升 22.75 分，8 题全上升，通过所选教学策略。它的通过不是“已解决所有 RAG 风险”的证据。

## 四种数据角色，四种用途

全套 18 个样例身份来自 2 + 8 + 6 + 2。每个身份有手工指定的 `familyId`。

| 角色 | 数量 | 本程序如何使用 | 不该怎么用 |
| --- | ---: | --- | --- |
| development | 2 | 校验身份、字段和分组，保留设计量表的示例；不参与数值拟合或发布均值 | 不能把它的贴合度当泛化表现 |
| calibration | 8 | 仅在预先给定的阈值网格和代价下选择阈值 | 不能一边看发布结果一边重新选阈值 |
| replay | 6 | 配对回放已知的条件丢失、漏答、无依据作答和权限问题 | 通过历史题不代表覆盖未来问题 |
| holdout-role | 2 | 阈值选好后才进入发布比较，并单独报告分母与均值 | 本合成样例并非真正未参与设计的留出集 |

程序拒绝跨角色重复的 `id` 或 `familyId`，也拒绝重复记录、缺少某个发布角色或只有单一类别的校准集。这只能发现**所声明身份/分组的冲突**，不会识别语义近重复、作者记住答案、先看结果再调规则或虚假分组。

`fitThreshold(calibration, policy)` 接口只接收校准记录和固定策略。测试用会抛错的 getter 阻止它读取 development、queries、baseline 和 candidates；另有测试重写开发标签、基线与候选分数，要求校准结果不变。接口隔离不能洗掉教学数据与规则共同设计的事实。

本演示将 replay 与 holdout-role 一起纳入事先声明的 8 题发布均值，并保留分别的结果。unsafe-uplift 的 replay 为 410/6 → 528/6，holdout-role 为 140/2 → 175/2；后者增益为 17.5 分。这是教学策略的组合方式，不是建议把实际回放集与真实留出集任意混合成一个分数。

## 模拟评审校准：分数高，也可能放过错误答案

校准集只包含作者指定的 8 组模拟分数和布尔参考标签。四个 `true` 分数为 95、82、74、58，四个 `false` 分数为 88、65、41、20。`true` 表示这条合成参考分配把答案标为可接受；不是经过真实人工复核的结论。

固定候选阈值为 50、70、90，分数 **大于或等于** 阈值就模拟接受。教学代价为：

```text
weightedError = 2 × falseAccept + 1 × falseReject
```

`falseAccept`：参考标签为 false，但模拟评审接受；`falseReject`：参考标签为 true，但模拟评审拒绝。选择加权错误最少的阈值；同分固定选择较小的阈值。这个取舍是为了展示错误代价与平局规则，**不是生产推荐、不是通用阈值，也不是概率校准**。

| 阈值 | trueAccept | falseAccept | trueReject | falseReject | 加权错误 | 一致率 |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 50 | 4 | 2 | 2 | 0 | 4 | 6/8 |
| 70 | 3 | 1 | 3 | 1 | 3 | 6/8 |
| 90 | 1 | 0 | 4 | 3 | 3 | 5/8 |

70 与 90 都是 3；按照写在策略里的规则选择 70。选择后仍留下：

- `cal-fluent-wrong`：把“所有备份全球复制”标为错误，却赋予 88 分，被阈值接受
- `cal-terse`：简短但足够的“8443”标为正确，却只有 58 分，被阈值拒绝

阈值 70 的 acceptPrecision = 3/(3+1)，acceptRecall = 3/(3+1)，agreement = (3+3)/8，三者在本例恰巧都是 0.75，定义并不相同。一致率分母是全部 8 条校准记录，不是 8 道发布比较题；两组数量相等只是巧合。阈值 50 和 70 的一致率相同，但加权错误不同。若没有预测接受项，precision 输出 `null`，不伪造为 0 或 1；无正类或无负类的校准输入则直接报错。

发布比较里，阈值 70 接受基线的 4/8、unsafe-uplift 的 7/8、clean-contrast 的 8/8。**接受率上升仍没有消除三个独立阻断信号。** 这 8 道发布题没有额外的真实标注，也不报告真实准确率。

## 五道门：每一道都通过，才输出 PASS_DEMO_POLICY

本例策略明确写入 `fixture.json`，程序也完整打印出来：

| 门禁 | 教学规则 | unsafe-uplift | clean-contrast |
| --- | --- | --- | --- |
| aggregate-judge-uplift | 8 题平均模拟分数至少比基线高 5 分 | 通过，+19.125 | 通过，+22.75 |
| paired-critical-regressions | 关键题不允许掉分，maximumCriticalDrop=0 | 失败，−30、−5 | 通过 |
| calibrated-acceptance-rate | 固定校准阈值下，接受率不得比基线下降 | 通过，+3/8 | 通过，+4/8 |
| context-and-citation-acl | 候选上下文和引用都不得出现允许列表外的文档 | 失败，Tenant B 上下文 | 通过 |
| required-abstention | 事先要求拒答的题必须声明 abstain | 失败，无依据仍作答 | 通过 |

任何一道失败即 BLOCK，不用更多普通题的分数抵消。ACL 与要求拒答是**候选的绝对零违例门**，并非只要“没有比基线更差”就通过；基线有同样违例也不能为候选开脱。输出仍保留基线信号供诊断。

配对以 `caseId` 连接，不依赖记录顺序。关键题在 `queries` 里事先声明，不能在看到失败之后才解释哪些题重要。输出列出逐题基线分、候选分、差值、两边的阈值接受情况和硬信号，并分别给出各角色的分母。

均值门比较未舍入的整数总分；接受率门将配置数值的最短十进制形式转换成分数，再用 BigInt 交叉相乘。这样 100 题增加 7 个接受，在阈值 0.07 处会通过，增加 6 个则失败；不会被 `0.07 * 100` 的二进制浮点舍入误拦。JSON 中保留 12 位的小数只用于展示，不参与门禁判断。

此策略**允许非关键题回退**，只要其余条件成立。`unexpectedAbstention` 也只作诊断，不是第六道隐藏门禁。本例测试明确展示了这两个边界；生产策略若需要更多切片底线、非关键题损失预算或误拒答门，应按风险单独设计，不能暗中给本例结果加上它没有检查的保证。

## 失败关闭与可信输入边界

校验器拒绝以下输入，避免“漏掉失败题后均值更高”：

- 基线或任意候选缺少题、出现重复/未知题号，或者用校准题冒充发布题
- 分数缺失、null、字符串、NaN、Infinity、非整数或超出 0–100
- 缺失/未知字段、伪造 `aclPassed` 等快捷标志、非法模式或非布尔标签
- 文档、引用或上下文 ID 重复/不存在，引用不在实际记录的上下文里
- 跨角色身份/分组重复，关键题或需拒答题覆盖为空，开发/校准角色为空
- 基线与候选声明的知识快照、问题集、评审器或量表版本不同
- 阈值网格为空/重复/越界，错误代价无效，或平局规则不受支持

“失败关闭”指数据结构和已声明合同无效时不产出有效发布报告。它**不代表对恶意但自洽的元数据也能建立信任**：

- 程序不会从回答正文重新打分、核实事实、识别隐含泄漏或判断真正的拒答。给错误正文填高分且保留安全的结构化字段，仍可能通过
- `responseMode`、`critical`、`requiredMode`、分数和参考标签都来自可信输入假设。伪报模式、改写标签或不诚实的评分，不会自动被识破
- 空上下文与空引用列表是合法记录，不等于回答有证据。本例没有最低证据覆盖门
- ACL 探针只做 `contextDocumentIds/citationDocumentIds − allowedDocumentIds` 集合差。允许列表和上下文记录均为合成声明，未访问任何真实账户或检查实际检索/日志/缓存/生成链路的权限执行
- 如果把 `tenant-b-private` 加进允许列表，探针就不再报这项违例。公开测试故意展示这个限制，说明可信授权来源与完整链路观测不能被一个布尔标签替代
- 版本字段只检查声明相等，不是内容哈希、签名或审计记录；输出快照验证的是确定性报告，不是输入不可篡改。仅改写未被语义分析的回答正文，甚至可能不改变报告

因此，这个脚本适合学习门禁分层、建立可审阅的故障回放和检查数据合同。真实使用仍需要可信数据采集、经过验证的权限边界、独立标注与复核、与实际风险匹配的策略，以及真实发布/回滚流程。8 道教学题没有覆盖罕见失败的保证，不计算显著性、置信区间或生产故障率。

## 结果与测试的复现约定

`results.json` 是运行默认报告命令得到的确切输出；没有时间戳、随机数或非确定性耗时。合法输入的排列变化不改变报告；测试检查重复执行不修改输入，报告对象、JSON 文件与 CLI stdout 字节一致。

不要先覆盖已发布的 `results.json` 再把测试通过当作旧结果仍正确。修改分数、策略、标签或分组时，先审查预期变化，再更新快照与独立断言。测试中的反例不会进入正文主表，也不会悄悄改写主 fixture。
