面向多轮 Agent 工作流的评估体系设计:任务成功率、工具调用质量与失败归因的可复现实践

多轮 Agent 一旦进了业务,评估就不能只看一句“答得像不像”。真实系统里,模型要拆任务、选工具、传参数、处理异常、回收中间结果,最后才到最终回复。只看最终答案,很多问题会被埋住。

我在最近一个内部项目里,给客服辅助 Agent 和工单处理 Agent 共用了一套评估框架。结果很直接:同样是 78% 的最终任务成功率,两套策略的失败原因完全不是一回事。一套主要死在工具参数错误,另一套主要死在中间决策漂移。短句说完:只看终局,容易误判。

这篇文章把这套方案拆开讲,重点放在可复现、实测结果、工程细节,包含:评估指标怎么定义、数据集怎么组织、回放器怎么写、失败归因怎么落地,以及一次真实对比实验的结果。


1. 先说问题:为什么 Agent 评估比普通问答难

普通问答任务,常见做法是拿标准答案算准确率,或者做语义相似度评分。Agent 不一样。它的输出不只是文本,还包括一串动作轨迹:

  • 是否做了正确的任务拆解
  • 是否调用了正确工具
  • 工具参数是否完整且合法
  • 工具返回异常时有没有恢复
  • 最终结果是否满足业务约束

这里最麻烦的地方,不在模型“会不会答”,而在“每一步是不是走对了”。有时最终答案看着没问题,但路径走得很危险;有时中间有瑕疵,最后却误打误撞得到正确结果。说实话,这类样本我一开始低估了,回放 200 条之后才发现占比不小。

所以我现在会把 Agent 评估拆成两层:

结果层评估:任务有没有完成。

过程层评估:动作轨迹是否可靠。

这两层必须分开记。


2. 一个可落地的评估框架

我实际采用的是四段式结构:

  1. 任务集定义:每条样本包含输入、上下文、期望结果、允许工具集合、关键约束。
  2. 轨迹采集:记录每轮思考结果、工具调用、参数、返回值、耗时、异常。
  3. 指标计算:任务成功率、工具调用质量、轨迹偏移率、恢复能力等。
  4. 失败归因:把失败归到固定类型,便于版本对比。

结构不复杂,但执行时细节很多。尤其是采集层,如果日志字段没收全,后面基本没法分析。

2.1 样本数据结构

先定义一条评测样本。建议不要只放 questionanswer,至少补齐任务约束和工具白名单。

{
  "case_id": "ticket_00128",
  "user_input": "帮我查询订单A123的物流,如果延迟超过3天就帮我创建催单工单,并把结果总结给我",
  "context": {
    "user_id": "u_9001",
    "channel": "app",
    "current_time": "2026-04-10T10:00:00+08:00"
  },
  "expected": {
    "final_status": "success",
    "must_use_tools": ["query_order_logistics"],
    "optional_tools": ["create_ticket"],
    "constraints": {
      "delay_threshold_days": 3,
      "ticket_required_if_delayed": true
    },
    "expected_slots": {
      "order_id": "A123"
    }
  },
  "golden_path": [
    {
      "tool": "query_order_logistics",
      "args": {"order_id": "A123"}
    },
    {
      "tool": "create_ticket",
      "args": {"type": "urge_delivery", "order_id": "A123"},
      "condition": "if delayed > 3 days"
    }
  ]
}

这里的 golden_path 不要求完全唯一,它更像参考轨迹。Agent 允许存在多条合理路径,只要满足约束即可。

2.2 运行日志结构

评估的核心,不是 prompt,而是日志。日志不全,很多结论都站不住。

我通常记录下面这些字段:

{
  "case_id": "ticket_00128",
  "run_id": "run_20260410_0001",
  "agent_version": "agent_v3.2.1",
  "model": "gpt-4.1",
  "steps": [
    {
      "step_id": 1,
      "type": "tool_call",
      "tool_name": "query_order_logistics",
      "arguments": {"order_id": "A123"},
      "tool_latency_ms": 182,
      "tool_result": {"delay_days": 4, "status": "delayed"},
      "status": "success"
    },
    {
      "step_id": 2,
      "type": "tool_call",
      "tool_name": "create_ticket",
      "arguments": {"type": "urge_delivery", "order_id": "A123"},
      "tool_latency_ms": 205,
      "tool_result": {"ticket_id": "T9008"},
      "status": "success"
    },
    {
      "step_id": 3,
      "type": "final_answer",
      "content": "订单A123物流已延迟4天,我已为你创建催单工单T9008。"
    }
  ],
  "final_result": "success",
  "total_latency_ms": 2410,
  "total_tokens": 1832
}

如果有条件,我建议把“模型原始工具调用意图”和“工具执行后的标准化结果”分开存。这样后面分析参数修正器、工具网关、重试器时会省很多事。


3. 指标设计:别只报一个成功率

我现在做 Agent 评估,一般会固定输出一张指标表,最少包含下面几类。

3.1 任务成功率

最基础的指标,但定义要写清楚。

严格成功率

满足业务目标,且没有违反强约束。

例如:用户要求“延迟超 3 天则创建工单”,如果 Agent 查到了延迟 4 天,却没创建工单,即使最后总结写得很像,也算失败。

宽松成功率

最终结果对用户可接受,但轨迹不完全符合参考路径。

这个指标适合看系统上限。严格成功率更适合上线门槛。

代码示例:

def calc_task_success(run, expected):
    constraints = expected.get("constraints", {})
    must_use_tools = set(expected.get("must_use_tools", []))
    used_tools = {
        step["tool_name"]
        for step in run["steps"]
        if step.get("type") == "tool_call" and step.get("status") == "success"
    }

    if not must_use_tools.issubset(used_tools):
        return False

    if constraints.get("ticket_required_if_delayed"):
        delayed_days = None
        created_ticket = False
        for step in run["steps"]:
            if step.get("tool_name") == "query_order_logistics":
                delayed_days = step.get("tool_result", {}).get("delay_days")
            if step.get("tool_name") == "create_ticket" and step.get("status") == "success":
                created_ticket = True

        if delayed_days is not None and delayed_days > constraints["delay_threshold_days"] and not created_ticket:
            return False

    return run.get("final_result") == "success"

这个函数不长,但足够表达一个原则:Agent 成功率必须和业务规则绑定。不能只看模型自己说“我完成了”。

3.2 工具调用质量

这是我在多轮工作流里最常看的部分。因为大量失败不是出在“不会选工具”,而是出在“参数看着差不多,实际不能用”。

我会把工具调用质量拆成以下几个分数:

  • 工具选择准确率
  • 参数完整率
  • 参数合法率
  • 工具调用成功率
  • 无效调用率
  • 重复调用率

注意一下。无效调用率和重复调用率,经常比成功率更能反映 Agent 是否稳定。

参数合法率示例:

TOOL_SCHEMAS = {
    "query_order_logistics": {
        "required": ["order_id"]
    },
    "create_ticket": {
        "required": ["type", "order_id"],
        "enum": {
            "type": ["urge_delivery", "refund", "complaint"]
        }
    }
}


def score_tool_arguments(tool_name, args):
    schema = TOOL_SCHEMAS.get(tool_name, {})
    required = schema.get("required", [])
    enum_fields = schema.get("enum", {})

    missing = [k for k in required if k not in args or args[k] in [None, ""]]
    invalid = []

    for field, candidates in enum_fields.items():
        if field in args and args[field] not in candidates:
            invalid.append(field)

    return {
        "is_complete": len(missing) == 0,
        "is_valid": len(missing) == 0 and len(invalid) == 0,
        "missing_fields": missing,
        "invalid_fields": invalid
    }

上线前,我会单独拉一张“缺参 Top10”报表。很有用。通常看两轮数据,就能知道 prompt 该改,还是 tool schema 该补默认值。

3.3 过程一致性指标

多轮 Agent 有一个常见问题:前面判断对了,后面又绕回去了。

比如:

  • 第 2 步已经查到用户身份
  • 第 4 步又重复校验一次
  • 第 5 步忘了之前查到的限制条件
  • 第 6 步调用了冲突工具

这种问题最终会体现在耗时、费用、失败率上。我一般用下面两个指标抓它:

轨迹偏移率

对比参考轨迹,统计是否出现不该有的动作,或漏掉关键动作。

状态一致性得分

检查上下文变量在多步中是否一致,例如 order_iduser_id、工单类型、时间条件。

示例:

def calc_state_consistency(run):
    tracked_fields = ["order_id", "user_id", "type"]
    values = {field: set() for field in tracked_fields}

    for step in run["steps"]:
        args = step.get("arguments", {})
        for field in tracked_fields:
            if field in args and args[field] not in [None, ""]:
                values[field].add(str(args[field]))

    inconsistent = {
        field: list(v)
        for field, v in values.items()
        if len(v) > 1
    }

    return {
        "score": 1.0 - len(inconsistent) / max(len(tracked_fields), 1),
        "inconsistent_fields": inconsistent
    }

这个指标很朴素,但实测很有价值。尤其在带记忆的 Agent 里,字段漂移问题很常见。

3.4 恢复能力

真实环境里,工具超时、接口 500、字段缺失都很常见。Agent 遇到异常后怎么处理,是不是能补问用户、是不是会自动重试,这部分建议单独测。

可记录:

  • 异常后恢复成功率
  • 平均恢复步数
  • 错误升级率
  • 误恢复率

所谓误恢复率,就是本来应该停下来问用户,结果它自己编了个参数继续跑。这类问题风险很高。


4. 失败归因:没有分类,评估就只是报数

很多团队做完评测,会得出一句话:A 版本 74%,B 版本 79%。然后呢?不知道该改哪里。

我建议固定一套失败类型,所有版本都按同一口径打标签。别太多。太多就没人用了。我常用的是下面这套:

失败类型定义例子
planning_error任务拆解错误该先查订单却先创工单
tool_selection_error工具选错查物流用了订单详情工具
argument_error参数缺失或非法order_id 漏传
state_drift中间状态漂移前后两次使用不同订单号
tool_execution_error工具执行失败未恢复接口超时后直接结束
response_error最终回复不满足要求没有告知工单编号
policy_violation违反业务约束未授权情况下发起退款

这套分类里,我个人最看重 argument_errorstate_drift。因为这两类最容易被表面成功率掩盖。

归因逻辑可以半自动化:先走规则,再让 LLM 辅助补标签,但最终落盘要是固定枚举值。

示例:

def classify_failure(run, expected):
    used_tools = [
        step for step in run["steps"]
        if step.get("type") == "tool_call"
    ]

    for step in used_tools:
        score = score_tool_arguments(step["tool_name"], step.get("arguments", {}))
        if not score["is_valid"]:
            return "argument_error"

    consistency = calc_state_consistency(run)
    if consistency["inconsistent_fields"]:
        return "state_drift"

    failed_tools = [s for s in used_tools if s.get("status") == "failed"]
    if failed_tools:
        return "tool_execution_error"

    required_tools = set(expected.get("must_use_tools", []))
    actual_tools = {s["tool_name"] for s in used_tools}
    if not required_tools.issubset(actual_tools):
        return "tool_selection_error"

    return "response_error"

这不是完美归因,但够稳定。评估框架先追求稳定,再慢慢加细。


5. 离线评测集怎么构造,结果才有参考价值

评测集如果都是“标准输入 + 标准路径”,最后往往会得到一个偏乐观的结论。生产流量不是这样的。

我会把数据分成几类,按比例混合:

  • 正常任务样本:流程清楚,参数完整
  • 缺信息样本:缺订单号、缺时间范围、缺用户身份
  • 干扰样本:一句话里夹多个任务,或含无关信息
  • 异常样本:工具超时、返回空值、返回脏字段
  • 长程样本:8 步以上,有跨轮状态传递

每类样本都要有明确占比。比如我最近一套 600 条评测集,分布大致是:正常任务 260 条,缺信息 110 条,干扰输入 90 条,异常注入 80 条,长程任务 60 条。

数字不大,但够用了。关键是每次版本对比都用同一套集,或者固定 train/dev/test 切分。

5.1 异常注入很有必要

这一点很多人会省掉,但我建议别省。

如果工具层永远返回正确值,你测不出 Agent 的恢复能力。我通常会在回放器里注入几类故障:

  • 超时
  • 5xx 错误
  • 空字段
  • 枚举值越界
  • 数据冲突

示例:

import random


def maybe_inject_fault(tool_result, fault_rate=0.1):
    if random.random() > fault_rate:
        return {"status": "success", "result": tool_result}

    fault_type = random.choice(["timeout", "empty_field", "server_error"])

    if fault_type == "timeout":
        return {"status": "failed", "error": "timeout"}
    if fault_type == "empty_field":
        corrupted = dict(tool_result)
        if corrupted:
            first_key = list(corrupted.keys())[0]
            corrupted[first_key] = None
        return {"status": "success", "result": corrupted}
    return {"status": "failed", "error": "server_error"}

有了这类数据,你才能看到 Agent 是“真恢复”还是“瞎继续”。


6. 一套最小可用评测流水线

下面给一个简化版工程结构,我自己也差不多按这个拆:

agent_eval/
├── datasets/
│   ├── cases_dev.jsonl
│   └── cases_test.jsonl
├── runners/
│   ├── agent_runner.py
│   └── replay_runner.py
├── evaluators/
│   ├── task_metrics.py
│   ├── tool_metrics.py
│   └── failure_classifier.py
├── reports/
│   └── generate_report.py
└── configs/
    └── eval_config.yaml

6.1 评测主程序

import json
from collections import Counter


def load_jsonl(path):
    with open(path, "r", encoding="utf-8") as f:
        return [json.loads(line) for line in f]


def evaluate(cases, runner):
    results = []
    failure_counter = Counter()

    for case in cases:
        run = runner(case)
        success = calc_task_success(run, case["expected"])
        consistency = calc_state_consistency(run)

        if not success:
            failure_type = classify_failure(run, case["expected"])
            failure_counter[failure_type] += 1
        else:
            failure_type = None

        results.append({
            "case_id": case["case_id"],
            "success": success,
            "latency_ms": run.get("total_latency_ms", 0),
            "tokens": run.get("total_tokens", 0),
            "consistency_score": consistency["score"],
            "failure_type": failure_type
        })

    success_rate = sum(r["success"] for r in results) / len(results)
    avg_latency = sum(r["latency_ms"] for r in results) / len(results)
    avg_tokens = sum(r["tokens"] for r in results) / len(results)
    avg_consistency = sum(r["consistency_score"] for r in results) / len(results)

    return {
        "summary": {
            "success_rate": round(success_rate, 4),
            "avg_latency_ms": round(avg_latency, 2),
            "avg_tokens": round(avg_tokens, 2),
            "avg_consistency": round(avg_consistency, 4)
        },
        "failure_breakdown": dict(failure_counter),
        "details": results
    }

6.2 输出报表

我一般会输出两份:

  • 面向研发的明细 CSV
  • 面向版本比较的聚合报表 JSON

聚合报表可以长这样:

{
  "agent_version": "v3.2.1",
  "dataset": "cases_test_v5",
  "summary": {
    "success_rate": 0.7817,
    "avg_latency_ms": 2432.5,
    "avg_tokens": 1910.4,
    "avg_consistency": 0.9133
  },
  "failure_breakdown": {
    "argument_error": 41,
    "state_drift": 27,
    "tool_execution_error": 18,
    "response_error": 13,
    "tool_selection_error": 9
  }
}

这个格式很适合接到 dashboard 或 CI。


7. 实测对比:只改 Prompt 不够,工具层约束也得上

这里给一组我做过的缩略版实验数据,场景是工单处理 Agent,评测集 600 条,多轮任务平均 5.8 步。

对比了两个版本:

  • V1:纯 Prompt 约束,工具参数只给文字描述
  • V2:Prompt 保持接近,增加 JSON Schema 校验、参数纠错器、失败重试规则

结果如下:

指标V1V2
严格任务成功率72.3%81.5%
宽松任务成功率79.8%85.7%
工具选择准确率91.2%92.6%
参数合法率76.4%89.1%
状态一致性得分0.880.93
平均耗时2210ms2485ms
平均 token16841819

我当时的第一反应很直接:没想到提升最大的不是“选工具”,而是“参数合法率”。

这很说明问题。很多时候 Agent 看起来像在“思考错误”,实际是工程层没把参数约束收紧。V2 的代价也很清楚,耗时多了 275ms,token 多了 135。这个代价我能接受,因为成功率提升了 9.2 个点。

再看失败归因分布:

失败类型V1V2
argument_error5823
state_drift3119
tool_execution_error1617
response_error2115
tool_selection_error1914

可以看到,V2 把 argument_error 压得很明显,但 tool_execution_error 没降。这说明下一步该查工具网关和重试策略,而不是继续死抠 prompt。

这种结论,只有做了失败归因才看得出来。


8. 把评估接进 CI,版本回归才有意义

如果评测只在出问题时手动跑一次,后面很容易失效。我比较推荐接到 CI 或定时任务里。

一个简单做法:

  • 每次 Agent Prompt、Planner、Tool Router 变更后自动跑 dev 集
  • 每天凌晨固定跑 test 集
  • 成功率下降超过阈值时拦截发布
  • 某类失败激增时发告警

阈值别设太死。我的经验是:

  • 严格成功率下降超过 1.5 个点,阻断
  • policy_violationargument_error 上升超过 20%,阻断
  • 平均耗时上升 15% 以上,提示人工复核

GitHub Actions 示例:

name: agent-eval
on: [push]

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - name: Install deps
        run: pip install -r requirements.txt
      - name: Run eval
        run: python runners/run_eval.py --config configs/eval_config.yaml
      - name: Check regression
        run: python reports/check_regression.py --report reports/latest.json --baseline reports/baseline.json

这部分工程量不大,但收益稳定。回归问题会早很多暴露。


9. 一个常见误区:让 LLM 当总裁判

很多人会直接让另一个 LLM 给 Agent 打分,问它“这次任务有没有完成”。这不是不能用,但别全交给它。

我自己的做法是:

  • 规则优先:工具是否调用、参数是否合法、约束是否满足,这些用代码判
  • LLM 辅助:最终回复是否覆盖了关键信息、失败描述是否合理,这类可用模型辅助

原因很简单。规则类问题交给模型,结果会飘,而且不同模型版本之间评分口径不稳定。评估系统本身也要可复现。

短句提醒:裁判也得可测。


10. 我现在推荐的最小实践

如果你正准备给多轮 Agent 上评测,我建议先别追求大而全,先做一个能稳定跑起来的版本:

  1. 定义 200 到 500 条有代表性的任务样本
  2. 固定运行日志结构,把每一步工具调用记录全
  3. 先算四个核心指标:严格成功率、参数合法率、状态一致性、失败归因分布
  4. 给工具层补 schema 校验和异常注入
  5. 接入 CI 做版本回归

这套东西跑通之后,再去加 LLM-as-a-judge、轨迹相似度、分场景报表,会轻松很多。

唯一的小缺点是前期样本标注比较费时间。我这边第一次整理 600 条数据,两个工程师花了接近 3 天。但后面版本对比就省事了,基本都是增量维护。


11. 结尾

多轮 Agent 的评估,核心不是“分数算得多花”,而是你能不能把失败定位到可修改的工程点。任务成功率回答的是“有没有做成”,工具调用质量回答的是“哪里容易出错”,失败归因回答的是“下一步该改什么”。

这三块一旦分开记录,版本演进会清楚很多。尤其在业务环境里,很多看上去像模型能力问题的 case,最后查出来其实是工具定义、参数校验、异常恢复没处理好。

如果你也在做 Agent 工作流评测,可以把你现在用的指标贴出来,我可以下一篇继续写一版:如何给 Agent 设计分层 benchmark,区分 Planner、Tool Router 和 Memory 模块的贡献

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐