Chapter 06

Optimizer:让 DSPy 帮你把 prompt 调到最好

优化器是 DSPy 的引擎。它读训练集,跑几百次 LLM,挑出最有效的示例和指令,把你的 Module 从"原型版"升级成"生产版"。

优化器全景

Optimizer优化的是成本适用
LabeledFewShot从训练集直接取 k 条当 demobaseline 对照
BootstrapFewShot让学生自己生成 demo(正确的才留)大多数场景
BootstrapFewShotWithRandomSearch上面 + 多次采样挑最佳组合预算充足
MIPROv2同时优化指令文本 + demo精度关键场景
BootstrapFinetune用编译过的 teacher 微调学生模型很高蒸馏到小/开源模型

准备数据

from dspy import Example

trainset = [
    Example(question="法国的首都?", answer="巴黎").with_inputs("question"),
    Example(question="H2O 是什么?", answer="水").with_inputs("question"),
    # ... 至少 20-50 条,多多益善
]
with_inputs 很关键
必须告诉 DSPy 哪几个字段是"输入",其余的才会当成 gold label 用来评分。

BootstrapFewShot:入门首选

from dspy.teleprompt import BootstrapFewShot

bootstrap = BootstrapFewShot(
    metric=exact_match,
    max_bootstrapped_demos=4,   # 最多用 4 个由 teacher 生成的 demo
    max_labeled_demos=16,      # 备选的人工 demo 个数
    max_rounds=2,              # 最多 2 轮 bootstrap
)

compiled = bootstrap.compile(student=RAG(), trainset=trainset)

原理

  1. 遍历 trainset 的每个样本,让 student 当前版本跑一遍
  2. 如果预测 + trace 能通过 metric → 收集起来作为候选 demo
  3. 把最多 k 个候选 demo 塞进 student 的每个 Predictor 的 few-shot 区
  4. 这就是"自助法":学生先跑对几道,就用这些题反过来教自己
什么时候用 teacher ≠ student
bootstrap.compile(student, teacher=strong_program, trainset=...) — 让 GPT-4o 做 teacher 产出 demo,再把 demo 塞给 GPT-4o-mini 做 student。成本降一半,精度掉得少。

BootstrapFewShotWithRandomSearch

from dspy.teleprompt import BootstrapFewShotWithRandomSearch

bootstrap_rs = BootstrapFewShotWithRandomSearch(
    metric=metric,
    max_bootstrapped_demos=4,
    max_labeled_demos=16,
    num_candidate_programs=10,   # 采样 10 套不同 demo 组合
    num_threads=8,
)

compiled = bootstrap_rs.compile(student=RAG(), trainset=trainset, valset=valset)

比 BootstrapFewShot 贵一个数量级,但验证集上通常能再涨 2-8%。

MIPROv2:state-of-the-art

from dspy.teleprompt import MIPROv2

mipro = MIPROv2(
    metric=metric,
    prompt_model=dspy.LM("openai/gpt-4o"),       # 专门负责生成候选指令
    task_model=dspy.LM("openai/gpt-4o-mini"),   # 执行任务的学生
    auto="medium",   # "light" | "medium" | "heavy"
)

compiled = mipro.compile(
    student=RAG(),
    trainset=trainset,
    valset=valset,
    requires_permission_to_run=False,
)

它在干什么

  1. Instruction 提议:prompt_model 看 task 描述和部分数据,生成 N 个候选指令
  2. Demo 引导:类似 Bootstrap,为每个 Predictor 收集候选 demo
  3. 贝叶斯优化:把 (指令, demo 组合) 看成超参,用 TPE 在 valset 上搜索
  4. 选最优:valset 分数最高的配置写回 student

auto="light"/"medium"/"heavy" 对应不同搜索步数,heavy 跑几小时很常见,但分数通常是所有优化器里最高的。

BootstrapFinetune:蒸馏到开源模型

from dspy.teleprompt import BootstrapFinetune

# 1) 先用强 teacher 编译一个高分程序
teacher = mipro.compile(RAG(), trainset=trainset, valset=valset)

# 2) 把 teacher 的高质量 trace 当训练数据,微调小模型
finetune = BootstrapFinetune(metric=metric, num_threads=8)

student = finetune.compile(
    teacher,
    trainset=trainset,
    target="meta-llama/Llama-3-8B-Instruct",
    epochs=3,
    lr=1e-5,
)

# student 现在是一个 LoRA 微调后的 Llama-3,调用成本 1/20

Optimizer 选择流程

你有多少预算? │ ├── 几美元 ──▶ BootstrapFewShot │ ├── 几十美元 ──▶ BootstrapFewShotWithRandomSearch │ │ │ └── 精度还不够? ──▶ MIPROv2(auto="light") │ ├── 几百美元 ──▶ MIPROv2(auto="medium" 或 "heavy") │ └── 几千美元 + 要上线开源模型 ──▶ MIPROv2 → BootstrapFinetune

关键超参解读

max_bootstrapped_demos
塞给 Predictor 的最大示例数。4-8 通常够,太多会挤压 context 还可能导致过拟合风格。
max_labeled_demos
从 trainset 直接取的"保底"示例数。当 bootstrap 失败时兜底。
num_threads
并发调 LLM 的线程数,直接决定你 rate-limit 撞墙的速度。gpt-4o-mini tier 1 建议 <= 8。
num_candidate_programs
RandomSearch 采样多少套组合。10-20 常见,上不封顶。

训练/验证/测试三集的划分

import random

random.seed(42)
random.shuffle(all_examples)
n = len(all_examples)
trainset = all_examples[: int(n*0.6)]
valset   = all_examples[int(n*0.6) : int(n*0.8)]
testset  = all_examples[int(n*0.8) :]

Optimizer 只看 train+val,testset 只在最后报告分数时用一次——不然你会得到一个"在 test 上调参的假象"。

成本控制 4 招

  1. 开缓存:默认 DSPy 的 LM 缓存就开着,relaunch 复用之前的调用
  2. 分层模型:prompt_model 用强模型,task_model 用便宜模型
  3. 小集合先试:拿 30 条跑 BootstrapFewShot 看看上限,再决定要不要 MIPRO
  4. dry run 先看成本:dspy.settings.track_usage=True 统计 token 花销

compile 后做什么

# 存
compiled.save("rag_v3.json")

# 换机器加载
prog = RAG()
prog.load("rag_v3.json")

# 看看优化器选了什么 demo / 什么指令
for name, pred in prog.named_predictors():
    print(name, pred.extended_signature.instructions)
    print(pred.demos)

本章小结