AI Agent 实战避坑 05|AI 版 TDD:Eval-Driven Development 完全指南

AI Agent 实战避坑 05|AI 版 TDD:Eval-Driven Development 完全指南

一个 Reviewer Agent 上线初期效果不错。两周后有人改了 prompt 里几个措辞,没跑任何验证就合并了。又过了一周,另一个人加了两条新的检查规则。再过一周,模型供应商默默升级了一个小版本。

一个月后团队发现 review 通过率从 85% 跌到了 60%。但没人知道是这三个变更中的哪一个导致的------因为没有任何自动化的回归检测。

传统软件有 TDD(Test-Driven Development)防回归。AI 系统需要 EDD(Eval-Driven Development)。


TDD 和 EDD 的类比

如果你有 TDD 经验,理解 EDD 会非常快:

复制代码
TDD(传统软件)              EDD(AI 系统)
──────────────              ──────────────
先写测试用例                先写 eval 用例
跑测试 → 红灯               跑 eval → 低分
写代码让测试通过             调 prompt/pipeline
跑测试 → 绿灯               跑 eval → 高分
重构时跑回归测试             改 prompt 时跑回归 eval
全绿 = 没有退化              分数 ≥ 基线 = 没有退化

核心区别在一点:TDD 的测试结果是二元的(PASS/FAIL),EDD 的 eval 结果是分数制的。

因为 AI 的输出是非确定性的------同一个输入跑 10 次可能得到 10 个不同的输出。你不能要求"输出必须和标准答案完全一致",只能要求"输出必须满足一组条件,满足率足够高"。


Eval 三要素

一个最小可用的 eval 体系需要三样东西:

1. 测试集:一组有代表性的输入-输出对

python 复制代码
eval_cases = [
    {
        "input": "一个有 SQL 注入漏洞的代码片段",
        "expected": "至少一条 finding 包含 'SQL injection' 或 'SQL 注入'",
        "category": "security",
        "difficulty": "easy"
    },
    {
        "input": "一个有 race condition 的并发代码",
        "expected": "至少一条 finding 提到并发/竞态/锁",
        "category": "concurrency",
        "difficulty": "hard"
    },
    # ... 35 个 case
]

测试集的设计原则:

  • 覆盖核心场景:不需要 1000 个 case,但必须覆盖最重要的 10-15 个场景类型
  • 有简单有困难:easy case 用来检测严重退化,hard case 用来检测能力上限
  • 有 gold case:3-5 个"标准答案"级别的 case,任何改动后这些必须通过

2. 评估函数:怎么判断输出"足够好"

这是 EDD 和 TDD 差异最大的地方。传统测试是 assertEqual(output, expected),AI eval 需要更灵活的判断。

三种评估策略,从硬到软:

python 复制代码
# 策略 1:确定性检查(最硬,不依赖 AI)
def check_format(output):
    """输出必须是合法 JSON,且包含 findings 数组"""
    data = json.loads(output)
    assert "findings" in data
    assert len(data["findings"]) > 0
    for f in data["findings"]:
        assert "file" in f and "line" in f and "issue" in f

# 策略 2:关键词/模式匹配(中等)
def check_content(output, case):
    """输出必须包含预期的关键信息"""
    if case["category"] == "security":
        keywords = ["SQL injection", "XSS", "注入", "漏洞"]
        assert any(kw in output for kw in keywords)

# 策略 3:AI 评估 AI(最软,用另一个模型打分)
def ai_judge(output, case):
    """让一个独立的模型评估输出质量"""
    score = judge_model.evaluate(
        criteria="输出是否正确识别了代码中的安全问题",
        output=output,
        reference=case["expected"]
    )
    assert score >= 0.7

推荐组合:策略 1 做门槛(不过就 FAIL),策略 2 做核心(覆盖主要场景),策略 3 做补充(处理难以硬编码的语义判断)。尽量少用策略 3------用 AI 评估 AI 会引入新的不确定性。

3. 基线:改动前的分数

复制代码
基线记录:
  日期: 2026-03-15
  prompt 版本: v2.3
  模型: claude-sonnet-4-6
  eval 分数:
    format_check:    35/35 (100%)
    content_check:   30/35 (86%)
    gold_case:       3/3   (100%)
    overall:         68/73 (93%)

每次改动后跑 eval,对比基线。如果分数下降超过阈值(比如 5%),就阻断这次变更。


非确定性回归测试:同一个 case 跑 N 次

这是 AI eval 独有的难题。传统测试跑一次就够了------确定性系统每次输出一样。AI 系统需要多次运行取统计值。

复制代码
Case #7: "检测未关闭的数据库连接"

跑 10 次的结果:
  Run 1: ✅ PASS --- 找到了未关闭连接
  Run 2: ✅ PASS --- 找到了未关闭连接
  Run 3: ❌ FAIL --- 漏掉了,只报了格式问题
  Run 4: ✅ PASS
  Run 5: ✅ PASS
  Run 6: ✅ PASS
  Run 7: ❌ FAIL --- 报了但描述不准确
  Run 8: ✅ PASS
  Run 9: ✅ PASS
  Run 10: ✅ PASS

通过率:8/10 = 80%

怎么判断这个 80% 是"正常"还是"退化了"?

复制代码
判断标准:对比基线时期的通过率

基线(prompt v2.3 时期):Case #7 的 10 次通过率 = 90%
当前(prompt v2.4 改动后):Case #7 的 10 次通过率 = 80%

下降了 10% → 超过 5% 阈值 → 标记为疑似退化,需要调查

关键点:

  • 单次 FAIL 不算退化,概率性系统允许偶尔失败
  • 通过率的显著下降才算退化,需要统计比较
  • gold case 例外:gold case 的标准是 100%,任何一次 FAIL 都需要调查

实战:从 0 搭建一个 Reviewer Agent 的 Eval

Step 1:收集初始测试集

不需要一开始就有 100 个 case。从 3 个 gold case 开始:

python 复制代码
gold_cases = [
    {
        "id": "gold_1",
        "name": "明显的空指针",
        "input": open("test_data/null_pointer.py").read(),
        "must_find": ["NoneType", "空指针", "null check"],
        "must_not_find": ["性能", "风格"],  # 不应该报无关问题
    },
    {
        "id": "gold_2",
        "name": "SQL 注入漏洞",
        "input": open("test_data/sql_injection.py").read(),
        "must_find": ["SQL", "injection", "参数化"],
    },
    {
        "id": "gold_3",
        "name": "无问题的干净代码",
        "input": open("test_data/clean_code.py").read(),
        "expected_findings": 0,  # 不应该误报
    },
]

gold case 的设计有一个容易忽略的点:必须包含一个"无问题"的 case。否则你只在测"能不能找到 bug",没在测"会不会误报"。

Step 2:定义评估函数

python 复制代码
def evaluate_review(output, case):
    results = {}
    
    # 格式检查(硬门槛)
    try:
        data = json.loads(output)
        results["format"] = "PASS"
    except:
        results["format"] = "FAIL"
        return results  # 格式都不对,后面不用看了
    
    # 内容检查
    findings_text = " ".join(f["issue"] for f in data.get("findings", []))
    
    if "must_find" in case:
        found = any(kw.lower() in findings_text.lower() for kw in case["must_find"])
        results["detection"] = "PASS" if found else "FAIL"
    
    if "must_not_find" in case:
        false_alarm = any(kw.lower() in findings_text.lower() for kw in case["must_not_find"])
        results["false_positive"] = "FAIL" if false_alarm else "PASS"
    
    if "expected_findings" in case:
        results["count"] = "PASS" if len(data["findings"]) == case["expected_findings"] else "FAIL"
    
    return results

Step 3:跑基线

bash 复制代码
python run_eval.py --prompt v1.0 --runs 5 --output baseline.json
复制代码
Eval Results (prompt v1.0, 5 runs per case):
─────────────────────────────────────────────
Case          Format   Detection   FP-Check   Avg
gold_1        5/5      5/5         4/5         93%
gold_2        5/5      4/5         5/5         93%
gold_3        5/5      n/a         5/5         100%
─────────────────────────────────────────────
Overall: 95%   ← 这就是你的基线

Step 4:改 prompt 后跑回归

复制代码
场景:有人想把 prompt 里 "找出所有缺陷" 改成 "找出最严重的 3 个缺陷"

跑 eval:
─────────────────────────────────────────────
Case          Format   Detection   FP-Check   Avg
gold_1        5/5      5/5         5/5         100%  ↑
gold_2        5/5      2/5         5/5         80%   ↓↓
gold_3        5/5      n/a         5/5         100%
─────────────────────────────────────────────
Overall: 90%   ← 比基线 95% 低了 5%

详细分析:
  gold_2 的 detection 从 4/5 降到 2/5
  原因:限制"最严重的 3 个"后,SQL 注入有时不被认为是"最严重"的
  
结论:这次 prompt 变更引入了回归,不应合并

没有 eval,这个退化要等线上用户投诉才能发现。有了 eval,改完 prompt 5 分钟就能知道。

Step 5:持续扩展测试集

每次线上发现一个 eval 没覆盖到的问题,就加一个 case:

复制代码
线上事件:Reviewer 漏掉了一个死锁问题
  → 加一个 case: test_data/deadlock.py
  → 加到 eval suite
  → 确认当前 prompt 在这个 case 上的通过率
  → 如果通过率低 → 优化 prompt → 再跑 eval → 确认不退化

这就是 EDD 的飞轮:线上问题 → 新 eval case → prompt 优化 → eval 验证 → 上线


和 CI/CD 集成

AI 系统的 CI 不是 PASS/FAIL 二元,而是分数制:

yaml 复制代码
# .github/workflows/ai-eval.yml
ai-eval:
  runs-on: ubuntu-latest
  steps:
    - name: Run eval suite
      run: python run_eval.py --runs 3 --output eval_result.json
    
    - name: Check regression
      run: |
        python check_regression.py \
          --baseline baseline.json \
          --current eval_result.json \
          --threshold 5  # 允许 5% 波动
python 复制代码
# check_regression.py
def check(baseline, current, threshold):
    baseline_score = baseline["overall_score"]
    current_score = current["overall_score"]
    diff = baseline_score - current_score
    
    if diff > threshold:
        print(f"REGRESSION: score dropped {diff}% ({baseline_score}% → {current_score}%)")
        sys.exit(1)  # 阻断合并
    
    # gold case 必须 100%
    for case in current["gold_cases"]:
        if case["pass_rate"] < 1.0:
            print(f"GOLD CASE FAIL: {case['name']}")
            sys.exit(1)
    
    print(f"PASS: score {current_score}% (baseline {baseline_score}%, diff {diff}%)")

常见误区

误区 1:"eval 太贵了,每次跑几十次 API 调用"

算一笔账:一次完整 eval(35 个 case × 3 次 × Sonnet)≈ 35 × 3 × 0.07 ≈ 7.35。一次线上退化被用户发现后的排查修复成本 ≈ 2 人天 ≈ $800+。eval 是最便宜的保险。

误区 2:"AI 输出不确定,所以没法做回归测试"

不是测"输出是否相同",是测"输出是否满足条件集"。10 次跑出 10 个不同的输出,但只要 10 次都通过所有检查 → 没有退化。

误区 3:"eval 写完就不用改了"

eval 本身也需要维护。模型能力提升后,原来的 hard case 可能变成 easy case,需要补充更难的;线上新发现的失败模式需要补充新 case。eval 是活的,不是一次性工程。


传统 TDD 防的是确定性 bug,EDD 防的是概率性退化。没有 eval 的 AI 系统,和没有测试的传统软件一样------改一行代码都心惊胆战。

相关推荐
米奇妙啊妙2 小时前
agent 学习 -模拟AI调用工具
人工智能·学习
试剂界的爱马仕2 小时前
AI学习实现:如何给基金实时估值?
大数据·人工智能·科技·学习·机器学习
笑不语2 小时前
从共病网络到可解释 AI:同济医院 10 分 SCI 全流程复现(R 语言)
开发语言·人工智能·r语言
xiangzhihong82 小时前
Claude Code系列教程之Claude Code 基础用法基础用法
人工智能
deephub2 小时前
2026年的 ReAct Agent架构解析:原生 Tool Calling 与 LangGraph 状态机
人工智能·大语言模型·agent·langgraph
淡海水2 小时前
【AI模型】概念-Token
人工智能·算法
数智化精益手记局2 小时前
什么是安全生产?解读安全生产的基本方针与核心要求
大数据·运维·人工智能·安全·信息可视化·自动化·精益工程
ManThink Technology2 小时前
KS31 4-20mA 模拟量采集器通过LoRaWAN 接入ThinkLink
人工智能·物联网
Zzj_tju2 小时前
大语言模型部署实战:生产环境怎么做高并发、监控、限流与故障恢复?
人工智能·语言模型·自然语言处理