为什么手写 Prompt 是脆弱的工程
一次换模型全部重写的现实
任何一个维护过大模型应用的工程师都经历过这个循环:上线时手写的提示词工作良好,两个月后换个底层模型,某处措辞失效,指标悄悄掉了十几个点;或者业务加了一个输出字段,提示词改了三处漏了一处,偶发解析失败。手写提示词的根本问题不是「写不好」,而是它是一个没有被定义搜索空间的优化问题——没有人知道当前措辞离最优有多远,也没有任何工具能回答「改 A 还是改 B」这类问题。Prompt optimization 的目标是把这个隐式的优化过程显式化:定义参数空间(提示词文本、少样本示例组合)、定义损失(任务指标)、定义搜索算法,然后让程序自己找。
这不是小题大做。Yang 等人 2023 年的 OPRO 论文(arXiv 2309.03409)给出了一个量化的震撼证据:让 LLM 当优化器去搜索提示词,在 GSM8K 数学题上找到的指令「Take a deep breath and work on this problem step-by-step」把 PaLM 2 的 pass rate 推到了 88.7%,显著优于人工精心设计的「Let's think step by step」。一句人类直觉永远不会想到的深呼吸,胜过数月的人工调优——这是「搜索空间里存在人类想不到的好解」的直接证明。
把优化问题形式化
先把问题定义清楚。一个 LLM 程序(不只是单次调用,可以是带检索、带工具、多阶段流水线的复合程序)由一组可变文本参数 θ 构成:每阶段的指令、每阶段的少样本示例(demonstrations)。优化目标是最大化验证集指标 J(θ)。搜索空间的规模值得掂量:一个两阶段程序,每阶段有候选指令几十条、候选示例几百条,组合空间轻松超过百万,而且每次评估 J(θ) 都要花真金白银调 API。这个形状——离散空间、评估昂贵、噪声大——决定了它的标准解法是昂贵的元优化算法:候选生成靠模型自己,选择靠贝叶斯优化或随机搜索,评估集要刻意控制在几十条以内。
DSPy 的三层抽象
Signature 与 Module:把提示词工程编译成类型系统
DSPy(Khattab 等人,2023 起,斯坦福 NLP)是目前事实上的标准框架,它的第一层抽象是 Signature——用类型签名声明一次调用的输入输出语义,而不是手写指令文本。第二层是 Module——把签名组合成程序,对应检索、思维链、多跳推理等固定模式。第三层是 Optimizer(早期叫 teleprompter)——输入程序和指标,输出优化后的程序(指令与示例都可能是新的)。关键设计是策略与参数分离:你写的是「我要什么」(签名)和「程序结构」(模块),「怎么对模型说」(提示词文本)由优化器负责生成与搜索。换底层模型时,重跑一次优化器即可,程序代码一行不改——这直接命中了手写提示词的最大痛点。
import dspy
# 第一层:签名——声明语义,不写提示词
class VerifyAnswer(dspy.Signature):
"""验证候选答案是否正确回答了问题。"""
question: str = dspy.InputField()
candidate: str = dspy.InputField()
verdict: bool = dspy.OutputField(desc="true 表示正确")
# 第二层:模块——程序结构
class Verifier(dspy.Module):
def __init__(self):
super().__init__()
self.generate = dspy.Predict(VerifyAnswer)
def forward(self, question, candidate):
return self.generate(question=question, candidate=candidate)
# 第三层:优化器——搜索指令与示例
from dspy.teleprompt import MIPROv2
optimizer = MIPROv2(
metric=lambda ex, pred, trace=None: pred.verdict == ex.label,
auto="light", # 自动控制预算:候选数与试验数
)
program = optimizer.compile(Verifier(), trainset=trainset[:200])
Optimizer 谱系:从 Bootstrap 到 MIPROv2
DSPy 内置的优化器是一条演进链。最早期的 BootstrapFewShot 什么都不改指令,只做一件事:用当前程序在训练集上跑,把「指标判定为正确」的轨迹自动抽出来当少样本示例——零额外成本地解决「示例从哪来」。加上指令搜索和随机搜索变体的是 BootstrapFewShotWithRandomSearch。真正把元优化算法做到生产级的是 MIPRO(Multi-prompt Instruction PRoposal Optimizer)及其继任 MIPROv2。
MIPROv2 的算法细节
两个候选空间与三步流程
MIPROv2(Opsahl-Ong 等人,2024,arXiv 2406.11695)把搜索空间拆成两个正交的候选集。指令候选:用一个强模型(提案模型)作为生成器,输入是任务的描述、训练集样本、程序各模块的现有代码与已有指令,要求它基于这些材料提出多样化候选指令——通常几十条,并且强制要求覆盖不同的风格轴(简短 vs 详细、正式 vs 口语、强调不同约束)。示例候选:Bootstrap 出的轨迹按质量筛选,形成几十组候选示例组合。
优化循环分三步。第一,初始化候选集(提案模型批量生成指令 + 轨迹采集筛示例)。第二,贝叶斯优化选择:维护一个代理模型(高斯过程),每次从「指令组合 × 示例组合」的联合空间里采样一个配置,跑验证集得分数,把结果喂回代理模型,让它的后续采样偏向历史上得分高的区域。第三,迭代几十次后返回最优配置。这个设计的聪明之处在于把「模型擅长的事」(写出多样好指令)交给模型,把「模型不擅长的事」(在昂贵噪声环境下做全局权衡)交给贝叶斯优化——后者正是统计方法的主场。
flowchart TD
A[输入: 程序 + 指标 + 小训练集] --> B[提案模型生成指令候选]
A --> C[Bootstrap 采集轨迹筛出示例候选]
B --> D[贝叶斯优化循环]
C --> D
D --> E[代理模型建议下一个配置]
E --> F[组装: 指令组合加示例组合]
F --> G[验证集评估得分]
G --> H[更新代理模型]
H --> E
G --> I{试验预算用尽}
I -- 否 --> E
I -- 是 --> J[返回最优配置的程序]
实测数字与预算控制
MIPROv2 论文在 6 个任务(含 GSM8K、多跳问答 HotPotQA、分类与合成任务)上系统评估:相对人工基线提示词,优化后的程序平均提升从几个点到二十个点不等,且在多数任务上超过人工迭代的提示词版本;相对随机搜索(同样的候选池但用随机采样选配置),贝叶斯优化在同等预算下更稳定地命中高分区。工程上的关键参数是预算:auto="light" 模式下大约十几次试验就能见效(成本可控制在几美元级),auto="medium" 到几十次。验证集必须与训练集严格分离——这不是可选的洁癖,贝叶斯优化对几十条的评估集天然会过拟合,评估集污染后返回的是「对这 30 条样本最优」的提示词,而非对任务最优。
指标与评估:优化器的第一公民
二元化指标与方差管理
优化器的输出质量由指标决定,而指标设计有两个必须处理的工程问题。第一个是二元化:LLM 程序的输出多是自由文本,直接用 LLM 打 1 到 10 分做优化目标会被打分噪声淹没——分数的方差与真实质量差异同量级,贝叶斯优化就是在拟合噪声。实践中有效的做法是把指标二元化:解析输出、跑断言、只返回通过与不通过(或 0/0.5/1 三档)。MIPROv2 的论文实验里指标几乎全部是二元精确匹配或单元测试通过率,这不是巧合而是设计:离散优化器需要的是低方差的信号,哪怕信号粗一点。第二个是方差管理:同一个配置跑两次,模型的随机性会让分数波动几个点,几条样本的验证集上尤其如此。控制手段是三件套——采样温度压到 0(注意 temperature 0 不等于完全确定,批处理与浮点顺序仍会带来微小抖动)、验证集样本量不低于 30(置信区间随样本量平方根收缩)、高价值配置复评 3 次取均值。评估成本直接翻倍是这些手段的代价,但这笔钱花在指标上比花在候选生成上划算——方向错了,后面全是沉没成本。
从优化器输出到可部署资产
优化结束拿到的不该只是一个提示词文本,而是一组工程资产:优化后的完整程序(指令、示例、组装逻辑)、优化过程的全部试验记录(每个配置的分数轨迹——这是你下一次优化的起点与先验)、以及失败的候选清单(知道自己试过什么同样重要)。把 DSPy 程序导出成普通提示词模板部署也可以,但会失去可重优化的能力;更可持续的做法是把优化器本身纳入 CI——指标函数、训练验证数据、优化器配置三者版本化,任何一处变更都能一键重跑。这条纪律的回报在模型升级时兑现:新模型上线,重跑一次 light 档优化即可获得适配新模型脾性的提示词,而不是组织一轮人工重新调优。
OPRO、APE 与更早的谱系
APE:把指令当自然语言程序来搜索
MIPROv2 之前有一条清晰的算法脉络。APE(Automatic Prompt Engineer,Zhou 等人,2022,arXiv 2211.01910)是最早的系统性工作:让模型从少量输入输出样本反推指令(「执行这个函数的程序可能是什么」),生成一批候选后用选定评估(估计每个候选指令下模型输出的对数概率)打分,保留 top 部分进入下一轮变异。OPRO(Yang 等人,2023)把选择环节换成了优化轨迹:每一轮把「历史指令与得分」作为上下文喂给模型,让它提出新候选——优化轨迹本身成了引导搜索的提示词。这两个工作与 MIPROv2 的关系一目了然:候选生成都靠 LLM,差异在选择算法(选定评估 → 优化轨迹上下文 → 贝叶斯优化代理模型),选择算法的演进直接对应评估预算的下降与噪声鲁棒性的提升。
共同的失败模式
这条技术路线有三个共享的坑,做工程决策前必须知道。第一,评估集过拟合:所有优化器都在「暴力搜索 + 事后解释」的边缘,评估集太小或与线上分布不一致时,找到的是对评估集的 memorize 式提示词,线上收益可能为零甚至为负。第二,指标可 gaming:如果你的指标是 LLM 打分,优化器会找到迎合打分模型偏差的指令(更长的回答、更花哨的格式),指标必须是可执行验证或与真实业务收益强相关。第三,过度配置的脆弱性:高度优化的指令可能过拟合当前模型的怪癖,底层模型升级后部分优化失效——所以优化后的程序要纳入回归测试,模型变更时重跑优化器。这三条决定了自动提示优化的正确用法:它是加速人工迭代的工具,不是替代人判定「什么算好」的自动化。指标设计仍然是要人负责的部分。
实操路线图
给出一条可落地的路径。第一步,定义指标:优先选可执行验证(单元测试通过率、解析成功率、精确匹配),主观维度才用 LLM 评分且要做校准。第二步,准备数据:训练集百条级(优化器要拿它生成候选与轨迹),验证集 30 到 100 条(供贝叶斯优化评估),测试集另留(优化结束后只碰一次)。第三步,先跑 BootstrapFewShot 拿到零成本的示例优化基线,确认指标管线工作正常。第四步,上 MIPROv2,从 auto="light" 开始,观察每轮试验分数的分布判断预算是否够。第五步,把优化后的程序在测试集上验收,并固定下来进版本管理与回归测试。整个流程中最贵的是第五步之后——当底层模型升级时,重跑优化器的成本只是当初的一半,因为训练数据与指标管线都已就位。
小结
自动提示词优化把「对模型说话的艺术」改造成了带明确搜索空间的离散优化问题:DSPy 用 Signature/Module/Optimizer 三层抽象实现策略与参数分离,换模型重跑优化即可;MIPROv2 让模型生成指令与示例候选、让贝叶斯代理模型在昂贵评估下做全局权衡,在 GSM8K 到 HotPotQA 的一组任务上稳定超过人工提示词;OPRO 的「深呼吸」实验证明了搜索空间里存在人类直觉之外的优质解。但这条路线的天花板由三件事决定——评估集的代表性、指标的不可 gaming 性、以及对过拟合提示词的回归测试纪律。它优化的永远只是「怎么说」,而「什么算好」的判定标准,仍然是工程师不可外包的责任。
读者留言
COMMENTS 暂无还没有留言,来说第一句?