news 2026/7/12 2:18:45

Agent测试到底怎么测?手写ReAct循环+工具调用链验证+决策路径断言,一套全跑通

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent测试到底怎么测?手写ReAct循环+工具调用链验证+决策路径断言,一套全跑通

先说结论

写LLM应用的人多,但认真给Agent写测试的人少。为什么?因为Agent测试比传统单元测试难一个量级——你要测的不只是"输入对不对、输出对不对",还要测"Agent选对工具了吗?传的参数对吗?调了几个工具?调用顺序对不对?多轮对话有没有记住上文?"

今天手写了4个Step、9个Java文件、106项断言,从零搭了一套Agent测试体系。0个Token、0次网络调用、毫秒级跑完。这篇文章把整个过程拆给你看,包括Mock方案、ReAct循环实现、多轮对话测试、决策路径断言,最后对比真实环境Function Calling的区别。

Agent测试为什么比RAG测试难?

先说清楚问题在哪。

传统单元测试:assertEquals(add(1,1), 2),确定性,同输入永远同输出。

RAG测试:输入"什么是RAG" → 检索文档 → 生成回答 → 检查幻觉率。虽然输出非确定,但流程是线性的——输入到输出一条路走到底。

Agent测试:输入"北京和上海温差多少" →

  • Agent决策:该调哪个工具?❓
  • Agent生成参数:传"北京"还是"Beijing"?❓
  • 工具执行:WeatherTool(“北京”) → “晴,32°C” ✅
  • Agent再决策:结果够了吗?要不要再查?❓
  • Agent再调:WeatherTool(“上海”) → “多云,28°C”
  • Agent再决策:够了,该算温差了 ❓
  • Agent调计算器:CalculatorTool(“32-28”) → “4.0”
  • Agent生成最终回复

三个不确定点:

  1. 工具选择——Agent选对工具了吗?该查天气时没调计算器吧?
  2. 参数生成——用户说"北京",工具收到的是"北京"不是"Beijing"吧?
  3. 调用顺序——先查再算,还是先算再查?顺序错了结果就错了。

RAG是直线,Agent有分支和循环。这就是测试难度跳了一个量级的原因。

测试策略:控制大脑,验证手脚

核心思路就一句话:用Mock控制LLM的"决策",用Recorder记录Agent的"行为",用断言验证两者是否一致。

你控制不了真实LLM每次返回什么,但在测试环境里,你可以预设LLM的决策序列。然后看Agent有没有正确执行这些决策。

MockChatModel预设决策 → Agent执行 → Recorder记录 → 断言验证 "大脑" "手脚" "摄像头"

这个思路和Spring Boot测试中Mock Service层是一脉相承的——你Mock掉外部依赖(LLM),测的是你的业务逻辑(Agent决策执行逻辑),不是模型智商。

Step 1:搭建可录制的工具体系

AgentTool接口

先定义工具接口。Agent为什么需要工具?因为LLM本身只会生成文本,不能查数据库、调API、执行计算。工具就是LLM的"手和脚"。

publicinterfaceAgentTool{StringgetName();StringgetDescription();Stringexecute(Stringinput);}

对比Spring的@Service——工具就是一个可被Agent调用的Service。区别在于:传统代码里你在Controller里写死调哪个Service,Agent里是LLM动态决定的。

ToolCallRecorder:工具调用的摄像头

这是整个测试体系的核心组件。包装Agent工具,记录每次调用的工具名、输入参数、输出结果、调用顺序。

publicclassToolCallRecorder{publicstaticclassToolCallRecord{publicfinalStringtoolName;publicfinalStringinput;publicfinalStringoutput;publicfinalintcallIndex;}// 5种断言方法publicbooleanassertCallCount(StringtoolName,intexpectedCount);publicbooleanassertTotalCalls(intexpected);publicbooleanassertToolCalledWith(StringtoolName,StringexpectedInput);publicbooleanassertCallOrder(String...expectedOrder);publicbooleanassertToolCalled(StringtoolName);}

设计模式是装饰器模式——RecordedTool包装原始工具,不改变行为只增加录制。和Spring AOP的@Around切面记录方法调用,思路一模一样。

三个具体工具

天气查询、计算器、代码分析。每个工具都是确定性的——同一个输入永远同一个输出。不确定性来自Agent的决策,不是工具本身。

跑4个场景:单工具调用、计算器调用、代码分析、多工具串联("北京和上海温差多少"需要2次天气+1次计算)。5项断言全通过。

Step 2:ReAct决策循环 + 工具调用链验证

Step 1是手动调工具,Step 2让Agent自动决策。

ToolCallAgent:最简ReAct实现

ReAct模式的核心:Think(LLM决策)→ Act(执行工具)→ Observe(看结果)→ Think…

publicStringexecute(StringuserInput){StringcurrentInput=userInput;while(iteration<maxIterations){StringllmDecision=llm.chat(currentInput);if(llmDecision.startsWith("CALL:")){// 解析工具名和参数,执行工具String[]parts=llmDecision.split(":",3);AgentTooltool=tools.get(parts[1].trim());Stringoutput=tool.execute(parts[2].trim());currentInput="工具返回: "+output;}elseif(llmDecision.startsWith("DONE:")){returnllmDecision.substring(5);}elseif(llmDecision.startsWith("DIRECT:")){returnllmDecision.substring(7);}}}

决策协议三种指令:

  • CALL:工具名:参数——调用工具
  • DONE:最终回复——结束循环
  • DIRECT:回复——不调工具,直接回复

4个场景测试:

  • 场景A:单工具调用(天气)——5/5断言通过
  • 场景B:不调工具直接回复(闲聊)——3/3通过
  • 场景C:多工具串联(温差=2次天气+1次计算)——9/9通过
  • 场景D:故意选错工具——2个断言失败(验证测试本身能抓到bug)

场景D是反向测试。用户问天气,Mock故意让Agent调CalculatorTool。断言"应该调WeatherTool"失败。这验证了测试本身是有效的——如果Agent真的选错工具,测试能抓到。

Step 3:多轮对话测试——指代消解+上下文累积

Step 2的Agent是一次性的,每次execute独立。Step 3加入对话历史。

多轮对话三个核心能力

  1. 指代消解——"那上海呢?"要理解为查上海天气
  2. 上下文累积——"温差多少"要从历史拿北京32°C和上海28°C
  3. 话题切换——"算了,帮我算1+1"要切到计算器,不追问天气

MultiTurnAgent:带记忆的Agent

privateStringbuildPromptWithContext(StringuserInput){if(history.isEmpty())returnuserInput;StringBuildersb=newStringBuilder();sb.append("[对话历史]\n");for(TurnRecordtr:history){sb.append(String.format("Turn %d: 用户=\"%s\" | 工具=%s | 回复=\"%s\"\n",tr.turn,tr.userInput,tr.toolCalls,tr.agentReply));}sb.append("[当前输入] ").append(userInput);returnsb.toString();}

每轮开始时,把历史拼进prompt给LLM。真实LLM用messages列表,这里简化为文本拼接。

三轮天气对话测试

Turn 1:用户问"北京天气怎么样" → Agent调WeatherTool(“北京”) → “北京32°C”
Turn 2:用户问"那上海呢" → Agent调WeatherTool(“上海”)(指代消解!Mock预设的决策"考虑"了历史) → “上海28°C”
Turn 3:用户问"温差多少" → Agent调CalculatorTool(“32-28”)(上下文累积!没调WeatherTool,因为数据在历史里) → “温差4°C”

35项断言全通过,包括:

  • 调用顺序验证:WeatherTool → WeatherTool → CalculatorTool
  • 参数验证:第一次"北京"、第二次"上海"(指代消解)、第三次"32-28"(上下文累积)
  • 历史传递验证:检查Mock的callLog,确认Turn 2的prompt包含Turn 1的历史

这个"检查prompt包含历史"的断言最有价值——它直接验证了上下文管理逻辑的正确性,不只是看结果对不对。

Step 4:决策路径断言——“结果对"不等于"路径对”

这是今天最深的一层。

Agent可能通过错误路径碰巧得到正确结果。比如用户问天气,Agent调了CalculatorTool(“北京”),CalculatorTool返回"格式错误",Agent换个工具再试,最后碰巧查到了天气。结果对了,但路径错了——如果测试只看结果,这个bug就漏了。

DecisionPathTracker:路径记录器

记录Agent执行过程中走了哪些节点,形成一条轨迹:

[1] 📥输入 北京天气怎么样 [2] 🧠决策 迭代#1 [3] 🔧工具 WeatherTool("北京") [4] 🧠决策 迭代#2 [5] ✅完成 北京今天晴,32°C。

路径签名:INPUT_RECEIVED->LLM_DECISION->TOOL_CALL->LLM_DECISION->DONE

不同输入走不同路径:

  • 天气查询:INPUT->LLM->TOOL->LLM->DONE
  • 闲聊:INPUT->LLM->DIRECT_REPLY
  • 错误:INPUT->LLM->ERROR

路径签名的价值:用于回归测试。改了代码后,如果路径签名变了=路径变了=可能有bug。不需要理解断言细节,看签名一眼就知道。

5个场景45项断言全通过,包括:

  • 单工具路径:验证5步路径顺序正确
  • 直接回复路径:验证3步路径,不包含TOOL_CALL
  • 多工具串联路径:验证9步路径,3次TOOL_CALL
  • 错误路径:验证3步路径,包含ERROR节点
  • 路径签名对比:三种输入走三条不同的路

Mock版 vs 真实环境:区别在哪?

这是最容易混淆的地方。

我们Mock的(教学版)

LLM返回文本字符串"CALL:WeatherTool:北京",我们自己写split解析、自己查Map找工具、自己调工具、自己写循环。

真实环境(Function Calling API)

LLM返回结构化JSON:

{"role":"assistant","content":null,"tool_calls":[{"id":"call_abc123","type":"function","function":{"name":"WeatherTool","arguments":"{\"city\": \"北京\"}"}}]}

LLM自己判断"该调工具了",返回tool_calls数组,指定工具名+JSON参数。

LangChain4j帮你封装了什么

真实写法:

classWeatherTools{@Tool("查询指定城市的天气")StringgetWeather(@P("城市名")Stringcity){return"晴,32°C";}}AiServices<MyAgent>agent=AiServices.builder(MyAgent.class).chatModel(realModel).tools(newWeatherTools()).build();Stringreply=agent.chat("北京天气怎么样");

@Tool注解自动生成工具描述给LLM,框架自动处理"LLM返回tool_calls → 解析JSON → 反射调用方法 → 把结果拼回messages → 再发给LLM"整个循环。你写的ToolCallAgent那个while循环,框架帮你做了。

那Mock的价值是什么?

  1. 理解Agent决策循环的本质——你手写了Think→Act→Observe→Think,知道框架内部在干什么,不是黑盒
  2. 测试管道逻辑——当LLM决定调WeatherTool时,你的代码有没有正确执行,这个逻辑Mock和真实都一样
  3. 0成本0网络——真实API每次调用烧Token,Mock跑1000次不花钱

真实环境的三种测试方案

方案思路适用场景
Mock HTTP层拦截OpenAI API的HTTP返回,模拟tool_calls JSON单元测试
本地小模型Ollama跑Qwen3-8B,真实走Function Calling集成测试
记录+回放真实调用一次录制响应,测试时回放回归测试

可能遇到的面试题

Q1:Agent测试和传统单元测试有什么区别?

传统单元测试测的是确定性——同输入永远同输出。Agent测试有三个不确定点:工具选择、参数生成、调用顺序。所以Agent测试的核心策略是Mock LLM的决策,验证Agent的执行——控制大脑,验证手脚。

Q2:为什么Mock LLM而不是直接调真实API?

三个原因:速度(Mock毫秒级,真实API几秒)、成本(Mock免费,真实API烧Token)、稳定性(Mock确定性,真实LLM每次返回不同)。测试要快、要稳、要可重复。

Q3:Agent测试的三个层次是什么?

工具选择正确性(选对工具了吗)、参数生成正确性(传对参数了吗)、调用顺序正确性(先查再算还是先算再查)。对应ToolCallRecorder的assertToolCalled、assertToolCalledWith、assertCallOrder三个断言。

Q4:多轮对话测试怎么验证指代消解?

用户Turn 2说"那上海呢",断言WeatherTool的输入参数是"上海"。如果Agent没有正确理解指代,参数可能是null或"那上海呢"原样传入。同时检查Mock的callLog,确认Turn 2的prompt包含了Turn 1的历史。

Q5:什么是路径签名?有什么用?

把Agent的执行路径转成节点类型序列字符串,如INPUT->LLM->TOOL->LLM->DONE。用于回归测试——改了代码后如果签名变了,说明Agent走的路径变了,可能有bug。不需要理解每条断言的细节,看签名一眼就知道路径有没有变。

Q6:Mock版和真实Function Calling的区别?

Mock版LLM返回文本"CALL:Tool:param",自己解析。真实版LLM返回结构化tool_callsJSON,框架自动解析。Mock的价值在于理解Agent决策循环的本质——你手写了框架内部做的事,不是黑盒。

Q7:决策路径测试和工具调用记录有什么区别?

ToolCallRecorder只记录工具调用——调了什么、参数是什么、顺序对不对。DecisionPathTracker记录整个执行过程——包括LLM决策节点、错误节点、直接回复节点。“结果对不等于路径对”——Agent可能通过错误路径碰巧得到正确结果,路径断言能抓到这类问题。

全部代码文件一览

文件Step用途
AgentTool.java1Agent工具接口
ToolCallRecorder.java1工具调用记录器(5种断言)
AgentTestingConceptDemo.java1概念演示(4场景5断言)
ToolCallAgent.java2最简ReAct实现
AgentToolCallTestDemo.java2工具调用链验证(4场景17断言)
MultiTurnAgent.java3带对话历史的Agent
MultiTurnConversationTestDemo.java3多轮对话测试(3场景35断言)
DecisionPathTracker.java4决策路径追踪器
DecisionPathTestDemo.java4决策路径断言(5场景45断言)

三个核心设计模式贯穿始终:装饰器模式(RecordedTool包装工具)、责任链模式(Agent决策循环)、策略模式(不同输入走不同路径)。

总结

Agent测试的精髓:控制大脑,验证手脚,回放路径

  • 控制大脑:MockChatModel预设LLM的决策序列
  • 验证手脚:ToolCallRecorder记录并断言Agent的实际行为
  • 回放路径:DecisionPathTracker生成路径签名,用于回归测试

106项断言,0个Token,0次网络调用,毫秒级跑完。这就是Mock测试的价值——不花一分钱,测到Agent每一条逻辑分支。

下一篇我们聊LLM-as-a-Judge——用强模型评估弱模型输出。这是Agent测试体系从"Mock版"走向"真实版"的关键一步。


欢迎关注,下周继续更新LLM应用开发学习笔记。有问题评论区聊。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/12 2:18:13

Java开发者转型AI应用开发:从大模型集成到工程化实战指南

对于已经具备 Java 后端开发经验的工程师来说&#xff0c;转型 AI 应用开发最大的优势不是从零学习编程&#xff0c;而是如何将已有的工程化能力、系统设计经验和问题解决思路平移到 AI 技术栈中。真正需要补足的是对大模型工作原理的理解、Prompt 工程技巧、RAG 系统构建和 Ag…

作者头像 李华
网站建设 2026/7/12 2:15:47

2026年超全企业文件共享平台推荐!中小企业适配方案实测

一、前言&#xff1a;企业文件共享的核心痛点在企业数字化办公普及的当下&#xff0c;文件传输、存储与协作是团队日常办公的核心刚需。但目前绝大多数企业都面临着统一的文件管理难题&#xff1a;微信/QQ传文件混乱无溯源、U盘传输效率极低、普通个人网盘无权限管控、大文件传…

作者头像 李华
网站建设 2026/7/12 2:13:04

随机抽样与数据分析实战:从基础原理到Python实现

在数据科学和统计学入门阶段&#xff0c;随机抽样与简单的数据分析是两项最基础也最核心的技能。很多初学者虽然知道这两个概念&#xff0c;但在实际项目中却常常混淆抽样方法的选择依据&#xff0c;或者做完描述性统计后不知道下一步该分析什么。本文将以一个完整的案例&#…

作者头像 李华
网站建设 2026/7/12 2:11:35

LZ77 算法 C 语言实现:滑动窗口 4096 字节,压缩率实测 35%

LZ77算法C语言实现&#xff1a;4096字节滑动窗口与35%压缩率实战解析1. 理解LZ77算法的核心机制LZ77算法作为字典编码的奠基之作&#xff0c;其核心在于滑动窗口机制的巧妙设计。当我们处理字符串"AABCBBABC"时&#xff0c;算法会将数据划分为两个关键区域&#xff1…

作者头像 李华
网站建设 2026/7/12 2:10:25

SVN 版本冲突实战:2种合并策略与3个降低冲突的团队规范

SVN版本冲突实战&#xff1a;2种合并策略与3个降低冲突的团队规范 在团队协作开发中&#xff0c;版本控制系统是必不可少的工具。作为集中式版本控制的代表&#xff0c;SVN&#xff08;Subversion&#xff09;至今仍在许多企业中被广泛使用。然而&#xff0c;随着团队规模的扩大…

作者头像 李华