Agent系统的复杂度与普通应用有本质区别:它的执行路径由大模型的推理结果动态决定,同一条用户指令可能触发不同的工具调用序列,输出也带有天然的随机性。这意味着传统单元测试中精确断言输入输出的做法在Agent场景下大量失效。集成测试与端到端验证因此成为Agent质量保障的核心手段——它关注的不是某个函数对不对,而是整个工作流串起来之后,状态流转是否正确、工具调用是否符合预期、异常分支能否兜底。本文将从测试分层、环境搭建、断言设计到CI落地,完整讲解如何为Agent构建一套端到端工作流验证体系。

一、Agent测试为什么要分层:单元、集成与端到端的边界
很多团队在测试Agent时的第一个困惑是:到底测什么?是把提示词、工具、记忆模块各自测一遍,还是直接跑完整对话?答案不是二选一,而是分层。单元测试负责验证各组件自身的确定性逻辑,比如工具函数的参数解析、输出格式化、记忆模块的存取行为。这一层可以用传统方式编写,断言精确,运行速度快,应该覆盖最广。
集成测试关注的是组件之间的契约。典型场景包括:Agent是否按照规划调用了正确的工具、工具返回的异常是否被正确捕获并触发重试或降级、多轮对话中状态是否在步骤间正确传递。这一层不需要真实的LLM,可以通过录制回放或Mock模型输出,让测试结果可复现。
端到端验证则使用真实模型和真实工具环境,模拟用户完整使用路径,验证整体行为是否达标。它的价值在于捕捉前两层无法发现的问题,例如提示词与工具描述不匹配导致模型选错工具、上下文超长导致记忆截断等。三层的投入比例建议大致为单元测试70%、集成测试20%、端到端测试10%,端到端因成本高且不稳定,应聚焦关键用户旅程而非追求覆盖率。
二、集成测试环境搭建:Mock模型输出与沙箱工具
Agent的不确定性主要来自LLM,因此集成测试的第一步是把不确定性隔离出来。常见做法是为模型客户端增加一个可切换的后端:测试时注入一个Mock后端,根据当前对话状态返回预先准备的模型响应。这样工具选择逻辑、调用顺序控制、错误处理流程都能被稳定验证。
以Python为例,可以设计一个简单的可控模型桩:
class ScriptedModelClient:
"""按脚本回放的模拟模型客户端,用于集成测试"""
def __init__(self, scripted_responses):
self.scripted_responses = scripted_responses
self.call_count = 0
async def chat(self, messages, tools=None):
response = self.scripted_responses[self.call_count]
self.call_count += 1
return response # 包含 tool_calls 或文本内容
# 测试中注入
agent = Agent(
model=ScriptedModelClient([
{"tool_calls": [{"name": "search_order", "arguments": {"order_id": "A100"}}]},
{"content": "您的订单A100已发货,预计明天送达。"}
]),
tools=[search_order_tool, refund_tool]
)
工具侧同样需要隔离。涉及写操作的工具(下单、发邮件、修改数据库)在集成测试中应指向沙箱环境:使用独立的测试数据库、邮箱捕获服务或内存文件系统。一个实用技巧是为工具定义环境标识,测试启动时自动切换到沙箱实现,避免污染生产数据。对于外部HTTP服务,可以启动本地Mock服务器并录制请求,验证Agent发出的请求参数是否符合契约。
断言设计是这一层的关键难点。不要断言模型的自然语言输出原文,而应断言结构化行为:调用了哪些工具、参数是否正确、调用顺序是否合理、最终状态是否变更。将Agent执行的轨迹抽象为事件列表后再断言,会让测试既稳定又可读。
async def test_order_query_workflow(agent):
result = await agent.run("帮我查一下订单A100")
events = result.trace.to_events()
# 断言行为轨迹而非文本输出
assert events.tool_calls[0].name == "search_order"
assert events.tool_calls[0].arguments["order_id"] == "A100"
assert result.final_state.get("order_fetched") is True
assert "A100" in result.answer # 宽松断言关键信息
三、端到端工作流验证:场景编排与稳定性控制
端到端验证的目标是回答一个问题:真实用户走完整流程时,Agent能否正确完成任务?落地方式通常是场景编排——把关键用户旅程写成结构化测试用例,包括初始状态、用户输入序列、期望的工具调用链和终态校验。例如电商客服Agent可以编排:查询订单、申请退款、确认退款结果三个连续场景,验证上下文在场景间的传递。
E2E_SCENARIOS = [
{
"name": "订单退款全流程",
"steps": [
{"user": "查一下订单A100", "expect_tool": "search_order"},
{"user": "我要退款", "expect_tool": "create_refund"},
{"user": "好的,确认", "expect_tool": "confirm_refund"},
],
"final_check": {"refund_status": "processing", "order_frozen": True}
}
]
@pytest.mark.e2e
@pytest.mark.parametrize("scenario", E2E_SCENARIOS)
async def test_e2e_workflow(scenario, live_agent):
state = {}
for step in scenario["steps"]:
result = await live_agent.run(step["user"], session_state=state)
assert step["expect_tool"] in [c.name for c in result.trace.tool_calls]
for key, value in scenario["final_check"].items():
assert state.get(key) == value
端到端测试最大的敌人是不稳定。同样的输入,模型偶尔选择不同的工具路径都属于正常波动,因此断言要区分硬性要求与软性要求:工具是否被调用是硬性的,调用顺序和措辞则尽量放宽。另一个常用技巧是多次重试取多数结果,例如同一场景跑五次,至少四次通过即视为通过,用统计置信度替代单次确定性。
成本控制同样重要。端到端测试直接消耗模型Token,跑全量场景可能非常昂贵。建议在CI中分两级:每次提交只跑冒烟级的核心场景,每日定时任务跑全量回归;同时开启低温度参数、使用较便宜的模型档位跑测试,只在发布前用生产同款模型做最终验证。测试结果应保存完整的执行轨迹,失败时能回放每一步的工具调用与模型响应,这对定位是提示词问题还是工具问题至关重要。
四、常见坑点与工程建议
实践中最容易踩的坑有三个。第一,在集成测试中过度Mock,把工具返回值简化到失真,导致真实环境下格式解析直接崩溃。工具Mock的返回值应尽量使用真实样本数据,包括脏数据与边界情况。第二,忽略上下文长度的累积效应,单轮测试都通过,多轮对话后上下文超限或被截断,历史信息丢失导致行为异常。端到端场景应包含一个长对话用例专门覆盖这一点。第三,没有为不可重放的外部操作设计清理机制,测试产生的真实邮件、真实订单堆积在环境里,最终干扰后续测试。
工程上的建议是:把执行轨迹作为一等公民来设计,Agent框架应原生支持记录每一步的输入、输出、工具调用与耗时,测试断言基于轨迹而非最终文本;同时建立场景用例库并与提示词版本绑定,提示词每次变更都跑一遍关联场景,形成回归保护。当这套体系跑通后,Agent的每次迭代都有了可靠的安全网,团队才能放心地优化提示词、更换模型或新增工具,而不必担心看不见的回归悄然上线。