Google 用 Agents CLI 评测循环演示 Agent 质量爬坡
Hill-Climbing Agent Quality with Agents CLI Evaluation
Google Developers Blog 以 Agents CLI v1.8.0 的 LangChain 模板为例,完整走查了 eval generate、grade、compare 等命令组成的 Agent 质量爬坡循环。
原文用一次完整走查展示评测循环如何定位提示词回归和模型替换的差异,可迁移到自己的 Agent 项目。
2026年10月28日
对 Agents CLI 评估循环的现场演示,从首次运行到回归跟踪、模型后端实验以及自动告警
引言
构建一个能在演示中运行的 agent,与构建一个能在模型升级、工具变更和全新对话形态下持续正常工作的 agent,是两个不同的问题。前者只需一个周末;后者需要一个循环。团队编写测试用例、运行它们、为它们评分、阅读失败原因、修改 agent,然后再次运行这些用例。如果分数上升,说明改动是好的。如果分数下降,就回滚。经过数周这样的迭代,agent 会逐步逼近模型、工具和提示词共同所能达到的上限。质量工程领域将这一过程称为爬山法。
Agents CLI 的 eval 命名空间正是围绕这一循环构建的。这些命令小巧且与框架无关:eval generate 让 agent 针对数据集中的用例运行,eval grade 为生成的轨迹评分,eval compare 并排对比两次运行结果,eval analyze 将失败聚类为主题,eval optimize 则对 agent 的提示词执行自动化的爬山优化。
下文:基线 → 回归 → 模型替换 → 调度与告警。
环境搭建
全文使用的脚手架是 Agents CLI v1.8.0 自带的 LangChain 模板。搭建只需一条命令:
agents-cli create my-agent \
--agent google/agents-cli/extensions/langchain/[email protected] \
-d cloud_run
cd my-agent && agents-cli install纯文本
已复制
该脚手架自带一个位于 tests/eval/datasets/basic-dataset.json, 的 eval 数据集、一个位于 tests/eval/eval_config.yaml 的配置,以及一个位于 tests/eval/response_quality.py 的自定义 LLM-as-judge 指标。配置中声明了要运行的指标:
# tests/eval/eval_config.yaml
metrics_to_run:
- custom_response_quality
custom_metrics:
- name: custom_response_quality
custom_function_file: response_quality.py纯文本
已复制
该指标以 gemini-3.8-flash 作为评判者,从准确性、相关性和清晰度三个方面对每个响应按 1 到 5 分进行评分。默认数据集只有一个简单的用例,不足以支撑爬山优化。我们做的第一件事是将其扩展为四个探测不同行为的用例:
{
"eval_cases": [
{"name": "greeting",
"agent_data": {"turns": [{"events": [{"author": "user", "content": {"parts": [{"text": "hello"}]}}]}]}},
{"name": "sf_weather",
"agent_data": {"turns": [{"events": [{"author": "user", "content": {"parts": [{"text": "What is the weather in San Francisco?"}]}}]}]}},
{"name": "rome_weather",
"agent_data": {"turns": [{"events": [{"author": "user", "content": {"parts": [{"text": "How is the weather in Rome today?"}]}}]}]}},
{"name": "off_topic",
"agent_data": {"turns": [{"events": [{"author": "user", "content": {"parts": [{"text": "Who won the 2024 World Series?"}]}}]}]}}
]
}纯文本
已复制
四个用例:一个问候语;一个天气问题,由 mock 工具以硬编码的“有雾”响应处理;一个针对该工具默认城市的天气问题;以及一个需要 agent 自行决定如何处理的离题问题。这个数据集是刻意做小的;当数据集小到可以在每次改动时都运行,爬山循环才能带来回报。
第 1 步:运行首次评估
数据集就位后,我们用 run eval generate 让 agent 针对每个用例运行,然后用 eval grade 为生成的轨迹评分:
agents-cli eval generate --dataset tests/eval/datasets/basic-dataset.json \
-o tests/eval/output/
agents-cli eval grade --traces tests/eval/output/eval_dataset_*.json \
--config tests/eval/eval_config.yaml纯文本
已复制
基线运行产生:
Evaluation Summary
custom_response_quality:
num_cases_total: 4
num_cases_valid: 4
num_cases_error: 0
mean_score: 4.0000
stdev_score: 1.4142
Saved full results to artifacts/grade_results/results_20261003_224516.json
Saved HTML results to artifacts/grade_results/results_20261003_224516.html纯文本
已复制
四个用例的平均分为 4.0(满分 5),标准差为 1.41。这个标准差立刻告诉我们,各用例的得分并不一致,这比清一色的 5.0 是更健康的起点。清一色的 5.0 要么意味着 agent 完美无缺(不太可能),要么意味着数据集太简单,无法体现差异。4.0 且存在真实离散度,才是一座有待攀登的山。
第 2 步:做出改动并重新运行
为了演示这一循环,我们引入一个贴近现实的回归。一位同时处理多个 agent 项目的开发者,不小心把错误的系统提示词粘贴到了我们的天气 agent 中:一个本用于另一个代码库的厨师人设。在 app/agent.py 中:
# Before
root_agent: CompiledStateGraph = create_agent(
model=llm, tools=[get_weather], system_prompt="You are a helpful assistant"
)
# After
root_agent: CompiledStateGraph = create_agent(
model=llm, tools=[get_weather],
system_prompt=(
"You are a professional chef. Help users with recipes, cooking "
"techniques, and ingredient substitutions. Always suggest a recipe "
"at the end of every response."
),
)纯文本
已复制
改动可以编译,agent 仍能运行,评估前的流水线(lint、单元测试、类型检查)中没有任何环节能发现它。重新运行同样的 eval generate 加 eval grade 序列,产生:
Evaluation Summary
custom_response_quality:
num_cases_total: 4
num_cases_valid: 4
num_cases_error: 0
mean_score: 2.7500
stdev_score: 0.9574
Saved full results to artifacts/grade_results/results_20261003_225158.jsonJSON
已复制
平均分 2.75,低于 4.0。评审的判断与我们的直觉一致:在天气 agent 上使用厨师人设,产品效果不如一个通用的有用助手。标准差也从 1.41 收窄到 0.96,这与厨师提示词把大多数用例推向同一个平庸区间的情况相符。
在 1 到 5 的评分尺度上,1.25 分的下降相当可观,而且这类回归很容易在代码审查中漏过——当 diff 看起来很小(“改了一个字符串字面量”)时。这个循环在 agent 进入生产环境之前就把这一改动暴露了出来,并附上了具体的数字。
第 3 步:比较两次运行
平均分只是起点;逐用例的 diff 才是爬山法真正发生的地方。eval compare 读取两个结果文件,并生成结构化的 JSON diff:
agents-cli eval compare \
artifacts/grade_results/results_20261003_224516.json \
artifacts/grade_results/results_20261003_225158.jsonJSON
已复制
输出会列出哪些键发生了变化、哪些保持不变:
{
"baseline_keys": ["agent_info", "eval_case_results", "evaluation_dataset",
"metadata", "summary_metrics", "win_rates"],
"candidate_keys": ["agent_info", "eval_case_results", "evaluation_dataset",
"metadata", "summary_metrics", "win_rates"],
"changed_keys": ["eval_case_results", "metadata.creation_timestamp"],
"unchanged_keys": ["agent_info", "evaluation_dataset", "metadata",
"summary_metrics", "win_rates"],
"differences": { ... per-case score diffs ... }
}JSON
已复制
differences 对象包含完整的逐用例明细,基线值与候选值并排展示。对于爬山法工作流,我们通常会把输出通过 jq 或一个小 Python 脚本处理,以提取信号;原始 JSON 就是完整的证据。
第 4 步:解读逐用例得分
遍历两个结果文件并逐用例对齐分数,就能看出回归落在哪里:
case baseline → candidate what the chef prompt did
greeting 5.0 → 4.0 ↓ launched into an unsolicited recipe pitch after "hello"
sf_weather 4.0 → 2.0 ↓ ignored the weather tool, hallucinated conditions
rome_weather 2.0 → 2.0 · already hallucinated at baseline; chef persona did not make it worse
off_topic 5.0 → 3.0 ↓ answered the Dodgers question but tacked on a recipePlain text
已复制
三个用例下降,一个保持不变。三者中最重要的是 sf_weather:厨师人设把模型的注意力从已注册的 get_weather 工具上拉走了,于是 agent 不再调用它,转而编造天气数值。单元测试抓不到这个问题:代码能跑,工具仍然注册着,但模型围绕它的行为已经变了。只有对上下文中的响应进行评分的端到端 eval 才能暴露这一点。
平均分的下降本身(−1.25)就是一个清晰的信号,但逐用例明细才能告诉开发者该往哪里看。一行系统提示词的改动以三种不同方式传导到了三个不同用例:greeting 在语气上丢了一分,sf_weather 在工具使用上丢了两分,off_topic 在范围蔓延上丢了两分。
第 5 步:更换模型后端
爬山法中一个常见的问题是:更便宜或更快的模型能否胜任。对于 LangChain 模板,模型定义在 app/agent.py 中:
LLM = "gemini-3.8-flash"
llm = ChatGoogleGenerativeAI(model=LLM, temperature=0)Plain text
已复制
我们把 gemini-3.8-flash 换成 gemini-2.5-flash(先恢复有用的系统提示词),重新运行 eval generate 加 eval grade,并比较两个结果文件。平均分从 4.00 降到 2.75,幅度与厨师提示词的回归相同,但逐用例明细讲述的却是完全不同的故事:
case 3.8-flash → 2.5-flash
greeting 5.0 → 5.0 ·
sf_weather 4.0 → 3.0 ↓
rome_weather 2.0 → 2.0 ·
off_topic 5.0 → 1.0 ↓↓Plain text
已复制
更便宜的模型在 greeting 和 rome_weather 用例上与 3.8-flash 完全一致,在 sf_weather 上丢了一分(措辞略欠打磨)。整个可见的下降都追溯到 off_topic:更便宜的模型以“事件尚未发生”的含糊说法拒绝回答(训练截止时间造成的假象),评审对此打了低分。
教训是汇总数字具有误导性。只看平均分,你会认为 gemini-2.5-flash 和厨师提示词一样糟糕。看逐用例明细,你会认为对于该 agent 的大部分工作负载而言这是一次合理的替换,只有一个用例(处理关于近期事件的超范围问题)暴露了真正的局限。如果生产环境中用户问题的分布更接近前三个用例而非第四个,那么这次替换值得考虑。如果 off_topic 这类问题很常见,那就不值得。
关于这个实验有两点注意事项。第一,模型替换会与提示词相互作用:为更强模型调优的提示词在移植到较弱模型时往往会泄漏能力,因此正确的比较是“较弱模型配为其调优的提示词”与“较强模型配其自己调优的提示词”。第二,Vertex AI Eval Service 会调用一个评判模型来给轨迹打分,且该评判模型在比较过程中保持不变。更换评判模型是另一个实验,应单独进行。
第 6 步:调度循环
Agents CLI 不附带调度器,但 eval 命令被设计为可直接接入标准基础设施。我们这里可以使用的模式是 cron 作业或 Cloud Scheduler 触发器,按固定周期运行 eval generate 加 eval grade,并将结果写入持久化位置:
#!/usr/bin/env bash
# scripts/scheduled_eval.sh — invoked nightly by cron or Cloud Scheduler
set -euo pipefail
cd "$(dirname "$0")/.."
DATE=$(date +%Y%m%d)
OUT=artifacts/scheduled_runs/$DATE
mkdir -p $OUT
agents-cli eval generate --dataset tests/eval/datasets/regression.json -o $OUT/
agents-cli eval grade --traces $OUT/eval_dataset_*.json \
--config tests/eval/eval_config.yaml \
--output $OUT/纯文本
已复制
针对稳定回归数据集的每晚运行可以捕获来自模型更新、agent 工具中的数据变化以及 agent 脚下发生变动的依赖项的漂移。对于生产 agent,定时运行还能捕获模型供应商的静默升级,这些升级可能在我们这边没有任何改动的情况下改变 agent 的行为。定时运行所用的数据集通常比内层循环所用的更大(几十到几百个用例,而非几个),因为它每天只运行一次。
第 7 步:分数下降时告警
定时运行每天生成一个结果文件,但没人看的文件不算告警。这里的模式可以是一个包装脚本,将每次新运行的均值与基线(上一次运行,或已提交的已知良好结果)进行比较,当下降超过阈值时以非零状态退出。非零退出码正是触发周围调度器发出告警的原因。
下面是一个示例:
#!/usr/bin/env python3
"""Exit non-zero if an eval result dips below baseline by more than --threshold."""
import argparse, json, sys
def mean(path):
with open(path) as f:
return json.load(f)['summary_metrics'][0]['mean_score']
p = argparse.ArgumentParser()
p.add_argument('candidate')
p.add_argument('--baseline', required=True)
p.add_argument('--threshold', type=float, default=0.05)
args = p.parse_args()
b, c = mean(args.baseline), mean(args.candidate)
drop = b - c
if drop > args.threshold:
print(f"FAIL: mean dropped {b:.3f} → {c:.3f} "
f"(drop {drop:.3f} > threshold {args.threshold})")
sys.exit(1)
print(f"OK: mean {b:.3f} → {c:.3f} (drop {drop:.3f} ≤ threshold {args.threshold})")Python
已复制
对我们的回归演示运行它(基线 4.0,来自 chef-prompt 运行的候选值 2.75):
$ python3 scripts/check_score.py \
artifacts/grade_results/results_20261003_225158.json \
--baseline artifacts/grade_results/results_20261003_224516.json \
--threshold 0.1
FAIL: mean dropped 4.000 → 2.750 (drop 1.250 > threshold 0.1)
$ echo "exit: $?"
exit: 1Python
已复制
再对一对完全相同的结果运行(自比较):
$ python3 scripts/check_score.py \
artifacts/grade_results/results_20261003_224516.json \
--baseline artifacts/grade_results/results_20261003_224516.json \
--threshold 0.1
OK: mean 4.000 → 4.000 (drop 0.000 ≤ threshold 0.1)
$ echo "exit: $?"
exit: 0Python
已复制
非零退出码正是 cron 作业、Cloud Scheduler 或 GitHub Actions 步骤用来发送告警的依据。
第 8 步:自动化提示词优化
对于 ADK 项目,eval optimize 使用 GEPA 框架对 agent 的提示词执行自动爬山优化(目前 LangChain 扩展不支持)。该命令接收一个数据集和一个目标指标,提出提示词变体,对每个变体打分,保留能提升指标的变体,并迭代进行。输出是一组按评估分数排序的候选提示词,然后由人工审查并提升到 agent 定义中。
agents-cli eval optimize \
--dataset tests/eval/datasets/regression.json \
--target-metric final_response_quality \
--config tests/eval/eval_config.yaml纯文本
已复制
关于 eval optimize 需要理解两点。第一,它的范围限于提示词优化,而非任意 agent 变更:agent 的工具、结构和模型保持不变。第二,GEPA 有时能找到真正的改进,有时则通过过拟合特定指标的措辞而找到在该指标上得分更高的提示词变体。审查者是控制手段。对于该循环适用的项目,纪律是在回归数据集上运行 eval optimize(改进在那里可衡量),并在提升前仔细审查候选提示词。
我们从循环中学到了什么
以下是我们自己运行这个循环以及观察团队做同样事情时总结出的一些模式与反模式。
从一个小而有代表性的数据集开始,而不是一个全面的数据集。开始做 eval 时的诱惑是试图覆盖 agent 可能遇到的每一个边缘情况。结果就是一个运行需要一小时、且没人会在改动时运行的数据集。如上所述,四个在一分钟内跑完的用例覆盖了主要行为,并且每次改动都会运行。一个包含三百个用例、运行一小时的回归数据集,往好了说是发布门禁,往坏了说就是束之高阁的摆设。
快照保存结果文件,而不是分数。分数是摘要;结果文件是证据。当回归在提示词改动三周后才浮现时,拥有改动前后的完整结果 JSON(而不只是平均分)才能让 eval compare 展示出具体是哪个用例发生了变化。上面第 4 步中的逐用例细分仅凭分数是无法做到的。
将数据集与其预期分离。数据集是输入。eval 配置是预期。它们以不同的节奏演进:数据集随着生产中新的真实用例出现而增长,配置随着团队对“好”的定义成熟而演进。将它们放在不同的文件中,并都提交到版本控制,就能清楚地看出当分数变化时是哪一半发生了变化。
对汇总指标保持怀疑。第 5 步中的模型替换实验是最清楚的例子。平均值下降了半分,但逐用例细分显示下降集中在一个用例上,而其他用例保持或有所改善。只看平均值的团队会做出错误的判断。
更多的 eval 并不会自动带来更多的修复。我们最近进行的一项对照研究发现,使用 agents-cli 的编码 agent 运行的 eval 数量是不使用它的编码 agent 的四倍,但在 eval 之后更改 agent 的频率与基线大致相同。行为上的提升来自技能在开始时放入构建中的内容,而不是来自内部迭代。Eval 对质量是必要的,但质量来自根据 eval 所显示的内容采取行动。
开始使用
用几条命令搭建一个 Agents CLI 项目并运行第一个 eval 循环:
uvx google-agents-cli setup
agents-cli create my-agent
cd my-agent && agents-cli install
agents-cli eval generate --dataset tests/eval/datasets/basic-dataset.json \
-o tests/eval/output/
agents-cli eval grade --traces tests/eval/output/eval_dataset_*.json \
--config tests/eval/eval_config.yaml纯文本
已复制
完整的 eval 命名空间文档位于 google.github.io/agents-cli/cli。源代码(包括第一方 eval 原语)位于 github.com/google/agents-cli。
上一页
下一页
来源:Google Developers Blog · developers.googleblog.com