Agent 看起来能跑,怎么判断它真的做对了?从测试用例到回归评测
做一个 Agent 演示不难:用户输入问题,模型调用工具,最后给出一段回答。难的是把它放进真实业务后,你怎么知道它没有查错数据、跳过授权、工具报错后编造结果,或者把一个操作执行两遍?
这篇文章以一个订单查询 Agent为例,拆开测试用例、工具调用轨迹、错误处理、副作用和回归评测,并提供一份可以直接运行的 Python 离线教学代码。示例里的 Agent、订单和工具均为确定性的模拟数据,不调用真实模型或业务接口,因此只能验证演示中的流程,不能代表真实模型的准确率。

一、回答看起来合理,不代表任务完成
用户问:“帮我看看订单 A101 到哪里了。”Agent 回答:“您的订单正在运输中。”表面上很顺畅,但一个开发者还应该追问:
- 查询的是否真的是 A101,而不是另一个订单?
- 当前用户是否有权访问这个订单?
- 状态是从工具结果得到的,还是查询失败后凭空生成的?
- 用户没有提供订单号时,Agent 是否会追问?
- 如果任务包含创建工单,网络重试后会不会创建两份?
一个答案可能字面正确,却走了错误的路径;也可能调用路径看似正常,最后输出却与工具结果矛盾。评测需要把这些问题拆开,分别留下证据。
| 观察维度 | 应该检查什么 | 常见失败 |
|---|---|---|
| 最终结果 | 回答是否满足任务要求、是否有依据 | 工具失败后仍说“已签收” |
| 工具选择 | 是否调用了合适的工具 | 需要查订单,却没有查询 |
| 参数和权限 | 订单号、用户身份是否正确 | 用错误订单号或访问他人订单 |
| 过程状态 | 失败、缺参、超时能否被识别 | 异常被吞掉,显示成功 |
| 业务副作用 | 写入、发消息等是否符合约定 | 同一请求创建两张工单 |
二、先定义任务边界和成功标准
评测之前,要先写清 Agent 的任务合同。对于本文的例子,我们约定:
- 用户只能查询自己有权访问的订单,权限由服务端验证,不能只靠提示词要求模型守规矩。
- 缺少订单号时先追问,不调用查询工具。
- 查询成功后,根据工具返回的状态回答。
- 服务不可用时明确告知失败,不编造订单状态。
- 用户授权创建工单后,如果同一请求被重复投递,业务侧应避免重复创建。
这些规则不一定适用于所有产品。比如另一套系统可能允许客服查询多个客户的订单,但也应由真实权限模型决定,而不是让 Agent 自行猜测。
这里的“成功”可以进一步拆成必要条件和可接受的差异:查询 A101 时,可以允许回答措辞不同;但是订单编号、状态、授权结果和是否产生额外写入,不能只因为措辞流畅就放过。
三、把要求变成测试用例
很多团队只准备几条“正常输入”,模型回答看起来不错就算通过。真正接入后,出问题的往往是缺参数、数据冲突和外部服务失败。
| 场景 | 输入或环境 | 预期结果 | 必须检查的过程 |
|---|---|---|---|
| 正常查询 | Alice 查询自己的 A101 | 回答“运输中” | 查询 A101,且无写入 |
| 缺少参数 | 没有订单号 | 询问订单号 | 不调用订单查询 |
| 越权查询 | Alice 查询 Bob 的 B202 | 拒绝或不给出订单详情 | 服务端验证身份与资源归属 |
| 服务异常 | 查询工具不可用 | 明确失败,不编造状态 | 异常被识别并返回 |
| 重复投递 | 同一授权工单请求投递两次 | 最终只产生一次创建 | 检查真实副作用,而非只数回答 |
| 故障注入 | 服务故障时错误版本声称“已签收” | 评测应报告失败 | 检查评测器能否抓住错误 |
注意这里的“越权”用例既检查 Agent 输出,也依赖服务端阻止未授权读取。若服务端本身返回了敏感数据,即使 Agent 最终没显示出来,也不能宣布整个系统安全。

四、结果评测和过程评测,分别看什么?
结果评测看最终交付:订单号是否正确、状态是否来自证据、用户能否理解失败。过程评测看完成任务时发生了什么:调用了什么工具、参数是什么、是否出现额外操作、错误有没有被处理。
有些任务有多种合法路径,不能机械地规定“必须调用工具 A,再调用工具 B”。应该只把业务必需的步骤、禁止行为和关键约束写进评测条件;对可以变化的顺序和措辞留出空间。
| 断言方式 | 示例 | 适合什么 | 局限 |
|---|---|---|---|
| 精确匹配 | 回答必须等于固定字符串 | 确定性离线示例 | 自然语言正确答案可能有不同表达 |
| 结构检查 | 返回状态、订单号、字段类型 | 结构化接口 | 结构正确仍可能事实错误 |
| 轨迹检查 | 不允许缺参时查询;检查调用参数 | 工具交互与权限流程 | 需要规范的日志和采集 |
| 业务状态检查 | 同一请求最终只创建一张工单 | 判断真实副作用 | 需要隔离环境和可靠数据源 |
| 人工或模型辅助评分 | 判断解释是否清楚、有依据 | 开放式回答 | 有主观性,需抽样校准 |
模型辅助评分也会出错。设计评测时应准备人工标注的样例、明确评分标准,定期抽查评审结果;对权限、金额和是否产生副作用等硬约束,优先使用确定性检查。
五、什么是轨迹?为什么它值得保存?
把 Agent 的一次运行理解成一条事件链:接收请求、决定调用工具、带着参数调用、得到结果、组织回答。如果失败,还要知道失败发生在哪一步。
一个最小事件记录可以包含:
任务 ID:eval-001
用户输入:查询 A101
调用:query_order(order_id=A101)
工具结果:运输中
最终回答:订单 A101 正在运输中
判定:答案正确 / 参数正确 / 无额外写入
这里记录的是系统可观察到的调用与结果,不等于模型内部思考过程。真实项目可使用 trace/span 组织跨服务事件,关联请求 ID、版本、错误类别与耗时;OpenTelemetry 对 trace 的介绍可作为参考。
| 记录阶段 | 示例记录 | 能帮助定位什么 |
|---|---|---|
| 请求 | 任务 ID、输入类别 | 对应哪一次任务 |
| 工具调用 | 工具名称、必要参数 | 是否查询了正确订单 |
| 工具返回 | 状态、错误类别 | 查询失败还是结果为空 |
| 最终结果 | 回答、业务结果引用 | 是否与已知事实一致 |
| 副作用 | 写入结果、幂等键 | 是否重复创建 |
日志应按项目的数据管理要求处理:只采集必要信息,限制访问和保留时间,避免记录密钥或不必要的个人数据。记录用于排查,不意味着可以无限保存原始输入。
六、动手搭一个离线回归评测
下面是完整教学代码。它使用 Python 标准库,用内存模拟订单服务、Agent、评估器和写入结果:不联网、不调用真实模型、不操作真实订单。faulty=True 是人为注入的错误行为,用来检查评测能否发现“工具失败后仍编造答案”。
把代码保存为 agent_eval_demo.py,然后运行 python agent_eval_demo.py:
"""离线教学:确定性模拟 Agent、工具与评估器,不调用模型、网络或数据库。"""
from dataclasses import dataclass
@dataclass
class Result:
answer: str
trace: list
writes: int = 0
class Store:
def __init__(self, down=False):
self.orders = {'A101': ('alice', '运输中'), 'B202': ('bob', '已签收')}
self.down = down
self.trace = []
self.tickets = {}
def query(self, actor, order_id):
self.trace.append(('query_order', order_id))
if self.down:
raise ConnectionError('模拟服务不可用')
order = self.orders.get(order_id)
if order is None or order[0] != actor:
raise PermissionError('订单不存在或无权访问')
return order[1]
def create_ticket(self, actor, key):
self.trace.append(('create_ticket', key))
# 这是单进程内存模拟,不代表真实数据库具备并发安全。
self.tickets.setdefault((actor, key), 'ticket-1')
return self.tickets[(actor, key)]
def agent(store, actor, order_id, *, ticket=False, faulty=False):
if not order_id:
return Result('请提供订单号', store.trace)
try:
status = store.query(actor, order_id)
except PermissionError:
return Result('订单不存在或无权访问', store.trace)
except ConnectionError:
# 故障注入:错误版本在查询失败后编造结果。
return Result('订单已签收' if faulty else '查询失败,请稍后重试', store.trace)
if ticket:
store.create_ticket(actor, 'request-123')
store.create_ticket(actor, 'request-123') # 模拟同一请求重复投递
return Result(f'订单{order_id}:{status}', store.trace, len(store.tickets))
def evaluate(name, result, expected_answer, expected_calls, expected_writes=0):
checks = {
'answer': result.answer == expected_answer,
'trace': result.trace == expected_calls,
'writes': result.writes == expected_writes,
}
print(f"{name}: {'PASS' if all(checks.values()) else 'FAIL'} {checks}")
return all(checks.values())
def run_suite(faulty=False):
outcomes = []
store = Store()
outcomes.append(evaluate('正常查询', agent(store, 'alice', 'A101'), '订单A101:运输中', [('query_order', 'A101')]))
store = Store()
outcomes.append(evaluate('缺少订单号', agent(store, 'alice', ''), '请提供订单号', []))
store = Store()
outcomes.append(evaluate('越权查询', agent(store, 'alice', 'B202'), '订单不存在或无权访问', [('query_order', 'B202')]))
store = Store(down=True)
outcomes.append(evaluate('服务不可用', agent(store, 'alice', 'A101', faulty=faulty), '查询失败,请稍后重试', [('query_order', 'A101')]))
store = Store()
outcomes.append(evaluate('重复投递', agent(store, 'alice', 'A101', ticket=True), '订单A101:运输中', [('query_order', 'A101'), ('create_ticket', 'request-123'), ('create_ticket', 'request-123')], 1))
print(f'总计: {sum(outcomes)}/{len(outcomes)}')
return outcomes
if __name__ == '__main__':
print('=== 基线版本 ===')
base = run_suite()
print('=== 故障注入版本(预期识别出失败) ===')
broken = run_suite(faulty=True)
assert all(base) and broken.count(False) == 1 and broken[3] is False
在这个代码中:
Store.query在模拟服务内检查订单归属,越权时不返回订单详情。agent是确定性流程,用来演示被测系统如何调用工具;它并不是一个真实大模型 Agent。evaluate分别检查回答、调用轨迹和内存里的写入数量。create_ticket对同一用户和请求键进行内存去重,示例故意投递两次以演示检查副作用。
基线版本应为 5/5 PASS;故障注入版本应为 4/5 PASS,其中“服务不可用”用例 FAIL 是预期结果:评估器抓住了错误版本的编造回答。总数只是这五条模拟用例的结果,不能用作产品准确率。

这个示例有哪些边界?
首先,内存去重只说明教学中的字典行为,无法证明真实数据库在并发、多进程或宕机恢复时的幂等性。真实系统需要根据业务使用持久化唯一约束、事务、幂等键生命周期和请求状态查询等机制。
这里 ticket=True 只模拟上游已经授权的测试输入,并没有实现工单写入授权;真实创建动作也必须在服务端单独鉴权。
其次,模拟 Agent 的行为固定、可预测;它不覆盖真实模型的随机性、供应商 API、网络、复杂工具调用和提示注入。它适合验证评测框架能否识别已知缺陷,接入真实 Agent 后还需要单独做受控测试。
七、回归评测是什么?
你修改了提示词、工具描述、模型版本或工作流,希望以前能做对的任务仍然能做对,就可以重新运行同一批测试用例,再比较结果。
| 变更 | 可能影响什么 | 可以重点回归 |
|---|---|---|
| 提示词 | 回答格式、追问、工具选择 | 缺参、拒绝、答案引用 |
| 模型版本 | 工具调用和结果稳定性 | 关键流程、不同输入表达 |
| 工具接口 | 字段、错误码、超时 | 参数校验、异常分支 |
| 权限规则 | 用户可访问的数据范围 | 授权、越权和租户隔离 |
| 重试与队列 | 执行次数和顺序 | 幂等、重复投递、部分成功 |
| 前端展示 | 加载和失败提示 | 成功、失败、等待状态 |
回归时不要只看总通过率。例如 95 条普通查询通过、5 条越权用例失败,单个总分可能掩盖严重问题。可以分别报告正常任务、权限、工具异常和副作用等类别,再为关键安全规则设置单独门槛。
八、真实 Agent 接进来时,要怎么扩展?
把离线评测升级成真实测试时,可以保留用例格式和断言,再把模拟 Agent 替换成真实调用的适配器。推荐逐步进行:
- 固定一批脱敏、可复现的输入,明确每条用例的预期和判定规则。
- 给工具准备隔离的测试环境,避免影响真实订单或向真实用户发送消息。
- 保存可观察轨迹,记录模型和提示词版本、工具响应、耗时与错误类型。
- 对不同结果使用合适判定方法:确定性规则、结构检查、人工抽样或辅助评分。
- 明确网络调用、配额和费用预算,必要时先用模拟工具验证接线。
- 多次运行并记录样本数,区分偶发失败与稳定退化;不要用一次结果推断整体表现。
真实模型评测可能产生费用,也可能受到模型更新、网络波动和测试数据变化影响。要比较两个版本,尽量固定用例、工具环境、评判标准和执行配置,并保留不可比较时的原因。
九、评测集怎么持续维护?
评测集不应该只由开发者临时想到的几句话组成。可以从产品需求、历史故障、人工审查和授权后的脱敏案例中整理,再按场景分类。
| 用例来源 | 可以覆盖什么 | 维护要点 |
|---|---|---|
| 产品需求 | 基本业务流程 | 要求变化时同步更新预期 |
| 历史故障 | 已经出现过的缺陷 | 修复后保留为回归用例 |
| 边界与对抗输入 | 缺参、越权、冲突指令 | 不把未授权内容当指令 |
| 工具故障 | 超时、限流、空结果 | 明确哪些错误可重试 |
| 人工审查 | 解释质量、模糊需求 | 记录评分标准和分歧 |
测试集也会过时。每个用例应带上适用业务版本、构造方式和预期来源;对缺少依据的案例标记待确认,而不是为了得到漂亮分数随意修改预期。
十、实践中常见的几个误区
**误区一:只看最终答案。**答案正确不代表没有越权或额外操作;需要结合轨迹和业务状态。
**误区二:用一个平均分覆盖所有问题。**回答不够简洁和泄露他人订单不是同一严重程度,关键约束应单独统计。
**误区三:把测试写成固定话术匹配。**自然语言任务允许不同措辞,但事实和权限必须有明确标准。
**误区四:把模拟环境通过当成真实能力达标。**Mock 可以隔离环境与验证逻辑,不证明模型在真实场景里的表现。
**误区五:重试之后只检查最终回答。**若先写入再超时,重试可能产生重复副作用,要检查最终业务状态和调用链。
十一、从哪里开始最划算?
如果你的项目刚能跑起来,可以先挑一个有明确业务边界的功能,准备五到十条覆盖正常、缺参、拒绝和工具失败的用例。先记录实际结果和工具事件,再加入最关键的确定性断言。
等这条链跑通后,再逐步补充真实测试环境、更多用户表达、人工抽查、费用和时延统计。评测不一定一开始就做成大型平台;它首先要帮助团队回答:这次改动有没有让关键任务变差,证据在哪里?
参考资料:
更多推荐

所有评论(0)