Day 27:🔧 Prompt 调试与优化------A/B 测试与效果评估
🔖 阶段三:Prompt Engineering 深度实战(Day 21 - Day 30)
写好一个 Prompt 只是开始。真正的挑战是:如何知道你的 Prompt 好不好?如何系统地优化它? 今天我们将学习 Prompt 的调试技巧和量化评估方法,让 Prompt Engineering 从"凭感觉"变成"有数据"。
🎯 写在最前面
Prompt 优化的闭环流程
#mermaid-svg-xQHbSkIQ4BMW0jUc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xQHbSkIQ4BMW0jUc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xQHbSkIQ4BMW0jUc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xQHbSkIQ4BMW0jUc .error-icon{fill:#552222;}#mermaid-svg-xQHbSkIQ4BMW0jUc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xQHbSkIQ4BMW0jUc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xQHbSkIQ4BMW0jUc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xQHbSkIQ4BMW0jUc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xQHbSkIQ4BMW0jUc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xQHbSkIQ4BMW0jUc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xQHbSkIQ4BMW0jUc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xQHbSkIQ4BMW0jUc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xQHbSkIQ4BMW0jUc .marker.cross{stroke:#333333;}#mermaid-svg-xQHbSkIQ4BMW0jUc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xQHbSkIQ4BMW0jUc p{margin:0;}#mermaid-svg-xQHbSkIQ4BMW0jUc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xQHbSkIQ4BMW0jUc .cluster-label text{fill:#333;}#mermaid-svg-xQHbSkIQ4BMW0jUc .cluster-label span{color:#333;}#mermaid-svg-xQHbSkIQ4BMW0jUc .cluster-label span p{background-color:transparent;}#mermaid-svg-xQHbSkIQ4BMW0jUc .label text,#mermaid-svg-xQHbSkIQ4BMW0jUc span{fill:#333;color:#333;}#mermaid-svg-xQHbSkIQ4BMW0jUc .node rect,#mermaid-svg-xQHbSkIQ4BMW0jUc .node circle,#mermaid-svg-xQHbSkIQ4BMW0jUc .node ellipse,#mermaid-svg-xQHbSkIQ4BMW0jUc .node polygon,#mermaid-svg-xQHbSkIQ4BMW0jUc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xQHbSkIQ4BMW0jUc .rough-node .label text,#mermaid-svg-xQHbSkIQ4BMW0jUc .node .label text,#mermaid-svg-xQHbSkIQ4BMW0jUc .image-shape .label,#mermaid-svg-xQHbSkIQ4BMW0jUc .icon-shape .label{text-anchor:middle;}#mermaid-svg-xQHbSkIQ4BMW0jUc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xQHbSkIQ4BMW0jUc .rough-node .label,#mermaid-svg-xQHbSkIQ4BMW0jUc .node .label,#mermaid-svg-xQHbSkIQ4BMW0jUc .image-shape .label,#mermaid-svg-xQHbSkIQ4BMW0jUc .icon-shape .label{text-align:center;}#mermaid-svg-xQHbSkIQ4BMW0jUc .node.clickable{cursor:pointer;}#mermaid-svg-xQHbSkIQ4BMW0jUc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xQHbSkIQ4BMW0jUc .arrowheadPath{fill:#333333;}#mermaid-svg-xQHbSkIQ4BMW0jUc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xQHbSkIQ4BMW0jUc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xQHbSkIQ4BMW0jUc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xQHbSkIQ4BMW0jUc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xQHbSkIQ4BMW0jUc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xQHbSkIQ4BMW0jUc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xQHbSkIQ4BMW0jUc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xQHbSkIQ4BMW0jUc .cluster text{fill:#333;}#mermaid-svg-xQHbSkIQ4BMW0jUc .cluster span{color:#333;}#mermaid-svg-xQHbSkIQ4BMW0jUc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xQHbSkIQ4BMW0jUc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xQHbSkIQ4BMW0jUc rect.text{fill:none;stroke-width:0;}#mermaid-svg-xQHbSkIQ4BMW0jUc .icon-shape,#mermaid-svg-xQHbSkIQ4BMW0jUc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xQHbSkIQ4BMW0jUc .icon-shape p,#mermaid-svg-xQHbSkIQ4BMW0jUc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xQHbSkIQ4BMW0jUc .icon-shape .label rect,#mermaid-svg-xQHbSkIQ4BMW0jUc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xQHbSkIQ4BMW0jUc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xQHbSkIQ4BMW0jUc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xQHbSkIQ4BMW0jUc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 📝 编写初版 Prompt
🧪 测试用例运行
📊 收集评估数据
🔍 分析问题模式
🛠️ 修改 Prompt
📖 构建评估数据集
评估数据集的设计原则
python
"""
评估数据集构建
=============
好的评估数据集是 Prompt 优化的基础
"""
import json
from dataclasses import dataclass, field
@dataclass
class EvalCase:
"""
单个评估用例
属性:
id: 用例唯一标识
input: 输入文本
expected_output: 期望输出(用于自动评估)
category: 用例分类(如"简单"、"复杂"、"边界")
tags: 标签列表
"""
id: str
input: str
expected_output: str = ""
category: str = "normal"
tags: list[str] = field(default_factory=list)
@dataclass
class EvalDataset:
"""
评估数据集
包含多个评估用例,支持按分类筛选
"""
name: str
cases: list[EvalCase] = field(default_factory=list)
def add_case(self, case: EvalCase):
"""添加一个评估用例"""
self.cases.append(case)
def get_by_category(self, category: str) -> list[EvalCase]:
"""按分类筛选用例"""
return [c for c in self.cases if c.category == category]
def get_by_tag(self, tag: str) -> list[EvalCase]:
"""按标签筛选用例"""
return [c for c in self.cases if tag in c.tags]
def summary(self) -> dict:
"""数据集摘要统计"""
categories = {}
for case in self.cases:
categories[case.category] = categories.get(case.category, 0) + 1
return {
"name": self.name,
"total": len(self.cases),
"categories": categories
}
# ============ 构建评估数据集 ============
# 场景:评估一个"客户邮件分类"的 Prompt
dataset = EvalDataset(name="客户邮件分类评估集")
# 添加评估用例
test_cases = [
# 简单案例
EvalCase(
id="simple_001",
input="我想退货,订单号是12345",
expected_output="refund",
category="simple",
tags=["退款", "简单"]
),
EvalCase(
id="simple_002",
input="你们的营业时间是几点到几点?",
expected_output="inquiry",
category="simple",
tags=["咨询", "简单"]
),
# 复杂案例
EvalCase(
id="complex_001",
input="我上周买的手机屏幕有问题,想换货,但我也想知道你们有没有以旧换新的活动,如果有的话我可能直接换新款",
expected_output="multi_intent",
category="complex",
tags=["换货", "咨询", "多意图"]
),
# 边界案例
EvalCase(
id="edge_001",
input="asdfghjkl",
expected_output="unclear",
category="edge",
tags=["无意义", "边界"]
),
EvalCase(
id="edge_002",
input="你们是不是骗子?我要去消费者协会投诉你们!",
expected_output="complaint",
category="edge",
tags=["投诉", "情绪化"]
),
]
for case in test_cases:
dataset.add_case(case)
print(f"📊 评估数据集摘要: {json.dumps(dataset.summary(), ensure_ascii=False, indent=2)}")
📚 A/B 测试框架
对比两个 Prompt 版本
python
"""
Prompt A/B 测试框架
==================
对比两个 Prompt 版本在同一组测试用例上的表现
"""
from openai import OpenAI
import os
import time
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url="https://api.openai.com/v1"
)
class PromptABTester:
"""
Prompt A/B 测试器
功能:
1. 在相同测试集上运行两个 Prompt 版本
2. 收集每个用例的输出
3. 使用 LLM-as-Judge 自动评分
4. 生成对比报告
"""
def __init__(self, client, model="gpt-4o-mini"):
self.client = client
self.model = model
def run_test(self, prompt_a: str, prompt_b: str, dataset: EvalDataset) -> dict:
"""
运行 A/B 测试
参数:
prompt_a: A 版本的 Prompt
prompt_b: B 版本的 Prompt
dataset: 评估数据集
返回:
测试结果字典
"""
results = {"prompt_a": [], "prompt_b": []}
print(f"🧪 开始 A/B 测试")
print(f" 数据集: {dataset.name} ({len(dataset.cases)} 个用例)")
print()
for case in dataset.cases:
print(f" 📝 用例 {case.id}...", end=" ")
# 运行 Prompt A
response_a = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt_a.replace("{{input}}", case.input)}],
temperature=0.0,
max_tokens=500
)
output_a = response_a.choices[0].message.content
results["prompt_a"].append({
"case_id": case.id,
"output": output_a,
"tokens": response_a.usage.total_tokens if response_a.usage else 0
})
# 运行 Prompt B
response_b = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt_b.replace("{{input}}", case.input)}],
temperature=0.0,
max_tokens=500
)
output_b = response_b.choices[0].message.content
results["prompt_b"].append({
"case_id": case.id,
"output": output_b,
"tokens": response_b.usage.total_tokens if response_b.usage else 0
})
print(f"✅ A:{len(output_a)}字 B:{len(output_b)}字")
return results
def judge(self, case_input: str, output_a: str, output_b: str, expected: str) -> dict:
"""
使用 LLM-as-Judge 评判两个输出
参数:
case_input: 输入
output_a: A 版本输出
output_b: B 版本输出
expected: 期望输出
返回:
评判结果
"""
judge_prompt = f"""你是一位 Prompt 质量评审专家。请对比以下两个回答,判断哪个更好。
## 用户输入
{case_input}
## 期望输出
{expected}
## 回答 A
{output_a}
## 回答 B
{output_b}
## 请从以下维度评分(1-10分):
1. 准确性:回答是否正确?
2. 完整性:是否覆盖了所有要点?
3. 格式:输出格式是否规范?
请输出 JSON:
{{
"score_a": {{"accuracy": N, "completeness": N, "format": N}},
"score_b": {{"accuracy": N, "completeness": N, "format": N}},
"winner": "A" | "B" | "tie",
"reason": "简要说明"
}}"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": judge_prompt}],
response_format={"type": "json_object"},
temperature=0.0,
max_tokens=500
)
return json.loads(response.choices[0].message.content)
# ============ A/B 测试示例 ============
# Prompt A:简单版
prompt_a = "请将以下客户邮件分类为:refund/complaint/inquiry/other。只输出类别名称。\n\n邮件:{{input}}"
# Prompt B:详细版(加了角色和示例)
prompt_b = """你是一位客服邮件分类专家。请将客户邮件分类为以下类别之一:
- refund: 退款/退货相关
- complaint: 投诉/不满
- inquiry: 咨询/提问
- multi_intent: 包含多个意图
- unclear: 无法理解
邮件内容:{{input}}
请只输出类别名称。"""
# 运行测试(使用之前构建的数据集)
tester = PromptABTester(client)
results = tester.run_test(prompt_a, prompt_b, dataset)
# 计算平均 Token 消耗
avg_tokens_a = sum(r["tokens"] for r in results["prompt_a"]) / len(results["prompt_a"])
avg_tokens_b = sum(r["tokens"] for r in results["prompt_b"]) / len(results["prompt_b"])
print(f"\n📊 Token 消耗对比:")
print(f" Prompt A 平均: {avg_tokens_a:.0f} tokens")
print(f" Prompt B 平均: {avg_tokens_b:.0f} tokens")
🏗️ 评估指标体系
#mermaid-svg-PKx7Tl5PpPV5vAqr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PKx7Tl5PpPV5vAqr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PKx7Tl5PpPV5vAqr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PKx7Tl5PpPV5vAqr .error-icon{fill:#552222;}#mermaid-svg-PKx7Tl5PpPV5vAqr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PKx7Tl5PpPV5vAqr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PKx7Tl5PpPV5vAqr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PKx7Tl5PpPV5vAqr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PKx7Tl5PpPV5vAqr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PKx7Tl5PpPV5vAqr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PKx7Tl5PpPV5vAqr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PKx7Tl5PpPV5vAqr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PKx7Tl5PpPV5vAqr .marker.cross{stroke:#333333;}#mermaid-svg-PKx7Tl5PpPV5vAqr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PKx7Tl5PpPV5vAqr p{margin:0;}#mermaid-svg-PKx7Tl5PpPV5vAqr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PKx7Tl5PpPV5vAqr .cluster-label text{fill:#333;}#mermaid-svg-PKx7Tl5PpPV5vAqr .cluster-label span{color:#333;}#mermaid-svg-PKx7Tl5PpPV5vAqr .cluster-label span p{background-color:transparent;}#mermaid-svg-PKx7Tl5PpPV5vAqr .label text,#mermaid-svg-PKx7Tl5PpPV5vAqr span{fill:#333;color:#333;}#mermaid-svg-PKx7Tl5PpPV5vAqr .node rect,#mermaid-svg-PKx7Tl5PpPV5vAqr .node circle,#mermaid-svg-PKx7Tl5PpPV5vAqr .node ellipse,#mermaid-svg-PKx7Tl5PpPV5vAqr .node polygon,#mermaid-svg-PKx7Tl5PpPV5vAqr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PKx7Tl5PpPV5vAqr .rough-node .label text,#mermaid-svg-PKx7Tl5PpPV5vAqr .node .label text,#mermaid-svg-PKx7Tl5PpPV5vAqr .image-shape .label,#mermaid-svg-PKx7Tl5PpPV5vAqr .icon-shape .label{text-anchor:middle;}#mermaid-svg-PKx7Tl5PpPV5vAqr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PKx7Tl5PpPV5vAqr .rough-node .label,#mermaid-svg-PKx7Tl5PpPV5vAqr .node .label,#mermaid-svg-PKx7Tl5PpPV5vAqr .image-shape .label,#mermaid-svg-PKx7Tl5PpPV5vAqr .icon-shape .label{text-align:center;}#mermaid-svg-PKx7Tl5PpPV5vAqr .node.clickable{cursor:pointer;}#mermaid-svg-PKx7Tl5PpPV5vAqr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PKx7Tl5PpPV5vAqr .arrowheadPath{fill:#333333;}#mermaid-svg-PKx7Tl5PpPV5vAqr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PKx7Tl5PpPV5vAqr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PKx7Tl5PpPV5vAqr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PKx7Tl5PpPV5vAqr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PKx7Tl5PpPV5vAqr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PKx7Tl5PpPV5vAqr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PKx7Tl5PpPV5vAqr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PKx7Tl5PpPV5vAqr .cluster text{fill:#333;}#mermaid-svg-PKx7Tl5PpPV5vAqr .cluster span{color:#333;}#mermaid-svg-PKx7Tl5PpPV5vAqr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PKx7Tl5PpPV5vAqr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PKx7Tl5PpPV5vAqr rect.text{fill:none;stroke-width:0;}#mermaid-svg-PKx7Tl5PpPV5vAqr .icon-shape,#mermaid-svg-PKx7Tl5PpPV5vAqr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PKx7Tl5PpPV5vAqr .icon-shape p,#mermaid-svg-PKx7Tl5PpPV5vAqr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PKx7Tl5PpPV5vAqr .icon-shape .label rect,#mermaid-svg-PKx7Tl5PpPV5vAqr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PKx7Tl5PpPV5vAqr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PKx7Tl5PpPV5vAqr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PKx7Tl5PpPV5vAqr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 📊 Prompt 评估指标体系
🛡️ 安全指标
幻觉率
编造信息的比例
越界率
超出能力范围的回答
注入抗性
抵抗 Prompt 注入的能力
💰 成本指标
Token 消耗
输入+输出 Token 数
延迟 Latency
响应时间
调用次数
是否需要重试
🎯 质量指标
准确性 Accuracy
输出是否正确
完整性 Completeness
是否覆盖所有要点
一致性 Consistency
多次运行结果是否稳定
📚 本章小结
| 方法 | 说明 | 适用阶段 |
|---|---|---|
| 评估数据集 | 构建标准化的测试用例集 | 所有阶段 |
| A/B 测试 | 对比两个 Prompt 版本 | 优化阶段 |
| LLM-as-Judge | 用 LLM 评估 LLM 的输出 | 自动评估 |
| 指标体系 | 质量+成本+安全三维评估 | 全面评估 |
🧠 课后思考
- 实践题:为你的一个 Prompt 构建 10 个测试用例(包含简单、复杂、边界案例),运行 A/B 测试。
- 分析题:LLM-as-Judge 本身也有偏差,如何减少评判的偏差?
- 设计题:设计一个自动化的 Prompt 优化系统,能够根据评估结果自动调整 Prompt。
📖 导航
- 🏠 返回课程简介
- 📋 查看课程大纲
- ⬅️ 上一篇:Day 26 - 输出格式控制
- ➡️ 下一篇:Day 28 - 高级 Prompt 模式
如果这篇文章对你有帮助,请点赞 👍 收藏 ⭐ 关注 🔔 三连支持!
你的支持是我持续更新的最大动力 💪