第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())
本章小结
-
评估指标必须从业务目标出发,逆向推导:先问"用户什么时候满意",再问"什么指标能反映这个"。直接从技术指标出发,很可能测了很多不重要的东西。
-
指标要分层:门禁 > 主要 > 次要 > 保护:门禁指标是红线(任何违反都不发布);主要指标是优化目标;次要指标是参考;保护指标防止过度优化主要指标而牺牲其他方面。
-
评估协议比评估结果更重要:协议定义了"4分是什么意思",有了协议才能对比不同人、不同时期的评估结果。没有协议,评估数据是噪声。
-
评分量表要有足够多的具体示例:抽象的描述(如"回答质量好")无法指导评估员。每个分数值都要有真实例子,让人读了之后能立刻判断手头的案例属于哪个分数。
-
门禁指标不参与加权平均,直接归零:如果一个输出含有有害内容,无论其他指标多高都不应该发布。在代码中要显式处理这个逻辑,不能用平均分稀释掉。
# 核心行动:用这个模板设计你的评估框架
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 开发的人也能准确使用。