智能体测试不能只做单元验证

封面信息图

智能体的风险常出现在多个工具串联之后:它是否用了正确的身份、是否重复调用、遇到超时会不会停止。单元测试依然必要,但还应在隔离环境中检查调用轨迹、权限拒绝和异常降级,避免把自然语言回答看成唯一结果。

在传统的软件工程中,写好单元测试(Unit Test)往往能覆盖 80% 以上的代码缺陷。但在 AI Agent 和多模态交互系统的开发里,仅仅依靠单元测试去校验单个 Tool 函数或者单个 Prompt 模板,上线后依然会频繁崩溃。Agent 的核心特征在于状态机推演的自发性与多轮 Tool Calling 的涌现行为,这些问题只有在端到端(E2E)的轨迹测试与沙箱评估中才能暴露出来。

1. 为什么单元测试对 Agent 系统往往会失效

单元测试的核心前提是确定性断言:给定输入 $A$,输出必须严格等于 $B$。

然而在 Agent 系统中,即使 Tool Calling 接口本身通过了 100% 的单元测试覆盖率,当模型在第 3 轮思考中因为网络抖动收到的工具返回格式稍有偏差时,它可能会选择调用另一个完全意想不到的工具,甚至陷入死循环。

[常规单元测试] 
输入 Mock 工具参数 ➔ 执行 Tool Function ➔ 断言 Return Value (可以通过,但无法验证 Agent 决策逻辑)

[Agent 轨迹测试]
用户目标 ➔ Agent 多轮思考 ➔ 工具 A ➔ 工具 B ➔ 条件分支 C ➔ 断言完整执行 Trajectory 与最终状态

单点函数的正确,无法保证 Agent 在多轮交互中不偏离最初的业务目标。

2. 多模态交互中的视觉文本对齐测试困境

在多模态 Agent 场景下(如根据 UI 截图自动点击或解析工单),测试复杂度会呈现指数级上升。

测试人员面临的典型困局是:

  1. 图像渲染微小差异导致的定位失效:前端 UI 按钮位置偏移了 5 个像素,多模态模型识别出的 bounding box 坐标就会改变,导致原本固定的坐标点击脚本失效。
  2. 多轮对话中图像历史的积累污染:模型在第 1 轮识别了图 A,第 2 轮识别了图 B,在第 3 轮回答时却把图 A 的特征误应用到了图 B 上。

这类视觉-文本跨模态的上下文污染,只有通过模拟多轮状态演进的集成测试才能捕捉到。

3. 构建 Agent 模拟沙箱与轨迹断言(Trajectory Assertion)

为了全面测试 Agent,我们需要建立一套能够 Mock 外部工具、记录 Agent 思考 Trajectory 并进行状态断言的测试框架。

以下是用 Python 实现的 Agent 轨迹测试器示例:

import json
from typing import List, Dict, Any, Callable

class TrajectoryAssertionError(Exception):
    pass

class MockToolRegistry:
    def __init__(self):
        self.tools: Dict[str, Callable] = {}
        self.call_logs: List[Dict[str, Any]] = []

    def register(self, name: str, func: Callable):
        self.tools[name] = func

    def execute(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
        self.call_logs.append({"tool": tool_name, "args": tool_args})
        if tool_name not in self.tools:
            raise KeyError(f"Mock 工具不存在: {tool_name}")
        return self.tools[tool_name](**tool_args)

class AgentTrajectoryEvaluator:
    def __init__(self, agent_under_test, mock_registry: MockToolRegistry):
        self.agent = agent_under_test
        self.mock_registry = mock_registry

    def assert_trajectory(
        self, 
        user_prompt: str, 
        expected_tool_sequence: List[str],
        max_allowed_turns: int = 5
    ):
        """
        断言 Agent 在执行特定任务时,调用的工具顺序与状态转移符合预期
        """
        # 1. 运行 Agent
        result = self.agent.run_task(user_prompt, max_turns=max_allowed_turns)
        
        actual_sequence = [log["tool"] for log in self.mock_registry.call_logs]
        
        # 2. 轨迹序列强校验
        if actual_sequence != expected_tool_sequence:
            raise TrajectoryAssertionError(
                f"Agent 调用的工具轨迹不匹配!\n"
                f"期望序列: {expected_tool_sequence}\n"
                f"实际序列: {actual_sequence}\n"
                f"实际执行日志: {json.dumps(self.mock_registry.call_logs, indent=2)}"
            )
            
        # 3. 步数上限断言,防止潜在死循环
        if result["total_turns"] > max_allowed_turns:
            raise TrajectoryAssertionError(
                f"Agent 运行轮次 ({result['total_turns']}) 超过最大限制 ({max_allowed_turns})"
            )

        print(f"✅ Agent 轨迹测试通过!共消耗 {result['total_turns']} 轮,工具轨迹: {actual_sequence}")

# 测试用例示范
def test_order_cancellation_agent_flow():
    registry = MockToolRegistry()
    # 注册 Mock 业务 API
    registry.register("search_order", lambda order_id: {"status": "PAID", "amount": 100})
    registry.register("cancel_order", lambda order_id: {"success": True})
    
    # 假设 MockAgent 为带测试的 Agent 实例
    class DummyAgent:
        def run_task(self, prompt, max_turns):
            # 模拟 Agent 正确执行了 search -> cancel
            registry.execute("search_order", {"order_id": "ORD_123"})
            registry.execute("cancel_order", {"order_id": "ORD_123"})
            return {"total_turns": 2}

    evaluator = AgentTrajectoryEvaluator(DummyAgent(), registry)
    evaluator.assert_trajectory(
        user_prompt="帮我取消订单 ORD_123",
        expected_tool_sequence=["search_order", "cancel_order"],
        max_allowed_turns=3
    )

if __name__ == "__main__":
    test_order_cancellation_agent_flow()

代码的关键点在于 assert_trajectory 函数。它不再关心模型单次吐出的自然语言标点符号是否完美,而是强行拦截并记录 Agent 调用的工具序列 actual_sequence。如果 Agent 跳过了身份校验工具直接去调用取消订单 API,测试会立刻抛出异常。

4. 混沌工程:在网络抖动与异常工具返回下测试 Agent 容错

在真实生产环境中,工具 API 可能会遇到超时、返回 500 错误或者吐出非法 JSON。Agent 在面对这些异常时能否实现优雅降级,是考验系统工程质量的试金石。

推荐在测试沙箱中加入混沌注入机制:

  1. 工具延迟与超时注入:随机让 10% 的 Mock API 延迟 3 秒返回,观察 Agent 是否会触发无休止的重试。
  2. 工具返回坏数据注入:让 API 随机吐出空对象 {} 或格式错误的 HTML,测试 Agent 是否会自动修正参数重试,还是直接抛出未捕获的 KeyError。
  3. Token 预算耗尽注入:在第 2 轮强制截断 Context Window,验证 Agent 框架能否正确触发工程保底逻辑,把错误平滑传给前端。

5. Agent 测试体系的三层金字塔构建

构建稳健的 Agent 测试架构,应当形成分层的测试金字塔:

测试层级测试对象断言焦点执行频率
底层 Tool 单元测试本地 Python / Go 工具函数边界参数、SQL 注入防御、数据格式正确性每次 Git Commit 自动触发
中层 轨迹沙箱测试Agent 状态机与 Tool Calling 循环工具调用顺序(Trajectory)、死循环熔断、异常降级每日 Nightly Build 自动运行
顶层 真实基准评估端到端大模型 + 真实多模态输入任务完成率(Task Success Rate)、平均 Token 消耗、P95 延时版本发布前全量回归

把测试重心从简单的“字符串匹配”转移到“工具调用轨迹与状态机断言”上,才是保障 AI Agent 系统在复杂生产环境中稳妥运行的根本保障。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐