Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions evals/baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# 评测基线(首轮 · 12 条口径)

首次跑通 `evals/runbook.md` 的三种问法,记录结果作为后续改动的对照点。

> **口径提示**:本文件记录的是**首轮 12 条 ground truth 口径**下的分数。随后 `ground-truth.md` 已扩充到 25 条(补进首轮实测发现的 13 条真缺陷,见 #14 / #15),`rubric.md` 的分母同步改为 25。
> **本文件的分数与新口径不可直接比较**——要比较,必须按新口径重跑一轮。
> 新口径下的重跑结果尚未产生,本文件暂按首轮事实保留。

**结论先行**

1. **两档模式分流成功**:问「有没有回归、能不能合」的那一轮输出了紧凑列表 + 一句结论,没有套六段骨架;另两轮问「按七维审」「给模块做体检」的输出了完整的七维覆盖表 + 拍板项 + 已查过没问题。
2. **三轮共同漏掉的只有 2 条,而这两条恰好是标准答案站不住的那两条**(见「口径缺陷」一节)——漏报不是本 skill 的系统性短板,是标准答案的问题。
3. **归类质量三轮接近**(66.7% / 68.8% / 68.8%,按已报条目算),**差距主要在命中覆盖**:快模式那轮命中 54%,两轮全量分别 71% / 79%。快模式少的是设计面条目,符合它「只报本次引入、能落到触发路径」的定位,不是缺陷。
4. **2/3 轮把「两个包都叫 `package shop`」报成了问题**——这正是标准答案「不该报」清单里点名的口味问题。边界没守住,需要在 skill 里补具体例(已开票)。
5. **标准答案漏了 13 条真缺陷**,三路合计发现,全部可复核。这意味着当前的 12 分制只覆盖了样本真实缺陷的一部分,基线偏乐观。

---

## 运行条件

| 项 | 值 |
|---|---|
| 日期 | 2026-10-21 |
| skill 版本 | `main` @ `594bee1`(v1.0.0 tag 之后,尚未打新 tag) |
| 被测上下文 | 三个全新子 Agent 上下文,互不污染,轮间不共享结论 |
| 样本 | `evals/fixtures/tiny-shop`,复制到仓外隔离目录后审(`scratchpads\dcr-eval\tiny-shop`) |
| 隔离 | `ground-truth.md` / `rubric.md` 不在样本目录内;样本 README 改成中性描述,不提示「有缺陷」 |
| 原始输出 | 仓外 `scratchpads\dcr-eval\runs\round-{A,B,C}.md`(不入版本控制) |

三轮问法与期望(来自 `evals/runbook.md`):

| 轮次 | 问法 | 期望 | 实际形态 | 判定 |
|---|---|---|---|---|
| A | 审一下这个 PR 有没有回归,能不能合 | 快模式:紧凑列表 + 结论,不套六段骨架 | 9 条紧凑条目 + 一句结论 + 三行说明 | 通过 |
| B | 按七维审 | 全量:七维 + 类型 playbook + 六段式 + 覆盖表 | 六段式 + 13 行覆盖表 + 5 条拍板 + 8 条已查过没问题 | 通过 |
| C | 给这个模块做个体检 | 全量,且应命中设计类条目 | 六段式 + 14 行覆盖表 + 2 条拍板 + 5 条已查过没问题 | 通过 |

---

## 口径说明(评分怎么算的)

按 `evals/rubric.md`:

- **命中**:对照 `ground-truth.md` 的 G1–G12,满分 12。命中且触发路径正确记 1;触发路径含糊记 0.5;漏报记 0。**只在「证据/边界说明」里点到、没有作为一条结论提出**的,记 0.5(C1 只算表达加分,不占命中)。
- **误报**:编造不存在的缺陷每条 −2;口味问题当问题报每条 −1;泛泛建议每条 −1;「不该报」清单里的候选每条 −1。
- **归类正确性**:逐条取平均,满分 12。级别与标准答案一致记 1;偏差一级记 0.5;偏差两级以上、或该拍板判成缺陷,记 0。**未报出的条目记 0**,所以这一项对漏报是二次惩罚——因此同时给出「按已报条目计」的辅助值。
- **证据强度**:每条 finding 定级(5 跑了测试/构造了最小复现;4 读了测试并确认覆盖或未覆盖;3 grep 了全部调用方;2 读了完整函数 + 相邻实现;1 只读了 diff),取该轮全部上报条目(含拍板项)的均值。声称强度高于实际做过的每条 −1;「已查过没问题」里没有验证方式的每条 −1。
- **表达质量**:逐条,满分 12。用大白话讲清机制且配了最小对照例子记 1;讲清机制没例子记 0.6;只有结论记 0.2;术语堆砌/行号当论据记 0。拍板类条目(G12)另加一条要求:给了「两条路 + 建议 + 要你定什么」才记 1。主动说清「哪块查过没问题」并给依据,对应 C1,额外 +0.5。
- **raw** = 命中 + 归类 + 证据均值 × 12 + 表达 − 误报扣分。

原始分数受标准答案缺陷影响(下一节),所以 raw 只在同口径内可比。

---

## 汇总

| 维度 | A(快模式) | B(按七维审) | C(模块体检) |
|---|---|---|---|
| 命中 | 6.5 / 12(54%) | 8.5 / 12(71%) | 9.5 / 12(79%) |
| 误报 | 0 条 | 1 条(−1:包名/目录名口味) | 1 条(−1:包名/目录名口味) |
| 归类(按 12 项,未命中计 0) | 4.0 / 12(33%) | 5.5 / 12(46%) | 5.5 / 12(46%) |
| 归类(按已报条目,辅助看) | 4.0 / 6(66.7%) | 5.5 / 8(68.8%) | 5.5 / 8(68.8%) |
| 证据强度均值 | 2.8 / 5 | 3.3 / 5 | 3.3 / 5 |
| 表达质量 | 6.6 / 12 | 8.2 / 12 | 8.6 / 12 |
| raw | 50.7 | 60.8 | 62.2 |
| 命中条目 | G1 G2 G3 G5 G6 G12 + G10(0.5) | G1 G2 G5 G6 G7 G9 G11 G12 + G10(0.5) | G1 G2 G5 G6 G7 G9 G11 G12 + G10(0.5) |

三轮的证据强度都在 5 级满分之下是合理的:三轮都没有在被审目录里新建复现用例(只读约束),最高只到 4(读测试确认覆盖缺口)。

---

## 逐条对照

`G×` 表示命中且触发路径正确;`G×(0.5)` 表示只在证据/边界说明里点到、未作为结论提出;`—` 表示未报出。

| GT | A | B | C | 说明 |
|---|---|---|---|---|
| G1 Stats 指针无构造函数 → panic | G× | G× | G× | 三轮都给了具体触发(`&Checkout{}` → nil 解引用),级别都对 |
| G2 timeout 形同虚设 | G× | G× | G× | 三轮都指出了零引用 |
| G3 Code 字段从未被读取 | G× | — | — | B、C 与 G9 同根因合并成一条拍板,按标准答案的「同一根因只算一次」条款记在 G9 |
| G4 godoc 只读承诺 vs 实际写计数器 | — | — | — | **三条都没报,且 A 明确判定该承诺成立**——见「口径缺陷」 |
| G5 批次中途失败不回滚计数 | G× | G× | G× | 触发路径都对(第二单负金额 → 计数已加);A、B、C 都报成 P2,标准答案是 P1 → 归类 0.5 |
| G6 两份 moneyToCents 舍入不同 | G× | G× | G× | A 报 P1、B/C 报 P0,标准答案是 P2 → 归类 0.5 / 0 |
| G7 两侧 E_INVALID 非同一值 | — | G× | G× | A 未报;B、C 都给了 `errors.Is` 恒假的论证 |
| G8 refundWindow 硬编码 | — | — | — | **三条都没报,且 B、C 明确按「已命名、已注释、单一使用者」判为不算**——见「口径缺陷」 |
| G9 DiscountStrategy 单一实现零调用 | — | G× | G× | B、C 都归入过度设计/拍板,标准答案 P2 → 归类 0.5 |
| G10 测试盲区 | G×(0.5) | G×(0.5) | G×(0.5) | 三轮都只在证据里点了「每次都显式传 `Stats: &Stats{}`」,没有作为结论提出 |
| G11 测试用字符串比较断言 | — | G× | G× | B、C 都在 E_INVALID 那条的修法里写了「测试改用 `errors.Is`」 |
| G12 Percent 无范围校验 | G× | G× | G× | **B、C 放进了「需要你拍板」=正确处理**;A 报成 P2 缺陷 → 归类 0、表达 0.6 |
| C1 并发查过没问题 | +0.5 | +0.5 | +0.5 | 三轮都主动写明并给了依据(行号 + 锁范围推理),没有编造竞态 |

### 每条 GT 的命中归类明细

| GT | 命中 A / 归类 / 表达 | 命中 B / 归类 / 表达 | 命中 C / 归类 / 表达 |
|---|---|---|---|
| G1 | 1 / 1 / 1 | 1 / 1 / 1 | 1 / 1 / 1 |
| G2 | 1 / 1 / 1 | 1 / 1 / 1 | 1 / 1 / 1 |
| G3 | 1 / 1 / 1 | 0 / 0 / 0 | 0 / 0 / 0 |
| G4 | 0 / 0 / 0 | 0 / 0 / 0 | 0 / 0 / 0 |
| G5 | 1 / 0.5 / 1 | 1 / 0.5 / 1 | 1 / 0.5 / 1 |
| G6 | 1 / 0.5 / 1 | 1 / 0 / 1 | 1 / 0 / 1 |
| G7 | 0 / 0 / 0 | 1 / 1 / 1 | 1 / 1 / 1 |
| G8 | 0 / 0 / 0 | 0 / 0 / 0 | 0 / 0 / 0 |
| G9 | 0 / 0 / 0 | 1 / 0.5 / 1 | 1 / 0.5 / 1 |
| G10 | 0.5 / 0 / 0.5 | 0.5 / 0 / 0.5 | 0.5 / 0 / 0.5 |
| G11 | 0 / 0 / 0 | 1 / 0.5 / 0.6 | 1 / 0.5 / 0.6 |
| G12 | 1 / 0 / 0.6 | 1 / 1 / 0.6 | 1 / 1 / 1 |
| 合计 | 6.5 / 4.0 / 6.6 | 8.5 / 5.5 / 8.2 | 9.5 / 5.5 / 8.6 |

---

## 口径缺陷(本次评测暴露的标准答案问题)

这两条不是模型漏报,是标准答案自己站不住。修法已开票,**本文件的分数按修前的标准答案(v1)记录**,修后复算见下一节。

### G4:godoc 的「只读」承诺没有被兑现(判定有误)

标准答案写的是「文档承诺只读,实际写了 `c.Processed`(`:77`)与 `c.Stats.CheckedOut`(`:71`)」。但源码里那句承诺是有限定的:

```
// Checkout is read-only with respect to the order store: it never mutates
// orders and never writes back to the database.
```

限定语是「read-only **with respect to the order store**」——它承诺的是不动订单、不回写数据库。`c.Processed` 和 `c.Stats` 是 `Checkout` 自己的字段,不是订单存储,写它们不违反这句承诺。三轮里有两轮明确判定「该承诺成立」并给了推理(A:`checkout.go:67-78` 只读 `orders` 副本;B:逐行读 `Run` 确认无写操作)。把「有副作用」等同于「违反 read-only 承诺」是我当初写标准答案时的混淆。

B 轮提出的另一个角度更接近真实问题:承诺里提到的「订单存储 / 数据库」在这个模块里根本不存在,所以这条承诺没有对应对象——那是一条该进拍板的文档问题,不是「文档与实现矛盾」的 P1。

### G8:硬编码判定与 skill 自己的第 5 维细则冲突

标准答案要求把 `refundWindow = 24 * time.Hour` 当作硬编码报出。但 `references/dimensions.md` 第 5 维的「不算」条款写着:

> **不算**:真不变量(协议固定值、7 天),已命名、已注释、只有一个使用者。

同一维度的「算」条款又写着「它必须随环境/租户/调用方/**数据规模/版本**变化」——退款窗口会随业务口径变化,也就是随版本变化,按「算」条款应该报。同一节的两句话互相打架,B、C 两轮都明确按「不算」条款判了「硬编码:无发现」,并且把依据写出来了(已命名、已注释、单一使用者)。**模型没做错,是细则自相矛盾。**

需要人工拍板的是:退款窗口这类「业务政策值」到底算不算硬编码。我倾向要算——协议固定值(单位换算 100、HTTP 状态码)和业务政策值(退款窗口、账期)不是一回事,细则的括号里那个「7 天」例子恰好把业务政策值归进了「真不变量」,是根源。

---

## 标准答案漏掉的 13 条真缺陷

三路合计发现,全部可复核(每条都带具体触发路径)。这些是标准答案该补而没补的——当前 12 分制只覆盖了样本真实缺陷的一部分,所以基线偏乐观。

| # | 缺陷 | A | B | C |
|---|---|---|---|---|
| 1 | `ExpiresAt` 用**查询时刻** +24h,签发时刻根本没存(`issued` 只存金额)→ 退款窗口永远关不上 | P1 缺陷 | 拍板 | P1 缺陷 |
| 2 | 退款侧 `E_INVALID` 每次现造,与 `checkout.ErrInvalid` 非同一值 → `errors.Is` 恒假 | — | P2 | P2 |
| 3 | `Issue` 注释说「第二个返回值」表示是否新签发,实际是**第一个** | P2 | — | — |
| 4 | NaN / Inf 绕过 `< 0` 校验 → 静默返回荒谬总额 | P2 | 拍板 | P2 |
| 5 | 低于阈值的订单也被计入 `Stats.CheckedOut`,与 `Processed` 口径冲突 | P2 | P2 | P2 |
| 6 | `Checkout` 共享指针 + 非原子自增 → 并发丢计数 / data race | — | P1 | P2 |
| 7 | `issued` map 只增不减,无淘汰路径 → 内存单调增长 | — | P2 | P2 |
| 8 | 0 元退款被记成「已退款」→ 后续真退款静默变 no-op | — | — | P2 |
| 9 | `Refunder{}` 零值不可用(往 nil map 写 → panic) | — | — | P2 |
| 10 | `Order.ID` 全模块零使用 | — | — | P3 |
| 11 | `Issue` 收「元」、`IssuedCents` 返「分」→ 传结算总额进去是 100 倍退款 | — | — | P1 |
| 12 | 退款幂等只在进程内,重启/多实例会重复退款 | — | 拍板 | — |
| 13 | `PercentageDiscount.Apply` 先截断再减(total=105、10% → 95,少减半分) | — | 拍板 | 拍板 |

第 1 条最值得注意:三轮里有两轮把它当成**缺陷**报出,一轮当成**待拍板**。分歧点在「这个模块到底该不该有退款窗口」——在没有任何调用方的模块里,这确实更接近设计问题。标准答案对此没有任何条目,所以目前无法评分。

---

## v2 口径复算(若 G4、G8 按上面的结论修正)

把 G4、G8 移出「应当报出」(G4 移到「不该报」并附理由,G8 改为待人工确认的争议项),有效条目变 10 条:

| 维度 | A | B | C |
|---|---|---|---|
| 命中 | 6.5 / 10(65.0%) | 8.5 / 10(85.0%) | 9.5 / 10(95.0%) |
| 归类(按 10 项) | 4.0 / 10(40%) | 5.5 / 10(55%) | 5.5 / 10(55%) |
| 归类(按已报条目) | 66.7% | 68.8% | 68.8% |

命中差距拉开是正常的:G4、G8 两条谁都没报,删掉它们对三轮是同量抬升,但相对差距不变(4.6 个百分点 → 30 个百分点的绝对差)。

**注意**:这是**只动 G4、G8 两条**的假设复算。实际修复后 GT 扩到了 25 条(补进 13 条真缺陷),分母变成 25,**上面这张表也不能代表新口径**——新口径必须重跑。

**后续比较用哪个口径**:v1 口径留给本次记录(`evals/baseline.md` 现状),后续改动要与本次对比就用 v1;等标准答案扩充到 v2(补上那 13 条)之后,需要**重跑一轮**才能得到与 v2 同口径的基线,否则 v1 与 v2 的分数不可比。

---

## 方法论局限

- **样本与标准答案同一人写**:基线偏乐观,真实水平要打折看。这是本次评测最大的系统性偏差。
- **每种问法只跑 1 轮**:没有方差估计。同一 skill 两轮之间有波动是已知的(README 里写了),所以单轮差异小于 2 个百分点的结论不要当结论。
- **快模式轮的 GT 覆盖天然不全**:G8 / G9 / G10 / G11 属设计面与测试面,快模式按契约本就不报。拿 A 轮的命中分去比全量轮是不公平的,上面的对比只应看「相对各自口径」。
- **数值复算用的是 IEEE-754 等价推演**(.NET double 模拟 Go 的 `int64()` 截断与 `math.Round`),没有在被审目录里落 Go 探针(C 轮自己声明了这点)。Go 探针会给出同样的结论,但严格性差一点。
- **A 轮有一条依赖「该目录无 git 历史」这个前提**:五个文件全算「本次引入」。换一个真实的 PR,A 轮的行为会不同(届时要区分既有与新引入)。
- **没有做变异对照**:没有改坏代码去验证每条 finding 是否真能让测试变红,所以「finding 是真的」靠的是读码 + 数值复算,不是测试红。

---

## 相关文件

| 文件 | 作用 |
|---|---|
| `evals/ground-truth.md` | 标准答案(v1)。**本文件的分数按它计算** |
| `evals/rubric.md` | 评分表与五维口径 |
| `evals/runbook.md` | 怎么跑一次评测 |
| `evals/fixtures/tiny-shop/` | 样本(约 150 行 Go) |
| 仓外 `scratchpads\dcr-eval\runs\round-{A,B,C}.md` | 三轮原始输出,不入版本控制 |
Loading
Loading