先说结论写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生成最终回复三个不确定点工具选择——Agent选对工具了吗该查天气时没调计算器吧参数生成——用户说北京工具收到的是北京不是Beijing吧调用顺序——先查再算还是先算再查顺序错了结果就错了。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里写死调哪个ServiceAgent里是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 2ReAct决策循环 工具调用链验证Step 1是手动调工具Step 2让Agent自动决策。ToolCallAgent最简ReAct实现ReAct模式的核心ThinkLLM决策→ Act执行工具→ Observe看结果→ Think…publicStringexecute(StringuserInput){StringcurrentInputuserInput;while(iterationmaxIterations){StringllmDecisionllm.chat(currentInput);if(llmDecision.startsWith(CALL:)){// 解析工具名和参数执行工具String[]partsllmDecision.split(:,3);AgentTooltooltools.get(parts[1].trim());Stringoutputtool.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加入对话历史。多轮对话三个核心能力指代消解——那上海呢要理解为查上海天气上下文累积——温差多少要从历史拿北京32°C和上海28°C话题切换——算了帮我算11要切到计算器不追问天气MultiTurnAgent带记忆的AgentprivateStringbuildPromptWithContext(StringuserInput){if(history.isEmpty())returnuserInput;StringBuildersbnewStringBuilder();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 APILLM返回结构化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;}}AiServicesMyAgentagentAiServices.builder(MyAgent.class).chatModel(realModel).tools(newWeatherTools()).build();Stringreplyagent.chat(北京天气怎么样);Tool注解自动生成工具描述给LLM框架自动处理LLM返回tool_calls → 解析JSON → 反射调用方法 → 把结果拼回messages → 再发给LLM整个循环。你写的ToolCallAgent那个while循环框架帮你做了。那Mock的价值是什么理解Agent决策循环的本质——你手写了Think→Act→Observe→Think知道框架内部在干什么不是黑盒测试管道逻辑——当LLM决定调WeatherTool时你的代码有没有正确执行这个逻辑Mock和真实都一样0成本0网络——真实API每次调用烧TokenMock跑1000次不花钱真实环境的三种测试方案方案思路适用场景Mock HTTP层拦截OpenAI API的HTTP返回模拟tool_calls JSON单元测试本地小模型Ollama跑Qwen3-8B真实走Function Calling集成测试记录回放真实调用一次录制响应测试时回放回归测试可能遇到的面试题Q1Agent测试和传统单元测试有什么区别传统单元测试测的是确定性——同输入永远同输出。Agent测试有三个不确定点工具选择、参数生成、调用顺序。所以Agent测试的核心策略是Mock LLM的决策验证Agent的执行——控制大脑验证手脚。Q2为什么Mock LLM而不是直接调真实API三个原因速度Mock毫秒级真实API几秒、成本Mock免费真实API烧Token、稳定性Mock确定性真实LLM每次返回不同。测试要快、要稳、要可重复。Q3Agent测试的三个层次是什么工具选择正确性选对工具了吗、参数生成正确性传对参数了吗、调用顺序正确性先查再算还是先算再查。对应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。不需要理解每条断言的细节看签名一眼就知道路径有没有变。Q6Mock版和真实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带对话历史的AgentMultiTurnConversationTestDemo.java3多轮对话测试3场景35断言DecisionPathTracker.java4决策路径追踪器DecisionPathTestDemo.java4决策路径断言5场景45断言三个核心设计模式贯穿始终装饰器模式RecordedTool包装工具、责任链模式Agent决策循环、策略模式不同输入走不同路径。总结Agent测试的精髓控制大脑验证手脚回放路径。控制大脑MockChatModel预设LLM的决策序列验证手脚ToolCallRecorder记录并断言Agent的实际行为回放路径DecisionPathTracker生成路径签名用于回归测试106项断言0个Token0次网络调用毫秒级跑完。这就是Mock测试的价值——不花一分钱测到Agent每一条逻辑分支。下一篇我们聊LLM-as-a-Judge——用强模型评估弱模型输出。这是Agent测试体系从Mock版走向真实版的关键一步。欢迎关注下周继续更新LLM应用开发学习笔记。有问题评论区聊。