首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Agent 评测体系架构:4 层分工、3 个判定陷阱,和一条能跑起来的回归流水线

Agent 评测体系架构:4 层分工、3 个判定陷阱,和一条能跑起来的回归流水线

原创
作者头像
Archive
发布于 2026-09-24 11:48:21
发布于 2026-09-24 11:48:21
1660
举报
文章被收录于专栏:随笔随笔

先从一次说不清的事故讲起

一个已经上线两周的 Agent,日调用量几千次,投诉很少。有人改了一版系统提示词,把"遇到不确定就反问用户"改成了"先给出最可能的答案"。

改完当天,客服侧的印象是"好像更快了"。第三天,售后工单里"退款金额不对"多了十几条。

回滚只用了一分钟。麻烦的是后面那半天:改的到底是哪一句话导致的?影响的是退款场景,还是所有涉及金额的场景?有没有别的分支也在悄悄变差?没人答得上来,因为改之前没留任何可比对的东西。

这类事故的形态基本一样,跟模型能力关系不大。缺的不是一个更聪明的 Agent,而是一套能回答"这次改动让哪些能力变好、哪些变差、变差多少"的东西。这就是评测体系要解决的问题。

为什么"随手跑几条用例"不管用

多数团队的评测起点是这样的:一个测试文件里写十来条断言,人工跑一遍,绿了就发。这套东西会在三个地方失效。

它测不出"没变差"。 十来条用例通常都覆盖主路径,而主路径恰恰是最不容易坏的。真正被提示词改动影响的是边界分支:金额为 0、工具超时、用户中途改口。这些用例没人写,也就永远绿灯。

它的结论不可比。 今天跑 12 条,明天加了 5 条变成 17 条,通过率从 11/12 变成 15/17——这是变好了还是变差了?没有固定基线,数字就是噪声。

它判不准。 Agent 的输出是自然语言,assert response == "..." 只能测结构化字段,测不了"这次回答是否真的解决了用户问题"。

要修掉这三点,评测得当成一个系统来设计,不是一组脚本。我一般把它拆成四层。

四层:用例层、执行层、判定层、回归层

L1 用例层:把评测资产版本化

用例层管三件事:一个 case 长什么样、一组 case 怎么组织、这些资产怎么随代码一起演进。

第一个决策是把用例当代码管,不是当数据管。理由很直接:用例要跟着业务规则一起改。退款金额的计算口径变了,相关 case 的期望值必须和业务代码在同一次提交里改掉。做不到这一点,评测就会长期红着,然后有人加上一个 skip 标记把它跳过——这是评测体系最常见的死法。

所以路径结构、命名、版本号都进 Git,评审时一起看。

L2 执行层:让 Agent 变得可重复

Agent 不可重复,评测就无从谈起。执行层要处理四件事:

外部依赖打桩。 搜索、支付、CRM 这些调用不能真打。做法是给每个 case 挂一组 fixture,命中就走桩,没命中直接让 case 失败。这一点很重要——静默放过未打桩的请求,会让评测结果里掺进真实线上数据,同一批用例的结论第二天就不可复现了。

状态隔离。 每个 case 一个干净沙箱。带记忆的 Agent 尤其要注意,上一条 case 的对话历史漏进来是最隐蔽的污染源,通常要跑几十条之后才会表现为"越测越乱"。

确定性。 温度设 0 只能降低随机性,不能消除。同一个 case 跑 3 次看一致率,比跑 1 次看对错更有信息量。

并发与配额。 200 条 case 串行跑要几十分钟,没人愿意等;并发一开,模型侧限流和成本又顶上来。这个取舍放到后面算账那节。

L3 判定层:把"好"变成可执行的规则

判定层是最容易被低估的一层。它要分三级:

  • 硬断言:结构化字段、工具调用序列、禁用动作。这级必须零容忍,也最便宜。
  • 模型判定:给判定模型一段 rubric(评分标准),按维度打分。适合"回答是否覆盖了 A、B 两个要点"这类判断。
  • 人工抽检:按固定比例抽样复核,倒推模型判定的准确率。

三级之间是降级链,不是替代关系。硬断言能判的绝不交给模型判定——不是不信任模型,是没必要花那个钱,何况硬断言的结论可复现、可解释。

L4 回归层:让评测长在流程上

这层解决"谁来跑、什么时候跑、结果给谁看"。

  • 基线:每个能力维度(退款、查单、工单升级……)有一份冻结的基线分数和该轮的 case 集合。
  • 门禁:提示词、模型版本、工具定义这三类改动,合入前必须跑对应 suite,低于基线就拦住。这里要设允许波动,比如 ±2%,否则会变成和噪声搏斗。
  • 灰度:评测通过只是准入门槛,线上还要按流量比例灰度,并采集真实失败样本回流。
  • 回填:线上抓到的失败 case,脱敏后进 L1,标注 source: prod。这是用例库唯一能持续长大的健康方式。

一份可以直接改的用例结构

说清楚分层之后,落到具体数据结构。下面这个 schema 的覆盖度比较均衡:

代码语言:json
复制
{
  "case_id": "refund-003",
  "suite": "tool-use/refund",
  "schema_version": 4,
  "severity": "p0",
  "input": {
    "messages": [{ "role": "user", "content": "上周买的那单退了,钱怎么还没到" }],
    "context": { "order_id": "O-88213", "user_tier": "normal" }
  },
  "fixtures": {
    "http": [
      {
        "match": { "method": "GET", "path": "/api/orders/O-88213" },
        "respond": { "status": 200, "body": { "amount": 128.0, "status": "refunded" } }
      }
    ]
  },
  "expect": {
    "must_call": ["get_order"],
    "must_not_call": ["create_refund"],
    "assert": ["final_answer.contains('128')", "final_answer.contains('1-3')"]
  },
  "rubric_ref": "refund.explain.v2",
  "tags": ["money", "p0", "regression"]
}

几个字段值得单独说:

  • must_not_call 比 must_call 更有价值。Agent 事故里,危险操作被误触发造成的损失,通常比"该调没调"大一个量级。
  • assert 用表达式而不是全等比较。自然语言场景下全等几乎没有命中率。
  • rubric_ref 只存引用,rubric 文本单独版本化。这样改评分标准不需要动所有 case。
  • severity 决定它进哪个门禁:p0 一条都不能挂,p2 允许挂。

执行侧可以薄到只包一层:

代码语言:python
复制
import pytest

@pytest.mark.parametrize("case", load_suite("tool-use/refund"))
def test_refund(case, sandbox, recorder):
    with sandbox(case["fixtures"], strict=True) as env:
        trace = run_agent(case["input"], env=env)

    assert_structured(trace, case["expect"])            # 硬断言,先跑
    score = judge(trace, rubric=load_rubric(case["rubric_ref"]))
    recorder.record(case, trace, score)                 # 落库,供回归层比对

注意 strict=True。未打桩的请求应该让 case 直接失败,而不是放行打到真实服务上。

三个判定陷阱

只要用了模型判定,就会踩这三个坑。它们的共同点是:分数会变,但被测对象没变。

位置偏差。 让判定模型比较 A、B 两个回答,把顺序对调再跑一次,结论翻转的比例在部分模型上能到 15%~25%。缓解办法是把"双向各判一次、不一致记为平局"做成硬性流程,而不是靠提醒自己小心。

长度与格式偏差。 分点、加粗、带小标题的回答明显更容易拿高分,哪怕信息量和一段平铺直叙的回答一样。缓解办法是在送判之前剥离格式(去掉列表符号和加粗),只保留纯文本;或者在 rubric 里显式写一条"格式不计分,只判断是否覆盖要点 X/Y/Z"。

同源偏好。 判定模型对自己家族生成的答案打分偏高,这个偏差在跨代版本之间尤其明显。缓解办法是让判定模型与生成模型尽量不同源;做不到的话,就在每一批里掺 5%~10% 的人工标注样本,用它校准该批次的判定分数。

三个陷阱不用一次全解决。但位置偏差必须第一天就处理,因为它成本极低(跑两次),偏差量级却最大。

一次全量回归要花多少钱

算账的目的是回答 L2 那个取舍:并发开多大、多久跑一次全量。

假设 200 条 case,平均每条约 6 轮模型调用,每轮输入 6K tokens(含工具返回)、输出 300 tokens。按输入 4 元/百万 tokens、输出 16 元/百万 tokens 计(价格请以你实际使用的模型为准):

项目

计算

金额

被测 Agent

7.2M 输入 + 0.36M 输出

≈ 34.6 元

模型判定

0.2M 输入 + 0.04M 输出

≈ 1.5 元

合计

≈ 36 元 / 次

36 元一次,意味着全量回归可以挂在每次合并请求上,不需要舍不得。真正的约束不是钱,是模型侧并发限流——200 条 case 在没有限流退避的情况下并发跑,很容易把整条流水线的失败率拉到 20% 以上,看起来像"模型不稳定",实际是自己调用侧没控速。

所以预算应该花在限流与重试上。金额再小的一次回归,如果失败率高到没人信,也等于没做。

落地顺序

从零开始的话,别一上来就搭平台。按这个顺序做,两周内能出第一个可用版本:

  1. 先挑一个能力维度(比如退款),攒 20 条 case。不要铺开,铺开就收不回来。
  2. 打通硬断言,让这 20 条能自动跑、自动出通过数。
  3. 冻结第一版基线,把它挂到该维度的合并门禁上。
  4. 再引入模型判定,并且只判硬断言判不了的维度。
  5. 接线上失败回填,让用例库自己长。

顺序不能反。先上模型判定再补硬断言,最后通常得到一个"分数天天变、但没人知道该信哪条"的系统。

收尾

评测体系的价值不在分数本身,而在于它把"这次改动要不要发"从一个感觉问题,变成一个可以摆在桌上讨论的问题。阈值定高定低都能商量,但没有阈值的时候,每一次上线都只能靠运气。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 先从一次说不清的事故讲起
  • 为什么"随手跑几条用例"不管用
  • 四层:用例层、执行层、判定层、回归层
    • L1 用例层:把评测资产版本化
    • L2 执行层:让 Agent 变得可重复
    • L3 判定层:把"好"变成可执行的规则
    • L4 回归层:让评测长在流程上
  • 一份可以直接改的用例结构
  • 三个判定陷阱
  • 一次全量回归要花多少钱
  • 落地顺序
  • 收尾
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档