周四晚上我在 Neuro 项目的测试环境里跑第三轮 agent 回归。日志滚得很快但问题很明显一个“查询用户订阅状态”的简单任务agent 连续三次调错了工具把get_subscription写成了get_user_profile。我盯着调用链发呆纠结要不要把 temperature 从 0.7 调成 0.2女儿推门进来扫了一眼屏幕说“你这是在跟电脑吵架还是在写程序饭都凉了。”说实话那一刻我整个人是懵的。不是因为她的话有多重而是因为她说到了要害——我忙了一整晚测试却根本说不清这轮测试到底证明了什么。三条用例全挂我也分不清是 prompt 的问题、工具定义的问题还是模型自己抽风。这种憋屈我相信很多做 agent 开发的人都体会过。后来我把测试方法整个重新梳理了一遍才想明白一个判断做测试做到破防大多数时候不是 agent 错得有多离谱而是我们还在用测普通软件的方法去测一个带随机性的复杂决策系统。这篇文章不打算讲某个具体工具的用法而是想把我在 agent 开发测试上的经验、坑和方法论整理出来。如果你也在做 agent 开发并且经常觉得测试像在赌运气那这篇应该能帮上忙。1. 先搞清楚 agent 测试和传统测试差在哪1.1 一个断言顶不住十次推理传统软件测试的思路其实很简单给定一个输入调用一个函数断言返回值等于预期。即使流程复杂一点单元测试、集成测试、接口测试本质上都是围绕“输入-输出”的确定性比较。但 agent 不一样。一次任务跑下来模型可能经过好几轮推理调好几个工具每一步都会改变上下文最终输出还是一段自然语言或者结构化 JSON。最麻烦的是同一个输入跑十次十次的中间轨迹都可能不一样。这就带来一个直接后果你很难用一条断言去验证“这次跑得对不对”。就算最终输出能解析、字段都齐全你也说不清 agent 是不是真的按正确路径完成了任务。反过来输出格式稍微变了一点断言就可能误报。测试在深夜里全红然后你以为是模型出了问题实际上是断言写得太死。这里的核心差异是传统测试断言的是函数的返回值而 agent 测试需要断言的是整个决策轨迹。轨迹里有哪几步、每步调了什么工具、传了什么参数、拿到什么返回值、最后怎么收尾这些才是 agent 真正的工作成果。1.2 agent 测试要面对的四个不确定源如果只是调用的步骤多一点问题还不算复杂。真正让 agent 测试变得困难的是下面四个不确定源模型输出不稳定。temperature、采样策略让结果天然有浮动同一个 prompt 在不同批次下可能有不同表达。工具调用依赖外部状态。后端接口限流、返回延迟、数据变化都会直接改变 agent 的后续行为。上下文是动态变化的。工具返回太长会触发截断策略截哪里、留哪些都会影响模型后续的判断。提示词改动有连锁反应。你修好一个场景可能弄坏另一个场景这种“按下葫芦浮起瓢”在 agent 里尤其常见。这四个来源叠加在一起agent 测试本质上更像在测一个带有随机性的复杂系统而不是在测一个纯函数。所以测试目标也要跟着变从“判断这次对错”切换成“评估风险、保留证据、控制回归”。注意如果你用测确定性软件的心态去测 agent第一反应一定是“怎么老是不稳定”。但换个角度不稳定本身就是这个系统的固有属性测试要做的是把不稳定量化出来而不是假装它不存在。2. 四个让我心态崩过的坑列出来给你当路标2.1 坑一把大模型输出当成精确断言对象我第一次写 agent 测试的时候本能地用了老办法跑完一个任务拿到返回的 JSON然后用assert response[status] success这种方式去校验。这种写法在传统接口测试里没问题但放在 agent 上就是灾难。模型输出的字段顺序可能变枚举值的大小写可能变有时它觉得表达成“SUCCESS”更合适有时又在 JSON 外面包了一层 markdown 代码块。第一天全绿第二天全红中间什么都没改。后来我把校验拆成了两段先做格式校验再做语义校验。格式校验用 JSON Schema 或严格的解析器只负责确认“结构对不对”语义校验再用规则或者二次模型评分确认“值对不对”。两段不能混在一起否则你永远分不清是模型抽风还是断言写得不合理。2.2 坑二用真实环境跑全部测试有段时间我图省事直接拿真实的大模型 API 和真实后端来跑测试。结果发现测试失败的原因有一大半跟 agent 逻辑没关系要么后端偶尔 5xx要么触发了限流要么测试数据被别的任务改了。这时候你面对着最尴尬的局面一条用例挂了你根本不知道是 agent 写错了还是环境临时抖了一下。如果为了赶进度重跑一次能过你多半会抱着“可能是偶发”的心态放过它。但真正的 bug 往往就藏在这种“偶发”里。我现在的做法是分层核心逻辑测试全部用 mock 或 stub 工具工具返回值、延迟、报错都由测试脚本控制真实环境只留给集成验收而且验收前会先确认外部依赖状态正常。mock 不代表不测真实而是把不确定性隔离在固定的一层里出了问题才能快速定位。2.3 坑三只测顺利路径新手写测试最容易把用例写成“岁月静好”版指令清晰、工具正常、数据齐全、模型一次答对。这种用例跑再多也只能证明 agent 在理想条件下不会崩。真正让 agent 翻车的往往是那些“不那么理想”的输入用户问题有歧义、工具返回了错误、数据查询结果为空、上下文已经很长快超限了。这些场景不测等于把 agent 最薄弱的部分丢给线上用户去发现。我在整理 Neuro 项目的测试语料时定了一个规则每条主流程用例至少配两到三条异常变体。主流程是“订阅用户查询成功”异常变体就是“查询不存在的用户”“工具返回权限错误”“用户问题同时包含两个意图”。agent 的价值恰恰体现在这些边缘情况里测试也要往这个方向倾斜。2.4 坑四不留轨迹出错全靠猜这个是所有坑里最要命的。早期我做测试只关心最终输出跑完打一个 PASS 或 FAIL然后下一个。等到某条用例挂了看到 FAIL 那行字才发现自己手里什么证据都没有。agent 跑五步中间到底哪一步出了问题工具参数传错了还是模型理解偏了上下文被什么内容污染了如果这些都没有记录你只能一遍又一遍重跑靠猜去试。运气好半小时运气不好一晚上就搭进去了。第四周的时候我痛定思痛给测试脚本加了一个硬性要求每次运行必须输出完整轨迹包含原始输入、每一步工具调用的参数和返回值、上下文更新情况、最终输出、token 用量、耗时。这个改动让调试成本下降得非常明显。没有轨迹的失败等于没有证据的锅后面所有排查方法都是建立在这个基础之上的。3. 把 agent 测试拆成三层输入、过程、输出想明白前面的坑之后我给自己定了一个三分法的测试框架。任何一次 agent 测试都可以拆成三个层面来看输入层、过程层、输出层。每一层有不同的校验内容和工具不能混在一起。3.1 第一层输入与指令测试这是最接近传统测试的一层也是应该最先跑的一层。输入与指令测试要确认的是prompt 模板有没有正确渲染、变量有没有缺失、工具定义name、description、parameters是否符合模型的 function calling 要求、上下文拼接顺序对不对、截断策略有没有生效。这一层有个特点它基本是确定性的。模板渲染结果不会随机schema 校验也不会随机。所以完全可以用普通单元测试跑在 CI 里每次改代码都能快速反馈。很多 agent 问题看起来是模型“理解错了”实际是输入层就没搭好。比如工具描述写得含糊模型根本不知道什么时候该调用某个工具或者上下文拼接顺序反了模型拿到的不是最新信息。输入层测好了过程层的很多诡异错误会自然消失。3.2 第二层过程测试过程层是 agent 测试的核心也是普通软件测试里没有的。它校验的是 agent 在完成任务过程中的决策轨迹具体包括工具调用序列是否符合预期有没有该调的没调、不该调的乱调工具参数是否合法类型、必填项、枚举值是否满足约束状态迁移是否正确比如一个“待处理 → 已确认 → 已发送”的流程有没有跳步遇到工具报错时agent 有没有走正确的兜底逻辑而不是反复重试或直接放弃。这一层的断言对象是整个 trace而不是最终输出。我在测试脚本里写了很多对轨迹中步骤的检查比如“整个过程中必须且只能调用一次get_subscription”“调用send_reminder前必须先有confirm_user的成功记录”。这种断言能把 agent 的思考过程约束在可控范围内。3.3 第三层输出测试输出层校验的是最终结果但要注意它不能只是格式检查。我的做法分三小步第一格式校验确认输出能被程序正确解析第二语义校验确认回答确实解决了用户的问题而不是“格式正确但胡说”第三质量评估通过评分规则或者直接让另一个模型打分判断这段输出是不是达标。语义校验是最难自动化的部分。我一般用两个手段一是对已知答案的问题做关键信息比对比如用户订阅状态是 active输出里必须包含 active二是对开放性问题用 LLM-as-judge 的方式打分但会提供明确的评分标准和参考样例避免让评分模型自己发挥。过程正确但输出难看或者输出漂亮但过程完全跑偏这两种情况都要靠三层分别暴露。下面是这个框架的整理可以直接照着搭测试层测什么常用方法通过标准示例运行频率输入/指令prompt 渲染、工具定义、上下文拼接、截断策略单元测试、schema 校验、模板快照模板变量齐全工具参数符合 JSON Schema每次代码变更过程工具调用序列、参数合法性、状态迁移、异常兜底轨迹断言、规则校验必须调用指定工具且参数合法每次回归输出格式、语义、质量JSON Schema、规则比对、LLM 评分、人工抽检输出可解析关键信息正确质量评分达标回归 定期抽检4. 从一条用例到批量回归最小可执行流程框架有了接下来是落地。我建议不要一上来就写几百条用例而是先走通一个最小可执行流程。4.1 先准备一份能代表真实问题的测试语料测试语料的质量直接决定这套流程有没有价值。我一般控制在 10 到 30 条覆盖四类场景主流程、异常输入、边界条件、历史 bad case。每条用例不写成“期望某段文字”而是写清楚四件事用例名称、输入内容、期望的工具调用轨迹、期望的输出行为。比如“查询不存在用户时必须调用get_subscription并在输出中明确提示用户不存在而不是返回空成功”。历史 bad case 是最值得收进语料的资产。凡是线上或测试中暴露的问题修复之后立刻转成回归用例防止同一个坑踩第二次。4.2 一个最小测试骨架示例下面是一个通用结构的示例主要说明思路不是让你直接复制。关键点是断言对象是 trace不是最终输出。# 示例结构需按你的 agent 框架调整 CASES [ { name: 订阅用户查询成功, input: 查询 user_123 的订阅状态, expect_tool: get_subscription, expect_key: active, }, { name: 工具返回错误时应兜底, input: 查询 user_999 的订阅状态, expect_tool: get_subscription, expect_error_handled: True, }, ] def run_agent(input_text, config): # 调用你的 agent 入口返回完整轨迹 return agent.run(input_text, configconfig) def assert_trace(trace, case): tools [step[tool] for step in trace[steps]] assert case[expect_tool] in tools, \ f期望调用 {case[expect_tool]}实际调用 {tools} # 这里补上输出格式校验和语义校验 for case in CASES: trace run_agent(case[input], configcfg) try: assert_trace(trace, case) print(f[PASS] {case[name]}) except AssertionError as exc: print(f[FAIL] {case[name]}: {exc}) save_trace(trace, ffail_{case[name]}.json)这段代码里save_trace是最不该省的一步。不管是 PASS 还是 FAIL轨迹都应该落盘方便之后做统计和分析。4.3 关键参数怎么理解跑测试之前有几个参数需要先想清楚否则后面会来回折腾temperature回归测试建议用较低的值减少输出随机性。但注意有些模型就算 temperature 设为 0也不能保证完全确定所以它只是降低概率不是消除概率。超时要区分单步超时和总超时。工具调用可能会卡住单步超时管住了单次等待但总超时才是防住整条链路死循环的保险。重试只对可重试的错误重试比如网络抖动、限流、临时 5xx。工具参数错误这类问题重试一百次也没用反而掩盖了真实 bug。并发从 1 开始批量跑之前先算清楚 token 成本和接口限流。不要一上来就把并发拉满否则你会在排查 agent bug 的同时还要排查限流问题。预算每次运行都要记录 token 用量。批量回归前先估算总成本很多 agent 测试跑不下去不是因为逻辑不对而是预算失控。4.4 验证顺序一条 → 五条 → 全部 → 过夜我踩过的教训是永远不要第一次就把所有用例全量跑完。更稳妥的顺序是先单独跑 1 条用例确认轨迹结构、断言逻辑、日志输出都正常再跑 5 条左右的小批次看断言会不会过严或过松输出里有没有意外格式确认稳定后跑全部用例重点看失败分类而不是只看通过率如果要长期回归最后再挂到定时任务里做夜间回归第二天早上看报告。每一步都是在验证测试框架本身是否可靠而不是急着去验证 agent。测试框架不可靠的时候agent 测试跑出来的结果没有任何意义。建议批量之前先把日志级别调成可按用例检索。每一条用例的输出都带上独立 trace 文件名或请求 ID否则 30 条用例一起跑你连哪条日志对应哪个输入都分不清。5. 测试挂了先别改 prompt按这个顺序排查测试跑挂了很多人的第一反应是“改 prompt”。但 agent 测试里直接改 prompt 是最危险的姿势因为你根本不知道问题到底出在哪一层。更合理的做法是先把问题定位到层再决定动哪里。5.1 一张排查顺序表我给自己定的排查顺序是这样排查顺序看什么怎么判断1. 输入层prompt 渲染结果、上下文是否完整、变量是否缺失模型收到的输入是否符合预期2. 环境层mock/真实环境、限流、超时、依赖版本外部依赖是否处于异常状态3. 工具层工具名、参数、返回值、错误信息工具执行与预期是否一致4. 模型层推理过程、temperature、prompt 表达模型选择是否与场景匹配5. 断言层断言标准是否合理、过严或过松是 agent 错了还是测试标准错了这个顺序的核心原则是先外部后内部先证据后猜测。只凭最终输出不对就回去改 prompt等于跳过所有中间环节直接下结论大概率会引入新问题。在常见实践里可以先把失败分成两类一类是格式类比如输出无法解析、工具参数类型不对这类大概率卡在输入层或工具层另一类是语义类比如工具调用顺序不对、最终回答答非所问这类多半在工具层或模型层。分好类再进表排查会更快。5.2 一个真实排查案例回到开头那个让我破防的场景输入“查询 user_123 的订阅状态”trace 显示 agent 调用了get_user_profile而不是get_subscription。按顺序走一遍输入层prompt 渲染正常上下文里确实有用户 ID环境层mock 环境稳定排除了外部抖动工具层两个工具本身都能正常调用参数也没问题模型层问题定位到这里。agent 把“查看用户信息”理解为应该先调get_user_profile。那怎么修不是简单加一句“不要用 get_user_profile”。我在 prompt 里改的是工具描述把get_subscription的 description 从“获取订阅状态”改成“当用户询问订阅、会员、套餐有效期时调用此工具查询订阅状态”并且补充了一个示例用法。同时给get_user_profile的描述加了一句边界说明避免模型在“查订阅状态”时联想到它。修复之后跑了三条相似用例全部通过。这里想强调的是定位到模型层之后还要继续往下拆一层是工具描述不清晰、是用户问题有歧义、还是 prompt 缺少约束规则。不同原因对应不同的改法不能一概而论。5.3 把每次失败变成测试资产单次修复解决不了长期问题。更重要的动作是把每次失败的用例、轨迹、原因分类、修改动作、验证结果记下来形成 bad case 清单。我一般用这样一条记录格式输入 → 失败轨迹 → 错误类型 → 修复动作 → 回归结果。错误类型会分四类模型理解问题、prompt 设计问题、工具设计问题、测试标准问题。每周复盘的时候把新增 bad case 按类型归类看看哪一类占大头。如果模型理解问题特别多可能要考虑换模型或改结构化指令如果工具设计问题多说明工具边界没划清楚如果测试标准问题多说明断言设计得太主观。这套复盘的收益是复利的。测试用例越积越多bad case 越积越多agent 的稳定性会肉眼可见地变好而不是每次都在原地打转。6. 长期来看agent 测试真正考验的是什么6.1 这套方法的适用边界前面讲的这套分层测试方法不是万能的。它更适用于有工具调用、有结构化决策、需要回归保障的 agent 项目比如 function calling 类任务、MCP 工具类 agent、流程编排类智能体。如果是纯开放域聊天、单轮问答、或者对创造性要求极高且没有标准答案的场景这套方法的收益会明显下降。原因很简单过程层没有明确的工具轨迹可以断言输出层也缺少客观的通过标准。这类场景更适合人工抽检加线上反馈而不是机器断言。还有一个前置条件你的项目必须能记录完整 trace。没有 trace过程层测试无从谈起。如果 agent 框架不支持输出中间步骤需要先补齐这一块再谈测试自动化。6.2 自动化测试替代不了的三件事即使分层测试做得再完善也有三件事是自动化替代不了的线上真实流量的监控和异常召回。测试用例是有限的真实用户会带来你完全没想到的输入。线上日志、监控告警、异常召回是自动化测试的补充不是增强项。用户反馈和 bad case 的持续复盘。agent 的产品化是一个持续迭代过程每一条用户反馈都是一条免费的高价值测试用例。模型升级前的全量回归。换了模型版本哪怕只是小版本行为也可能漂移。升级之前必须把测试语料完整跑一遍不要盲目相信“新版一定更好”。6.3 回到“破防”那天那天晚上我最终没有调 temperature。第二天早上到办公室花了二十分钟就定位了问题原因是前一夜重写测试脚本时保留了完整 trace。看到轨迹里 agent 在两次工具调用之间的犹豫我才意识到不是随机性问题而是工具描述不够清晰。后来我养成了一个习惯每次想对着日志发火之前先问自己三个问题——这条用例的输入有没有问题轨迹里的证据够不够我是在改 bug还是在碰运气对于一个还在快速演进的领域来说测试方法本身也会不断变化。但有些东西是确定的把输入、过程、输出拆开验证把每次失败沉淀成用例把每次修复留下证据。流程稳了心态就不会崩。饭还是要按时吃agent 也是要一步步调出来的。