Google 跟踪代码管理器(noscript) 结束 Google 跟踪代码管理器(noscript) HTML 用 Agents CLI 评测循环持续提升智能体质量 2026年10月28日 Pier Paolo Ippolito 产品经理 Elia Secchi 解决方案专家

Agents CLI 评测循环的实机演示:从首次运行到回归跟踪、模型后端实验和自动告警

1 (3)

引言

构建一个能在演示中正常工作的智能体,与构建一个能在模型升级、工具变更和全新对话形态下持续正常工作的智能体,是两个不同的问题。前者只需一个周末;后者需要一个循环。团队编写测试用例、运行它们、为它们打分、阅读失败原因、修改智能体,然后再次运行这些用例。如果分数上升,说明改动是好的。如果分数下降,就回滚。经过数周的反复,智能体会逐渐逼近模型、工具和提示词共同所能达到的上限。质量工程领域将这一过程称为爬山法。

Agents CLI 的 eval 命名空间正是围绕这一循环构建的。这些命令小巧且与框架无关:eval generate 让智能体针对用例数据集运行,eval grade 为生成的轨迹打分,eval compare 并排对比两次运行结果,eval analyze 将失败聚类为主题,eval optimize 则对智能体的提示词执行自动化的爬山优化。

下文:基线 → 回归 → 模型替换 → 调度与告警。

2 (3)

设置

贯穿全文使用的脚手架是 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 作为评判指标,位于 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
纯文本 已复制

该指标按准确性、相关性和清晰度对每个响应进行 1 到 5 分的评分,并使用 gemini-3.8-flash 作为评判者。默认数据集只有一个简单的用例,不足以进行爬山优化。我们做的第一件事是将其扩展到四个用例,以探查不同的行为:

{
  "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."
    ),
)
纯文本 已复制

这次更改能够编译,智能体也仍能运行,而预评测流水线(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.json
JSON 已复制

平均分为 2.75,低于此前的 4.0。评判者与我们的直觉一致:在天气智能体上使用厨师人设,比一个通用型的有用助手是更差的产品。标准差也从 1.41 收窄到 0.96,这与厨师提示词把大多数用例推向同一个平庸区间的情况相符。

在1到5分的量表上下降1.25分是相当大的,而且这种程度的退化在代码审查中很容易被漏掉,因为差异看起来很小(“只改了一个字符串字面量”)。这个循环在智能体进入生产环境之前就把这一变化暴露了出来,并且附带了具体的数字。

第 3 步:对比两次运行

3 (2)

平均分只是一个起点;逐用例的差异才是攀登发生的地方。eval compare 读取两个结果文件,并生成结构化的 JSON 差异:

agents-cli eval compare \
    artifacts/grade_results/results_20261003_224516.json \
    artifacts/grade_results/results_20261003_225158.json
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 ... }
}
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 recipe
纯文本 已复制

三个用例被丢弃,一个被保留。三者中最重要的是 sf_weather:厨师人设把模型的注意力从已注册的 get_weather 工具,于是智能体停止调用它,并开始编造天气数值。单元测试无法捕捉这一点:代码仍能运行,工具仍处于注册状态,但模型围绕该工具的行为已经发生了变化。只有能在上下文中对响应进行评分的端到端评测才能暴露这一问题。

4 (2)

平均分下降本身(−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 (先恢复有帮助的系统提示词),重新运行 和 ,并对比两个结果文件。平均分从 4.00 降到 2.75,降幅与厨师提示词回归相同,但逐用例的细分却讲述了完全不同的故事: 纯文本 已复制 更便宜的模型在问候语和 rome_weather 用例上与 3.8-flash 完全一致,在 sf_weather 上丢了一分(措辞略显粗糙)。整个可见的下降都追溯到 ,在那里更便宜的模型拒绝 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这类问题很常见,但并非如此。

5 (2)

关于这个实验有两点注意事项。第一,模型替换会与提示词相互作用:为更强模型调优的提示词在移植到较弱模型时往往会泄漏能力,因此正确的比较是“较弱模型搭配为其调优的提示词”与“较强模型搭配其自身调优的提示词”。第二,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})")
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: 1
Python 已复制

并与完全相同的配对进行比较(自比较):

$ 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: 0
Python 已复制

这个非零退出码正是 cron 作业、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
Plain text Copied

关于这一点,需要理解两件事。 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.

发布于: 上一篇 下一篇 相关文章 Build Generative UI Apps with A2UI & CopilotKit Agents 移动端 网页 教程 操作指南

使用 A2UI 与 CopilotKit Agents 构建生成式 UI 应用

2026年10月29日
Fast, reliable A2UI, with swappable inference formats 移动端 网页 教程 操作指南

快速、可靠的 A2UI,并支持可替换的推理格式

2026年10月27日
Smaller apps and faster GPU inference with LiteRT in Google Play services 移动端 AI 操作指南 公告

通过 Google Play 服务中的 LiteRT 实现更小的应用和更快的 GPU 推理

2026年10月28日