Gemini CLI 行为评估(Behavioral Evals)体系:面向非确定性 LLM Agent 的回归测试设计与实战
本文基于 gemini-cli 仓库的 evals/README.md 及其配套的 test-helper、回归检查脚本 等源码,系统讲解行为评估的设计动机、双策略(ALWAYS_PASSES / USUALLY_PASSES)机制、EvalCase 完整属性、本地运行与稳定性晋级流程,以及 PR 级别的概率式回归判定逻辑(60/80 可信过滤器、50% 通过规则、动态基线验证)。读完本文,你可以独立编写、运行、诊断并晋级一个行为评估用例,并理解 CI 如何在不被 LLM 非确定性噪声干扰的前提下拦截真实的模型行为回归。
一、行为评估是什么:与集成测试、行业基准的区别
行为评估(Behavioral Evals)是验证 Agent 对特定提示是否"选择"了正确行为的测试。在 gemini-cli 中,它扮演着两类关键角色:
- 反馈回路:当系统提示词、工具定义或其他"模型引导(model steering)"机制被修改时,评估能立刻反映这些改动如何影响模型的决策。例如:某次系统提示词修改是否让模型更少使用某个工具?某个新工具定义是否让模型感到困惑?
- 回归防护:按模型维度评估功能可靠性,防止模型行为在版本间悄悄退化。
文档明确区分了三类测试的定位:
| 测试类型 | 验证对象 | 典型问题 |
|---|---|---|
| 集成测试(integration tests) | 系统功能是否正确 | "文件写入器真的写到了磁盘吗?" |
| 行业基准(如 SWE-bench) | 跨复杂任务的综合能力 | 模型在大规模基准上的得分 |
| 行为评估 | 模型是否选择采取正确动作 | "当用户要求保存代码时,模型是否决定去写磁盘?" |
行为评估聚焦于与 Gemini CLI 具体功能相关的细粒度行为,因此它既是 CI 防线,也是产品健康度的长期指标。
其关键特性(Key Characteristics)有三点:
- 反馈回路(Feedback Loop):帮助团队理解提示词或工具改动对模型决策的影响;
- 回归测试(Regression Testing):防止模型引导层面的回归;
- 非确定性(Non-Determinism):与单元测试不同,LLM 行为可能不稳定。因此评估体系区分两类期望——行为应当稳固的(
ALWAYS_PASSES),以及总体可靠但偶尔波动的(USUALLY_PASSES)。
二、设计行为评估的最佳实践
文档给出五条设计准则,核心思想是"既要贴近真实使用,又要足够小可维护":
- 现实复杂度(Realistic Complexity):评估要"真实"到足够复杂——应操作真实文件、真实源码目录,模拟 Agent 与工作区的真实交互方式。注意:Agent 在大型代码库中的行为可能与小场景不同,过于简单的场景缺乏代表性。
- 好例子:提供一个小而可运行的 React 组件,要求 Agent 添加特定功能——它需要读文件、理解上下文、写出正确的改动;
- 坏例子:问一个冷知识问题,或在没有任何本地工作区上下文的情况下让它写一个通用脚本。
- 可维护的规模(Maintainable Size):2~3 个文件的测试夹具(如源码文件 + 配置文件 + 测试文件)足以隔离被评估的行为;不要塞入几十文件的复杂框架,夹具逻辑本身容易坏。
- 明确且可靠的断言(Unambiguous and Reliable Assertions):断言必须具体,确保测试因正确的原因通过。
- 好例子:检查修改后的文件包含特定 AST 节点或精确字符串,或验证某工具以正确参数被调用;
- 坏例子:只检查"某工具被调用过"(它可能因无关原因被调用),或期望特定的 LLM 输出文本。
- 先失败再修复(Fail First):在改动提示词/工具之前,先让测试处于失败状态。否则很容易写出一个"断言了我们本来就能免费得到的行为"的测试,且首次运行即通过。原则上,每个评估都应伴随一个提示词改动,且大多数提示词改动都应伴随一个评估。
- 好例子:观察到失败 → 写一个能稳定复现失败的评估 → 修改提示词/工具 → 验证评估通过;
- 坏例子:写一个首跑就通过的评估,并假设新提示词改动起了作用。
- 少即是多(Less is More):优先少量、更贴近现实、覆盖主干路径的测试,而不是大量单元测试式的测试。行为评估的价值就在于半真实场景下检验 Agent 的工作方式。
三、编写一个行为评估
行为评估位于 evals 目录,每个评估是一个 Vitest 测试文件,使用 evals/test-helper.ts 中的 evalTest 函数。Vitest 侧的执行约定见 evals/vitest.config.ts:
include: ['**/*.eval.ts']—— 只有*.eval.ts文件会被纳入评估套件;testTimeout: 300000—— 每个用例默认超时 5 分钟;- JSON 报告输出到
evals/logs/report.json(供回归脚本解析)。
3.1 evalTest 与两种策略
evalTest(policy, evalCase) 运行单个评估用例,接收两个参数:
policy:一致性期望,'ALWAYS_PASSES'或'USUALLY_PASSES';evalCase:定义测试用例的对象。
策略(Policies)控制用例的验证严格度:
ALWAYS_PASSES:预期 100% 通过的测试,通常只验证基础功能。它们每次 CI 都会运行,失败可阻断 PR。USUALLY_PASSES:预期大多数时候通过,但因非确定性行为可能有偶发失败。它们只在夜间(nightly)运行,用于跟踪产品逐版本的模型引导健康度。
所有新行为评估必须以
USUALLY_PASSES创建。经过时间检验、证明高度稳定的子集才可以晋级为ALWAYS_PASSES(流程见第五节)。
源码视角:策略是如何生效的
阅读 runEval 的实现可以确认策略分发的具体机制:
// evals/test-helper.ts(节选,L380-L389)
} else if (
!process.env['RUN_EVALS'] &&
(policy === 'USUALLY_PASSES' || policy === 'USUALLY_FAILS')
) {
it.skip(name, options, fn); // 未设置 RUN_EVALS 时跳过
} else if (policy === 'USUALLY_FAILS') {
it.fails(name, options, fn); // 预期失败的用例
} else {
it(name, options, fn);
}
即:未设置 RUN_EVALS 环境变量时,USUALLY_PASSES 用例会被 it.skip 跳过,只有 ALWAYS_PASSES 真正执行——这正是"npm run test:always_passing_evals 是 CI 安全命令"的底层原因。此外,EvalPolicy 类型定义 中还存在一个文档未提及的 USUALLY_FAILS 分支(用 it.fails 运行,用于验证某行为当前尚未被修复),属于实验性策略,新用例不应使用。
3.2 EvalCase 属性
README 文档列出的核心属性为:
name:评估用例名称;prompt:发送给模型的提示词;params:传给测试夹具(test rig)的可选参数(如 settings);assert:异步断言函数,接收 test rig 与运行结果,断言结果正确;log:可选布尔值,为true时将工具调用日志写入evals/logs目录。
对照 EvalCase 接口定义,实际可用的属性比文档列出的更丰富:
suiteName/suiteType:用例所属套件('behavioral' | 'component-level' | 'hero-scenario'),用于按套件筛选运行(通过EVAL_SUITE_TYPE/EVAL_SUITE_NAME环境变量跳过不匹配用例);files:Record<string, string>形式的测试工作区文件,会由 prepareWorkspace 写入临时目录并自动初始化一个 git 仓库(git init+ 初始提交),模拟真实 Agent 工作区;setup:运行前的自定义夹具钩子(如打断点注入用户提示);messages/sessionId:预载会话历史,通过--resume让 CLI 子进程恢复该会话;approvalMode:审批模式,默认'yolo';timeout:单用例超时覆盖。
一个值得注意的实现细节:ForbiddenToolSettings 在类型层面禁止评估用例通过 params.settings.tools.core 限制核心工具集——评估必须面对完整默认工具集运行,才能保证行为验证是真实的。
3.3 完整示例
README 中的最小示例:
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
describe('my_feature', () => {
// 新测试必须以 USUALLY_PASSES 起步,后续依据一致性指标晋级
evalTest('USUALLY_PASSES', {
name: 'should do something',
prompt: 'do it',
assert: async (rig, result) => {
// assertions
},
});
});
仓库中的真实用例 file_creation_behavior.eval.ts 展示了"好断言"的完整写法:提供真实文件夹具(package.json + src/index.ts),要求创建 src/logger.ts,断言同时验证三件事——write_file 工具确实被调用(通过 rig.readToolLogs() 过滤 toolRequest.name)、既有文件未被篡改(逐字比对 src/index.ts 内容)、新文件已创建。另一个用例(L49-L108)更进一步断言工具调用顺序:read_file(config.json) 的索引必须小于 write_file(config.json),确保模型"先读后写"。
交互类评估则使用 app-test-helper.ts 的 appEvalTest,例如 model_steering.eval.ts 通过 rig.setBreakpoint([...]) 在工具调用处暂停,注入一条纠偏提示("停下,讲个机器人的 knock-knock 笑话"),再断言模型输出确实转向了新任务——这是对"模型引导"行为的端到端验证。
3.4 运行失败的韧性:API 错误重试
withEvalRetries 包裹每个用例执行:当错误信息被识别为 API 侧的 500/503(UNAVAILABLE、INTERNAL 等特征串),最多重试 3 次,每次重试都会把事件同步追加到 evals/logs/api-reliability.jsonl 供后续采集;若持续失败,则跳过该用例而不判定失败,避免服务端抖动阻断 PR。真正的断言失败则直接抛出。
四、本地运行行为评估
4.1 前置:构建捆绑包
每次代码改动后都必须重新构建捆绑版 Gemini CLI:
npm run build
npm run bundle
4.2 两条运行命令
来自 package.json 的脚本定义:
# 只运行 CI 安全的 ALWAYS_PASSES 评估
npm run test:always_passing_evals
# 实际等价于:vitest run --config evals/vitest.config.ts
# 运行全部评估(含可能偶发失败的 USUALLY_PASSES)
npm run test:all_evals
# 实际等价于:cross-env RUN_EVALS=1 vitest run --config evals/vitest.config.ts
test:all_evals 的关键在于设置 RUN_EVALS=1——结合 3.1 节的源码分析,正是这个环境变量让 runEval 不再跳过 USUALLY_PASSES 用例。文档明确提示:全量运行可能耗时很长,不建议日常使用。
此外,评估默认模型由 EVAL_MODEL 决定:读取 GEMINI_MODEL 环境变量,缺省回退到内置的 preview flash 模型。
4.3 日志产物
每个用例运行后,internalEvalTest 会在 evals/logs 下写入 <用例名>.log(格式化工具调用链的 JSON)与 <用例名>.stderr.log;失败时还会把完整的工具调用链摘要附加到错误信息中,方便定位"模型为什么走到了错误路径"。
五、稳定性门槛与晋级流程
5.1 夜间运行是稳定性事实来源
[Evals: Nightly] 工作流是评估质量的事实来源(source of truth):每次运行将同一测试连续执行 3 次(针对每个支持的模型),按通过次数计分为 0%、33%、66% 或 100%。入库(check-in)前,测试必须在包括 Gemini 3.1 Pro、Gemini 3.0 Pro 和 Gemini 3 Flash 在内的关键模型上得分至少 66%,且晋级前必须 100% 通过。
5.2 晋级三步走(deflaking process)
为维护 CI 稳定,所有新行为评估必须经历强制"去抖(deflake)"流程:
- 孵化(Incubation):新测试一律以
USUALLY_PASSES创建,只被夜间运行监控、不阻断 PR; - 监控(Monitoring):测试须在所有支持模型上完成至少 7 次夜间运行;
- 晋级(Promotion):由 Agent 在验证"跨多次运行 100% 成功"的要求满足后,将测试文件的策略更新为
ALWAYS_PASSES。
该晋级流程对防止不稳定评估进入 CI 至关重要。晋级同样由 Agent 自动化完成:分析 main 分支最近 7 次夜间运行的结果 → 确认所有启用模型均 100% 通过 → 更新测试文件策略 → 本地重跑被晋级测试验证正确性。
六、报告与结果解读
评估结果在 GitHub Actions 中分两条线呈现:
- CI Evals:包含在 E2E (Chained) 工作流中,每个 PR 必须 100% 通过(即
ALWAYS_PASSES集合); - Nightly Evals:每日运行,跟踪模型引导的长期健康度与稳定性。
夜间工作流会将完整套件**重复执行多次(当前为 3 次尝试)**以吸收非确定性,结果被聚合为附加在工作流运行上的 Nightly Summary。解读报告时的三个要点:
- Pass Rate (%):每个单元格是该测试在该工作流实例中成功运行的百分比;
- History:表格展示最近 7 次夜间运行的通过率,用于识别某模型行为是否正滑向不稳定;
- Total Pass Rate:该批次所有评估的聚合指标。
诊断信号:一个 USUALLY_PASSES 测试通过率的显著下滑——即使没有掉到 0%——往往意味着最近的系统提示词或工具定义改动让模型行为变得更不可靠。
七、PR 回归检查:三个脚本与概率式质量门槛
7.1 脚本职责
项目提供三个自动化高信号回归检查脚本(也可本地运行用于调试):
- scripts/get_trustworthy_evals.js:分析夜间历史,识别稳定测试(聚合通过率 80%+);
- scripts/run_regression_check.js:使用"Best-of-4"逻辑与"动态基线验证"运行特定测试集合;
- scripts/run_eval_regression.js:主编排器,遍历模型并生成最终 PR 报告。
7.2 本地模拟 PR 回归检查
# 对指定模型运行完整回归循环
MODEL_LIST=gemini-3-flash-preview node scripts/run_eval_regression.js
用 CI 相同逻辑调试某个失败测试:
# 1. 获取可信测试的 Vitest 匹配模式
OUTPUT=$(node scripts/get_trustworthy_evals.js "gemini-3-flash-preview")
# 2. 对这些测试运行回归逻辑
node scripts/run_regression_check.js "gemini-3-flash-preview" "$OUTPUT"
7.3 回归质量门槛(The Regression Quality Bar)
由于 LLM 非确定性,PR 回归检查采用高信号概率式方法而非 100% 通过要求,由三条规则构成:
- 可信性过滤(60/80 Filter):只运行有稳定履历的测试。源码 get_trustworthy_evals.js 中对应常量:
LOOKBACK_COUNT = 6(回看 6 天)、MIN_VALID_RUNS = 5(至少 5 天数据)、PASS_RATE_THRESHOLD = 0.6(每晚至少 60%,即 2/3)、AGGREGATE_PASS_RATE_THRESHOLD = 0.8(6 天聚合通过率至少 80%)。不满足最低数据量的测试会被归入 new tests,而非直接判为不可信; - 50% 通过规则:PR 中一次测试,只要模型至少一半时间(最多 4 次尝试中 2 次成功)正确执行了该行为,即记为 Pass;run_regression_check.js 的头部注释确认了"乐观首跑 + Best-of-4 重试 + 0/3 时动态基线验证"的执行路径;
- 动态基线验证(Dynamic Baseline Verification):若测试在 PR 中失败(如 0/3),系统会自动检查
main分支——如果在那里同样失败,则标记为 Pre-existing 并放行该 PR。这保证只由你本次改动引入的回归才会阻断合并,而非模型自身的漂移或既有问题。
八、修复与晋级评估:behavioral-evals skill 驱动的闭环
当某个评估失败或通过率回退时,推荐让 Agent 使用 behavioral-evals skill 来调查和修复。Agent 自动化的流程为:
- 调查(Investigate):通过
ghCLI 拉取夜间工作流的最新结果,定位失败测试,检查evals/logs中的测试轨迹日志; - 修复(Fix):提出并应用针对提示词或工具定义的定向修改,优先对
prompt.ts与工具指令做最小改动,除非必要不改动测试本身; - 验证(Verify):本地跨多个模型重跑测试,确认稳定性;
- 报告(Report):给出成功率摘要。
手动排查失败时,可设置 GEMINI_DEBUG_LOG_FILE 环境变量开启详细的 Agent 调试日志。
文档最后给出一条关于提示词修改的最佳实践:即使提示词改动通过了所有评估,也强烈建议人工审查或让 Agent 迭代打磨。提示词应优先使用正向表述("do X"),只有在正向表述无法达成目标时才退而使用负向表述("do not do X")——Gemini 在被恰当提问时,对自身提示词有很强的自省能力。
九、延伸阅读:仓库内相关文件索引
| 文件 | 说明 |
|---|---|
| evals/README.md | 行为评估体系文档(本文主体依据) |
| evals/test-helper.ts | evalTest/runEval 策略分发、重试与日志落盘实现 |
| evals/vitest.config.ts | 评估套件 Vitest 配置(5 分钟超时、*.eval.ts 收集规则) |
| evals/app-test-helper.ts | 交互式应用级评估辅助(断点、提示注入、输出等待) |
| evals/file_creation_behavior.eval.ts | 文件创建行为评估完整示例(含工具调用顺序断言) |
| evals/model_steering.eval.ts | 模型引导(纠偏/建议提示)端到端评估 |
| evals/llm-judge.ts | LLM 评判器辅助 |
| scripts/get_trustworthy_evals.js | 60/80 可信性过滤实现 |
| scripts/run_regression_check.js | Best-of-4 与动态基线验证实现 |
| scripts/run_eval_regression.js | 回归检查主编排器 |
| package.json | test:always_passing_evals / test:all_evals 脚本定义 |
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