1. 为什么Agent Harness测试不能靠黑盒
做AI Agent项目也有段时间了,最深的感触是:Agent的入场门槛是真低,但把它做稳做可靠的难度全在后半程。尤其是Agent Harness Engineering——这套承载Agent运行的工作台、执行循环、工具机制、记忆机制的工程骨架——很多时候决定了整个系统的上限。模型是Agent的“大脑”,Harness就是承载大脑的“驾驶舱”。可我观察下来,很多人对“驾驶舱”到底靠不靠谱,几乎没有系统性的验证手段。
所谓Harness,直译是“马具、挽具”,在Agent工程里被引申为“让Agent稳定跑起来的整套框架”。它不负责输出智能,但负责让智能稳定地工作。工具调用循环、上下文管理、记忆读写、结果校验、异常恢复,这些都属于Harness的范畴。问题是,大多数团队测试Agent的方式还是端到端黑盒:给定一句话,看回复像不像样。这种测法不是没用,而是太粗。模型输出千变万化,你很难判断一次失败到底是模型能力问题,还是Harness处理逻辑的问题。
我自己的答案,是把测试下沉到Harness内部去做白盒测试:从单元测试到集成测试,把每个分支、每个状态转换、每个调用参数都变成可验证的断言。这篇文章就是我实践的完整方案,适合正在做Agent开发、想搭建测试体系,或者准备Agent相关工程面试的朋友。你看完可以直接套用里面的框架和用例设计思路,不用再从零踩坑。
1.1 先认清Agent Harness的“身体结构”
要测一个东西,首先得知道它由什么组成。我接手过的Agent项目里,Harness的代码结构无论怎么包装,最终都逃不开下面五层:
- 接口与协议层:对外暴露的API、消息格式转换、鉴权逻辑。
- 编排与控制层:Agent的主循环。模型返回之后下一步该做什么,是调工具、结束、还是重试。
- 工具与动作层:注册了哪些工具、工具参数怎么校验、执行结果如何返回给模型。
- 状态与记忆层:多轮对话上下文、内置记忆、任务进度状态。
- 模型调用层:请求组装、token预算控制、超时与重试。
这五层加在一起就是Agent Harness。模型本身是黑盒,你没法预测它下一句生成什么;但Harness不是黑盒,它每一行都是你写的代码,走哪些分支、什么时候返回、什么条件抛异常,理论上全部可判断、可验证。
有意思的是,很多团队的测试恰恰避开了这些可控代码,只盯着不可控的模型输出。测试用例全是“让Agent写一首诗,然后判断输出像不像诗”。这类用例一次能过,一百次可能挂三十次,因为模型输出漂移了。真正该测的——工具参数有没有正确传给执行器、循环会不会卡死、上下文会不会被截断——反而没人管。
白盒测试的核心思路,是看到内部结构,为内部结构设计用例。对Agent来说,内部结构就是上面这五层。你能在代码级别看清楚每个模块的输入输出契约,测试才有落脚点。
1.2 白盒测试的边界:把非确定性“关起来”
这里有一个绕不开的问题:LLM输出是非确定性的。你没法保证模型每次返回的tool_call JSON都合法,也不能断言它生成的文本一定等于某个字符串。如果直接拿真实模型当测试输入,那测试本身也会变得不稳定。
解决思路是把非确定性“隔离”在边界之外。具体来说,在单元测试阶段用假模型客户端替换真实模型,把模型输出当作可控制的测试输入。这样做有一个本质转变:测试对象从“模型”变成了“Harness”。模型不是我们要测的东西,Harness才是。Harness面对模型输出时的各种处理逻辑,恰好是白盒测试最擅长覆盖的场景:
- 模型返回了一段非法JSON,解析层是否报错?错误信息是否可理解?
- 模型返回的tool_call里缺参数,工具层是拒绝执行还是尝试补默认值?
- 模型反复请求同一个工具,最大步数限制有没有生效?
- 上下文超长时,截断策略是否保住了system prompt和当前用户输入?
这些用例的共同特点是:给特定输入,验证特定分支,得到精确结论。这正是白盒测试能提供“确定性答案”的地方,也是黑盒测试永远给不了的东西。
我曾在生产环境遇到过一个很隐蔽的bug:模型返回的tool_call多嵌套了一层object,参数校验时抛了TypeError,重试逻辑把它当成模型异常重试了三次,三次之后整个请求失败。这个bug用黑盒端到端测试极难复现,但白盒单测只需要mock一个嵌套结构的返回,五分钟就能让它现形。
1.3 一个可落地的分层测试策略
把Harness拆开之后,整个测试策略可以分层设计,每一层负责不同的深度和成本:
| 测试层级 | 测试对象 | LLM状态 | 重点验证内容 |
|---|---|---|---|
| 单元测试 | 工具层、状态层、提示词层、循环控制 | 完全mock | 分支覆盖、参数校验、错误处理 |
| 半集成测试 | Harness+真实或假的单侧组件 | 真实LLM或实时mock | 编排逻辑、解析路由、副作用 |
| 端到端测试 | 完整Harness+真实依赖 | 真实LLM或录制回放 | 完整生命周期、外部依赖协作 |
单元测试追求快和准,跑完整套不能超过几秒;半集成测试追求场景真实性,允许慢一点,也允许用录制回放保证稳定;端到端测试数量要少,只覆盖最核心的两三条用户链路。
这个金字塔和普通后端测试最大的区别,是中间两层被放到了非常高的位置。为什么?因为对Agent来说,“编排逻辑”本身就是核心业务,是连接模型智能和外部动作的桥梁。这一层如果只靠端到端去碰运气,那上线之后出问题几乎是必然的。
2. 单元测试:把大模型mock掉,专测Harness骨架
单元测试是白盒的主力军,但很多人一上来就被异步、mock、fixture搞得头大。别急,先解决一个前置问题:你的Harness代码能不能被测试?我见过大量Agent项目没法做白盒测试,根源不是测试不会写,而是类内部把模型客户端、registry、memory全部new死在构造函数里,外部根本没有注入点。
所以第一步,是把边界做干净。
2.1 先搭一个可测试的Harness骨架
下面这个例子我做了大量简化,但保留了真实Harness的核心执行逻辑。你可以在它基础上扩展prompt缓存、流式输出、事件回调,结构不变。
# harness.py class ToolSpec: def __init__(self, name: str, handler, parameters: dict | None = None): self.name = name self.handler = handler self.parameters = parameters or {} class AgentLoopLimitError(Exception): pass class AgentHarness: def __init__(self, model_client, tool_registry, memory, max_steps=5): self.model = model_client self.tools = tool_registry self.memory = memory self.max_steps = max_steps async def run(self, user_input: str) -> str: messages = await self.memory.load() messages.append({"role": "user", "content": user_input}) for step in range(self.max_steps): response = await self.model.chat(messages) action = self._parse_action(response) if action["type"] == "final": return action["content"] spec = self.tools.get(action["name"]) if spec is None: messages.append({ "role": "system", "content": f"工具 {action['name']} 不存在,请更换工具" }) continue try: result = await spec.handler(**action["arguments"]) except Exception as exc: result = f"工具执行失败: {exc}" messages.append({ "role": "tool", "name": spec.name, "content": str(result) }) raise AgentLoopLimitError(f"超过最大步数 {self.max_steps}") @staticmethod def _parse_action(response: dict) -> dict: if response.get("type") == "final": return {"type": "final", "content": response["content"]} if response.get("type") == "tool_call": name = response.get("name") args = response.get("arguments") or {} return {"type": "tool_call", "name": name, "arguments": args} raise ValueError(f"无法识别的响应: {response}")这套骨架里包含三个对“可测试性”至关重要的设计:
- 依赖注入。model_client、tool_registry、memory全部通过构造函数传入,没有在内部直接new。单测时把真实对象替换成假对象,不需要改任何业务代码。
- 纯逻辑与IO分离。
_parse_action是纯函数,输入一个dict,输出一个结构化的action,可以单测直接调用。model.chat和handler是IO边界,只在集成测试阶段用真实实现。 - 显式循环上限。max_steps是配置参数而不是魔法数字,测试时可以传一个很小的值,快速触发
AgentLoopLimitError。
2.2 工具注册与参数校验的测试
工具层是Agent最容易出问题的地方。模型说“我要查北京天气”,Harness要把这个意图精确翻译成get_weather(city="北京")的调用,中间错一步整个对话就断了。
工具层的单元测试通常覆盖这几类场景:
- 工具注册:同名工具重复注册会不会冲突;注册后能否正确获取。
- 参数校验:参数缺一个、类型不对、多传未声明参数,分别怎么处理。
- 执行器:工具正常返回、抛异常、超时,Harness如何把结果回传给模型。
- 结果归一化:工具返回dict、字符串、空值,消息格式会不会被破坏。
先准备两个基础的假对象:
class FakeModel: def __init__(self, responses): self.responses = list(responses) self.request_log = [] async def chat(self, messages): self.request_log.append(messages) return self.responses.pop(0) class FakeMemory: def __init__(self): self.messages = [] async def load(self): return list(self.messages) async def append(self, message): self.messages.append(message)然后写一个最关键的用例:模型调用了一个不存在的工具,Harness应该把纠正信息回传给模型,而不是直接崩溃。
import pytest async def test_unknown_tool_returns_system_feedback(): fake_model = FakeModel([ {"type": "tool_call", "name": "not_exists", "arguments": {}}, {"type": "final", "content": "好的,那我换个方式"}, ]) harness = AgentHarness( model_client=fake_model, tool_registry={}, memory=FakeMemory(), max_steps=3, ) result = await harness.run("帮我处理一下") assert result == "好的,那我换个方式" assert fake_model.request_log[1][-1]["role"] == "system" assert "不存在" in fake_model.request_log[1][-1]["content"]注意最后的断言:我们检查了第二次发给模型的消息列表里,最后一条是system角色且包含“不存在”。这就是典型的白盒断言——不仅验证最终答案,还验证Harness内部状态变化是否合理。这种断言是黑盒测试写不出来的。
再看一个工具参数传递的用例:
async def test_tool_call_receives_parsed_arguments(): collected = {} def get_weather(city, unit="celsius"): collected["city"] = city collected["unit"] = unit return "晴,26度" registry = { "get_weather": ToolSpec(name="get_weather", handler=get_weather) } fake_model = FakeModel([ {"type": "tool_call", "name": "get_weather", "arguments": {"city": "北京"}}, {"type": "final", "content": "北京晴天,26度"}, ]) harness = AgentHarness(fake_model, registry, FakeMemory(), max_steps=3) result = await harness.run("北京天气怎么样") assert result == "北京晴天,26度" assert collected["city"] == "北京" assert collected["unit"] == "celsius"这里的关键是验证arguments里的JSON字段被正确展开成了Python函数的关键字参数,并且默认参数生效了。我见过很多线上事故就是在这一步出问题:模型传了city,Harness却把city塞给了别的参数,最后调用了一个风马牛不相及的工具。
2.3 提示词组装与上下文窗口的测试
提示词组装往往被当成“字符串拼接”看待,but它的质量直接影响模型输出和token消耗。白盒测试在这一层能验证的东西非常多:
- 模板变量是否正确替换,缺变量时有没有静默失败。
- system prompt、工具描述、历史消息、用户输入在消息数组里的顺序是否稳定。
- 估算token超长时,截断策略是否生效,截断的是历史消息而不是system prompt和当前输入。
- 不同模型对消息格式的兼容性,比如有些模型不认tool角色,需要做格式转换。
一个可测试的PromptBuilder大概是这样的:
class PromptBuilder: def __init__(self, system_template: str, max_tokens: int = 2000): self.system_template = system_template self.max_tokens = max_tokens def build(self, history, user_input, tools_desc): messages = [ {"role": "system", "content": self.system_template.replace("{{tools}}", tools_desc)} ] budget = self.max_tokens - estimate_tokens(user_input) for msg in reversed(history): cost = estimate_tokens(msg["content"]) if budget - cost < 0: break messages.insert(1, msg) budget -= cost messages.append({"role": "user", "content": user_input}) return messages对应的单元测试可以这样写:
def test_prompt_builder_truncates_history_not_system(): builder = PromptBuilder( system_template="你是助手,可用工具:{{tools}}", max_tokens=120, ) history = [ {"role": "user", "content": "x" * 50}, {"role": "assistant", "content": "y" * 50}, ] messages = builder.build(history, "今天天气", "get_weather") assert messages[0]["role"] == "system" assert messages[-1]["content"] == "今天天气" assert "{{tools}}" not in messages[0]["content"] assert total_tokens(messages) <= 120这类测试的价值在于:它能保证你的prompt工程不是“拍脑袋调参”,每一次改动都有回归保护。我见过一个团队,优化prompt时不小心把system prompt里的工具描述删掉了,结果Agent忽然不会调用工具了,排查了大半天才发现是prompt模板问题。如果有一个像上面这样的测试,这个问题在发布前就会被拦住。
2.4 循环与状态转换的测试
Harness循环是整个Agent运行的心脏:模型返回一个action,Harness判断是继续还是结束。循环控制有几个必测的场景:
- 正常链条:工具调用 -> 结果回传 -> 再次调用模型 -> final -> 返回结果。
- 工具不存在:模型调用了未注册工具,Harness回传提示让模型纠正。
- 循环不退出:模型反复调用工具不返回final,max_steps触发后抛异常。
- 工具抛异常:异常被捕获后转成给模型的提示信息,而不是让整个请求崩掉。
上面已经提过工具不存在的情况,这里补一个max_steps的用例:
async def test_max_steps_limit_raises(): fake_model = FakeModel( [{"type": "tool_call", "name": "loop", "arguments": {}}] * 5 ) registry = { "loop": ToolSpec(name="loop", handler=lambda: "again") } harness = AgentHarness(fake_model, registry, FakeMemory(), max_steps=3) with pytest.raises(AgentLoopLimitError): await harness.run("开始循环")这个用例虽然只有几行,但它用一个很小的max_steps快速验证了Agent不会无限循环。很多线上故障——比如Agent自己和自己对话直到token耗尽——就是败在这一行逻辑上。
我在项目里把这几个核心用例全部堆到上百个,覆盖工具层、循环控制、上下文管理,跑一次几秒钟。这块后来成了整个Agent系统里最稳固的部分,后续迭代代码时的信心完全是被这些用例托住的。
3. 集成测试:让真实组件开始“碰头”
单元测试把每个组件都隔离测了一遍,但组件之间一碰面,往往又出新问题。集成测试就是要回答“它们协作时契约是否正确”。
3.1 先想清楚:集成测试里哪些用真的,哪些用假的
集成测试最容易犯的错,是把所有组件全换成真的,然后跑端到端。这样既慢又不稳定,出了问题还很难定位。我的做法是分层替换,每次只放开一个“真实变量”:
| 模型 | 工具 | 场景 | 验证重点 |
|---|---|---|---|
| Mock | Mock | 单元测试 | Harness逻辑 |
| Mock | 真 | 半集成A | 工具副作用、重试、幂等 |
| 真 | Mock | 半集成B | 真实模型输出到工具路由的解析链路 |
| 真 | 真 | 端到端 | 完整生命周期、外部依赖 |
为什么中间两档很重要?因为真实模型和真实工具各自都会带来不确定性,如果一次全放开,出问题你很难判断是模型理解错了、工具执行错了,还是Harness编排错了。一次只换一个变量,问题定位会清晰得多。
3.2 真LLM+假工具:验证解析与路由的稳定性
这一层的核心价值是:用真实模型输出暴露出mock永远发现不了的问题。比如模型对工具名称的表达方式千奇百怪,可能叫“get_weather”,也可能在参数里塞进一个文档里根本没写的额外字段,也可能把枚举值理解错了。这些只有真实模型跑一遍才能看到。
具体做法是:Harness用真实模型,工具用stub,stub负责记录收到的参数并返回固定结果。然后跑一个查询类任务,验证模型是否成功把意图路由到了正确的工具。
async def test_real_model_routes_weather_query_to_tool(): model = OpenAIModel(model_name="gpt-4o-mini") got_city = [] async def fake_weather(city: str): got_city.append(city) return "晴,26度" registry = { "get_weather": ToolSpec( name="get_weather", handler=fake_weather, parameters={ "type": "object", "properties": {"city": {"type": "string"}}, }, ) } harness = AgentHarness(model, registry, FileMemory(), max_steps=5) result = await harness.run("北京适合出门吗?") assert got_city, "真实模型应该成功调用天气工具" assert "26" in result or "晴" in result注意这层测试的断言要尽量“宽”。不要去精确断言最终回复的文本,因为模型换个说法就会挂。更合理的做法是:断言关键工具确实被调用了、关键信息确实出现在回复里。
还有一个实操细节:这层测试强烈建议加一次自动重试。因为真实模型偶尔会抽风,一次没调用工具不代表代码有问题,可能是采样概率导致的。加一次重试能显著降低测试的偶发失败率,又不影响它对“路由逻辑”的验证。
3.3 假LLM+真工具:验证副作用与故障恢复
另一种半集成是反过来:模型是假的,但工具是真的。这样我们精确控制模型“接下来要做哪一步”,同时验证工具执行的真实副作用。
这个场景特别适合测三类问题:
第一,真实副作用。比如一个预订类工具真的往staging数据库插了一条记录。我们可以用假模型连续给出两个tool_call,第二次调用基于第一次的真实返回值,然后断言数据库里确实多了一条订单,参数完全正确。
第二,失败重试。工具第一次抛异常,Harness把异常信息回传给模型,假模型第二次输出正确的参数,工具调用成功。这验证的是“Harness能否把工具异常转化为对模型友好的错误信息”。
async def test_tool_exception_is_passed_back_to_model(): calls = [] async def flaky_api(param: str): calls.append(param) if len(calls) == 1: raise RuntimeError("上游超时") return "ok-200" fake_model = FakeModel([ {"type": "tool_call", "name": "call_api", "arguments": {"param": "A"}}, {"type": "tool_call", "name": "call_api", "arguments": {"param": "A"}}, {"type": "final", "content": "已重试成功"}, ]) harness = AgentHarness( fake_model, {"call_api": ToolSpec(name="call_api", handler=flaky_api)}, FakeMemory(), max_steps=5, ) result = await harness.run("执行") assert result == "已重试成功" assert len(calls) == 2 assert "超时" in fake_model.request_log[1][-1]["content"]这个用例的最后一行的价值极高。它验证了Harness把异常包装成消息回传给模型之后,模型能看到具体的错误原因。如果这里实现有误,比如错误信息没被附加到消息列表里,那模型永远只能看到“工具调用失败”这种模糊提示,纠错能力会大打折扣。
第三,幂等性。用同一个参数调用工具两次,断言结果一致,且没有额外的副作用(比如重复扣费、重复插记录)。这类问题在Agent场景特别隐蔽,因为模型可能会因为一次网络抖动就重复发起同一个工具调用。
3.4 端到端集成:完整生命周期的确认
端到端测试数量不用多,两三条核心链路即可。我的建议是覆盖“查询+追问”“多工具协作”“上下文超长后的恢复”这三类代表性场景。
端到端最大的问题是稳定性。真实模型加真实外部API,跑一次不仅慢,还可能因为上游故障、限额、网络波动而挂掉。这里推荐用VCR录制回放方案:第一次跑测试时把真实模型响应和外部API响应录制下来,存成cassette文件;之后测试回放录制数据,不再发真实请求。
@vcr.use_cassette("cassettes/weather_agent.yaml") async def test_e2e_weather_agent(): model = OpenAIModel(model_name="gpt-4o-mini") registry = build_real_registry() # 接入真实天气API harness = AgentHarness(model, registry, FileMemory(), max_steps=5) result = await harness.run("上海明天会下雨吗?") assert "雨" in result or "晴" in result第一次跑这个用例时,vcrpy会自动录下所有HTTP请求和响应;之后跑就纯本地回放。这样既保留了端到端的“真实脚本”,又拿到了单元测试级别的稳定性。
端到端还有个不可忽略的前提:独立的测试环境。独立API key配额、独立数据库、独立存储目录,绝不能拿生产数据来测。这个原则我踩过坑后才真正刻进脑子。
4. 落地过程中的常见问题与避坑实录
方案说起来一套一套,真正落地时到处是坑。这一节我把踩过得比较多的几个问题集中说下,基本可以当速查表用。
4.1 断言太严格、太脆弱
刚做Agent测试时,我犯过最大的错是把黑盒时代的习惯带进来:总觉得“这轮对话应该回复什么”。结果模型换了个表达方式,测试就挂,五个用例挂三个,最后整个测试集形同虚设。
后来总结出一个原则:对LLM输出做语义级断言,对Harness内部数据做精确断言。工具调用参数、消息列表结构、状态字段、错误信息这些内部数据,完全可以用等于、包含、类型检查去精确断言;而模型生成的最终文本,只做关键词包含、语义相似度判断,或者直接用LLM-as-judge打分。
特别是“模型最终回复”这种断言,不要写死“必须是某个字符串”,改成“必须包含工具返回的关键信息”就稳得多。
4.2 mock太厚,测了个寂寞
mock的粒度是个大学问。很多初学者喜欢把model、tools、memory全部mock掉,甚至把自己写的prompt builder也mock掉,最后跑完测试发现,真正被测试的代码只有几行if-else,覆盖率低得可怜。
我的原则是:只mock边界,不mock逻辑。什么是边界?模型客户端、外部API、数据库、文件系统、时间函数。什么是逻辑?工具注册表、参数解析、循环控制、消息组装、状态维护。逻辑代码必须用真实实现跑到,mock了就不再是白盒测试,而是自欺欺人。
一个简单的判断标准:如果某个测试在改动一行核心Harness代码后不会失败,说明它mock肉太厚了,根本没测到改动逻辑。
4.3 并发测试的环境隔离
Agent测试逻辑复杂、用例多,天然想并发跑。但共享环境会带来灾难。最常见的坑是多个测试共用一个工具注册表,而注册表是全局单例,并行执行时相互覆盖注册信息。还有数据库测试互相污染数据,导致断言随机失败。
解法其实很常规:注册表做成实例级,fixture里每个测试重建一个;涉及数据库的测试用独立schema或者testcontainer,结束统一清理;用pytest-xdist时给每个worker分配独立数据目录。
还有一个隐蔽问题:对象内部的静态缓存。比如token估算函数有缓存,并发时可能读到半初始化状态。这类问题很难查,但确实会偶发失败。建议对缓存类代码专项排查,在测试fixture里统一清理干净。
4.4 覆盖率不应该是“硬指标”,要看落点
白盒测试绕不开覆盖率。我的观点是:覆盖率要有,但不能盲目追求数字。Agent项目里,模型调用层的覆盖率没有意义——你把一堆mock算进去,数字可能很漂亮,但测试的并不是真实逻辑。
更合理的做法:
- 只统计Harness内部确定性模块的覆盖率,模型调用层和外部工具执行层在配置文件里直接排除。
- 优先看分支覆盖率,其次才是语句覆盖率。Harness里最怕的不是某行没执行,而是分支没覆盖:循环没退出、工具找不到、参数解析失败、重试次数耗尽,这些全是分支场景。
- 给关键模块设置护栏值:工具层、循环控制、提示词组装的覆盖率建议85%以上,其他辅助模块可以放宽。
我见过不少项目“行覆盖100%但分支覆盖只有30%”,测试看起来漂亮,实际最危险的错误处理分支完全没测到。所以看覆盖率时,一定要单独拉分支覆盖率报告。
5. 工具链选型与落地经验
方案讲完,说说工具和推进节奏。工具不在多,顺手最重要。
5.1 一套趁手的测试工具链
以Python生态为例,我目前的主力组合是这些:
| 工具 | 用途 | 适用阶段 |
|---|---|---|
| pytest + pytest-asyncio | 异步测试框架 | 全阶段 |
| respx / aioresponses | 模拟HTTP客户端调用 | 半集成 |
| vcrpy | 录制回放真实模型请求 | 端到端 |
| testcontainers | 容器化数据库和中间件 | 集成测试 |
| jsonschema | 结构化校验工具参数 | 单元测试 |
| pytest-cov | 覆盖率统计 | 全阶段 |
如果项目用的是Java或TypeScript,对应生态里也有等价物,核心思路完全一致:异步测试支持、HTTP mock、录制回放、容器化中间件、覆盖率工具。
有一个容易被忽略的点:jsonschema可以同时用在业务代码和测试代码里。工具注册时做一次参数校验,测试里再对模型返回的arguments做一次校验,双重保险。
5.2 从0到1推进测试的路线
如果现在你接手的是一个几乎没有测试的Agent项目,不要想着一夜之间补齐所有测试。我建议按这个顺序推进:
- 先做依赖注入改造。把model client、tool registry、memory从内部new改成构造参数注入。这步不动业务逻辑,但为后续所有测试打开空间。
- 给工具层补单元测试。性价比最高,因为工具层最稳定、最容易断言,也最容易出泄漏类的bug。
- 给循环控制补测试。覆盖final、tool_call、not found、max_steps这四条主分支。
- 搭两个半集成用例。真LLM加假工具一个,假LLM加真工具一个,打通集成测试框架。
- 最后加端到端和vcr回放,验证整体流程,再用覆盖率报告查漏补缺。
每完成一步,提测前的回归成本就会明显降一截。我自己的感受是,做完前三步之后,线上Agent因为逻辑bug导致的故障率至少下降了一半以上。
5.3 一个额外的小经验:把测试用例当工具规格说明书
最后分享一个很小的实操习惯。我写工具层测试时,喜欢把用例描述写成“给谁什么,期望什么”的句式,比如“给get_weather传入city=北京,期望返回晴26度并记录城市”。时间一长,这些测试用例本身就成了工具行为的活文档。
后端开发、新来的同事、甚至产品经理都能通过读测试用例快速理解每个工具的行为边界。团队协作时,这份“活的规格说明书”比任何设计文档都好用,因为它会随着代码变更自动失效,逼着团队保持同步,而不是文档写一套、代码跑另一套。
这是我个人很受益的一点,也算是白盒测试带来的额外价值:测试不只是质量保障,更是把系统内部结构“讲清楚”的过程。