第01章 为什么评估难:LLM输出的不确定性与评估困境

第01章 为什么评估难:LLM输出的不确定性与评估困境

“你无法改进你无法测量的东西。” —— 彼得·德鲁克


绝大多数 Agent 开发者都遇到过同样的困境:改了 Prompt,感觉好了一点,但说不清楚到底好在哪里,下次改了会不会又退回去。这种感觉不是个人能力的问题,而是 LLM 评估本质上就是困难的。


1.1 传统软件 vs LLM 输出:为什么评估完全不同

# ====================================================
# 评估困境:LLM 输出的独特挑战
# ====================================================

"""
传统软件评估的世界:
    input("1 + 1") → 期望: 2
    如果输出 2 → ✅ PASS
    如果输出 3 → ❌ FAIL
    
    规则明确,答案唯一,评估自动化,100% 可重复。

LLM 输出的世界:
    input("分析苹果公司的竞争优势") → 期望: ???
    
    可能的好答案有无数个:
    - "苹果有强大的生态系统、品牌溢价和供应链管理能力..."
    - "苹果的护城河在于硬件软件服务的垂直整合..."
    - "从波特五力模型看,苹果在买方议价能力上有显著优势..."
    
    所有这些都可能是好答案,也都可能不够好,取决于:
    - 评估者的背景和偏好
    - 任务的具体使用场景
    - 当前的质量标准
"""

import asyncio
import json
import statistics
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Any, Callable
from openai import AsyncOpenAI

client = AsyncOpenAI()


# 演示 LLM 输出的随机性有多真实
async def demonstrate_nondeterminism():
    """
    运行同一个 Prompt 5次,观察输出的差异
    """
    print("=== LLM 输出的非确定性 ===\n")
    print("问题:简短描述AI的主要优势(30字内)\n")
    
    outputs = []
    for i in range(5):
        response = await client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": "简短描述AI的主要优势(30字以内)"}],
            max_tokens=80,
            temperature=0.9,  # 高温度,增加随机性
        )
        output = response.choices[0].message.content
        outputs.append(output)
        print(f"第{i+1}次: {output}")
    
    # 分析差异
    lengths = [len(o) for o in outputs]
    print(f"\n字符数范围: {min(lengths)} - {max(lengths)}")
    print(f"字符数标准差: {statistics.stdev(lengths):.1f}")
    print("\n观察:即使是同一个问题,5次运行也产生了5个不同的答案。")
    print("这意味着:你不能用'输出是否等于期望值'来判断好坏。")


asyncio.run(demonstrate_nondeterminism())

1.2 评估的三大陷阱

# ====================================================
# 常见评估错误:开发者最容易犯的三个错误
# ====================================================

"""
陷阱 #1:用你自己当裁判
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

错误做法:
    改了 Prompt 之后,自己读一读,感觉好多了 → 发布

问题:
    你是这个系统的创建者,对它有情感偏见
    你知道你想要什么,所以你"看到"了你想看到的
    你的偏好不一定代表用户的偏好
    你无法评估每个可能的输入场景

更好的做法:
    制定评估标准(写下来,不是记在脑子里)
    让没有参与开发的人评估
    或者用 LLM-as-Judge(第4章详细讲)

陷阱 #2:在太少的样本上测试
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

错误做法:
    改了 Prompt,测试了5个例子,都通过了 → 发布

问题:
    5个例子无法代表用户的真实分布
    你很可能选了你知道能成功的例子(确认偏误)
    边缘情况(长输入、模糊问题、特殊格式)通常不在这5个里

更好的做法:
    建立最少50个案例的测试集(覆盖常见+边缘)
    测试集要从真实用户数据中采样,不是手工构造

陷阱 #3:只测最新版本,不做对比
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

错误做法:
    新 Prompt 跑完,80%的案例通过 → 发布(感觉不错)

问题:
    如果旧 Prompt 也能通过85%,那新版本是退步
    如果某一类案例从100%通过降到60%,你不知道

更好的做法:
    始终与 baseline 比较(旧版本)
    按类别分析,不只看总体分数
    A/B 测试确认提升(第6章)
"""


@dataclass 
class EvaluationPitfall:
    """记录一个评估陷阱"""
    name: str
    description: str
    symptom: str        # 怎么发现自己犯了这个错误
    fix: str            # 修复方法


COMMON_PITFALLS = [
    EvaluationPitfall(
        name="主观裁判偏误",
        description="用自己的主观感受代替系统性评估",
        symptom="当被问到'你的评估标准是什么'时,无法给出书面的、可量化的答案",
        fix="在动手评估之前,先把评估维度和打分标准写成文档",
    ),
    EvaluationPitfall(
        name="小样本幸存者偏误",
        description="在少量手工构造的案例上测试,忽略了真实分布",
        symptom="测试案例都是自己写的,而不是从真实用户数据中采样的",
        fix="从生产日志中随机采样100个真实案例,并覆盖边缘情况",
    ),
    EvaluationPitfall(
        name="无基线比较",
        description="只知道当前版本的绝对分数,不知道相对上一版本的变化",
        symptom="说不出来新版本比旧版本好多少,或在哪些维度退步了",
        fix="每次评估都和 baseline 版本同时运行,做差异分析",
    ),
    EvaluationPitfall(
        name="指标与业务脱钩",
        description="测量的指标和用户真正关心的价值无关",
        symptom="技术指标很好,但用户投诉没有减少",
        fix="从业务结果倒推,问'什么指标的提升意味着用户更满意'",
    ),
]


def print_pitfall_analysis():
    print("\n=== Agent 评估常见陷阱 ===\n")
    for i, pitfall in enumerate(COMMON_PITFALLS, 1):
        print(f"陷阱 #{i}: {pitfall.name}")
        print(f"  问题: {pitfall.description}")
        print(f"  发现信号: {pitfall.symptom}")
        print(f"  解决方案: {pitfall.fix}")
        print()


print_pitfall_analysis()

1.3 评估的四个层次

# ====================================================
# 评估层次:从"能运行"到"持续改进"
# ====================================================

"""
层次1:冒烟测试(Smoke Test)
    验证 Agent 基本上不会崩溃
    问题:输入进去了,有输出吗?格式对吗?
    典型指标:成功率、格式合规率
    
层次2:质量评估(Quality Evaluation)
    验证 Agent 的输出质量达到标准
    问题:输出准确吗?有用吗?符合要求吗?
    典型指标:准确率、相关性得分、满足度评分
    
层次3:对比评估(Comparative Evaluation)
    比较不同版本或配置
    问题:A 比 B 好吗?好多少?在哪些场景下好?
    典型指标:胜率、改善幅度、按类别分析
    
层次4:持续监控(Continuous Monitoring)
    在生产中实时追踪质量变化
    问题:今天的质量和上周一样吗?有没有退化?
    典型指标:质量趋势、退化告警、用户满意度
"""

@dataclass
class EvaluationLevel:
    level: int
    name: str
    question: str
    metrics: List[str]
    when_to_use: str
    minimum_sample: int


EVALUATION_LEVELS = [
    EvaluationLevel(
        level=1,
        name="冒烟测试",
        question="Agent 基本能运行吗?",
        metrics=["成功率", "格式合规率", "超时率"],
        when_to_use="每次代码变更后,作为 CI 门禁",
        minimum_sample=20,
    ),
    EvaluationLevel(
        level=2,
        name="质量评估",
        question="输出质量达到标准了吗?",
        metrics=["准确率", "相关性分(1-5)", "完整性分(1-5)", "有害内容率"],
        when_to_use="每次发布前,和用户看到新版本之前",
        minimum_sample=100,
    ),
    EvaluationLevel(
        level=3,
        name="对比评估",
        question="新版本比旧版本好吗?",
        metrics=["对比胜率", "改善幅度", "回退率", "分类别表现"],
        when_to_use="评估 Prompt 改进、模型升级、参数调整的效果",
        minimum_sample=200,
    ),
    EvaluationLevel(
        level=4,
        name="持续监控",
        question="生产中的质量是否稳定?",
        metrics=["实时质量分", "退化告警", "用户满意度", "问题分类"],
        when_to_use="系统上线后,持续运行",
        minimum_sample=500,
    ),
]


def print_evaluation_roadmap():
    print("\n=== Agent 评估层次路线图 ===\n")
    for level in EVALUATION_LEVELS:
        print(f"层次 {level.level}:{level.name}")
        print(f"  核心问题: {level.question}")
        print(f"  关键指标: {', '.join(level.metrics)}")
        print(f"  使用时机: {level.when_to_use}")
        print(f"  最小样本量: {level.minimum_sample}")
        print()
    
    print("建议起步策略:")
    print("  第1周: 建立层次1(冒烟测试)→ 加入 CI,每次提交自动运行")
    print("  第2周: 建立层次2(质量评估)→ 定义评分标准,建立基准")
    print("  第4周: 建立层次3(对比评估)→ 每次改进都和上一版本比较")
    print("  上线后: 建立层次4(持续监控)→ 生产质量的实时追踪")


print_evaluation_roadmap()

本章小结

  1. LLM 输出的非确定性是评估困难的根本原因:同一个输入,不同时间运行会得到不同答案,而且多个不同答案都可能同样"正确"。这就是为什么不能像传统软件那样用"是否等于期望值"来评估。

  2. 主观感受是评估中最危险的成分:开发者对自己的系统有情感偏见,容易看到自己想看到的。在动手评估之前,先把评估标准写成文档——如果写不出来,说明标准还不清晰。

  3. 没有基线的评估没有意义:知道当前版本得了 80 分没有意义,除非你知道上一个版本得了多少分。每次评估都必须同时运行基线版本作为对比。

  4. 评估要从业务目标出发,不能只看技术指标:BLEU 分高了,用户投诉可能还是一样多。正确的做法是先问"什么业务结果变好了意味着 Agent 在变好",然后倒推出技术指标。

  5. 评估是一个系统,不是一次行为:从冒烟测试、质量评估、对比评估到持续监控,不同层次的评估在不同时机发挥作用。建立完整的评估体系,而不是在出了问题后才想起来评估一下。

# 核心行动:今天就给你的 Agent 建立最基础的评估框架
def create_minimal_eval_framework(agent_func, test_cases: List[dict]) -> dict:
    """
    最小可行的评估框架
    test_cases: [{"input": "...", "expected_contains": ["关键词1", "关键词2"]}]
    """
    results = []
    for case in test_cases:
        # 简单的关键词检查(入门级评估)
        output = agent_func(case["input"])
        passed = all(kw in output for kw in case.get("expected_contains", []))
        results.append({"input": case["input"], "passed": passed})
    
    pass_rate = sum(1 for r in results if r["passed"]) / len(results)
    return {"pass_rate": pass_rate, "details": results}

本章提示词模板

【模板1:评估维度设计提示词】
我有一个 AI Agent,负责:{agent_description}
它的主要输出是:{output_type}

请帮我设计评估维度:
1. 对用户最重要的3个质量维度是什么(按重要性排序)?
2. 每个维度如何用1-5分量化?请给出每分值的具体描述
3. 有哪些是"一票否决"的硬性要求(满足才能发布)?
4. 我应该用哪些代表性案例来测试这些维度?

输出:一份可以直接发给标注员的评分手册
【模板2:评估计划制定提示词】
我准备对 Agent 进行正式评估,以下是背景:

当前状态:{current_state}(如:首次评估/版本对比/回归测试)
可用资源:{resources}(如:3天时间,无预算,只有1人)
最关心的问题:{key_question}(如:新Prompt是否真的更好)

请给出:
1. 在我的资源限制下,最务实的评估方案
2. 最小测试集规模(多少个案例才够)
3. 如何快速建立一个基线版本
4. 评估结束后,我应该如何根据结果做决策

→ 第02章:评估框架设计:从业务目标到可测量指标