Solo Unicorn Club logoSolo Unicorn
2,700

AI Agent 测试 — 部署前怎么做评估

AI Agent测试评估LLM-as-judge基准测试回归测试实战
AI Agent 测试 — 部署前怎么做评估

AI Agent 测试 — 部署前怎么做评估

开场

去年我上线了一个 Agent,测试环境一切正常,生产环境第三天就出了事故——Agent 在特定时区的客户对话中把 "AM" 和 "PM" 搞混了,导致批量发出了错误的会议邀请。根本原因:我的测试用例全部用的 UTC 时间,没有覆盖时区转换场景。这件事让我彻底重新设计了 Agent 的测试体系。这篇文章分享我现在用的完整测试框架。

问题背景

Agent 测试和传统软件测试有本质区别:

输出不确定:同样的输入,Agent 可能给出不同的输出。你不能用 assertEqual(output, expected) 来测试。

行为链路长:一个 Agent 可能经过 "理解问题 → 选择工具 → 调用 API → 解析结果 → 生成回复" 五个步骤,每一步都可能出错。

外部依赖多:Agent 依赖 LLM API、数据库、外部工具。这些依赖的行为也不确定。

边界模糊:什么算 "正确" 的回答?客户问 "你们最便宜的方案多少钱",Agent 回答 "$29/月" 和 "我们的入门方案是每月 29 美元" 都算对,但 "大约 30 美元" 就有争议。

传统的单元测试和集成测试仍然需要,但不够。Agent 需要额外的评估层。

核心框架

测试金字塔(Agent 版)

          ╱ ╲
         ╱ E2E╲          ← 端到端场景测试(少量,耗时长)
        ╱ Tests ╲
       ╱─────────╲
      ╱  Agent    ╲      ← Agent 行为评估(LLM-as-judge)
     ╱ Evaluation  ╲
    ╱───────────────╲
   ╱  Integration    ╲   ← 工具调用 + API 集成测试
  ╱   Tests           ╲
 ╱─────────────────────╲
╱      Unit Tests       ╲ ← 纯函数测试(工具函数、解析逻辑)
╱─────────────────────────╲

Unit Tests:测试不依赖 LLM 的部分——工具函数、数据解析、格式验证。快、便宜、确定性。

Integration Tests:测试工具调用链路——能不能正确调用 API、解析返回值、处理错误。

Agent Evaluation:用 LLM 评估 Agent 输出的质量。这是 Agent 测试的核心,也是最有挑战的部分。

E2E Tests:完整的用户场景模拟,从输入到最终输出,覆盖多轮对话。

实现细节

Step 1: Unit Tests——测试确定性部分

把 Agent 系统中所有不依赖 LLM 的逻辑抽出来做单元测试:

import pytest
from agent.tools import parse_order_id, format_price, validate_email

# 确定性函数的测试:传统 assert 即可
class TestToolFunctions:
    def test_parse_order_id_valid(self):
        assert parse_order_id("ORD-2026-0001") == "ORD-2026-0001"

    def test_parse_order_id_from_text(self):
        text = "我的订单号是ORD-2026-0001,请帮我查一下"
        assert parse_order_id(text) == "ORD-2026-0001"

    def test_parse_order_id_invalid(self):
        assert parse_order_id("没有订单号的文本") is None

    def test_format_price_cny(self):
        assert format_price(2999, "CNY") == "¥2,999.00"

    def test_format_price_usd(self):
        assert format_price(29.99, "USD") == "$29.99"

    def test_validate_email(self):
        assert validate_email("user@example.com") is True
        assert validate_email("not-an-email") is False

这些测试跑得快(毫秒级),不花 API 钱,可以在每次 commit 时运行。

Step 2: Integration Tests——Mock LLM,测试工具链路

import pytest
from unittest.mock import AsyncMock, patch
from agent.core import CustomerServiceAgent

class TestToolIntegration:
    """测试 Agent 的工具调用是否正确"""

    @pytest.fixture
    def agent(self):
        return CustomerServiceAgent(model="claude-sonnet-4-5-20250514")

    @pytest.mark.asyncio
    async def test_order_query_calls_correct_tool(self, agent):
        """验证订单查询消息触发正确的工具调用"""
        # Mock LLM 返回一个 tool_use 响应
        mock_response = create_mock_tool_use_response(
            tool_name="get_order_status",
            tool_input={"order_id": "ORD-2026-0001"}
        )

        with patch.object(agent.client.messages, "create", return_value=mock_response):
            # Mock 工具执行
            with patch.object(agent, "execute_tool") as mock_tool:
                mock_tool.return_value = '{"status": "shipped", "tracking": "SF1234"}'
                await agent.handle_message(
                    "我的订单 ORD-2026-0001 现在什么状态?",
                    customer_id="CUST001"
                )

                # 验证调用了正确的工具和参数
                mock_tool.assert_called_once_with(
                    "get_order_status",
                    {"order_id": "ORD-2026-0001"}
                )

    @pytest.mark.asyncio
    async def test_tool_error_handling(self, agent):
        """验证工具调用失败时的错误处理"""
        with patch.object(agent, "execute_tool", side_effect=TimeoutError("API timeout")):
            result = await agent.handle_message(
                "查一下订单 ORD-2026-0001",
                customer_id="CUST001"
            )
            # Agent 应该优雅降级,而不是抛出异常
            assert "稍后" in result or "重试" in result or "人工" in result

Step 3: LLM-as-Judge——Agent 输出质量评估

这是 Agent 测试的核心。用一个 "评判模型" 来评估 Agent 的输出质量:

import anthropic
import json
from dataclasses import dataclass

@dataclass
class EvalCase:
    """评估用例"""
    input_message: str      # 用户输入
    context: str            # 场景上下文
    expected_behavior: str  # 期望行为(自然语言描述)
    criteria: list[str]     # 评估维度

@dataclass
class EvalResult:
    """评估结果"""
    score: float            # 0-1
    passed: bool
    reasoning: str
    dimension_scores: dict[str, float]

class LLMJudge:
    """用 LLM 评估 Agent 输出的质量"""

    def __init__(self):
        self.client = anthropic.Anthropic()

    async def evaluate(
        self,
        eval_case: EvalCase,
        agent_response: str
    ) -> EvalResult:
        """对 Agent 的回复打分"""

        judge_prompt = f"""你是一个 AI Agent 输出质量的评估专家。

## 评估场景
用户输入: {eval_case.input_message}
场景上下文: {eval_case.context}
期望行为: {eval_case.expected_behavior}

## Agent 的实际回复
{agent_response}

## 评估维度
{json.dumps(eval_case.criteria, ensure_ascii=False)}

## 评估规则
- 对每个评估维度打分 (0.0-1.0)
- 关注事实准确性(最重要)、信息完整性、语气适当性
- 如果 Agent 编造了信息,该维度直接给 0
- 如果 Agent 正确地表示不确定并建议转人工,即使没有直接回答问题也应给高分

输出 JSON:
{{
  "overall_score": 0.0-1.0,
  "dimensions": {{
    "维度名": {{"score": 0.0-1.0, "reason": "原因"}}
  }},
  "critical_issues": ["严重问题列表"],
  "reasoning": "整体评估理由"
}}"""

        response = self.client.messages.create(
            model="claude-sonnet-4-5-20250514",  # 评判模型用 Sonnet
            max_tokens=1024,
            messages=[{"role": "user", "content": judge_prompt}]
        )

        result = json.loads(response.content[0].text)

        return EvalResult(
            score=result["overall_score"],
            passed=result["overall_score"] >= 0.75,
            reasoning=result["reasoning"],
            dimension_scores={
                k: v["score"] for k, v in result["dimensions"].items()
            }
        )

# 评估用例集
EVAL_SUITE = [
    EvalCase(
        input_message="你们最便宜的方案多少钱?",
        context="产品有三个方案: Starter $29/月, Pro $79/月, Enterprise 定制",
        expected_behavior="准确报出 Starter 方案的价格 $29/月",
        criteria=["价格准确性", "信息完整性", "语气适当性"]
    ),
    EvalCase(
        input_message="我要退款",
        context="退款政策: 购买后 30 天内可全额退款,需人工处理",
        expected_behavior="告知退款政策,然后转接人工客服处理",
        criteria=["政策准确性", "是否正确转人工", "语气适当性"]
    ),
    EvalCase(
        input_message="你们支持 SAML SSO 吗?",
        context="知识库中没有关于 SAML SSO 的信息",
        expected_behavior="承认不确定,建议转人工或查看文档,不要编造答案",
        criteria=["诚实性", "是否编造信息", "引导行为"]
    ),
]

Step 4: A/B 测试框架

在生产环境中对比两个版本的 Agent:

import random
import time
from dataclasses import dataclass, field

@dataclass
class ABTestConfig:
    """A/B 测试配置"""
    test_name: str
    variant_a: dict         # Agent A 配置
    variant_b: dict         # Agent B 配置
    traffic_split: float    # A 的流量比例 (0.0-1.0)
    min_sample_size: int    # 最小样本量

@dataclass
class ABTestMetrics:
    """A/B 测试指标"""
    variant: str
    response_time_ms: float
    token_cost: float
    customer_satisfaction: float | None = None
    escalation_rate: float = 0.0

class ABTestRunner:
    def __init__(self, config: ABTestConfig):
        self.config = config
        self.metrics: dict[str, list[ABTestMetrics]] = {"A": [], "B": []}

    def assign_variant(self, customer_id: str) -> str:
        """基于客户 ID 确定性分组(同一客户始终在同一组)"""
        hash_val = hash(customer_id) % 100
        return "A" if hash_val < self.config.traffic_split * 100 else "B"

    async def run_and_record(
        self, customer_id: str, message: str
    ) -> tuple[str, str]:
        """执行 A/B 测试并记录指标"""
        variant = self.assign_variant(customer_id)
        agent_config = (
            self.config.variant_a if variant == "A"
            else self.config.variant_b
        )

        start = time.time()
        response = await run_agent(message, customer_id, agent_config)
        elapsed = (time.time() - start) * 1000

        self.metrics[variant].append(ABTestMetrics(
            variant=variant,
            response_time_ms=elapsed,
            token_cost=response["token_cost"],
        ))

        return variant, response["text"]

    def get_summary(self) -> dict:
        """汇总 A/B 测试结果"""
        summary = {}
        for variant in ("A", "B"):
            data = self.metrics[variant]
            if not data:
                continue
            summary[variant] = {
                "sample_size": len(data),
                "avg_response_ms": sum(m.response_time_ms for m in data) / len(data),
                "avg_cost": sum(m.token_cost for m in data) / len(data),
                "p95_response_ms": sorted(
                    [m.response_time_ms for m in data]
                )[int(len(data) * 0.95)],
            }
        return summary

Step 5: 回归测试——每次改 prompt 都要跑

# 回归测试套件
class AgentRegressionTest:
    """每次修改 prompt 或模型后必须运行的测试"""

    def __init__(self):
        self.judge = LLMJudge()
        self.test_cases = self._load_test_cases()

    def _load_test_cases(self) -> list[EvalCase]:
        """从 JSON 文件加载测试用例"""
        import json
        with open("tests/regression_cases.json") as f:
            cases = json.load(f)
        return [EvalCase(**case) for case in cases]

    async def run_full_suite(self, agent) -> dict:
        """运行完整回归测试"""
        results = []
        for case in self.test_cases:
            # 运行 Agent
            response = await agent.handle_message(
                case.input_message,
                customer_id="TEST_USER"
            )
            # LLM 评估
            eval_result = await self.judge.evaluate(case, response)
            results.append({
                "case": case.input_message[:50],
                "score": eval_result.score,
                "passed": eval_result.passed,
                "reasoning": eval_result.reasoning
            })

        # 汇总
        total = len(results)
        passed = sum(1 for r in results if r["passed"])
        avg_score = sum(r["score"] for r in results) / total

        return {
            "total_cases": total,
            "passed": passed,
            "failed": total - passed,
            "pass_rate": passed / total,
            "avg_score": avg_score,
            "failed_cases": [r for r in results if not r["passed"]]
        }

实战经验

生产数据

我的客服 Agent 测试体系:

测试层级 用例数 运行时间 运行频率 成本/次
Unit Tests 127 3 秒 每次 commit $0
Integration Tests 43 25 秒 每次 PR $0
LLM-as-Judge 评估 85 12 分钟 每次 prompt 变更 $2.50
回归测试全套 85 15 分钟 每周 $3.20
A/B 测试 持续 持续 上新版本时 ~$50/周

踩过的坑

坑 1:LLM-as-Judge 的偏见。 LLM 倾向于给更长、更正式的回答更高分。解决方案:在 judge prompt 中明确强调 "简洁精确的回答优于冗长但准确的回答",并且加入长度惩罚项。

坑 2:测试用例的维护成本。 每次产品功能更新,测试用例也要更新。我们建立了一个规则:每个新功能的 PRD 里必须包含 3-5 个 Agent 测试用例。

坑 3:回归测试的结果波动。 同一套测试用例,两次运行的 LLM-as-Judge 评分可能有 5-10% 的波动。解决方案:每个用例跑 3 次取中位数,减少随机性。

坑 4:A/B 测试的样本量问题。 日活 2 万的产品,客服请求只有几百次。要积累到统计显著的样本量需要 2-3 周。加速方案:同时做 synthetic evaluation(用 LLM 模拟客户发送测试消息)。

总结

三条核心 takeaway:

  1. Agent 测试是分层的,不是一种方法打天下——确定性部分用传统单元测试,工具链路用集成测试,输出质量用 LLM-as-Judge,生产效果用 A/B 测试。每一层解决不同的问题。

  2. LLM-as-Judge 是 Agent 评估的核心手段,但要注意它的局限性——评判模型自身有偏见(偏好长回答、正式语气),需要在 prompt 中显式校正。关键质量指标(价格、政策)仍然需要规则检查。

  3. 回归测试是安全网——每次改 prompt、换模型、更新知识库,都必须跑回归测试。没有这个安全网,你不敢改任何东西,Agent 就会停在第一版不再迭代。

如果你还没有给 Agent 建测试体系,从最简单的开始:写 10 个核心场景的 LLM-as-Judge 评估用例,配合 20 个确定性的单元测试。这个组合足以覆盖最常见的问题。

你是怎么测试 Agent 的?有没有踩过类似的坑?欢迎分享。