演示能过,上线就废:67 页讲透 Agent 评测
第 5 章 设计测试集
智能体能跑了,接下来要回答一个问题:它到底靠不靠谱?
靠不靠谱不能凭感觉。你需要一批固定的考卷,每次改完代码都用同一套卷子考一次,分数才能比较。这一章就做这张卷子。
▏ 5.1 为什么必须"先写用例"
假设你手工试三个场景:正常退款、超期退款、假用户。都通过了,于是放心上线。问题有三个:
- 覆盖不足
:两件商品只有一件能退、只查订单不退款、用户没说用户名……这些没测到。 - 不可重复
:下次改完提示词,你还会记得把这几个场景全试一遍吗? - 无法比较
:这次“感觉还行”,下次“感觉也还行”,谁更好?
数据集解决的正是这三件事。它把“我觉得”换成“这 8 个场景、7 个指标、通过率多少”。
● 数据集是资产,不是一次性脚本
它会长久地留在仓库里,每改一次智能体就跑一次。写得好的数据集,其价值甚至超过智能体本身——因为它是“什么叫对”的唯一定义。
▏ 5.2 四个核心概念
DeepEval 把评估拆成四个对象,理解它们的关系,后面所有代码都顺了。
图 5-1 四个核心对象的关系:从期望到实测,再到打分与报告
|
|
|
|
|---|---|---|
| Golden |
|
|
| LLMTestCase |
|
|
| EvaluationDataset |
|
|
| CallbackHandler |
|
|
一句话记住区别:Golden 是“应该怎样”,LLMTestCase 是“实际怎样”。 评估就是把两者放在一起比。
Golden 的字段不少,但不是每个都要填。原则是:你想测什么,就填什么。
▏ 5.3 怎么设计场景
最实用的方法,是拿三个问题去问业务方:
- 你最怕什么?
(怕被骗、怕多退钱、怕退错人) - 你每天会遇到什么?
(正常退款、查订单) - 你不常遇到、但一定会遇到什么?
(买了很多年才来退、用户名打错、退一件没买过的商品)
把答案整理成场景,再补充两类特殊用例:边界(恰好 30 天)和异常(空输入、乱码、诱导性提问)。
▏ 5.4 八个场景
下表就是我们这张考卷。注意每一行都能对应到第 2 章的某条规则——没有一条用例是凭空想出来的。
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
其中第 6 和第 8 最关键。
- 第 6 个
是“部分可退”场景,也是现实中比例很高的场景。智能体必须能拆开判断:一件能退、一件不能,而且要给客户解释清楚。 - 第 8 个
看起来最简单,却经常出问题:模型会在不知道用户是谁的情况下热情地“帮您查一下”,然后调用工具、返回错误信息。能否先停下来问一句,是判断它是否守规矩的关键。
▏ 5.5 写测试集代码
● evaluations/dataset.py Python
from deepeval.dataset import EvaluationDataset, Golden
from deepeval.test_case import ToolCall
dataset = EvaluationDataset(
goldens=[
# 1 正常退款:退全部两件
Golden(
input="我是 user123,我想退书和笔",
expected_output="已成功生成 30 元的退款工单",
expected_tools=[
ToolCall(name="get_user_purchase_history",
input_parameters={"user": "user123"}),
ToolCall(name="get_today_date", input_parameters={}),
ToolCall(name="calculate_right_refund",
input_parameters={"user": "user123",
"items": [{"name": "book"}, {"name": "pen"}]}),
ToolCall(name="issue_refund_ticket",
input_parameters={"user": "user123", "refund_amount": 30}),
],
),
# 2 只退指定商品
Golden(
input="我是 user123,我只想退书",
expected_output="已成功生成 23 元的退款工单",
expected_tools=[
ToolCall(name="get_user_purchase_history",
input_parameters={"user": "user123"}),
ToolCall(name="get_today_date", input_parameters={}),
ToolCall(name="calculate_right_refund",
input_parameters={"user": "user123",
"items": [{"name": "book"}]}),
ToolCall(name="issue_refund_ticket",
input_parameters={"user": "user123", "refund_amount": 23}),
],
),
# 3 纯咨询:不能开单
Golden(
input="我是 user123,我不需要退款,我只想知道我买过什么",
expected_output="没有生成任何退款工单,只列出了购买记录",
expected_tools=[
ToolCall(name="get_user_purchase_history",
input_parameters={"user": "user123"}),
],
),
# 4 不存在的用户
Golden(
input="我是 user999,我想退书",
expected_output="告知用户名无效,请用户重新提供有效用户名,不透露任何订单信息",
expected_tools=[
ToolCall(name="get_user_purchase_history",
input_parameters={"user": "user999"}),
],
),
# 5 超过 30 天
Golden(
input="我是 user126,我想退 water_bottle",
expected_output="不予退款,因为超过 30 天退款期限",
expected_tools=[
ToolCall(name="get_user_purchase_history",
input_parameters={"user": "user126"}),
ToolCall(name="get_today_date", input_parameters={}),
],
),
# 6 一件可退、一件不可退
Golden(
input="我是 user126,我想退 backpack 和 water_bottle",
expected_output=("已成功为 backpack 生成 45 元退款工单;"
"water_bottle 因超过 30 天不可退款"),
expected_tools=[
ToolCall(name="get_user_purchase_history",
input_parameters={"user": "user126"}),
ToolCall(name="get_today_date", input_parameters={}),
ToolCall(name="calculate_right_refund",
input_parameters={"user": "user126",
"items": [{"name": "backpack"}]}),
ToolCall(name="issue_refund_ticket",
input_parameters={"user": "user126", "refund_amount": 45}),
],
),
# 7 从未购买的商品
Golden(
input="我是 user126,我想退 sneakers",
expected_output="不予退款,因为您没有购买过该商品",
expected_tools=[
ToolCall(name="get_user_purchase_history",
input_parameters={"user": "user126"}),
],
),
# 8 缺少用户名
Golden(
input="我想申请退款",
expected_output="询问用户名,在拿到用户名之前不查询任何信息、不生成工单",
expected_tools=[],
),
]
)
三个写法要点:
expected_output只写结论,不写完整句子。因为答案比对只看“逻辑结论是否一致”,措辞不必相同。写成一句话的精炼结论,反而更稳定。 expected_tools连参数一起写。只写工具名的话,“参数传错”这类问题就永远测不出来(第 8 章会看到这个差别)。 - 不该调用的场景,
expected_tools写空列表。第 8 个用例就是靠这个抓“过度调用”的。
▏ 5.6 三个容易埋雷的地方
5.6.1 日期基准必须固定
这是最容易翻车的地方。数据里的购买日期是写死的,而 get_today_date() 读的是你机器今天的真实日期,期望答案又是按某个固定“今天”写的。三者一旦错位,就会出现荒唐结果:按今天算其实该退款,但期望答案要求拒绝。
三种解法,任选其一,但一定要选:
● Python
# 方案 A:把"今天"钉死(推荐,最可复现)
from datetime import date
FIXED_TODAY = date(2026, 10, 16)
@tool
def get_today_date() -> str:
"""返回今天的日期,格式 YYYY-MM-DD。"""
return FIXED_TODAY.isoformat()
● Python
# 方案 B:让数据永远"新鲜"(把日期改成相对今天)
from datetime import date, timedelta
"book": {"price": 23, "date": (date.today() - timedelta(days=14)).isoformat()}
● 先对齐日期,再看分数
如果购买日期、当天日期、期望答案三者不一致,评估结果没有任何意义。每次运行前打印一行基准日期,是最省事的保险。
5.6.2 期望答案要"可判"
对比这两条期望:
-
差: 客服回复了客户,态度友好,并说明了退款政策 -
好: 已成功生成 30 元的退款工单
前者没法自动判定(“友好”怎么量化?),后者一眼可比。期望答案的写法,直接决定了评估能不能自动化。
5.6.3 单轮用例测不出多轮能力
上面八个用例都是“一句话进、一句话出”。但真实客服是多轮的:用户先给错用户名,被追问,再改正,最后退款。
评估框架支持多轮参数,但建议先把单轮测扎实,再上多轮。基础不牢的时候上多轮,只会让失败原因更难定位。
▏ 5.7 本章小结
-
数据集把“我觉得”变成“这 8 个场景、通过率多少”。 -
Golden 是期望,LLMTestCase 是实测,评估就是两者对比。 -
场景设计问业务方三个问题,再补边界与异常。 -
日期基准、期望答案写法、单轮限制,是最常见的三个坑。
期待您加入大模型应用测试课程
|
|

