做一个 Agent 演示不难:用户输入问题,模型调用工具,最后给出一段回答。难的是把它放进真实业务后,你怎么知道它没有查错数据、跳过授权、工具报错后编造结果,或者把一个操作执行两遍?

这篇文章以一个订单查询 Agent为例,拆开测试用例、工具调用轨迹、错误处理、副作用和回归评测,并提供一份可以直接运行的 Python 离线教学代码。示例里的 Agent、订单和工具均为确定性的模拟数据,不调用真实模型或业务接口,因此只能验证演示中的流程,不能代表真实模型的准确率。

Agent 评测链路示意

一、回答看起来合理,不代表任务完成

用户问:“帮我看看订单 A101 到哪里了。”Agent 回答:“您的订单正在运输中。”表面上很顺畅,但一个开发者还应该追问:

  1. 查询的是否真的是 A101,而不是另一个订单?
  2. 当前用户是否有权访问这个订单?
  3. 状态是从工具结果得到的,还是查询失败后凭空生成的?
  4. 用户没有提供订单号时,Agent 是否会追问?
  5. 如果任务包含创建工单,网络重试后会不会创建两份?

一个答案可能字面正确,却走了错误的路径;也可能调用路径看似正常,最后输出却与工具结果矛盾。评测需要把这些问题拆开,分别留下证据。

观察维度应该检查什么常见失败
最终结果回答是否满足任务要求、是否有依据工具失败后仍说“已签收”
工具选择是否调用了合适的工具需要查订单,却没有查询
参数和权限订单号、用户身份是否正确用错误订单号或访问他人订单
过程状态失败、缺参、超时能否被识别异常被吞掉,显示成功
业务副作用写入、发消息等是否符合约定同一请求创建两张工单

二、先定义任务边界和成功标准

评测之前,要先写清 Agent 的任务合同。对于本文的例子,我们约定:

  • 用户只能查询自己有权访问的订单,权限由服务端验证,不能只靠提示词要求模型守规矩。
  • 缺少订单号时先追问,不调用查询工具。
  • 查询成功后,根据工具返回的状态回答。
  • 服务不可用时明确告知失败,不编造订单状态。
  • 用户授权创建工单后,如果同一请求被重复投递,业务侧应避免重复创建。

这些规则不一定适用于所有产品。比如另一套系统可能允许客服查询多个客户的订单,但也应由真实权限模型决定,而不是让 Agent 自行猜测。

这里的“成功”可以进一步拆成必要条件和可接受的差异:查询 A101 时,可以允许回答措辞不同;但是订单编号、状态、授权结果和是否产生额外写入,不能只因为措辞流畅就放过。

三、把要求变成测试用例

很多团队只准备几条“正常输入”,模型回答看起来不错就算通过。真正接入后,出问题的往往是缺参数、数据冲突和外部服务失败。

场景输入或环境预期结果必须检查的过程
正常查询Alice 查询自己的 A101回答“运输中”查询 A101,且无写入
缺少参数没有订单号询问订单号不调用订单查询
越权查询Alice 查询 Bob 的 B202拒绝或不给出订单详情服务端验证身份与资源归属
服务异常查询工具不可用明确失败,不编造状态异常被识别并返回
重复投递同一授权工单请求投递两次最终只产生一次创建检查真实副作用,而非只数回答
故障注入服务故障时错误版本声称“已签收”评测应报告失败检查评测器能否抓住错误

注意这里的“越权”用例既检查 Agent 输出,也依赖服务端阻止未授权读取。若服务端本身返回了敏感数据,即使 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

在这个代码中:

  1. Store.query 在模拟服务内检查订单归属,越权时不返回订单详情。
  2. agent 是确定性流程,用来演示被测系统如何调用工具;它并不是一个真实大模型 Agent。
  3. evaluate 分别检查回答、调用轨迹和内存里的写入数量。
  4. create_ticket 对同一用户和请求键进行内存去重,示例故意投递两次以演示检查副作用。

基线版本应为 5/5 PASS;故障注入版本应为 4/5 PASS,其中“服务不可用”用例 FAIL 是预期结果:评估器抓住了错误版本的编造回答。总数只是这五条模拟用例的结果,不能用作产品准确率。

回答与执行轨迹对比示意

这个示例有哪些边界?

首先,内存去重只说明教学中的字典行为,无法证明真实数据库在并发、多进程或宕机恢复时的幂等性。真实系统需要根据业务使用持久化唯一约束、事务、幂等键生命周期和请求状态查询等机制。

这里 ticket=True 只模拟上游已经授权的测试输入,并没有实现工单写入授权;真实创建动作也必须在服务端单独鉴权。

其次,模拟 Agent 的行为固定、可预测;它不覆盖真实模型的随机性、供应商 API、网络、复杂工具调用和提示注入。它适合验证评测框架能否识别已知缺陷,接入真实 Agent 后还需要单独做受控测试。

七、回归评测是什么?

你修改了提示词、工具描述、模型版本或工作流,希望以前能做对的任务仍然能做对,就可以重新运行同一批测试用例,再比较结果。

变更可能影响什么可以重点回归
提示词回答格式、追问、工具选择缺参、拒绝、答案引用
模型版本工具调用和结果稳定性关键流程、不同输入表达
工具接口字段、错误码、超时参数校验、异常分支
权限规则用户可访问的数据范围授权、越权和租户隔离
重试与队列执行次数和顺序幂等、重复投递、部分成功
前端展示加载和失败提示成功、失败、等待状态

回归时不要只看总通过率。例如 95 条普通查询通过、5 条越权用例失败,单个总分可能掩盖严重问题。可以分别报告正常任务、权限、工具异常和副作用等类别,再为关键安全规则设置单独门槛。

八、真实 Agent 接进来时,要怎么扩展?

把离线评测升级成真实测试时,可以保留用例格式和断言,再把模拟 Agent 替换成真实调用的适配器。推荐逐步进行:

  1. 固定一批脱敏、可复现的输入,明确每条用例的预期和判定规则。
  2. 给工具准备隔离的测试环境,避免影响真实订单或向真实用户发送消息。
  3. 保存可观察轨迹,记录模型和提示词版本、工具响应、耗时与错误类型。
  4. 对不同结果使用合适判定方法:确定性规则、结构检查、人工抽样或辅助评分。
  5. 明确网络调用、配额和费用预算,必要时先用模拟工具验证接线。
  6. 多次运行并记录样本数,区分偶发失败与稳定退化;不要用一次结果推断整体表现。

真实模型评测可能产生费用,也可能受到模型更新、网络波动和测试数据变化影响。要比较两个版本,尽量固定用例、工具环境、评判标准和执行配置,并保留不可比较时的原因。

九、评测集怎么持续维护?

评测集不应该只由开发者临时想到的几句话组成。可以从产品需求、历史故障、人工审查和授权后的脱敏案例中整理,再按场景分类。

用例来源可以覆盖什么维护要点
产品需求基本业务流程要求变化时同步更新预期
历史故障已经出现过的缺陷修复后保留为回归用例
边界与对抗输入缺参、越权、冲突指令不把未授权内容当指令
工具故障超时、限流、空结果明确哪些错误可重试
人工审查解释质量、模糊需求记录评分标准和分歧

测试集也会过时。每个用例应带上适用业务版本、构造方式和预期来源;对缺少依据的案例标记待确认,而不是为了得到漂亮分数随意修改预期。

十、实践中常见的几个误区

**误区一:只看最终答案。**答案正确不代表没有越权或额外操作;需要结合轨迹和业务状态。

**误区二:用一个平均分覆盖所有问题。**回答不够简洁和泄露他人订单不是同一严重程度,关键约束应单独统计。

**误区三:把测试写成固定话术匹配。**自然语言任务允许不同措辞,但事实和权限必须有明确标准。

**误区四:把模拟环境通过当成真实能力达标。**Mock 可以隔离环境与验证逻辑,不证明模型在真实场景里的表现。

**误区五:重试之后只检查最终回答。**若先写入再超时,重试可能产生重复副作用,要检查最终业务状态和调用链。

十一、从哪里开始最划算?

如果你的项目刚能跑起来,可以先挑一个有明确业务边界的功能,准备五到十条覆盖正常、缺参、拒绝和工具失败的用例。先记录实际结果和工具事件,再加入最关键的确定性断言。

等这条链跑通后,再逐步补充真实测试环境、更多用户表达、人工抽查、费用和时延统计。评测不一定一开始就做成大型平台;它首先要帮助团队回答:这次改动有没有让关键任务变差,证据在哪里?

参考资料:

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐