同样的模型,两种问法:一种在小学数学题上准确率 17%,另一种 78%。差别不是玄学,而是提示词工程。这篇文章系统梳理那些真正有效的技巧、它们为什么有效,以及如何在生产环境里把提示词当成一个工程问题来优化。
你会学到什么 基础 :零样本、少样本、多样本、任务分解,以及"五块结构"的提示词骨架。推理 :Chain-of-Thought、Self-Consistency、Tree of Thoughts、Graph of Thoughts、ReAct。自动化 :APE、DSPy、LLMLingua 压缩。实战模板 :结构化输出、代码生成、数据抽取、多轮对话。评估与调试 :指标选择、A/B 测试、错误归因、调试工具箱。前置要求 :会用 Python,调用过任何一家的 LLM API。不需要数学背景。
为什么提示词工程值得花时间 2020 年 GPT-3 刚出来的时候,研究者很快发现了一个反常识的现象:同一个模型,问法不同,结果天差地别。一个写得不好的提示词只能换来胡言乱语,一个精心设计的提示词却能解决复杂的推理任务。这不是 bug,而是这类模型学习语言的方式决定的。
传统编程是确定性的:写函数、定义入参出参,机器照做。语言模型不是这么工作的。它本质上是在根据万亿级 token 训练出的模式预测"下一个最有可能的词"。你的提示词不是在给模型下命令,而是在为它构造一个上下文,把它的概率分布往你想要的方向"推"一下。
把它想成米其林餐厅的菜单:如果你只写一句"给我做点好吃的",厨师只能凭直觉发挥;写清楚"清蒸鲈鱼,少盐,配芦笋",他才知道怎么端盘。提示词工程的本质,就是写出让模型不需要猜的菜单。
这件事的回报很高。一个写得好的提示词,可以通过更紧凑的上下文把 API 成本降到原来的十分之一;可以把一个复杂推理任务的准确率从 40% 拉到 90%。对于每月百万级请求的生产系统,这是真金白银。
一个生产级提示词的解剖
几乎所有上线的提示词都能拆成同样的五块:角色(role) 、上下文(context) 、指令(instruction) 、示例(examples) 、输出格式(format) 。把它当骨架,每个任务换"血肉",骨架不动。这种统一结构会让评估、缓存、版本控制都变得容易。
基础技巧 零样本提示 零样本就是不给任何示例,直接让模型做事。完全靠它在预训练里见过的世界来理解你想干什么。
1
2
3
4
prompt = """请判断这条评论的情感:
"这电影又无聊又老套,不推荐。"
情感:"""
模型没看到任何情感分类的示例,但它能从"无聊"“不推荐"这些词里推断出是负面。
适合零样本的场景 :模型在预训练阶段大量见过的简单任务(翻译、摘要、常识问答);约定俗成的标签体系(情感的"正/负/中”);快速验证一个想法。
不适合的场景 :领域术语多的任务(医学、法律),指令本身就模糊,要求严格输出格式的任务。
GPT-3 论文里有个对照数据:自然语言推理任务上,零样本 59%,少样本 70%。差距就是没给示例的代价。
三个几乎总是有效的优化点 :
任务说清楚 。不要写"看看这条评论",要写"把这条评论分类为正面/负面/中性"。格式钉死 。加一句"只回复一个词:positive、negative 或 neutral",模型就不会啰嗦。加约束 。一句"忽略反讽,只看字面情感"能避开最常见的翻车点。一个能直接用的模板:
1
2
3
4
5
6
7
def zero_shot_classify ( text : str , labels : list [ str ]) -> str :
return f """任务:把下面的文本分类到这些类别中的一个:
{ ', ' . join ( labels ) }
文本: { text }
类别(只回复类别名称):"""
少样本提示 少样本就是在问问题之前,先给 2 到 10 个例子。准确率会肉眼可见地上升,因为模型看到了它该模仿的"模式"。
1
2
3
4
5
6
7
8
9
10
11
12
13
prompt = """判断下列评论的情感:
评论:"简直爱了!今年看过最好的电影。"
情感:positive
评论:"还行吧,没什么特别但也不差。"
情感:neutral
评论:"纯粹浪费时间。演技稀烂。"
情感:negative
评论:"摄影非常出色,剧情也抓人。"
情感:"""
这几个示例同时做了三件事:钉死了输出格式(一个小写单词),覆盖了边界情况(褒贬混杂归为 neutral),用相关的语义模式给模型"开了一道门"。
心智模型 :少样本示例是一种软约束 。模型的下一 token 预测会去匹配示例里的输入-输出模式,再套到新的输入上。本质上是在用示范来"编程"。
怎么挑示例 。Liu et al. (2021) 的实证研究有几个结论很有用:
多样性比数量重要 。五个差异大的示例比二十个相似的更有用。难例值得加 。模型最容易翻车的边界情况,恰恰最该出现在示例里。顺序有影响 。和当前问题最相关的那个示例,放在最靠近问题的位置。一个把"相关性"和"多样性"结合起来的简单选取器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from sklearn.metrics.pairwise import cosine_similarity
def select_examples ( query : str , pool : list [ dict ], k : int = 5 ) -> list [ dict ]:
"""挑 k 个例子:既要和问题相关,又要彼此差异大。"""
q_emb = get_embedding ( query )
embs = [ get_embedding ( ex [ "text" ]) for ex in pool ]
sims = cosine_similarity ([ q_emb ], embs )[ 0 ]
candidates = sims . argsort ()[ - k * 2 :][:: - 1 ] # 先取 2k 个最相关的
selected = [ candidates [ 0 ]]
for idx in candidates [ 1 :]:
if len ( selected ) >= k :
break
if all ( cosine_similarity ([ embs [ idx ]], [ embs [ s ]])[ 0 ][ 0 ] < 0.9
for s in selected ):
selected . append ( idx )
return [ pool [ i ] for i in selected ]
SuperGLUE 上 GPT-3 的对照:零样本 69.5%,单样本 71.8%,32 样本 75.2%。大多数任务在 10–15 个示例之后就遇到边际递减了。
多样本提示 Anthropic 在 2024 年的工作显示,当上下文窗口够大(10 万 token 以上),可以塞进几百上千个示例。这种 many-shot 几乎能逼近微调的效果,又不需要训练流水线。
典型场景:你想做一个能识别公司内部反模式的代码评审器。微调要数据流水线、要长期维护,太重;不如直接把 200 条评审示例放进上下文里:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
prompt = """请检查以下代码的反模式。
[示例 1]
代码:def get_data(): return db.query('SELECT * FROM users')
问题:原生 SQL,注入风险。改用参数化查询。
严重程度:高
[示例 2]
代码:result = api.call(); result.field
问题:没有错误处理。包一层 try/except。
严重程度:中
…… 还有 198 个示例 ……
现在请评审这段代码:
{user_code}
问题:"""
为什么有效 。几百个示例足以让模型隐式地学到任务相关的分布——和微调相似,但不更新参数。示例越多,越能覆盖边角情况,模糊的地方越少。格式一致性也几乎完美,因为模型已经看过几十次同样的模板。
Anthropic 的发现:500-shot 在专门任务上可以逼近微调;200–300 个示例之后增益开始平台;最适合 Claude 的 200K 窗口。
代价 。20 万 token 的提示词按 GPT-4 标价大概 2 美元一次,延迟也会显著上升。解药是 prompt caching (Claude、GPT-4 都支持):静态前缀缓存一次,后面的请求按折扣价复用。
任务分解 复杂任务失败的常见原因,是你试图让模型一口吃成胖子。把它拆开,每一步都简单到能验证:
不要写"把这份合同的所有义务条款抽出来",而要:
1
2
3
4
5
6
7
8
9
10
11
12
sections = extract_sections ( contract ) # 1) 拆章节
obligations = [] # 2) 逐节抽义务
for section in sections :
prompt = f """法律文本: { section }
列出所有义务,按以下格式:
[当事方] 必须 [行为] [条件]"""
obligations . extend ( llm_call ( prompt ))
classified = classify_obligations ( obligations ) # 3) 按类型分类
summary = summarize_obligations ( classified ) # 4) 汇总
为什么有用 。每个子任务都简单,每一步的成功标准都清晰,中间结果可以直接看,出错时一眼就知道是哪一环坏了。GitHub Copilot Workspace 用的就是这套思路:理解代码库 → 找出受影响文件 → 生成单文件修改 → 合成完整方案,每一环都有自己的提示词。
三个值得放进工具箱的范式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def etl_pattern ( data , extract_p , transform_p ):
extracted = llm_call ( extract_p . format ( data = data ))
return llm_call ( transform_p . format ( data = extracted ))
def map_reduce ( items , map_p , reduce_p ):
mapped = [ llm_call ( map_p . format ( item = i )) for i in items ]
return llm_call ( reduce_p . format ( items = mapped ))
def validate_retry ( prompt , validator , max_retries = 3 ):
for _ in range ( max_retries ):
result = llm_call ( prompt )
if validator ( result ):
return result
prompt += f " \n\n 上一次结果不合法: { result } \n 再试一次:"
raise ValueError ( "超过最大重试次数" )
三种范式的横向对比
同一道算术题三种问法。零样本最便宜,但在多步推理上最弱;少样本花约 8 倍 token 换来约 16 个百分点的提升;CoT 用比少样本还少一点的 token,把 GSM8K 量级问题的准确率拉到 78.7%(Wei et al., 2022;Kojima et al., 2022)。
进阶推理技巧 Chain-of-Thought(CoT) CoT 让模型把思考过程写出来 :先生成中间推理步骤,再给最终答案。这一个改动就能在数学、逻辑、多步推理上带来巨大提升。
Wei et al. (2022) 发现,仅仅加上一句"让我们一步步思考",GSM8K 上的准确率就从 17.1% 跳到 78.2%。
不加 CoT:
1
2
3
4
prompt = """问:罗杰有 5 个网球。他又买了 2 罐网球,每罐 3 个。
他现在有多少个网球?
答:"""
# 经常输出:"10"(错)
加上 CoT:
1
2
3
4
5
6
7
8
prompt = """问:罗杰有 5 个网球。他又买了 2 罐网球,每罐 3 个。
他现在有多少个网球?
答:让我们一步步想。"""
# 输出:
# 罗杰一开始有 5 个。
# 又买了 2 罐,每罐 3 个 = 6 个。
# 总共 5 + 6 = 11 个。
# 答案:11(对了)
为什么 CoT 有效 。可解释性研究里有个比较站得住脚的解释:模型在单次前向传播里能做的"隐式计算"是有上限的,每一层都在精修表示,但不能凭空多算。生成中间 token 等于让模型多走了几次前向——每个生成的 token 都是一次新的前向——把计算量摊开。把它想成模型的"工作记忆"。
三种值得记住的变体 。
Zero-shot CoT :什么例子都不给,加一句"让我们一步步想"。意外地有效:
1
2
def zero_shot_cot ( question : str ) -> str :
return f " { question } \n\n 让我们一步步想。"
Few-shot CoT :示例里也带上推理过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
prompt = """问:5 台机器 5 分钟做 5 个零件,100 台机器做 100 个零件需要多久?
答:让我们一步步想。
- 5 台机器 5 分钟做 5 个零件
- 那么 1 台机器 5 分钟做 1 个零件
- 100 台机器 5 分钟做 100 个零件
答案:5 分钟
问:农场主有 15 只羊,除了 8 只都死了,还剩几只?
答:让我们一步步想。
- "除了 8 只都死了"意思是有 8 只活下来
- 所以剩下 8 只
答案:8
问: {new_question}
答:让我们一步步想。"""
结构化 CoT :强制一个固定的推理模板:
1. 已知:……
2. 求:……
3. 用到的原理:……
4. 分步求解:……
5. 最终答案:……
Wei et al. (2022) 的对照实验 :
数据集 Baseline CoT 提升 GSM8K(数学) 17.1% 78.2% +61pp SVAMP(数学) 63.7% 79.0% +15pp CommonsenseQA 72.5% 78.1% +6pp StrategyQA 54.3% 66.1% +12pp
CoT 适合 :多步推理、需要中间计算的题、需要解释推理路径的场景、有 trade-off 的决策。
CoT 不适合 :简单查询(“法国首都?")、模型本身缺乏知识的题(CoT 无中生不了有)、要求短答案的任务(推理 token 全是浪费)。
一个可复用的 CoT 引擎:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ChainOfThoughtEngine :
def __init__ ( self , model , temperature : float = 0.7 ):
self . model = model
self . temperature = temperature
def solve ( self , question : str , mode : str = "zero-shot" ):
if mode == "zero-shot" :
prompt = f " { question } \n\n 让我们一步步想。"
elif mode == "few-shot" :
prompt = self . _build_few_shot_prompt ( question )
else :
raise ValueError ( f "未知模式: { mode } " )
response = self . model . generate (
prompt , temperature = self . temperature , max_tokens = 512 ,
)
return self . _extract_answer ( response ), response
def _extract_answer ( self , response : str ) -> str :
for marker in ( "答案:" , "Answer:" , "因此," , "所以" ):
if marker in response :
tail = response . split ( marker )[ - 1 ] . strip ()
return tail . split ( "。" )[ 0 ] . split ( " \n " )[ 0 ] . strip ()
return response . strip () . split ( " \n " )[ - 1 ]
Self-Consistency:用多数投票降方差 单条 CoT 链可能走偏。Self-Consistency(Wang et al., 2022)的思路朴素得有点漂亮:多采样几条推理路径,对最终答案做多数投票 。
直觉上完全是贝叶斯的:如果 10 条不同的推理路径里有 7 条得到同一个答案,这个答案对的概率就远高于任何一条单独路径。
1
2
3
4
5
6
7
8
9
10
11
12
13
from collections import Counter
def self_consistency ( question : str , n : int = 5 ,
temperature : float = 0.7 ) -> tuple [ str , float ]:
"""采样 n 条推理链,对答案多数投票。"""
prompt = f " { question } \n\n 让我们一步步想。"
answers = []
for _ in range ( n ):
response = llm_call ( prompt , temperature = temperature , max_tokens = 512 )
answers . append ( extract_answer ( response ))
most_common , count = Counter ( answers ) . most_common ( 1 )[ 0 ]
return most_common , count / n
一个真实的小例子:
问:"如果你超过了第二名,你现在第几?"
链 A:"超过第二,所以你是第二。" -> 第二 ✓
链 B:"你原来在第二之后,超过后是第一。" -> 第一 ✗
链 C:"超过第二就占据他的位置。" -> 第二 ✓
链 D:"你超过了第二的人,你现在就是第二。" -> 第二 ✓
链 E:"超过第二,你变成第一。" -> 第一 ✗
多数票:第二 (3/5 = 0.6 置信度)
Wang et al. (2022) 的数字:GSM8K 上单次 CoT 74.4%,Self-Consistency(n=40)83.7%;CommonsenseQA 78.1% → 81.5%。在硬任务上完全值回票价。
成本 。Self-Consistency 把推理成本乘了 n 倍,按任务重要性挑 n:
任务级别 推荐 n 成本 探索性 3 3x 生产 5 5x 高风险 10–20 10–20x
更聪明的"自适应"版本:先用低 n,置信度低再加倍:
1
2
3
4
5
def adaptive_self_consistency ( question : str , threshold : float = 0.7 ):
answer , conf = self_consistency ( question , n = 3 )
if conf >= threshold :
return answer , conf
return self_consistency ( question , n = 10 )
更进一步的加权投票 会让模型同时给每条链的逻辑质量打分,再按质量加权——多花一次推理,但能自动给摇摆的推理打折。
Tree of Thoughts(ToT):把推理变成搜索 ToT(Yao et al., 2023)把 CoT 推到下一层:把推理建模为状态空间搜索 。不再走单链,而是展开成一棵树,给每个分支打分,遇到死胡同就回溯。
一个简化的深度优先实现:
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 tree_of_thoughts ( problem : str , depth : int = 3 , breadth : int = 3 ):
def explore ( state : str , d : int ) -> int :
if d >= depth :
return evaluate_solution ( state )
thoughts = generate_thoughts ( state , k = breadth )
scored = [( t , explore ( state + " \n " + t , d + 1 )) for t in thoughts ]
return max ( scored , key = lambda x : x [ 1 ])[ 1 ]
return explore ( problem , 0 )
def generate_thoughts ( state : str , k : int = 3 ) -> list [ str ]:
prompt = f """当前推理:
{ state }
请生成 { k } 个可能的下一步:
1."""
response = llm_call ( prompt , temperature = 0.8 , max_tokens = 300 )
return [ t . strip () for t in response . split ( " \n " ) if t . strip ()][: k ]
def evaluate_solution ( state : str ) -> int :
prompt = f """按 1-10 给这段推理打分(逻辑连贯性、向解的进展、正确可能性):
{ state }
分数(1-10 的整数):"""
return int ( llm_call ( prompt , temperature = 0 , max_tokens = 5 ))
24 点示例 。用 4、9、10、13 加减乘除得到 24:
根:{4, 9, 10, 13}
├─ 13 - 9 = 4 -> {4, 4, 10} v=8 (保留)
│ └─ 10 - 4 = 6 -> {4, 6} v=9
│ └─ 6 * 4 = 24 解出
├─ 10 - 4 = 6 -> {6, 9, 13} v=5 (待探索)
└─ 9 + 10 = 19 -> {4, 13, 19} v=2 (剪枝)
Yao et al. (2023) 的对照:
任务 CoT ToT Δ 24 点 7.3% 74% +66pp 创意写作 7.3 7.9 +0.6 填字游戏 15.6% 78% +62pp
代价 。breadth=3、depth=4 一道题就要 ~80 次 LLM 调用。ToT 真正划算的前提有两个:解空间里确实存在多条可行路径,模型能可靠地给中间状态打分。组合优化、规划、约束满足类问题适合用;普通问答别上。
生产里推荐用带优先队列的最佳优先搜索,配一个硬性的调用次数上限,单题不能炸预算。
Graph of Thoughts(GoT):从树到 DAG GoT(Besta et al., 2023)把 ToT 推广成任意有向无环图。思想可以合并 (多分支汇总成一个),也可以迭代 (一个思想跨轮次反复打磨),表达能力比树严格强。
最经典的例子是多文档摘要:
文档 -> 单文档摘要 ┐
单文档摘要 ┼-> 主题合并 -> 最终综述
单文档摘要 ┘
每个单文档摘要互相独立,可以并行。合并步把它们汇总。这是图,不是树。
1
2
3
4
5
def graph_of_thoughts_summarize ( documents : list [ str ]) -> str :
summaries = [ llm_call ( f "摘要: \n { d } " ) for d in documents ]
themes = [ llm_call ( f "提取主题: \n { s } " ) for s in summaries ]
merged = llm_call ( "合并以下主题: \n " + " \n\n " . join ( themes ))
return llm_call ( f "基于以下内容写最终综述: \n { merged } " )
在 32 个数字的排序任务上,Besta et al. 报告 GoT 准确率 89%,成本比 ToT 低 62%——合并节点把重复探索砍掉了。
ReAct:边想边做 ReAct(Yao et al., 2022)把推理 和行动 交错起来。模型在思考和工具调用之间反复切换,每一步行动的结果都被观察并喂回去。
Thought: 先要知道巴黎人口。
Action: search("Paris population")
Observation: 216 万 (2019)
Thought: 再查东京人口。
Action: search("Tokyo population")
Observation: 3740 万 (2021)
Thought: 东京更大。
Action: finish("东京人口比巴黎多。")
ReAct 解决了纯语言模型的三个老问题:训练数据有截止时间(无法回答最新信息)、不擅长精确计算、不能访问私有数据。一个最小的 agent:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ReActAgent :
def __init__ ( self , model , tools : dict , max_steps : int = 10 ):
self . model = model
self . tools = tools
self . max_steps = max_steps
def run ( self , task : str ) -> str :
trajectory = [ f "Task: { task } " ]
for _ in range ( self . max_steps ):
response = self . model . generate (
self . _build_prompt ( trajectory ), temperature = 0 ,
)
thought , action , action_input = self . _parse ( response )
trajectory . append ( f "Thought: { thought } " )
trajectory . append ( f "Action: { action } [ { action_input } ]" )
if action == "finish" :
return action_input
tool = self . tools . get ( action )
obs = tool ( action_input ) if tool else f "未知动作: { action } "
trajectory . append ( f "Observation: { obs } " )
return "步数耗尽,未完成。"
HotpotQA(多跳问答)上:标准提示 28.7% → CoT 32.9% → ReAct 37.4%。AlfWorld(交互环境)上:12% → 34%。
实战要点 :
工具描述要写好 。模型靠 docstring 选工具。观察结果要截断 。搜索引擎可能返回几千字,会撑爆上下文。强制步数上限 。防止死循环。错误信息要描述性 。让模型有机会自我纠正,而不是一崩到底。提示词默认是脆弱的
同一个模型、同一组示例、同一个任务。仅仅改变示例的格式 (Q:/A: 还是 Input/Output)或者它们的顺序 ,准确率可能波动 20+ 个百分点(Lu et al., 2022;Sclar et al., 2024)。这就是为什么经验性评估不能省。在宣布"找到最优提示词"之前,至少在多种顺序下都跑一遍。
自动化与工程化 手工调提示词不可持续。下面这些方法把这件事工程化。
Automatic Prompt Engineering(APE) APE(Zhou et al., 2022)把"找最优提示词"当成一个搜索问题:
生成 :让 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
def automatic_prompt_engineering ( task_description , train_examples ,
val_examples , num_candidates = 20 ):
meta_prompt = f """任务: { task_description }
示例:
{ format_examples ( train_examples [: 5 ]) }
请生成 { num_candidates } 个不同的提示词,用不同的措辞和角度。
提示词:"""
candidates = parse_prompts (
llm_call ( meta_prompt , temperature = 1.0 , max_tokens = 2000 )
)
results = []
for prompt in candidates :
correct = sum (
normalize ( llm_call ( f " { prompt } \n\n 输入: { x } \n 输出:" ,
temperature = 0 )) == normalize ( y )
for x , y in val_examples
)
results . append (( prompt , correct / len ( val_examples )))
return max ( results , key = lambda x : x [ 1 ])
Zhou et al. 发现 APE 找到的提示词常常比人写的高 3–8 个百分点。原因不复杂:APE 探索人类不会想到的措辞,直接在你的数据上优化,而且能轻松测试上百个候选。
迭代版 会把当前最优作为基准再生成变体,相当于在提示词空间里做爬山。
DSPy:把提示词当代码写 DSPy(Khattab et al., 2023)把提示词工程当成编程问题。你不再手写提示词字符串,而是写一个生成提示词的程序 ,由编译器自动调优。
三个核心抽象:
Signature :带类型标注的输入输出签名。Module :可组合的提示词模板。Optimizer :自动选择 demonstration 和 instruction 的优化器。一个情感分类器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import dspy
class SentimentSignature ( dspy . Signature ):
"""对文本做情感分类。"""
text = dspy . InputField ()
sentiment = dspy . OutputField ( desc = "positive、negative 或 neutral" )
class SentimentClassifier ( dspy . Module ):
def __init__ ( self ):
super () . __init__ ()
self . predictor = dspy . Predict ( SentimentSignature )
def forward ( self , text : str ):
return self . predictor ( text = text )
dspy . settings . configure ( lm = dspy . OpenAI ( model = "gpt-3.5-turbo" ))
classifier = SentimentClassifier ()
print ( classifier ( "这部电影简直太棒了!" ) . sentiment ) # positive
DSPy 能从训练集自动 bootstrap 出 demonstration:
1
2
3
4
5
6
7
8
9
10
11
from dspy.teleprompt import BootstrapFewShot
train_data = [
dspy . Example ( text = "产品很赞!" , sentiment = "positive" ),
dspy . Example ( text = "服务太差。" , sentiment = "negative" ),
# ...
]
optimized = BootstrapFewShot ( metric = exact_match ) . compile (
SentimentClassifier (), trainset = train_data ,
)
多阶段流水线也很自然:
1
2
3
4
5
6
7
8
9
10
11
12
class MultiHopQA ( dspy . Module ):
"""需要多步推理的问答。"""
def __init__ ( self ):
super () . __init__ ()
self . retrieve = dspy . Retrieve ( k = 3 )
self . gen_query = dspy . ChainOfThought ( "question -> search_query" )
self . answer = dspy . ChainOfThought ( "context, question -> answer" )
def forward ( self , question : str ):
q = self . gen_query ( question = question ) . search_query
ctx = self . retrieve ( q ) . passages
return self . answer ( context = ctx , question = question ) . answer
DSPy 编译器会同时优化三个子提示词。代价也是真实的:有学习曲线,对最终措辞的控制力会下降,前期需要一笔编译预算。当你已经有稳定的训练集和靠谱的评估指标时,DSPy 是最划算的选择。
LLMLingua:给提示词"瘦身” LLMLingua(Jiang et al., 2023)通过删除信息量低的 token 来压缩提示词。一个小模型给每个 token 打一个"重要性分",分数低的删掉。
1
2
3
4
5
6
7
8
from llmlingua import PromptCompressor
compressor = PromptCompressor ()
compressed = compressor . compress (
original_prompt ,
rate = 0.5 , # 目标压缩率 50%
target_token = 200 , # 或者直接给一个 token 预算
)
底层用的是条件困惑度:删掉某个 token,看下一个 token 的预测困惑度增加多少;增加越多,说明这个 token 越重要,越该保留。
实测影响:
问答任务 2x 压缩:准确率掉 2–3%,成本省 50%,延迟降 1.4x。 RAG 任务 4x 压缩:准确率掉 5–7%,成本省 75%。 适合 :长上下文场景(RAG、文档分析),且能接受一点准确率损失。慎用 :法律、医疗等每个字都可能有责任的场景。
一个自适应 压缩器的草图,按重要性给不同段分配预算:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AdaptiveCompressor :
def __init__ ( self , base ): self . base = base
def compress ( self , prompt : str , budget : int ) -> str :
sections = split_sections ( prompt )
budgets = {
"instruction" : int ( budget * 0.3 ), # 永远不饿死
"examples" : int ( budget * 0.4 ),
"context" : budget - int ( budget * 0.7 ), # 压得最狠
}
return merge ({
k : self . base . compress ( sections [ k ], target_token = b )
for k , b in budgets . items ()
})
实战模板
这张图展示了同一套五块骨架在六类常见任务上的特化。复用骨架的好处不仅仅是看起来整齐——它让评估、缓存、版本控制都变得容易。
结构化输出 让 LLM 稳定输出合法 JSON 是一件出名的麻烦事。三种策略,鲁棒性递增:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def generate_structured ( data : str , schema : dict ) -> dict :
"""Schema 优先 + 失败时带反馈重试。"""
prompt = f """根据下面的 schema 生成一个 JSON 对象:
{ json . dumps ( schema , indent = 2 ) }
要求:
- 必填字段不能漏
- 类型要对
- 枚举字段必须用允许的值
- 不要加多余字段
输入数据:
{ data }
只输出合法 JSON:"""
response = llm_call ( prompt , temperature = 0 )
try :
parsed = json . loads ( response )
validate_against_schema ( parsed , schema )
return parsed
except Exception as e :
return retry_with_feedback ( prompt , response , str ( e ))
策略 2 是给几个合法 JSON 示例做 few-shot——简单 schema 下足够。策略 3 是用 OpenAI/Anthropic 的原生 function/tool calling,由 API 保证 JSON 合法,是最稳的。
代码生成 + 自检 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def generate_code ( task : str , language : str = "python" , tests = None ) -> str :
prompt = f """请用 { language } 完成下面的任务。
任务: { task }
要求:
- 注释清晰
- 处理边界情况
- 遵循 { language } 最佳实践
{ format_test_cases ( tests ) if tests else '' }
提供完整可运行的代码:
``` { language }
"""
code = extract_code_block ( llm_call ( prompt , temperature = 0.3 ))
if tests :
results = run_tests ( code , tests , language )
if not all ( r . passed for r in results ):
code = debug_and_fix ( code , results , prompt )
return code
模式是 生成 → 测试 → 修复 。修复 prompt 把失败的测试反馈给模型,原始指令保持不变。
多轮对话管理 长对话会撑爆上下文。常见做法:保留最近若干轮的滑动窗口,老对话用 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
29
30
31
32
33
34
35
36
class ConversationManager :
def __init__ ( self , model , system_prompt : str , max_tokens : int = 4000 ):
self . model = model
self . system_prompt = system_prompt
self . max_tokens = max_tokens
self . history : list [ dict ] = []
def chat ( self , user_message : str ) -> str :
self . history . append ({ "role" : "user" , "content" : user_message })
context = self . _get_context ()
response = self . model . generate ( self . _build_prompt ( context ))
self . history . append ({ "role" : "assistant" , "content" : response })
return response
def _get_context ( self ) -> list [ dict ]:
ctx = [{ "role" : "system" , "content" : self . system_prompt }]
used = count_tokens ( self . system_prompt )
for msg in reversed ( self . history ):
t = count_tokens ( msg [ "content" ])
if used + t > self . max_tokens :
break
ctx . insert ( 1 , msg )
used += t
return ctx
def summarize_old_turns ( self ) -> None :
if len ( self . history ) < 10 :
return
old = self . history [: 6 ]
summary = self . model . generate (
f "简明地总结以下对话: \n { format_messages ( old ) } \n\n 总结:"
)
self . history = (
[{ "role" : "system" , "content" : f "早期对话摘要: { summary } " }]
+ self . history [ 6 :]
)
评估与调试 提示词工程是经验科学,没有指标就只能凭感觉。
几种指标,按成本排序 :
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 exact_match ( pred , truth ):
return normalize ( pred ) == normalize ( truth )
def f1 ( pred , truth ):
p = set ( normalize ( pred ) . split ())
t = set ( normalize ( truth ) . split ())
if not p or not t :
return float ( p == t )
common = p & t
if not common :
return 0.0
prec = len ( common ) / len ( p )
rec = len ( common ) / len ( t )
return 2 * prec * rec / ( prec + rec )
def semantic_similarity ( pred , truth , model = "all-MiniLM-L6-v2" ):
from sentence_transformers import SentenceTransformer , util
m = SentenceTransformer ( model )
return util . cos_sim ( m . encode ( pred ), m . encode ( truth )) . item ()
def llm_as_judge ( pred , truth , criteria ):
return parse_score ( llm_call ( f """请评估以下输出。
任务: { criteria [ 'task' ] }
期望: { truth }
实际: { pred }
请给 0-10 分并简要说明:""" ))
挑能反映你真正在意的东西的最便宜的指标。
系统化的 A/B 测试 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class PromptExperiment :
def __init__ ( self , test_set , metrics ):
self . test_set = test_set
self . metrics = metrics
def evaluate ( self , prompt_fn ):
return {
m . __name__ : sum (
m ( llm_call ( prompt_fn ( x ), temperature = 0 ), y )
for x , y in self . test_set
) / len ( self . test_set )
for m in self . metrics
}
def compare ( self , variants : dict ):
import pandas as pd
rows = [{ "prompt_name" : name , ** self . evaluate ( fn )}
for name , fn in variants . items ()]
df = pd . DataFrame ( rows )
return df . sort_values ( by = df . columns [ 1 ], ascending = False )
测试集一定要是提示词作者没看过的数据。
提示词调试清单 现象 可能原因 解法 输出含糊、跑题 指令模糊 加具体约束和示例 某条要求被忽略 指令互相矛盾 解决冲突,明确优先级 即使尝试也答错 缺少上下文 提供事实或检索段落 输出格式不对 没指定格式 给 schema 和示例 每次答案不一样 任务对单次推理太复杂 拆步骤
一个能扫出常见问题的小检查器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PromptDebugger :
AMBIGUOUS = { "相关" , "合适" , "好" , "差" , "一些" , "若干" , "大量" ,
"stuff" , "things" }
CONFLICT = { "但" , "然而" , "尽管" , "除了" }
def check_ambiguity ( self , prompt : str ) -> bool :
return any ( w in prompt for w in self . AMBIGUOUS )
def check_conflicts ( self , prompt : str ) -> bool :
return sum ( w in prompt for w in self . CONFLICT ) >= 2
def infer_format ( self , text : str ) -> str :
t = text . strip ()
if t . startswith ( "{" ) and t . endswith ( "}" ): return "json_object"
if t . startswith ( "[" ) and t . endswith ( "]" ): return "json_array"
if " \n -" in t or " \n *" in t or " \n 1." in t : return "list"
return "prose"
错误归因 把失败案例分桶。下面这套类别能覆盖大多数错误模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
def categorize_error ( pred : str , truth : str ) -> str :
p , t = normalize ( pred ), normalize ( truth )
if not p :
return "empty_output"
if p in t or t in p :
return "partial_match"
p_w , t_w = set ( p . split ()), set ( t . split ())
overlap = len ( p_w & t_w ) / max ( len ( p_w ), len ( t_w ))
if overlap > 0.5 :
return "semantic_error"
if overlap > 0 :
return "partial_hallucination"
return "complete_hallucination"
然后从最大的桶开始改。
常见坑与解法 坑 解法 指令模糊 列出要优化的具体维度:清晰度、长度、语气、格式。 假设模型知道你脑子里的东西 把代码、文档、数据补到提示词里。 提示词太长 拆段、摘要,或用 RAG。“lost in the middle"是真实的。 不指定输出格式 显式给 schema、单位、语言,并配示例。 没有验证 包一层"验证 → 重试 → 失败"循环。
常见问题 温度参数怎么选? 0:分类、抽取、数学、代码——所有要求一致性的场景。0.7–0.8:写作、头脑风暴、营销文案。1.0+ 几乎用不上。结构化任务默认 0,创意任务默认 0.7。
少样本应该给几个例子? 简单任务 2–3 个;多数任务 5–7 个最合适;10+ 只有在示例足够多样时才有用。超过 50 就该考虑微调了。
什么时候该微调? 已经有 1000+ 高质量标注数据、任务高度专业化、延迟/成本是关键约束、提示词工程已经触顶。其他情况都先用提示词,因为迭代速度快一千倍。
怎么减少幻觉? 用检索的上下文 grounding,明确告诉模型"不知道就说不知道”,要求带引用,把温度调到 0,加一道验证。
长文档怎么处理? chunk + map-reduce、检索增强生成、分层摘要,或者直接用大上下文模型(Claude 3 200K,Gemini 1.5 1M)。RAG 通常是默认正确答案。
提示词能在不同模型间复用吗? 通用技巧(清晰指令、少样本、格式说明、CoT)能复用;具体措辞、格式偏好(Claude 偏爱 XML)、tool calling 语法不能。务必在目标模型上重新测。
能自动优化吗? 能。APE、DSPy、遗传搜索都可以。先手工调到能用,再自动化。
XML、JSON、纯文本怎么选? 简单提示词用纯文本;结构化输入输出用 JSON;复杂多块提示词用 XML(特别是 Claude)。三个都行,取决于下游解析器。
CoT、ToT、GoT 什么时候用哪个? 技术 结构 适合 成本 CoT 线性链 多步推理、数学、逻辑 1–2x ToT 树形搜索 多解空间、规划、谜题 5–50x GoT 任意 DAG 并行处理、合并洞察 视情况而定
未来方向? 多模态提示词、自动化更紧(DSPy、APE)、激进压缩、meta-prompting(用提示词生成提示词)、具身 agent。这门技能不会消失,只会从手工措辞往优化目标设计、评估框架搭建、系统编排 迁移。
收束 提示词工程从最初的试错,已经成长为一个有研究支撑、有可复用工具的工程学科。基础——清晰的指令、合适的示例、稳定的输出格式——放之四海而皆准。Chain-of-Thought、Tree of Thoughts 这类进阶技术,把"裸跑"做不到的能力解锁出来。APE、DSPy 这类框架把这些实践搬上生产。
但仅有技术不够。真正有效的提示词工程需要四样东西:
经验主义 :什么都要测。在某个模型某个任务上有效的方法,换一个未必有效。迭代 :第一版几乎不可能是最好的版本。基于真实失败修。评估 :没有指标就只是在猜。背景判断 :理解模型擅长什么、任务要什么、在成本/延迟/质量之间怎么取舍。简单开始。持续度量。反复迭代。最好的提示词,不是最巧妙的,而是能稳定解决你问题的那一个。
参考文献 Brown et al., 2020. Language Models are Few-Shot Learners. NeurIPS. Wei et al., 2022. Chain-of-Thought Prompting Elicits Reasoning in Large Language Models. NeurIPS. Kojima et al., 2022. Large Language Models are Zero-Shot Reasoners. 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. Yao et al., 2022. ReAct: Synergizing Reasoning and Acting in Language Models. ICLR. 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. Liu et al., 2021. What Makes Good In-Context Examples for GPT-3? arXiv. Lu et al., 2022. Fantastically Ordered Prompts and Where to Find Them. ACL. Sclar et al., 2024. Quantifying Language Models’ Sensitivity to Spurious Features in Prompt Design. ICLR. Min et al., 2022. Rethinking the Role of Demonstrations: What Makes In-Context Learning Work? EMNLP.