← 返回首页

Prompt Engineering实战:从玄学到工程的200个案例

2024-05-30 | 提示工程 LLM 工程实践
过去一年,我在公司内部负责LLM应用的Prompt优化,累计处理了200多个不同场景的案例。这篇文章总结了我从"随缘调参"到"系统性优化"的方法论转变,以及一些实测有效的技巧。不讲理论,只讲实战。

我对Prompt Engineering的认知变化

一年前我觉得Prompt Engineering就是"玄学":同样的意思换个说法,效果可能天差地别。现在我的认知是:Prompt Engineering是一门工程学科,有方法论,可复现,可量化。

关键转变是:建立评测体系。没有评测,一切优化都是瞎蒙。

我的工作流程

# Prompt优化的标准流程
1. 收集真实case (至少50个,覆盖各种情况)
2. 定义评测指标 (准确率/格式正确率/用户满意度)
3. 建立baseline
4. 系统性尝试不同策略
5. A/B测试验证
6. 上线后持续监控

技巧一:结构化输出是基本功

问题场景

做一个简历解析服务,需要从简历文本中提取姓名、学校、工作经历等字段。最初的Prompt:

请从以下简历中提取关键信息: {resume_text}

问题:输出格式不稳定,有时是列表,有时是段落,有时字段名不一致。

解决方案:明确输出格式

你是一个简历解析助手。请从简历中提取信息,严格按照JSON格式输出。 ## 输出格式 ```json { "name": "姓名", "phone": "手机号,如果没有则为null", "email": "邮箱,如果没有则为null", "education": [ { "school": "学校名称", "degree": "学位(本科/硕士/博士)", "major": "专业", "start_year": "入学年份", "end_year": "毕业年份" } ], "experience": [ { "company": "公司名称", "title": "职位", "start_date": "开始日期(YYYY-MM格式)", "end_date": "结束日期(YYYY-MM格式)或'至今'", "description": "工作内容摘要(不超过100字)" } ] } ``` ## 注意事项 - 所有字段必须存在,没有信息则填null - 日期格式统一为YYYY-MM - education和experience按时间倒序排列 ## 简历内容 {resume_text}

效果对比

指标 优化前 优化后
格式正确率 62% 98%
字段提取准确率 71% 89%
需人工修正比例 45% 8%
⚠️ 踩坑: 一开始我在Prompt里写"输出JSON",但没给示例。结果有时候模型会输出```json代码块,有时候直接输出JSON。加上完整示例后问题解决。

技巧二:Few-shot的正确姿势

常见误区

很多人知道要给example,但example给得不好,效果反而变差。我总结了几个常见问题:

正确做法:精选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要具体引导

问题场景

数学应用题,模型经常算错。

请一步一步思考,然后回答这个问题: 小明有5个苹果,给了小红2个,又买了3个,请问小明现在有几个苹果?

这种泛泛的"一步一步"效果有限。

解决方案:提供思考框架

请按照以下步骤解决这个数学问题: ## 思考步骤 1. 【理解题意】识别初始数量、增加量、减少量 2. 【列出已知】写出所有数值 3. 【建立等式】初始 - 减少 + 增加 = 结果 4. 【计算验证】逐步计算,每步写出中间结果 5. 【最终答案】用一句话陈述答案 ## 问题 小明有5个苹果,给了小红2个,又买了3个,请问小明现在有几个苹果? 请严格按照上述步骤回答:

模型输出:

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%
💡 经验: n=5是性价比较高的选择。从n=5到n=7,准确率提升不到1%,但成本增加40%。对于高价值场景(如医疗、法律),可以用n=7或更高。

技巧五:角色设定真的有用

实验:同一任务,不同角色

任务:分析一段代码的潜在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)
⚠️ 踩坑: 分块时要注意语义完整性。不要在句子中间切断,最好在段落边界切分。我们用nltk的sent_tokenize先分句,再按token数合并成chunk。

技巧七:处理模型"犟嘴"

问题

有时候你明确告诉模型"不要做X",它偏要做。比如:

请总结这篇文章,不要添加你自己的观点。 {article}

模型还是经常会加上"总的来说,这是一篇很有价值的文章"之类的评价。

解决方案:正面表述 + 格式约束

请总结这篇文章。 要求: 1. 只陈述文章中明确提到的事实 2. 使用"文章指出..."、"作者认为..."等客观表述 3. 输出格式:3-5个要点,每个要点一句话 {article}

更多"反向约束"的正面表述

想达到的效果 ❌ 不好的写法 ✅ 更好的写法
不要编造信息 不要瞎编 只使用提供的信息,不确定时说"信息不足"
不要太长 回答不要太长 回答控制在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:简单分类

请将用户问题分类到以下类别之一: - 查询订单 - 退款退货 - 投诉建议 - 产品咨询 - 其他 用户问题:{query} 类别:

准确率:78%。比原来还差!

V2:添加类别说明

请将用户问题分类。每个类别的含义: - 查询订单:查物流、查订单状态、确认收货等 - 退款退货:要求退款、退换货、取消订单等 - 投诉建议:对服务/产品不满、提出改进建议 - 产品咨询:询问产品功能、规格、使用方法等 - 其他:不属于以上任何类别 用户问题:{query} 类别(只输出类别名,不要解释):

准确率:85%。

V3:添加Few-shot + 边界说明

你是客服意图分类助手。请将用户问题分类到以下类别。 ## 类别定义 1. 查询订单 - 查物流、查订单状态、确认收货 2. 退款退货 - 要求退款、退换货、取消订单 3. 投诉建议 - 对服务/产品不满、提出改进建议 4. 产品咨询 - 询问产品功能、规格、使用方法 5. 其他 - 不属于以上类别(闲聊、无关问题等) ## 示例 问:我的快递到哪了 → 查询订单 问:这个手机支持5G吗 → 产品咨询 问:买错了想退货 → 退款退货 问:你们服务太差了 → 投诉建议 问:发货太慢了,能不能催一下 → 查询订单(注意:虽然有抱怨,但核心诉求是查物流) ## 边界情况处理 - 同时涉及多个意图:选择最核心的诉求 - 带有情绪的问题:忽略情绪,关注实际诉求 用户问题:{query} 分类结果(只输出类别名):

准确率: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: "初始版本"

参考资料

总结

200个案例下来,我总结的核心原则:

  1. 有评测才能优化 - 没有数据支撑的优化都是自嗨
  2. 结构化是基础 - 明确输出格式,减少后处理成本
  3. Few-shot要精不要多 - 3个覆盖边界的例子胜过10个平庸的
  4. CoT要给框架 - "一步步思考"不如给具体步骤
  5. 正面表述胜过禁止 - 告诉模型该做什么,而不是不该做什么
  6. 版本管理很重要 - Prompt也是代码,要有changelog

更新记录:
2024-05-30: 初版发布
2024-06-15: 补充客服分类案例
2024-07-20: 增加Prompt版本管理实践
2024-08-10: 更新Self-Consistency的实测数据