外观
提示工程实践
提示工程不是"写一句漂亮的 prompt",而是围绕模型上下文窗口的一次受控实验设计:你设计输入分布,观察输出分布,并通过回归测试锁定改进。
提示(prompt)是 LLM 应用最廉价的"可编程接口"。本文回答三个问题:有哪些被验证过的模板模式?怎么系统性地调提示而不是靠手感?什么时候该停止调提示、转去微调或接 RAG? 全文假设你已理解提示工程的基础概念(角色、少样本、思维链)与推理基础的解码参数。
提示工程是"易用难精"
随便写几句就能让模型干活,但稳定地让模型按你要求的方式输出,是工程。本页的核心纪律:没有回归测试的提示修改,等于没有提交信息的代码修改——都无法回滚、无法归因。
一、提示工程在整个系统中的位置
一个典型 LLM 应用的失败,约 80% 出在"提示之外"(数据、上下文、评估、路由),但 80% 的人第一反应是"再改改提示"。先把决策顺序定下来:
问题出现
│
▼
① 提示设计/修复(成本最低,先做)──── 本文主题
▼
② 上下文供给(检索、工具、多轮记忆)── RAG / Agent
▼
③ 模型升级(更强的基座)──────────── 模型选型
▼
④ 微调(风格/格式/领域适配)──────── 微调实战每一级都比上一级贵一个数量级。提示能解决的,绝不动模型——这条原则后面还会在决策树里展开。
二、提示模板设计模式
以下是经过公开实践验证的七种模式。任何模式都可能有效或无效,关键是用评测而不是感觉来判断(方法见第四节)。
| 模式 | 一句话描述 | 适用场景 | 示例骨架 |
|---|---|---|---|
| 角色扮演 | 给模型一个身份与立场 | 风格统一、专业语气 | 你是一位资深的 X 专家… |
| 指令 + 约束 | 明确任务 + 边界/格式 | 绝大多数任务 | 请做 X,不要做 Y,输出… |
| 少样本示例(few-shot) | 给 2~5 个输入-输出示例 | 需要模仿格式/口径 | 输入:…\n输出:… |
| 思维链(CoT) | 要求先推理再作答 | 数学/逻辑/多步任务 | 请一步步思考 |
| 思维树(ToT) | 显式探索多条路径并自评 | 复杂规划、搜索型任务 | 对分支进行打分与回溯 |
| 自我反思 | 让模型检查并修正自己的输出 | 问答、摘要、代码审查 | 检查以上回答,指出错误并改正 |
| 结构化输出模板 | 用模板/JSON 约束输出形状 | 程序化消费输出 | 以 JSON 输出:{"结论":…} |
1. 指令 + 约束:最基础也最容易被忽略
text
任务:把下面的用户反馈归类为「投诉」「建议」「咨询」「其他」,并输出一句理由。
约束:
- 只输出 JSON,不要任何多余文字
- 理由不超过 20 个字
- 拿不准时归类为「其他」
用户反馈:你们的 App 又闪退了,气死我了!约束的本质是缩小输出分布。观察发现:把"约束"从一句扩到四句(格式/长度/边界/兜底),稳定输出的比例显著提升——但约束要克制,过长的提示会稀释注意力(见第六节失败模式)。
2. 少样本示例:最强的"能力外挂"
少样本(in-context learning)能让模型在不更新参数的情况下模仿新格式、新口径。三个要点:
- 示例要覆盖边界情况:好示例 = 2 个常规 + 1 个边界 + 1 个反例。
- 示例与真实输入的格式严格一致:包括标点、换行、前缀。
- 顺序敏感:模型对最近示例的模仿倾向最强,难例放最后。
text
请把句子改写为更礼貌的版本。
句子:把报告给我。
改写:麻烦您把报告发我一份,谢谢。
句子:这个 bug 太低级了。
改写:这个 bug 有点意外,麻烦确认一下根因。
句子:你快点。
改写:3. 思维链(CoT):让模型"展示工作过程"
Let's think step by step 式提示对多步推理任务(GSM8K 数学题等)的提升已被广泛验证(参考资料)。工程化的 CoT 写法:
text
请回答下面的问题。在给出最终答案前,先写出你的推理步骤:
1. 列出已知条件
2. 逐步推导
3. 给出最终答案(用 ① 标记)两个工程细节:
- 把"推理"与"答案"分隔:让模型输出
推理:…\n答案:…,你就能在应用层只取答案、同时保留可审计的推理。 - self-consistency:同一个问题采样 N 次(温度调高),对答案做多数投票,可显著提升准确率(以 3~5 倍推理成本换约 10~20% 相对提升,数值因任务而异)。这是"推理时扩展"最简单的一种,见规模法则中的 test-time scaling 讨论。
思维链的边界
CoT 对"需要多步逻辑"的任务有效,对"模型本就不会"的任务(幻觉高发)无效——它不能凭空创造知识。知识问题请走检索(RAG 实战)。
4. 系统提示(system prompt)的设计准则
系统提示是"对话的宪法":它在每次请求都注入,且模型对 system 部分的遵循度通常高于 user 部分(多数模型对 system 的权重更高)。四条设计准则:
| 准则 | 做法 | 反例 |
|---|---|---|
| 定义角色与边界 | 一句话定位 + 明确"不做什么" | 只写"你是助手" |
| 关键指令前置 | 最重要的约束放开头 | 埋在第 8 条之后 |
| 少写规则、多给示例 | 格式用示例表达比用规则表达更稳 | 二十条"请务必……" |
| 固定版本、纳入版本管理 | system 是代码,走评审与回归 | 在聊天框里随手改 |
system prompt 是攻击面
系统提示里的内容(尤其权限、工具描述、内部信息)会被提示注入尝试套取。不要把密钥、内部系统细节写进 system prompt;对不可信输入保持隔离,见安全与风险。
三、结构化输出:让模型进入你的程序
LLM 的输出必须能被程序解析,这是生产落地的硬门槛。三条路线,从弱到强:
| 路线 | 做法 | 可靠性 | 实现成本 |
|---|---|---|---|
| 提示约束 + 手动解析 | 提示里规定 JSON 格式,代码里 json.loads + 兜底重试 | 低~中(约 90% 以内) | 低 |
| JSON mode | API 强制输出合法 JSON(不保证 schema) | 中(约 99% 合法 JSON) | 中 |
| 结构化输出 / 工具调用 | API 按给定 JSON Schema 生成,或声明 function 让模型填充参数 | 高(约 99% 符合 schema) | 中 |
1. 用 few-shot 引导 JSON 输出(不依赖平台特性)
text
把下面的邮件转为 JSON。字段:type(inquiry/complaint/other)、urgency(low/medium/high)、summary。
邮件:您好,请问我昨天提交的退款申请什么时候能到账?比较着急。
输出:{"type": "inquiry", "urgency": "high", "summary": "询问退款到账时间"}
邮件:你们的会员价格太贵了,隔壁家只要一半。
输出:2. JSON mode 与工具调用(以 OpenAI 风格 API 为例)
python
from openai import OpenAI
client = OpenAI() # 读取环境变量 OPENAI_API_KEY
# 方案 A:response_format 强制合法 JSON
resp = client.chat.completions.create(
model="gpt-4o-mini",
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": "提取信息,输出 JSON。"},
{"role": "user", "content": "邮件:您的订单 O-1024 已发货,预计 3 天送达。"},
],
)
print(resp.choices[0].message.content)
# {"order_id": "O-1024", "status": "shipped", "eta_days": 3}
# 方案 B:函数调用(function calling),让模型"填参数"而非"写格式"
tools = [{
"type": "function",
"function": {
"name": "extract_order_info",
"description": "从邮件中提取订单信息",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"status": {"type": "string", "enum": ["shipped", "pending", "cancelled"]},
},
"required": ["order_id", "status"],
},
},
}]
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "邮件:订单 O-1024 已发货。"}],
tools=tools,
tool_choice="auto",
)
print(resp.choices[0].message.tool_calls) # 结构化参数,程序直接消费别自己手写 JSON 解析硬抗
手写解析 + 重试在"演示项目"阶段可以,但生产中格式错误会引发连锁异常。JSON mode / function calling 是平台保证的接口,优先使用。开源自部署模型可借助 outlines、guidance、xgrammar 等约束解码库达到同等效果(见框架与工具选型)。
四、系统性调提示:把玄学变成实验
1. 五步方法论
调提示的最高效路径,不是"想到哪改到哪",而是:
① 定基线(baseline)── 先跑通一个最简版本,记录其指标
② 提假设(hypothesis)── 明确"我改 X 是为了提升 Y"
③ 单变量控制(isolate)── 一次只改一个变量
④ 跑评测(measure)── 在固定测试集上量化对比
⑤ 回归(regress)── 确认新提示不破坏已有能力每一步对应一个可复现的产物。特别是第 ④ 步:没有评测的提示优化是自我催眠。评测体系的搭建见评估实战。
2. 一个可复现的对照实验
python
import json, random
# 固定的测试集(golden set):结构 = [{"input": ..., "expected": ...}, ...]
with open("test_cases.json") as f:
test_cases = json.load(f)
def run_prompt(prompt_template: str, cases, sample_n=20) -> dict:
"""给定提示模板,在测试集子集上打分,返回正确率"""
random.seed(42)
sample = random.sample(cases, min(sample_n, len(cases)))
correct = 0
for case in sample:
prompt = prompt_template.replace("{{INPUT}}", case["input"])
output = call_model(prompt) # 你自己的调用封装
if normalize(output) == normalize(case["expected"]):
correct += 1
return {"accuracy": correct / len(sample), "n": len(sample)}
baseline = run_prompt(TEMPLATE_V1)
candidate = run_prompt(TEMPLATE_V2)
print(f"v1: {baseline} v2: {candidate}")
# 只有候选显著高于基线才切换;样本量小时注意随机波动变量控制的最小纪律
一次只改一个变量。把"加了示例 + 改了措辞 + 换了格式"混在一次改动里,你就再也无法归因是哪个改动起的作用。改完在同一份测试集上重测——换测试集的对比毫无意义。
3. 温度等解码参数也是"提示的一部分"
同样一段提示,temperature=0.7 和 temperature=0 的行为完全不同。工程上先固定解码参数,再调提示文本;把解码参数写进实验记录,否则无法复现。
五、提示版本管理
提示是代码的一部分,必须被版本化管理:
| 手段 | 做法 | 解决什么问题 |
|---|---|---|
| 代码化存储 | 提示写进仓库(.py/.json/.md),不散落在聊天窗口 | 可 diff、可回滚 |
| 版本号 | 每个提示有 v1、v2 或语义版本号 | 归因、沟通 |
| 回归测试 | 关键路径提示纳入 CI:跑 golden set 才允许合并 | 防止"改 A 坏 B" |
| 变更记录 | 记录每次改动的假设与结果 | 避免重复踩坑 |
text
# prompts/extract_order_info/v3.md
### 变更记录
- v1: 初版,few-shot + 手写解析。accuracy 0.82
- v2: 改用 JSON mode。accuracy 0.91(修了非法 JSON 报错)
- v3: 增加 enum 约束字段 status。accuracy 0.95(修了状态值不规范)提示与代码的边界
一次生产事故排查的常见结论是"提示被人在聊天里悄悄改过"。把提示当代码管(评审、测试、回滚),是 LLM 应用工程化水平的分水岭。
六、典型失败与修复对照表
| 失败现象 | 根因 | 修复 | 示例 |
|---|---|---|---|
| 输出格式不稳定,偶尔多段废话 | 约束不足 / 示例不够 | 加格式约束、few-shot、JSON mode | 见第三节 |
| 拒绝回答太频繁 | 系统提示过度防御 / 过度强调安全 | 区分"危险"与"敏感"边界,收敛安全措辞 | 相关讨论见安全与风险 |
| 提示越长效果越差 | 上下文稀释:关键指令被淹没 | 关键指令放开头和结尾;删冗余 | 见下方示例 |
| 回答编造事实 | 模型知识截止 / 长尾知识弱 | 检索增强(RAG),见RAG 实战 | — |
| 少样本时强时弱 | 示例质量/顺序不佳 | 覆盖边界情况、难例放最后 | 见第二节 |
| 被用户输入带偏(提示注入) | 未隔离不可信输入 | 输入边界隔离、输出过滤 | 见安全与风险 |
| 换模型后行为突变 | 提示过拟合于某模型 | 跨模型回归测试、用更稳的约束手段 | 见第四节 |
上下文稀释:提示越写越长的代价
一个典型反模式示例——把历史遗留的二十条规则全部堆进 system prompt,结果模型反而忽略关键指令:
text
# ❌ 反模式:超长系统提示,关键指令淹没在中段
你是客服机器人。请保持礼貌。我们的退款政策是……(500字)
请把用户反馈分类为 A/B/C。请用 JSON 输出……text
# ✅ 修复:关键指令前置 + 精简
把用户反馈分类为 A/B/C,输出 JSON,禁止多余文字。
(政策说明放独立段落,必要时移到提示末尾,避免与关键指令竞争注意力。)**"关键指令放开头和结尾"**是对提示工程中"注意力聚焦"原理的工程化运用。
提示注入的工程防御
提示注入(prompt injection)是"用户或第三方文本试图改写你的指令"。工程上的分级防御:
| 层级 | 做法 |
|---|---|
| 输入隔离 | 系统提示/指令与用户内容、检索内容分开放置且明确标记 |
| 内容过滤 | 对检索内容、网页内容做脱敏与指令标记剥离 |
| 输出校验 | 对模型输出做 schema/内容校验,重要动作需二次确认 |
| 权限最小化 | 工具权限按最小授权设计,关键操作人工确认 |
原则:把"不可信输入"当作代码注入对待——永不直接拼接、永不隐式执行。完整威胁模型见安全与风险。
七、"提示 vs 微调 vs RAG"决策树
遇到"模型不满足需求"时,按下面的顺序判断,不要跳跃:
模型输出不符合预期?
├─ 是知识/事实问题(不知道、编造)──→ RAG(补充外部知识)
├─ 是推理/格式问题(算错、不按格式)─→ 先提示(CoT、few-shot、结构化输出)
│ └─ 提示已到极限(不稳定、成本高)─→ 考虑微调(格式化/风格/领域)
├─ 是风格/口径/领域语言问题 ────────→ 微调 或 更详细的 few-shot
└─ 是能力天花板问题(模型本身不会)──→ 换更强的模型(或接受现状)| 维度 | 提示 | RAG | 微调 |
|---|---|---|---|
| 成本 | 最低(只花推理 token) | 中(检索链路 + 推理) | 最高(训练 + 推理) |
| 时效 | 秒级更新 | 分钟级更新(索引重建) | 需重新训练 |
| 知识注入 | 弱(上下文有限) | 强(任意文档) | 强但固化 |
| 稳定性 | 依赖模型,易漂移 | 检索质量主导 | 稳定 |
| 适用 | 通用任务、快速迭代 | 私有知识、时效知识 | 输出格式、领域风格、任务口径 |
判断标准总结:能靠输入解决的(知识、示例、格式)先用提示和 RAG;只有"模型的内部行为"需要改变时(风格、专有任务)才微调。 微调的具体代价与流程见微调实战。
延伸阅读
- 提示工程(概念篇) —— 提示的组成、零样本 vs 少样本、CoT 的理论版
- 推理基础:自回归与采样 —— 温度、top-k、top-p 对提示效果的底层影响
- 评估实战 —— 把"提示好不好"变成可量化的指标
- RAG 实战 —— 知识型问题从提示转向检索的完整路径
- 微调实战:LoRA 全流程 —— 提示到达极限后的下一步
- 常见陷阱与反模式 —— "提示越写越长"等提示相关的坑
- 安全与风险 —— 提示注入与越狱的防御
参考资料
- OpenAI:Prompt engineering guide —— 六条策略的官方实践指南
- Anthropic:Prompt engineering overview —— Claude 家族的提示工程文档
- Chain-of-Thought Prompting(arXiv:2201.11903) —— 思维链提示的原始论文
- Self-Consistency Improves Chain of Thought(arXiv:2203.11171) —— self-consistency 多数投票的原始论文
- Tree of Thoughts(arXiv:2305.10601) —— 思维树提示的原始论文
- OpenAI:Function calling 指南 —— 结构化工具调用的官方文档
- OpenAI:Structured outputs 指南 —— 按 JSON Schema 强制输出的官方文档