# 第二步 A/B 实验报告:rubric 与范例锚点对交付重合率的影响 > **📌 后续更正(2026-09-12)** —— 正文一个字未改,更正单独成篇: > [`ADDENDUM-ROUND2.md`](ADDENDUM-ROUND2.md)。 > 之后查出本轮阶段 2 买来的裁决**从未到达排序器**(`cli.py` 建了 judge 却没传进 `rank_folder`); > 但用归档裁决做的反事实回放证明**交付的 20 张一模一样**,下面 §4.1 的主结论成立。 > 两件事必须一起读 —— 只看其中一件都会读错。 **测的是哪一步**:第二步(组内选优)。rubric 与锚点只进入这一步的成对比较, 不碰第一步(闭眼资格门)和第三步(时间段配额 + 同组上限)。 **量的是什么**:不是这一步自己判得准不准,而是**最终交付的 20 张**会不会变。 **执行**:2026-09-05 ~ 09-06 · webui 真实会话 · MiniMax-M3 **判据**:`CRITERIA-RUBRIC-ANCHORS.md`(跑前预登记,跑后未改判据) **成本**:有效支出 **360 次**(三组各 120 次)。 作废支出:对照组 #1 完整跑完 **120 次**(口径错);其余 4 次失败尝试各止于第一局 (preflight + 1 次调用,个位数);另有每轮 agent 自身的对话轮次若干(未逐次计数)。 **合计约 490 次**,其中约 27% 花在作废的轮次上。明细见 §3.4 --- ## 1. 实验目的 验证**在有 rubric、有范例锚点的时候,agent 交付的 20 张与用户金标 20 张的重合率是多少**。 这里的「重合率」是主指标,也是这个 agent 的最终分数:用户自己从同一批照片里挑了 20 张, agent 挑的 20 张里有几张跟他重合。 需要说清楚它测的是什么:**它测的是「跟这一个人的口味的一致程度」,不是「照片质量得分」。** 金标是一个人的选择,两个人挑同一批照片重合度本来就不会高。 本轮**没有人类基准**(没有第二个人也挑 20 张),所以 35% 这个数**无法判断是高还是低**, 只能跟随机期望和纯本地排序比。 --- ## 2. 实验设计 ### 2.1 三组阶梯,每步只变一个变量 | | rubric | 锚点 | profile | |---|---|---|---| | 对照组 | ✗ | ✗ | `photo-v4-arm-control` | | 实验组 1 | ✓ | ✗ | `photo-v4-arm-rubric` | | 实验组 2 | ✓ | ✓ | `photo-v4-arm-treat` | ``` 实验1 − 对照 = rubric 的净作用 实验2 − 实验1 = 锚点的净作用(在已有 rubric 的前提下) ``` 两组设计(只有对照和实验 2)测不出这两件事 —— 中间会同时变两个变量。 **注意这个阶梯不经过生产现状。** 生产配置是「有锚点、无 rubric」,是 2×2 里没测的那一格。 所以**实验组 2 不等于「现在的 agent」**,它是「现在的 agent + rubric」。 ### 2.2 rubric 和锚点在哪里起作用 两者**只进入阶段 2**(`rank_photos` 内部的组内成对比较),不碰阶段 1(闭眼资格门) 和阶段 3(同组上限 + 时间段配额,决定最终 20 张)。 阶段 2 的机制: - 按组内张数排组,取前 8 张进场,擂主依次跟其余每张比,共 60 局(`refine_max_matches = 60`) - 每局 **AB / BA 正反各问一次**,2 次调用 - 每张照片烧一个 4 位码在图上,答案必须带码 —— 码优先于槽位,防止「不看图只按位置作答」 - 4 种判决:甲赢 / 乙赢 / 都好(tie) / 都不够格(neither) - 正反不一致 → `inconsistent`,计弃权 - 对照组、实验组 1 每局发 **4 幅图**;实验组 2 因带 10 张锚点,每局发 **24 幅** ### 2.3 数据集与口径 ``` 目录 eval-people-309-acceptance 候选池 299(顶层 309 − 锚点组 10) 指纹 5e9947ea9eae8391 金标 me-pick 里 20 张 .JPG(另有 5 个 .MOV 不计入分母) ``` **为什么排除锚点组**:`anchors.py` 的 `split_annotation()` 本就要求锚点组与考题组按**组**互斥 (同组照片高度相似,一张进锚点等于整组被剧透),但那份切分从未落进生产配置 —— `leaks()` 全仓库只有测试在调,`rank.py`/`pipeline.py` 里 grep anchor 为空。 所以排除是补上这条从未接线的约束。核对过:这 10 张所在的 3 个本地家族正好只含这 10 张, 无额外牵连,**锚点 ∩ 金标为空**。 ### 2.4 基线 ``` 随机期望 20 × 20 / 299 ≈ 1.3 / 20 = 6.5% 纯本地排序 7 / 20 = 35% ← 0 次调用,确定性,三组共同起点 ``` 旧基线 6/20 是全池 309 口径,已作废(排除锚点后交付名单里的 `DSCF8902` 被移出、 `DSCF8896` 递补,后者恰是金标,故 6 → 7)。 --- ## 3. 实验结果 ### 3.1 主指标:三组完全相同 | | 重合 | 百分比 | |---|---|---| | 随机期望 | 1.3/20 | 6.5% | | 纯本地(0 次调用) | 7/20 | 35% | | **对照组** | **7/20** | **35%** | | **实验组 1(+rubric)** | **7/20** | **35%** | | **实验组 2(+rubric+锚点)** | **7/20** | **35%** | 三组交付的 20 张**文件名零差异、内容逐字节相同**,命中的是同样 7 张: ``` DSCF8896 DSCF9111 DSCF9325 DSCF9379 DSCF9452 DSCF9529 DSCF9533 ``` **360 次付费调用,交付名单一张没动。** ### 3.2 但处理确实起作用了 —— 阶段 2 的判决明显不同 | | 对照组 | 实验组 1 | 实验组 2 | |---|---|---|---| | 每局图数 | 4 幅 | 4 幅 | **24 幅** | | 局数 / 调用 | 60 / 120 | 60 / 120 | 60 / 120 | | 判 a | 12 | 17 | 15 | | **判 b** | **8** | **4** | **6** | | tie | 3 | 2 | 6 | | inconsistent | 37 | 36 | **32** | | neither | 0 | 1 | 1 | | 整组淘汰 | 0 组 | 0 组 | 0 组 | | 读码率 | 59/60 = 98.3% | 59/60 = 98.3% | **60/60 = 100%** | | contradiction | 0/60 | 0/60 | 0/60 | | 双向一致率 | 20/60 = **33.3%** | 22/60 = **36.7%** | 22/60 = **36.7%** | | 对局表 | 60 局 | 逐对相同 ✓ | 逐对相同 ✓ | 双向一致率的定义以代码为准(`compare.ts:375`):`abPick === baPick && abPick !== 'tie'` —— **两次都答 tie 不算一致**。用 `60 − inconsistent` 会算成 38.3%/40%,那是另一个口径,本报告不用。 ### 3.3 为什么判决变了而交付没变 —— 逐局证据 要换掉交付名单里的照片,必须出现 `b`(挑战者赢)。实测: ``` 实验组 1 判 b 4 局 → 涉及交付 20 张的 0 局 实验组 2 判 b 6 局 → 涉及交付 20 张的 0 局 ``` **没有一局 b 落在交付名单上。** 根因是结构性的: ``` 阶段 2 打过 69 张(60 局) 交付 20 张 两者交集 2 张 ← DSCF9379、DSCF9308 其余 18 张 一局都没打过 ``` 那 2 张共打 14 局,结果是 `a` 4 局、`inconsistent` 10 局、**`b` 0 局**。 `tournament_plan` 按**组内张数**排组(打最大的组),而交付名单来自全局高分照片, 多在小组里 —— 两拨基本不重叠。这一条在跑之前就预登记了。 ### 3.4 执行过程中的失败与作废(如实记录) | 轮次 | 口径 | 阶段 2 | 处置 | |---|---|---|---| | 对照组 #1 | 309(preset 覆盖了 profile 的排除) | 120/120 成功 | 主指标作废,仪器指标有效 | | 对照组 #2 | 299 | 超时回落,0 次 | 作废,导出已删 | | 对照组 #3 | 299 | 120/120 成功 | ✅ 采用 | | 实验组 1 | 299 | 120/120 成功 | ✅ 采用 | | 实验组 2 #1~#3 | 299 | 未执行(见 §5.2) | 作废 | | 实验组 2 #4 | 299 | 120/120 成功 | ✅ 采用 | **对照组的逐局 verdicts 已丢失** —— 该文件按数据集指纹命名,三组同名,被实验组 1 覆盖。 聚合数字(判决分布、读码率)与对局表 `plan-control.json` 保住了,逐局判决对不了了。 教训已改为规矩:**每组跑完立刻存档,再起下一组**。 --- ## 4. 结论 ### 4.1 主结论 > **三组重合率完全相同(7/20 = 35%),但这不是「rubric 和锚点没用」, > 而是「阶段 2 到交付之间没有传导路径」。这两个结论完全不同。** 依据:三组的阶段 2 判决明显不同(判 b 分别为 8 / 4 / 6),说明处理确实生效; 而没有任何一局 `b` 落在交付的 20 张上,说明改判够不到交付名单。 **本轮无法回答「rubric 和锚点有没有用」。** 要回答,得先让阶段 2 的结果能传导到交付名单。 ### 4.2 阶段 2 的钱没有转化成分数 三组各花 120 次付费调用,交付结果与 0 次调用的纯本地排序**逐字节相同**。 按「阶段 2 的职责是判组内谁最好、不是改变交付」这个定义,这不算缺陷; 但作为**读结果时的限制**必须写明:阶段 2 对交付的影响上限是 2/20,本轮实际改动 0 张。 ### 4.3 阶段 2 这台仪器本身没过验收线 双向一致率 33.3% / 36.7% / 36.7%,**验收线 60%** (这条线来自**上一轮**的预登记判据 `REPORT.md:76`,本轮判据只定义了一致率怎么算、未重述这条线)。 同一对照片重复问一遍,62.8% 会改口(此前标定实测)。 即使传导路径修好,它给出的改判也有相当一部分是噪声。 **读码通道是可信的**:三组读码率 98.3% / 98.3% / 100%,contradiction 全部 0/60。 所以「答案指错照片」这一类错误可以排除,问题出在判断本身不稳。 ### 4.4 此前担心的「图多会压糊码」没有出现 三组读码率 98.3% / 98.3% / 100% —— **没有差别**(60 局里差一局, 在 n=1、改口率 62.8% 的条件下与噪声不可分)。实验组 2 每局 24 幅 (10 张锚点 ×2 + 考题 4 幅),码照样读得出来。 查证 `requestImagePixelBudget` 是**逐图**预算(`context.ts:105`,`orderedRefs.map` 每张单独带同一份 policy,不累加),我们的 512/448 像素图比 4.19M 的预算小一个数量级。 **这条只能说「担心不成立」,不能说「图多更好」。** 后者是 §4.6.1 明令禁止的读法。 ### 4.5 一个静默失效:排除锚点把锚点本身也排掉了 为防泄题而排除的那 10 张,正是锚点自己。而 `buildAnchorBlock` 取锚点图时 用了**同一份候选池排除清单** —— 于是一张锚点图都取不回来 (本地实测:无排除 10 张,带排除 0 张、missing 10)。 提示词照旧写着「范例图排在最前面」「判表情看人脸特写」,实际一张没附。 **后果:实验组 2 前三次跑的都是没有范例图的空壳** —— 它测的不是「rubric + 锚点」,是「rubric + 一段描述不存在图片的文字」。 模型被要求看不存在的图,于是改用散文作答。 **形态值得单独记住:一个为了提高严谨性而做的修正(排除锚点), 通过一条谁都没想到的耦合,把被测的处理变量整个抹掉了,而所有指标照常绿。** `anchors.py` 的 docstring 记着前三次同类事故,这是第四次; 区别是前三次的触发源都是「某一路悄悄落后」,这次是「我们刚做的一次修正」。 已修 `8099ffa`(取锚点图传空排除清单 + 一张都没取到就抛错)。详见 §5.2。 ### 4.6 效度限制(跑前预登记,不因结果改口) 1. **n = 1**。每组只跑一次,而同一调用重复问有 62.8% 改口。任何组间差值都无法与噪声分开。 本报告**不把任何差值称为「提升」**。 2. **三组先后跑**,时间是混杂因子,n=1 下无法拆开。 3. **锚点 10 张全在候选池内**(已排除,但机制未接线,见 §2.3)。 4. **没有人类基准**,35% 无法判断高低。 5. **阶梯不经过生产现状**(有锚点无 rubric 那一格未测)。 --- ## 5. 下一步 按「先做能改变结论的、再做锦上添花的」排序。 ### 5.1 修传导路径(唯一能让这个实验有意义的一步) 现在 `tournament_plan` 按组内张数排组(`pipeline.py:329`,`groups.sort(key=len, reverse=True)`, 注释写着「大组优先:信息量更高」),与交付名单基本不重叠。 在此之前,**任何关于 rubric / 锚点 / 阶段 2 的实验都测不出东西**。 **这不是预算不够,是顺序选错了 —— 修它一分钱不用多花。** ``` 全部 125 组 阶段2 打了 9 组 最大的九个:13,13,9,9,8,8,7,7,7 张 → 60 局,预算打满 交付 20 张来自 20 组 13,9,6,6,5,4,4,3,3,3,2,2,2,2,2,2,2,1,1,1 张 两边重合 2 组 ``` 一组打 `min(张数, stage2_cap) − 1` 局 —— **`stage2_cap = 8`**(`config.py:147`), `tournament_plan` 里 `sorted(members, …)[:cap]` 先按本地分截到 8 张再打擂台。 所以 13 张的组打 7 局而不是 12 局。 这个 cap 有实测铁证:那 9 个组若按「张数 − 1」该打 **72** 局,而计划表实际是 **60** 局, 正好等于按 cap=8 算的结果。 把这 20 个交付相关组**全部打完**需要: ``` 7+7+5+5+4+3+3+2+2+2 + 1×7 + 0×3 = 47 局 ← 比现在的 60 局预算少 13 局 ``` 口径说明:这 20 个组是**当前交付名单**所在的组。阶段 2 现在改不动交付,所以这就是起点; 一旦改判真的换掉了某张,相关组会跟着变,47 这个数是一阶估计,不是定值。 同时要记一条**结构上的天花板**:交付的 20 张里有 **3 张来自只有 1 张照片的组** —— 那种组没有对手,阶段 2 对它们的影响恒为零;另有 7 张来自 2 张的组,各只有 1 局。 所以即使排序修到最优,阶段 2 能影响的也只是 20 张里的 17 张,且其中 7 张只由单局决定。 ⚠️ 不要简单复活已删除的 `refine_plan`(「只打冠军进了名单、前两名咬得紧的组」)。 它被否掉的理由记在 `tournament_plan` 的 docstring 里:**改变了送去判的对的分布, 而 VLM 在新分布上的表现没有任何数据**。要做需重新设计并先验证。 这件事改变产品行为,属于**产品决策**,需标注者定夺。 ### 5.2 量化实验组 2 的间歇失败 实验组 2 跑了 4 次:3 次阶段 2 未执行,1 次跑满 120 次成功。 - 前 3 次中最早那次的真因已查明并修复(`8099ffa`):`buildAnchorBlock` 把**候选池的排除清单** 传给了取锚点图那一步,而清单里正好是那 10 张锚点自己 → **锚点图一张都取不回来**, 提示词照旧写着「范例图排在最前面」而实际一张没附 → 模型改用散文作答。 本地实测:无排除取回 10 张,带排除取回 0 张、missing 10。 - 修好之后仍有一次失败:诊断现场为 `tool-call 0 个,返回块 [text],散文开头「[Image content」`。 已排除「图被替换掉」(harness 的替换文本是 `[image omitted …`,不长这样)。 **所以「锚点配置在生产路径下产不出结构化答案」这个说法要收一档:不是必然失败,是不稳定。** 需要量清楚失败率,再决定是否值得修。 ⚠️ 措辞纪律:实验组 2 相对前两组同时变了**两件事**(图 4→24 幅 + 多了范例前言), 单凭现有数据**分不开是哪一半造成的**。不得写成「24 幅图导致模型不填表」。 ### 5.3 提高阶段 2 的判断稳定性 一致率 33~37% 对 60% 的线(该线出自上一轮预登记判据 `REPORT.md`,本轮判据未重述)。 可选方向(均未验证): 每对多问几次取多数、把 tie 的语义收紧、或换模型。 **先有设计和预登记,再花钱。** ### 5.4 补上锚点切分的接线 `split_annotation()` 产出的 `test_groups` 应当自动落进生产的排除清单, 而不是靠人记得在配置里手填。现在 `leaks()` 只有测试在调。 ### 5.5 建立人类基准 让第二个人也从同一批 299 张里挑 20 张,测他与金标的重合度。 **没有这个数,35% 无法解读。** 这是让主指标可读的前提。 ### 5.6 阶段 3 是否引入 VLM(标注者 2026-09-06 提出,待决策) 现在的分工: ``` 阶段2 VLM 判「同一组里派谁出场」 阶段3 纯本地分决定「哪些组能上榜」 ← VLM 完全不参与 ``` **「谁进最终 20 张」这个决定里没有 VLM 的份。** 这是比 §5.1 更彻底的一条传导路径。 ``` 修 §5.1 的排序 0 次额外调用 让 VLM 换得动「组里派谁」 上限 17/20 阶段3 引入 VLM 约 248 次调用 让 VLM 换得动「哪些组上」 上限 20/20 ``` **建议排在 §5.3 之后,理由有两条:** 1. **现在这台仪器不达线。** 一致率 33~37% 对 60% 的线。它眼下的失败是**无害**的 (改判传不到交付,只浪费钱);一旦接进阶段 3,它就直接决定交付 —— 失败模式从「没效果」变成「可能有负效果」,会真把好照片挤掉。 2. **跨组比较是更难的任务。** 组内比的是几乎相同的连拍(谁睁眼、谁表情好); 跨组是「雪山这张和咖啡馆那张哪个更值得留」,纯口味,本地分给不出参照。 一台在简单任务上只有 35% 一致率的仪器,在更难的任务上不太可能更好。 **要做需重新标定,不能沿用组内的一致率结论。** ### 5.7 遗留项(本报告发出时的状态) - **preset 已还原**(md5 `76cc63001848601b3d928f0396c3f5ee`,DSCF 条目 0), DSH 已重启于生产 profile `photo-v4`。 - **三个实验 profile `photo-v4-arm-*` 已删除**(归档于 `arm-profiles-backup.tgz`, 并可由仓库里的 `make_web_arms.py` 重新生成)。 删除的理由不是洁癖:留着它们会让 `doctor.sh` 第 3c 层长期报 3 条红, **而长期报红会把人训练成忽略告警 —— 那正是守卫失效的方式**。 - `photo-v4-ab` 的 `excludedRelativePaths: []` 被 preset 覆盖 —— 3c 查出的现存缺陷, 影响 47 对那条评测路径的口径。**建议单独处理,不要就手改**(会动到历史数据可比性)。 清理之后 `doctor.sh` 3c **只剩这一条**,告警重新有信号。 **收尾自检(本报告发出时实测)**:doctor 第 1/2/3/3b/4/5 层全绿; 3c 仅剩 `photo-v4-ab` 一条已知缺陷。 --- ## 附:过程中被推翻的判断(留痕) 这个项目的一半价值在错误留痕上,以下均为本轮中被证伪的说法,包括我自己的: 1. **「三组 profile 都带 10 条排除、复核通过」** —— 验的是一个**不生效**的设置。 preset 与 profile 逐键合并,preset 定义的键让 profile 那份失效。花了 120 次调用才发现。 现已加 `doctor.sh` 第 3c 层自动检查。 2. **「排锚点不改变任何数字」** —— 错。`DSCF8902` 是锚点又在交付名单里,排除后必然递补, 基线 6/20 → 7/20。代价不是「失去可比性」,是「必须重算基线」,而重算是 0 次调用。 3. **「重跑很便宜 → 可能烧了 120 次 → 其实很便宜」** —— 改口那次是错的。 `compare.ts` 的比较循环里没有 try/catch,第一局失败就抛出中止,请求数是个位数。 因这次错误改口,中止了唯一一次能定性的重跑。 4. **「24 幅图导致模型不填表」** —— 提出时无依据(当时实际只发 4 幅), 驳回是对的;后来在真发 24 幅时确实失败一次,但同配置又成功一次,**至今未证实**。 5. **「锚点排除是修传导路径的前置条件」** —— 该耦合依赖于「要修传导路径」这个前提, 而标注者已定不修,故不成立。排锚点的真实理由是:当下泄题为 0 属**巧合非保证**, 而排除零成本、对三组一致。 6. **校验和标签贴反** —— 归档时把干净版与实验版的 md5 写反了。 按内容判据(`grep -c '"DSCF'`)核对才发现。**校验和必须连内容判据一起记, 光有哈希认不出方向。** **共同形态**:这些错误里有 4 个的表现都是「配置/数据被悄悄弄残 → 得出一个假结论 → 所有指标照常绿」。 其中一个尤其值得记:**一个为了提高严谨性而做的修正(排除锚点), 通过一条谁都没想到的耦合,把被测的处理变量整个抹掉了,而所有指标照常绿。**