Agent开发Easy,But测试Max

乐云一
  • 不止所云
  • 杂谈
  • AI
About 4160 wordsAbout 14 min

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 你怎么写?

你写不出来。因为:

  1. 答案不唯一:同样是对的,表述千变万化,你没法穷举所有"正确答案"。
  2. 它会飘:哪怕你把"正确答案"全列出来,它偶尔还会给你一个完全不沾边的。
  3. "对"的标准是模糊的:第二次回答算对吗?它没说"切换"俩字,但意思到了。第三次对了但啰嗦。这怎么打分?

这就是 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,它给你 中文Englishenglish……你拿这个去断言?断言个锤子。

点名: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,别光开发爽。留点精力给测试,不然上线那天,破防的就是你了。Σ(っ °Д °;)っ

Last update:
Contributors: LeYunone
Comments
  • Latest
  • Oldest
  • Hottest
Powered by Waline v2.14.7