第03章 人工评估:系统性收集人类判断

第03章 人工评估:系统性收集人类判断

“当机器无法判断好坏时,人类仍然是最终的裁判。” —— 评估工程实践


LLM-as-Judge 很强大,但对于需要高精度的评估(涉及专业判断、用户偏好、行业知识),人工评估仍然不可替代。本章讲的不是"让人随便看一看",而是如何系统性地设计和执行高质量的人工评估。


3.1 人工评估的设计原则

# ====================================================
# 人工评估系统设计
# ====================================================

"""
人工评估的常见失败原因:

1. 标注任务设计不清晰 → 标注员不知道怎么判断
   修复:提供详细的评分手册 + 示例

2. 标注员选择不当 → 标注员不具备判断所需的知识
   修复:明确标注员资质要求,必要时培训

3. 标注员内部一致性低 → 同一任务不同时间打了不同的分
   修复:加入重复案例,监控自一致性

4. 标注员间一致性低 → 不同人打了不同的分
   修复:培训期的校准练习,定期对齐会议

5. 顺序效应 → 看了一批高质量的案例后,对下一批的评分更苛刻
   修复:随机化案例顺序,定期重置基准
"""

import asyncio
import random
import statistics
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from openai import AsyncOpenAI
import json
import time

client = AsyncOpenAI()


@dataclass
class AnnotationTask:
    """一个标注任务"""
    task_id: str
    input_text: str
    output_text: str
    is_calibration: bool = False  # 是否是校准用的已知分案例
    gold_score: Optional[float] = None  # 校准案例的标准分数
    metadata: Dict = field(default_factory=dict)


@dataclass
class Annotation:
    """一条标注记录"""
    task_id: str
    annotator_id: str
    scores: Dict[str, float]  # dimension → score
    comments: str = ""
    duration_seconds: float = 0.0
    timestamp: float = field(default_factory=time.time)


class AnnotationQualityMonitor:
    """
    标注质量监控器
    
    监控:
    1. 标注员自一致性(同一个案例不同时间的分是否一致)
    2. 标注员间一致性(不同标注员对同一案例的分是否一致)
    3. 校准准确性(标注员在校准案例上的表现)
    4. 异常检测(答题时间异常短,可能是随机填写)
    """
    
    def __init__(self, calibration_threshold: float = 0.8):
        self.calibration_threshold = calibration_threshold
        self.annotations: List[Annotation] = []
    
    def add_annotation(self, annotation: Annotation):
        self.annotations.append(annotation)
    
    def calculate_inter_annotator_agreement(
        self, 
        task_id: str,
        dimension: str
    ) -> Optional[float]:
        """
        计算特定任务和维度上的标注员间一致性
        使用 Cohen's Kappa 的简化版(相关系数)
        """
        # 找出对这个任务打了分的所有标注员
        task_annotations = [
            a for a in self.annotations 
            if a.task_id == task_id and dimension in a.scores
        ]
        
        if len(task_annotations) < 2:
            return None
        
        scores = [a.scores[dimension] for a in task_annotations]
        
        if len(scores) < 2:
            return None
        
        # 简化:用分数的标准差衡量一致性(标准差越小,一致性越高)
        # 真实场景应该用 Cohen's Kappa 或 Krippendorff's Alpha
        std_dev = statistics.stdev(scores) if len(scores) > 1 else 0
        max_possible_std = 2.0  # 假设分数范围0-5
        
        agreement = 1.0 - (std_dev / max_possible_std)
        return max(0.0, agreement)
    
    def check_annotator_reliability(
        self, 
        annotator_id: str,
        calibration_tasks: List[AnnotationTask]
    ) -> Dict:
        """
        评估标注员的可靠性:
        - 在校准案例上的准确率
        - 平均评分时间(太快可能在随机填写)
        """
        
        annotator_annotations = [
            a for a in self.annotations 
            if a.annotator_id == annotator_id
        ]
        
        if not annotator_annotations:
            return {"status": "no_data"}
        
        # 检查校准案例准确率
        calibration_scores = []
        for task in calibration_tasks:
            if not task.is_calibration or task.gold_score is None:
                continue
            
            matching = next(
                (a for a in annotator_annotations if a.task_id == task.task_id),
                None
            )
            
            if matching:
                # 用第一个维度的分数和标准答案比较
                first_score = list(matching.scores.values())[0] if matching.scores else None
                if first_score is not None:
                    diff = abs(first_score - task.gold_score)
                    calibration_scores.append(1.0 - min(diff / 4.0, 1.0))  # 归一化
        
        calibration_accuracy = (
            statistics.mean(calibration_scores) 
            if calibration_scores else None
        )
        
        # 检查评分时间
        avg_duration = statistics.mean([a.duration_seconds for a in annotator_annotations])
        suspiciously_fast = avg_duration < 10  # 10秒以内完成一个任务很可疑
        
        # 检查分数分布(是否只给一个分数,如全给5分)
        all_scores_flat = [
            s for a in annotator_annotations 
            for s in a.scores.values()
        ]
        score_variance = statistics.variance(all_scores_flat) if len(all_scores_flat) > 1 else 0
        low_variance = score_variance < 0.1  # 几乎没有差异化
        
        return {
            "annotator_id": annotator_id,
            "total_annotations": len(annotator_annotations),
            "calibration_accuracy": calibration_accuracy,
            "avg_duration_seconds": avg_duration,
            "reliability_flags": {
                "too_fast": suspiciously_fast,
                "low_variance": low_variance,
                "calibration_fail": (
                    calibration_accuracy is not None and 
                    calibration_accuracy < self.calibration_threshold
                ),
            }
        }
    
    def generate_report(self) -> str:
        if not self.annotations:
            return "暂无标注数据"
        
        annotators = list(set(a.annotator_id for a in self.annotations))
        total = len(self.annotations)
        
        report = f"标注质量报告\n"
        report += f"总标注数: {total},标注员数: {len(annotators)}\n\n"
        
        avg_duration = statistics.mean([a.duration_seconds for a in self.annotations])
        report += f"平均标注时间: {avg_duration:.1f}秒/条\n"
        
        return report


# 演示人工评估流程
async def simulate_human_evaluation():
    print("=== 人工评估流程演示 ===\n")
    
    monitor = AnnotationQualityMonitor()
    
    # 模拟3个标注员对同一个案例打分
    task = AnnotationTask(
        task_id="t001",
        input_text="分析电动汽车市场的竞争格局",
        output_text="特斯拉领先,比亚迪快速追赶,传统车企转型中...",
    )
    
    # 模拟标注员打分(真实情况下是人工界面采集)
    simulated_annotations = [
        Annotation("t001", "ann_001", {"准确性": 4.0, "完整性": 3.0}, duration_seconds=45),
        Annotation("t001", "ann_002", {"准确性": 4.0, "完整性": 4.0}, duration_seconds=52),
        Annotation("t001", "ann_003", {"准确性": 3.0, "完整性": 3.0}, duration_seconds=38),
    ]
    
    for ann in simulated_annotations:
        monitor.add_annotation(ann)
    
    # 计算一致性
    agreement_accuracy = monitor.calculate_inter_annotator_agreement("t001", "准确性")
    agreement_completeness = monitor.calculate_inter_annotator_agreement("t001", "完整性")
    
    print(f"案例 t001 标注员间一致性:")
    print(f"  准确性维度: {agreement_accuracy:.2f}" if agreement_accuracy else "  准确性维度: 数据不足")
    print(f"  完整性维度: {agreement_completeness:.2f}" if agreement_completeness else "  完整性维度: 数据不足")
    
    # 0.8以上认为一致性良好
    if agreement_accuracy and agreement_accuracy >= 0.8:
        print("\n✅ 标注员间一致性良好,可以信任这批标注数据")
    else:
        print("\n⚠️ 标注员间分歧较大,建议增加讨论对齐,或使用平均值")
    
    print(monitor.generate_report())


asyncio.run(simulate_human_evaluation())

3.2 小规模高效人工评估策略

# ====================================================
# 高效人工评估:没有标注预算时的策略
# ====================================================

"""
现实情况:大多数 AI Agent 项目没有专业标注预算

解决策略:
1. 开发者自评 + 校准
   - 风险:主观偏见
   - 对策:使用盲评(隐藏版本信息),延迟评估(隔天再看)

2. 用户反馈转化
   - 利用现有用户反馈(点赞/踩、重新生成率)
   - 主动设计简单的1-3分评价界面

3. 聚焦精评
   - 不是每条输出都评,而是重点评估:
     a. 最坏的案例(找到问题)
     b. 最好的案例(建立基准)
     c. 随机采样(代表性样本)

4. 精简评估维度
   - 从5-8个维度压缩到1-2个最关键的维度
   - 甚至只做偏好评估(A 和 B 哪个更好?)
"""

class LeanEvaluationPlan:
    """
    低成本高效评估计划
    专为资源有限的场景设计
    """
    
    def __init__(self, time_budget_hours: float, evaluators: int):
        self.time_budget_hours = time_budget_hours
        self.evaluators = evaluators
        
        # 假设每条评估需要3分钟
        self.total_capacity = int(time_budget_hours * 60 / 3 * evaluators)
    
    def generate_sampling_plan(
        self, 
        total_outputs: int,
        high_risk_count: int = 0
    ) -> Dict:
        """
        生成采样计划:在预算内最大化覆盖
        """
        budget = self.total_capacity
        
        # 优先级1:高风险案例(100%评估)
        high_risk_eval = min(high_risk_count, int(budget * 0.3))
        budget -= high_risk_eval
        
        # 优先级2:随机采样
        random_sample = min(
            max(50, int(total_outputs * 0.1)),  # 至少50条,或10%
            budget
        )
        
        return {
            "total_capacity": self.total_capacity,
            "high_risk_cases": high_risk_eval,
            "random_sample": random_sample,
            "total_to_evaluate": high_risk_eval + random_sample,
            "coverage_rate": (high_risk_eval + random_sample) / total_outputs,
            "recommendation": (
                "样本量充足,结果可信" if random_sample >= 100
                else "样本量偏小,增加评估员或延长时间"
            )
        }
    
    def suggest_quick_eval_format(self) -> str:
        """
        根据预算推荐最简化的评估格式
        """
        if self.total_capacity < 50:
            return "偏好评估(A vs B 哪个更好?)— 最快,每条只需30秒"
        elif self.total_capacity < 200:
            return "1-3分整体评分 — 简单,每条约1分钟"
        else:
            return "多维度评分(2-3个维度)— 细致,每条约3分钟"


# 演示低成本评估计划
plan = LeanEvaluationPlan(time_budget_hours=4, evaluators=2)
sampling = plan.generate_sampling_plan(total_outputs=500, high_risk_count=20)

print("=== 低成本评估计划 ===\n")
print(f"时间预算: 4小时,2名评估员")
print(f"总评估能力: {sampling['total_capacity']} 条")
print(f"\n采样分配:")
print(f"  高风险案例: {sampling['high_risk_cases']} 条")
print(f"  随机采样: {sampling['random_sample']} 条")
print(f"  总计: {sampling['total_to_evaluate']} 条")
print(f"  覆盖率: {sampling['coverage_rate']:.1%}")
print(f"\n建议评估格式: {plan.suggest_quick_eval_format()}")
print(f"评估建议: {sampling['recommendation']}")

本章小结

  1. 人工评估的质量取决于设计,而不是执行:如果标注任务设计不清晰,增加标注员只会增加更多的噪声。在开始收集数据之前,先把评估协议、评分量表和示例准备好。

  2. 标注员间一致性是评估数据可信度的核心指标:如果两个人对同一个输出的评分差异超过一个等级,说明评估标准不清晰,或标注员需要更多培训。目标是一致性(相关系数)> 0.8。

  3. 校准案例是发现低质量标注员的最有效工具:在任务集中随机混入"已知答案"的案例,可以快速识别随机作答或理解有偏差的标注员,而不需要昂贵的全数据校验。

  4. 资源有限时,聚焦精评而不是广评:与其粗略评估1000条,不如精细评估100条(覆盖高风险案例 + 随机样本)。评估质量比数量更重要。

  5. 偏好评估(A vs B)通常比绝对打分更可靠:人类更擅长做比较判断,而不是给出绝对分数。如果预算有限,让人判断"哪个更好"比让人打1-5分通常更一致。

# 核心行动:建立你的首批人工评估数据
def create_annotation_batch(agent_outputs: list, sample_size: int = 30) -> list:
    """
    从一批输出中创建标注任务:
    - 10条随机采样(代表性)
    - 10条最短输出(可能质量差)
    - 10条最长输出(复杂案例)
    """
    sorted_by_len = sorted(agent_outputs, key=lambda x: len(x.get("output", "")))
    
    tasks = (
        random.sample(agent_outputs, min(10, len(agent_outputs))) +
        sorted_by_len[:10] +
        sorted_by_len[-10:]
    )
    
    return tasks[:sample_size]  # 去重后不超过 sample_size

本章提示词模板

【模板1:标注任务设计提示词】
我需要设计一个人工评估任务,评估我的 AI Agent 的输出质量:

Agent 类型:{agent_type}
评估维度:{dimensions}(如:准确性、完整性、可读性)
评估人:{evaluator_profile}(如:没有专业背景的内部员工)
每条任务的预期时间:{time_per_task}分钟

请设计:
1. 评估界面应该展示什么信息(输入?输出?参考答案?)
2. 每个维度的评分量表(1-5分,每分的明确定义)
3. 3-5个校准案例(含标准答案和评分理由)
4. 标注员培训材料的核心内容(一页纸)
5. 如何处理争议案例(标注员分歧较大时)
【模板2:人工评估结果分析提示词】
我完成了一轮人工评估,有以下结果:

评估样本量:{sample_size}
各维度平均分:{scores_by_dimension}
标注员间一致性:{agreement_scores}
校准案例准确率:{calibration_accuracy}
评分分布:{score_distribution}(如:1分占10%,2分占15%...)

请帮我分析:
1. 这批评估数据的可信度如何?(根据一致性和校准准确率)
2. 哪个维度的问题最突出?
3. 分数分布是否存在异常(如评分过于集中)?
4. 根据评估结果,最优先需要改进的是什么?
5. 下一轮评估应该增加哪类案例?

→ 第04章:LLM-as-Judge:用AI评估AI