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

从零样本基础到思维树、DSPy 和自动化优化,掌握提示词工程。包含基准测试、代码示例和调试工具箱。

同样的模型,两种问法在小学数学题上的准确率分别为 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

几乎所有上线的提示词都能拆成同样的五块:角色(role)、上下文(context)、指令(instruction)、示例(examples)、输出格式(format)。把它当骨架,每个任务换“血肉”,骨架保持不变——这种统一结构能显著简化评估、缓存和版本控制。

基础技巧#

零样本提示#

零样本就是不给任何示例,直接让模型做事,完全依靠它在预训练中见过的世界来理解意图。

Yao 等人 (2023) 的对照:

任务CoTToTΔ
24 点7.3%74%+66pp
创意写作7.37.9+0.6
填字游戏15.6%78%+62pp

代价。breadth=3、depth=4 一道题就要约 80 次 LLM 调用。ToT 真正划算的前提有两个:解空间里确实存在多条可行路径,模型能可靠地给中间状态打分。组合优化、规划、约束满足类问题适合用;普通问答则不适合。

生产里推荐用带优先队列的最佳优先搜索,配一个硬性的调用次数上限,单题不能炸预算。

Graph of Thoughts (GoT):从树到 DAG#

GoT(Besta 等人, 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 等人, 2022)将推理行动交错起来。模型在思考和工具调用之间反复切换,每一步行动的结果都被观察并反馈。

1
2
3
4
5
6
7
8
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 选工具。
  • 观察结果要截断。搜索引擎可能返回几千字,会撑爆上下文。
  • 强制步数上限。防止死循环。
  • 错误信息要描述性。让模型有机会自我纠正,而不是一崩到底。

提示词默认是脆弱的#

提示词敏感性:仅改变格式或示例顺序,准确率可能波动 20+ 个百分点

同一个模型、同一组示例、同一个任务。仅仅改变示例的格式(Q:/A: 还是 Input/Output)或者它们的顺序,准确率可能波动 20+ 个百分点(Lu et al., 2022; Sclar et al., 2024)。这就是为什么经验性评估不能省。在宣布“找到最优提示词”之前,至少在多种顺序下都跑一遍。

自动化与工程化#

手工调提示词不可持续。下面这些方法把这件事工程化。

自动提示工程(APE)#

APE (Zhou et al., 2022)把“找最优提示词”当成一个搜索问题:

  1. 生成:让 LLM 根据任务描述和少量示例,自动生成几十个候选提示词。
  2. 评估:每个候选在验证集上跑一遍。
  3. 选择:留下表现最好的那一个。
 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 ''}

提供完整可运行的代码:
    ```
"""
    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 自己生成的摘要替换。

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"简明地总结以下对话:
{format_messages(old)}

 总结:"
        )
        self.history = (
            [{"role": "system", "content": f"早期对话摘要:{summary}"}]
            + self.history[6:]
        )

评估与调试#

提示词工程是经验科学,没有指标就只能凭感觉。

几种指标,按成本排序

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 测试#

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 和示例
每次答案不一样任务对单次推理太复杂拆步骤

一个能扫出常见问题的小检查器:

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 "
-" in t or "
*" in t or "
1." in t: return "list"
        return "prose"

错误归因#

把失败案例分桶。下面这套类别能覆盖大多数错误模式:

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.

读有所得?

GitHub 关注我 → 新文周更

GitHub