DAY 51 / PHASE 6 · 新前沿工程化

自动 Prompt 优化

DSPy · APE / OPRO · Eval 驱动 · Few-shot 自动选样

2026-07-01 · BigCat

别再手搓 prompt——把它当成一个由 metric 编译出来的参数,让搜索去调。

前置概念 → ai-ml-daily Day 3 (Prompt Engineering)

// WHY THIS MATTERS

资深用户手调 prompt 的边际收益正在递减:你凭直觉改一版、跑几个例子、觉得「好像好点了」就上线——这既不可复现,也无法证明真的变好了。自动 Prompt 优化把这件事变成一个可度量、可搜索的优化问题:定义 metric,让程序去搜索 instruction 措辞和 few-shot 组合。这一期不讲 prompt engineering 是什么、CoT 为什么有效——那是 ai-ml-daily Day 3。这里只讲四个工程决策:什么时候值得自动优化、什么时候纯属浪费DSPy 怎么把 prompt 当程序编译APE / OPRO 这类指令搜索的真实收益与坑few-shot 自动选样和 overfit 治理。核心反直觉:没有一个可信的 eval,自动优化只会让你更快、更自信地过拟合到错的目标上。

// 01

手调 vs 自动优化:先问值不值得

论断:自动 prompt 优化的前置条件不是「prompt 不好」,而是「你有一个可信的 metric 和足够的标注样本」。

背景与原理

自动优化的本质是用 eval 分数做梯度的黑盒搜索:它只会朝你定义的 metric 爬坡。所以 metric 有多糙,优化就有多歪。触发自动优化要三条同时满足:(1) 有客观或半客观的 metric(准确率、F1、格式合规、LLM-judge 且已校准);(2) 有几十到几百条带 ground truth 的样本,且能切出独立的 train / val / test;(3) 这个 prompt 会反复高频调用,值得一次性投入优化成本。反过来,一次性任务、没法定义分数的开放创作、样本个位数——手调更快更省。还有个常被忽略的判据:任务是否稳定。需求每周变、schema 天天改的场景,优化产物很快过期,维护成本盖过收益。

要不要自动优化 prompt? │ ▼ ┌──────────────────────┐ 否 │ 有可信 metric? ├────▶ 先建 eval(Day 6),别急着优化 └──────────┬───────────┘ │ 是 ▼ ┌──────────────────────┐ 否 │ ≥ 数十条带标注样本? ├────▶ 手调 + 少量 few-shot └──────────┬───────────┘ │ 是 ▼ ┌──────────────────────┐ 否 │ 高频复用 & 任务稳定? ├────▶ 手调(优化产物会过期) └──────────┬───────────┘ │ 是 ▼ 自动优化(DSPy / APE / OPRO)
失败模式:拿 20 条没切分的样本、用一个没校准的 LLM-judge 就开跑自动优化。结果 metric 被刷高,线上没变好——你只是过拟合了噪声。eval 质量是自动优化的天花板,先补 eval(Day 6 / Day 29),别本末倒置。
进阶资源 · Anthropic Prompt improver(最轻量的自动优化入口), docs.anthropic.com/.../prompt-improver
// 02

DSPy:把 prompt 当程序编译

论断:DSPy 让你写「签名 + 模块」的声明式程序,再用 optimizer 从 metric 编译出 instruction 和 few-shot,而不是手写字符串。

背景与原理

DSPy(Khattab 等 2023,arXiv 2310.03714)的核心转变:你声明输入/输出的 signature 和模块结构,把「具体 prompt 长什么样」交给 optimizer 去搜。一个 Signature 定义字段(如 text -> label),一个 Module(如 ChainOfThought)定义调用方式,optimizer 则在 trainset 上联合优化两样东西:每个模块的 instruction 措辞,以及自动 bootstrap 出来的 few-shot 示例。MIPROv2(Opsahl-Ong 等 2024,arXiv 2406.11695)用贝叶斯优化搜索 instruction+demo 组合;它对多阶段 LM 程序做无模块级标签的信用分配——你只给最终 metric,它自己分摊到各模块。价值在于:换模型、改需求时,你重新 compile 一次,而不是重写所有 prompt。

实战示例

import dspy
dspy.configure(lm=dspy.LM("anthropic/claude-haiku-4-5"))

class Classify(dspy.Signature):
    """判断工单的紧急程度。"""
    ticket: str = dspy.InputField()
    urgency: str = dspy.OutputField(desc="low|medium|high")

program = dspy.ChainOfThought(Classify)

# metric:只需返回一个可比较的分数
def metric(gold, pred, trace=None):
    return gold.urgency == pred.urgency

from dspy.teleprompt import MIPROv2
opt = MIPROv2(metric=metric, auto="medium")      # 搜索强度档位
compiled = opt.compile(program, trainset=train, valset=val)
compiled.save("classify.v3.json")                  # 产物入 registry(Day 35)

关键心智:你调的不是「prompt 文本」,是 metric、trainset、optimizer 档位三个旋钮。优化出来的 instruction 常常读起来平平无奇甚至有点怪,但在 val 上分更高——这正是把审美判断让给数据的意义。

失败模式:(1) trainset 太小(十几条)→ optimizer 把噪声当信号,val 崩。(2) metric 是布尔全对全错、粒度太粗 → 搜索没有梯度可爬,等于随机。(3) 把 DSPy 当「魔法调优」黑盒用,从不看它生成的 prompt——出问题无法 debug。compile 完务必打印最终 prompt 存档。
进阶资源 · Khattab et al. DSPy, arXiv 2310.03714 · DSPy 官方 optimizer 文档, dspy.ai/learn/optimization
// 03

指令搜索:APE 与 OPRO 的真实收益

论断:让 LLM 自己提议并迭代 instruction(APE/OPRO)确实能超过人手写,但产物「反直觉、跨模型不迁移」是常态,不是 bug。

背景与原理

两条经典路线。APE(Zhou 等 2022,arXiv 2211.01910)把「指令」当程序:让 LLM 生成一批候选 instruction,用目标模型的 eval 分数打分选最优——在 24 个 NLP 任务上追平甚至超过人手写。OPRO(Yang 等 2023,arXiv 2309.03409)更进一步:把「历史 (prompt, score) 对」按分数排序喂回给 LLM,让它基于轨迹提议更好的下一版,形成迭代爬坡。这类方法搜出来的 prompt 常出人意料——OPRO 在 GSM8K 上搜出的名句是「Take a deep breath and work on this problem step by step」,比人写的 CoF 引导更高分。这说明两件事:优化目标是分数不是可读性;且这种胜出强烈依赖被优化的那个模型,换模型往往要重搜。

OPRO 迭代式指令搜索: ┌─────────────────────────────────────┐ │ meta-prompt:历史(instr, score)按分排序 │ └───────────────┬─────────────────────┘ ▼ LLM 提议 N 个新 instruction ┌────────────────────┐ │ 在 eval set 上打分 │ └─────────┬──────────┘ ▼ 写回历史,保留 top-k ┌────────────────────┐ 未收敛 │ 分数还在涨? ├──────────▶ 回到顶部再迭代 └─────────┬──────────┘ │ 收敛 ▼ 在 held-out test 上验证后定版

实战示例

不引第三方库也能手搓一个最小 OPRO 循环:

def opro_step(history, n=8):
    # history: [(instr, score), ...] 已按 score 升序
    shown = "\n".join(f"instr: {i}\nscore: {s:.2f}" for i,s in history[-10:])
    meta = f"""下面是指令及其得分(越高越好)。
{shown}
写出 {n} 条指令,目标是拿到更高分。只输出指令。"""
    cands = propose(meta)                     # LLM 生成候选
    return [(c, eval_on_valset(c)) for c in cands]  # 逐条打分
# 反复迭代,把新 (instr,score) 并回 history,收敛后在 test 上定版

工程守则:候选一定要在 held-out val 上打分,最终版必须在从未参与搜索的 test 上验证——否则你搜到的是「刷分咒语」,不是真提升。想省事,Anthropic Console 的 prompt improver 是零代码入口,适合先拿到一个强基线再考虑上搜索。

失败模式:(1) 在同一个 set 上又搜又验 → 过拟合,test 打回原形。(2) 把 A 模型搜出的「神咒」直接搬到 B 模型 → 收益消失甚至变差,指令搜索几乎不跨模型迁移。(3) 迭代不设停机准则,烧掉几千次调用换来 0.5% 提升——收益早已被成本盖过。
进阶资源 · Zhou et al. APE, arXiv 2211.01910 · Yang et al. Large Language Models as Optimizers (OPRO), arXiv 2309.03409
// 04

Few-shot 自动选样与 overfit 治理

论断:自动优化最大的产出常常不是 instruction,而是一组自动挑出的 few-shot;而它最大的风险是过拟合到 eval,必须像模型训练一样做数据切分与治理。

背景与原理

一个被低估的事实:在很多任务上,自动 bootstrap 出的 few-shot 示例,收益大于 instruction 措辞的微调。DSPy 的 BootstrapFewShot 就是让 teacher 模型在 trainset 上跑、用 metric 筛出「答对的完整轨迹」当示例,再塞进 prompt。这比人手挑例子更系统。但示例是把双刃剑:选进来的样本会把它们的分布、格式、甚至偏见一起带进 prompt,在分布外输入上可能反噬。所以自动优化必须借用机器学习的纪律:train / val / test 三分,搜索只碰 train+val,报告只信 test;并把优化产物纳入 Day 35 的 prompt registry——版本化、可回滚、记录「用哪个模型、哪份数据、哪个 metric 编译的」。换模型或数据漂移时,靠 registry 触发重编译,而不是让线上悄悄退化。

实战示例

# 数据切分是自动优化的安全带,不是可选项
train, val, test = split(data, 0.6, 0.2, 0.2)

from dspy.teleprompt import BootstrapFewShot
opt = BootstrapFewShot(metric=metric,
        max_bootstrapped_demos=4,   # teacher 生成的示例数
        max_labeled_demos=4)      # 直接取自 train 的示例数
compiled = opt.compile(program, trainset=train)

# 只信 test 上的分数——它从未参与搜索
print(evaluate(compiled, test))
# 记录 provenance:model + data 版本 + metric + 分数 → registry

治理清单:val 和 test 分数的差距就是过拟合温度计——差距大说明搜过头了,回退搜索强度或加数据。上线后监控线上分数 vs test 分数,出现 prompt 漂移(同一 prompt 因模型更新而效果变化)时,registry 里那份 provenance 让你能一键复现并重编译。

失败模式:(1) 只切 train/test,用 test 反复调超参 → test 悄悄变成 val,泄漏。(2) few-shot 选进了高度同质的样本 → 窄化了模型行为,长尾输入崩。(3) 优化产物没版本化,换了个模型端点后无人知道该重编译——线上准确率阴跌几周才被发现。
进阶资源 · Opsahl-Ong et al. MIPRO(多阶段程序的指令+示例联合优化), arXiv 2406.11695 · DSPy choosing an optimizer, dspy.ai/learn/optimization

// 综合实战 · 给一个高频 prompt 上一次自动优化

把四点串成一个能落地的周末改造:挑一个每天调用上千次的 prompt,走完「判断→编译→搜索→治理」全流程。

  1. 先过判断门(§1):确认它有可信 metric、≥数十条带标注样本、高频且稳定。缺 eval 就先停下补 eval——这是天花板。
  2. 三分数据:train/val/test = 6/2/2,test 锁进保险箱,全程只在最后碰一次。
  3. DSPy 编译(§2):写 signature + ChainOfThought,用 MIPROv2(auto="medium") 在 train+val 上编译,打印并存档最终 prompt。
  4. 要更极致再上指令搜索(§3):DSPy 不够就手搓 OPRO 循环迭代 instruction,设好停机准则(分数不再涨 / 预算上限)。
  5. 治理收口(§4):对比 val 与 test 分数看过拟合;把产物连同 model+data+metric+分数 存进 registry(Day 35);上线后监控线上分数,漂移即重编译。

做完这套,你的 prompt 从「凭手感改」升级为「有 provenance、可复现、可回滚的编译产物」——换模型时重跑一次,而不是通宵重写。

// ENGLISH GLOSSARY

APO (Automatic Prompt Optimization)
用 eval 分数驱动搜索来自动改进 prompt 的统称。
DSPy
声明式 LM 编程框架:写 signature+module,由 optimizer 从 metric 编译出 prompt。
Signature / Module
DSPy 中声明输入输出字段(signature)与调用方式(module,如 ChainOfThought)的抽象。
APE (Automatic Prompt Engineer)
让 LLM 生成候选指令、按 eval 分数选优的方法(Zhou 2022)。
OPRO
把历史 (prompt, score) 喂回 LLM 让其迭代提议更优 prompt 的优化框架(Yang 2023)。
MIPROv2
DSPy 的贝叶斯优化器,联合搜索 instruction 与 few-shot demo。
Bootstrap Few-shot
用 teacher 模型跑 trainset、按 metric 筛出正确轨迹当示例的自动选样。
Overfitting to Eval
优化过度贴合 eval set,导致 test/线上不升反降。
Provenance
优化产物的血统记录:用哪个模型、哪份数据、哪个 metric 编译。

// 深入思考

既然自动优化只朝 metric 爬坡,那它和「Goodhart 定律」是不是同一个陷阱?怎么防?
是同源风险:一旦 metric 成为优化目标,它就不再是好的度量。防线有三层。(1) metric 多目标化:别只优化准确率,把格式合规、拒答率、长度约束一起入 metric,堵单点刷分。(2) held-out test 严格隔离:test 只在定版时碰一次,一旦拿它调超参就失效。(3) 线上真实分布抽样复评:eval set 永远是现实的近似,定期用线上新数据刷新 test,防止优化贴合了一个过时的分布。本质上,自动优化把「metric 设计」的重要性放大了——metric 就是你交给机器的价值函数。
DSPy 说「programming not prompting」,但优化出的还是 prompt 字符串。这个抽象到底省了什么?
省的是手写与手维护 prompt 的耦合成本。传统写法里,模型、任务、prompt 措辞、few-shot 全焊死在一根字符串上;换模型或改需求就得整体重写。DSPy 把它解耦成「声明的程序结构」+「可编译的参数」:结构(signature/module)稳定,prompt 是编译产物。换模型时结构不动,重 compile 即可。类比编译器——你写高级语言,让编译器针对目标架构生成汇编,而不是手写汇编。省的不是「不出现 prompt」,是「不用手动为每个模型/需求组合去调 prompt」。
OPRO 搜出「深呼吸再一步步想」这类反直觉神咒——为什么会这样?这对「可解释的 prompt 工程」意味着什么?
因为优化目标是eval 分数,不是人类可读性或语义正当性。模型的 prompt 敏感性是高维非线性的,某些措辞恰好激活了对该模型该任务有利的内部状态,人类先验根本预测不到。这意味着「可解释的 prompt 工程」有天花板:靠讲道理写出的 prompt 未必是最优点。但反过来也警示——这类神咒脆且不迁移,换模型/换分布就失效,本质是对特定权重的过拟合。工程上的取舍:追求分数就接受不可解释并用 registry 管住脆性;追求鲁棒与可维护,就宁可要一个略低但讲得通、跨模型稳的 prompt。
自动优化会不会最终让「prompt engineer」这个技能消失?
会转移,不会消失。消失的是「凭手感逐字调措辞」这一层——那本就该交给搜索。留下且更值钱的是:定义好 metric(价值函数)、构造有代表性的数据集、判断何时该自动化、诊断过拟合与漂移。这些是把模糊业务目标翻译成可优化问题的能力,恰恰是搜索替代不了的。类比:编译器没消灭程序员,消灭的是手写汇编;它把人的注意力抬到了更高的抽象层。自动 prompt 优化对 prompt engineer 是同一种抬升。
如果每次换模型都要重新编译 prompt,这和「供应商锁定」(Day 45 反模式)是什么关系?
它其实是解锁而非锁定——前提是你用对了工具。手写 prompt 才是隐性锁定:措辞深度耦合某个模型,迁移=重写,成本高到没人愿动。DSPy 这类框架把「结构」和「针对模型的 prompt」解耦后,换模型的代价降为「重跑一次 compile」,反而让你更敢换供应商。真正的锁定风险在别处:如果优化框架本身只支持单一供应商、或你的 eval/数据管道绑死某家 API,那才是锁定。所以治理上要保证 eval 与数据层供应商中立,把「可重编译」变成一种迁移能力,而不是又一层依赖。

// 延伸阅读