给Agent写测试-工具调用的回归怎么保
给 Agent 写测试:工具调用的回归怎么保
改了两句提示词,昨天还好好的 Agent 今天开始乱调工具。想写测试,可 Agent 的输出是模型生成的,每次都不一样,断言写什么?这是所有写 Agent 的人迟早撞上的墙。答案是把「测行为」和「测文字」分开:行为可以断言,文字不必。三层防线把这件事说清楚。
三层防线
第一层:工具单测
工具是普通的 Java 方法,入参出参确定,单元测试原样适用,这一层没有任何新东西。@Tool只是便签,测试测的是方法本身。把工具写成纯函数(不依赖模型、不依赖上下文),这层测试就是免费的。
第二层:循环集成测
这一层是 Agent 测试的核心。思路是把不确定的那头(模型)换成确定的脚本,只测你的循环和工具协作是否正确。Mock 模型返回预设的 tool_use,你的循环照常解析、执行、回填,然后断言三件事。
该调的工具被调了:用户问天气,就必须调 get_weather,不能调 get_user。
参数是对的。city 的值是「北京」,不是「上海」。
回填是对的。tool_result 的配对 ID 与 tool_use 对上,格式没散。
文字不必断言相等,模型的措辞会变,行为不该变。这就是「测行为不测文字」的分界,记住一句话:断言工具的调用,不要断言模型的措辞。
第三层:评测集
攒一批固定的问答对(问题、期望的行为、期望的工具调用),每次改提示词或换模型就全量跑一遍,输出行为快照并与上次对比。退没退步有数据说话,改动才敢合并。提示词和评测集要一起版本化,它们是一对,就像代码和测试。
评测集的质量比数量重要,二十个覆盖典型场景的用例,胜过两百个重复的闲聊。
亲手跑
下面用一段 Java 写一个自带断言的回归演示,mock 模型返回预设工单,跑完整循环,检查工具调用。断言不过直接抛异常,这就是一个最简测试运行器。直接运行java AgentRegression.java(JDK 16+,用了 record 语法)。
// 直接运行:java AgentRegression.java// agent 回归测试:mock 模型驱动循环,断言工具行为。importjava.util.ArrayList;importjava.util.List;publicclassAgentRegression{recordToolCall(Stringid,Stringname,Stringargs){}staticclassToolTracker{finalList<ToolCall>calls=newArrayList<>();Stringexecute(ToolCallcall){calls.add(call);returnswitch(call.name()){case"get_weather"->call.args()+",晴,25 度";default->thrownewIllegalStateException("不该被调的工具:"+call.name());};}}// mock 模型:按脚本返回工单,替掉不确定的那头staticToolCallmockModel(StringuserMessage){if(userMessage.contains("天气")){returnnewToolCall("tu_1","get_weather","北京");}returnnull;}staticvoidrunLoop(StringuserMessage,ToolTrackertracker){ToolCallcall=mockModel(userMessage);if(call!=null){Stringresult=tracker.execute(call);// 工单与回执的配对在真实实现里靠 idSystem.out.println("回填 tool_result:"+result);}System.out.println("生成最终回答(略)。");}staticvoidassertTrue(booleanok,Stringmessage){if(!ok){thrownewAssertionError("回归失败:"+message);}}publicstaticvoidmain(String[]args){ToolTrackertracker=newToolTracker();runLoop("北京今天多少度?",tracker);// 断言行为三件事assertTrue(tracker.calls.size()==1,"应该恰好调用一次工具,实际 "+tracker.calls.size());assertTrue(tracker.calls.get(0).name().equals("get_weather"),"应该调 get_weather");assertTrue(tracker.calls.get(0).args().contains("北京"),"参数里应该有北京");System.out.println("回归通过:该调的调了,参数对了。");// 反例:改坏的提示词让模型调错工具,断言立刻抓住ToolTrackerbad=newToolTracker();bad.calls.add(newToolCall("tu_2","delete_user","北京"));try{assertTrue(bad.calls.get(0).name().equals("get_weather"),"应该调 get_weather,实际 "+bad.calls.get(0).name());}catch(AssertionErrore){System.out.println("反例被抓住了:"+e.getMessage());}}}适用边界
这一篇管回归的骨架。LLM-as-judge(用模型给模型打分)、线上的行为监控与告警、以及评测集的自动构造,是另外的题。Mock 模型的脚本要随真实行为更新,它模拟的是「模型会怎么写工单」,模型升级时记得同步。跑完把 Demo 的断言抄进你的 CI,从今天起,改提示词之前先跑回归,改完心里有底。