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

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

“不是所有能计数的东西都重要,也不是所有重要的东西都能计数。” —— 威廉·布鲁斯·卡梅伦


评估的最大陷阱是测了很多,但测的都是次要的东西。本章从业务目标出发,设计一套真正能指导决策的评估框架——让每一个指标都和用户价值挂钩。


2.1 从业务目标推导评估指标

# ====================================================
# 评估框架:从业务出发设计指标体系
# ====================================================

"""
错误的起点:
    "我要测准确率" → 准确率对这个业务有意义吗?
    "我要测 BLEU 分" → 用户关心 BLEU 吗?

正确的起点:
    "用户用这个 Agent 想达成什么目的?"
    "他们什么时候会感到满意/失望?"
    然后才是:测什么指标能反映这些?

示例推导链:

业务:客服 Agent(回答产品问题)
     ↓
用户目标:得到准确的产品信息,快速解决问题
     ↓
满意的信号:
    - 问题被准确回答了(准确性)
    - 不需要再次提问(完整性)
    - 回答容易理解(可读性)
    - 没有让人不安的内容(安全性)
     ↓
评估指标:
    - 事实准确率(核心:与官方文档对比)
    - 问题完整覆盖率(核心:所有问题都被回答了吗)
    - 阅读难度(次要)
    - 有害内容率(门禁指标:任何有害输出都不接受)
"""

from dataclasses import dataclass, field
from typing import List, Dict, Optional, Any
from enum import Enum
import asyncio
import json
from openai import AsyncOpenAI

client = AsyncOpenAI()


class MetricType(Enum):
    GATE = "gate"           # 门禁指标:必须满足,否则不发布
    PRIMARY = "primary"     # 主要指标:直接体现用户价值,优化的主要目标
    SECONDARY = "secondary" # 次要指标:辅助分析,不单独驱动决策
    GUARDRAIL = "guardrail" # 保护指标:防止优化主要指标时损害其他方面


@dataclass
class EvalMetric:
    """一个评估指标的完整定义"""
    name: str
    metric_type: MetricType
    description: str
    how_to_measure: str            # 具体如何测量
    scale: str                     # 如 "0-1 (0最差,1最好)" 或 "bool"
    threshold: Optional[float]     # 通过/失败的阈值
    weight: float = 1.0            # 在综合评分中的权重


@dataclass
class EvalFramework:
    """
    一个完整的评估框架:
    定义了针对某类 Agent 任务的完整评估体系
    """
    name: str
    target_agent: str
    user_goal: str
    metrics: List[EvalMetric]
    
    def get_gate_metrics(self) -> List[EvalMetric]:
        return [m for m in self.metrics if m.metric_type == MetricType.GATE]
    
    def get_primary_metrics(self) -> List[EvalMetric]:
        return [m for m in self.metrics if m.metric_type == MetricType.PRIMARY]
    
    def calculate_score(self, metric_scores: Dict[str, float]) -> float:
        """
        计算综合评分
        1. 先检查门禁指标:任何门禁指标不达标 → 直接返回0
        2. 加权平均主要指标
        """
        # 检查门禁指标
        for metric in self.get_gate_metrics():
            score = metric_scores.get(metric.name, 0)
            threshold = metric.threshold or 1.0
            if score < threshold:
                return 0.0  # 门禁失败,直接0分
        
        # 加权平均主要指标
        primary = self.get_primary_metrics()
        if not primary:
            return 0.0
        
        total_weight = sum(m.weight for m in primary)
        weighted_sum = sum(
            metric_scores.get(m.name, 0) * m.weight 
            for m in primary
        )
        
        return weighted_sum / total_weight if total_weight > 0 else 0.0
    
    def print_framework(self):
        print(f"\n评估框架: {self.name}")
        print(f"目标 Agent: {self.target_agent}")
        print(f"用户目标: {self.user_goal}")
        print()
        
        for metric_type, label in [
            (MetricType.GATE, "门禁指标(必须满足)"),
            (MetricType.PRIMARY, "主要指标(优化目标)"),
            (MetricType.SECONDARY, "次要指标(参考用)"),
            (MetricType.GUARDRAIL, "保护指标(防退化)"),
        ]:
            metrics = [m for m in self.metrics if m.metric_type == metric_type]
            if metrics:
                print(f"  {label}:")
                for m in metrics:
                    threshold_str = f",阈值: {m.threshold}" if m.threshold else ""
                    print(f"    - {m.name}: {m.description}{threshold_str}")
        print()


# 示例:为内容生成 Agent 设计评估框架
content_agent_framework = EvalFramework(
    name="内容生成评估框架",
    target_agent="市场内容生成 Agent",
    user_goal="快速获得可直接使用或稍作修改的高质量市场内容",
    metrics=[
        EvalMetric(
            name="有害内容",
            metric_type=MetricType.GATE,
            description="内容不含歧视、误导或违规信息",
            how_to_measure="LLM-as-Judge + 关键词过滤",
            scale="bool (True=通过)",
            threshold=1.0,
        ),
        EvalMetric(
            name="事实准确性",
            metric_type=MetricType.PRIMARY,
            description="内容中的事实和数据是否准确",
            how_to_measure="对照参考文档,人工或LLM核查",
            scale="0-1 (1=完全准确)",
            threshold=0.95,
            weight=2.0,  # 权重更高,因为错误事实代价最大
        ),
        EvalMetric(
            name="任务完成度",
            metric_type=MetricType.PRIMARY,
            description="是否回答了 Brief 中的所有关键问题",
            how_to_measure="逐一检查 Brief 的要求是否被覆盖",
            scale="0-1",
            threshold=0.8,
            weight=1.5,
        ),
        EvalMetric(
            name="可读性",
            metric_type=MetricType.SECONDARY,
            description="内容是否流畅、易于阅读",
            how_to_measure="人工评分 1-5,或 LLM-as-Judge",
            scale="1-5 (5=非常易读)",
            threshold=None,  # 次要指标,不设阈值
            weight=1.0,
        ),
        EvalMetric(
            name="字数符合要求",
            metric_type=MetricType.GUARDRAIL,
            description="输出字数在要求范围内(±20%)",
            how_to_measure="直接计数",
            scale="bool",
            threshold=1.0,
        ),
    ]
)

content_agent_framework.print_framework()

# 演示评分计算
sample_scores = {
    "有害内容": 1.0,       # 通过门禁
    "事实准确性": 0.92,    # 略低于阈值 0.95
    "任务完成度": 0.85,
    "可读性": 4.0 / 5.0,
    "字数符合要求": 1.0,
}

final_score = content_agent_framework.calculate_score(sample_scores)
print(f"样本综合评分: {final_score:.2f}")
print("(事实准确性 0.92 低于阈值 0.95,但不是门禁指标,所以不归零)")

2.2 评估协议:让不同人的评估可以比较

# ====================================================
# 评估协议:标准化评估过程
# ====================================================

"""
问题:
    你给输出打了 4 分,同事打了 2 分
    你们不是在测不同的东西,是对"4分"的理解不同

解决:
    评估协议 = 打分标准 + 评估步骤 + 判断规则
    在开始评估之前,每个人都要看这份协议
    协议要包含足够多的示例,让不同人得到一致的结论
"""

@dataclass
class ScoringRubric:
    """评分量表:定义每个分数的含义"""
    dimension: str
    scores: Dict[int, str]  # 分数 → 描述
    examples: Dict[int, str]  # 分数 → 示例


TASK_COMPLETION_RUBRIC = ScoringRubric(
    dimension="任务完成度",
    scores={
        1: "几乎没有完成任务,主要内容缺失或完全偏题",
        2: "完成了不到一半的要求,有重大遗漏",
        3: "完成了主要要求,但有明显遗漏或部分偏差",
        4: "基本完成所有要求,只有细节上的小问题",
        5: "完整、准确地完成了所有要求,无明显问题",
    },
    examples={
        1: "问题:分析AI市场竞争格局。输出:介绍了AI的历史发展。",
        2: "问题:同上。输出:只提到了OpenAI,没有提其他公司。",
        3: "问题:同上。输出:提到了主要公司,但没有比较优劣势。",
        4: "问题:同上。输出:比较了各公司,但没有提到小型创业公司。",
        5: "问题:同上。输出:全面比较了大中小各类玩家,有洞察有数据。",
    }
)


class EvaluationProtocol:
    """
    标准评估协议:确保不同评估者得到一致的结果
    """
    
    def __init__(self, framework: EvalFramework):
        self.framework = framework
        self.rubrics: Dict[str, ScoringRubric] = {}
    
    def add_rubric(self, rubric: ScoringRubric):
        self.rubrics[rubric.dimension] = rubric
    
    async def evaluate_single(
        self, 
        input_text: str,
        output_text: str,
        use_llm_judge: bool = True
    ) -> Dict[str, float]:
        """
        对单个(输入,输出)对进行评估
        """
        scores = {}
        
        if use_llm_judge:
            scores = await self._llm_evaluate(input_text, output_text)
        
        return scores
    
    async def _llm_evaluate(self, input_text: str, output_text: str) -> Dict[str, float]:
        """用 LLM 作为评估者"""
        
        # 构建评分量表说明
        rubric_descriptions = []
        for metric in self.framework.get_primary_metrics():
            if metric.name in self.rubrics:
                rubric = self.rubrics[metric.name]
                rubric_text = f"\n{metric.name}({metric.scale}):\n"
                for score, desc in rubric.scores.items():
                    rubric_text += f"  {score}分: {desc}\n"
                rubric_descriptions.append(rubric_text)
        
        rubric_text = "\n".join(rubric_descriptions) if rubric_descriptions else "按1-5分评估整体质量"
        
        response = await client.chat.completions.create(
            model="gpt-4o",
            messages=[{
                "role": "user",
                "content": f"""
请根据以下评分标准,评估AI的回复质量。

用户输入:{input_text}

AI回复:{output_text}

评分标准:
{rubric_text}

同时评估:
- 有害内容(0=有害内容,1=无害)
- 整体质量(1-5分)

以JSON输出:
{{
  "有害内容": 0或1,
  "整体质量": 1-5的整数,
  "各维度说明": "简要说明评分理由"
}}
"""
            }],
            response_format={"type": "json_object"},
            max_tokens=300,
        )
        
        result = json.loads(response.choices[0].message.content)
        
        return {
            "有害内容": result.get("有害内容", 1),
            "整体质量": result.get("整体质量", 3) / 5.0,  # 归一化到0-1
        }


# 演示评估协议
async def demo_evaluation_protocol():
    print("\n=== 评估协议演示 ===\n")
    
    protocol = EvaluationProtocol(content_agent_framework)
    protocol.add_rubric(TASK_COMPLETION_RUBRIC)
    
    test_input = "分析2025年生成式AI市场的主要趋势"
    test_output = """2025年生成式AI市场呈现三大趋势:
    1. 多模态能力成为标配,主流模型都支持文字、图像、音频的混合输入
    2. 企业级应用加速落地,尤其在客服、内容生产和代码辅助领域
    3. 开源模型缩小与闭源模型的差距,Llama系列在成本敏感场景广泛采用"""
    
    scores = await protocol.evaluate_single(test_input, test_output)
    
    print(f"输入: {test_input}")
    print(f"输出(节选): {test_output[:100]}...")
    print(f"\n评分结果: {scores}")


asyncio.run(demo_evaluation_protocol())

本章小结

  1. 评估指标必须从业务目标出发,逆向推导:先问"用户什么时候满意",再问"什么指标能反映这个"。直接从技术指标出发,很可能测了很多不重要的东西。

  2. 指标要分层:门禁 > 主要 > 次要 > 保护:门禁指标是红线(任何违反都不发布);主要指标是优化目标;次要指标是参考;保护指标防止过度优化主要指标而牺牲其他方面。

  3. 评估协议比评估结果更重要:协议定义了"4分是什么意思",有了协议才能对比不同人、不同时期的评估结果。没有协议,评估数据是噪声。

  4. 评分量表要有足够多的具体示例:抽象的描述(如"回答质量好")无法指导评估员。每个分数值都要有真实例子,让人读了之后能立刻判断手头的案例属于哪个分数。

  5. 门禁指标不参与加权平均,直接归零:如果一个输出含有有害内容,无论其他指标多高都不应该发布。在代码中要显式处理这个逻辑,不能用平均分稀释掉。

# 核心行动:用这个模板设计你的评估框架
YOUR_FRAMEWORK = EvalFramework(
    name="你的 Agent 评估框架",
    target_agent="填写你的 Agent 描述",
    user_goal="填写用户使用这个 Agent 的核心目标",
    metrics=[
        EvalMetric("有害内容", MetricType.GATE, "不含违规内容", "LLM检测", "bool", 1.0),
        EvalMetric("准确性", MetricType.PRIMARY, "信息准确", "对照事实核查", "0-1", 0.9, weight=2.0),
        EvalMetric("完整性", MetricType.PRIMARY, "覆盖所有要求", "逐项检查", "0-1", 0.8, weight=1.5),
    ]
)

本章提示词模板

【模板1:评估指标推导提示词】
我有一个 AI Agent:{agent_description}

用户使用它的主要目的:{user_goal}

请帮我推导评估指标:
1. 用户"感到满意"的具体信号是什么?(3-5个)
2. 用户"感到失望"的具体信号是什么?(3-5个)
3. 什么是绝对不能接受的(门禁条件)?
4. 基于以上,推荐的核心评估维度(按重要性排序)
5. 每个维度如何量化(1-5分 / 0-1 / 百分比)

请避免推荐技术指标(如BLEU、困惑度),除非它们和用户价值直接相关。
【模板2:评分量表设计提示词】
我需要为以下评估维度设计详细的评分量表:

维度名称:{dimension_name}
描述:{description}
分数范围:1-5分

请为每个分数(1、2、3、4、5)提供:
1. 简洁的定义(一句话)
2. 一个真实场景下的具体例子(不超过50字)

要求:各分数之间有清晰的区分边界,避免模糊地带。
最终量表要让没有参与 Agent 开发的人也能准确使用。

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