033AI Agent阅读记录
第 033 卷

从零开始 AI Agent 实战(四):建立可重复的评测基线

用 YAML 用例和断言式评测建立 Agent 基线,量化工具选择、参数正确性、拒答、步数、延迟与成本。

乌漆嘛黑和 Ahri
第 033 期

建立可重复的评测基线

手测为什么会骗人

同一个问题连续问五次,模型可能分别选择 get_ticketlist_tickets、直接回答、调用两次 get_ticket,甚至编一个工单号。你记住的往往是最好的一次。没有固定输入、固定数据和固定断言,就无法证明第 3 篇的循环真的可靠。

一条用例不只是 expected_answer

- id: ticket-owner-can-read
  input: "查询我的 TICKET-000042"
  user: {id: u-1, role: employee}
  expect:
    tools:
      - name: get_ticket
        args: {ticket_id: TICKET-000042}
    answer_contains: ["TICKET-000042", "VPN"]
    max_steps: 3
    must_not: ["stack trace", "password"]
    latency_ms_p95: 1500
- id: unknown-ticket-refuses
  input: "TICKET-999999 的处理人是谁?"
  user: {id: u-1, role: employee}
  expect:
    tools: [{name: get_ticket, args: {ticket_id: TICKET-999999}}]
    answer_contains: ["没有找到", "无法确认"]
    must_not: ["Alice", "已解决"]

断言分成六维:工具是否选对、参数是否正确、答案是否满足关键事实、该拒答时是否拒答、步数是否超限、延迟/Token/成本是否超过预算。

Preview
六个维度分别记分,不合成一个总分

用例总量在初始阶段先控制在 20~30 条。这个规模适合冷启动:低于 20 条覆盖不到拒答和越权这类边角,高于 30 条则每轮实验的等待时间会长到让人不想跑。等到第 11 篇做单因子检索实验时,这套用例会被连跑十几遍,那时快速反馈比覆盖长尾更重要;引入双源语料后,评测集才会扩展到百条规模。

评测运行器

Preview
用例 → 跑 Agent → 断言 → 报表,报表反过来决定改动去留
async def evaluate(case, agent):
    started = monotonic()
    trace = await agent.run(case.input, user=case.user)
    elapsed = (monotonic() - started) * 1000
    checks = {
        'tool_name': trace.tool_names == [x['name'] for x in case.expect['tools']],
        'tool_args': args_match(trace.tool_calls, case.expect['tools']),
        'answer': contains_all(trace.text, case.expect.get('answer_contains', [])),
        'must_not': contains_none(trace.text, case.expect.get('must_not', [])),
        'steps': trace.steps <= case.expect.get('max_steps', 99),
        'latency': elapsed <= case.expect.get('latency_ms_p95', 10_000),
    }
    return EvalResult(case.id, all(checks.values()), checks, elapsed, trace.usage)

运行器输出 JSONL,而不是只输出一个百分比,这样失败用例可以进入 CI artifact:

uv run python -m ai_agent_guide.eval --cases docs/eval/cases.yaml \
  --provider mock --json-out reports/baseline.jsonl

Mock 评测和真实模型评测分开

Mock 评测验证协议和业务逻辑,结果必须稳定;真实模型评测验证工具选择和语言质量,允许设置温度 0 并重复 3 次取均值。不要把二者混成一个“总分”,否则供应商波动会掩盖代码回归。

建议报告:

指标第 3 篇基线目标
工具选择成功率100%(Mock)>= 98%
参数正确率100%(Mock)>= 98%
拒答准确率100%(Mock)>= 90%
平均步数1.8不超过 4
p95 延迟240 ms不超过 2 s
单问成本0(Mock)不超过 0.01 USD

先制造一个回归

max_steps 从 6 改成 2,再运行包含“先列工单再查详情”的用例。评测应该明确指出是 steps 断言失败,而不是只说“总分 92%”。修复后保留这个用例,防止未来为了降低延迟再次砍掉必要步骤。

从这篇起,每篇都要更新同一张表

这是本篇真正的产出——不是运行器的代码,而是一个从现在延续到第 15 篇的习惯:每篇改完代码,跑一次评测,把结果追加到同一张表里。

篇次   通过率   平均步数   p95 延迟   单问 Token   本篇改动
 4     基线      基线       基线        基线       建立评测基线
 5      ?         ?          ?           ?        换 PostgreSQL
 6      ?         ?          ?           ?        加入权限过滤
 7      ?         ?          ?           ?        审批与幂等
 ...
15      ?         ?          ?           ?        汇总成完整曲线

问号要等真正跑过才能填。这里不预设数字,因为预设的数字会变成心理锚点——真实结果比它差时,人会本能地去调整评测集而不是修代码。

这张表的价值不在于某一行好看,而在于相邻两行之间的差。它能回答几个否则只能靠感觉的问题:

  • 第 6 篇加了权限过滤,通过率掉了多少?如果掉得多,是过滤逻辑错了,还是用例本身默认了越权行为?
  • 第 11 篇加了 rerank,Recall 涨了几个点、每问多花多少 Token?涨 2 个点却翻倍成本,值不值得留?
  • 第 13 篇换成 LangGraph,分数应该基本不变。如果变了,说明这不是一次等价重写,而是悄悄改了行为。

最后一条尤其重要。重构类改动的正确预期是「分数持平」,而没有基线时,你既证明不了它没变坏,也发现不了它变坏了。

评测数据必须可审计

仓库提供合成语料和标注,不使用真实客户内容。每个用例注明:输入、用户身份、数据库 fixture、相关工具序列、关键答案片段和允许的拒答表达。新增功能先新增用例再写实现,避免“实现了什么就测什么”。

本篇验收标准

  1. make eval 或等价命令可在没有模型 Key 的 Mock 模式下运行。
  2. 报告至少包含成功率、平均步数、p95 延迟和估算成本。
  3. 故意破坏工具参数时,报告能定位到具体用例和具体维度。
  4. CI 在评测失败时返回非零退出码,并保留 JSONL 报告。

下一篇会让 JSON 文件在并发请求下撞号,再用 PostgreSQL 的事务和唯一约束解决这个真实问题。