提示词工程完全指南:从零基础到高级优化
Chen Kai BOSS

大语言模型( LLM)的能力边界,很大程度上取决于你怎么"问"它。同样的模型,换个提问方式,输出质量可能天差地别。提示词工程( Prompt Engineering)就是研究如何与 LLM 高效沟通的学问——它不仅是技术,更是一门艺术。

本文从零开始,系统梳理提示词工程的完整知识体系:从基础的清晰性原则,到 Chain-of-Thought 、 Self-Consistency 等进阶技巧,再到 DSPy 、 APE 等自动化优化框架,最后落地到实战场景。无论你是刚接触 LLM 的新手,还是想深入了解提示词优化的研究者,都能在这里找到答案。

基础篇:理解提示词的本质

提示词是什么?为什么重要?

想象你走进一家米其林餐厅,菜单上只写着"给我做点好吃的"。厨师会怎么办?他不知道你想吃中餐还是西餐,不知道你对海鲜过敏,不知道你喜欢重口味还是清淡。最后端上来的菜,很可能不符合你的期待。

提示词( Prompt)就是你给 LLM 的"菜单"。你说得越清楚,模型就越能理解你的需求,生成的内容也就越符合预期。提示词工程,本质上就是学习如何精确表达需求,让模型的能力最大化发挥

为什么提示词如此重要?因为 LLM 是通过"上下文学习"( In-Context Learning)来理解任务的。不同于传统的微调( Fine-tuning)需要更新模型参数,提示词工程只需要调整输入文本,就能引导模型完成各种任务。这意味着:

  1. 零成本迁移:同一个模型,换个提示词就能从写代码切换到写诗,无需重新训练。
  2. 快速迭代:调整提示词比微调模型快几千倍,适合快速验证想法。
  3. 普适性强:好的提示词技巧可以跨模型复用( GPT-4 、 Claude 、 Gemini 等)。

但提示词工程也有"玄学"的一面。同样的意思,换个表达方式,模型表现可能完全不同。比如在数学推理任务中,加一句"Let's think step by step"(让我们一步步思考),准确率能从 17.7% 跳到 78.7%( Kojima et al., 2022)。这种现象背后的原理,需要我们理解 LLM 的工作机制。

LLM 工作原理:必要的背景知识

要写好提示词,得先知道 LLM 是怎么"看懂"你的话的。这里不展开复杂的 Transformer 架构,只讲核心逻辑:

LLM 是一个"下一个词预测器"。给定前面的文本,它会计算所有可能的下一个词的概率分布,然后选择概率最高的那个(或者根据温度参数随机采样)。比如:

1
2
输入:天空是
输出:蓝色的(概率 0.4)、灰色的(概率 0.2)、……

这个过程是自回归的:生成第一个词后,把它加到输入里,再预测下一个词,一直重复直到遇到结束符。所以 LLM 本质上是在"续写"你的提示词。

这带来几个关键启示:

  1. 模型没有"理解":它只是在模式匹配。你的提示词如果和训练数据中的某个模式相似,模型就能表现得很好;否则可能胡说八道。
  2. 顺序很重要: LLM 是从左到右处理文本的,后面的词依赖前面的词。所以提示词的结构(先说什么、后说什么)会显著影响输出。
  3. 上下文窗口有限:模型只能"看到"最近的 N 个 Token( GPT-4 是 128k, Claude 3 是 200k)。超出窗口的内容会被遗忘。

理解这些后,很多提示词技巧就变得自然了。比如为什么要把最重要的信息放在开头或结尾?因为模型对这些位置的"记忆"更牢固(这叫"首因效应"和"近因效应")。为什么要给例子?因为例子能让模型"看到"输入输出的模式,从而模仿。

零样本、少样本、多样本提示:从简单到复杂

根据提供的例子数量,提示词可以分为三种类型:

零样本提示( Zero-Shot Prompting)

最简单直接,不给任何例子,只描述任务:

1
2
3
4
5
提示词:
把下面的句子翻译成英文:我今天很开心。

输出:
I am very happy today.

优点:快速、不需要准备例子、适合通用任务。

缺点:对复杂任务效果差,容易理解偏差。比如你让模型"总结文章",它可能不知道你要的是三句话的摘要还是逐段分析。

少样本提示( Few-Shot Prompting)

给 1-10 个例子,让模型学习模式:

1
2
3
4
5
6
7
8
9
10
11
提示词:
把下面的句子翻译成英文:

中文:我今天很开心。
英文: I am very happy today.

中文:他正在看书。
英文: He is reading a book.

中文:这道菜真好吃。
英文:

模型会输出:"This dish is delicious."

为什么有效? 因为 LLM 在预训练时见过大量"输入-输出"对的模式。给几个例子后,它能快速识别出你想要的格式和风格,这就是"上下文学习"。

关键技巧: - 例子要多样化:覆盖不同情况(肯定句、否定句、疑问句等)。 - 例子要高质量:错误的例子会误导模型。 - 例子的顺序影响输出:相似的例子放在一起,模型更容易泛化。

多样本提示( Many-Shot Prompting)

Google 在 2024 年的研究发现,当上下文窗口足够大时,给成百上千个例子能显著提升性能。比如在机器翻译任务中, Few-Shot( 5 个例子)的 BLEU 分数是 32.5,而 Many-Shot( 500 个例子)能达到 39.2 。

为什么更多例子更好? 因为覆盖了更多边界情况,让模型学到更细致的规律。但要注意: - 需要长上下文模型(至少 64k Token)。 - 例子过多会增加推理成本(按 Token 收费)。 - 存在边际效应递减:从 0 到 10 个例子提升明显,从 100 到 200 提升有限。

实战建议:对于简单任务用零样本,复杂任务用 3-5 个样本,极端困难的任务(如低资源语言翻译)才考虑多样本。

提示词的四要素:角色、任务、格式、约束

一个高质量的提示词,通常包含这四个部分:

角色( Role)

告诉模型"你是谁",设定身份和语气。比如:

1
你是一名资深的 Python 工程师,擅长编写高性能、可维护的代码。

为什么有效? 因为训练数据中,不同角色的表达风格不同。"专家"的回答通常更专业、更详细;"老师"的回答更循循善诱。设定角色能激活模型中相应的知识分布。

常见角色: - 专家/顾问:用于需要专业知识的任务。 - 老师/导师:用于教学、解释概念。 - 助手/秘书:用于辅助完成任务。 - 批评家/评审:用于评估、挑错。

任务( Task)

明确告诉模型"要做什么"。越具体越好:

1
2
❌ 模糊:帮我处理这段代码。
✅ 清晰:找出这段代码中的所有性能瓶颈,并给出优化建议。

关键点: - 用动词开头(分析、生成、总结、转换……)。 - 指明输入和预期输出。 - 拆解复杂任务为多个子任务。

格式( Format)

规定输出的结构,避免模型自由发挥:

1
2
3
4
5
6
请用 JSON 格式输出,包含以下字段:
{
"summary": "一句话总结",
"key_points": ["要点 1", "要点 2"],
"sentiment": "positive/negative/neutral"
}

常用格式: - 列表( Markdown 、编号) - 表格 - JSON/YAML - 代码块 - 问答对

技巧:用"你的输出必须以 XXX 开头"来强制格式。比如:"你的输出必须以 { 开头,以 } 结尾。"

约束( Constraints)

设定边界条件,防止模型跑偏:

1
2
3
4
约束:
1. 回答不超过 200 字。
2. 不要使用专业术语,用小白能懂的语言。
3. 如果无法确定答案,明确说"我不确定",不要编造。

常见约束: - 长度限制(字数、 Token 数) - 风格要求(正式/口语、技术/通俗) - 内容限制(不允许的话题、必须包含的关键词) - 真实性要求(不编造、引用来源)

综合示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
【角色】你是一名经验丰富的技术博客作者,擅长把复杂概念讲清楚。

【任务】为"什么是 Docker"写一篇科普文章,面向没有容器技术背景的读者。

【格式】
分为三部分:
1. 用一个生活类比解释 Docker 是什么
2. 列举 3 个使用 Docker 的实际好处
3. 给出一个简单的入门示例

【约束】
- 全文不超过 500 字
- 不使用"镜像""容器编排"等术语,或者在首次出现时解释清楚
- 用 Markdown 格式输出

这样的提示词,比简单说"介绍一下 Docker"清晰百倍。

基本技巧:清晰性、具体性、结构化

基础阶段的核心原则:

清晰性原则

避免歧义。模型不会反问你,只会按自己的理解执行。比如:

1
2
❌ 模糊:这个方案怎么样?
✅ 清晰:从性能、成本、可维护性三个角度,评估用 Redis 做缓存的方案。

测试方法:把提示词给同事看,如果他们也不确定你想要什么,模型肯定也不知道。

具体性原则

细节决定成败。与其说"写得好一点",不如说"避免重复的形容词,多用动词,句子平均长度控制在 20 字以内"。

1
2
❌ 抽象:让这段话更专业。
✅ 具体:替换口语化表达为学术用语,补充数据支撑,添加文献引用。

结构化设计

用分隔符组织信息。当提示词包含多个部分时,用明显的标记区分:

1
2
3
4
5
6
7
8
9
10
11
12
## 背景信息
用户是一名大学生,正在准备求职。

## 输入内容
[简历内容]

## 任务要求
找出简历中的 3 个主要问题,并给出改进建议。

## 输出格式
问题 1:[描述]
建议:[改进方案]

常用分隔符: - Markdown 标题(##) - 分隔线(---) - XML 标签(<input>...</input>) - 特殊符号("""###

为什么有效? 因为结构化的文本在训练数据中很常见(代码、文档、配置文件),模型能更好地理解不同部分的功能。


进阶篇:让模型"思考"的艺术

基础技巧解决的是"怎么说清楚需求",但对于复杂任务(数学推理、多步规划),光说清楚还不够——你得教模型怎么思考

Chain-of-Thought( CoT):把推理过程写出来

原理与动机

人类解决问题时,通常会把复杂任务拆解为多个步骤。比如计算"23 × 17",你会这样想:

1
2
3
4
5
23 × 17
= 23 × (10 + 7)
= 23 × 10 + 23 × 7
= 230 + 161
= 391

而不是直接蹦出答案"391"。这种"中间推理步骤"就是思维链( Chain-of-Thought)。

LLM 默认是直接输出答案的,但研究发现,让模型生成推理步骤,能显著提升复杂任务的准确率。 Wei et al. 在 2022 年的论文中发现,在 GSM8K(小学数学题)数据集上,标准提示词的准确率是 17.9%,加上 CoT 后提升到 40.7%,增长了 127%。

为什么 CoT 有效?

  1. 激活相关知识:生成中间步骤时,模型会调用更多相关的参数和模式。
  2. 降低推理难度:把"一步到位"变成"多次简单跳跃",每一步都更容易正确。
  3. 可调试性:看到推理过程,你能知道模型在哪里出错,从而调整提示词。

Zero-Shot CoT vs Few-Shot CoT

Zero-Shot CoT:只加一句话引导,不给例子。

1
2
3
4
5
6
7
8
9
10
11
提示词:
罗杰有 5 个网球。他又买了 2 罐网球,每罐 3 个。他现在有多少个网球?

让我们一步步思考。

输出:
1. 罗杰最初有 5 个网球。
2. 他买了 2 罐,每罐 3 个,所以买了 2 × 3 = 6 个。
3. 总共有 5 + 6 = 11 个网球。

答案: 11 个。

关键是那句"让我们一步步思考"( Let's think step by step)。 Kojima et al. (2022) 发现这句话能触发模型的推理模式,准确率从 17.7% 跳到 78.7%。

为什么一句话就有效? 因为训练数据中,很多教学内容、论坛回答都包含"Let's think..."这样的引导语,后面通常跟着详细的推理过程。模型学会了这种模式。

Few-Shot CoT:给几个带推理过程的例子。

1
2
3
4
5
6
7
8
9
10
提示词:
问题:咖啡馆有 23 个苹果。他们用了 20 个做午餐,又买了 6 个。他们现在有多少个苹果?
推理:咖啡馆最初有 23 个苹果。他们用了 20 个,剩下 23 - 20 = 3 个。又买了 6 个,所以现在有 3 + 6 = 9 个。
答案: 9 个。

问题:莉亚有 32 块巧克力,她的姐姐有 42 块。如果她们吃了 35 块,她们总共还剩多少块?
推理:莉亚有 32 块,姐姐有 42 块,总共 32 + 42 = 74 块。吃了 35 块,剩下 74 - 35 = 39 块。
答案: 39 块。

问题:罗杰有 5 个网球。他又买了 2 罐网球,每罐 3 个。他现在有多少个网球?

模型会输出类似的推理过程。

对比: - Zero-Shot CoT 更简单,但依赖模型本身的推理能力(对小模型效果差)。 - Few-Shot CoT 更稳定,但需要准备高质量的例子(推理步骤要正确且清晰)。

实战建议: - 对 GPT-4 、 Claude 3 Opus 等大模型, Zero-Shot CoT 通常够用。 - 对特定领域任务(医学诊断、法律分析), Few-Shot CoT 能提供更专业的推理模式。 - 例子中的推理步骤不要太长( 3-5 步即可),否则模型可能学会啰嗦而不是深刻。

代码实现示例

用 OpenAI API 实现 Zero-Shot CoT:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import openai

def solve_with_cot(problem):
prompt = f"""{problem}

让我们一步步思考。"""

response = openai.ChatCompletion.create(
model="gpt-4",
messages=[
{"role": "user", "content": prompt}
],
temperature=0 # 降低随机性,提高推理稳定性
)

return response.choices[0].message.content

# 测试
problem = "一个停车场有 3 层,每层能停 15 辆车。如果现在已经停了 38 辆车,还能停多少辆?"
result = solve_with_cot(problem)
print(result)

输出:

1
2
3
4
5
1. 停车场总共有 3 × 15 = 45 个车位。
2. 已经停了 38 辆车。
3. 还能停 45 - 38 = 7 辆车。

答案: 7 辆。

优化技巧: - 设置 temperature=0 让推理更确定性。 - 如果任务需要创造性(写故事、头脑风暴),提高 temperature 到 0.7-0.9 。 - 用正则表达式提取最终答案(比如匹配"答案:"后的数字)。

Self-Consistency:用投票提升准确率

CoT 解决了"怎么推理"的问题,但还有一个隐患:同一个问题,模型每次生成的推理路径可能不同,结果也可能不同。哪个结果更可靠?

Self-Consistency(自洽性)的思路很简单:多次运行,投票选出最常见的答案

Sample-and-Marginalize 机制

传统的解码方式是贪心解码( Greedy Decoding):每次选概率最高的词。 Self-Consistency 改用采样( Sampling):每次从概率分布中随机选一个词(根据温度参数),生成多条不同的推理路径。

具体步骤:

  1. 用 CoT 提示词,采样生成 N 条推理路径(比如 N=40)。
  2. 从每条路径中提取最终答案。
  3. 统计答案频次,选择出现次数最多的作为最终答案。

数学上,这相当于对所有可能的推理路径做边缘化( Marginalization):

\[P(\text{答案} | \text{问题}) = \sum_{\text{推理路径}} P(\text{答案}, \text{推理路径} | \text{问题}) \] 直观解释:如果一个答案通过多种不同的推理方式都能得到,说明它更可能正确。

性能提升数据

Wang et al. (2022) 在多个推理数据集上测试了 Self-Consistency,结果显著:

数据集 CoT(单次) Self-Consistency( 40 次采样) 提升
GSM8K(数学) 40.7% 58.6% +17.9%
SVAMP(数学) 64.3% 73.9% +9.6%
AQuA(数学) 35.8% 45.7% +9.9%
StrategyQA(常识) 62.3% 69.5% +7.2%

为什么不是 100% 采样次数越多越好? 因为: - 增加采样次数会线性增加 API 调用成本。 - 边际收益递减:从 1 次到 10 次提升明显,从 30 次到 40 次提升有限。 - 研究发现 10-40 次是性价比最高的区间。

实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from collections import Counter
import openai

def self_consistency_solve(problem, num_samples=10):
prompt = f"""{problem}

让我们一步步思考。"""

answers = []

for _ in range(num_samples):
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=0.7, # 提高随机性以获得多样化的推理路径
max_tokens=500
)

text = response.choices[0].message.content
# 提取答案(假设答案在"答案:"后面)
if "答案:" in text:
answer = text.split("答案:")[1].strip().split()[0]
answers.append(answer)

# 投票
if not answers:
return None

most_common = Counter(answers).most_common(1)[0]
return {
"final_answer": most_common[0],
"confidence": most_common[1] / len(answers),
"all_answers": dict(Counter(answers))
}

# 测试
problem = "一个数的 3 倍加 5 等于 26,这个数是多少?"
result = self_consistency_solve(problem, num_samples=10)
print(f"最终答案:{result['final_answer']}")
print(f"置信度:{result['confidence']:.1%}")
print(f"所有答案分布:{result['all_answers']}")

输出示例:

1
2
3
最终答案: 7
置信度: 90.0%
所有答案分布:{'7': 9, '8': 1}

优化建议: - 对简单任务用 5-10 次采样,复杂任务用 20-40 次。 - 如果答案格式多样("7"、"7 个"、"七"),需要标准化后再投票。 - 记录置信度:如果最高票答案只占 30%,说明模型很不确定,可能需要改进提示词。

Tree of Thoughts( ToT):树形搜索推理

CoT 是线性的推理链,但很多问题需要探索多个可能性,然后回溯。比如走迷宫、下棋、规划旅行路线。 Tree of Thoughts( Yao et al., 2023)就是为此设计的。

树形推理结构

把推理过程建模为一棵树: - 根节点:初始问题。 - 中间节点:中间思考步骤( Thought)。 - 叶节点:最终答案。

模型在每个节点会生成多个可能的下一步(子节点),然后评估每个子节点的"好坏",选择最有希望的继续扩展。如果发现走进死胡同,可以回溯到上一个节点尝试其他路径。

示例任务:用数字 2, 8, 8, 14 通过加减乘除得到 24 。

1
2
3
4
5
6
7
8
9
10
11
12
根节点:{2, 8, 8, 14}
├─ 子节点 1: 2 + 8 = 10 → {10, 8, 14}
│ ├─ 子子节点 1.1: 10 × 8 = 80 → {80, 14}(评估:无法得到 24,剪枝)
│ └─ 子子节点 1.2: 10 - 8 = 2 → {2, 14}(评估:无法得到 24,剪枝)
├─ 子节点 2: 8 ÷ 2 = 4 → {4, 8, 14}
│ └─ 子子节点 2.1: 4 × 8 = 32 → {32, 14}
│ └─ 子子子节点: 32 - 14 = 18(评估:错误)
└─ 子节点 3: 14 - 8 = 6 → {6, 2, 8}
└─ 子子节点 3.1: 6 × 2 = 12 → {12, 8}
└─ 子子子节点: 12 + 8 = 20(评估:错误)
└─ 子子节点 3.2: 6 × 8 = 48 → {48, 2}
└─ 子子子节点: 48 ÷ 2 = 24(成功!)

四个模块

ToT 框架包含四个核心模块:

  1. Prompter(提示生成器):生成候选的下一步思考。

    1
    2
    3
    4
    5
    6
    当前状态:{6, 2, 8}
    提示:请生成 3 个可能的下一步操作。
    输出:
    - 6 × 2 = 12
    - 6 × 8 = 48
    - 2 + 8 = 10

  2. Checker(评估器):评估每个候选步骤的质量。

    1
    2
    3
    候选步骤: 6 × 8 = 48 → {48, 2}
    提示:这个状态离目标 24 有多近?评分 1-10 。
    输出: 8 分。因为 48 ÷ 2 = 24,很接近。

  3. Memory(记忆):记录已探索的路径和评估结果,避免重复搜索。

  4. Controller(控制器):决定搜索策略(广度优先、深度优先、最佳优先)。

与 CoT 对比

维度 Chain-of-Thought Tree of Thoughts
推理结构 线性链 树形图
探索性 单路径 多路径 + 回溯
适用任务 有明确推理步骤的任务 需要试错、规划的任务
成本 低(生成 1 条路径) 高(生成多条路径 + 评估)
实现复杂度 简单 复杂(需要搜索算法)

什么时候用 ToT? - 任务有多个可能的解法(数学题、编程题)。 - 需要规划多步操作(写小说大纲、设计系统架构)。 - 单次推理容易出错,需要对比多个方案。

实现示例(简化版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class TreeOfThoughts:
def __init__(self, model="gpt-4"):
self.model = model

def generate_thoughts(self, state, num_thoughts=3):
"""生成候选的下一步思考"""
prompt = f"""当前状态:{state}
请生成 {num_thoughts} 个可能的下一步操作。"""

# 调用 LLM 生成
response = openai.ChatCompletion.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
n=num_thoughts,
temperature=0.8
)

thoughts = [choice.message.content for choice in response.choices]
return thoughts

def evaluate_thought(self, thought, goal):
"""评估思考步骤的质量"""
prompt = f"""目标:{goal}
当前步骤:{thought}

这个步骤离目标有多近?评分 1-10,并简要说明理由。"""

response = openai.ChatCompletion.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0
)

# 解析评分(假设输出格式是"评分: 8 分。理由:...")
text = response.choices[0].message.content
score = int(text.split(":")[1].split()[0])
return score

def search(self, problem, goal, max_depth=5, beam_width=3):
"""最佳优先搜索"""
import heapq

# 优先队列:(-评分, 深度, 当前状态, 路径)
queue = [(0, 0, problem, [])]

while queue:
neg_score, depth, state, path = heapq.heappop(queue)

if depth >= max_depth:
continue

# 生成候选思考
thoughts = self.generate_thoughts(state, num_thoughts=beam_width)

for thought in thoughts:
score = self.evaluate_thought(thought, goal)
new_path = path + [thought]

# 检查是否达到目标
if score >= 9:
return new_path

# 加入队列
heapq.heappush(queue, (-score, depth + 1, thought, new_path))

return None

# 测试
tot = TreeOfThoughts()
result = tot.search(
problem="用数字 2, 8, 8, 14 通过加减乘除得到 24",
goal="等式结果为 24",
max_depth=4,
beam_width=3
)
print("推理路径:", result)

注意: ToT 的 API 调用次数是 CoT 的几十倍,成本很高。只在确实需要多路径探索时使用。

Graph of Thoughts( GoT):更灵活的 DAG 结构

Tree of Thoughts 的限制是"树"结构——每个节点只有一个父节点。但有些推理过程需要合并多个思路(比如综合多个论据得出结论),这时候需要有向无环图( DAG)。

Graph of Thoughts( Besta et al., 2023)允许: - 聚合( Aggregation):多个节点合并为一个。 - 循环优化( Refinement):迭代改进某个思考。

DAG 结构示例

任务:写一篇技术文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
节点 1:头脑风暴主题(并行生成 3 个主题想法)
├─ 主题 A:深度学习优化器
├─ 主题 B:提示词工程技巧
└─ 主题 C:开源项目评测

节点 2:评估每个主题(打分)
├─ 主题 A: 7 分(技术性强,但受众窄)
├─ 主题 B: 9 分(热门且实用)
└─ 主题 C: 6 分(太泛)

节点 3:选择最佳主题 → 主题 B

节点 4:生成大纲(并行生成 2 个版本)
├─ 大纲 V1:基础 → 进阶 → 实战
└─ 大纲 V2:原理 → 案例 → 工具

节点 5:聚合两个大纲的优点 → 合并大纲

节点 6:撰写文章初稿

节点 7:迭代改进(循环 3 次)
├─ 修正语法错误
├─ 补充例子
└─ 优化逻辑

节点 8:最终文章

这里节点 5 就是"聚合"操作,节点 7 是"循环"操作。

三种操作类型

  1. Generation(生成):创建新节点。

    1
    2
    输入:无(或父节点)
    输出:新想法/内容

  2. Aggregation(聚合):合并多个节点。

    1
    2
    3
    输入:节点 A, 节点 B, 节点 C
    提示:综合以上三个论点,给出一个统一的结论。
    输出:综合结论

  3. Refinement(优化):迭代改进。

    1
    2
    3
    输入:当前版本
    提示:找出问题并改进。
    输出:改进版本

优势分析

vs Tree of Thoughts: - ToT 只能"分叉", GoT 还能"合并"和"循环"。 - GoT 更适合创意任务(写作、设计、头脑风暴)。

实际效果: Besta et al. 在排序任务中发现, GoT 能减少 62% 的 API 调用次数(相比 ToT),同时保持相同的准确率。原因是聚合操作避免了重复探索相似路径。

实现框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class GraphOfThoughts:
def __init__(self):
self.nodes = {} # {node_id: content}
self.edges = [] # [(parent_id, child_id, operation)]

def generate(self, parent_ids, prompt):
"""生成新节点"""
# 获取父节点内容
parent_contents = [self.nodes[pid] for pid in parent_ids]

full_prompt = f"""{prompt}

参考内容:
{parent_contents}"""

response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": full_prompt}]
)

node_id = len(self.nodes)
self.nodes[node_id] = response.choices[0].message.content

for pid in parent_ids:
self.edges.append((pid, node_id, "generate"))

return node_id

def aggregate(self, node_ids, prompt):
"""聚合多个节点"""
contents = [self.nodes[nid] for nid in node_ids]

full_prompt = f"""{prompt}

需要聚合的内容:
""" + "\n\n".join([f"内容{i+1}{c}" for i, c in enumerate(contents)])

response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": full_prompt}]
)

node_id = len(self.nodes)
self.nodes[node_id] = response.choices[0].message.content

for nid in node_ids:
self.edges.append((nid, node_id, "aggregate"))

return node_id

def refine(self, node_id, prompt, iterations=3):
"""迭代优化节点"""
current_id = node_id

for i in range(iterations):
content = self.nodes[current_id]

full_prompt = f"""{prompt}

当前版本:
{content}

请改进。"""

response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": full_prompt}]
)

new_id = len(self.nodes)
self.nodes[new_id] = response.choices[0].message.content
self.edges.append((current_id, new_id, "refine"))

current_id = new_id

return current_id

# 使用示例
got = GraphOfThoughts()

# 生成三个初始想法
idea1 = got.generate([], "头脑风暴:提示词工程的主题")
idea2 = got.generate([], "头脑风暴:提示词工程的主题")
idea3 = got.generate([], "头脑风暴:提示词工程的主题")

# 聚合为最佳主题
best_topic = got.aggregate([idea1, idea2, idea3], "综合三个主题,选择最有价值的")

# 生成大纲
outline = got.generate([best_topic], "为这个主题生成详细大纲")

# 迭代优化大纲
final_outline = got.refine(outline, "改进这个大纲,增加深度和实用性", iterations=2)

print(got.nodes[final_outline])

ReAct:推理与行动的统一

前面的技巧都是"纯推理"——模型只在脑子里想。但很多任务需要与外部世界交互(查数据库、调 API 、运行代码)。 ReAct( Reason + Act)框架让模型能够"边思考边行动"。

Reasoning + Acting 循环

ReAct 的核心是一个四步循环:

  1. Think(思考):分析当前状态,决定下一步做什么。
  2. Act(行动):调用工具执行操作(搜索、计算、查询)。
  3. Observe(观察):获取行动的结果。
  4. Reflect(反思):根据结果调整策略。

示例任务:回答"2023 年诺贝尔物理学奖得主的出生地在哪个国家?"

1
2
3
4
5
6
7
8
Thought 1:我需要先知道 2023 年诺贝尔物理学奖得主是谁。
Action 1: Search["2023 Nobel Prize in Physics winner"]
Observation 1: Anne L'Huillier, Pierre Agostini, and Ferenc Krausz.
Thought 2:现在我需要找到 Anne L'Huillier 的出生地(假设查第一个得主)。
Action 2: Search["Anne L'Huillier birthplace"]
Observation 2: Paris, France.
Thought 3:巴黎在法国。
Answer:法国。

Think-Act-Observe-Reflect 详解

Think(思考):模型生成自然语言的推理。

Act(行动):调用预定义的工具。常见工具: - Search[query]:网络搜索。 - Lookup[keyword]:在文档中查找关键词。 - Calculate[expression]:计算数学表达式。 - Execute[code]:运行代码。

Observe(观察):工具返回结果,追加到上下文。

Reflect(反思):如果结果不符合预期,生成新的思考。

完整示例

1
2
3
4
5
6
7
8
9
10
问题:一个长方形的长是 12 米,宽是长的一半,面积是多少平方米?

Thought 1:需要先算出宽。
Action 1: Calculate[12 / 2]
Observation 1: 6
Thought 2:宽是 6 米。现在算面积。
Action 2: Calculate[12 * 6]
Observation 2: 72
Thought 3:面积是 72 平方米。
Answer: 72 平方米。

应用场景

ReAct 特别适合: - 需要实时信息的任务:天气查询、新闻摘要、股票价格。 - 多步计算:科学计算、财务分析。 - 代码生成与执行:写代码 → 运行 → 调试 → 修正。 - 数据库查询:把自然语言转为 SQL → 执行 → 解释结果。

实现示例(结合 LangChain):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from langchain.agents import initialize_agent, Tool
from langchain.llms import OpenAI
from langchain.tools import DuckDuckGoSearchRun

# 定义工具
search = DuckDuckGoSearchRun()

tools = [
Tool(
name="Search",
func=search.run,
description="用于搜索实时信息。输入应该是一个搜索查询。"
),
Tool(
name="Calculate",
func=lambda x: eval(x),
description="用于数学计算。输入应该是一个数学表达式。"
)
]

# 初始化 ReAct Agent
llm = OpenAI(temperature=0)
agent = initialize_agent(
tools,
llm,
agent="zero-shot-react-description",
verbose=True
)

# 测试
result = agent.run("2024 年 1 月 1 日的北京温度是多少摄氏度?")
print(result)

输出过程(简化):

1
2
3
4
5
Thought: 我需要搜索北京 2024 年 1 月 1 日的天气信息。
Action: Search["北京 2024 年 1 月 1 日 温度"]
Observation: 北京 2024 年 1 月 1 日最高温度 5 ° C,最低温度 -3 ° C 。
Thought: 我得到了温度范围。
Final Answer: 2024 年 1 月 1 日北京的温度在 -3 ° C 到 5 ° C 之间。

注意事项: - 工具描述要清晰,模型才知道什么时候用哪个工具。 - 限制工具调用次数,防止死循环(比如最多 10 次 Act)。 - 对于安全敏感的工具(如执行代码),需要沙盒环境。


高级优化篇:自动化与工程化

手写提示词有两个问题:费时间不稳定。当任务变复杂时,手动调整提示词就像在黑暗中摸索。高级优化技术的目标是让提示词生成和优化自动化

In-Context Learning 深入:少样本到多样本

Few-Shot ICL 原理

In-Context Learning( ICL)是 LLM 的核心能力:给几个例子,模型就能学会任务,无需更新参数。但为什么会有效?

理论解释( Min et al., 2022): 1. 模式识别:例子展示了输入输出的映射关系。 2. 格式学习:例子规定了输出的格式和风格。 3. 任务定位:例子帮模型从海量预训练知识中定位到相关的"子空间"。

关键发现: - 例子的标签可以随机!即使输入和输出不对应,只要格式对,模型性能下降不多。 - 例子的分布比具体内容更重要。覆盖多样化的输入空间比只给相似例子效果好。

Many-Shot ICL 最新进展

Google 在 Gemini 1.5 论文中发现,当上下文窗口达到 100 万 Token 时,可以放数百甚至上千个例子,性能持续提升。

数据(在机器翻译任务中): - 0-shot: BLEU 25.3 - 5-shot: BLEU 32.5 - 50-shot: BLEU 37.8 - 500-shot: BLEU 39.2

为什么更多例子更好? - 覆盖更多边界情况(罕见词汇、复杂句式)。 - 隐式学习到任务的"规则"而非死记例子。

成本问题: 500 个例子可能占 50k Token,输入成本是普通提示词的 100 倍。解决方法: - 例子压缩:去掉冗余内容,只保留关键信息。 - 例子检索:动态选择与当前输入最相关的例子,而非每次都用全部。

Reinforced ICL 与 Unsupervised ICL

Reinforced ICL:用强化学习选择最优例子。

流程: 1. 从候选池中选 K 个例子。 2. 用这 K 个例子测试模型性能(在验证集上)。 3. 把性能作为奖励,训练一个选择策略(比如用 Q-Learning)。

效果:在某些任务中,选对例子比随机选择性能提升 10-20%。

Unsupervised ICL:没有标注数据时,怎么选例子?

方法: - Self-generated examples:让模型自己生成例子。

1
提示:请生成 5 个"情感分类"任务的例子,包含输入文本和标签(正面/负面)。
- Retrieval-based:从大规模语料中检索与任务相关的文本片段作为例子。

Automatic Prompt Engineering( APE):让 LLM 写提示词

手动写提示词费时费力,能不能让 LLM 自己优化提示词? APE( Zhou et al., 2022)的答案是"能"。

自动化提示词生成

核心思路:把提示词优化当成搜索问题——在"提示词空间"中搜索性能最好的那个。

步骤

  1. 生成候选提示词:让 LLM 根据任务描述生成多个提示词。

    1
    2
    3
    4
    5
    6
    输入:任务是情感分类,输入是电影评论,输出是正面/负面标签。

    输出:
    候选 1:判断以下评论是正面还是负面:[评论]
    候选 2:这条评论表达的是积极情绪还是消极情绪?[评论]
    候选 3:作为一名情感分析专家,请分析这条评论的情感倾向:[评论]

  2. 评估候选提示词:在少量测试样本上测试每个候选的性能。

    1
    2
    3
    候选 1 准确率: 75%
    候选 2 准确率: 68%
    候选 3 准确率: 82%

  3. 选择最优提示词:根据评估结果选择表现最好的。

  4. 迭代优化:把最优提示词作为基础,生成变体,重复步骤 2-3 。

评估与筛选

评估指标: - 准确率(分类任务) - BLEU/ROUGE(生成任务) - 人工评分(创意任务)

筛选策略: - Top-K 选择:保留得分最高的 K 个候选。 - 多样性采样:在保证性能的前提下,选择风格多样的候选(避免局部最优)。

实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def generate_prompt_candidates(task_description, num_candidates=5):
"""生成候选提示词"""
prompt = f"""任务描述:{task_description}

请生成 {num_candidates} 个不同的提示词,用于引导语言模型完成这个任务。
每个提示词应该清晰、具体,并包含任务的关键信息。

输出格式(每行一个候选):
候选 1:...
候选 2:...
"""

response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=0.8
)

text = response.choices[0].message.content
candidates = [line.split(":", 1)[1].strip()
for line in text.split("\n") if line.startswith("候选")]
return candidates

def evaluate_prompt(prompt_template, test_samples):
"""评估提示词性能"""
correct = 0

for sample in test_samples:
prompt = prompt_template.replace("[INPUT]", sample["input"])

response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=0
)

output = response.choices[0].message.content.strip()
if output.lower() == sample["label"].lower():
correct += 1

return correct / len(test_samples)

def auto_prompt_engineering(task_description, test_samples, iterations=3):
"""APE 主流程"""
best_prompt = None
best_score = 0

for iteration in range(iterations):
# 生成候选
candidates = generate_prompt_candidates(task_description, num_candidates=5)

# 评估每个候选
for candidate in candidates:
score = evaluate_prompt(candidate, test_samples)
print(f"候选提示词:{candidate[:50]}... | 准确率:{score:.2%}")

if score > best_score:
best_score = score
best_prompt = candidate

# 用最优提示词生成下一轮候选
task_description = f"{task_description}\n\n 参考提示词:{best_prompt}"

return best_prompt, best_score

# 测试
task = "判断电影评论的情感(正面/负面)"
test_data = [
{"input": "这部电影太棒了!", "label": "正面"},
{"input": "浪费时间。", "label": "负面"},
# ... 更多测试样本
]

best_prompt, score = auto_prompt_engineering(task, test_data)
print(f"\n 最优提示词:{best_prompt}")
print(f"准确率:{score:.2%}")

优势: - 节省人力:不需要手动尝试几十个版本。 - 发现反直觉的提示词:有时最优提示词的表达方式人类不会想到。

劣势: - 需要测试样本(冷启动问题)。 - 计算成本高(生成 + 评估需要大量 API 调用)。

DSPy 框架:把提示词当代码

手写提示词就像写汇编语言——费力且易错。 DSPy( Khattab et al., 2023)提出"把提示词当代码",用高层抽象自动生成和优化提示词。

Treat Prompts As Code 理念

传统方式

1
2
3
4
5
6
7
8
prompt = """你是一名数据分析师。请根据以下数据回答问题。

数据:{data}
问题:{question}

答案:"""

response = llm(prompt.format(data=data, question=question))

DSPy 方式

1
2
3
4
5
6
7
8
9
10
import dspy

class QA(dspy.Signature):
"""根据数据回答问题"""
data = dspy.InputField()
question = dspy.InputField()
answer = dspy.OutputField()

qa = dspy.Predict(QA)
response = qa(data=data, question=question)

区别在于: - DSPy 用类型化的签名( Signature)定义输入输出,而非手写字符串。 - DSPy 会自动生成最优的提示词格式。 - DSPy 支持编译优化( Compilation):根据训练数据调整提示词。

Optimizers 类型

DSPy 提供多种优化器,自动改进提示词:

  1. BootstrapFewShot:自动选择最佳的 Few-Shot 例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from dspy.teleprompt import BootstrapFewShot

    # 定义评估指标
    def accuracy(example, prediction):
    return example.answer == prediction.answer

    # 编译(优化)
    optimizer = BootstrapFewShot(metric=accuracy, max_bootstrapped_demos=5)
    optimized_qa = optimizer.compile(qa, trainset=train_data)

  2. MIPRO( Multi-Prompt Instruction Optimization):搜索最优的指令文本。

  3. Ensemble:组合多个提示词的输出,投票得到最终答案。

实战案例

任务:从新闻文章中提取关键信息(时间、地点、人物)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import dspy

# 配置 LLM
lm = dspy.OpenAI(model="gpt-4")
dspy.settings.configure(lm=lm)

# 定义签名
class ExtractInfo(dspy.Signature):
"""从新闻文章中提取关键信息"""
article = dspy.InputField(desc="新闻文章全文")
time = dspy.OutputField(desc="事件发生时间")
location = dspy.OutputField(desc="事件发生地点")
person = dspy.OutputField(desc="相关人物")

# 创建模块
extractor = dspy.ChainOfThought(ExtractInfo)

# 测试(未优化)
article = "2024 年 3 月 15 日,特斯拉 CEO 埃隆·马斯克在加州宣布了新款电动卡车的量产计划。"
result = extractor(article=article)
print(result)

# 准备训练数据
train_data = [
dspy.Example(
article="2023 年 12 月, OpenAI 在旧金山发布了 GPT-4 Turbo 。",
time="2023 年 12 月",
location="旧金山",
person="OpenAI"
),
# ... 更多例子
]

# 优化
from dspy.teleprompt import BootstrapFewShot

def validate(example, prediction):
return (example.time == prediction.time and
example.location == prediction.location and
example.person == prediction.person)

optimizer = BootstrapFewShot(metric=validate, max_bootstrapped_demos=3)
optimized_extractor = optimizer.compile(extractor, trainset=train_data)

# 测试(优化后)
result = optimized_extractor(article=article)
print(result)

优化效果: - 未优化:准确率 60%(经常漏提关键信息)。 - 优化后:准确率 85%( DSPy 自动添加了 CoT 推理步骤和最佳例子)。

为什么 DSPy 好用? - 抽象层次高:关注"做什么"而非"怎么说"。 - 自动优化:省去手动调整提示词的痛苦。 - 可组合:多个模块可以串联(比如"检索 → 总结 → 问答")。

提示词压缩:用更少的 Token 做更多事

LLM 按 Token 收费,长提示词意味着高成本。提示词压缩技术通过去除冗余信息,在保持性能的前提下减少 Token 数。

LLMLingua 系列

LLMLingua( Jiang et al., 2023)的思路:用一个小模型判断哪些 Token 对任务重要,删除不重要的。

步骤

  1. 重要性打分:用小型语言模型(如 GPT-2)计算每个 Token 的困惑度( Perplexity)。困惑度低 = 信息量大 = 重要。

  2. 动态删除:保留困惑度最低的 K% Token,删除其余。

  3. 重组提示词:把保留的 Token 拼接成新的提示词。

示例

原始提示词( 100 Token):

1
2
3
4
5
请根据以下背景信息回答问题。背景信息非常详细,包含了很多上下文细节,需要仔细阅读。

背景:在遥远的未来,人类已经殖民了火星。火星上的第一座城市叫做"新希望",建立于 2157 年。这座城市有 300 万居民,主要经济来源是采矿和科研。

问题:火星第一座城市叫什么?

压缩后( 40 Token,压缩率 60%):

1
2
3
4
5
背景信息回答问题。

背景:未来人类殖民火星第一座城市"新希望"建立 2157 年 300 万居民采矿科研。

问题:火星第一座城市?

效果:虽然可读性下降,但 LLM 依然能理解(因为关键词都在)。在问答任务中,压缩 50% 后准确率只下降 2%。

Token 去除策略

几种常见策略:

  1. 去除停用词:删除"的""是""在"等高频但低信息量的词。

  2. 保留关键名词和动词:用 NER(命名实体识别)和 POS(词性标注)识别重要词汇。

  3. 句子级压缩:删除对任务无关的整句话(比如寒暄、解释性文字)。

  4. 动态压缩:根据上下文窗口使用率调整压缩率(窗口快满了就压缩多一点)。

20x 压缩率

LongLLMLingua(最新版本)能达到 20 倍压缩率,同时保持 90% 以上的性能。

关键技术: - 问题感知压缩( Question-Aware):根据问题保留相关内容,删除无关内容。

1
2
3
4
5
问题:巴黎在哪个国家?

原文:巴黎是法国的首都,也是该国最大的城市。它位于法国北部,塞纳河畔。巴黎有"光之城"的美称,是全球最受欢迎的旅游目的地之一。每年有数百万游客来此参观埃菲尔铁塔、卢浮宫等景点。

压缩后:巴黎法国首都

  • 迭代压缩:多轮压缩,每轮用不同策略(第一轮删句子,第二轮删词)。

实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

class PromptCompressor:
def __init__(self, model_name="gpt2"):
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForCausalLM.from_pretrained(model_name)

def calculate_perplexity(self, text):
"""计算每个 Token 的困惑度"""
inputs = self.tokenizer(text, return_tensors="pt")
with torch.no_grad():
outputs = self.model(**inputs, labels=inputs["input_ids"])
loss = outputs.loss
return torch.exp(loss).item()

def compress(self, text, compression_rate=0.5):
"""压缩文本"""
tokens = self.tokenizer.tokenize(text)
token_ids = self.tokenizer.convert_tokens_to_ids(tokens)

# 计算每个 Token 的重要性(简化版:用 Token 频率的倒数)
from collections import Counter
freq = Counter(tokens)
importance = {token: 1 / freq[token] for token in tokens}

# 排序并保留重要的 Token
sorted_tokens = sorted(
enumerate(tokens),
key=lambda x: importance[x[1]],
reverse=True
)

num_keep = int(len(tokens) * compression_rate)
keep_indices = sorted([idx for idx, _ in sorted_tokens[:num_keep]])

compressed_tokens = [tokens[i] for i in keep_indices]
compressed_text = self.tokenizer.convert_tokens_to_string(compressed_tokens)

return compressed_text

# 测试
compressor = PromptCompressor()
original = "请详细解释一下什么是机器学习,包括它的定义、应用场景以及与深度学习的区别。"
compressed = compressor.compress(original, compression_rate=0.4)

print(f"原始({len(original)} 字符):{original}")
print(f"压缩后({len(compressed)} 字符):{compressed}")

注意事项: - 过度压缩会导致语义丢失。建议在验证集上测试不同压缩率的效果。 - 对于需要精确表达的任务(法律、医疗),慎用压缩。 - 压缩本身也有计算成本(运行小模型打分),权衡是否值得。


实战篇:从理论到应用

前面讲了一大堆技巧,但"知道"和"会用"是两回事。这部分聚焦实战:不同任务怎么设计提示词?怎么迭代优化?常见错误如何避免?

不同任务的提示词模板

文本生成

任务:写一篇技术博客。

提示词结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
【角色】你是一名有 10 年经验的技术作家,擅长把复杂概念讲清楚。

【任务】为"什么是 Kubernetes"写一篇 1000 字的科普文章。

【目标读者】刚学编程的大学生,没有容器技术背景。

【要求】
1. 开头用一个生活类比引入
2. 解释 Kubernetes 解决的核心问题
3. 给出一个简单的使用场景
4. 结尾给出学习路径建议

【风格】
- 用"你"而非"您"
- 多用短句
- 避免行话(或在首次出现时解释)

【输出格式】
标题
正文(分 4 个小节,每节 2-3 段)

技巧: - 明确字数限制(防止输出太长或太短)。 - 规定结构(小节数量),保证逻辑完整。 - 提供负面例子("不要这样写:……")。

问答系统

任务:基于知识库回答用户问题。

提示词结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
【背景知识】
{从数据库检索到的相关文档}

【问题】
{用户的提问}

【回答要求】
1. 只使用背景知识中的信息,不要编造
2. 如果背景知识不足以回答问题,明确说"根据现有信息无法确定"
3. 用简洁的语言回答(不超过 100 字)
4. 如果可能,引用具体的段落或数据

【输出格式】
答案:...
来源:[引用的段落编号]

技巧: - 用"背景知识"框住信息来源,减少幻觉( Hallucination)。 - 强制引用来源,提高可信度。 - 设置"我不知道"的退出机制。

进阶: RAG( Retrieval-Augmented Generation)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def rag_qa(question, knowledge_base):
# 1. 检索相关文档
from sentence_transformers import SentenceTransformer, util

retriever = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')
question_emb = retriever.encode(question)
doc_embs = retriever.encode(knowledge_base)

scores = util.cos_sim(question_emb, doc_embs)[0]
top_k = scores.topk(3)

relevant_docs = [knowledge_base[idx] for idx in top_k.indices]

# 2. 构建提示词
prompt = f"""【背景知识】
{chr(10).join([f'文档{i+1}{doc}' for i, doc in enumerate(relevant_docs)])}

【问题】
{question}

【回答要求】
只使用背景知识中的信息,不要编造。如果背景知识不足以回答问题,明确说"根据现有信息无法确定"。

答案:"""

# 3. 调用 LLM
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=0
)

return response.choices[0].message.content

代码生成

任务:生成可运行的 Python 函数。

提示词结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
【任务】
写一个 Python 函数,实现二分查找。

【函数签名】
def binary_search(arr: List[int], target: int) -> int:
"""
在有序数组中查找目标值。

参数:
arr: 有序整数数组
target: 要查找的目标值

返回:
目标值的索引,如果不存在返回 -1
"""

【要求】
1. 使用迭代而非递归
2. 时间复杂度 O(log n)
3. 添加详细注释
4. 包含错误处理(空数组等)

【输出格式】
只输出代码,不要有额外的解释文字。
代码要用 Python 的 ```python ``` 包裹。

【测试用例】
assert binary_search([1, 3, 5, 7, 9], 5) == 2
assert binary_search([1, 3, 5, 7, 9], 4) == -1
assert binary_search([], 1) == -1

技巧: - 给出函数签名和类型标注(约束输出格式)。 - 提供测试用例(让模型自我验证)。 - 明确算法要求(防止用暴力解法)。

进阶: ReAct 迭代生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def generate_and_test(task, test_cases, max_iterations=3):
for i in range(max_iterations):
# 生成代码
prompt = f"""{task}

已尝试 {i} 次。请生成代码。"""

code = llm_generate(prompt)

# 运行测试
try:
exec(code)
# 执行测试用例
all_passed = True
for test in test_cases:
if not eval(test):
all_passed = False
break

if all_passed:
return code
else:
task += "\n\n 【上次生成的代码未通过测试,请修正】"
except Exception as e:
task += f"\n\n 【上次代码运行出错:{str(e)},请修正】"

return None

数据分析

任务:从数据中提取洞察。

提示词结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
【数据】
{CSV 或 JSON 格式的数据}

【任务】
分析这份销售数据,找出:
1. 销量最高的 3 个产品
2. 销量增长最快的品类
3. 销量下降的原因(如果有)

【输出格式】
## 关键发现
- 发现 1:...
- 发现 2:...

## 数据支撑
- 产品 A 销量: XXX(占比 XX%)
- ...

## 建议
- 建议 1:...
- 建议 2:...

【要求】
- 所有结论必须有数据支撑
- 用百分比和具体数字,不要用"很多""较少"等模糊表达
- 如果数据不足以得出某个结论,说明需要哪些额外数据

技巧: - 结构化输出(用 Markdown 标题分节)。 - 要求量化(数字比形容词更有说服力)。 - 区分"发现"和"建议"(分析和行动)。

提示词迭代优化流程

提示词很少一次写对。推荐一个系统化的迭代流程:

第一步:基线测试

用最简单的提示词测试任务可行性。

1
2
3
任务:把英文翻译成中文。

输入: Hello, world!

记录输出质量。如果完全不行,可能任务超出模型能力。

第二步:添加细节

根据基线输出的问题,逐步添加约束。

1
2
3
4
5
【问题】输出太口语化。
【改进】添加约束"使用书面语,避免口语化表达"。

【问题】漏翻了专业术语。
【改进】添加"遇到专业术语,音译或保留原文(如 'API' 翻译为 'API')"。

每次只改一个点,测试效果。

第三步:添加例子

如果模型还是理解偏差,给 Few-Shot 例子。

1
2
3
4
5
6
7
8
9
10
示例 1:
输入: The API returns a JSON response.
输出:该 API 返回 JSON 格式的响应。

示例 2:
输入: Machine learning models require large datasets.
输出:机器学习模型需要大规模数据集。

(现在翻译)
输入: Deep learning is a subset of machine learning.

第四步:结构化

用分隔符和标题组织复杂提示词。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## 背景
你是一名专业的技术文档翻译员。

## 任务
把下面的英文文档翻译成中文。

## 输入
{文档内容}

## 要求
- 使用书面语
- 保留专业术语的英文原文
- 保持 Markdown 格式

## 输出
{翻译后的内容}

第五步:量化评估

在测试集上跑批量测试,记录指标(准确率、 BLEU 、人工评分)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def batch_evaluate(prompts, test_set):
results = {}

for prompt_name, prompt_template in prompts.items():
scores = []

for sample in test_set:
output = llm_generate(prompt_template.format(**sample))
score = calculate_metric(output, sample["reference"])
scores.append(score)

results[prompt_name] = {
"mean": np.mean(scores),
"std": np.std(scores),
"scores": scores
}

return results

第六步: A/B 测试

如果有多个候选提示词,用 A/B 测试选最优。

1
2
3
4
5
6
7
8
9
10
11
12
# 统计显著性检验
from scipy import stats

scores_A = [0.8, 0.85, 0.9, ...]
scores_B = [0.82, 0.88, 0.91, ...]

t_stat, p_value = stats.ttest_ind(scores_A, scores_B)

if p_value < 0.05:
print("提示词 B 显著优于 A")
else:
print("两者无显著差异")

常见错误与调试技巧

错误 1:提示词过于模糊

症状:每次输出都不同,质量不稳定。

例子

1
❌ 模糊提示词:帮我写点东西。

调试: - 问自己:如果让同事帮忙,你会怎么说? - 添加具体要求(字数、格式、风格)。

改进

1
✅ 清晰提示词:写一篇 500 字的科普文章,介绍"什么是量子计算",面向高中生读者,用类比的方式解释,不涉及复杂公式。

错误 2:指令冲突

症状:模型输出不符合任何一个要求。

例子

1
2
3
❌ 冲突指令:
- 回答要简洁(不超过 50 字)
- 回答要详细(包含背景、原因、影响)

调试: - 检查要求之间是否矛盾。 - 用"优先级"明确哪个要求更重要。

改进

1
2
3
✅ 明确优先级:
- 优先保证简洁(不超过 100 字)
- 在简洁的前提下,尽量包含背景和原因

错误 3:过度依赖隐含知识

症状:模型在你熟悉的领域表现好,在陌生领域表现差。

例子

1
2
❌ 隐含假设:优化这段代码。
(模型不知道你关心的是速度还是可读性)

调试: - 把所有假设写明。 - 假装模型是个新人,需要手把手教。

改进

1
✅ 显式说明:优化这段代码的运行速度。重点关注循环和数据结构。可读性是次要的。

错误 4:忽略负面例子

症状:模型总是犯同样的错误。

例子

1
2
3
❌ 只给正例:
好的回答:...
好的回答:...

调试: - 添加"不要这样做"的例子。

改进

1
2
3
✅ 正例 + 反例:
好的回答:简洁且有数据支撑。
不好的回答:用了很多形容词但没有具体信息(避免这种)。

错误 5:温度参数设置不当

症状:输出太保守(每次都一样)或太发散(完全不可控)。

调试技巧

任务类型 推荐温度 理由
数学推理、代码生成 0-0.2 需要确定性输出
文本摘要、翻译 0.3-0.5 需要准确但允许一些变化
创意写作、头脑风暴 0.7-1.0 需要多样性和新颖性

实验:在验证集上测试不同温度,画出"温度-质量"曲线。

1
2
3
4
temperatures = [0, 0.2, 0.5, 0.7, 1.0]
for temp in temperatures:
score = evaluate(prompt, test_set, temperature=temp)
print(f"Temperature {temp}: {score:.2%}")

评估方法

自动评估

分类任务:准确率、 F1 分数、混淆矩阵。

1
2
3
4
5
6
from sklearn.metrics import classification_report

y_true = ["正面", "负面", "正面", ...]
y_pred = ["正面", "正面", "正面", ...]

print(classification_report(y_true, y_pred))

生成任务: BLEU(机器翻译)、 ROUGE(摘要)、 BERTScore(语义相似度)。

1
2
3
4
5
from rouge_score import rouge_scorer

scorer = rouge_scorer.RougeScorer(['rouge1', 'rougeL'], use_stemmer=True)
scores = scorer.score(reference, generated)
print(scores)

人工评估

当自动指标不可靠时(创意写作、对话系统),需要人工评分。

评分维度: - 准确性:内容是否正确? - 流畅性:语言是否自然? - 相关性:是否回答了问题? - 创新性:是否有新颖见解?

评分量表( 1-5 分):

1
2
3
4
5
1 分:完全不可用
2 分:有严重问题
3 分:基本可用,有明显不足
4 分:良好,有小瑕疵
5 分:优秀,无明显问题

评估流程: 1. 随机抽样(如 100 个样本)。 2. 多个评估者独立打分(至少 2 人)。 3. 计算评估者间一致性( Cohen's Kappa)。 4. 讨论分歧案例,达成共识。

LLM-as-a-Judge

用另一个 LLM 评估输出质量(成本低于人工评估)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def llm_judge(question, answer):
prompt = f"""【任务】评估以下回答的质量。

【问题】
{question}

【回答】
{answer}

【评分标准】
- 准确性(是否正确回答问题)
- 完整性(是否覆盖关键点)
- 清晰性(是否易于理解)

【输出格式】
评分: X/10
理由:...
"""

response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=0
)

text = response.choices[0].message.content
score = int(text.split("/")[0].split(":")[1])
return score

注意: LLM 评估有偏见(倾向给自己生成的内容高分)。用不同的模型交叉验证。


总结与展望

提示词工程是 LLM 时代的核心技能。从最基本的清晰性原则,到 Chain-of-Thought 、 Self-Consistency 等进阶技巧,再到 DSPy 、 APE 等自动化框架,我们看到了这个领域的快速演进。

核心要点回顾

  1. 基础很重要:清晰、具体、结构化的提示词是一切优化的前提。
  2. 让模型思考:复杂任务需要 CoT 、 ToT 等技术引导推理过程。
  3. 投票提升稳定性: Self-Consistency 通过多次采样和投票减少随机性。
  4. 工具增强能力: ReAct 让模型能调用外部工具,突破纯语言的限制。
  5. 自动化是趋势:手写提示词费时费力, APE 、 DSPy 等框架能自动优化。
  6. 压缩降低成本: LLMLingua 等技术在保持性能的前提下大幅减少 Token 数。

未来方向

  • 多模态提示词:结合文本、图像、音频的提示词( CLIP 、 GPT-4V)。
  • 个性化提示词:根据用户历史自动调整提示词风格。
  • 提示词安全:防止提示词注入攻击( Prompt Injection)。
  • 可解释性:理解为什么某个提示词有效,而非黑盒优化。

提示词工程还在快速发展。今天的最佳实践,可能明天就被新技术取代。但核心思想不变:清晰地表达需求,引导模型的推理过程,迭代优化直到满意

掌握这些技巧,你就能最大化发挥 LLM 的能力——无论是写代码、分析数据,还是创作内容。提示词工程不仅是技术,更是一种与 AI 高效协作的思维方式。


参考文献

  1. Wei et al. (2022). Chain-of-Thought Prompting Elicits Reasoning in Large Language Models. NeurIPS.
  2. Wang et al. (2022). Self-Consistency Improves Chain of Thought Reasoning in Language Models. ICLR.
  3. Yao et al. (2023). Tree of Thoughts: Deliberate Problem Solving with Large Language Models. NeurIPS.
  4. Besta et al. (2023). Graph of Thoughts: Solving Elaborate Problems with Large Language Models. AAAI.
  5. Kojima et al. (2022). Large Language Models are Zero-Shot Reasoners. NeurIPS.
  6. Zhou et al. (2022). Large Language Models Are Human-Level Prompt Engineers. ICLR.
  7. Khattab et al. (2023). DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines. arXiv.
  8. Jiang et al. (2023). LLMLingua: Compressing Prompts for Accelerated Inference of Large Language Models. EMNLP.
  9. Min et al. (2022). Rethinking the Role of Demonstrations: What Makes In-Context Learning Work?. EMNLP.
  10. Yao et al. (2022). ReAct: Synergizing Reasoning and Acting in Language Models. ICLR.
  • 本文标题:提示词工程完全指南:从零基础到高级优化
  • 本文作者:Chen Kai
  • 创建时间:2025-03-03 10:15:00
  • 本文链接:https://www.chenk.top/%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%B7%A5%E7%A8%8B%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%EF%BC%9A%E4%BB%8E%E9%9B%B6%E5%9F%BA%E7%A1%80%E5%88%B0%E9%AB%98%E7%BA%A7%E4%BC%98%E5%8C%96/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论