几个月前我接手了一个 AI Agent 项目,功能很酷——能自动处理用户工单、查数据库、调用 API、发通知。看起来一切完美,直到生产环境出了事。

问题是这样的:Agent 在测试环境表现不错,95% 的任务都能正确完成。上线第一周,准确率直接掉到 60%。不是代码变了,是用户的提问方式变了。

之前测试用的都是"标准问题"——我知道答案的问题。但真实用户不会按剧本提问。他们会问模糊的问题、有歧义的问题、甚至前后矛盾的问题。Agent 的"能力"在可控环境下看起来很好,但一遇到真实世界的噪音就露馅了。

踩了这个坑之后,我开始认真研究怎么系统地评估和测试 AI Agent。这篇文章就是想把这些思考写下来,给正在或准备构建 AI Agent 的人一些参考。

传统测试为什么不够用

做过后端开发的都知道,写单元测试是基本功。输入 A,期望输出 B,结果不对就报错。这套逻辑在传统软件里很好用——确定性的输入输出,明确的边界条件。

但 AI Agent 不一样。

它内部是一个 LLM,输入自然语言,输出行动序列。同一个问题,今天问和明天问,结果可能完全不同。更关键的是,一个任务可能有多条正确的完成路径。比如"帮我查一下上周的销售数据",Agent 可以是查 SQL 数据库,可以是查 BI 报表,也可以是用 API 拉数据——只要结果对,过程不重要。

传统断言式的测试根本覆盖不了这种场景。你没法写"assert agent.do_task() == expected_result"然后指望它像单元测试一样通过或不通过。

这个问题在我之前做 React Native 性能优化的项目里也遇到过——当时我写了一篇技术选型的文章记录对比过程。但 Agent 测试的问题更棘手,因为它不是"选错了框架"这种确定性错误,而是"有时候好用有时候不好用"的随机性问题。

Eval-Driven Development 是什么

Eval-Driven Development(EDD,评估驱动开发)是我借鉴别人的实践,加上自己踩坑总结出来的一套方法。核心思路就一句话:

先定义「好用」的标准,再写 Agent 逻辑。

听起来像 TDD 的变体?确实类似,但重点不同。TDD 关注的是"功能是否正确实现",EDD 关注的是"Agent 是否按照期望的方式完成任务"。

EDD 的工作流长这样:

定义评估标准 → 创建评估数据集 → 写评估脚本
    ↓
跑 Agent → 收集结果 → 评分
    ↓
不满意?→ 改 Prompt / 调参数 / 加工具 → 重新评估
    ↓
满意了?→ 部署到生产 → 持续监控 → 补充数据集

跟传统开发最大的区别是什么?写代码之前先写评估集。

传统开发是先实现功能,再写测试来验证。EDD 是先定义"什么样的结果算好",再让 Agent 去试。评估标准本身就是规格文档。

一个具体的例子:做客服回复 Agent,先不要急着写 Prompt 说"你要礼貌地回复用户",而是先定义:

  • 回复是否解决了用户的问题(是/否)
  • 回复时是否引用了知识库条目(是/否)
  • 回复语气是否专业(1-5 分)
  • 回复中是否包含不准确的信息(是/否)

然后找 50-100 个历史工单,人工写好期望的回复。这些人工标注的数据就是评估集。Agent 写好了,用这套评估集去测,得分过线了再考虑上线。

构建评估体系的四个维度

在实践中,我总结了一个四维评估框架。每个维度关注不同的问题,合在一起能比较全面地衡量 Agent 的质量。

维度一:任务完成率

最基础的维度。给定一个任务,Agent 是否成功完成了?

这个看似简单,实际上坑很多。因为"完成"的定义不总是二元化的。比如"帮用户修改订单地址",Agent 可能改了地址但没发通知邮件——算完成还是不算?

我的做法是任务颗粒度分层

Level 0: Agent 完全没有动作(卡住了、报错了)
Level 1: Agent 开始执行但不完整(改了数据库但没更新缓存)
Level 2: Agent 完成了主流程但遗漏了副流程(改了地址没发通知)
Level 3: 全部完成

我一般只把 Level 3 算作"完成",Level 1-2 算部分完成,单独统计。

维度二:执行效率

Agent 完成一个任务花了多少代价?包括:

  • 步骤数:调用了多少次工具?一个优秀的 Agent 应该用最少的步骤完成任务。我见过一个 Agent 查个简单的用户信息调了 5 次 API——先查用户 ID,再查用户详情,再查订单,再查最近活动,再查偏好设置,而实际上一次 API 就能搞定。
  • Token 消耗:总输入输出 token 数。这不只是省钱的问题,token 多通常意味着 Agent 做了不必要的推理或重复查询。
  • 延迟:从接受到任务的端到端耗时。这个容易被忽视,但对用户体验影响很大。

维度三:决策质量

这个维度衡量 Agent 在关键决策点的表现:

  • 工具选择是否合适:查天气用天气 API 是正确的,用网页爬虫去爬天气网站就是不合适的
  • 参数传递是否正确:调用数据库查询时传的参数是否合理
  • 错误处理是否得当:API 返回 429 时,Agent 是直接放弃还是重试?
  • 安全边界是否遵守:有没有尝试执行权限之外的敏感操作?

这个维度最难自动化评估,通常需要人工抽检或者用更强的模型做裁判。

维度四:可复现性

这里有一个关键问题:Agent 做对一次,和它每次都能做对,是两回事。

因为 LLM 生成的结果有随机性,同一个 Prompt 同一套工具,今天能完成任务明天可能就不行。可复现性衡量的就是 Run-to-Run 的稳定性。

具体做法:同一个评估用例跑 N 次(建议 N≥5),统计成功的概率。

用例:查询用户 #12345 的最近订单
第一次:✅ 成功了
第二次:✅ 成功了
第三次:❌ 查了两次不同 API,然后说"用户没有订单"
第四次:✅ 成功了
第五次:✅ 成功了

成功率:80%(4/5)

如果成功率低于 90%,说明这个用例是脆弱的——表面的成功可能是碰巧。

实际跑通的工具链

说了这么多理论,分享一下我现在实际在用的工具链。这套东西不完美,但够用。

评估数据集

每个 Agent 项目维护一个 evals/ 目录:

evals/
├── datasets/
   ├── basic.json          # 基础功能测试(20-30 条)
   ├── edge-cases.json     # 边界情况(10-15 条)
   └── regression.json     # 回归测试(往期失败的用例)
├── scorers/
   ├── task_completion.py
   ├── decision_quality.py
   └── consistency_check.py
├── report/
   └── 2026-06-10.json
└── run.py

每个评估用例的结构:

{
  "id": "basic-001",
  "description": "用户查询自己最近的订单",
  "input": {
    "user_message": "帮我看看我最近买的耳机发货了没",
    "user_id": "usr_12345",
    "context": {
      "user_orders": ["order_001", "order_002"],
      "current_order": "order_002"
    }
  },
  "expected": {
    "tool_calls": [
      {
        "tool": "query_order",
        "params": { "order_id": "order_002" }
      }
    ],
    "final_answer_contains": ["已发货", "快递单号"],
    "should_not_contain": ["我不知道", "系统错误"]
  },
  "criticality": "high"
}

expected.tool_calls 不是要精确匹配——Agent 调 API 的次序和参数不完全一样很正常。我实际用的是模糊匹配:检查关键工具是否被调用了,关键参数是否正确传入,最终回答是否包含了期望的信息。

评分脚本

一个简化的评分器:

class TaskCompletionScorer:
    """任务完成率评分器"""

    def __init__(self, weight=0.4):
        self.weight = weight

    def score(self, trace, expected):
        """
        评估一个 trace(Agent执行轨迹)是否完成了任务。

        返回 0.0 ~ 1.0 的分数。
        """
        score = 0.0

        # 1. Agent 有没有实际执行操作?
        if not trace.tool_calls:
            return 0.0

        # 2. 关键工具是否被调用了?
        required_tools = expected.get("required_tools", [])
        called_tools = [c.tool for c in trace.tool_calls]

        tool_coverage = sum(
            1 for t in required_tools if t in called_tools
        ) / max(len(required_tools), 1)

        # 3. 最终回答是否包含期望的关键信息?
        answer = trace.final_answer
        required_content = expected.get("final_answer_contains", [])

        content_coverage = sum(
            1 for phrase in required_content 
            if phrase.lower() in answer.lower()
        ) / max(len(required_content), 1)

        # 4. 回答中是否有不应该出现的表述?
        forbidden = expected.get("should_not_contain", [])
        has_forbidden = any(
            phrase.lower() in answer.lower() 
            for phrase in forbidden
        )

        score = (tool_coverage * 0.5 + content_coverage * 0.5)
        if has_forbidden:
            score *= 0.5  # 出现不应该出现的表述,分数减半

        return score

持续集成集成

评估脚本接入了 CI,每次修改都自动跑:

# .github/workflows/agent-eval.yml
name: Agent Evaluation
on: [push, pull_request]

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run agent evaluations
        run: |
          python evals/run.py \
            --dataset evals/datasets/basic.json \
            --model claude-sonnet-4 \
            --runs 3 \
            --output evals/report/${{ github.sha }}.json
      - name: Check minimum score
        run: |
          python -c "
          import json
          report = json.load(open('evals/report/${{ github.sha }}.json'))
          score = report['aggregate_score']
          print(f'Aggregate score: {score}')
          assert score >= 0.8, f'Score {score} below minimum 0.8'
          "

注意 --runs 3——这是关键。每次都跑 3 轮取平均,避免单次运气的干扰。在本地开发时跑 1 轮就行了,合并 PR 前至少跑 3 轮。

真实案例:MCP Weather Server 的评估

拿 MCP 官方文档里的 Weather Server 举个例子,说明评估是怎么落地的。

这是官方示例的架构:Agent 通过 MCP 服务器暴露两个工具 get_alertsget_forecast,Agent 根据用户的天气查询选择调用。

好的评估数据集

我扩充了官方的测试,自己构建了评估集:

[
  {
    "id": "weather-001",
    "input": "今天旧金山天气怎么样?",
    "expected": {
      "tool_calls": ["get_forecast"],
      "params_check": { "latitude": true, "longitude": true },
      "expected_answer_contains": ["温度", "天气状况"]
    }
  },
  {
    "id": "weather-002",
    "input": "洛杉矶有台风预警吗?",
    "expected": {
      "tool_calls": ["get_alerts"],
      "expected_answer_contains_not": ["是", "有"]
    }
  },
  {
    "id": "edge-001",
    "input": "巴黎的天气怎么样",
    "expected": {
      "tool_calls": ["get_forecast"],
      "params_check": { 
        "latitude": { "expected_range": [48.8, 48.9] },
        "longitude": { "expected_range": [2.3, 2.4] }
      }
    }
  }
]

注意 edge-001 这个用例——我故意给了巴黎的坐标范围。因为有些 Agent 会把"巴黎"的坐标搞混(Texas 也有个 Paris),这个用例帮我发现了一个坐标映射错误。

用评估发现问题

跑评估的第一轮结果:

weather-001: ✅ (分数: 0.92)
weather-002: ✅ (分数: 0.88)
weather-003: ✅ (分数: 0.95)
edge-001:   ❌ (分数: 0.35)
  - 调用了 get_forecast ✅
  - 但传的坐标是 33.7, -95.5(Texas Paris)❌
  - 回答的是 Texas 的天气,不是法国巴黎

这个 Bug 在人工测试时没被发现——测试人员随手写了"查一下巴黎天气",看了眼结果有温度有天气状况,觉得没问题。但在 EDD 流程里,我明确标注了 Paris 的坐标范围,Agent 超出预期范围就会被扣分。这种边界情况在实际生产中很常见,传统测试很难覆盖。

修复方案:在 MCP Server 的工具描述里加一行说明。

@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """获取指定位置的天气预报。

    注意:如果用户输入的是城市名,请先核实该城市的坐标。
    例如"Paris"可能是法国巴黎也可能是美国德克萨斯州的巴黎(Paris, TX)。
    如有歧义,请向用户确认。
    """

改完之后重新跑:

edge-001: ✅ (分数: 0.85)
  - 调用了 get_forecast ✅
  - 请求确认城市 ✅(Agent 问"请问是哪个巴黎?")

这种问题用传统测试很难发现,但在 EDD 框架下,从"感觉不靠谱"变成了"可测量、可修复、可验证"。

坑与经验

踩了不少坑,有些经验可以提前分享。

坑 1:评估集就是你的规格文档

最开始我觉得 20 条评估用例就够了,结果越加越多。每条用例都在定义"Agent 应该怎么做"。

到后面我意识到:评估集就是 Agent 的规格文档。 需求变了,优先改评估集,再改 Prompt。因为评估集是可执行的——改完跑一遍就知道回归有没有破坏旧功能。

坑 2:评分器本身也会有问题

评分器的逻辑也可能出错。比如 final_answer_contains 检查"已发货"这个关键词,Agent 的回答是"您的订单已于昨日**发货,快递单号是 SF123456"——正常来说这是对的。但如果评分器只检查子串匹配,"已发货,快递单号"是包含的。但如果 Agent 说"您的订单状态为已发货,快递公司为顺丰","快递单号"这个关键词不存在,会被判定为缺失。

所以评分器也要写测试。我现在会给评分器写单元测试:

def test_scorer_detects_shipping_info():
    scorer = TaskCompletionScorer()
    trace = MockTrace(final_answer="您的订单已于昨日发货,快递单号是 SF123456")
    expected = {"final_answer_contains": ["已发货", "快递单号"]}
    score = scorer.score(trace, expected)
    assert score >= 0.9, f"Expected high score, got {score}"

def test_scorer_penalizes_fake_completion():
    scorer = TaskCompletionScorer()
    trace = MockTrace(final_answer="我无法查询您的订单状态")
    expected = {"final_answer_contains": ["订单状态", "发货时间"]}
    score = scorer.score(trace, expected)
    assert score < 0.5, f"Expected low score, got {score}"

坑 3:善用模型评估模型

有些维度很难用规则评分器衡量——比如"回复是否专业"、"方案是否最优"。这时候可以用更强的大模型来做评判(LLM-as-a-Judge)。

但我发现一个陷阱:裁判模型也会出错。 我有一次用 Claude 评估一个客服 Agent 的回复,Claude 打了高分因为"回复很礼貌",但实际回复内容是根据错误的知识库条目给出的。裁判模型被外表的礼貌迷惑了。

后来我在评估 Prompt 里加了明确的惩罚项:

评估标准:
1. 回复是否准确(权重 50%)—— 必须引用知识库具体条目编号
2. 回复是否完整(权重 30%)—— 是否覆盖了用户所有问题
3. 回复语气是否合适(权重 20%)—— 不过分热情也不冷淡

扣分项:
- 回复中包含不准确信息 → -50 分(一票否决)
- 回复中有语义矛盾 → -30 分
- 回复超出权限范围 → -40 分

把评估标准结构化,让裁判模型有章可循,比笼统地说"你觉得这个回复好不好"靠谱得多。

应该投入多少

说实话,这套体系有成本。一个中等复杂度的 Agent 项目,评估数据集需要 50-100 条用例,每条人工标注预计 5-10 分钟,加上评分器开发和 CI 集成,初始投入大概 2-3 天。

值不值?我的判断标准很简单:

场景 建议投入
个人玩具项目 / Demo 10-20 条用例 + 简单规则评分器,半天搞定
内部工具 / 团队使用 30-50 条用例 + 规则评分器,1-2 天
面向客户的 Agent 100+ 条用例 + 规则 + 模型评分,3-5 天
金融 / 医疗 / 合规场景 500+ 条用例 + 多维度评分 + 人工抽检,持续投入

值得注意的是,评估集不是一次性的。每次生产环境出了新问题,就把问题用例补充到评估集里。这样 Agent 的质量会逐步提升,因为"犯过的错不会再犯"。

下一步

这篇文章写的是怎么评估 Agent 的"输出质量",但其实还有几个方向我没深度展开:

  • 安全性评估:Agent 能否抵御 prompt injection?边界在哪里?
  • 成本评估:不同模型组合的 token 消耗和成本平衡
  • 用户满意度追踪:评估分数和用户实际满意度之间的关联

这些我会在后续的文章里持续分享。目前这套 EDD 框架是我在项目中验证过、确实有效的方案,它解决了我最头疼的问题——"我怎么知道 Agent 改好没改坏"。

如果你也在做 AI Agent 项目,不妨试试从这个框架入手。从 20 条用例开始,慢慢扩充。最重要的是:先定义「好用」的标准,再动手写代码。


这篇文章基于我在三个 Agent 项目中的实践。每个项目遇到的评估挑战都不太一样,但核心思路是一致的——用评估驱动开发,让 Agent 的质量从「感觉还行」变成「我知道它能用」。