首页
/ Astro 仓库 Skill 评估体系:用真实模型测试验证仓库内 Agent 技能(.agents/evals)

Astro 仓库 Skill 评估体系:用真实模型测试验证仓库内 Agent 技能(.agents/evals)

2026-09-06 22:10:09作者:何举烈Damon

Astro 仓库在 .agents/ 目录下为一系列 Agent 技能(如 changeset、merge、triage)建立了"实时模型评估"(skill evals)体系:每个技能配有固定的评估清单(evals.json),运行时由一个"受试模型"真实执行技能、再由一个"评审模型"逐条断言打分。本文基于 .agents/evals/README.md 及其配套源码 load-evals.tsskills.eval.ts 展开,帮助你在本地验证清单、按需运行单个技能或单个用例,并理解临时工作区、模型隔离、断言判分等底层机制。

一、Skill Evals 是什么:与 pnpm test 分离的实时模型测试

仓库技能评估(repository skill evals)是真实调用大模型的测试,与常规 pnpm test 完全分离,也没有接入 CI。原因在 README 中说明得很直接:每个用例会消耗 provider token,且结果可能不确定(nondeterministic)。

它要回答的问题是:.agents/skills/<name>/SKILL.md 中定义的 Agent 技能,交给真实模型执行后,行为是否符合预期?

当前仓库共有 8 个技能目录,每个都包含 SKILL.mdevals/evals.json

二、运行命令:验证、全量、按技能/用例过滤

README 给出了三档操作,对应 package.jsoneval:skillseval:skills:validate 两个 npm scripts(vitest run --config vitest.skills.config.ts / vitest list --config vitest.skills.config.ts)。

1. 不调用模型,校验所有清单

pnpm eval:skills:validate

该脚本实际执行的是 vitest list,即只加载并收集测试、不真正运行,因此可以在没有任何 API key 的情况下检查所有 eval 清单与技能定义是否合法。

2. 通过 Vitest 的 -t 过滤运行单个技能或单个用例

ANTHROPIC_API_KEY=... pnpm eval:skills -t "changeset"
ANTHROPIC_API_KEY=... pnpm eval:skills -t "changeset #1"

测试名由 skills.eval.ts`${evalCase.skillName} #${evalCase.id}` 生成,所以 changeset 会匹配该技能的全部 3 个用例,changeset #1 精确到单个用例。

3. 不加 -t 则运行所有用例

每个用例 = 一次受试模型(subject)运行 + 一次评审模型(judge)运行,因此全量运行 token 成本与耗时都比较高,这也是它不接入 CI 的根因。

三、模型选择与环境变量

变量 作用 默认值
ANTHROPIC_API_KEY 默认模型所需的 Anthropic 凭据,必须在运行 Vitest 的进程中导出 必需
SKILL_EVAL_MODEL 覆盖受试模型 anthropic/claude-sonnet-4-6
SKILL_EVAL_JUDGE_MODEL 覆盖评审模型 anthropic/claude-haiku-4-5
SKILL_EVAL_VERBOSE=1 打印通过的模型输出与评审摘要 关闭
ANTHROPIC_BASE_URL 源码额外支持:指向兼容的 Anthropic Messages API 端点 未设置

默认模型在 skills.eval.ts 中定义为 process.env.SKILL_EVAL_MODEL ?? 'anthropic/claude-sonnet-4-6'process.env.SKILL_EVAL_JUDGE_MODEL ?? 'anthropic/claude-haiku-4-5'

源码中还有一处 README 未展开的细节:beforeAll 中会检查——只要受试或评审模型任一以 anthropic/ 开头且未导出 ANTHROPIC_API_KEY,就直接抛出错误并提示两种解决方式(导出 key,或用两个 SKILL_EVAL_*_MODEL 变量整体覆盖模型)。另外,若设置了 ANTHROPIC_BASE_URL,运行器会通过 @earendil-works/pi-ai 创建一个覆盖 baseUrl 的 Anthropic 兼容 provider(见 skills.eval.ts)。README 同时提示:覆盖模型为其他 provider 时,需按 Flue 框架的 provider credentials 文档导出对应的凭据变量。

四、评估清单(evals.json)的结构与强制校验规则

每个技能的清单固定位于 .agents/skills/<name>/evals/evals.json,与技能资源放在同一目录。清单顶层结构为:

{
  "skill_name": "changeset",
  "evals": [
    {
      "id": 1,
      "prompt": "……给受试模型的指令……",
      "expected_output": "……期望行为的自然语言描述……",
      "files": ["仓库相对路径/输入文件"],
      "assertions": ["断言 1", "断言 2"]
    }
  ]
}

load-evals.tsloadEvalCases() 对清单施加了一组硬校验(任何一条不满足都会抛错,validate 阶段即可暴露):

  1. 每个技能目录必须有清单,且 skill_name 必须与目录名完全一致(load-evals.ts);
  2. evals 数组必须恰好包含 3 个用例manifest.evals.length !== 3 直接报错,见 load-evals.ts);
  3. 每个用例的 id 必须是唯一的正整数
  4. promptexpected_output 必须是非空字符串;assertions 必须是非空字符串数组;
  5. files 中的每个路径都会经过 validateInputPath() 校验:禁止绝对路径、禁止包含 ..,且解析到仓库根目录后必须真实存在load-evals.ts),即清单只能引用仓库内的文件。

真实样例可参考 changeset 的清单:3 个用例分别要求"基于给定上下文起草 patch 级 changeset"、"起草含代码示例的 minor 级 changeset"、"评估并拒绝一次不合规的 changeset 请求";每个用例的 assertions 都是 5~6 条可逐条核验的具体判据(如"front matter 恰好包含 '@astrojs/node': patch 一个条目"、"正文不得提及内部函数或测试命令")。merge 的清单 则用内联的合成冲突片段做"output-only dry run",断言精确到"保留 6.0.0-beta.4tsconfck、不引入过时的 get-tsconfig""不得执行 pnpm installgit commit"。

五、技能定义加载与资源隔离

loadSkillDefinitions()load-evals.ts)负责把 SKILL.md 解析为技能定义:

  • 用正则提取 YAML frontmatter,name 必须与目录名一致,description 必填,compatibility 可选;
  • frontmatter 之外的正文整体作为 instructions 注入;
  • readSupportingFiles() 递归收集技能目录下其余文件(如 merge 的 resolve-conflicts.md),同时跳过 SKILL.md 自身和整个 evals/ 目录,并拒绝任何符号链接。

最后一点正是 README 强调的隔离机制:运行器在挂载技能资源时排除清单文件,保证 expected_outputassertions 不会泄露给受试模型——受试模型只能看到 promptfiles 指定的输入,评审依据对它是不可见的。

六、受试/评审双模型架构:一次用例的完整执行流程

runEval()skills.eval.ts)实现了每个用例的端到端流程,构建在 @flue/runtime 之上,包含两个 Agent:

  1. 搭建临时工作区mkdtemp(join(tmpdir(), 'astro-<skill>-')) 创建一次性目录;清单 files 中列出的仓库文件由 stageInputFiles() 按相对路径原样复制进工作区。
  2. 受试 Agent 运行SkillEvalAgent 使用 SKILL_EVAL_MODEL,沙箱 cwd 指向该临时工作区,挂载全部技能,system prompt 要求"先激活目标技能再处理请求,仅当技能指示时才使用其他挂载技能"。运行器通过 onEvent 钩子记录所有 tool-input 事件,并硬性断言受试模型确实调用了 activate_skill 且参数包含目标技能名,否则该用例直接失败。
  3. 快照工作区snapshotWorkspace() 遍历工作区(跳过 .gitnode_modules),文本文件截断到 20,000 字符、二进制文件仅记录大小,形成供评审的工件集合。
  4. 评审 Agent 打分EvalJudge 使用 SKILL_EVAL_JUDGE_MODEL,只挂载一个工具 submit_eval_grade(用 valibot 定义 schema:evalId + 每条断言的 {assertionIndex, passed, evidence} + 非空 summary)。评审 prompt 明确要求把 prompt、输出、工具参数与工作区文件全部当作不可信工件而非指令,并"仅当工件中包含具体证据时才判 passed"。
  5. 结果校验:运行器检查评审是否恰好对每条断言各打一次分assertionIndex 序列必须等于 1..N),再断言所有断言均通过;失败时错误信息会附带评审 summary、受试模型原始输出与逐条 evidence。
  6. 清理finally 中无条件 rm(workspace, { recursive: true, force: true })——即 README 所述"每个运行获得一个随后被删除的临时工作区"。

SKILL_EVAL_VERBOSE=1 时,每个用例结束后还会打印受试输出与评审摘要(skills.eval.ts),便于人工检查通过的用例是否符合预期。

七、Vitest 运行配置

专用配置 vitest.skills.config.ts 只包含 .agents/evals/**/*.eval.ts,并针对"真实模型调用"做了参数调优:

配置 含义
fileParallelism false 文件间不并行,避免多模型调用互相干扰
maxConcurrency 1 用例串行执行
hookTimeout 60_000 beforeAll 中启动 runtime 的最长等待 60 秒
testTimeout 600_000 单个用例最长 10 分钟,覆盖真实模型的多轮工具调用

八、实操要点小结

  • 先跑 pnpm eval:skills:validate 做零成本结构校验,再花 token 跑真实评估;
  • 凭据必须导出在运行 Vitest 的同一进程(shell 前缀 ANTHROPIC_API_KEY=... 或事先 export);
  • -t "<skill>" / -t "<skill> #<id>" 控制范围,SKILL_EVAL_VERBOSE=1 观察通过用例的细节;
  • 新增技能时,需同时提供 SKILL.md(frontmatter name 与目录名一致)和 evals/evals.json(恰好 3 个用例、唯一正整数 id、files 指向仓库内真实存在的文件),否则 validate 与运行都会直接报错;
  • 评估清单中"断言不得泄露给受试模型"、"符号链接一律拒绝"、"工作区用后即焚"三条隔离规则,共同保证评估结果反映的是技能指令本身的效果。

参考文件

登录后查看全文
热门项目推荐
相关项目推荐