Agent开发Easy,But测试Max
Agent开发Easy,But测试Max
序
最近在搞 physical-ai 这个项目,一个基于 Spring AI 的 AI Agent 平台,应用方向是通用领域下的IOT场景。
说实话,开发阶段我膨胀了。
ChatClient 一接,工具一挂,提示词一写,ReAct 循环 Spring AI 直接给你跑起来。一个能聊天、能调工具、能记住上下文、还能流式输出的 Agent,几天就跑起来了。我当时坐那看着屏幕里 AI 一边"思考"一边自己调接口,心想:就这?AI Agent 也就这点事?
然后我把项目丢给了测试同学。
第二天测试同学跑来问我:
"你这个 Agent,准确率是多少?"
我:……😶
"我随便问十遍同一个问题,它有八遍给我对的,两遍给我扯淡,这算合格吗?"
我:……😶😶
"我能不能像测普通接口那样,给它写个测试用例,断言它返回 '成功'?"
我:……😶😶😶
那一刻我才意识到一件特别残忍的事——
Agent 开发 Easy,测试 Max。
开发的时候你是造物主,想让它干嘛干嘛;测试的时候你是个面对黑匣子的憨批,输入进去,输出飘出来,你连它为什么这么回答都说不清。
这篇就来好好吐槽一下这件荒谬的事
开发 Easy:搭一个 Agent 跟搭脚手架一样
先把"开发不难"这事说清楚,免得显得我在凡尔赛。
Spring AI,基础设施都给你铺好了
2025 年 Spring AI 出了 1.0 正式版之后,用 Java 做 Agent 这事,门槛直接被踩烂了。它把几样最脏最累的活全包了:
| 能力 | Spring AI 给你的 | 你还要操心的 |
|---|---|---|
| 调大模型 | ChatClient 一行流式调用 | 选哪个模型 |
| 工具调用 | @Tool 注解 + ToolCallback 自动注册 | 写工具的业务逻辑 |
| 对话记忆 | ChatMemory 接口 | 用什么存(MySQL?Redis?) |
| ReAct 循环 | 全自动,你啥都不用写 | 真的不用写 |
| 流式输出 | Flux<ChatResponse> | 前端怎么接 SSE |
最爽的是 ReAct(Reasoning + Acting)。以前自己撸过 ReAct 循环的都知道那玩意有多恶心——判断模型要不要调工具、调完工具结果怎么塞回去、要不要再问一轮……Spring AI 这部分是黑盒全自动的:
// 开发一个能调工具的 Agent,核心就这么几行
ChatClient.prompt()
.system(systemPrompt) // 你是谁
.messages(historyMessages) // 之前聊过啥
.user(userMessage) // 用户这次说啥
.toolCallbacks(toolCallbacks) // 你能干啥(工具)
.call(); // 剩下的 Spring AI 全包
// Spring AI 内部自动:
// 问模型 → 模型说"我要调工具A" → 调 → 结果塞回去
// → 再问模型 → 模型说"我还要调工具B" → 调 → ...
// → 直到模型给出最终回答
看上去是不是非常的简单
physical-ai 的架构,也就那么回事
我把项目拆成了几个模块,核心链路长这样:
用户发消息
│
▼
ChatController(接请求)
│
▼
AgentOrchestrator(编排器,大总管)
│
├── AgentRouter → 这句话该哪个 Agent 接?
├── DialogManager → 把之前的聊天记录捞出来
├── SystemPromptBuilder→ 拼系统提示词
│
└── LlmGateway(调大模型)
│
├── 带上工具
├── 带上历史记忆
└── 丢给 Spring AI → ReAct 自动跑
│
▼
保存响应 / 推 SSE 给前端
工具怎么注册?一个 @Tool 注解,Spring 启动时 BeanPostProcessor 自动扫描,零配置:
@Tool(description = "查询用户列表")
public String queryUsers(@ToolParam(description = "页码") String pageNum) {
// 你的业务代码
}
危险操作(删用户、改配置这种)?给方法打个 @DangerousOperation,自动包一层确认流程,AI 说删不会真删,先弹个确认按钮,用户点了才执行。
对话记忆?MySQL + Redis 二级缓存,实现一下 ChatMemory 接口就完事。
所以你看依然 Java 工程师那套老三样——分层、解耦、设计模式。 模块拆分、注册中心、装饰器、策略、模板方法,一个不少。
(这套架构我之前专门写过一篇《Spring AI Agent 应用架构设计》,想看细节的可以翻翻,这里就不展开了。)
所以你看,开发一个 Agent,本质上和开发一个普通后台系统没啥区别——以前你调数据库,现在你调大模型;接口变了,套路没变。
开发阶段,我甚至有点无聊。
然后,大的来了。
测试 Max:你连"对不对"都定义不了
开发完丢给测试,我才发现一个根本性的问题:
传统的软件测试方法论,在 Agent 面前显得非常单薄。
先看普通接口是怎么测的
写个正常的单元测试 / 接口测试,无非是这一套:
@Test
void createUser_shouldReturnSuccess() {
// 给定
UserDTO input = new UserDTO("test001");
// 当
Result result = userService.create(input);
// 那么
assertEquals(200, result.getCode());
assertEquals("test001", result.getData().getAccount());
}
这套 Given-When-Then + assertEquals,是程序员二十年的肌肉记忆。它成立的前提是——同样的输入,永远产生同样的输出。这是确定性系统的灵魂。
而 Agent,是个概率系统
Agent 背后是大模型,大模型是个概率机器。它每次生成回答,是在一堆 token 里按概率采样。这意味着:
传统接口: 输入 A ──→ 永远是 输出 B (确定的)
AI Agent: 输入 A ──→ 70% 输出 B (概率的)
20% 输出 C
10% 输出 D(甚至开始胡扯)
你拿 assertEquals 去断言一个 Agent?好家伙:
// 你想这么写:
assertEquals("已为您切换成英文", agent.chat("把大屏切成英文"));
// 实际跑五次,它给你五种说法:
// 第1次:"好的,已切换为英文 ✅"
// 第2次:"切换完成。"
// 第3次:"已经帮您把会议室那台大屏的语言设置为 en_US。"
// 第4次:"您是要切英文吗?我帮您切了哈。"
// 第5次:"抱歉,我没找到这台设备。" ← 它飘了
前面四次意思都对,但字符串一个都不相等。第五次直接跑偏了。
请问,这个 assertEquals 你怎么写?
你写不出来。因为:
- 答案不唯一:同样是对的,表述千变万化,你没法穷举所有"正确答案"。
- 它会飘:哪怕你把"正确答案"全列出来,它偶尔还会给你一个完全不沾边的。
- "对"的标准是模糊的:第二次回答算对吗?它没说"切换"俩字,但意思到了。第三次对了但啰嗦。这怎么打分?
这就是 Agent 测试的第一个黑洞:你连"什么算对"都定义不清楚。
再看更恶心的:越用越偏
如果只是"答案不唯一",咬咬牙还能忍。真正让我破防的是这个现象——
同一个 Agent,会话越长,越不准。
我们在项目里实测过(也对着线上日志扒过),准确率随会话轮次的变化大致是这样:
准确率
100% ┤●
85% ┤ ●
70% ┤ ●
55% ┤ ● ← 第 20 轮,已经惨不忍睹
└──────────────────
第1轮 5 10 15 20+ 会话轮次
第一轮问它,准得一批。聊到第二十轮,它开始答非所问、调错工具、参数乱填。
为什么?我顺着代码扒了一遍,根因有三个,每一个都让人血压升高:
根因一:工具全摆桌上,让它自己挑
我们把系统里所有工具——查询、创建、删除、改配置……加起来大几十个——一次性全塞给模型,让它自己决定用哪个。
这相当于把一个仓库的 56 件工具全倒在地上,跟一个新人说"你自己看着挑"。它不挑错才有鬼。56 选 1 的出错概率,远远大于 5 选 1。
根因二:记忆全带上,越聊越脏
整个会话的历史——包括前面聊岔了的话题、过期的数据、用户随口一提的废话——一股脑全塞进上下文。聊到第二十轮,模型面前堆了十九轮的垃圾,它能不被带偏?
这就像开会,你让它处理眼前这件事,它脑子里还记着半小时前大家吵过的另一件事,时不时串台。
根因三:一心三用,全靠模型自觉
模型同时干三件不确定的事:选哪个工具 + 把参数填对 + 写一句像样的回复。任何一件它"概率失误"了,整条链就歪了。
这三件事加起来,就是"越用越偏"的元凶。它不是 bug,它是这种使用方式的结构性必然。
参数填错
就算模型选对了工具,它填的参数也可能是个惊喜:
用户:"把大屏切成英文"
模型调用工具:setLanguage(deviceKey="...", lang="中文") ← ???
模型调用工具:setLanguage(deviceKey="...", lang="English") ← 设备协议要 en_US,不认
模型调用工具:setLanguage(deviceKey="...", lang="english") ← 大小写都不对
模型调用工具:setLanguage(deviceKey="...", volume=999) ← 它甚至把字段都搞错了
同一个意图,它能给你变出四种参数写法,其中三种是错的。设备协议要的是 en_US,它给你 中文、English、english……你拿这个去断言?断言个锤子。
点名:Agent 是个"黑匣子"
把上面这些现象总结一下,你会发现 AI Agent 这玩意,本质上是个黑匣子:
┌─────────────────────────────┐
输入 →│ ??????????????????????? │→ 输出(飘忽不定、不可复现、不可解释)
│ ??????????????????????? │
│ (里面是 175B 个参数, │
│ 没人知道它怎么想的) │
└─────────────────────────────┘
- 不可复现:同样输入,输出每次不一样。
- 不可解释:它为什么这么回答?为什么调这个工具?你打开源码也看不出个所以然(源码就是一堆矩阵乘法)。
- 不可断言:你没法用
assertEquals这种确定性手段去卡它。 - 会漂移:用着用着自己就跑偏了,你还不知道为啥。
而我们程序员这辈子学的所有测试武器——单元测试、集成测试、断言、覆盖率、回归测试——全是针对确定性系统的。
拿这套武器去打一个概率系统,就像拿卷尺去量风速。工具没错,对象错了。
这就是为什么 Agent "测试 Max"——不是测试难,是我们压根没有一套测试概率系统的方法论和工具。
【黑匣子对话测试工具】:怎么把黑匣子掰开
吐槽归吐槽,活还是得干。Agent 不能测,难道就裸奔上线?那产品不得把我祭天。
所以我给自己定了个方向:别指望"调优 AI"来保证质量,得造一个工具,用确定性的工程方法,去度量这个不确定性的黑匣子。
我把这个东西暂且叫——【黑匣子对话测试工具】。
思路就一句话:你没法断言它的"答案",但你能统计它的"表现"。
它要干这几件事

第一步:攒一套"测试用例集",但期望不是标准答案,是"期望表现"
普通测试用例长这样:输入 → 期望输出。 Agent 测试用例得长这样:输入 → 期望意图 / 期望工具 / 期望关键字段 / 禁止行为。
- case_id: lang_switch_001
input: "把会议室那台大屏切换成英文"
expect:
intent: "设备控制·语言切换" # 期望识别出的意图
tool: ["resolveDeviceOrGroup", "setLanguage"] # 期望调用的工具
param_lang_normalized: "en_US" # 期望参数被归一成协议值
forbid: ["setVolume", "deleteDevice"] # 绝不能碰的工具
你看,我不要求它说哪句话,我只要求它干对事。表述随意,行为要对。这就绕开了"答案不唯一"的坑。
第二步:每个用例跑 N 遍,因为它是概率系统
一个用例跑一遍没意义——它这次对了不代表稳定。每个用例回放 10~20 遍,统计通过率。20 遍里对了 18 遍,这个数字才有意义。
这也是"准确率"这个词在 Agent 语境下的真实含义:不是"对不对",是"有多大比例对"。
第三步:判定靠"规则 + AI 裁判"双管齐下
模型输出千变万化,怎么判它"干对了"?
- 能规则的先规则:调没调对工具?参数是不是
en_US?这些是结构化的,直接断言,确定、快、便宜。 - 规则判不了的,上 LLM-as-Judge:回答得不得体?有没有答非所问?让另一个更强的模型当裁判打分。贵,但能处理语义。

第四步:按"关卡通过率"统计,而不是只看最终答案
这一步是关键中的关键,也是和我那个"精准防飘移"方案的衔接点。
我前面不是说 Agent 走六道确定性关卡嘛(意图边界、意图分诊、记忆隔离、表单填空、参数质检、高危确认)。那测试就该逐关统计通过率,而不是只盯着最终那句话对不对:
| 关卡 | 这关通过率 | 说明 |
|---|---|---|
| 意图边界 | 98% | 2% 的问题被它硬答了超纲内容 |
| 意图分诊 | 95% | 5% 的时候选错了业务工具集 |
| 记忆隔离 | 88% | ← 长会话在这里掉链子 |
| 表单填空 | 92% | 偶尔多填、少填字段 |
| 参数质检 | 85% | ← "中文/en_US"问题集中在这 |
| 高危确认 | 100% | 这关是工程兜底,稳 |
这样一拉,哪一关在漏水一目了然。比起一个笼统的"准确率 70%",这种分维度的诊断报告才有用——它直接告诉你该去修哪。
第五步:出一份分维度准确率报告
最后输出长这样:
========================================
Agent 黑匣子测试报告 · 2026-07-21
========================================
用例总数: 156
每例回放: 20 遍
总执行次数: 3120
整体准确率: 73.2% (目标 >95%,未达标 ❌)
分维度:
意图识别准确率: 91.5% ✓
工具选择准确率: 84.2% ⚠
参数填充准确率: 68.7% ❌ ← 重点优化
长会话(>15轮): 54.3% ❌ ← 越用越偏实锤
回归对比:
较上次: +2.1% (改了提示词,有提升)
========================================
有了这个东西,Agent 的质量才第一次变得"可度量、可对比、可回归"。
改了一版提示词?跑一遍,看准确率是涨了还是跌了。加了个新工具?跑一遍,别让它把别的工具带歪了。这才叫"测试",之前那只能叫"祈祷"。
测试之外:从"调优 AI"到"流程管控"
写到这,其实结论已经很清楚了。
Agent 难测的根子,不在于测试工具不够好,而在于 Agent 本身太自由了。一个什么都能答、什么工具都能调、参数随便填的黑匣子,你怎么测都测不稳——因为你测的是一匹脱缰的野马。
所以真正的解法有两层:
- 短期:用【黑匣子对话测试工具】把它现在的"野"程度量化出来,至少心里有数、能回归。
- 长期:别指望把马驯服,给它套上缰绳。用确定性的工程流程(意图边界、工具收敛、记忆隔离、表单填空、参数质检、高危确认),把模型的自由度压到最小,让它"想飘都没机会飘"。
这两层是配合的:流程管控把飘移从源头摁住,测试工具持续盯着还剩多少飘移。
飘移不是 AI 的错,是使用方式的错。换一种使用方式,才能根治。
最后
我之前一直觉得,AI 时代最难的是"开发"。做完这个项目我才明白,开发只是门票,测试才是主战场。
那些觉得"AI 能取代一切、Agent 随便做做就能上线"的人,麻烦你先回答一个问题:
你的 Agent 准确率是多少?你怎么测出来的?
答不上来,就别吹了。
一个准确率未知、跑偏率失控的 Agent 丢到生产环境,那不叫 AI 赋能,那叫给用户埋雷。
工具在进化,但度量工具的能力,永远比工具本身更稀缺。
能造出 Agent 的人一抓一大把,能说清楚"我的 Agent 到底有多靠谱"的人,凤毛麟角。
我希望自己是后者。
(【黑匣子对话测试工具】这玩意我已经在 physical-ai 里开搞了,等跑通了再写一篇实操的。这篇先把这些憋在心里的话吐为快。)
顺带提一句:如果你也在搞 Agent,别光开发爽。留点精力给测试,不然上线那天,破防的就是你了。Σ(っ °Д °;)っ
