Agent 工程 · 第 9 章|评测体系:场景设计、judge 校准、A/B 实验、回归

第 9 章 · 评测体系

没有评测的 Agent 开发等于盲飞:每次改 prompt、换模型、调工具描述,你都不知道系统是变好了还是变坏了——而这三类改动每天都发生在每个 Agent 团队。评测是把 Agent 开发从"手工艺"变成"工程"的分界线。

9.1 场景(scenario):评测的基本单位

一个场景 = 一次可重复的 Agent 运行 + 对结果的判定:

scenario = {
    "id": "refund-policy-basic",
    "name": "退款政策问答",
    "task_prompt": "用户问:上个月买的耳机能退款吗?请查询政策并回答。"
                   "假设你已获取用户订单信息(耳机,35 天前购买)。",
    "modul": "chat",                          # 运行形态
    "assertions": [                            # 程序化断言
        {"type": "contains", "value": "35 天"},
        {"type": "not_contains", "value": "无法退款"},
        {"type": "regex", "value": "(?i)退款"},
    ],
    "llm_rubric": "回答是否准确引用了退款政策并给出了可执行的下一步?",
}

两类判定器,各有分工:

  • 程序化断言:contains / not_contains / regex。快、零成本、确定性强——但只能约束"形式";
  • LLM-as-judge:用(更强的)模型按 rubric 给分。能评"质量",但有偏差(见 9.2)——所以评审不可用时不能阻塞判定,程序化断言必须独立成立。

场景设计的原则:从生产失败里长出来。每个线上失败案例都应该问"能不能变成一个场景";场景库的价值随真实度上升——纯拍脑袋的场景测不出真问题。

9.2 LLM-as-judge 的偏差与校准

judge 用模型评模型,偏差是系统性的,不是随机噪声:

偏差 表现 缓解
位置偏差 两个候选放一起评时,偏好先出现/后出现的那个 交换顺序评两次,取一致结论
自我偏好 judge 偏爱与自己风格相似的输出(用模型 X 评模型 X 的输出要警惕) judge 与被评模型来自不同家族
长度偏差 偏爱更长的回答 rubric 里明确"简洁性是评分维度"
rubric 漂移 同样的 rubric 不同时间给分不同 固定打分锚点(每个分值给示例)+ 定期抽样人审校准

judge 的 prompt 工程同样重要——差的 judge prompt:

请评价以下回答的质量,给出 1-10 分。     # 没有锚点、没有维度、没有输出格式

合格的 judge prompt:

你是严格的评审。基于以下维度独立评分(每项 0-2 分):
1. 事实准确性:陈述与给定政策原文是否一致
2. 完整性:用户问题的各部分是否都被回答
3. 可执行性:用户能否直接按回答行动

输出 JSON:{"accuracy": x, "completeness": x, "actionability": x, "reasons": ["…"]}
逐条引用回答中的原句作为依据,不允许泛泛评价。

打分理由强制输出是校准的抓手:定期抽样人审"judge 的理由 vs 人的判断",分歧案例沉淀回 rubric——judge 本身也是一个需要评测的系统。

9.3 pass^k:测量非确定性

LLM 输出有随机性:同一场景跑 10 次,可能 7 过 3 不过。单次通过率掩盖了这一点。pass^k 指标:同一场景独立重跑 k 次,k 次全部通过才算通过。

pass@1 = 单次通过率(乐观,上线体验的下界不可靠)
pass^k = k 次全过率(保守,接近"每次都行"的用户体验)

工程含义:pass@1 = 90% 的场景,pass^8 ≈ 43%——如果用户每天触发这个场景 8 次,"感觉经常出错"就是真实体验。对高频场景必须用 pass^k 做门槛。成本按 k 倍计,所以 pass^k 用于关键场景子集,全量场景跑 pass@1。

9.4 轨迹评测:不只评结果,还评过程

只断言最终输出,会漏掉过程性退化。例:换模型后,回答质量不变,但"该查数据库时不再查了,开始瞎编"——输出断言全过,系统已经退化。

轨迹(trajectory)断言示例:

{"type": "tool_called", "value": "query_refund_policy"},     # 必须调用过政策查询
{"type": "tool_not_called", "value": "execute_shell"},        # 不允许碰 shell
{"type": "max_tool_calls", "value": 5},                       # 效率上限

轨迹数据的采集粒度分级(全量保存的代价是存储与隐私):

  • 轻量:工具名 + 参数摘要 + 耗时 + 是否成功(日常全量采集);
  • 完整:全参数与完整输出(仅评测场景/失败案例/抽样开启)。

轨迹级回归的威力:改一个工具的 description,跑一遍场景库,报告显示"3 个场景的工具调用序列变了,其中 1 个判定退化"——这类退化用输出断言完全看不见。

9.5 回归测试:把评测接进开发流程

Agent 的 CI 与传统 CI 的差异:测试本身调用 LLM(有成本、有随机性)。分层设计:

每次 PR:核心冒烟集(10-20 个场景,便宜模型 judge 或纯程序化断言)—— < $1
每晚:全量场景库(数百场景)—— 成本可控,早晨看报告
每周:pass^k 深跑关键场景 + judge 校准抽样
发布前:候选 vs 线上基线 的对比评测(gate)

候选门禁(candidate gate)是发布流程的关键闸门:改动的候选版本在与线上完全一致的评测环境里跑场景库,与近 N 天基线分对比,退化即拦截。工程细节:候选运行的评测结果要单独标记(如 trigger=candidate),不能混入基线时间序列——否则基线被候选污染,门禁失去参照系。

9.6 在线评测与 A/B 实验

离线场景测不出的是:真实流量分布、真实用户反馈、真实成本。A/B 实验补上这一层。

分桶:确定性哈希,同一用户永远同桶:

def assign_bucket(user_id: str) -> int:
    return int(hashlib.md5(user_id.encode()).hexdigest(), 16) % 100
    # bucket < experiment.bucket_pct → 实验组;否则 → 基线组

指标:实验组的覆盖配置(如新 prompt/便宜模型)随请求生效,按桶聚合:

  • 点踩率(用户负反馈率)——最直接的质量信号;
  • 任务失败率、重试率;
  • token 消耗与成本(实验组常为了省钱,省了多少要用数据说话)。

显著性检验:桶间点踩率差异用两比例 z 检验,避免"看两天数据拍脑袋":

import math

def two_proportion_z_test(success_a, n_a, success_b, n_b):
    """a=实验组, b=基线组。返回 z 与双尾 p 值。"""
    if n_a <= 0 or n_b <= 0:
        return None, None
    p1, p2 = success_a / n_a, success_b / n_b
    p_pool = (success_a + success_b) / (n_a + n_b)
    se = math.sqrt(p_pool * (1 - p_pool) * (1 / n_a + 1 / n_b))
    if se == 0:
        return 0.0, 1.0
    z = (p1 - p2) / se
    p = math.erfc(abs(z) / math.sqrt(2))          # 双尾
    return z, p

z, p = two_proportion_z_test(success_a=30, n_a=1000, success_b=50, n_b=1000)
# p < 0.05 才谈得上"差异显著";p=0.2 时看到的差距大概率是噪声

实验纪律:先定指标再开跑(否则会变成看着数据找口径);样本量要提前估算(低频事件如点踩需要更大的量);实验结束就关(长期分桶会让两群用户的体验持续分裂)。

9.7 数据飞轮:让失败产生价值

评测的终局形态是与改进闭环连接:

线上失败案例(点踩/任务失败)
  → LLM 归因(错在哪类环节:理解/工具选择/知识缺失/执行)
  → 改进候选(修 prompt / 补知识 / 改工具描述——按归因分类走不同管道)
  → 候选门禁评测(9.5)→ 人工审核 → 发布
  → 场景库沉淀:每个失败案例转化为一到多个新场景

再往前一步是训练数据:轨迹(9.4 完整模式)+ 结果信号(成功/失败/评分)→ SFT 数据集(成功轨迹)与 DPO 偏好对(同场景成败配对)→ 微调 → 回灌评测集验证。评测通过率就是这条流水线的质量门禁——没有评测体系的数据飞轮是在盲灌模型。

实现作业

  1. 给第 2-5 章的笔记 Agent 建 8 个场景(含 2 个故意刁难的:文件不存在、要求做危险操作),实现断言引擎与运行器;
  2. 实现 judge:按 9.2 的模板写 rubric prompt,输出 JSON 分数,评审失败时不阻塞(程序化断言独立成立);
  3. 实现 pass^k:同一场景 k=5 重跑,对比 pass@1 与 pass^8 的差异——找一个"大部分时候对偶尔翻车"的场景体会两者差距;
  4. 实现 A/B 分桶 + z 检验报告器,用两个假桶数据(各 1000 次)跑一遍,再调小样本量观察 p 值怎么失去意义。

深入材料

  • τ-bench(arXiv 2406.12045,Sierra:工具 Agent + 用户模拟 + pass^k 指标的经典设定)
  • Judging LLM-as-a-Judge(arXiv 2306.05685,MT-Bench:位置/长度/自我偏好偏差的系统性实验)
  • promptfoo / OpenAI Evals(开源评测框架,场景组织方式的参考)
← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?