首页
/ Gemini CLI 行为评估(Behavioral Evals)运行与晋级实战指南:从本地 Vitest 调试到 USUALLY_PASSES 晋级 ALWAYS_PASSES

Gemini CLI 行为评估(Behavioral Evals)运行与晋级实战指南:从本地 Vitest 调试到 USUALLY_PASSES 晋级 ALWAYS_PASSES

2026-09-05 18:06:48作者:邓越浪Henry

行为评估(Behavioral Evals)是 Gemini CLI 用来验证「模型是否会做出正确决策」而非「功能是否可用」的测试体系。本文基于仓库内置的 behavioral-evals 技能文档 running.md,系统讲解如何构建评估产物、配置 API 密钥、运行不同策略的评估用例,以及如何通过工具轨迹日志和调试日志定位失败根因,最终掌握从 USUALLY_PASSES 孵化到 ALWAYS_PASSES 晋级的完整晋级流程。

背景:行为评估验证的是什么

与普通集成测试验证「文件写入器是否真的写盘」不同,行为评估验证的是模型是否主动选择了正确的动作(例如:被要求保存代码时,模型是否决定调用写文件工具)。它们也是系统提示词(system prompt)、工具定义等模型引导(model steering)机制变更的关键反馈回路。由于 LLM 行为具有非确定性,Gemini CLI 将评估划分为两个一致性质等级别,这一区分直接决定了后文所有运行与晋级命令的设计:

  • ALWAYS_PASSES:预期 100% 通过,通常为无歧义的基线行为。运行在每次 CI 中,失败会阻塞 PR 合并;
  • USUALLY_PASSES:预期大多数时候通过,但因依赖非确定性的提示行为而允许偶发抖动。仅在夜间(nightly)孵化运行中执行,用于追踪产品质量趋势。

关于策略、断言编写的完整概念,evals/README.md 是单一事实来源,本文聚焦其中的运行与晋级环节。

前置条件:必须先构建编译产物

行为评估直接运行在编译后的二进制产物上,因此每次修改代码后都必须先完成构建与打包,否则评估测的是过期代码:

npm run build && npm run bundle

对照 package.json 中的脚本定义可以确认,build 对应 node scripts/build.js,而 bundle 是一条较长的组合命令(生成资源、构建 devtools 包、浏览器 MCP 打包、esbuild 打包并复制 bundle 资源)。这解释了为什么文档强调「构建是前置强制步骤」——评估子进程启动的就是这条流水线最终输出的 CLI。

配置环境变量与 API 密钥

评估需要标准的 Gemini API Key。如果 .env 文件中存在多个 key 或注释行,直接 source .env 可能拿到错误的值,文档给出了精确提取的写法:

export GEMINI_API_KEY=$(grep '^GEMINI_API_KEY=' .env | cut -d '=' -f2) && RUN_EVALS=1 npx vitest run --config evals/vitest.config.ts <file_name>

这条命令做了三件事:

  1. grep '^GEMINI_API_KEY=' 只匹配以该键开头的行,排除注释和其他 key;
  2. cut -d '=' -f2 截取等号后的值并导出为环境变量;
  3. 附带 RUN_EVALS=1 运行指定评估文件。

RUN_EVALS=1 为何是硬性要求

从源码 evals/test-helper.ts 中的 runEval 函数可以看到其底层机制:

} else if (
  !process.env['RUN_EVALS'] &&
  (policy === 'USUALLY_PASSES' || policy === 'USUALLY_FAILS')
) {
  it.skip(name, options, fn);
}

即:未设置 RUN_EVALS 时,所有 USUALLY_PASSESUSUALLY_FAILS 策略的用例会被 it.skip 静默跳过。这就是为什么单独运行某个孵化中(incubated)的测试文件时必须显式加上 RUN_EVALS=1,否则会看到「全部 skip」而误以为测试不存在。

运行命令速查

命令 覆盖范围 说明
npm run test:always_passing_evals ALWAYS_PASSES 快速反馈,运行在 CI 中
npm run test:all_evals 全部 夜间孵化测试,脚本内部会设置 RUN_EVALS=1

两条命令在 package.json 中的实际定义分别是:

  • test:always_passing_evalsvitest run --config evals/vitest.config.ts
  • test:all_evalscross-env RUN_EVALS=1 vitest run --config evals/vitest.config.ts

运行指定评估文件

针对单个功能文件(孵化期测试必须带 RUN_EVALS=1):

RUN_EVALS=1 npx vitest run --config evals/vitest.config.ts my_feature.eval.ts

这里指定 evals/vitest.config.ts 作为配置文件并非多余。该配置(见 evals/vitest.config.ts)为评估场景做了专门定制:

  • include: ['**/*.eval.ts']:只匹配 .eval.ts 命名的评估文件,与常规单元测试隔离;
  • testTimeout: 300000:单用例 5 分钟超时——一次真实的 agent 完整执行远比普通单测耗时长;
  • reporters: ['default', 'json'],JSON 报告输出到 evals/logs/report.json,供后续聚合分析脚本消费;
  • setupFiles 指向 packages/cli/test-setup.ts,复用 CLI 包的测试全局初始化。

调试与日志分析

测试失败时,文档给出两个层次的排查手段。

1. 工具轨迹日志(Tool Trajectory Logs)

每次评估执行后,evals/test-helper.ts 中的 internalEvalTest 会把完整的工具调用序列写入 evals/logs/<test_name>.log(文件名经 prepareLogDir 清洗为小写下划线格式):

await fs.promises.writeFile(
  logFile,
  JSON.stringify(rig.readToolLogs(), null, 2),
);

失败抛错时,测试框架还会自动把工具调用链摘要(经 formatToolLogChain 格式化)追加到错误信息中,使失败原因与「模型实际做了什么」直接对应。此外,子进程的 stderr 会单独落盘为 <test_name>.stderr.log,活动日志写入 JSONL 活动文件并在成功时自动清理。

2. 详细推理日志(Verbose Reasoning)

需要原始缓冲区追踪(raw buffer traces)时,设置环境变量捕获完整调试日志:

export GEMINI_DEBUG_LOG_FILE="debug.log"

3. API 可靠性与自动重试

从源码还可以看到一层对运行稳定性至关重要的机制:withEvalRetries 会对 500/503 等 API 侧错误自动重试至多 3 次,若持续失败则以「skip 而非 fail」的方式优雅退出,避免基础设施抖动阻塞 PR;每次重试/跳过事件都会同步追加到 evals/logs/api-reliability.jsonl(使用同步文件 I/O 确保进程被 CI 强杀时日志也已落盘)。排查「偶发失败」时应先区分是模型行为问题还是 API 抖动问题,后者的证据就在这份 JSONL 中。

模型定向校验经验

文档在 Model Targeting 小节给出了一条重要经验:标准评估会在多个模型变体上做基准对比。如果一个测试在 Flash 上通过而在 Pro 上失败(或反过来),问题通常出在**工具描述(tool description)**而非提示词定义本身。原因是两类模型的敏感度不同:Flash 对「指令膨胀(instruction bloat)」敏感,Pro 对「意图模糊(ambiguous intent)」敏感。这与 evals/README.md 中「夜间报告 pass rate 显著下滑往往提示近期系统提示词或工具定义变更」的判断方法互为印证。

去抖(Deflaking)与孵化期

为维持 CI 稳定性,所有新评估都要经过严格的孵化期,对应工作流标题 Evals: Nightly,它不阻塞 PR 合并。

1. 孵化:以 USUALLY_PASSES 策略创建

新测试必须这样声明(这是硬性约束):

evalTest('USUALLY_PASSES', { ... })

这些测试只在夜间工作流中运行并持续被监控。Evals: Nightly 每次运行会把同一测试连续执行 3 次、覆盖每个支持模型,按单次执行通过比例记 0% / 33% / 66% / 100%。提交前,测试在包含 Gemini 3.1 Pro、Gemini 3.0 Pro、Gemini 3 Flash 在内的关键模型上应至少达到 66% 分。

本地也可以借助 scripts/deflake.js 对任意测试命令重复执行多轮(--runs 默认 5 次)来预检稳定性,例如:

npm run deflake -- --command="npx vitest run --config evals/vitest.config.ts <file_name>" --runs=3

2. 失败调查:交给 agent 自动排查

夜间评估回归时,通过仓库内置的 behavioral-evals 技能命令交给 agent 调查(可附带一次具体运行记录):

gemini /fix-behavioral-eval [optional-run-uri]

该命令背后的流程(见 fixing.md):拉取夜间工作流结果、审查 evals/logs 中的轨迹日志、优先对 prompt.ts 与工具指令做最小改动(而非改测试本身),然后跨多模型本地复跑验证。配套技能总览见 SKILL.md

3. 晋级:USUALLY_PASSESALWAYS_PASSES

当一个测试在多个夜间周期内达到 100% 一致性后执行晋级命令:

gemini /promote-behavioral-eval

文档特别强调:不要手动晋级。该命令会在更新文件策略前先验证轨迹日志,确保 100% 成功率是被实证而非假设的。

对照 promoting.mdevals/README.md 的晋级判据,完整标准比文档中一句「多个夜间周期」更严格:

  1. 审计夜间日志:从 main 分支最近 7 次夜间运行 中汇总结果(汇总摘要会自动整合最近 7 次运行历史),且所有验证仅限本地,不推送、不触发远端运行;
  2. 稳定性判定:候选测试必须在最近 7 次夜间运行中、对所有启用模型每次 3 连跑全部通过(即每个模型每轮都是 3/3);
  3. 最小化修改:晋级变更只允许修改策略参数为 evalTest('ALWAYS_PASSES', { ... }),严禁顺手重构测试或 fixtures;
  4. 本地复核:晋级后用非交互 Vitest 本地跑一遍被晋级的测试,确认其被标准可运行范围(npm run test:always_passing_evals)正确拾取;
  5. 报告:说明晋级了哪些测试、附上成功率证据(如 7/7 轮全模型通过);若无达标候选,则列出最接近的候选及其当前通过率。

进阶:PR 回归检查与本地模拟

晋级机制之外,仓库还内置了 PR 级别的高信号回归检查脚本(详见 evals/README.md 的 Regression Check Scripts 一节),可以在推送前本地模拟 CI 行为:

# 对特定模型运行完整回归循环
MODEL_LIST=gemini-3-flash-preview node scripts/run_eval_regression.js

# 仅跑「可信测试」子集(60/80 过滤器:每夜不低于 60% 且 6 日聚合不低于 80%)
OUTPUT=$(node scripts/get_trustworthy_evals.js "gemini-3-flash-preview")
node scripts/run_regression_check.js "gemini-3-flash-preview" "$OUTPUT"

其判定逻辑值得理解:PR 中一个测试只要最多 4 次尝试里成功 2 次即算通过(Best-of-4),且若测试在 main 分支同样失败则标记为 Pre-existing 放行——保证只被「你自己的变更引入的回归」阻塞。评估默认模型由 GEMINI_MODEL 环境变量控制(默认取 PREVIEW_GEMINI_FLASH_MODEL,见 evals/test-helper.ts),本地复现 CI 问题时可通过 --model 或该变量切换模型做交叉验证。

小结

行为评估的运行与晋级链路可以概括为一条清晰的主线:先构建(build + bundle)→ 配好 API Key 与 RUN_EVALS → 按策略选择命令运行 → 失败时用 evals/logs 轨迹日志与 GEMINI_DEBUG_LOG_FILE 定位 → 新测试一律 USUALLY_PASSES 孵化 → 7 轮夜间 100% 稳定后由 /promote-behavioral-eval 实证晋级。这套机制的精髓在于:用「先孵化、后晋级、全程留证」的流程把 LLM 的非确定性关进 CI 的笼子,让提示词与工具定义的每一次变更都有可量化、可追溯的行为反馈。

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