Prompt Engineering实战:从玄学到工程的200个案例
过去一年,我在公司内部负责LLM应用的Prompt优化,累计处理了200多个不同场景的案例。这篇文章总结了我从"随缘调参"到"系统性优化"的方法论转变,以及一些实测有效的技巧。不讲理论,只讲实战。
我对Prompt Engineering的认知变化
一年前我觉得Prompt Engineering就是"玄学":同样的意思换个说法,效果可能天差地别。现在我的认知是:Prompt Engineering是一门工程学科,有方法论,可复现,可量化。
关键转变是:建立评测体系。没有评测,一切优化都是瞎蒙。
我的工作流程
# Prompt优化的标准流程
1. 收集真实case (至少50个,覆盖各种情况)
2. 定义评测指标 (准确率/格式正确率/用户满意度)
3. 建立baseline
4. 系统性尝试不同策略
5. A/B测试验证
6. 上线后持续监控
技巧一:结构化输出是基本功
问题场景
做一个简历解析服务,需要从简历文本中提取姓名、学校、工作经历等字段。最初的Prompt:
问题:输出格式不稳定,有时是列表,有时是段落,有时字段名不一致。
解决方案:明确输出格式
效果对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 格式正确率 | 62% | 98% |
| 字段提取准确率 | 71% | 89% |
| 需人工修正比例 | 45% | 8% |
技巧二:Few-shot的正确姿势
常见误区
很多人知道要给example,但example给得不好,效果反而变差。我总结了几个常见问题:
- 例子太简单:只给happy path,遇到边界情况模型不知道怎么处理
- 例子太多:10个例子不如3个精选例子
- 例子不一致:不同例子的输出风格不统一
正确做法:精选3个覆盖不同情况的例子
# 情感分析任务的few-shot设计
examples = [
# Example 1: 明确的正面
{
"input": "这家餐厅的牛排太好吃了,环境也很棒,下次还来!",
"output": {
"sentiment": "positive",
"confidence": 0.95,
"keywords": ["好吃", "环境棒", "还来"]
}
},
# Example 2: 明确的负面
{
"input": "等了一个小时才上菜,服务员态度还很差,再也不来了",
"output": {
"sentiment": "negative",
"confidence": 0.92,
"keywords": ["等一个小时", "态度差", "不来了"]
}
},
# Example 3: 混合情感(边界情况!)
{
"input": "菜品味道不错,但是价格偏贵,性价比一般",
"output": {
"sentiment": "neutral",
"confidence": 0.65,
"keywords": ["味道不错", "价格偏贵", "性价比一般"]
}
}
]
Few-shot的数量实验
我在情感分析任务上测试了不同数量的example:
| Example数量 | 准确率 | Token消耗 | 延迟(ms) |
|---|---|---|---|
| 0 (zero-shot) | 76.3% | ~150 | ~320 |
| 1 | 81.2% | ~250 | ~380 |
| 3 (推荐) | 88.7% | ~450 | ~520 |
| 5 | 89.1% | ~650 | ~680 |
| 10 | 88.4% | ~1100 | ~1050 |
结论:3个精选example是性价比最高的选择。超过5个后准确率不再提升,甚至略有下降(可能是context太长导致)。
技巧三:Chain-of-Thought要具体引导
问题场景
数学应用题,模型经常算错。
这种泛泛的"一步一步"效果有限。
解决方案:提供思考框架
模型输出:
1. 【理解题意】
- 初始数量:5个苹果
- 减少:给了小红2个
- 增加:买了3个
2. 【列出已知】
- 初始 = 5
- 减少 = 2
- 增加 = 3
3. 【建立等式】
结果 = 5 - 2 + 3
4. 【计算验证】
- 5 - 2 = 3
- 3 + 3 = 6
5. 【最终答案】
小明现在有6个苹果。
不同任务的CoT模板
# 代码调试任务的CoT
debug_template = """
请按以下步骤分析这段代码的bug:
1. 【代码功能】这段代码想做什么?
2. 【预期行为】正确的输出应该是什么?
3. 【实际行为】现在的输出是什么?
4. 【逐行分析】从第一行开始,每行的状态变化
5. 【定位问题】哪一行出现了问题?为什么?
6. 【修复方案】如何修改?给出修改后的代码
"""
# 文本摘要任务的CoT
summary_template = """
请按以下步骤生成摘要:
1. 【主题识别】这篇文章讲的是什么主题?
2. 【关键点提取】列出3-5个最重要的信息点
3. 【逻辑关系】这些信息点之间是什么关系?
4. 【摘要生成】用2-3句话概括全文,包含所有关键点
"""
技巧四:Self-Consistency提升准确率
原理
让模型对同一问题生成多个回答(用temperature>0),然后投票选择最一致的答案。直觉是:正确答案更容易被多次"发现"。
实现代码
import openai
from collections import Counter
def self_consistency(prompt: str, n_samples: int = 5, temperature: float = 0.7):
"""
Self-Consistency推理
Args:
prompt: 问题prompt
n_samples: 生成多少个回答
temperature: 采样温度,建议0.5-0.8
Returns:
most_common_answer: 投票选出的答案
confidence: 该答案出现的比例
"""
responses = []
for _ in range(n_samples):
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=temperature,
max_tokens=500
)
answer = extract_answer(response.choices[0].message.content)
responses.append(answer)
# 统计投票
counter = Counter(responses)
most_common = counter.most_common(1)[0]
return {
"answer": most_common[0],
"confidence": most_common[1] / n_samples,
"all_responses": responses
}
def extract_answer(text: str) -> str:
"""从回复中提取最终答案"""
# 根据任务类型定制提取逻辑
# 这里假设答案在最后一行
lines = text.strip().split('\n')
return lines[-1].strip()
# 使用示例
result = self_consistency(
prompt="小明有5个苹果...(CoT prompt)",
n_samples=5
)
print(f"答案: {result['answer']}, 置信度: {result['confidence']:.0%}")
实测数据
| 任务类型 | Single(n=1) | SC(n=3) | SC(n=5) | SC(n=7) |
|---|---|---|---|---|
| 数学应用题 | 72.3% | 81.5% | 85.2% | 86.1% |
| 逻辑推理 | 68.7% | 76.2% | 79.8% | 80.5% |
| 代码生成 | 61.4% | 68.9% | 71.3% | 72.1% |
技巧五:角色设定真的有用
实验:同一任务,不同角色
任务:分析一段代码的潜在bug。
| 角色设定 | 发现bug数量(avg) | 误报率 |
|---|---|---|
| 无角色 | 2.3 | 35% |
| "你是一个程序员" | 2.8 | 28% |
| "你是一个有10年经验的高级软件工程师" | 3.5 | 22% |
| "你是Google的Staff Engineer,专门负责代码审查" | 4.1 | 18% |
角色设定越具体,效果越好。但要注意不要过于夸张(如"你是世界上最厉害的程序员"),反而可能导致模型产生不切实际的输出。
有效的角色设定模板
# 通用模板
role_template = """
你是{职业/身份},具有以下特点:
- 专业背景:{相关领域}
- 工作经验:{年限}
- 核心技能:{列举2-3个}
- 工作风格:{简洁/详细/谨慎等}
你的任务是{具体任务描述}。
"""
# 示例:代码审查
code_review_prompt = """
你是一位资深的Python后端工程师,具有以下特点:
- 专业背景:分布式系统、高并发处理
- 工作经验:8年
- 核心技能:代码优化、性能调优、安全审计
- 工作风格:严谨细致,关注边界情况
请审查以下代码,重点关注:
1. 潜在的bug和异常处理
2. 性能问题
3. 安全漏洞
4. 代码风格和可维护性
{code}
"""
技巧六:处理长文本的分块策略
问题
要分析一份100页的PDF报告,远超模型的上下文窗口。
方案:Map-Reduce
from typing import List
def map_reduce_summarize(
chunks: List[str],
chunk_prompt: str,
reduce_prompt: str,
model: str = "gpt-4"
) -> str:
"""
Map-Reduce方式处理长文本
Args:
chunks: 分块后的文本列表
chunk_prompt: 处理单个chunk的prompt
reduce_prompt: 合并多个结果的prompt
"""
# Map阶段:并行处理每个chunk
chunk_results = []
for i, chunk in enumerate(chunks):
prompt = chunk_prompt.format(
chunk_index=i+1,
total_chunks=len(chunks),
content=chunk
)
result = call_llm(prompt, model)
chunk_results.append(result)
# Reduce阶段:合并所有结果
combined = "\n\n---\n\n".join([
f"第{i+1}部分摘要:\n{r}"
for i, r in enumerate(chunk_results)
])
final_prompt = reduce_prompt.format(
all_summaries=combined,
total_parts=len(chunks)
)
final_result = call_llm(final_prompt, model)
return final_result
# 使用示例
chunk_prompt = """
你正在分析一份长报告的第{chunk_index}/{total_chunks}部分。
请提取这部分的关键信息:
1. 主要观点(2-3个)
2. 重要数据(如果有)
3. 与其他部分可能的关联
内容:
{content}
"""
reduce_prompt = """
以下是一份长报告各部分的摘要。请综合这些信息,生成一份完整的执行摘要。
要求:
- 保留所有重要数据
- 理清各部分之间的逻辑关系
- 最终摘要控制在500字以内
各部分摘要:
{all_summaries}
"""
result = map_reduce_summarize(chunks, chunk_prompt, reduce_prompt)
技巧七:处理模型"犟嘴"
问题
有时候你明确告诉模型"不要做X",它偏要做。比如:
模型还是经常会加上"总的来说,这是一篇很有价值的文章"之类的评价。
解决方案:正面表述 + 格式约束
更多"反向约束"的正面表述
| 想达到的效果 | ❌ 不好的写法 | ✅ 更好的写法 |
|---|---|---|
| 不要编造信息 | 不要瞎编 | 只使用提供的信息,不确定时说"信息不足" |
| 不要太长 | 回答不要太长 | 回答控制在100字以内 |
| 不要用专业术语 | 不要用术语 | 用小学生能理解的语言解释 |
| 不要重复问题 | 不要复述问题 | 直接给出答案,第一个词是答案本身 |
技巧八:动态Prompt生成
场景
做一个SQL生成助手,需要根据用户的数据库schema动态构造Prompt。
from typing import Dict, List
def build_sql_prompt(
question: str,
tables: Dict[str, List[Dict]],
examples: List[Dict] = None
) -> str:
"""
动态构建SQL生成的Prompt
Args:
question: 用户问题
tables: 数据库schema,格式 {table_name: [column_info]}
examples: 可选的few-shot示例
"""
# 1. 构建Schema描述
schema_desc = "## 数据库Schema\n\n"
for table_name, columns in tables.items():
schema_desc += f"### 表: {table_name}\n"
schema_desc += "| 列名 | 类型 | 说明 |\n|---|---|---|\n"
for col in columns:
schema_desc += f"| {col['name']} | {col['type']} | {col.get('desc', '')} |\n"
schema_desc += "\n"
# 2. 构建示例(如果有)
examples_desc = ""
if examples:
examples_desc = "## 示例\n\n"
for ex in examples:
examples_desc += f"问题: {ex['question']}\n"
examples_desc += f"SQL: ```sql\n{ex['sql']}\n```\n\n"
# 3. 组装完整Prompt
prompt = f"""你是一个SQL专家。根据用户问题生成SQL查询。
{schema_desc}
{examples_desc}
## 规则
- 只使用上述表和列,不要假设存在其他表
- 使用标准SQL语法
- 如果问题无法用SQL回答,说明原因
## 用户问题
{question}
请生成SQL(只输出SQL,不需要解释):
"""
return prompt
# 使用
tables = {
"users": [
{"name": "id", "type": "INT", "desc": "用户ID"},
{"name": "name", "type": "VARCHAR(100)", "desc": "用户名"},
{"name": "created_at", "type": "DATETIME", "desc": "注册时间"}
],
"orders": [
{"name": "id", "type": "INT", "desc": "订单ID"},
{"name": "user_id", "type": "INT", "desc": "用户ID"},
{"name": "amount", "type": "DECIMAL(10,2)", "desc": "订单金额"},
{"name": "created_at", "type": "DATETIME", "desc": "下单时间"}
]
}
prompt = build_sql_prompt(
question="查询每个用户的总消费金额",
tables=tables
)
实战案例:客服意图分类系统
背景
公司客服系统需要自动识别用户意图,路由到正确的处理流程。原来用的是传统NLU模型,准确率82%,想用LLM提升。
V1:简单分类
准确率:78%。比原来还差!
V2:添加类别说明
准确率:85%。
V3:添加Few-shot + 边界说明
准确率:93%!
最终效果
| 版本 | 准确率 | 处理边界case能力 |
|---|---|---|
| 传统NLU | 82% | 差 |
| V1 | 78% | 差 |
| V2 | 85% | 中 |
| V3 (最终) | 93% | 好 |
Prompt版本管理
Prompt也是代码,需要版本管理。我们的实践:
# prompts/intent_classifier/v3.yaml
name: intent_classifier
version: "3.0.0"
description: "客服意图分类Prompt"
author: "kbaicai"
created_at: "2024-05-15"
updated_at: "2024-05-28"
# 适用模型
models:
- gpt-4
- gpt-3.5-turbo
# 性能指标
metrics:
accuracy: 0.93
test_set_size: 500
# Prompt模板
template: |
你是客服意图分类助手...
# 变更日志
changelog:
- version: "3.0.0"
date: "2024-05-28"
changes: "添加边界情况处理说明,准确率从85%提升到93%"
- version: "2.0.0"
date: "2024-05-20"
changes: "添加类别详细说明"
- version: "1.0.0"
date: "2024-05-15"
changes: "初始版本"
参考资料
- Chain-of-Thought Prompting Elicits Reasoning in LLMs
- Self-Consistency Improves Chain of Thought Reasoning
- Prompt Engineering Guide
- OpenAI Prompt Engineering Guide
总结
200个案例下来,我总结的核心原则:
- 有评测才能优化 - 没有数据支撑的优化都是自嗨
- 结构化是基础 - 明确输出格式,减少后处理成本
- Few-shot要精不要多 - 3个覆盖边界的例子胜过10个平庸的
- CoT要给框架 - "一步步思考"不如给具体步骤
- 正面表述胜过禁止 - 告诉模型该做什么,而不是不该做什么
- 版本管理很重要 - Prompt也是代码,要有changelog
更新记录:
2024-05-30: 初版发布
2024-06-15: 补充客服分类案例
2024-07-20: 增加Prompt版本管理实践
2024-08-10: 更新Self-Consistency的实测数据