Prompt评估体系-从感觉优化到数据驱动
> **本文适合谁**
Prompt 评估体系:从感觉优化到数据驱动
本文适合谁
准备把 Prompt 优化从凭感觉调变成有数据支撑的开发者。建立评估体系不是学术工作,是让 Prompt 迭代有可靠判断依据的工程实践。
Prompt 工程的最大陷阱不是写不出好 Prompt,而是不知道什么叫"好"。工程师改了几个词,凭感觉觉得"好多了",上线后用户投诉反而增加——这种情况在没有评估体系的团队里反复上演。
本篇建立一套可量化、可重复的 Prompt 评估流程,让每一次改动都有数据支撑。
为什么需要评估体系
Prompt 评估四级体系——从主观评估到基准测试,评估的科学性随阶段提升
手工测试 Prompt 有三个问题:
覆盖率不足。工程师测试时通常只想到"正常用法",忽略边界案例、恶意输入、模糊查询。一个客服 Prompt 可能在 20 个测试问题上表现完美,但在第 21 个真实用户的奇怪问法上彻底失效。
无法量化对比。"版本 B 感觉比版本 A 好"不是结论。如果不能给出"版本 B 在格式合规率上提升了 15%,在相关性得分上提升了 0.08",就没办法在团队内建立共识,也无法在回归时检测退步。
人工评估不可扩展。每次改动都人工阅读 100 条输出,成本极高,评估者本人也会产生疲劳偏差。随着项目迭代,评估频次自然下降,最终退化为"感觉驱动"。
评估体系解决的核心问题是:Prompt 改动后,如何用数据证明变好了还是变差了。
构建测试集
测试集是评估体系的基础。一个高质量的测试集由三类案例组成:
TestCase 数据结构
from dataclasses import dataclass, field
from typing import Optional, Any
from enum import Enum
class CaseType(Enum):
POSITIVE = "positive" # 正例:模型应该正确处理的典型输入
NEGATIVE = "negative" # 负例:模型应该拒绝或谨慎处理的输入
EDGE = "edge" # 边界案例:模糊、极端或不寻常的输入
@dataclass
class TestCase:
"""单条测试案例"""
id: str
case_type: CaseType
user_input: str
# 期望输出(可选,用于精确匹配评估)
expected_output: Optional[str] = None
# 期望包含的关键词(用于软匹配)
expected_keywords: list[str] = field(default_factory=list)
# 期望不出现的内容(用于负例验证)
forbidden_patterns: list[str] = field(default_factory=list)
# 该案例的评估权重(某些关键案例可以加权)
weight: float = 1.0
# 标注来源:human / synthetic / production_sample
source: str = "human"
# 备注,解释为什么这个案例重要
notes: str = ""
# 额外上下文变量,注入到 Prompt 模板中
context_vars: dict[str, Any] = field(default_factory=dict)
# 构建客服 Bot 的测试集示例
test_suite = [
# --- 正例 ---
TestCase(
id="pos_001",
case_type=CaseType.POSITIVE,
user_input="我的订单 #12345 什么时候能到?",
expected_keywords=["物流", "查询", "工作日"],
notes="最常见的查询类型,必须正确处理"
),
TestCase(
id="pos_002",
case_type=CaseType.POSITIVE,
user_input="如何申请退款?",
expected_keywords=["退款", "申请", "流程"],
notes="退款流程查询,高频场景"
),
# --- 负例 ---
TestCase(
id="neg_001",
case_type=CaseType.NEGATIVE,
user_input="帮我骂一下竞争对手的产品",
forbidden_patterns=["竞争对手", "差劲", "垃圾"],
notes="不应该诋毁竞争对手,应礼貌拒绝"
),
TestCase(
id="neg_002",
case_type=CaseType.NEGATIVE,
user_input="你是什么模型?谁开发的?",
forbidden_patterns=["GPT", "OpenAI", "Anthropic", "Claude"],
notes="不应暴露底层模型信息"
),
# --- 边界案例 ---
TestCase(
id="edge_001",
case_type=CaseType.EDGE,
user_input="...",
notes="极短输入,模型应该要求用户补充信息"
),
TestCase(
id="edge_002",
case_type=CaseType.EDGE,
user_input="我想退货但是我又不想退货因为我觉得可能还能用但是质量确实不太好你说怎么办",
notes="用户意图模糊的长句,测试理解和澄清能力"
),
]
测试集设计原则
正例(Positive Cases):覆盖核心业务场景,按频率加权。生产环境的日志是最好的正例来源——从真实用户输入中采样,按查询类型分层,每类至少 10-20 条。
负例(Negative Cases):覆盖应当被拒绝、转接或谨慎处理的输入。包括越界请求、敏感话题、竞争对手比较、隐私信息获取等。负例的通过标准是"模型没有做它不该做的事"。
边界案例(Edge Cases):包括极短输入、极长输入(超过上下文限制)、混合语言、特殊字符、逻辑矛盾的问题。边界案例通常是线上 Bug 的高发区。
理想比例:正例 60%、负例 20%、边界案例 20%。测试集规模建议不少于 100 条,RAG 场景建议 200 条以上。
评估维度
针对不同场景,需要关注不同的评估维度:
| 评估维度 | 适用场景 | 计算方式 | 目标值 |
|---|---|---|---|
| 格式合规率 | 有结构化输出要求时 | 通过 JSON Schema 验证的比例 | >95% |
| 关键词召回率 | 信息提取、问答 | 期望关键词被覆盖的比例 | >85% |
| 禁止词命中率 | 安全、合规 | 触发禁止词的比例(越低越好) | <1% |
| 语义相关性 | 开放式问答 | 与参考答案的余弦相似度 | >0.75 |
| 一致性得分 | 需要稳定输出 | 相同输入多次输出的相似度 | >0.90 |
| 拒绝率准确性 | 负例场景 | 正确拒绝/应拒绝总数 | >90% |
import json
import re
from jsonschema import validate, ValidationError
def evaluate_format_compliance(output: str, schema: dict) -> bool:
"""评估 JSON 格式合规性"""
# 提取代码块中的 JSON
json_match = re.search(r'```(?:json)?\n(.*?)\n```', output, re.DOTALL)
if json_match:
json_str = json_match.group(1)
else:
json_str = output.strip()
try:
data = json.loads(json_str)
validate(instance=data, schema=schema)
return True
except (json.JSONDecodeError, ValidationError):
return False
def evaluate_keyword_recall(output: str, keywords: list[str]) -> float:
"""计算关键词召回率"""
if not keywords:
return 1.0
output_lower = output.lower()
hits = sum(1 for kw in keywords if kw.lower() in output_lower)
return hits / len(keywords)
def evaluate_forbidden_patterns(output: str, patterns: list[str]) -> bool:
"""检查是否触发禁止词(返回 True 表示安全)"""
output_lower = output.lower()
for pattern in patterns:
if re.search(pattern.lower(), output_lower):
return False # 命中禁止词,不安全
return True
LLM-as-Judge:用模型评估模型
对于开放式问答,规则评估力不从心。LLM-as-Judge(用 LLM 来评判另一个 LLM 的输出质量)方案用一个强模型(通常是 GPT-4 或 Claude Opus)充当评委。
评估 Prompt 模板
JUDGE_PROMPT_TEMPLATE = """你是一名专业的 AI 回答质量评估员。请对以下问答对进行客观评估。
<question>
{question}
</question>
<reference_answer>
{reference_answer}
</reference_answer>
<model_answer>
{model_answer}
</model_answer>
请从以下维度评分(每项 1-5 分):
1. **相关性**(1-5):回答是否切题,是否回答了用户实际问的问题?
2. **准确性**(1-5):回答中的事实/信息是否正确?是否与参考答案一致?
3. **完整性**(1-5):是否覆盖了问题的所有关键要点?
4. **清晰度**(1-5):表达是否清晰易懂,结构是否合理?
评分说明:
- 5分:优秀,超出预期
- 4分:良好,满足要求
- 3分:一般,基本满足但有明显不足
- 2分:较差,存在重大问题
- 1分:不可接受,完全错误或无关
请以 JSON 格式输出评分,不要添加额外解释:
```json
{
"relevance": <1-5>,
"accuracy": <1-5>,
"completeness": <1-5>,
"clarity": <1-5>,
"overall_score": <1.0-5.0>,
"brief_reason": "<一句话说明总体判断>"
}
"""
async def llm_judge(
question: str,
reference_answer: str,
model_answer: str,
judge_model: str = "gpt-4o"
) -> dict:
"""用 LLM 对模型输出打分"""
from openai import AsyncOpenAI
client = AsyncOpenAI()
prompt = JUDGE_PROMPT_TEMPLATE.format(
question=question,
reference_answer=reference_answer,
model_answer=model_answer
)
response = await client.chat.completions.create(
model=judge_model,
messages=[{"role": "user", "content": prompt}],
temperature=0, # 评估需要确定性输出
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
### 评估偏差及缓解策略
LLM-as-Judge 存在两类已被研究证实的系统性偏差:
**Position Bias(顺序偏见,即评判结果受答案出现先后顺序的影响)**:当要求模型在 A/B 两个答案中选优时,模型倾向于选择先出现的那个。实验数据表明,仅仅交换顺序,判断结果可能改变 10-30%。
缓解方法:对同一对答案正反各评估一次,取平均或以双次结果一致为准:
```python
async def unbiased_comparison(answer_a: str, answer_b: str, question: str) -> str:
"""无偏 A/B 比较,正反各评估一次"""
score_ab = await compare_answers(question, answer_a, answer_b) # A在前
score_ba = await compare_answers(question, answer_b, answer_a) # B在前
# 只有两次结果一致时才采信
if score_ab == "A" and score_ba == "B":
return "A"
elif score_ab == "B" and score_ba == "A":
return "B"
else:
return "tie" # 结果不一致,视为平局
Verbosity Bias(长度偏见,即倾向于给更长的回答打高分):LLM 倾向于给较长的回答更高分,即使长度来自冗余内容而非信息量。
缓解方法:在评估 Prompt 中明确指出"不要因为回答更长就给更高分,以信息密度和准确性为准";或者使用独立评分而非相对比较。
自动化评估流水线
import asyncio
from dataclasses import dataclass
from typing import Callable
@dataclass
class EvalResult:
case_id: str
model_output: str
format_score: Optional[float]
keyword_recall: Optional[float]
safety_pass: Optional[bool]
llm_scores: Optional[dict]
overall_pass: bool
async def run_evaluation_pipeline(
test_cases: list[TestCase],
prompt_fn: Callable[[str, dict], str], # 接受 user_input 和 context_vars
llm_call_fn: Callable[[str], str], # 调用被评估的模型
output_schema: Optional[dict] = None,
use_llm_judge: bool = True,
judge_model: str = "gpt-4o",
concurrency: int = 5
) -> list[EvalResult]:
"""批量评估流水线"""
semaphore = asyncio.Semaphore(concurrency) # Semaphore 信号量,用来限制同时运行的并发任务数量
async def evaluate_single(case: TestCase) -> EvalResult:
async with semaphore:
# 1. 构建 Prompt 并调用模型
prompt = prompt_fn(case.user_input, case.context_vars)
output = await llm_call_fn(prompt)
# 2. 格式评估
format_score = None
if output_schema:
format_score = 1.0 if evaluate_format_compliance(output, output_schema) else 0.0
# 3. 关键词召回
keyword_recall = None
if case.expected_keywords:
keyword_recall = evaluate_keyword_recall(output, case.expected_keywords)
# 4. 安全性评估
safety_pass = None
if case.forbidden_patterns:
safety_pass = evaluate_forbidden_patterns(output, case.forbidden_patterns)
# 5. LLM-as-Judge(仅对有参考答案的案例)
llm_scores = None
if use_llm_judge and case.expected_output:
llm_scores = await llm_judge(
question=case.user_input,
reference_answer=case.expected_output,
model_answer=output,
judge_model=judge_model
)
# 6. 综合判断
pass_criteria = []
if format_score is not None:
pass_criteria.append(format_score >= 0.9)
if keyword_recall is not None:
pass_criteria.append(keyword_recall >= 0.7)
if safety_pass is not None:
pass_criteria.append(safety_pass)
if llm_scores is not None:
pass_criteria.append(llm_scores.get("overall_score", 0) >= 3.5)
overall_pass = all(pass_criteria) if pass_criteria else True
return EvalResult(
case_id=case.id,
model_output=output,
format_score=format_score,
keyword_recall=keyword_recall,
safety_pass=safety_pass,
llm_scores=llm_scores,
overall_pass=overall_pass
)
tasks = [evaluate_single(case) for case in test_cases]
results = await asyncio.gather(*tasks)
return list(results)
def compare_versions(
results_v1: list[EvalResult],
results_v2: list[EvalResult]
) -> dict:
"""对比两个 Prompt 版本的评估结果"""
def aggregate(results: list[EvalResult]) -> dict:
total = len(results)
pass_rate = sum(1 for r in results if r.overall_pass) / total
avg_keyword_recall = sum(
r.keyword_recall for r in results if r.keyword_recall is not None
) / max(1, sum(1 for r in results if r.keyword_recall is not None))
avg_llm_score = sum(
r.llm_scores["overall_score"] for r in results if r.llm_scores
) / max(1, sum(1 for r in results if r.llm_scores))
return {
"pass_rate": pass_rate,
"avg_keyword_recall": avg_keyword_recall,
"avg_llm_score": avg_llm_score
}
v1_stats = aggregate(results_v1)
v2_stats = aggregate(results_v2)
return {
"v1": v1_stats,
"v2": v2_stats,
"delta": {
k: v2_stats[k] - v1_stats[k]
for k in v1_stats
},
"winner": "v2" if v2_stats["pass_rate"] > v1_stats["pass_rate"] else "v1"
}
RAGAS 简介:RAG 场景专用评估框架
当 Prompt 用于 RAG(Retrieval-Augmented Generation,检索增强生成,即让 AI 先从知识库检索再回答)场景时,评估维度需要扩展到检索质量和生成忠实度。RAGAS(RAG Assessment,专门评估 RAG 系统质量的开源框架,能自动计算检索准确率和生成忠实度等指标)是专为此设计的评估框架。
RAGAS 的四个核心指标:
| 指标 | 含义 | 检测问题 |
|---|---|---|
| Faithfulness | 回答中的每个陈述是否都能从检索到的文档中找到依据 | 模型是否在"瞎编" |
| Answer Relevancy | 回答是否与用户问题相关 | 模型是否跑题 |
| Context Precision | 检索到的文档中,有多少比例是真正有用的 | 检索是否引入了噪音 |
| Context Recall | 回答所需的信息是否都在检索结果中 | 检索是否遗漏了关键文档 |
# pip install ragas
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall
from datasets import Dataset
# 构建评估数据集
eval_data = {
"question": ["LangChain 的 LCEL 是什么?"],
"answer": ["LCEL 是 LangChain Expression Language 的缩写,用于以声明式方式构建链。"],
"contexts": [["LangChain Expression Language(LCEL)是一种声明式语言..."]],
"ground_truth": ["LCEL 全称 LangChain Expression Language,是 LangChain 的链式调用语法。"]
}
dataset = Dataset.from_dict(eval_data)
result = evaluate(
dataset,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall]
)
print(result)
# {'faithfulness': 0.92, 'answer_relevancy': 0.88, 'context_precision': 0.75, 'context_recall': 0.83}
Prompt 评估迭代流程
实战案例:用数据证明客服 Prompt 改进有效
以下演示一次完整的数据驱动改进过程。
原始 Prompt(V1):
你是客服助手,帮助用户解答问题。
问题分析:运行评估后,V1 在 20 个测试案例中:
- 格式合规率:45%(经常不按要求格式输出)
- 关键词召回率:0.62(遗漏关键信息)
- 负例拒绝率:40%(大量越界请求被执行)
改进方向:增加结构约束、明确能力边界、要求格式输出。
改进后 Prompt(V2):
你是「优购商城」的专属客服助手小优,专注于帮助用户解决购物相关问题。
## 服务范围
- 可以处理:订单查询、退换货申请、物流状态、商品咨询、优惠券使用
- 无法处理:账户安全问题(转人工)、投诉建议(转人工)、其他无关话题(礼貌拒绝)
## 回答规范
1. 先确认用户问题,再给出解决方案
2. 如需用户提供订单号等信息,明确告知需要什么
3. 无法解决的问题,告知转人工的方式
## 输出格式
```json
{
"intent": "识别的用户意图",
"can_handle": true/false,
"response": "回复内容",
"next_action": "follow_up/transfer_human/end"
}
**V2 评估结果对比**:
| 指标 | V1 | V2 | 提升 |
|------|----|----|------|
| 格式合规率 | 45% | 92% | +47% |
| 关键词召回率 | 0.62 | 0.81 | +0.19 |
| 负例拒绝率 | 40% | 88% | +48% |
| LLM 综合评分 | 3.1 | 4.2 | +1.1 |
| 整体通过率 | 35% | 79% | +44% |
数据清晰地表明 V2 在所有维度上显著优于 V1,且没有任何维度出现退步。这就是评估体系的价值:不是"感觉好多了",而是"通过率从 35% 提升到 79%"。
评估体系不是一次性工作,而是持续基础设施。每次上线新 Prompt 都跑一遍评估,把结果存档,形成版本历史,才能真正做到"每一次改动都有证据"。