gemini-cli 行为评估(Behavioral Evals)实战指南:从创建、修复到测试晋升的完整工作流
本文基于 gemini-cli 仓库中的 behavioral-evals 技能文档(.gemini/skills/behavioral-evals/SKILL.md)及其配套参考指南编写,系统讲解如何为 LLM Agent 编写"决策验证"类测试:选择正确的 Rig、设计真实工作区场景、断言工具调用轨迹、运行与调试评估,以及将孵化中的测试稳定晋升进 CI。读完本文,你可以独立完成一条从"prompt 变更 → 编写行为评估 → 修复失败 → 晋升为 ALWAYS_PASSES"的完整回归防护链路。
什么是行为评估
行为评估(behavioral evaluations,简称 evals)验证的是 Agent 的决策过程,而不是纯功能正确性。用 evals/README.md 的说法:传统集成测试验证"系统是否正确运作"(比如"文件写入器是否真的写入了磁盘"),而行为评估验证"模型是否选择了正确的动作"(比如"当被要求保存代码时,模型是否决定写盘?")。
它们同样区别于 SWE-bench 等宽泛的行业基准:行业基准衡量复杂挑战下的通用能力,而 gemini-cli 的行为评估聚焦于与产品特性强相关的具体、细粒度行为。其核心价值体现在三个方面:
- 反馈回路:理解 prompt 或工具定义变更如何影响模型决策——"系统 prompt 的改动是否让模型更少使用工具 X?"、"新的工具定义是否让模型困惑了?";
- 回归防护:防止模型 steering(引导)层面的回归;
- 非确定性治理:与单元测试不同,LLM 行为是非确定性的。gemini-cli 通过策略(policy)区分"应当稳健"的行为(
ALWAYS_PASSES)和"总体可靠但偶尔波动"的行为(USUALLY_PASSES)。
技能入口 SKILL.md 明确将 evals/README.md 定位为概念、策略与运行方式的"单一事实来源(Single Source of Truth)",技能文档本身则提供流程化操作指引。
工作流决策树:先判断,再动手
SKILL.md 给出的决策树是进入任何 eval 任务前的第一步判断,完整继承如下:
- prompt/工具变更是否需要验证?
- 否 → 走普通集成测试即可。
- 是 → 继续。
- 是否 UI/交互密集?
- 是 → 使用
appEvalTest(AppRig),参见 creating.md。 - 否 → 使用
evalTest(TestRig),同上。
- 是 → 使用
- 是否新测试?
- 是 → 策略设为
USUALLY_PASSES(进入孵化期)。 - 否 →
ALWAYS_PASSES(锁定回归,禁止 PR 合入失败)。
- 是 → 策略设为
- 是在修复失败还是晋升测试?
- 修复 → 参见 fixing.md。
- 晋升 → 参见 promoting.md。
快速检查清单(Quick Checklist)概括为三步:
- 布置工作区:用
files对象向工作区注入必要文件,模拟真实场景(例如带package.json的 NodeJS 项目); - 编写断言:用
rig.setBreakpoint()(仅 AppRig)或rig.readToolLogs()的索引校验来审计 Agent 的决策; - 本地验证:用 Vitest 单独运行目标测试,确认本地稳定后再依赖 CI 工作流。
创建行为评估:Rig 选择、场景设计与断言模式
creating.md 是编写过程的核心指南。
Rig 选型表
| Rig 类型 | 导入来源 | 架构 | 适用场景 |
|---|---|---|---|
evalTest |
./test-helper.js |
子进程。在独立进程中运行 CLI 并等待退出。 | 标准工作区测试。不要使用 setBreakpoint;审计历史(readToolLogs)更安全。 |
appEvalTest |
./app-test-helper.js |
进程内。直接跑在 runner 循环里。 | UI/Ink 渲染。可安全触发 setBreakpoint。 |
这一选型建议有源码支撑。evals/test-helper.ts 中 internalEvalTest 通过 rig.run() 在子进程中执行 CLI(默认 approvalMode: 'yolo'),进程退出后才执行 evalCase.assert;而 evals/app-test-helper.ts 中 appEvalTest 则直接实例化 AppRig(来自 packages/cli/src/test-utils/AppRig.ts),按 initialize → 写入 files → setup(可设断点)→ render → waitForIdle → sendMessage → assert 的进程内流程执行,因此只有后者能在执行中途挂起并等待确认。
场景设计原则
评估必须模拟真实的 Agent 环境:
- 工作区状态:测试通用能力时注入标准项目锚点文件——NodeJS 环境给
package.json,外加最少的配置文件(tsconfig.json、GEMINI.md)。 - 结构复杂度:提供足够多的文件,迫使 Agent 需要搜索或导航,而不是直接把答案喂给它。除非在测试精确的 prompt steering,否则避免单文件平凡用例。
evals/README.md 对此进一步量化了"好"与"坏":好的用例是"提供一个小而可运行的 React 组件,要求 Agent 添加某个特性";坏的用例是"问一个冷知识问题或让它写一个没有任何本地上下文的通用脚本"。文件数建议 2-3 个,既隔离被评估行为,又不至于 setup 逻辑本身脆弱。
Fail First 原则
在断言新能力或锁定修复之前,先验证测试确实会失败:很容易写出断言"默认就满足的行为"的用例。正确流程是——用测试复现失败 → 应用修复(prompt/工具)→ 验证测试通过。README 强调"每个 eval 都应伴随一次 prompt 变更,每次 prompt 变更也尽量应伴随一个 eval"。
四种测试模式
1. 断点(Breakpoint):验证 Agent 在执行前就"打算"使用某个工具,适合交互式确认与安全校验。
// ⚠️ 仅适用于 appEvalTest(AppRig)
setup: async (rig) => {
rig.setBreakpoint(['ask_user']);
},
assert: async (rig) => {
const confirmation = await rig.waitForPendingConfirmation('ask_user');
expect(confirmation).toBeDefined();
}
2. 工具确认竞态(Tool Confirmation Race):断言多个顺序触发(例如"先进入计划模式再提问")时,需要处理确认的先后竞态:
assert: async (rig) => {
let confirmation = await rig.waitForPendingConfirmation([
'enter_plan_mode',
'ask_user',
]);
if (confirmation?.name === 'enter_plan_mode') {
rig.acceptConfirmation('enter_plan_mode');
confirmation = await rig.waitForPendingConfirmation('ask_user');
}
expect(confirmation?.toolName).toBe('ask_user');
};
3. 审计工具日志:精确审计操作序列,验证效率(例如没有冗余读):
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const writeCall = toolLogs.find(
(log) => log.toolRequest.name === 'write_file',
);
expect(writeCall).toBeDefined();
};
4. Mock MCP 门面:评估经 MCP 接入的工具而不触碰真实端点,在 setup 钩子中加载 mock 服务器配置:
setup: async (rig) => {
rig.addMockMcpServer('workspace-server', 'google-workspace');
},
assert: async (rig) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const workspaceCall = toolLogs.find(
(log) => log.toolRequest.name === 'mcp_workspace-server_docs.getText'
);
expect(workspaceCall).toBeDefined();
};
安全与效率护栏
- 断点死锁:
setBreakpoint会暂停执行。标准evalTest中rig.run()在断言前就等待进程退出,断点会导致无限挂起。交互式模拟用appEvalTest+ 断点,标准轨迹测试用工具日志审计。 - 失控超时:始终在
EvalCase中设置预算边界,防止配额失控循环:
evalTest('USUALLY_PASSES', {
name: '...',
timeout: 60000, // 1 分钟安全上限
// ...
});
- 效率断言(轮次上限):用索引检查工具是否被尽早调用:
assert: async (rig) => {
const toolLogs = rig.readToolLogs();
const toolCallIndex = toolLogs.findIndex(
(log) => log.toolRequest.name === 'cli_help',
);
expect(toolCallIndex).toBeGreaterThan(-1);
expect(toolCallIndex).toBeLessThan(5); // 在前 5 轮内被调用
};
运行评估:构建、命令与环境变量
行为评估运行在编译产物上,因此每次代码变更后必须先构建打包(见 running.md):
npm run build && npm run bundle
评估还需要 API 密钥。若 .env 中有多把 key 或注释,使用精确提取:
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>
仓库根目录 package.json 中定义了两个 npm 脚本,与技能文档的命令表一一对应:
| 命令 | 范围 | 说明 |
|---|---|---|
npm run test:always_passing_evals |
ALWAYS_PASSES |
快速反馈,CI 中运行(脚本实质是 vitest run --config evals/vitest.config.ts) |
npm run test:all_evals |
全部 | 运行夜间孵化测试,设置 RUN_EVALS=1(cross-env RUN_EVALS=1 vitest run ...) |
定向运行单个孵化测试文件时,RUN_EVALS=1 是必需的:
RUN_EVALS=1 npx vitest run --config evals/vitest.config.ts my_feature.eval.ts
这些命令背后有明确的源码机制。evals/vitest.config.ts 中 include: ['**/*.eval.ts'] 说明评估文件统一以 .eval.ts 命名(这也解释了 evals/ 目录下大量 xxx.eval.ts 文件);testTimeout 为 300000ms(5 分钟),JSON reporter 输出到 evals/logs/report.json。而策略到 Vitest 用例的映射在 evals/test-helper.ts 的 runEval 函数中:
- 未设置
RUN_EVALS时,USUALLY_PASSES与USUALLY_FAILS策略的用例被it.skip跳过——这就是"孵化测试不进 CI"的实现方式; USUALLY_FAILS策略则注册为it.fails(期望失败的用例,源码中存在但 SKILL 文档未展开,可以推断用于反向验证);- 用例还可携带
suiteType/suiteName元数据,通过EVAL_SUITE_TYPE、EVAL_SUITE_NAME环境变量按套件过滤跳过; - 默认评估模型由
GEMINI_MODEL环境变量覆盖,缺省为PREVIEW_GEMINI_FLASH_MODEL(见EVAL_MODEL常量)。
此外源码还有两个值得注意的健壮性设计:withEvalRetries 对 500/503 API 错误最多重试 3 次并在持续失败时"跳过而非判失败",把事件写入 evals/logs/api-reliability.jsonl(仓库中确有 api-reliability 集成测试 配套);prepareWorkspace 在为测试写入 files 后会执行 git init、禁用交互式 editor/pager 并做一次初始提交,让 Agent 面对一个真实可操作的 git 工作区。
修复失败的评估
fixing.md 给出四阶段流程:
1. 调查(Investigate)
- 用
ghCLI 拉取evals-nightly.yml工作流的最新运行结果; - 隔离:不推送变更、不启动远端运行,调查限定在本地工作区;
- 读日志:评估日志位于
evals/logs/<test_name>.log(对应源码中prepareLogDir生成的${sanitizedName}.log,内容为rig.readToolLogs()的 JSON 序列化);导出GEMINI_DEBUG_LOG_FILE="debug.log"可开启详细调试; - 诊断:审计工具日志与遥测,判断失败源于 setup 还是 assert;主动加自定义日志验证假设。
2. 修复策略(Fix Strategy)
- 定位:找到测试用例与对应的 prompt/代码位置;
- 迭代范围:先做一个极端变更验证作用域,再收敛为最小、精准修改;
- 断言保真:修改测试 prompt 是最后手段(prompt 常常按设计写得模糊),不要让测试因 prompt 过于直接而失去保真度;主要修复点应落在工具描述、系统 prompt(
packages/core/src/prompts/snippets.ts)或贡献于 prompt 模板的模块上; - 指令通用性:系统 prompt 修改应尽可能通用,只按需增加特异性。与其罗列"禁用清单"(如"不要用
Object.create()"),不如提炼覆盖底层问题的工程原则(如"显式组合优先于隐式原型操作")。文档给出了三级特异性参考:低——"遵循生态最佳实践";中——"视情况运用 OOP 与函数式最佳实践";高——把生态特定提示作为广义原则的示例而非直接指令; - prompt 简化:测试通过后,用
ask_user确认是否希望简化 prompt;仅当存在可去重或可归并到单一标题下的相关子句时才尝试简化,并且必须找出并运行受影响的 eval 以防回归; - 禁止作弊:不得通过修改测试的
GEMINI.md或改写 prompt 来"绕开"bug 复现; - 架构选项:若 prompt 调整无改善,分析循环构成。AgentLoop 由
context + toolset + prompt定义;循环在直接 prompt、更少的无关工具、低目标密度、最小低价值上下文的组合下表现最佳。修改方向可以是编排 subagent 或隔离工具,且必须基于观察到的 trace。
3. 验证(Verify)
- 用非交互 Vitest 只跑目标文件;
- 优先通过日志对比诊断,再触发重量级测试;
- 在关键模型(Gemini 3.0、Gemini 3 Flash、Gemini 2.5 Pro)上本地跑 3 次,可用脚本并行提速;
- 抖动规则:若 3 次中通过 2 次,可能是内禀噪声,不做结构性拆分难以再提升。
4. 报告(Report)
总结每个模型的成功率(如 3/3 = 100%)、根因定位与修复说明;若未修复,给出高置信度的架构建议。
晋升评估:从 USUALLY_PASSES 到 ALWAYS_PASSES
promoting.md 定义了严格的两阶段晋升流程。
1. 调查候选
- 用
ghCLI 拉取evals-nightly.yml的结果(提示:最近一次运行的聚合摘要会自动整合最近 7 次运行的历史);全程本地验证,不推送、不启动远端运行; - 稳定性判定:找出在
main分支最近 7 次夜间运行中、所有启用模型上都 100% 通过的测试——100% 意味着每个模型、每次运行都 3/3 通过(夜间工作流每轮把每个测试连跑 3 次,以 0%/33%/66%/100% 计分); - 满足条件的测试即
USUALLY_PASSES→ALWAYS_PASSES的晋升候选。
evals/README.md 补充了入库门槛:测试在主要模型(如 Gemini 3.1 Pro、Gemini 3.0 Pro、Gemini 3 Flash)上应至少达到 66%,晋升前必须 100% 通过。
2. 晋升步骤
- 在
evals/目录定位评估文件; - 将策略参数改为
ALWAYS_PASSES:
evalTest('ALWAYS_PASSES', { ... })
- 目标文件组织遵循
evals/README.md的 stable suite 规范; - 约束:最终变更必须最小、且严格限于晋升测试状态本身,不得重构测试或 setup fixtures。
running.md 还提到可通过 Agent 命令 gemini /promote-behavioral-eval 自动化晋升(技能文档描述的工作流入口;在当前仓库中该命令仅见于技能文档本身),以及 gemini /fix-behavioral-eval [optional-run-uri] 用于修复夜间回归。技能文档明确"不要手动晋升"——命令会先核验轨迹日志再更新文件策略。
3. 验证与报告
本地用非交互 Vitest 跑被晋升的测试确认结构有效性;确认测试能被标准运行范围拾取。报告内容:哪些测试被晋升、成功率证据(如"7/7 次运行、所有模型通过");若无合格候选,列出最接近的候选及其当前通过率。
回归检查脚本与 PR 质量门槛
evals/README.md 描述了 PR 上高信号回归检查的三件套脚本,均可本地运行用于调试:
scripts/get_trustworthy_evals.js:分析夜间历史,识别稳定测试(综合通过率 80%+);scripts/run_regression_check.js:用"Best-of-4"逻辑与"动态基线校验"运行指定测试集;scripts/run_eval_regression.js:主编排器,循环各模型并生成最终 PR 报告。
本地模拟 PR 回归检查:
# 对指定模型跑完整回归循环
MODEL_LIST=gemini-3-flash-preview node scripts/run_eval_regression.js
# 调试某个失败测试(与 CI 相同逻辑)
OUTPUT=$(node scripts/get_trustworthy_evals.js "gemini-3-flash-preview")
node scripts/run_regression_check.js "gemini-3-flash-preview" "$OUTPUT"
由于 LLM 非确定性,PR 回归检查采用高信号概率化方案而非 100% 通过要求:
- 可信度过滤(60/80):只运行有履历的测试——每个夜晚单模型至少 60%(2/3),且最近 6 天综合通过率 80%;
- 50% 通过规则:PR 中某测试最多尝试 4 次,成功 2 次即判为通过;
- 动态基线校验:若测试在 PR 上失败(如 0/3),系统自动在
main分支复查,同样失败则标记为 Pre-existing 并放行该 PR——只让你的变更本身造成的回归阻塞你。
报告解读方面:Pass Rate (%) 是该次工作流实例中单个测试的成功率;History 展示最近 7 次夜间运行的通过率,用于识别行为是否趋向不稳定;Total Pass Rate 为该批次所有评估的聚合指标。USUALLY_PASSES 测试通过率的显著下滑(哪怕未到 0%)通常意味着最近的系统 prompt 或工具定义变更降低了模型行为的可靠性。
设计最佳实践总览
结合 evals/README.md 与技能文档,创建行为评估时应遵循五条准则:
- 真实复杂度:在真实文件与源目录上操作,模仿 Agent 与真实工作区的交互方式;
- 可维护规模:小到可以推理和维护(2-3 个文件的隔离场景),避免 setup 本身脆弱;
- 断言无歧义且可靠:检查修改后的文件包含特定 AST 节点/精确字符串,或验证工具以正确参数被调用;不要只检查"发生过某工具调用"(可能出于无关原因),更不要期望特定的 LLM 输出文本;
- Fail First:先观察到失败、写出可靠复现的 eval、再改 prompt/工具并验证通过;
- 少即是多:优先少量真实、覆盖主要路径的测试,而非大量单测化的细碎用例。
此外 README 还给出两条经验法则:prompt 修复优先正向表述("do X"),仅在正向无法达成目标时才用负向("do not do X");且人工(或让 Agent)对任何 prompt 变更做复核——即使它通过了全部 eval。值得注意的是,evals/test-helper.ts 与 evals/app-test-helper.ts 均在类型层面把 excludeTools/coreTools/allowedTools 等配置标记为 never——eval 必须面对完整的默认工具集运行,以确保持续验证真实行为,这条约束在源码中是强制性的。
小结
gemini-cli 的行为评估体系由三部分组成:以 SKILL.md 为入口的技能(面向 Agent 自动化修复与晋升)、以 evals/README.md 为概念与运行规范的事实来源、以及 evals/ 目录下 evalTest/appEvalTest 两套 Rig 支撑的可执行测试。其工程精髓在于:用"策略 + 孵化 + 晋升"机制驯服 LLM 的非确定性,用工具日志审计而非输出文本匹配来断言决策,用可信度过滤与动态基线让 CI 只对你的变更负责。这套实践可以直接迁移到任何需要为 LLM Agent 建立行为回归防护的开源或商业项目。
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 StartedRust0623
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