
给一串 LM 调用自动「调 prompt」:MIPRO 怎么给多阶段流水线做优化
如果你拼过一个 RAG 系统,或者一个多跳检索的 QA 流水线,大概率改过某个模块的 prompt。
改完跑一遍,分数涨了。但下一轮你动了另一个模块,分数又掉回去了。
这不是你调得不好。这是多阶段 LM 程序的通病——每个模块的 prompt 都是一个变量,它们联动在一起决定最终分数。手写试错只能处理一个变量,处理不了联动。
Stanford 和 UC Berkeley 的 DSPy 团队(Opsahl-Ong、Ryan、Khattab 等)在 arXiv 2406.11695 里把这件事正式化成了一个优化问题:没有梯度、没有中间模块的标签,只有最终指标,怎么自动调好每个模块的指令和示例。他们提出的优化器叫 MIPRO,在自建的七任务 benchmark 上拿下了五个最优,最高比未优化程序提升 13%。

为什么多阶段流水线不能靠手调
单次 LM 调用的 prompt 工程已经很成熟。APE、OPRO、EvoPrompt 这些方法都能在单任务上自动搜出好 prompt。GEPA 和 TextGrad 走得更远,一个在自然语言空间做反思式进化,一个把反向传播搬进文本空间。
但流水线不是单任务。
论文把困难拆成两块。第一块叫提案(proposal):prompt 是字符串,字符串空间无穷大,再加上模块数量变多,候选组合爆炸,怎么从中挑出几个高质量的。第二块叫信用分配(credit assignment):一轮跑完,最终分数出来了,但你不知道这个分数该归功于哪个模块的 prompt,也不知道哪个模块在拖后腿。
这两块是全文的骨架。后面所有方法、所有优化器,都是对这两块的不同回答。
更严格地说,作者把问题写成了一个没有梯度的黑盒优化。你只有一个 LM 程序 Φ、一个指标 μ、一份训练集。每个模块的 prompt 里有两类变量:自由格式的指令(instruction),和若干个 few-shot 示例(demonstration)。目标是找到一组变量赋值,让 Φ 在训练集上的平均分数最高。
难点在于:变量是离散的字符串,指标只在程序出口提供监督,中间模块全是隐变量,LM 的梯度和 log-probability 都拿不到。这正是多数真实 LM API 的约束——文本进、文本出,别的什么都没有。
三招提案,三招信用分配
论文的设计空间是一张配方表。提案侧三招,信用分配侧三招,六种策略两两组合,就能造出不同性格的优化器。
提案侧的三招:
Bootstrap demonstrations。用程序自己跑训练样本,留下那些最终得分够高的运行轨迹,把轨迹里每个模块的输入输出当成候选 few-shot 示例。这是 DSPy 之前就有的招数,简单但出奇有效——后面会看到,它常常比优化指令还管用。和 A-MEM 的卡片笔记式记忆思路相通,都是让系统从自己的成功经验里抽取可复用的结构。
Grounding。给负责提指令的 proposer LM 喂上下文:数据集长什么样、程序的控制流是什么、这个模块之前跑出过哪些成功的示例。论文发现,给 proposer 看这些”接地”信息,能让它提出更贴合任务的指令。
Learning to propose。提案本身有超参数(温度、要不要喂数据摘要、喂哪组示例)。用贝叶斯模型学这些超参数的效用,让提案策略自己适应任务。
信用分配侧的三招:
Greedy。一次只改一个模块,其他模块固定,逐个爬坡。直观,但效率低——某些模块的改动要等别的模块先变好才能体现出来。
Surrogate。用一个贝叶斯代理模型,从历史评估里学”哪些参数组合好”,引导下一步往哪个区域采样。论文用的是 Tree-structured Parzen Estimator(TPE),它能建模变量之间的联合贡献。好处是高效,短板是只能在已有候选里挑,没法用学到的经验去改进候选本身。
History-based。直接把历史轨迹和分数塞给 proposer LM,让它自己判断哪个模块的指令该改、怎么改。这是 OPRO 的思路,假设 LM 自己能完成信用分配。

MIPRO 把指令和示例一起优化
MIPRO(Multi-prompt Instruction PRoposal Optimizer)是论文的主角。它的配方是:Grounding 提案 + 贝叶斯 surrogate 信用分配 + mini-batch 评估。
关键设计是把提案和信用分配彻底拆开。proposer LM 只管提候选指令,不背信用分配的锅;surrogate 模型只管从历史里学,不负责生成。两件事各司其职,互不干扰。
具体流程分三步。第一步,bootstrap 出 N 组示例,每个模块都有;同时用 Grounding 提出每个模块的若干条候选指令。第二步,用 TPE 的采样规则,从这些候选里组合出”每个模块用哪条指令 + 用哪组示例”的配置,跑一遍程序。第三步,用 mini-batch 上的得分更新 TPE 的先验,让好候选的权重升、差候选的权重降。隔几步把表现最好的几个配置在完整训练集上复评一次,最后返回得分最高的那个。
mini-batch 这一步是有意为之。贝叶斯优化对噪声鲁棒,能把不确定性吸收进优化过程,所以不用每轮都跑完整训练集,省下大量 LM 调用。论文里,一次”完整评估”背后对应着大约六倍的小批量评估次数,等于把预算花在了更多参数组合的探索上。

和它对比的两个 baseline 各代表一种极端。Bootstrap Random Search 只优化示例,指令保持默认,用随机搜索挑最好的那组示例。Module-Level OPRO 只优化指令,靠把每个模块的指令历史和分数喂给 proposer LM,让它自己改指令——也就是把信用分配也交给 LM。MIPRO 则两个都优化,且信用分配交给 surrogate。
七任务横评的五条经验
论文建了一个七任务 benchmark,覆盖多跳问答(HotPotQA)、带条件规则的多跳问答(HotPotQA Conditional)、分类(Iris、Heart Disease)、逻辑推理(ScoNe)、多跳事实验证(HoVer)。这些程序少则一个模块,多则四个模块串联,正好覆盖了多智能体 LLM 系统里常见的”多调用联动”结构。任务模型用 Llama3-8B,proposer 用 GPT-3.5。每个方法在每个任务上跑五次。
结果压成五条经验,每一条都值得单独看。
第一,优化 bootstrap 示例通常收益最大。 用 Wilcoxon 检验,Bootstrap Random Search 在七个任务里的六个上压过了最好的指令优化器。作者的推断是:好的示例教会的是”怎么推理”,而不只是”答案长什么样”。换句话说,示例承载的信息比格式示范更多。
第二,指令加示例联合优化(MIPRO)整体最好。 七个任务里五个最优,统计显著。最显眼的数字来自 HotPotQA Conditional——未优化程序只有 6%,MIPRO 拉到 23.3%。HoVer 的多跳检索也从 25.3% 涨到 39.0%。
但这条经验有例外。HotPotQA、Heart Disease、Iris 这三个任务上,MIPRO 没有显著超过只优化示例的 baseline。作者给的解释很坦诚:HotPotQA 的最后一步是标准问答,模型本来就熟,指令优化的空间不大;Heart Disease 的 seed 指令没给分类标准,现在的指令优化器还推不出来。
第三,带条件规则的任务,优化指令反而赢。 这是最反直觉的一条。HotPotQA Conditional 的规则是:答案是人就全小写,是地方就去标点,是日期就结尾加”Peace!”,其他情况全大写。这种规则靠几个示例很难完整表达,但靠一条明确的指令可以。在这个任务上,纯指令优化(14.6%)压过了纯示例优化(10.4%)。Iris 的带错别字版本也印证了这点——优化器甚至能把 seed prompt 里的拼写错误纠正过来。
第四,Grounding 整体有用,但最优配置因任务而异。 在 HotPotQA 和 HoVer 上,Grounding 是性能提升的关键;但在 ScoNe 上反而掉分。0-Shot MIPRO++ 靠元优化 proposal 超参,能把 ScoNe 上的性能找回来。这也说明,没有一个提案策略是万能的,能自适应才有出路。
第五,还有很多没搞明白。 Module-Level OPRO、0-Shot MIPRO、0-Shot MIPRO++ 三者互有胜负;Bayesian Bootstrap 对 Bootstrap RS 的优势在 ScoNe 上显著,在另外两个多跳任务上不显著。作者自己留了一个猜想:低预算下 mini-batch 多的优化器可能更占优,高预算下会学提案的 MIPRO++ 才显出优势。这个猜想留给未来工作。
优化器学到了什么,以及它滑稽的失败
经验数字之外,论文附录里有一组很值得看的素材:优化过程中 proposer LM 提出来的那些指令,长什么样。
有些看着就离谱。给 Iris 鸢尾花分类的任务,优化器提出来一条:“给定一种鸢尾花正处于灭绝边缘的危急情况,根据花瓣和萼片的尺寸预测物种,以拯救它免于灭绝。“得分 45.33,是那条 run 里最好的。
给心脏病诊断,proposer 给每个模块都加了”急诊室""生死关头”的前缀。给 HotPotQA Conditional,它编出了一个关于”卧底特工在绝密行动中”的指令来强制全大写输出。
作者在附录 H 里坦率地讨论了这种 overfit 现象。proposer 容易把 meta-prompt 里的 few-shot 示例过拟进指令——比如看到一条关于芬兰拳击手的示例,就写一条专门针对芬兰拳击手的指令。奇怪的是,这种过拟指令有时反而有效。作者猜测,要么需要更精细的信用分配才能剔除它们,要么它们其实起着”软示例”的作用,把模型的行为往正确方向偏。这件事还没结论。
这恰恰是这类优化器目前的真实状态:它们能把 prompt 从手艺活变成可形式化的优化问题,但离”自动推断任务规则”还差得远。论文在 Limitations 里明确写了,优化器在缺 seed 指令时很难自己推断复杂规则,proposer 和 task LM 都还只测了固定组合,泛化性没验证。
换句话说,MIPRO 回答了”怎么在给定程序和指标下自动调 prompt”,但没有回答”怎么让优化器自己理解任务”。前者是工程问题,已经有了像样的答案;后者是更难的那一层。
一个视角的转变
把 prompt 当成可优化的参数,而不是手写的咒语——这个视角的转变,可能比某个具体优化器更重要。
GEPA 在自然语言空间里做反思式进化,TextGrad 把反向传播搬进文本空间,MIPRO 用贝叶斯代理模型给多阶段流水线做离散优化。它们路径不同,但指向同一件事:prompt 不是工程艺术的终点,而是优化的起点。
MIPRO 的价值不只是那 13% 的提升。它把”多模块联动""信用分配""离散字符串变量”这些过去只能靠手感处理的东西,写进了一个可以复现、可以比较、可以迭代的框架里。代码和 benchmark 都开源在 DSPy 里。
至于那些”拯救濒危鸢尾花”的荒诞指令,与其当成 bug,不如当成一个提醒:当优化器开始用我们看不懂的方式得分时,也许不是它错了,而是我们对”好 prompt”的直觉,本来就不完整。
论文:Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programs
代码与 benchmark:DSPy