Astro 仓库 Skill 评估体系:用真实模型测试验证仓库内 Agent 技能(.agents/evals)
Astro 仓库在 .agents/ 目录下为一系列 Agent 技能(如 changeset、merge、triage)建立了"实时模型评估"(skill evals)体系:每个技能配有固定的评估清单(evals.json),运行时由一个"受试模型"真实执行技能、再由一个"评审模型"逐条断言打分。本文基于 .agents/evals/README.md 及其配套源码 load-evals.ts、skills.eval.ts 展开,帮助你在本地验证清单、按需运行单个技能或单个用例,并理解临时工作区、模型隔离、断言判分等底层机制。
一、Skill Evals 是什么:与 pnpm test 分离的实时模型测试
仓库技能评估(repository skill evals)是真实调用大模型的测试,与常规 pnpm test 完全分离,也没有接入 CI。原因在 README 中说明得很直接:每个用例会消耗 provider token,且结果可能不确定(nondeterministic)。
它要回答的问题是:.agents/skills/<name>/SKILL.md 中定义的 Agent 技能,交给真实模型执行后,行为是否符合预期?
当前仓库共有 8 个技能目录,每个都包含 SKILL.md 与 evals/evals.json:
- analyze-github-action-logs
- astro-code-review
- astro-developer(附带 architecture.md、constraints.md、debugging.md、testing.md)
- astro-pr-writer
- changeset
- merge(附带 resolve-conflicts.md、clean-changesets.md、fix-ci.md)
- triage(附带 diagnose/fix/reproduce/verify 子文档)
- writing-comments
二、运行命令:验证、全量、按技能/用例过滤
README 给出了三档操作,对应 package.json 中 eval:skills 与 eval: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.ts 的 loadEvalCases() 对清单施加了一组硬校验(任何一条不满足都会抛错,validate 阶段即可暴露):
- 每个技能目录必须有清单,且
skill_name必须与目录名完全一致(load-evals.ts); evals数组必须恰好包含 3 个用例(manifest.evals.length !== 3直接报错,见 load-evals.ts);- 每个用例的
id必须是唯一的正整数; prompt、expected_output必须是非空字符串;assertions必须是非空字符串数组;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.4 与 tsconfck、不引入过时的 get-tsconfig""不得执行 pnpm install 或 git commit"。
五、技能定义加载与资源隔离
loadSkillDefinitions()(load-evals.ts)负责把 SKILL.md 解析为技能定义:
- 用正则提取 YAML frontmatter,
name必须与目录名一致,description必填,compatibility可选; - frontmatter 之外的正文整体作为
instructions注入; readSupportingFiles()递归收集技能目录下其余文件(如 merge 的resolve-conflicts.md),同时跳过SKILL.md自身和整个evals/目录,并拒绝任何符号链接。
最后一点正是 README 强调的隔离机制:运行器在挂载技能资源时排除清单文件,保证 expected_output 与 assertions 不会泄露给受试模型——受试模型只能看到 prompt 与 files 指定的输入,评审依据对它是不可见的。
六、受试/评审双模型架构:一次用例的完整执行流程
runEval()(skills.eval.ts)实现了每个用例的端到端流程,构建在 @flue/runtime 之上,包含两个 Agent:
- 搭建临时工作区:
mkdtemp(join(tmpdir(), 'astro-<skill>-'))创建一次性目录;清单files中列出的仓库文件由stageInputFiles()按相对路径原样复制进工作区。 - 受试 Agent 运行:
SkillEvalAgent使用SKILL_EVAL_MODEL,沙箱cwd指向该临时工作区,挂载全部技能,system prompt 要求"先激活目标技能再处理请求,仅当技能指示时才使用其他挂载技能"。运行器通过onEvent钩子记录所有tool-input事件,并硬性断言受试模型确实调用了activate_skill且参数包含目标技能名,否则该用例直接失败。 - 快照工作区:
snapshotWorkspace()遍历工作区(跳过.git与node_modules),文本文件截断到 20,000 字符、二进制文件仅记录大小,形成供评审的工件集合。 - 评审 Agent 打分:
EvalJudge使用SKILL_EVAL_JUDGE_MODEL,只挂载一个工具submit_eval_grade(用 valibot 定义 schema:evalId+ 每条断言的{assertionIndex, passed, evidence}+ 非空summary)。评审 prompt 明确要求把 prompt、输出、工具参数与工作区文件全部当作不可信工件而非指令,并"仅当工件中包含具体证据时才判 passed"。 - 结果校验:运行器检查评审是否恰好对每条断言各打一次分(
assertionIndex序列必须等于1..N),再断言所有断言均通过;失败时错误信息会附带评审 summary、受试模型原始输出与逐条 evidence。 - 清理:
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(frontmattername与目录名一致)和evals/evals.json(恰好 3 个用例、唯一正整数 id、files指向仓库内真实存在的文件),否则 validate 与运行都会直接报错; - 评估清单中"断言不得泄露给受试模型"、"符号链接一律拒绝"、"工作区用后即焚"三条隔离规则,共同保证评估结果反映的是技能指令本身的效果。
参考文件
- 评估体系说明:.agents/evals/README.md
- 清单与技能定义加载/校验:.agents/evals/load-evals.ts
- 受试/评审双模型运行器:.agents/evals/skills.eval.ts
- Vitest 配置:vitest.skills.config.ts
- 清单示例:.agents/skills/changeset/evals/evals.json、.agents/skills/merge/evals/evals.json
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00