gemini-cli 行为评估失败诊断:从日志排查到提示词修复的四阶段方法论
gemini-cli 使用行为评估(behavioral evals)来验证模型在给定场景下“是否做出正确决策”,而非验证功能本身。当这类测试在夜间流水线中回归或本地运行失败时,盲目改动测试提示词往往只会掩盖问题、降低测试保真度。本文基于仓库内的排查指南 fixing.md,完整展开其“调查—修复—验证—报告”四阶段方法论,并结合 evals/test-helper.ts、debugLogger.ts 等源码印证日志生成、重试机制与提示词修复落点,帮助读者掌握一套可复制的行为评估调试流程。
什么是行为评估:先明确你在修什么
行为评估验证的是 Agent 的决策逻辑(例如工具选择是否正确),而不是纯功能(例如文件是否真的写入了磁盘)。evals/README.md 对此的定义是:它们是系统提示词、工具定义等模型引导机制变更的关键反馈回路,也是按模型评估功能可靠性、防止回归的手段。
理解这一点的直接后果是:修复的“主战场”通常是提示词与工具描述,而不是测试本身。fixing.md 给出的整套流程正是围绕这一前提展开:先定位失败究竟来自测试环境(setup)还是断言(assert),再把修复聚焦到对模型行为有实际影响的模块上。
从源码结构看,每个 eval 用例通过 evals/test-helper.ts 中的 evalTest(policy, evalCase) 注册,由 runEval 根据策略决定是否真正执行:未设置 RUN_EVALS=1 时,USUALLY_PASSES 与 USUALLY_FAILS 用例会直接被 it.skip 跳过,只有 ALWAYS_PASSES 会进入常规 CI。因此排查前必须先确认你复现失败所用的命令确实激活了对应的用例集(见下文“验证”一节)。
第一阶段:调查(Investigate)
获取夜间运行结果,但把排查限制在本地
指南要求的第一步是用 gh CLI 检查 evals-nightly.yml 工作流的最新运行结果,确认失败发生在哪个模型、哪个用例上。同时有一条明确的纪律:不要推送变更、不要发起远程运行,把调查完全限制在本地工作区内。这样做的目的是避免未经验证的修改污染远端历史,也让日志对比始终基于同一份代码。
读懂两类关键日志
eval 运行后会在 evals/logs/ 目录留下两类核心产物。从 evals/test-helper.ts 的 internalEvalTest 实现看:
- 工具调用轨迹日志
evals/logs/<test_name>.log:无论成功还是失败,finally块都会把rig.readToolLogs()序列化为 JSON 写入该文件; - 标准错误日志
<test_name>.stderr.log:如果 CLI 子进程产生了 stderr 输出,会一并落盘; - 活动日志
<test_name>.jsonl(GEMINI_CLI_ACTIVITY_LOG_TARGET指定):仅在测试成功时被主动删除,失败时保留,可用于进一步回放。
此外,失败时 internalEvalTest 的 catch 分支会调用 formatToolLogChain 把工具调用链摘要(Tool Call Chain (N calls): ...)追加到错误信息中——这意味着 Vitest 的失败输出本身就携带了“模型实际调用了哪些工具、以什么顺序”的第一手证据,通常足以先于完整日志定位方向。
开启详细调试日志
需要看模型原始推理与完整上下文时,指南要求设置:
export GEMINI_DEBUG_LOG_FILE="debug.log"
其底层实现是 packages/core/src/utils/debugLogger.ts 中的 DebugLogger:构造时检测 GEMINI_DEBUG_LOG_FILE 环境变量,存在则以追加模式创建写入流,此后 log/warn/error/debug 四级调用都会以 [ISO时间戳] [级别] 消息 的格式写入该文件,同时保留控制台输出。也就是说,这个开关对交互式调试和本地 eval 复现同样有效,且不会吞掉任何终端可见的输出。
诊断:区分 setup 失败与 assert 失败
指南要求审计工具日志与遥测数据,并明确记录失败归因是 setup(工作区准备)还是 assert(断言验证)。这一点在源码中有清晰边界:internalEvalTest 中 evalCase.setup(rig) 与 prepareWorkspace(写入 files 对象中的文件、初始化 git 仓库、设置 GEMINI_CLI_TRUST_WORKSPACE=true 等)属于 setup 阶段,evalCase.assert(rig, result) 才是断言阶段。两者失败的修复路径完全不同:setup 失败往往指向本地环境(API key、构建产物缺失),而 assert 失败才进入第二阶段的提示词/工具修复。
指南还给出一个实用技巧:主动加入自定义日志或诊断代码来检验假设,而不是反复猜测后运行。
第二阶段:修复策略(Fix Strategy)
这是四阶段中信息密度最高的部分,核心原则是“修模型行为,不修测试本身”。
定位与迭代式收敛
先定位出问题的测试用例与对应的提示词/代码,然后采用“迭代式范围”策略:先做一个极端改动确认问题确实在你怀疑的位置(验证 scope),再收敛为最小、精准的修改。这是一种典型的二分诊断法,避免在未确认归因前就在细微措辞上反复试错。
提示词保真度:改测试 prompt 是最后手段
指南对“断言保真度(Assertion Fidelity)”的约束非常明确:
- 修改测试 prompt 是最后手段——eval 的 prompt 往往故意写得模糊,以模拟真实用户输入;
- 警告:不要把 prompt 改得过于直接或简单,否则会丧失测试保真度,测试变成“为通过而通过”;
- 首选修复落点是工具描述、系统提示词(packages/core/src/prompts/snippets.ts)或任何参与提示词模板拼装的上游模块——指南明确说“修复应首先尝试改进
@packages/core/src/prompts/snippets.ts中的 prompt”。
从源码结构看,snippets.ts 确实定义了各工具的参数与用法片段(如 GREP_PARAM_CONTEXT、READ_FILE_PARAM_START_LINE 等来自 tool-names.js 的常量),并通过 SystemPromptOptions 等结构体参与系统提示词组装;提示词的实际解析与注入则由同目录的 prompt-registry.ts 和 promptProvider.ts 负责。修改 snippets.ts 中某段指令后,建议用 grep 确认该片段是否会被当前测试所用模型配置实际拼入提示词。
- 指令泛化原则:对系统提示词的修改应尽量泛化,只有确有必要才增加具体性。指南给出一个对照——不要写“禁用
Object.create()”这类针对具体语法的禁止清单,而应表述为覆盖底层问题的工程原则(如“优先显式组合,避免隐式原型操作”),这样对一大片相似场景都有效。文中还给出了三档具体性参考:- 低具体性:“遵循生态最佳实践”;
- 中具体性:“在适用时运用 OOP 与函数式最佳实践”;
- 高具体性:把生态特定提示作为宽泛原则的示例而非直接指令,例如“绝不使用绕过类型系统或‘隐藏’逻辑(反射、原型操作)的 hack,而应使用保持结构完整性的显式、惯用特性(类型守卫、显式类实例化、对象展开)”。
- 两条红线:不得通过修改测试工作区的
GEMINI.md来“作弊”;不得把测试 prompt 改写成“指示模型不要复现该 bug”。 - 配置提醒:提示词存在多套配置,确保修复针对的是目标模型实际使用的那一套。
修复通过之后:提示词简化
指南要求:当测试转绿后,用 ask_user 交互确认是否希望做提示词简化。简化有明确的准入标准——仅当存在可去重(de-duplicate)或可归并到单一标题下的相关子句时才尝试;且简化时必须识别出可能受影响的其它行为评估并运行它们,确保不引入回归。这体现了 eval 套件作为“提示词变更回归网”的定位:任何对共享提示词的收敛操作,都要用其它 eval 兜底验证。
架构选项:提示词调不动时,改环(Loop)
如果提示词与指令层面的调整都不见效,指南建议分析循环构成,给出的是一个简洁公式:
- AgentLoop = context + toolset + prompt;
- 循环在以下条件下表现最佳:直接(direct)的 prompt、更少的无关工具、低目标密度、最小化的低价值/无关上下文;
- 可行的改造方向包括组合子代理(subagents)或隔离工具,且必须以观察到的实际 trace 为依据;
- 最后一条警告值得强调:给出建议前要深入思考,避免照搬抽象设计指南的空话。
第三阶段:验证(Verify)
本地单文件运行
修复后用 Vitest 以非交互模式只运行目标文件。仓库 package.json 中对应的脚本为:
# 仅 ALWAYS_PASSES(CI 快速反馈集)
npm run test:always_passing_evals
# 全部 evals(含 incubation 测试,自动设置 RUN_EVALS=1)
npm run test:all_evals
针对单个文件的精确命令(RUN_EVALS=1 是运行 USUALLY_PASSES 用例的必要条件,与 test-helper.ts 中 runEval 的跳过逻辑一致):
RUN_EVALS=1 npx vitest run --config evals/vitest.config.ts my_feature.eval.ts
注意 evals/vitest.config.ts 将 testTimeout 设为 5 分钟、include 模式为 **/*.eval.ts、JSON 报告输出到 evals/logs/report.json——如果长时间无输出,先确认没有超过单用例超时。
日志对比优先于重跑
指南要求“日志对比优先”:在触发重量级测试运行之前,先通过对比修复前后的工具调用轨迹日志来诊断失败是否已消除。这与第一阶段的结论呼应——evals/logs/ 下的轨迹 JSON 是最便宜、最高信号的判据。
稳定性门槛:3 次 × 3 个关键模型
修复必须通过稳定性验证:在关键模型上本地各运行 3 次(可用脚本并行加速)。指南点名的三个模型是:
- Gemini 3.0
- Gemini 3 Flash
- Gemini 2.5 Pro
从源码看,模型由环境变量 GEMINI_MODEL 决定,缺省为预览版 Flash 模型(test-helper.ts 中 EVAL_MODEL = process.env['GEMINI_MODEL'] || PREVIEW_GEMINI_FLASH_MODEL),因此三个模型的 3 次运行可拼为:
for model in gemini-3-pro gemini-3-flash gemini-2.5-pro; do
GEMINI_MODEL=$model RUN_EVALS=1 npx vitest run --config evals/vitest.config.ts my_feature.eval.ts
done
仓库还提供 scripts/deflake.js 这类多次重复执行工具(npm run deflake -- --command="..." --runs=3),可用于批量重复同一命令并汇总失败次数,替代手写的 for 循环。
抖动规则(Flakiness Rule)
指南给出一条务实的判定规则:3 次中通过 2 次(2/3),可能属于固有噪声——这类不稳定在不做结构性拆分(structural split)的情况下难以继续改善,不应无限投入提示词微调。这与 evals/README.md 的夜间评分口径一致:夜间工作流对每个用例连跑 3 次,按 0% / 33% / 66% / 100% 打分,用例在入库前需在 Gemini 3.1 pro、Gemini 3.0 pro、Gemini 3 flash 等关键模型上达到至少 66%,晋升 ALWAYS_PASSES 则要求 100%。
第四阶段:报告(Report)
修复完成后,指南要求输出一份结构化总结,包含三项:
- 各模型测试成功率(如 3/3 = 100%),逐模型列出;
- 根因定位与修复说明:失败归因(setup 还是 assert)、最终改动落点(
snippets.ts、工具描述或工作区文件)、为何该改动是精准且泛化的; - 若最终未修复:给出高置信度的架构层建议(回到第二阶段的 AgentLoop 分析),而不是停留在猜测。
源码层面的两处补充机制
了解以下两个实现细节,能让排查过程少踩坑:
- API 错误自动重试:evals/test-helper.ts 的
withEvalRetries会捕获 500/503 类 API 错误并自动重试(最多 3 次),每次尝试以RETRY/SKIP状态写入evals/logs/api-reliability.jsonl;重试用尽后优雅跳过而非判定测试失败,以避免 API 抖动阻塞 PR。因此看到“测试没报错也没跑断言”时,应优先检查这份可靠性日志,区分“API 不稳定导致的 SKIP”与真实的 assert 失败; - 工作区即测试现场:
prepareWorkspace会把 eval 用例的files对象写入临时测试目录并执行git init+ 初始提交,同时把根目录node_modules符号链接进测试目录加速工具执行。排查 setup 类失败时,直接检查evalCase.files的路径是否合法(源码对..与绝对路径会直接抛错)是快速排除项。
小结
这套方法论的骨架是:调查(只动本地、读日志、区分 setup/assert)→ 修复(先修提示词与工具描述、保持指令泛化、禁止对测试作弊)→ 验证(日志对比优先、3 模型 × 3 次、2/3 视为固有噪声)→ 报告(成功率、根因或架构建议)。配套资产分布在仓库的 fixing.md(本文主体)、evals/README.md(策略与晋升规则)、evals/test-helper.ts(日志与重试机制)、packages/core/src/prompts/snippets.ts(首选修复落点)与 scripts/deflake.js(重复运行工具),按“先日志、后修改、先泛化、后具体”的顺序使用,即可在不牺牲测试保真度的前提下完成行为评估的系统性修复。
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