2615 字
约 8 分钟
19
S21-X15 · Agent 的评测与可观测

问答题的评测可以只比字符串。Agent 不行:同一个任务有无数条合法路径,中间还调了工具、改了数据库。于是"跑十次有八次对"这句话没有信息量——你不知道那两次是路径错了、工具调错了,还是参数少了一个字段。这篇给一套能落地的层次划分、轨迹字段和回归集组织方式。


1. 难在哪:三个不成立的假设

单次问答的假设 Agent 的现实
答案唯一可比 路径不唯一,最终输出相同但过程可能违法
一次调用即一次判定 一次任务数十次调用,错一步全盘皆错
只看输出就够 副作用写在外部系统里,输出看不出来

第三点最容易被忽略:agent 说"已退款",钱可能没动,也可能动了两次。 评测必须能读到工具的真实结果,而不只是模型的自述。


2. 评测的四个层次

四层从下往上,下层不过就别评上层。

层次 评什么 判定方式 失败典型
L1 最终结果 输出是否正确、格式是否合规 断言 / 规则 / 人工抽检 JSON 缺字段
L2 中间步骤 步骤序列是否合理、有无多余绕路 关键步骤存在性断言 该先校验却直接提交
L3 工具调用 选得对不对、参数对不对、次数对不对 与期望调用序列比对 重复调用、漏调校验
L4 成本与延迟 token、调用次数、端到端耗时 阈值 + 趋势监控 一次任务几十次调用

只在 L1 打分,会出现"结果对但过程不可用":一次退款成功,但中间查了三次同一个订单、调了两次创建接口(第二次幂等挡掉了)。 这种系统在并发上来时会先崩。


3. 轨迹要记什么

字段 内容 用途
trace_id / span_id 会话与步骤标识 串联、跨服务关联
step_index 第几步 复现顺序
input_digest 该步输入的摘要或全文 复盘"它当时看到了什么"
tool.name 选中的工具 判断选择是否正确
tool.args 完整入参 没有它就无法复盘参数错误
tool.result 结果(截断)与 isError 判断是否真的成功
latency_ms / tokens 每步开销 定位成本热点
model / version 模型与提示词版本 A/B 归因
step = {
    "trace_id": trace_id, "span_id": new_span(), "step_index": i,
    "agent": role, "model": MODEL_ID, "prompt_version": PROMPT_VER,
    "input_digest": digest(payload),
    "tool": {"name": name, "args": args, "result": truncate(result, 2000),
             "is_error": is_error},
    "latency_ms": elapsed, "tokens": usage,
}

tool.args 单独标粗:没有入参的日志,只能证明"调了",无法证明"调对了"。


4. 步骤级评测怎么写

不要只断言最终答案,对关键步骤下断言。

def assert_trajectory(trace: list[dict]) -> list[str]:
    errs = []
    names = [s["tool"]["name"] for s in trace if s.get("tool")]
    if "verify_order" not in names:
        errs.append("缺少前置校验 verify_order")
    if names.count("create_refund") > 1:
        errs.append("create_refund 被重复调用")
    for s in trace:
        if s["tool"]["name"] == "create_refund":
            if "order_id" not in s["tool"]["args"]:
                errs.append("create_refund 缺少 order_id")
    return errs

断言分三类,按稳定性排序:

断言类型 例子 稳定性
必须发生 调用过 verify_order 高,路径无关
禁止发生 未校验就 create_refund 高
顺序约束 verify 早于 create 中,注意并行
次数约束 每个工具调用不超过 N 次 中

不要断言"完整步骤序列":模型换个顺序仍可能正确,断言太死会让评测集天天红。


5. 工具调用准确率怎么度量

把它拆成四个可分别统计的错误类型。

错误类型 定义 危害
漏调 该调没调 后续全错,静默失败
误调 不该调却调了 副作用、成本
参数错 调了但字段缺/值错 重试掩盖问题
重复调 同一动作多次执行 幂等挡得住,挡不住就事故
def tool_metrics(cases):
    m = {"missing": 0, "extra": 0, "bad_args": 0, "dup": 0, "total": 0}
    for c in cases:
        called = [s["tool"]["name"] for s in c["trace"] if s.get("tool")]
        m["total"] += 1
        m["missing"] += len(set(c["expect_tools"]) - set(called))
        m["extra"]   += len(set(called) - set(c["expect_tools"]))
        m["dup"]     += sum(1 for n in set(called) if called.count(n) > 1)
        m["bad_args"] += args_mismatch(c)
    return {k: (v / max(m["total"], 1)) for k, v in m.items() if k != "total"}

四个指标要分开看:漏调上升通常是描述含糊,误调上升通常是工具语义重叠,重复调上升通常是重试策略没配幂等键。 合成一个"准确率"数字,就失去了定位能力。


6. 回归集怎么组织

回归集的来源只有一个:线上真实失败的轨迹。凭空编造的用例测不出系统的真实弱点。

来源 转化方式 优先级
线上失败轨迹 固化为用例(输入 + 期望工具集 + 期望结论) 最高
用户投诉 补一条带边界的用例 高
模型/提示词升级后的偏差 新旧对比后取差集 中
人工构造的极端输入 空值、超长、权限不足 中
{
  "case_id": "refund-8821-dup",
  "input": {"user": "订单 8821 退款", "order_id": "8821"},
  "expect_tools": ["query_order", "verify_order", "create_refund"],
  "forbid": ["create_refund > 1"],
  "expect_check": {"verdict": "pass"},
  "source_trace": "trace-2026-11-03-0412"
}

保留 source_trace 字段:用例和原始轨迹的链接是复盘的地基,没有它,半年后没人知道这条用例为什么存在。


7. 可观测的三个视图

视图 粒度 回答的问题
单次会话时间线 一次 trace 这次为什么失败
按环节聚合 按工具/按步骤 哪个环节最常失败
失败原因分布 按错误类型 该先修哪一类

单次视图用于排障,聚合视图用于决策。只有单次视图的团队,会陷入"每次都修个案"的循环。

# 聚合:按工具统计失败率(示例查询,字段名按你的存储调整)
# SELECT tool_name, count(*) FILTER (WHERE is_error) / count(*) AS fail_rate
# FROM steps WHERE ts > now() - interval '7 days' GROUP BY 1 ORDER BY 2 DESC;

8. 人工介入率要单独监控

人工介入率是 Agent 系统最诚实的一个指标:它同时反映能力上限与体验。

指标 定义 上升意味着
触发率 需要人确认的任务 / 总任务 模型判断力不足或权限过窄
采纳率 人确认通过的 / 触发确认的 建议质量差,人在反复否
平均等待 从触发到人处理 流程设计问题,不是模型问题

采纳率长期偏低说明"确认"变成了形式:人开始无脑点通过,这时候的确认环节既不安全也不高效,应该改规则而不是加提示词。


9. A/B 与灰度

做法 变量控制 适用
同任务双版本并行 只换模型或只换提示词 评测阶段
按流量比例灰度 只换一个版本参数 上线阶段
影子流量 新版本只跑不写 有副作用的任务
def run_ab(case, variant):
    cfg = {"A": {"model": MODEL_A, "prompt": PROMPT_V1},
           "B": {"model": MODEL_A, "prompt": PROMPT_V2}}[variant]   # 只变一个
    trace = run_agent(case["input"], **cfg)
    return {"variant": variant, "trace": trace,
            "pass": not assert_trajectory(trace)}

一次只改一个变量。 同时换模型和提示词,出问题后无法归因——这是 A/B 最常见的浪费。

有副作用的任务走影子流量:新版本照常推理、照常调工具,但工具层进入 dry-run,只记录不执行。


10. 排错:五个高频坑

现象 根因 处理
评测通过率很高,线上仍然出错 只看最终答案,不看轨迹 加 L2/L3 断言,覆盖工具调用
每次修完都复发 评测集不含工具失败路径 把线上失败轨迹固化进回归集
复盘时说不清"它为什么这么调" 日志没有工具入参 记录 tool.args 全文
一次成功就宣布能力达标 单次结果当稳定能力 固定种子/温度,跑 N 次看分布
改完提示词"感觉好多了" 没有对照基线 固定回归集 + 同版本对比

第二条是整个评测体系的分水岭:评测集的价值等于它覆盖的失败模式数量,而不是用例条数。


11. 边界:这些别用评测框架解决

问题 该用什么 理由
工具本身有 bug 单元测试 + 契约测试 agent 评测会把它算成模型错
权限配置错 集成测试 属于系统配置
延迟高 链路追踪 + 性能剖析 评测只看分布,不定位
输出格式不稳 结构化输出约束 + schema 校验 采样评测治不了
提示词风格不满意 人工评审 主观项不适合自动断言

12. 自测

  1. 为什么"跑十次八次对"这句话没有信息量?缺了哪两个维度?
  2. L1 到 L4 四层,哪一层最容易被跳过,后果是什么?
  3. 轨迹里为什么必须记录 tool.args 全文?
  4. 漏调与误调的上升,分别指向什么根因?
  5. 回归集的第一优先级来源是什么?为什么不能靠人工编造?
  6. A/B 实验里同时改模型和提示词,会让你失去什么?
S21-X15 · Agent 的评测与可观测
https://www.clxhxhhr.top/posts/5919/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。