# rubric 与范例锚点对交付重合率的影响 · 预登记 写于 2026-09-04,**一次付费调用都还没发**。判据写在这里,跑完不改。 --- ## 实验目标(标注者原话) > 验证在有 rubrics 和有范例锚点的时候,跟 golden set(或者说答案)的重合率是多少。 --- ## 三组 每一步**只变一个东西**,这样差异可归因: | | rubrics | 范例锚点 | |---|---|---| | 对照组 | ✗ | ✗ | | 实验组 1 | ✓ | ✗ | | 实验组 2 | ✓ | ✓ | ``` 实验1 − 对照 = rubric 的净作用 实验2 − 实验1 = 锚点的净作用(在已有 rubric 的前提下) ``` 两组设计(只有对照和实验 2)测不出这两件事 —— 中间同时变了两个变量。 ## 怎么搭(已验证) ~~`photo-v4-headless` + `--patch` 覆盖层,不新建 profile。~~ **2026-09-04 21:4x 改为:三个 web profile,`photo-v4-arm-{control,rubric,treat}`。** **这条修改写在任何一组跑出结果之前 —— 当时三组的产出都还是 0 张。** 改的原因是用户的要求:三组要各自是 webui 里看得见的一个会话, 「模拟最真实的用户操作」。headless 跑出来的会话**不会被登记进 `storages/workspace.json`**(实测:磁盘 127 个会话,注册表里只有 20 个), 所以侧边栏永远不列它,刷新也没用。 代价要说清楚,这是一次**真实的路径变更**,不是等价改写: | | headless | web | |---|---|---| | bundle | `dsh-headless` | `dsh-web-app` | | persona | profile 直接覆盖 `system-prompt` 的 persona 字段 | 从 preset 加载 | | 分组差异 | `--patch` 覆盖层 | 一组一个 profile | 也就是说,被测的是**用户真正会跑的那条链路**,而不是当初为了自动化方便选的那条。 判据(重合数、读码率、contradiction、整组淘汰)一个都没改。 下面关于 `--patch` 是整段替换的那段警告**保留** —— 它记的教训(比较之前先逐组自检) 在新做法上原样适用,而且已经照做了:三个 web profile 逐组 `--dump-config` 确认 8 项 config 齐全,然后才做的两两 diff。 ⚠️ **补丁的 `config` 是整段替换,不是合并。** 只写要改的两项会把 `allowedRoots` / `engineBinary` / `stage2Vlm` 等 12 项一起抹掉。 第一次就是这么错的:跑起来报「没有配置照片授权目录」。 更要命的是它**骗过了验证**:两组被同样地削掉,diff 出来反而干净地「只差两行」。 所以规则是 —— **比较两组之前,先单独确认每一组自身是完整的。** 三个补丁都带完整的 14 项 config,已逐组核对: ``` 每组自身 allowedRoots / engineBinary / stage2Vlm / allowNeither / defaultTarget 均在 对照 vs 实验1 只差 rubricFile 实验1 vs 实验2 只差 anchorsFile ``` ## 数据集 ``` ~/Desktop/照片测试/eval-people-309-acceptance 候选池 309 张(根目录) 金标 me-pick 20 张照片(另有 5 个 .MOV,不计入) 金标可达性 20/20 在上级目录都有副本 → 排除 me-pick 后仍选得到,重合度无天花板 ``` ⚠️ 不要用 `eval-people-309`:它**没有金标子目录**,重合度根本拿不到。 其余几个目录是「移动」式的(精选被移进 me-pick、上级无副本),排除后金标永远选不到。 ## 主指标 **交付的 20 张与金标 20 张的重合数。** 基线(都是 0 次调用、确定性实测): ``` 随机期望 20 × 20 / 299 ≈ 1.3 / 20 纯本地排序 7 / 20 ← 三组共同的起点,差异只能来自阶段 2 命中 8896 9111 9325 9379 9452 9529 9533 候选池 299(309 − 锚点组 10) 指纹 5e9947ea9eae8391 ``` **旧基线 6 / 20 作废** —— 那是**全池 309 的口径**,候选池里还含着锚点组。 2026-09-04 排除锚点组之后重算(0 次调用),交付名单里的 `DSCF8902.JPG`(锚点) 被移出,`DSCF8896.JPG` 递补进来,而后者恰好是金标,所以 6 → 7。 旧口径保留在此仅供追溯,**不得再作为对照基线引用**: ``` 旧(全池 309,指纹 ca88cc1864d41b25) 6 / 20 命中 9111 9325 9379 9452 9529 9533 ``` 排除锚点组的理由、以及为什么它是「改对局计划」的前置条件,见下面的已知缺陷第 4 条。 ## 必须同时报的次要指标 - 整组淘汰组数(`n_rejected_families`):旧数据重算是 2/9 - 读码率(`codeReadOk`):基线 68/70 = 97.1% - contradiction:基线 0/60 - 双向一致率 —— **定义以代码为准**:`abPick === baPick && abPick !== 'tie'` (`compare.ts:375`)。两次都答 tie **不算**一致,代码里写明了理由: 「两次都答 tie 不代表判断稳定」。所以用 verdicts 里的 `consistent` 标志直接数, **不要**用 `60 − inconsistent` —— 那会把 tie 算成一致,得到偏高的数。 对照组实测:20/60 = **33.3%**(`60 − inconsistent` 会算成 38.3%,作废)。 - **agent 自己输出的措辞是否属实**(45% 有没有带出处、还说不说「位置偏好是真实存在的」、 组内比较有没有把 neither 报成「维持原判」)—— 这三处 2026-09-04 刚改, 而它们**只有人去读输出才发现得了,没有任何测试会红** ## ⚠️ 预登记的结构性限制 —— 写在看结果之前 **阶段 2 目前对交付名单几乎没有影响。** 用同一份 verdicts 实测过: ``` 阶段2 打过的照片 69 张(60 局 / 120 次调用) 最终交付 20 张 两者交集 2 张 ← 影响上限 实际改动 0 张 ← 加不加阶段2,交付都是同样的 20 张、同样的重合度 ``` 根因:`tournament_plan` 按**组内张数**排序打最大的组,而交付的 20 张来自 全局高分照片,多在小组里。两拨基本不重叠。 而 rubric 和锚点**只作用于阶段 2**。所以: > **三组很可能得到完全相同的重合度,且不是因为噪声,是因为没有传导路径。** 这一条在跑之前就记下来,是为了防止跑完之后把「三组一样」解释成 「rubric 和锚点没用」。**那两个结论完全不同。** 标注者已知悉此风险并决定照跑。 同理,60 局里只有 **8 局**恰好一张是金标(有标准答案),51 局两张都不是金标。 ## 本轮已知缺陷 —— 登记于跑之前,本轮**不修** 标注者 2026-09-04 决定:跑之前不动被测对象,缺陷如实记进报告,下一轮再修。 **1. preset 里留着一句被自己实验推翻的话。** `preset-photo-filter-v4/agent.cordis.yml:180`: ``` (位置偏好真实存在) ``` 标定实测是 72% / 72%,位置没有可测量的影响;真正原因是模型对同一问题 答不稳(重复调用 62.8% 改口)。同一段里「实测跑过 3 对全是平局」也是 n=3 的旧数据。 **为什么不影响本轮的测量**(已查证,不是"影响很小",是结构上到不了): - 阶段 2 的视觉调用**自建消息、自带 system**(`harness-vision.ts:416-426`), `system` 只有 `[makeSystem, rubric, anchors.text]`,会话 persona 一个字都不进去。 决定名单怎么重排的判决,看不见 preset。 - 三组共用同一份 preset,做差时抵消,不可能制造或掩盖组间差异。 它影响的是**「agent 自己输出的措辞属不属实」这项交付物** —— 报告里必须记这一枪。 **2. 守这条的测试是漏的。** `test_user_facing_claims.py:41` 用精确子串 `"位置偏好是真实存在的"`。 preset 里少了「是」和「的」,从底下滑过去了。这是过程缺陷,比那句话本身更值得修。 **3. 两处过时注释描述了已删除的 `refine_plan`。** `rank.py:235-239` 和 `agent-v4/src/index.ts:317-320` 都写着计划「只挑冠军进了 最终名单、且本地分前两名咬得紧的组」。**实际的 `tournament_plan` 按组内张数排序, 跟交付名单无关**(`pipeline.py:328-330`),其 docstring 明写那套筛选 "三条规则一条都没验证过"、是错的、已废弃。 这两处注释在本轮实验准备中**真的把执行者误导过一次** —— 据此一度得出 「每一局都会直接改交付名单、所以传导路径存在」的相反结论。 上面那条结构性限制以**代码**为准,不以注释为准。 **4.(已修,2026-09-04)锚点的 10 张照片全部落在候选池内。** **修法(甲)**:`make_web_arms.py` 把锚点组追加进三组的 `excludedRelativePaths`。 按**组**不按张 —— 同组照片高度相似,排一张等于剧透整组(`anchors.py` 开头写了)。 已核对:这 10 张所在的 3 个本地家族(21 / 50 / 80)**正好只含这 10 张**, 没有额外牵连,也没有金标被误伤。候选池 309 → 299,基线 6/20 → 7/20(见主指标)。 **为什么修(owner 2026-09-05 复核后的口径,跟执行者当时写的不一样)**: 先说不成立的那条。「甲是乙的前置条件」这个论证**本轮用不上** —— 乙(改对局计划)已经由标注者定为不做(见下),前置条件也就无从谈起。 owner 此前给执行者的指令是「不排锚点」,理由正是这条耦合已经失效。 **改判的理由是另一条**:实测泄题 0 是**巧合,不是保证**。 它成立靠三件事同时为真 —— 锚点∩金标为空、10 张锚点一局都没进 `tournament_plan`、 `DSCF8902` 虽然占了交付名额但本来也不是金标。三件里任何一件变了, 指标都不会报警。而排掉它**零成本、对三组一致、不影响组间可比性**, 换来的是「实验组 2 的绝对数字里没有它自己看过的范例」这一条可以直说。 代价只有一个,已经付了:基线从 6/20 变成 7/20。这是**更干净的地板**, 不是变好 —— 腾出来的那个名额本来就被一张非金标的锚点占着。 **乙(让对局计划真的打影响交付的组)不做。** 标注者 2026-09-04 定了阶段 2 的职责 是「判组内谁最好」,**不是「改变交付」**。按这个定义,`tournament_plan` 按组内张数 挑组就是在照职责办事,不是缺陷。owner 此前把它称作「产品缺陷」的说法**已撤回**。 留下的只是一条如实记录:本轮 120 次付费调用打的 60 局,可证明改不动交付名单 (交集 2 张、实际改动 0 张)—— 这是读结果时必须带上的限制,不是待修的缺陷。 以下是修之前的记录,保留追溯: **原始问题:** `anchors.json` 的 10 张与 309 张候选**全部重合**,与金标 20 张**零交集**。 所以泄的不是答案,但模型在提示词里见过池中 10 张的人工标签。 方向**对实验组不利**:锚点标为「他留下了」的 4 张(例1丙、例3甲乙丙)都在池里、 又都不是金标,模型照着提上来只会挤掉本该交付的照片,把重合率往下拉。 因此实验组 2 若仍然赢,结论更可信;若输,须先排除这一条再下判断。 **5. 答案子目录不是被排除的,是被文件名撞掉的(owner 复核时发现,本轮不修)。** `eval-people-309-acceptance/` 里含 `me-pick/`,装着 20 张金标的副本。 少写一条 `--exclude me-pick`,`scan` 枚举到的是 **329** 张 —— 答案进了池子。 但 `pick` 报出来的 `n_candidates` 仍是 **309**、名单里也全是裸文件名, 因为 `me-pick/DSCF8896.JPG` 和顶层的 `DSCF8896.JPG` **文件名相同**,后面按名字合掉了。 **兜住这个错的是文件名巧合,不是排除规则。** 指标看不出异常: 张数对、名单对、重合度对(实测漏排也是 6/20,命中名单一模一样)。 一旦答案副本改过名,它就会当成新照片进池,而所有指标依旧「正常」。 本文开头那句「排除晚一步,整轮重合率就作废」说的正是这件事 —— 现在没有任何检查在守它。三组 profile 的 `excludedRelativePaths` 都写着 `me-pick`, 本轮不受影响;**要修的是缺一道断言,不是缺一条配置**。 ## owner 独立复核(2026-09-05 01:30–01:50,0 次付费图片调用) 判据里的数字由 owner 用另一条路径重算过,不是复述执行者的结果: ``` 全池 候选 309 指纹 ca88cc1864d41b25 6/20 命中 9111 9325 9379 9452 9529 9533 排锚点组 候选 299 指纹 5e9947ea9eae8391 7/20 命中 8896 9111 9325 9379 9452 9529 9533 ``` 三处对上了:递补进来的 `DSCF8896.JPG` 确实是金标;全池那 20 张里确实含锚点 `DSCF8902.JPG`,排除后不再出现;锚点 10 张与金标 20 张交集为空。 两个指纹在 `eval-people-309` 和 `eval-people-309-acceptance` 上**相同** (指纹算的是相对路径+大小+mtime,两处顶层 309 张一致),所以判据跨这两个目录都成立。 `make_web_arms.py` 重跑一遍,产出与已部署的三组 profile **逐字节一致** —— 来源可追。 ## 实验组 2 三次全废的真因(2026-09-06 查明,已修 `8099ffa`) **是「排除锚点」这个为了严谨而做的决定,通过一条没人想到的耦合,把被测的处理变量整个抹掉了。** ``` 标注者选「排除锚点」(299 口径) → 排除清单里正好就是那 10 张锚点 → buildAnchorBlock 取锚点图时用了同一份清单(index.ts:186) → 锚点图 0 张(.filter(Boolean) 把 undefined 全滤掉,不报错) → 提示词照旧写「范例图排在最前面」「判表情看人脸特写」,实际一张没附 → 模型被要求看不存在的图,改用散文作答 → STRUCTURED_OUTPUT_UNSUPPORTED ``` 本地实测(0 次付费调用,两边独立复现过): ``` preview --names <10 张锚点> 预览 10 · 人脸 10 · missing 0 preview --names <同样 10 张> --exclude <同样 10 张> 预览 0 · 人脸 0 · missing 10 ``` 时间线佐证:排除写进 preset 是 **09-05 11:02**,实验组 2 首跑是当天 **17:17**。 **实验组 2 从来没有一次是在「有锚点图」的状态下跑过。** ### 两条曾经写进结论的假设,作废 ``` 「24 幅图导致模型不填表」 作废 —— 实际每局只发 4 幅,与对照组同形态 「可能是 MiniMax 的图片限制」作废 —— 是我们自己的代码,与 provider 无关 ``` 三道图片闸门都查过、都没到上限(插件 40 / harness 40 / preset 未定义该键)。 ### 为什么这一条值得单独记 `buildAnchorBlock` 的 docstring 里已经记着**前三次**同类事故(少传 withFace、 少传 labels、用错 folder),每次的表现都一样:**锚点被悄悄弄残 → 得出「锚点没用」的 假结论 → 而所有指标全绿**。这是第四次,但触发源和前三次不同 —— **是我们自己刚做的一次「提高严谨性」的修正**。 修法有两部分,缺一不可: ``` 1 取锚点图传空排除清单 —— 排除清单管的是「候选池能选谁」, 锚点是「故意要给模型看的范例」,按定义不受候选池约束 2 一张锚点图都没取到就**硬失败** —— 宁可整轮中断, 也不要「文字说有范例、实际一张没附」这种指标全绿的静默残废 ``` 复核过的边界:`index.ts` 里五处 `ranker.preview` 调用,另外四处都在 `missing` 非空时抛错,**只有锚点这一处用 `.filter(Boolean)` 静默吞掉**。这个洞是独一份的, 不是普遍模式。 ## 本轮的前提,写死在判据里 > 本轮在**「阶段 2 无法传导到交付」的前提**下测量。三组若返回同一个重合度, > 唯一诚实的结论是**没有传导路径**,不是「rubric 和锚点没用」。 > 这一条在跑之前就已预登记。 2026-09-04 排除锚点组之后重测,传导路径**没有改善**: 对局表仍是 60 局 / 69 张,与交付 20 张的交集仍是 **2 张**。 排除锚点解决的是泄题风险,不是传导路径 —— 两件事互相独立。 ## 预先定义的读法 | 结果 | 只能这样读 | |---|---| | 三组重合度相同,且与纯本地 6/20 相同 | 阶段 2 未传导到交付 —— **测不到 rubric/锚点的作用**,不是「它们没用」 | | 三组之间有差异 | 记录差值,但 n=1、ε=62.8%,**不足以断言方向** | | 某组低于 6/20 | 阶段 2 在这一组上起了负作用,同样受 n=1 限制 | **不许做的事:** - 不许把「三组一样」写成「rubric 和锚点无效」 - 不许把单次运行的差值写成「提升 / 下降了 X」 - 不许和历史的 6/20 做「提升了多少」的比较(那不是受控对照,可并列摆出) ## 成本 ``` 每组上限 60 局 × 2 方向 = 120 次,三组共 ≤360 次 ``` ## 执行 三个 profile 由 `make_web_arms.py` 生成:整份复制已部署的 `photo-v4`, **只改 `rubricFile` / `anchorsFile` 两行**,其余 config 与注释原样保留。 一组一次,每组都要重启 DSH(profile 在启动时决定,不能中途换): ```bash DSH_HOME=~/.dsh-v4 pnpm dsh --profile photo-v4-arm-control # 然后浏览器开 127.0.0.1:3080 DSH_HOME=~/.dsh-v4 pnpm dsh --profile photo-v4-arm-rubric DSH_HOME=~/.dsh-v4 pnpm dsh --profile photo-v4-arm-treat ``` 在界面里新建会话、选 "Photo Filter v4" preset,发同一句话(只有导出目录不同)。 导出要走确认码那一轮 —— 那本来就是真实用户会经历的一步。 ⚠️ 停 DSH 用**精确 PID**。不要用 `pkill -f "bin.ts --profile photo-v4"`: `-f` 是不带锚点的子串匹配,`photo-v4` 又是 `photo-v4-arm-*` 的前缀, 那条命令会把正在飞的付费任务一起打掉。 ⚠️ 三组的会话标题会长得一样(首条消息几乎相同)。分辨靠**导出目录**: `arm-control` / `arm-rubric` / `arm-treat`。 导出那一步别省 —— 两个文件夹(阶段 3 精选 / 其余每组最好的)是用户最终拿到手的东西, 也是这条链路上唯一没验过的一段。