通过 Agents CLI 评估提升智能体质量
了解如何使用 Agents CLI 评估循环来提升智能体质量,涵盖从初始基线运行和回归跟踪,到模型替换和自动警报的全过程。
2026年10月28日
Agents CLI 评估循环的现场演示,涵盖从首次运行到回归追踪、模型后端实验以及自动告警的全过程
简介
构建一个在演示中能工作的智能体,与构建一个在模型升级、工具变更和新对话形态下仍能持续工作的智能体,是两个不同的问题。前者一个周末就能完成,后者则需要一个循环。团队编写测试用例、运行它们、评分、查看失败原因、修改智能体,然后再次运行用例。如果分数上升,说明改动是好的;如果下降,就回滚。经过数周这样的迭代,智能体逐步逼近模型、工具和提示词协同作用的能力上限。质量工程领域将这种方法称为爬山法(hill-climbing)。
Agents CLI 的 eval 命名空间正是围绕这一循环构建的。这些命令小巧且与框架无关:eval generate 针对用例数据集运行智能体,eval grade 为生成的轨迹评分,eval compare 并排对比两次运行结果,eval analyze 将失败聚类为主题,eval optimize 则针对智能体的提示词执行一次自动爬山优化。
下文流程:基线 → 回归 → 模型替换 → 调度与告警。
环境设置
全文使用的脚手架是 Agents CLI v1.8.0 自带的 LangChain 模板。设置只需一条命令:
agents-cli create my-agent \
--agent google/agents-cli/extensions/langchain/template@v1.8.0 \
-d cloud_run
cd my-agent && agents-cli install该脚手架自带一个评估数据集 tests/eval/datasets/basic-dataset.json,、一个配置文件 tests/eval/eval_config.yaml,以及一个自定义的 LLM 即评判者(LLM-as-judge)指标 tests/eval/response_quality.py。配置文件声明了要运行的指标:
# 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?"}]}}]}]}}
]
}四个用例分别是:一个问候、一个由模拟工具用硬编码的“多雾”回复处理的天气问题、一个工具使用默认城市处理的天气问题,以及一个智能体必须自行决定如何处理的离题问题。这是一个刻意精简的小型数据集;爬山循环只有在数据集足够小、能够在每次改动后都运行时,才能发挥价值。
第 1 步:运行首次评估
数据集就绪后,我们 run eval generate 驱动智能体逐个处理用例,然后 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 要么意味着智能体完美无缺(不太可能),要么意味着数据集太简单,无法体现差异。4.0 分加上真实的分数分布,才是一座值得攀登的山峰。
第 2 步:做出改动并重新运行
为了演示这个循环,我们引入了一个真实的回归问题。一位同时负责多个智能体项目的开发者,不小心将错误的系统提示词粘贴到了我们的天气智能体中:那是一个本该用于另一个代码库的厨师人设。在 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."
),
)改动编译通过,智能体依然能运行,而且评估前的流水线(代码检查、单元测试、类型检查)都没有发现它。重新运行相同的 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.json平均分从 4.0 降至 2.75。评判者与我们的直觉一致:给天气智能体套上厨师人设,比做一个通用的乐于助人的助手,产品体验更差。标准差也从 1.41 收紧到 0.96,这与厨师提示词将大多数用例都推向同一个平庸区间的表现相符。
在 1 到 5 的量表上,1.25 分的下降是相当显著的,而且这种回归很容易在代码审查中被漏掉,因为 diff 看起来很小("只改了一个字符串字面量")。这个循环在智能体进入生产环境之前就发现了这一变化,并附带了具体的数字。
步骤 3:比较两次运行结果
平均分只是一个起点;逐案例的差异才是爬山优化发生的地方。eval compare 会读取两个结果文件,并生成一个结构化的 JSON 差异报告:
agents-cli eval compare \
artifacts/grade_results/results_20261003_224516.json \
artifacts/grade_results/results_20261003_225158.json输出列出了哪些键发生了变化,哪些保持不变:
{
"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 ... }
}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 recipe三个案例的分数下降了,一个保持不变。其中最重要的是 sf_weather:厨师人设分散了模型对已注册的 get_weather 工具的注意力,导致智能体不再调用它,而是开始编造天气数值。单元测试无法发现这个问题:代码能运行,工具也仍然注册着,但模型围绕它的行为已经发生了变化。只有在上下文中对响应进行评分的端到端评估才能揭示这一点。
平均分的下降本身(−1.25)是一个明确的信号,但逐案例的明细才能告诉开发者该去哪里查找。一行系统提示的修改,以三种不同的方式影响了三个不同的案例:问候案例在语气上丢了一分,sf_weather 在工具使用上丢了两分,off_topic 在范围蔓延上丢了两分。
步骤 5:更换模型后端
一个常见的爬山优化问题是:更便宜或更快的模型是否能胜任?对于 LangChain 模板,模型定义在 app/agent.py 中:
LLM = "gemini-3.8-flash"
llm = ChatGoogleGenerativeAI(model=LLM, temperature=0)我们将 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 ↓↓更便宜的模型在问候和 rome_weather 案例上与 3.8-flash 表现完全一致,只在 sf_weather 上丢了一分(措辞稍显粗糙)。整个可见的下降都归因于 off_topic,在该案例中,更便宜的模型以"事件尚未发生"为由进行了回避(这是训练截止时间的产物),而评审模型对此给出了严厉的评分。
这里的教训是,汇总数字具有误导性。只看平均分,你会认为 gemini-2.5-flash 和厨师提示一样糟糕。看逐案例的明细,你会认为对于该智能体的大多数工作负载而言,这是一个合理的替换,只有一个案例(处理关于近期事件的超范围问题)暴露了一个真实的局限。如果生产环境中用户问题的分布更接近前三个案例而非第四个,那么这个替换值得考虑。如果 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/每晚针对一个稳定的回归数据集运行一次,可以捕捉到模型更新、智能体工具的数据变化,以及在智能体脚下悄悄变动的依赖所带来的漂移。对于生产环境中的智能体,定时运行还能捕捉到模型供应商的静默升级——这些升级可能在我们这边毫无变化的情况下改变智能体的行为。定时运行所用的数据集通常比内循环中使用的更大(几十到几百个用例,而不是几个),因为它每天只运行一次。
第 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})")针对我们的回归演示运行它(基线为 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: 1再针对一对相同的结果运行(自我比较):
$ 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: 0cron 任务、Cloud Scheduler 或 GitHub Actions 步骤正是通过这个非零退出码来接收信号并发送告警的。
第 8 步:自动化提示词优化
对于 ADK 项目,eval optimize 会使用 GEPA 框架对智能体的提示词执行自动爬山优化(目前不支持 LangChain 扩展)。该命令接受一个数据集和一个目标指标,提出提示词变体,对每个变体进行评分,保留能提升指标的变体,然后迭代。输出是一组按评估分数排名的候选提示词,随后由人工审查并将其提升到智能体定义中。
agents-cli eval optimize \
--dataset tests/eval/datasets/regression.json \
--target-metric final_response_quality \
--config tests/eval/eval_config.yaml关于 eval optimize 有两点需要了解。第一,它仅限于提示词优化,而非任意的智能体改动:智能体的工具、结构和模型都保持不变。第二,GEPA 有时能找到真正的改进,有时则只是找到了通过过拟合特定指标的措辞而获得更高分数的提示词变体。审查者就是控制机制。对于适用此循环的项目,规范做法是在回归数据集上运行 eval optimize(在那里改进是可衡量的),并在提升候选提示词之前进行仔细审查。
我们从这个循环中学到了什么
以下是我们自己运行这个循环以及观察团队实践后总结出的一些模式与反模式。
先用一个小而有代表性的数据集起步,而不是追求全面的数据集。刚开始做评估时,人们总想覆盖智能体可能遇到的每一个边缘情况。结果是一个要跑一个小时的数据集,而且每次改动都没人去跑。像上面那样四个用例在一分钟内跑完,覆盖了主要行为,每次改动都会运行。一个包含三百个用例、要跑一小时的回归数据集,往好了说是个发布关卡,往坏了说就是束之高阁的摆设。
保存结果文件快照,而不是分数。分数是摘要;结果文件才是证据。当一个回归问题在提示词改动三周后才浮现时,拥有改动前后的完整结果 JSON(而不仅仅是平均分)才能让 eval compare 展示出具体是哪个用例发生了变化。上面第 4 步中的逐用例明细,仅凭分数是无法得出的。
将数据集与其期望分开。数据集是输入,评估配置是期望。它们以不同的节奏演进:数据集随着生产环境中出现新的真实案例而增长,而配置则随着团队对“好”的定义成熟而演进。将它们保存在不同的文件中,并都提交到版本控制,可以清楚地看出当分数发生变化时,是哪一半发生了变化。
对汇总指标保持怀疑。第 5 步中的模型替换实验是最清晰的例子。平均分下降了半分,但逐案例的细分显示,下降集中在其中一个案例上,而其他案例保持不变或有所改善。只看平均值的团队会做出错误的判断。
更多的评估并不自动带来更多的修复。我们最近进行的一项对照研究发现,使用 agents-cli 的编码智能体运行的评估次数是没有使用它的四倍,但在评估后修改智能体的频率与基线大致相同。行为上的提升来自于技能在开始时构建的内容,而不是来自内部迭代。评估对于质量是必要的,但质量来自于根据评估结果采取行动。
快速开始
只需几条命令即可搭建一个 Agents CLI 项目并运行第一个评估循环:
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。源代码(包括第一方评估原语)位于 github.com/google/agents-cli。