Python 与 AI 开发教程

评测指标

改了一句提示词,Agent 是变好了还是变差了?靠试几个例子看不出来。评测把这个问题变成一组能比较的数字。

  • 评测与安全 · 第 1 章
  • 8 分钟
  • 1 组对比
  • 基于 Python 3.12 · Pydantic 2.14 · JUnit 6

为什么要评测

传统代码的行为是确定的,改完跑测试就知道对不对。Agent 不一样:

  • 同样的输入,每次的结果可能不同
  • 提示词改一句,可能修好一个问题,又弄坏另外三个
  • 换模型或升级版本,行为会在你看不见的地方发生变化
  • 结果的好坏经常没有唯一标准

不做评测的话,每次改动都只能试几个例子,凭感觉判断。评测就是一批固定的任务加一套打分方法,改动前后各跑一遍,看数字的变化。

评什么

Agent 要从三个层面看,越往后越接近用户的感受:

  • 最终结果:任务完成了吗?回答对吗?
  • 过程:工具选对了吗?步骤是否多余?
  • 代价:花了多少 token、多长时间?

看结果时,判断最终状态,不要过分要求具体路径。模型换一种方式完成了任务,一般也算对。退款 Agent 的评测应该检查订单是否真的退款了,而不是它调用工具的顺序。

准备数据集

一条用例包括输入和判断标准。开始时不用很多,20 到 50 条来自真实场景的任务就够有用,之后不断增加。

用例要覆盖这几类:

  • 常见任务:用户最常做的事
  • 边界情况:信息不全、输入含糊
  • 负面用例:不该做的事,比如模型不应该调用退款工具的情况
  • 踩过的坑:线上出过问题的例子
Python
from pydantic import BaseModel, Field

class Case(BaseModel):
    id: str
    task: str
    expected_tools: list[str] = Field(default_factory=list)
    forbidden_tools: list[str] = Field(default_factory=list)
    answer_contains: list[str] = Field(default_factory=list)

CASES = [
    Case(id="weather", task="北京天气", expected_tools=["get_weather"], answer_contains=["晴"]),
    Case(id="chat", task="你好", forbidden_tools=["get_weather"]),
]

CASES[0].expected_tools  # => ['get_weather']

运行和打分

评测程序要记录 Agent 的每一步,再由打分函数判断。这里先用代码打分,能用代码判断的就不要用模型,这样又快又便宜,结果还是确定的。

给一个用例打分

Python
from dataclasses import dataclass

@dataclass
class Trace:
    answer: str
    tools: list[str]
    tokens: int
    seconds: float

def grade(case: dict, trace: Trace) -> dict[str, bool]:
    return {
        "tools_ok": set(case["expected"]) <= set(trace.tools),
        "safe": not set(case["forbidden"]) & set(trace.tools),
        "answer_ok": all(text in trace.answer for text in case["contains"]),
    }

case = {"expected": ["get_weather"], "forbidden": [], "contains": ["晴"]}
trace = Trace("北京今天晴", ["get_weather"], 320, 1.2)

grade(case, trace)  # => {'tools_ok': True, 'safe': True, 'answer_ok': True}
record Trace(String answer, List<String> tools) {}

record Case(List<String> expected, List<String> forbidden,
            List<String> contains) {}

Map<String, Boolean> grade(Case c, Trace t) {
    return new TreeMap<>(Map.of(
        "tools_ok", t.tools().containsAll(c.expected()),
        "safe", Collections.disjoint(t.tools(), c.forbidden()),
        "answer_ok", c.contains().stream().allMatch(t.answer()::contains)
    ));
}

var c = new Case(List.of("get_weather"), List.of(), List.of("晴"));
var t = new Trace("北京今天晴", List.of("get_weather"));
System.out.println(grade(c, t));  // => {answer_ok=true, safe=true, tools_ok=true}
对比项PythonJava
记录数据dataclassrecord
子集判断set(a) <= set(b)containsAll
交集为空not a & bCollections.disjoint
全部满足all()allMatch

打分函数只返回结果,不做断言。评测要的是汇总之后的比例,一条失败不应该中断整批运行。

常用指标

把所有用例的结果汇总成几个数字:

延迟要看 P95,不要只看平均值。平均值会被大量快速请求掩盖,少数用户等待几分钟的情况看不出来。

Python
import statistics

results = [
    {"success": True, "tokens": 800, "seconds": 2.1},
    {"success": True, "tokens": 1200, "seconds": 3.4},
    {"success": False, "tokens": 5000, "seconds": 18.0},
    {"success": True, "tokens": 900, "seconds": 2.5},
]

def summarize(results: list[dict]) -> dict:
    seconds = [r["seconds"] for r in results]
    return {
        "success_rate": sum(r["success"] for r in results) / len(results),
        "avg_tokens": statistics.mean(r["tokens"] for r in results),
        "p95_seconds": round(statistics.quantiles(seconds, n=20, method="inclusive")[-1], 1),
    }

summarize(results)  # => {'success_rate': 0.75, 'avg_tokens': 1975, 'p95_seconds': 15.8}

用例少时,几个点的变化可能只是随机波动。50 条用例里多对一条就是 2%,不要对这样的差距太敏感。

线上指标

离线评测只覆盖你想到的情况。上线后还要看这些数字,它们能发现数据集里没有的问题:

  • 用户点赞和点踩的比例
  • 用户重新提问或中途放弃的比例
  • 转人工的比例
  • 工具报错率,每个任务的费用

把线上的失败案例加进数据集,评测就会越来越贴近真实使用。下一章讲结果没法用代码判断时怎么办。

评测概念对照
PythonJava说明
评测程序JUnit 测试评测输出比例,不是通过或失败
@dataclass Tracerecord记录一次运行
statistics.quantilesMicrometer 的百分位计算 P95
基线分数性能基线和上一次比较

本章要点

  • Agent 的行为不确定,改动的影响要靠评测判断,不能靠感觉
  • 评测的结果是比例,通过标准是不低于基线
  • 从结果、过程和代价三个层面看,优先检查最终状态
  • 数据集从 20 到 50 条真实任务开始,要包含不该做的事
  • 能用代码打分的就用代码,打分函数只返回结果,不做断言
  • 看成功率、token 和 P95 延迟,并把线上失败加回数据集

参考资料:揭开 Agent 评测的面纱构建有效的 AgentOpenAI 评测最佳实践