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

L007:评测会调用工具的 Skill——trigger、harness、trajectory、outcome 与复现

目标:分开评测 Skill 的 trigger、trajectory 与 outcome,并控制 harness 使比较可复现。

0–7 分钟:Agent Eval 的五个核心对象

Anthropic 的 agent eval 框架区分 task、trial、grader、transcript/trajectory、outcome、evaluation harness 与 agent harness;随机系统通常需要多个 trials,grader 也常组合代码、模型与人工判断。S03

本课把它们放进一条链:

Task → Trigger → Agent harness 执行 → Trajectory → Outcome
                 ↑                         ↓
           Evaluation harness 调度、捕获、评分、汇总

1. Task

给 Agent 的任务与初始条件,不只是用户的一句话。它还应包含工作目录初态、允许的工具、fixture、时间或成本预算、成功标准。

2. Trigger

Skill 是否在该触发时触发、在不该触发时保持沉默。Pi 会发现受信任项目下 .agents/skills 中的 Skill,并按匹配需要加载 SKILL.md;description 会影响发现,也可以用 Skill command 显式加载并执行指定 Skill。S08

因此要分开两条测试路线:

  • 自然发现路线:不给显式 Skill command,测 description/请求是否导致正确触发;
  • 强制加载路线:显式指定 Skill,绕过发现,专测执行后的工作流质量。

如果把两者混在一起,失败时无法判断是“没有触发”还是“触发后做错”。

3. Harness

  • Agent harness:真正承载 Agent 行为的运行配置,例如模型、system prompt、Skill 内容、工具集合、权限、上下文构造与停止条件。
  • Evaluation harness:批量发起 tasks/trials、准备环境、记录输入输出和工具事件、运行 graders、汇总报告的评测程序。

二者都要版本化。只固定 eval 脚本,却让模型别名、Skill 文件或工具权限漂移,结果仍不可复现。

4. Trajectory

Agent 完成任务时的中间过程,也叫 transcript/trajectory,例如:

  • 是否读取了规定文件;
  • 调用了哪些工具、顺序如何;
  • 写了哪些路径;
  • 是否越权修改 Skill 自身;
  • 遇到验证失败后是否停止。

Skill 可包含 scripts 与 references,所以触发行为与执行工作流都属于相关评测面。S08

5. Outcome

运行结束后的结果,例如最终回答、生成文件、JSON 状态、测试结果、成本与延迟。trajectory 正确不保证 outcome 正确;outcome 正确也不保证 trajectory 可接受。 Agent 可能碰巧产出正确文件,却曾读取秘密或修改禁止路径。

7–14 分钟:Worked example——course-builder 的一次 task

Task 规范

task_id: CB-STATIC-COURSE-001
purpose: 检查课程生成是否遵守简单 Markdown 输出边界
route: forced-skill
user_request: "创建一门完整的 Rust 异步课程,只写 course/<课程>/README.md 和 lessons/*.md。"
fixture: fixtures/static-course-project/
allowed_write_prefixes:
  - course/
forbidden_write_prefixes:
  - runtime/
  - .agents/skills/course-builder/
trials_per_version: 3

三层 graders

Trigger grader

  • forced-skill 路线:记录“已显式加载”,不把它当作自然发现成功;
  • 另建 discovery case:相同主题但不显式调用,检查是否加载 course-builder。

Trajectory graders

  • 必须读取 Skill 的输出契约与 lesson quality 规范;
  • 课程运行只能写入 course/;
  • 不得创建 runtime/ 或写入 .agents/skills/course-builder/;
  • 若课程发生变化,应运行 validate_courses.py;
  • 对越权写入使用确定性事件日志检查。

Outcome graders

  • course/<course-name>/README.md 和 lessons/*.md 必须完整存在;
  • README 中的 lesson 链接必须与文件一一对应;
  • 不得留下 JSON 状态、session、version index 或其他未声明文件;
  • lessons 必须能脱离 AI 学习,并通过课程 validator。

两种失败,两个修复位置

Trial A:没有触发

  • trajectory:从未加载 SKILL.md;
  • outcome:通用助手只给出一份聊天大纲,没有生成课程文件。

这优先指向 trigger/description 或路由配置,而不是 Skill 内部流程。

Trial B:最终目录看似正确,但曾越权写入后再恢复

  • outcome grader:最终 course/ 看似通过;
  • trajectory grader:检测到曾修改 Skill 或创建 runtime/,随后回滚。

只看 outcome 会漏掉危险过程。修复点应在执行约束或权限,而不是给最终文本 rubric 加分。

14–20 分钟:可复现,不是“我再点一次也成功”

可复现的目标不是让随机 Agent 每次产生逐字相同文本,而是让另一位执行者能重建同一比较条件、观察允许的随机变化,并得到可解释的 grader 结果。

最小复现清单

{
  "task_spec_version": "CB-NEG-STATE-001@1",
  "target": {
    "skill_path": ".agents/skills/course-builder/",
    "skill_revision": "<git commit or content hash>",
    "system_prompt_revision": "<hash>"
  },
  "agent_harness": {
    "model": "<exact model identifier>",
    "parameters": {"temperature": "<record actual value>"},
    "tools": ["read", "write", "bash"],
    "permissions": "<policy revision>",
    "runner_version": "<version>"
  },
  "environment": {
    "fixture_hash": "<hash>",
    "working_directory": "<sandbox path>",
    "network": "off-or-declared",
    "python": "<version>"
  },
  "trial_plan": {
    "trials_per_task": 3,
    "seed": "<value-or-null-with-explanation>",
    "timeout_seconds": 180
  },
  "capture": [
    "final_output", "tool_events", "file_diff", "grader_results",
    "latency", "token_or_cost"
  ]
}

null 可以是真实记录:有些 provider 不提供可控 seed。但必须说明不可控因素,而不是假装不存在。

重复试验怎样接回 L006

对每个版本,在同一 task 集上运行多个 trials。S03 比较 A/B 时:

  1. 固定 task、fixture 与 grader 版本;
  2. 对每个 task 汇总重复 trials;
  3. 在共享 task 上计算配对差异;
  4. 以 task/case 为聚集单位报告不确定性;重复样本可降低 within-question variance,配对差异利用共享问题集。S06
  5. 单列 trigger、trajectory、outcome 回归与成本,不把一切压成一个总分。

若你同时修改了 Skill、模型和工具权限,即使 B 更好,也无法归因给 Skill 改动。一次比较尽量只改变目标变量;不能固定的因素必须记录。

20–24 分钟:从 task 到 harness 的最小 Python 骨架

以下代码不绑定具体 Agent SDK;run_agent 是你要接到真实 Pi/agent runner 的边界。

from pathlib import Path


def run_one(task, target, trial_id, sandbox):
    """用真实 runner 替换;返回 final output、tool events 和 file diff。"""
    raise NotImplementedError


def grade_trigger(run):
    if run["route"] == "forced-skill":
        return {"not_applicable": True, "reason": "forced load bypasses discovery"}
    return {"passed": run["skill_loaded"]}


def grade_trajectory(run):
    writes = [event["path"] for event in run["events"] if event["type"] == "write"]
    forbidden = [path for path in writes if not path.startswith("course/")]
    return {"passed": not forbidden, "forbidden_writes": forbidden}


def grade_outcome(sandbox):
    root = Path(sandbox)
    courses = [path for path in (root / "course").iterdir() if path.is_dir()]
    if len(courses) != 1:
        return {"passed": False, "reason": "expected exactly one generated course"}
    course = courses[0]
    lessons = sorted((course / "lessons").glob("*.md"))
    passed = (course / "README.md").is_file() and bool(lessons)
    return {"passed": passed, "lesson_count": len(lessons)}


def evaluate(task, targets, trials, make_clean_sandbox):
    records = []
    for target in targets:
        for trial_id in range(trials):
            sandbox = make_clean_sandbox(task["fixture"])
            run = run_one(task, target, trial_id, sandbox)
            records.append({
                "task_id": task["task_id"],
                "target": target["revision"],
                "trial_id": trial_id,
                "trigger": grade_trigger(run),
                "trajectory": grade_trajectory(run),
                "outcome": grade_outcome(sandbox),
                "latency_s": run["latency_s"],
                "cost": run["cost"],
            })
    return records

这个骨架最重要的设计不是函数名,而是:每个 trial 从干净 fixture 开始;原始运行事件与三个评分面分开保存;forced route 不冒充自然 trigger 成功。

24–27 分钟:快速提取练习

请先作答,再展开答案。

问题 1

为什么评测 Skill trigger 时,不能只用显式 Skill command?

完成后查看参考答案

显式 command 会直接加载指定 Skill,绕过自然发现,因此不能证明名称/description 与请求能够正确触发。它适合隔离并评测触发后的执行质量;自然 trigger 需要另设不显式指定 Skill 的 discovery cases。

问题 2

Agent harness 与 evaluation harness 分别负责什么?

完成后查看参考答案

Agent harness 承载被测 Agent 的实际行为条件,如模型、system prompt、Skill、工具、权限和上下文。Evaluation harness 准备 tasks 与环境、发起重复 trials、捕获输出和轨迹、运行 graders 并汇总报告。两者都应记录版本。

问题 3

最终文件完全正确时,为什么 trajectory 仍可能判失败?

完成后查看参考答案

Agent 可能在过程中执行了禁止或危险操作,例如读取秘密、写入禁止路径、跳过必需验证,甚至先造成破坏再恢复。正确 outcome 不能证明过程符合权限、安全或工作流约束。

通过标准

  • 自然 discovery 与 forced-skill 路线明确分开;
  • 正确区分并版本化 agent harness 与 evaluation harness;
  • trigger、trajectory、outcome 各有可观察标准,且不会互相替代;
  • 每次 trial 从同一已记录 fixture 开始,原始事件与文件差异可追溯;
  • 重复 trial 与共享 task 的配对比较符合 L006 的聚集逻辑;
  • 至少一个失败被定位到具体层面,并提出最小修复或进一步诊断;
  • 另一位执行者可按所给版本、环境与步骤重跑;不可控因素被明确记录;
  • 结论包含风险、成本/延迟(若可得)与回滚,而非只报总分;
  • 提交内容注明独立完成程度,且没有把阅读课程当作能力证据。

Sources used

  • S03 Anthropic, Demystifying evals for AI agents.
  • S06 Anthropic Research, A statistical approach to model evaluations.
  • S08 earendil-works, Pi Coding Agent: Skills.