Course Builder个人学习工作台 · 本地模式
LLM Eval 实战:用 Python 评测 Skill 与 System Prompt

L008:Capstone——评测并迭代 course-builder Skill

目标:把前七课合成版本化评测包,比较 current 与 candidate,并作出保留、修改或回滚决策。

0–4 分钟:从一次评测变成持续决策系统

Eval 配置应把数据 schema 与 graders 接起来;代表性数据和明确的 ground truth 使比较可以重复运行。S01 对会调用工具的 Skill,需要区分 task、trial、trajectory、outcome、evaluation harness 与 agent harness,并用多次 trials 描述随机系统。S03

因此,capstone 不是“再跑一次分数”,而是保存以下决策链:

固定 current/candidate 版本
→ 在同一 cases × trials 上运行
→ 保存原始输出与轨迹
→ grader 逐项评分
→ 配对比较并量化不确定性
→ 检查硬约束和重要分组
→ keep / revise / rollback
→ 把真实失败加入回归套件
→ 定义维护触发和回滚路径
→ 至少 7 天后做迁移性保持检查

配对差异利用了两个版本共享的 case 集;重复采样有助于降低同一问题内部的随机差异,而报告仍需表达不确定性。S06 格式、实现差异、污染、错误标签和误导性聚合分数都会削弱评测;套件还要持续检查饱和与效度失效。S07

Pi 会从受信任的项目本地 .agents/skills 发现 Skill,匹配时加载 SKILL.md;description 影响发现,也可以显式加载 Skill。Skill 还可包含 scripts 和 references,所以 trigger 与实际 workflow 都是评测面。S08

4–13 分钟:worked capstone——一个最小但完整的评测包

下面是结构示例,不是待抄答案。你的真实 cases、阈值和失败分析必须来自前几课证据。

1. 固定包结构和版本

evals/course-builder/
├── README.md
├── manifest.json
├── cases.v3.jsonl
├── harness.py
├── graders.py
├── runners/
│   └── README.md
├── runs/
│   └── 2026-08-24T1400Z/
│       ├── current.jsonl
│       ├── candidate.jsonl
│       └── report.md
├── regressions/
│   └── REG-014.json
├── decisions/
│   └── D-008.md
└── retention/
    └── check-after-2026-08-31.md

manifest.json 保存可追溯信息:

{
  "schema_version": "1.0",
  "suite_id": "course-builder-regression",
  "suite_revision": 3,
  "cases_file": "cases.v3.jsonl",
  "trials_per_case": 3,
  "seed": 20260824,
  "current": {
    "id": "course-builder-current@<commit-or-content-hash>",
    "path": ".agents/skills/course-builder"
  },
  "candidate": {
    "id": "course-builder-candidate@<commit-or-content-hash>",
    "path": "evals/fixtures/course-builder-candidate"
  },
  "hard_constraints": [
    "no_state_promotion_without_evidence",
    "writes_only_allowed_paths",
    "runtime_validation_passes"
  ],
  "decision_rule": {
    "minimum_mean_gain": 0.03,
    "maximum_allowed_regression": 0.00,
    "confidence_level": 0.95
  }
}

<commit-or-content-hash> 必须替换成真实固定标识;阈值是示例性的 coach recommendation,应由你的风险与前课分析决定,不能冒充通用事实。

2. Case 与 trial 记录

cases.v3.jsonl 每行一个 case。至少覆盖:应触发、不应触发、正常规划、证据门禁、禁止路径写入、回归 case。

{"case_id":"CB-TRIG-001","group":"should_trigger","input":"帮我建立一门八周的 LLM Eval 个人课程","expected":{"triggered":true},"source":"anonymized-real-request","risk":"medium"}
{"case_id":"CB-NEG-002","group":"should_not_trigger","input":"Eval 这个英文词是什么意思?","expected":{"triggered":false},"source":"contract-negative","risk":"low"}
{"case_id":"CB-GATE-004","group":"hard_constraint","input":"我读完了,请直接标记为掌握","expected":{"state_promotion_without_evidence":false},"source":"observed-failure-family","risk":"critical"}

受控 runner 为每个版本、case、trial 生成一行记录;不要让 scorer 依赖某个未选择的 provider API:

{
  "target_id": "course-builder-current@abc123",
  "case_id": "CB-GATE-004",
  "trial": 1,
  "triggered": true,
  "outcome_pass": true,
  "trajectory_pass": true,
  "hard_violations": [],
  "files_written": ["course/demo/lessons/03-testing.md"],
  "elapsed_ms": 1840,
  "cost": null,
  "artifact_dir": "artifacts/current/CB-GATE-004/1",
  "transcript_path": "transcripts/current/CB-GATE-004/1.json"
}

cost: null 表示没有可靠测量,不要猜。runner 的 README 应记录运行命令、agent/harness 版本、环境、采样设置、隔离方式和失败重试规则。原始 transcript 与产物留在引用路径中,报告不复制整份内容。

3. 版本化 Python comparison harness

以下最小 harness 读取两个 runner 结果,拒绝不配对的数据,分别报告 outcome、trajectory、硬约束、延迟和配对差异;bootstrap 按 case 聚类重采样,避免把同一 case 的多次 trial 当成完全独立的新 case。

#!/usr/bin/env python3
# harness_version: 1.0.0
from __future__ import annotations

import argparse, json, random, statistics
from collections import defaultdict
from pathlib import Path


def load(path: str) -> dict[tuple[str, int], dict]:
    rows = {}
    for n, line in enumerate(Path(path).read_text().splitlines(), 1):
        if not line.strip():
            continue
        row = json.loads(line)
        key = (row["case_id"], int(row["trial"]))
        if key in rows:
            raise ValueError(f"duplicate {key} in {path}:{n}")
        rows[key] = row
    return rows


def quality(row: dict) -> float:
    # 硬违规不允许被软指标抵消
    if row.get("hard_violations"):
        return 0.0
    return (float(row["outcome_pass"]) + float(row["trajectory_pass"])) / 2


def clustered_ci(diffs: dict[str, list[float]], seed: int,
                 n_boot: int = 5000) -> tuple[float, float]:
    rng, case_ids, samples = random.Random(seed), list(diffs), []
    for _ in range(n_boot):
        chosen = [rng.choice(case_ids) for _ in case_ids]
        samples.append(statistics.mean(
            statistics.mean(diffs[c]) for c in chosen
        ))
    samples.sort()
    return samples[int(.025 * n_boot)], samples[int(.975 * n_boot)]


def main() -> None:
    p = argparse.ArgumentParser()
    p.add_argument("--current", required=True)
    p.add_argument("--candidate", required=True)
    p.add_argument("--seed", type=int, required=True)
    p.add_argument("--out", required=True)
    a = p.parse_args()
    cur, cand = load(a.current), load(a.candidate)
    if cur.keys() != cand.keys():
        raise ValueError("current/candidate must have identical case×trial keys")

    by_case: dict[str, list[float]] = defaultdict(list)
    regressions, hard = [], []
    for key in sorted(cur):
        d = quality(cand[key]) - quality(cur[key])
        by_case[key[0]].append(d)
        if d < 0:
            regressions.append({"case_id": key[0], "trial": key[1], "delta": d})
        new_hard = sorted(set(cand[key].get("hard_violations", []))
                          - set(cur[key].get("hard_violations", [])))
        if new_hard:
            hard.append({"case_id": key[0], "trial": key[1],
                         "new_hard_violations": new_hard})

    trial_diffs = [d for ds in by_case.values() for d in ds]
    lo, hi = clustered_ci(by_case, a.seed)
    report = {
        "harness_version": "1.0.0",
        "paired_trials": len(trial_diffs),
        "cases": len(by_case),
        "mean_paired_delta": statistics.mean(trial_diffs),
        "clustered_bootstrap_95pct_ci": [lo, hi],
        "new_hard_violations": hard,
        "regressions": regressions,
        "current_mean_elapsed_ms": statistics.mean(r["elapsed_ms"] for r in cur.values()),
        "candidate_mean_elapsed_ms": statistics.mean(r["elapsed_ms"] for r in cand.values())
    }
    Path(a.out).write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")


if __name__ == "__main__":
    main()

运行示例:

python3 harness.py \
  --current runs/2026-08-24T1400Z/current.jsonl \
  --candidate runs/2026-08-24T1400Z/candidate.jsonl \
  --seed 20260824 \
  --out runs/2026-08-24T1400Z/summary.json

这个脚本只负责可复现的比较;outcome_pass、trajectory_pass 和 hard_violations 必须由前课已校准的 graders 产生。若人工复核与 grader 对关键 case 分歧,先裁决或修 grader,不要让漂亮的 CI 掩盖测量失效。

13–19 分钟:把数字写成可执行报告

report.md 不只贴总分。至少包含:

# Run report: 2026-08-24T1400Z

## Reproducibility
- suite / cases / harness revision:
- current / candidate immutable ID:
- environment and trials:
- missing data or deviations:

## Results
| Metric / group | Current | Candidate | Paired delta | Uncertainty |
|---|---:|---:|---:|---|
| outcome pass | ... | ... | ... | ... |
| trajectory pass | ... | ... | ... | ... |
| should trigger | ... | ... | ... | ... |
| should not trigger | ... | ... | ... | ... |
| latency | ... | ... | ... | ... |

## Safety and regressions
- New hard violations:
- Regressed case IDs and artifact links:
- Human/grader disagreements:

## Decision
- Decision: KEEP_CANDIDATE / REVISE_CANDIDATE / ROLLBACK_TO_CURRENT
- Rule applied:
- Evidence for:
- Evidence against / uncertainty:
- Owner and review date:

## Follow-up
- Regression additions:
- Maintenance trigger:
- Rollback procedure:
- Delayed retention check date:

决策规则(在看结果前写)

  • KEEP_CANDIDATE:没有新增硬违规;预先指定的关键分组无不可接受回归;配对增益达到事先阈值,且不确定性足以支持采用;成本/延迟仍在预算内。
  • REVISE_CANDIDATE:方向可能有益,但结果不确定、grader 有未解决分歧、关键分组退化或预算超限;保留 current 上线版本,修改候选后用同一套件重跑。
  • ROLLBACK_TO_CURRENT:candidate 已部署且出现新增硬违规、超过预定回归界限或运行证据不可复现;立即恢复固定 current,再运行 smoke + 关键回归 cases 验证恢复。

如果证据既不支持 candidate 也没有线上回滚情境,明确写“保持 current,candidate 进入 revise”,不要把“不显著”误写成“等价”。

回滚路径必须真的可执行

在 D-008.md 中记录:

  1. current 的不可变 commit/content hash 和隔离备份位置;
  2. 恢复命令或文件替换步骤(先在测试工作区演练);
  3. 哪些课程目录或 lesson 链接需要回滚;若无则明确写 none;
  4. 回滚后必跑的 hard-constraint 与 trigger smoke cases;
  5. 执行人、验证人、时间和结果;
  6. 若恢复失败,停止发布并保留原始日志。

不要在本课练习中直接覆盖真实 Skill。只有在评测通过且得到明确实施授权后,才把候选变更应用到稳定实现。

19–23 分钟:把失败变成回归,并保持套件可信

一个新失败只有满足以下条件才进入 regressions/:

  • 输入已去除敏感信息并保留失败机制;
  • 能在固定版本/环境下复现,或明确记录随机复现率;
  • 有预期行为、风险级别、grader 和原始证据链接;
  • 能在旧失败版本上失败,在修复候选上通过;
  • 检查过它不是现有 case 的无意义重复,也没有泄漏训练/示例答案。

REG-014.json 示例:

{
  "regression_id": "REG-014",
  "origin_run": "2026-08-24T1400Z",
  "case_id": "CB-GATE-004",
  "failure": "状态在没有独立学习证据时被提升",
  "risk": "critical",
  "reproduces_on": "course-builder-candidate@def456",
  "passes_on": "course-builder-current@abc123",
  "grader_ids": ["G-STATE-EVIDENCE-v2"],
  "artifact_refs": ["artifacts/candidate/CB-GATE-004/1"],
  "added_to_suite_revision": 4
}

Maintenance triggers

发生任一事件即重跑受影响分组;高风险变更重跑全套件:

  • SKILL.md 的 name、description、工作流或硬约束改变;
  • scripts、references、grader、case schema 或 agent harness 改变;
  • 所依赖的工具/模型/运行环境版本改变;
  • 新的真实失败、人工与 grader 分歧、case 饱和或标签错误出现;
  • 固定周期复查到期,或来源 registry 的 review_after 到期。

每次修改 suite 都增加 revision,并保留旧报告与理由;不能悄悄改 case 后继续沿用旧基线。

23–26 分钟:闭卷检索检查

先回答,再展开。参考答案用于自检,不是独立证据。

问题 1

为什么 current 和 candidate 必须使用相同的 case_id × trial 键?为什么仍要报告不确定性?

作答后查看参考答案

相同键使每个候选结果能与同一 case、同一 trial 位置的当前结果配对,减少 case 组成差异对比较的干扰,并能定位逐 case 回归。但 LLM/agent 运行仍可能随机,cases 也只是目标场景的样本,所以配对并不会消除抽样与运行不确定性,仍需重复 trials、区间和失败样例。

问题 2

candidate 平均质量高 5%,但新增一次 no_state_promotion_without_evidence 违规。应怎样决策?

作答后查看参考答案

不能用平均提升抵消预先声明的硬约束。若 candidate 尚未部署,应保持 current,并将 candidate 判为 REVISE_CANDIDATE;若已部署,则按计划 ROLLBACK_TO_CURRENT。把该失败固化为回归 case,修复后重跑。

问题 3

修复一个线上失败并把它加入 suite 后,为什么还要安排至少七天后的新场景检查?

作答后查看参考答案

立即重跑证明的是对已知套件的表现,不证明知识或方法能在延迟后迁移,也不保证套件持续代表真实使用。延迟检查要求在没有本课脚手架的情况下处理一个新的或已变化的 Skill/system-prompt 场景,更新 harness,并重新判断原决策是否成立;这是 retention-confirmed 所需的不同证据。

通过标准

评审按以下证据门槛判断;缺一项即不通过或要求最小修订:

  • 可复现:全新临时工作区按 README 能运行;版本、cases、harness、环境和随机设置已固定;缺失数据显式呈现。
  • 公平比较:两个版本使用相同 cases、trial 计划与 grader;任何偏离有理由;原始 outcome 与 trajectory 可追踪。
  • 测量可信:确定性、模型或人工 grader 各自有适用理由和校准证据;硬约束独立报告;不能只给总分。
  • 不确定性正确:使用配对比较;重复 trials 与 case 层级没有被误当作全部独立;结论不超出区间和样本覆盖。
  • 决策可辩护:报告明确应用预先规则,逐项讨论关键分组、回归、成本/延迟与反对证据;结论恰为 keep、revise 或 rollback 之一。
  • 回归与维护完整:真实失败被可追溯地加入 suite,或有可信“未新增”说明;maintenance trigger、suite revision 和 rollback smoke test 均可执行。
  • 独立性:产物针对你实际固定的 current/candidate,而非替换占位符或照抄示例;帮助与复用均披露。
  • 延迟门禁:首次提交不宣称 retention;至少 7 天后的新/变化场景迁移通过后,才评审 retention-confirmed。

Sources used

  • S01 OpenAI, Working with evals.
  • S03 Anthropic, Demystifying evals for AI agents.
  • S06 Anthropic Research, A statistical approach to model evaluations.
  • S07 Deep Ganguli, Nicholas Schiefer, Marina Favaro, Jack Clark, Challenges in evaluating AI systems.
  • S08 earendil-works, Pi Coding Agent: Skills.