大语言模型(
LLM)的能力边界,很大程度上取决于你怎么"问"它。同样的模型,换个提问方式,输出质量可能天差地别。提示词工程(
Prompt Engineering)就是研究如何与 LLM
高效沟通的学问——它不仅是技术,更是一门艺术。
本文从零开始,系统梳理提示词工程的完整知识体系:从基础的清晰性原则,到
Chain-of-Thought 、 Self-Consistency 等进阶技巧,再到 DSPy 、 APE
等自动化优化框架,最后落地到实战场景。无论你是刚接触 LLM
的新手,还是想深入了解提示词优化的研究者,都能在这里找到答案。
基础篇:理解提示词的本质
提示词是什么?为什么重要?
想象你走进一家米其林餐厅,菜单上只写着"给我做点好吃的"。厨师会怎么办?他不知道你想吃中餐还是西餐,不知道你对海鲜过敏,不知道你喜欢重口味还是清淡。最后端上来的菜,很可能不符合你的期待。
提示词( Prompt)就是你给 LLM
的"菜单"。你说得越清楚,模型就越能理解你的需求,生成的内容也就越符合预期。提示词工程,本质上就是学习如何精确表达需求,让模型的能力最大化发挥 。
为什么提示词如此重要?因为 LLM 是通过"上下文学习"( In-Context
Learning)来理解任务的。不同于传统的微调(
Fine-tuning)需要更新模型参数,提示词工程只需要调整输入文本,就能引导模型完成各种任务。这意味着:
零成本迁移 :同一个模型,换个提示词就能从写代码切换到写诗,无需重新训练。
快速迭代 :调整提示词比微调模型快几千倍,适合快速验证想法。
普适性强 :好的提示词技巧可以跨模型复用( 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 本质上是在"续写"你的提示词。
这带来几个关键启示:
模型没有"理解" :它只是在模式匹配。你的提示词如果和训练数据中的某个模式相似,模型就能表现得很好;否则可能胡说八道。
顺序很重要 : LLM
是从左到右处理文本的,后面的词依赖前面的词。所以提示词的结构(先说什么、后说什么)会显著影响输出。
上下文窗口有限 :模型只能"看到"最近的 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 ❌ 模糊:帮我处理这段代码。 ✅ 清晰:找出这段代码中的所有性能瓶颈,并给出优化建议。
关键点 : - 用动词开头(分析、生成、总结、转换……)。
- 指明输入和预期输出。 - 拆解复杂任务为多个子任务。
规定输出的结构,避免模型自由发挥:
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 有效?
激活相关知识 :生成中间步骤时,模型会调用更多相关的参数和模式。
降低推理难度 :把"一步到位"变成"多次简单跳跃",每一步都更容易正确。
可调试性 :看到推理过程,你能知道模型在哪里出错,从而调整提示词。
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 openaidef 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):每次从概率分布中随机选一个词(根据温度参数),生成多条不同的推理路径。
具体步骤:
用 CoT 提示词,采样生成 N 条推理路径(比如 N=40)。
从每条路径中提取最终答案。
统计答案频次,选择出现次数最多的作为最终答案。
数学上,这相当于对所有可能的推理路径做边缘化(
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 Counterimport openaidef 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 框架包含四个核心模块:
Prompter(提示生成器) :生成候选的下一步思考。
1 2 3 4 5 6 当前状态:{6, 2, 8} 提示:请生成 3 个可能的下一步操作。 输出: - 6 × 2 = 12 - 6 × 8 = 48 - 2 + 8 = 10
Checker(评估器) :评估每个候选步骤的质量。
1 2 3 候选步骤: 6 × 8 = 48 → {48, 2} 提示:这个状态离目标 24 有多近?评分 1-10 。 输出: 8 分。因为 48 ÷ 2 = 24,很接近。
Memory(记忆) :记录已探索的路径和评估结果,避免重复搜索。
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} 个可能的下一步操作。""" 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 ) 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 是"循环"操作。
三种操作类型
Generation(生成) :创建新节点。
Aggregation(聚合) :合并多个节点。
1 2 3 输入:节点 A, 节点 B, 节点 C 提示:综合以上三个论点,给出一个统一的结论。 输出:综合结论
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 = {} self.edges = [] 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 的核心是一个四步循环:
Think(思考) :分析当前状态,决定下一步做什么。
Act(行动) :调用工具执行操作(搜索、计算、查询)。
Observe(观察) :获取行动的结果。
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, Toolfrom langchain.llms import OpenAIfrom langchain.tools import DuckDuckGoSearchRunsearch = DuckDuckGoSearchRun() tools = [ Tool( name="Search" , func=search.run, description="用于搜索实时信息。输入应该是一个搜索查询。" ), Tool( name="Calculate" , func=lambda x: eval (x), description="用于数学计算。输入应该是一个数学表达式。" ) ] 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)的答案是"能"。
自动化提示词生成
核心思路 :把提示词优化当成搜索问题——在"提示词空间"中搜索性能最好的那个。
步骤 :
生成候选提示词 :让 LLM
根据任务描述生成多个提示词。 1 2 3 4 5 6 输入:任务是情感分类,输入是电影评论,输出是正面/负面标签。 输出: 候选 1:判断以下评论是正面还是负面:[评论] 候选 2:这条评论表达的是积极情绪还是消极情绪?[评论] 候选 3:作为一名情感分析专家,请分析这条评论的情感倾向:[评论]
评估候选提示词 :在少量测试样本上测试每个候选的性能。
1 2 3 候选 1 准确率: 75% 候选 2 准确率: 68% 候选 3 准确率: 82%
选择最优提示词 :根据评估结果选择表现最好的。
迭代优化 :把最优提示词作为基础,生成变体,重复步骤
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 dspyclass 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 提供多种优化器,自动改进提示词:
BootstrapFewShot :自动选择最佳的 Few-Shot 例子。
1 2 3 4 5 6 7 8 9 from dspy.teleprompt import BootstrapFewShotdef accuracy (example, prediction ): return example.answer == prediction.answer optimizer = BootstrapFewShot(metric=accuracy, max_bootstrapped_demos=5 ) optimized_qa = optimizer.compile (qa, trainset=train_data)
MIPRO ( Multi-Prompt Instruction
Optimization):搜索最优的指令文本。
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 dspylm = 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 BootstrapFewShotdef 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 对任务重要,删除不重要的。
步骤 :
重要性打分 :用小型语言模型(如 GPT-2)计算每个
Token 的困惑度( Perplexity)。困惑度低 = 信息量大 = 重要。
动态删除 :保留困惑度最低的 K%
Token,删除其余。
重组提示词 :把保留的 Token
拼接成新的提示词。
示例 :
原始提示词( 100 Token):
1 2 3 4 5 请根据以下背景信息回答问题。背景信息非常详细,包含了很多上下文细节,需要仔细阅读。 背景:在遥远的未来,人类已经殖民了火星。火星上的第一座城市叫做"新希望",建立于 2157 年。这座城市有 300 万居民,主要经济来源是采矿和科研。 问题:火星第一座城市叫什么?
压缩后( 40 Token,压缩率 60%):
1 2 3 4 5 背景信息回答问题。 背景:未来人类殖民火星第一座城市"新希望"建立 2157 年 300 万居民采矿科研。 问题:火星第一座城市?
效果 :虽然可读性下降,但 LLM
依然能理解(因为关键词都在)。在问答任务中,压缩 50% 后准确率只下降
2%。
Token 去除策略
几种常见策略:
去除停用词 :删除"的""是""在"等高频但低信息量的词。
保留关键名词和动词 :用 NER(命名实体识别)和
POS(词性标注)识别重要词汇。
句子级压缩 :删除对任务无关的整句话(比如寒暄、解释性文字)。
动态压缩 :根据上下文窗口使用率调整压缩率(窗口快满了就压缩多一点)。
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, AutoModelForCausalLMimport torchclass 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) from collections import Counter freq = Counter(tokens) importance = {token: 1 / freq[token] for token in tokens} 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 ): 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] prompt = f"""【背景知识】 {chr (10 ).join([f'文档{i+1 } :{doc} ' for i, doc in enumerate (relevant_docs)])} 【问题】 {question} 【回答要求】 只使用背景知识中的信息,不要编造。如果背景知识不足以回答问题,明确说"根据现有信息无法确定"。 答案:""" 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 statsscores_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 ✅ 清晰提示词:写一篇 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_reporty_true = ["正面" , "负面" , "正面" , ...] y_pred = ["正面" , "正面" , "正面" , ...] print (classification_report(y_true, y_pred))
生成任务 : BLEU(机器翻译)、 ROUGE(摘要)、
BERTScore(语义相似度)。
1 2 3 4 5 from rouge_score import rouge_scorerscorer = 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
等自动化框架,我们看到了这个领域的快速演进。
核心要点回顾 :
基础很重要 :清晰、具体、结构化的提示词是一切优化的前提。
让模型思考 :复杂任务需要 CoT 、 ToT
等技术引导推理过程。
投票提升稳定性 : Self-Consistency
通过多次采样和投票减少随机性。
工具增强能力 : ReAct
让模型能调用外部工具,突破纯语言的限制。
自动化是趋势 :手写提示词费时费力, APE 、 DSPy
等框架能自动优化。
压缩降低成本 : LLMLingua
等技术在保持性能的前提下大幅减少 Token 数。
未来方向 :
多模态提示词 :结合文本、图像、音频的提示词( CLIP
、 GPT-4V)。
个性化提示词 :根据用户历史自动调整提示词风格。
提示词安全 :防止提示词注入攻击( Prompt
Injection)。
可解释性 :理解为什么某个提示词有效,而非黑盒优化。
提示词工程还在快速发展。今天的最佳实践,可能明天就被新技术取代。但核心思想不变:清晰地表达需求,引导模型的推理过程,迭代优化直到满意 。
掌握这些技巧,你就能最大化发挥 LLM
的能力——无论是写代码、分析数据,还是创作内容。提示词工程不仅是技术,更是一种与
AI 高效协作的思维方式。
参考文献
Wei et al. (2022). Chain-of-Thought Prompting Elicits Reasoning
in Large Language Models . NeurIPS.
Wang et al. (2022). Self-Consistency Improves Chain of Thought
Reasoning in Language Models . ICLR.
Yao et al. (2023). Tree of Thoughts: Deliberate Problem Solving
with Large Language Models . NeurIPS.
Besta et al. (2023). Graph of Thoughts: Solving Elaborate
Problems with Large Language Models . AAAI.
Kojima et al. (2022). Large Language Models are Zero-Shot
Reasoners . NeurIPS.
Zhou et al. (2022). Large Language Models Are Human-Level Prompt
Engineers . ICLR.
Khattab et al. (2023). DSPy: Compiling Declarative Language
Model Calls into Self-Improving Pipelines . arXiv.
Jiang et al. (2023). LLMLingua: Compressing Prompts for
Accelerated Inference of Large Language Models . EMNLP.
Min et al. (2022). Rethinking the Role of Demonstrations: What
Makes In-Context Learning Work? . EMNLP.
Yao et al. (2022). ReAct: Synergizing Reasoning and Acting in
Language Models . ICLR.