资讯详情

资讯详情

Agent为什么难测?从概率系统到可回归的测试体系

做 Agent 开发的人都知道测试阶段最容易“破防”。网上很多开发复盘帖子标题动辄就是“被女儿破防大骂”乍一看以为是家庭矛盾点进去才发现是开发者拿自家 Agent 毫无办法的生动写照——你精心设计的智能体平时逻辑清晰、能力在线一到测试环节就像个叛逆期的孩子让它查天气它写诗让它调用工具 A 它偏选工具 B昨天还能通过的用例今天突然全部翻车。这种“失控感”才是 Agent 开发进入深水区之后最真实的日常。如果你正在做 Agent 开发、智能体应用或者准备从传统后端转入大模型应用开发我觉得最值得先想清楚的一件事不是怎么把 Agent 做得更聪明而是怎么验证它“没有变蠢”。因为 LLM 本身是概率系统同样的输入它这次给你正确答案下次可能给你一本正经的胡说八道。如果你把传统软件开发那套“输入-执行-断言输出”的测试方法直接搬过来大概率会被现实教育。这篇文章会以“Neuro 开发复盘”为切入点讲清楚四件事Agent 为什么这么难测它和传统软件测试的差异到底在哪里如何为 Agent 搭建一套可落地的测试体系以及实际工程中常见的问题和排查思路。全文会包含可直接复制的最小示例代码你可以照着跑通一个 Agent 回归测试的最小闭环。1. 这篇文章真正要解决的问题先说结论Agent 测试难不是因为你测试水平不够而是因为 Agent 从架构层面就和传统应用不一样。传统后端服务是一个确定性系统。你传入固定的参数经过固定的逻辑分支得到固定的响应。测试人员要做的是把输入空间和输出空间覆盖完整然后断言结果。这里面的核心假设是同样的输入一定会产生同样的输出。但 Agent 不是这样的。Agent 的核心是 LLM 推理而 LLM 的解码过程带有随机性哪怕使用相同的提示词、相同的参数、相同的输入两次输出的内容也可能不同。更麻烦的是Agent 还有工具调用、上下文记忆、多轮状态累积这使得它的行为空间比传统接口大得多。很多团队在这个阶段会采取一种“伪测试”策略在聊天框里手动输入几个问题看一下回答像不像那么回事然后就说“测试通过了”。这种验证方式有三个致命问题第一不可复现。你手动验证通过的场景写进自动化用例里可能就跑不过。第二覆盖不全。聊天式验证只能覆盖“对话流畅度”覆盖不到工具选择、参数格式、权限边界、失败恢复等关键工程环节。第三回归失效。上线后模型版本一升级提示词一调整所有行为都可能漂移但你没有任何手段发现它漂移了。所以这篇文章真正要解决的问题是如何把 Agent 从“聊天机器人”变成“可测试的工程系统”。读完这篇文章你会得到一套包含测试分层、用例设计、辅助函数、pytest 工程组织的最小可落地测试方案以及一套排查 Agent 测试问题的通用思路。2. Agent 为什么难测从架构层面理解不确定性要解决测试问题先要理解测试对象的本质。2.1 传统应用的确定性传统 Web 应用的测试模型非常清晰输入HTTP 请求参数、数据库数据、配置文件执行Controller - Service - DAO输出固定 JSON 响应在这个模型里测试可以做到精确断言。你甚至可以计算代码覆盖率因为你清楚每一行代码的走向。整个系统是状态可预测的。2.2 Agent 的概率性Agent 的测试模型变成了这样输入用户自然语言 历史对话 可用的工具集合 系统提示词执行LLM 推理决定下一步动作直接回答、调用工具、提问澄清- 执行工具 - 把结果返回给 LLM - 循环输出最终回答文本 中间的工具调用轨迹这里的每一步都存在不确定性。LLM 可能理解错意图可能选错工具可能生成非法 JSON 参数可能在一轮工具执行后忘了最初的任务。也就是说Agent 的行为不是一个函数而是一个分布。这是它难测的根本原因。2.3 测试策略的必然转变既然行为是分布传统“断言精确值”的策略就失效了。正确的策略是在两个方面做控制一是降低不确定性。测试环境固定模型版本、固定温度、禁用随机采样让行为尽量可复现。二是用约束来断言。不再断言“输出等于某句话”而是断言“工具调用是否正确”“参数是否符合 JSON Schema”“是否包含了关键信息”“是否没有越权访问”。从材料来看当前 Agent 开发领域最典型的测试误区就是试图让 Agent 每次输出一模一样。实际上与其追求完全相同不如追求关键路径可控——只要最终结果满足约束中间的细微差异是可以接受的。对比维度传统应用测试Agent 应用测试系统性质确定性系统概率性系统断言对象精确输出值行为轨迹、约束、结果质量输入形态结构化参数自然语言 多轮上下文主要风险逻辑缺陷、边界条件行为漂移、幻觉、工具乱用测试目标验证功能正确验证行为可靠自动化难点用例设计可复现性 评估标准3. 先想清楚你要测的是 Agent 的哪个层面在做测试框架之前我建议先完成一个认知对齐。很多团队把“模型能力测试”“Agent 行为测试”“业务效果测试”混在一起导致测试目标混乱。可以拆成三个独立层面3.1 模型能力测试这一层测试的是底层模型本身。例如GPT 类模型能不能理解中文、能不能做数学推理、有没有幻觉。这一层的测试其实和你的 Agent 架构没有关系直接用 Prompt 数据集评测某个模型即可。3.2 Agent 行为测试这一层测试的是 Agent 的“工程外壳”模型是否正确调用了注册的工具、工具参数是否合法、工具链的执行顺序是否正确、工具报错后 Agent 能否恢复、上下文是否得到了正确维护。这是 Agent 开发者最应该关注的一层也是本篇文章的重点。因为模型能力往往不是你能控制的你决定不了 GPT 或开源模型效果好不好但工具调用、参数校验、状态管理这些工程代码是你完全可以掌控和测试的。3.3 业务效果测试这一层测试的是最终用户价值用户通过 Agent 能不能完成任务、回答是否让用户满意、转化率有没有提升。这类测试通常需要用户评估、在线评测或 LLM 评估属于偏运营和产品侧的闭环。很多 Agent 测试文章把这三层混在一起写结果读者看完了也不知道自己要测什么。这里给你一个更稳妥的判断做 Agent 工程开发优先投入行为测试做模型选型才需要做能力测试做产品上线才需要做效果测试。4. 搭建一个最小可测的 Agent 工程接下来进入实操环节。我们以 Python 为例搭建一个包含工具调用能力的最小 Agent并用 pytest 为它编写测试。不需要引入重量级框架先跑通最小闭环你就能理解 Agent 测试的核心逻辑。4.1 环境准备建议使用以下环境Python 3.10 或更高版本pytestrequests用于调用 LLM HTTP 接口jsonschema用于校验工具参数格式安装命令pip install pytest requests jsonschema如果你还没有可用的 LLM API也可以用本地模型服务替代只要接口兼容 OpenAI Chat Completions 风格即可。为了可复现性测试环境建议固定模型名称和 temperature。4.2 项目目录结构一个简单但清晰的目录结构如下neuro-agent/ ├── agent.py ├── tools.py ├── tests/ │ ├── conftest.py │ └── test_agent.py ├── pytest.ini └── requirements.txt4.3 实现一个简化版 Agent我们先写一个不依赖具体框架的 Agent 类它负责维护对话消息、注册工具、调用 LLM 接口并解析模型返回的工具调用。# 文件路径agent.py import json import os import requests from dataclasses import dataclass, field from typing import Callable, Dict dataclass class Tool: name: str description: str parameters_schema: dict handler: Callable None class SimpleAgent: 一个最小可测的 Agent 实现。 它只负责两件事 1. 维护多轮对话上下文 2. 向 LLM 发送消息并返回模型响应包含可能的工具调用 def __init__(self, model: str gpt-4o-mini, temperature: float 0.0): self.model model self.temperature temperature self.messages [] self.tools: Dict[str, Tool] {} self.base_url os.getenv(LLM_BASE_URL, https://api.openai.com/v1) self.api_key os.getenv(LLM_API_KEY, ) def register_tool(self, tool: Tool): self.tools[tool.name] tool def _build_tool_schema(self): schema [] for tool in self.tools.values(): schema.append({ type: function, function: { name: tool.name, description: tool.description, parameters: tool.parameters_schema, }, }) return schema def chat(self, user_input: str) - dict: 向模型发送用户输入返回 model 的完整 message 对象。 如果模型要调用工具message 中会包含 tool_calls 字段。 self.messages.append({role: user, content: user_input}) payload { model: self.model, messages: self.messages, temperature: self.temperature, tools: self._build_tool_schema(), } headers {Authorization: fBearer {self.api_key}} resp requests.post( f{self.base_url}/chat/completions, jsonpayload, headersheaders, timeout30, ) resp.raise_for_status() data resp.json() message data[choices][0][message] self.messages.append(message) return message说明一下这个类做了什么register_tool用于向 Agent 注册可用工具每个工具包含名称、描述、参数 JSON Schema 三个核心信息。_build_tool_schema把工具信息转成 OpenAI 风格的 function schema方便模型识别。chat维护多轮消息列表调用 LLM 接口后把模型的 response message 完整返回给调用方。如果模型想调用工具返回的message里会包含tool_calls字段。这里刻意不对工具执行流程做过多封装方便你直接观察测试目标模型是否选对了工具是否传对了参数。4.4 定义一些演示工具接下来我们定义两个演示工具一个获取天气一个做数学计算。注意测试重点不是工具内部逻辑而是模型是否正确选择工具并生成参数。# 文件路径tools.py from agent import Tool def get_weather_tool() - Tool: return Tool( nameget_weather, description根据城市名查询当前天气, parameters_schema{ type: object, properties: { city: {type: string, description: 城市名例如北京、上海} }, required: [city], }, ) def calculator_tool() - Tool: return Tool( namecalculator, description执行简单的四则运算, parameters_schema{ type: object, properties: { expression: {type: string, description: 数学表达式例如1 2} }, required: [expression], }, )5. 为 Agent 设计测试用例从断言输出到验证行为有了最小 Agent我们来看测试用例怎么设计。5.1 硬断言验证工具调用行为一个最直观的测试是用户问“北京今天天气怎么样”模型应该返回一个tool_calls并且工具名必须是get_weather。这种断言我们称为硬断言因为它直接验证 Agent 的关键行为路径。# 文件路径tests/test_agent.py import json import jsonschema import pytest from agent import SimpleAgent, Tool from tools import get_weather_tool, calculator_tool pytest.fixture def agent(): a SimpleAgent() a.register_tool(get_weather_tool()) a.register_tool(calculator_tool()) return a def test_weather_question_should_call_weather_tool(agent): message agent.chat(北京今天天气怎么样) tool_calls message.get(tool_calls, []) assert len(tool_calls) 0, 模型没有产生任何工具调用 assert tool_calls[0][function][name] get_weather def test_math_question_should_call_calculator_tool(agent): message agent.chat(计算一下 12 34 等于多少) tool_calls message.get(tool_calls, []) assert len(tool_calls) 0, 模型没有产生任何工具调用 assert tool_calls[0][function][name] calculator这两个用例看起来很简单但已经能抓住 Agent 工程中最高频的回归问题意图识别正确了但工具选错了。如果你调整了提示词或者更换了模型版本这里的失败率会明显上升。5.2 参数 Schema 校验堵住“工具选对了但参数不合法”工具选对了参数却乱传是另一个高频问题。例如模型调用了get_weather但city参数写成了数组类型导致工具执行时报错。所以任何工具调用参数都必须做 JSON Schema 校验。# 文件路径tests/test_agent.py def assert_tool_call_args(message, expected_tool_name, schema): tool_calls message.get(tool_calls, []) assert len(tool_calls) 0, f期望调用工具 {expected_tool_name}但没有产生工具调用 call tool_calls[0][function] assert call[name] expected_tool_name, ( f期望调用工具 {expected_tool_name}实际调用 {call[name]} ) args json.loads(call[arguments]) jsonschema.validate(instanceargs, schemaschema) def test_weather_tool_args_should_match_schema(agent): message agent.chat(上海明天会不会下雨) assert_tool_call_args( message, expected_tool_nameget_weather, schemaagent.tools[get_weather].parameters_schema, )这里我们用jsonschema库做结构化校验而不是判断参数解析后的 Python 对象。这样做的好处是无论模型返回的参数顺序、空格差别如何只要符合 JSON Schema测试就通过。5.3 软评估当结果没有唯一正确答案时工具调用可以硬断言但最终回答文本往往没有唯一正确答案。例如“北京今天天气怎么样”模型可能在调用天气工具后把结果组织成不同的话术。这时候不适合断言“包含北京”这种关键词更适合用软评估。软评估有三种常见做法关键词约束回答必须包含关键事实如城市名、温度单位。相似度阈值用文本相似度判断回答是否覆盖要点。LLM 评估让另一个较大模型充当裁判判断回答是否满足用户意图。下面是一个简单的软评估辅助函数示例# 文件路径tests/test_agent.py def assert_answer_covers_required_facts(answer, required_facts): missing [fact for fact in required_facts if fact not in answer] assert not missing, f回答缺少关键信息{missing}测试用例可以这样组织def test_weather_answer_should_contain_city_and_temperature_keywords(agent): # 假设工具系统已经把天气结果注入到对话中这里直接检查最终答案 answer 北京今天晴气温 -2 到 8 摄氏度请注意保暖。 assert_answer_covers_required_facts(answer, [北京, 摄氏度])这种测试思路非常适合业务层你不需要知道模型怎么措辞只需要确认它没有遗漏核心信息。5.4 Mock 外部依赖测试的不只是模型Agent 测试还有一个容易踩的坑测试过程真实调用了外部 API。如果天气工具真的去请求天气服务测试就会受到外部网络、API 限额、返回数据变化的影响。所以工程上必须引入 Mock。下面是conftest.py里的一个配置示例使用pytest的 monkeypatch 机制屏蔽真实外部调用# 文件路径tests/conftest.py import pytest pytest.fixture def mock_weather_api(monkeypatch): 在测试环境中把天气工具的真实网络请求替换为固定返回值。 def fake_fetch_weather(city): return { city: city, weather: 晴, temperature: 8, unit: 摄氏度, } monkeypatch.setattr(tools.weather_api.fetch_weather, fake_fetch_weather) return fake_fetch_weather需要注意的是不同项目的工具函数路径不同这里只是演示思路。核心原则只有一个测试环境不允许出现不可控的真实依赖。6. 用 pytest 组织 Agent 回归测试单个测试用例好写难的是组织成可持续运行的回归测试套件。这里给你推荐一个适合 Agent 项目的分层组织方式。6.1 测试分层层次测试对象是否调用真实 LLM运行频率单元测试工具函数、参数解析、Schema 校验否每次提交组件测试Agent 上下文管理、错误处理逻辑否每次提交行为测试工具选择、参数格式、多轮记忆是固定模型每次合并前端到端测试完整业务链路是固定模型发版前前两层速度极快可以在 CI 每次提交时运行。行为测试因为依赖 LLM 调用速度较慢建议放在合并请求前运行并且固定模型版本。端到端测试最慢建议放在发版前。6.2 pytest 的标记机制在pytest.ini中配置标记# 文件路径pytest.ini [pytest] markers unit: 单元测试不依赖 LLM behavior: Agent 行为测试依赖固定模型 e2e: 端到端测试 addopts -m not e2e然后在测试文件里打标记pytest.mark.behavior def test_weather_question_should_call_weather_tool(agent): ...运行命令# 默认跳过慢速 e2e 测试 pytest # 只跑行为测试 pytest -m behavior # 全部测试都跑 pytest -m unit or behavior or e2e6.3 测试失败后的重跑策略Agent 行为测试天然有偶发性一次失败不代表真的回归。建议在 CI 中配置自动重试机制例如pytest-rerunfailurespip install pytest-rerunfailures pytest --reruns 2 --reruns-delay 1这里需要提醒重试不应该是“假装没失败”。如果重试后仍然失败必须人工分析失败轨迹否则回归测试就形同虚设。更稳妥的做法是在跑完测试后导出完整的 Agent 运行轨迹包括用户输入、模型输出、工具调用顺序、最终回答方便失败时回溯。7. 完整示例从“破防”到可回归的测试闭环为了让你能直接跑通这里整理一个完整的体验流程。7.1 安装依赖mkdir neuro-agent cd neuro-agent python -m venv .venv source .venv/bin/activate pip install pytest requests jsonschema pytest-rerunfailures7.2 创建项目文件把前面第 4 节的agent.py、tools.py第 5 节的test_agent.py第 6 节的pytest.ini分别创建到对应路径。7.3 配置环境变量export LLM_BASE_URL你的 LLM 服务地址 export LLM_API_KEY你的 API Key如果使用 OpenAI 官方接口LLM_BASE_URL可以设置为默认值不需要导出。7.4 运行测试pytest -v预期输出类似tests/test_agent.py::test_weather_question_should_call_weather_tool PASSED tests/test_agent.py::test_math_question_should_call_calculator_tool PASSED tests/test_agent.py::test_weather_tool_args_should_match_schema PASSED tests/test_agent.py::test_weather_answer_should_contain_city_and_temperature_keywords PASSED看到 4 个 PASSED说明这个最小测试闭环已经跑通了。7.5 如何判断测试是真正有效的有一个简单的验证方法故意改坏你的 Agent 提示词例如把系统提示改成“忽略所有工具调用”然后重新跑测试。如果测试用例能发现这个变化并失败说明你的测试是有效的如果仍然通过说明你的测试根本没有触达 Agent 的关键行为。这条规则我建议你做进团队的测试评审清单。8. 常见问题与排查思路Agent 测试过程会遇到很多表面看不出来的问题这里整理一份排查表按优先级排列。问题现象可能原因排查方式解决方案同一条用例多次执行结果不同模型 temperature 过高或未固定模型版本查看请求日志中的模型名和采样参数测试环境固定 temperature0固定模型版本模型没有产生工具调用工具描述不清晰或提示词不强调工具可用打印完整请求 payload检查 tools 字段优化工具 description在系统提示词中补充工具使用说明工具调用参数不是合法 JSON模型输出格式漂移查看 model message 中 tool_calls 参数原文在代码层做 JSON 解析兜底失败时返回错误信息让模型重新生成测试之间互相影响上下文泄漏Agent 实例或消息列表未隔离检查 fixture 是否返回了同一个 Agent 实例每个测试用例使用独立 Agent 实例必要时清空 messagesMock 外部依赖不生效工具函数路径与 monkeypatch 目标不一致确认工具函数实际调用路径检查 import 路径使用真实函数所在模块路径做 patch测试环境真实调用外部 API环境变量未隔离检查请求日志中的 base_url 与 key使用独立测试环境变量设置“不允许外部网络调用”的断言失败原因无法定位日志只记录最终输出没有记录工具轨迹检查日志是否包含完整 message 序列增加运行轨迹日志记录每次工具调用和中间结果模型升级后大量用例失败模型行为漂移对比新旧模型在同一测试集的通过率在行为测试中固定模型版本模型升级时单独跑全量对比这张表里的前四个问题几乎出现在每一个 Agent 测试项目里。其中最容易被忽视的是第二个模型没有产生工具调用很多时候不是因为模型不行而是工具描述的语义不够清晰或者没有在提示词里明确告诉模型“你可以使用以下工具”。工程上应该把这部分当成“接口文档”来对待工具描述需要反复打磨。9. 工程化最佳实践与团队协作建议测试代码只是起点真正让 Agent 测试发挥作用的是工程规范。这里给出几条可以落地的建议。9.1 建立“黄金用例集”从第一天起就把 Agent 核心链路的用例沉淀下来形成黄金用例集。这个用例集不需要很大覆盖高频用户意图、关键工具调用、典型边界条件即可。每次改动提示词、升级模型、调整工具 Schema都先跑黄金用例集再谈其他优化。没有基线就没有回归可言。9.2 固定测试环境的模型与参数行为测试环境必须显式固定模型版本和 temperature最好也固定最大输出 token 限制。这些参数建议通过环境变量注入而不是写死在代码里。模型升级应该是一个主动行为而不是线上悄悄变化的偶然事件。9.3 测试数据隔离与脱敏测试过程中不要使用用户真实数据。如果确实需要真实数据辅助测试必须做脱敏处理。特别是 Agent 会调用外部工具的场景你无法保证模型不会把某些隐私信息拼进工具参数。更稳妥的做法是构建一套完全虚拟的测试数据与生产环境物理隔离。9.4 记录完整运行轨迹传统接口测试只需要记录请求和响应Agent 测试则必须记录完整轨迹用户输入、每次 LLM 响应、每次工具选择与执行结果、最终答案。这些轨迹既可以用于失败复现也可以沉淀为后续评测集的数据来源。9.5 不要试图用“更多用例”掩盖设计缺陷如果发现大量用例都在同一个环节失败优先思考 Agent 架构和工具描述设计是否存在问题而不是继续增加断言。测试用例的密度高并不代表测试有效覆盖到关键行为的用例才会有实际价值。9.6 明确安全边界与权限控制在测试和实际运行中都要给 Agent 设定权限边界。工具调用需要做白名单控制不允许模型访问未注册的能力涉及数据库操作、文件写入、删除类操作时测试环境必须使用独立沙箱任何破坏性操作都不能触达真实数据。这是 Agent 工程化中最容易被忽略但也最重要的红线。10. 总结与后续学习方向回到开头的“破防”场景。当你理解了 Agent 的测试逻辑你会发现“被女儿破防大骂”这件事本质上是因为你把 Agent 当成了一个确定性系统去要求而它的人生观是概率性的。你能做的不是让它每次都说一模一样的话而是给它设定清晰的行为边界然后在边界内容忍它的即兴发挥。这篇文章给出的体系可以总结为一句话把关键路径变成约束把约束变成自动化测试把自动化测试变成回归基线。如果你接下来要继续深入我建议按这个顺序学习先把你目前 Agent 项目的工具调用轨迹全部打印出来建立第一份测试基线。然后补充硬断言用例覆盖工具选择、参数 Schema、错误恢复三条主线。再引入软评估解决“没有唯一正确答案”的结果质量管理。最后接入 CI让测试成为每次改动后的自动关卡。做完这四步你会发现 Agent 开发从“玄学炼丹”变成了工程问题。至少下次再看到“做测试被破防”的帖子时你可以从容地回复这不是心态问题是测试体系没建好。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →