高级 📋 7 个步骤 第 539 / 541 篇

用 DSPy 把提示词和 RAG 当参数自动优化:从手调到编译器

DSPy 把"提示词 + 工具 + RAG"当作可优化参数,用编译器在真实样本上自动搜索最优配置。本教程带你从装包、定义 Signature、写 Module,到用 BootstrapFewShot 与 MIPROv2 在验证集上自动优化,并接入你自己的 RAG 管线,每一步都可复制运行。

2026.10.17· 5 分钟上手· 约 1501 字· 🔧 DSPy / 🧪 提示词优化

你有没有过这种经历:提示词改了一版又一版,RAG 召回率还是不稳,换一个模型效果就崩?问题往往不是模型不行,而是提示词、示例、检索策略这些"软参数"全靠手调。DSPy 的思路是:把这些东西当成程序里的可优化参数,用一台"编译器"在真实样本上自动搜索最优配置。本教程从装包到接入你自己的 RAG 管线,每一步都能复制运行。

🧩 本教程适合:已经在用 LLM 做分类、抽取、问答或 RAG,想摆脱"手调提示词"的开发者。你需要 Python 3.10+、一个 OpenAI 或兼容的 API Key。

先搞懂:DSPy 不是在"写提示词",是在"声明接口"

传统做法是把提示词写成一段长字符串;DSPy 反过来——你只声明输入和输出长什么样(叫 Signature),再选一个推理模块(叫 Module),由 DSPy 在后台把 Signature 编译成具体提示词,并在你的样本上自动挑选 few-shot 示例、甚至重写指令。你的代码只描述"做什么",不写"怎么问"。

你写出什么DSPy 替你做什么
Signature:输入/输出字段生成提示词骨架
Module:推理流程决定调用几次模型、是否工具调用
Trainset / Devset自动选示例、调指令(teleprompt)

先把"模型名"和"版本"当变量。DSPy 的 LM 接口和模型代号随版本变动较快,本教程的模型字符串以你安装时官方文档为准,不要照抄就上线。

Step 1:安装 DSPy 并连上模型

1 一行装好并指定后端

DSPy 支持 OpenAI、Anthropic、本地 Ollama、Together 等后端。下面用 OpenAI 兼容接口举例:

pip install dspy-ai

# 在代码里接入模型(模型代号以官方文档为准)
import dspy
lm = dspy.LM("openai/gpt-4o-mini", api_key="sk-...")
dspy.configure(lm=lm)
💡 不想每次传 key,可以设环境变量 OPENAI_API_KEY,然后用 dspy.LM("openai/gpt-4o-mini") 让 SDK 自己读。换本地模型就用 dspy.LM("ollama/qwen2.5") 之类。

Step 2:用 Signature 声明输入输出契约

2 字段 + 文档字符串 = 提示词规格

Signature 就是"输入字段 → 输出字段",字段上的文档字符串会被 DSPy 用来生成指令。下面声明一个"给问题生成答案"的最简契约:

class BasicQA(dspy.Signature):
    """根据用户问题给出简洁准确的答案。"""

    question: str = dspy.InputField()
    answer: str = dspy.OutputField(desc="一句话答案,不确定就明说")
🔑 关键习惯:给输出字段写 desc。desc 会进提示词,约束格式比在正文里喊"请输出 JSON"稳得多。

Step 3:用 Module 把推理流程拼起来

3 Predict / ChainOfThought 选一个

最简单的 Module 是 dspy.Predict,本质就是"按 Signature 调一次模型"。想要模型先推理再答,换成 ChainOfThought:

qa = dspy.Predict(BasicQA)
# 或直接跑
result = qa(question="Claw 是什么?")
print(result.answer)

# 需要推理链时
cot = dspy.ChainOfThought(BasicQA)
print(cot(question="为什么天空是蓝的?").answer)

这一步只是"能跑",还没优化。现在用的都是零示例(zero-shot)提示词,效果随模型波动。真正的提升来自下一步的 teleprompt。

Step 4:准备训练集与验证集

4 二三十条样本就够起步

DSPy 不需要海量数据。准备一组"输入示例 + 期望输出或评分标准"即可。用 dspy.Example 描述,并标记哪些字段是答案:

trainset = [
    dspy.Example(question="Claw 是什么?", answer="一个 AI 智能体产品库").with_inputs("question"),
    dspy.Example(question="RAG 是什么?", answer="检索增强生成").with_inputs("question"),
]

def exact_match(example, pred, trace=None):
    return example.answer in pred.answer
💡 评分函数(metric)决定"什么是好结果"。分类用 exact match,生成类用 LLM 当评委(dspy.Evaluate 配一个评审 Signature)更合适。

Step 5:用 BootstrapFewShot 自动挑 few-shot 示例

5 让编译器替你选示例

BootstrapFewShot 会在训练集上跑一遍,把"模型答对的样本"自动塞进提示词当 few-shot 示例。这一步通常就能明显提分:

from dspy.teleprompt import BootstrapFewShot

teleprompter = BootstrapFewShot(metric=exact_match, max_bootstrapped_demos=4)
compiled_qa = teleprompter.compile(qa, trainset=trainset)

# 之后用 compiled_qa 替代 qa
print(compiled_qa(question="什么是 Agent?").answer)

编译用到的样本会进提示词。如果训练数据含敏感内容,few-shot 示例可能把私密信息带进上下文,生产前请脱敏或只用合成样本。

Step 6:用 MIPROv2 自动搜索指令与示例(进阶)

6 一并调指令和示例

MIPROv2 比 BootstrapFewShot 更进一步:它同时搜索"指令文案"和"few-shot 示例组合",在验证集上用贝叶斯式搜索找更优配置。适合你已经有稍大的 devset:

from dspy.teleprompt import MIPROv2

optimizer = MIPROv2(
    metric=exact_match,
    auto="light",          # light / medium / heavy 控制搜索强度
    num_candidates=8,
)
compiled = optimizer.compile(
    qa,
    trainset=trainset,
    valset=trainset,       # 小样本时训练验证同源,数据多时分开
)
compiled.save("compiled_qa.json")
🔑 编译结果可以用 compiled.save / load 落盘,上线时直接 load,不必每次重跑优化。auto="light" 几分钟就能出结果,先跑 light 看收益。

Step 7:把优化后的程序接到你的 RAG 管线

7 Module 里调用检索,再让编译器调检索策略

RAG 场景下,把"检索"写成 Module 里的一步,Signature 的输入再加一个 context 字段,就能让 DSPy 连检索提示词一起优化:

class RAG(dspy.Module):
    def __init__(self, n_passages=3):
        self.retrieve = dspy.Retrieve(k=n_passages)
        self.generate = dspy.ChainOfThought("context, question -> answer")

    def forward(self, question):
        context = self.retrieve(question).passages
        return self.generate(context=context, question=question)

rag = RAG()
compiled_rag = BootstrapFewShot(metric=exact_match).compile(rag, trainset=trainset)

检索质量才是天花板。DSPy 优化的是"怎么用检索结果",不能补回召回本身的问题。先把向量库和切分调好,再让编译器接管提示词层。

常见问题速查

现象原因与处理
模型不调用工具 / 不输出约定字段Signature 字段 desc 太模糊;把 OutputField 的 desc 写具体
编译后反而变差训练样本太少或有噪声;先扩到几十条干净样本
想换模型后效果崩重新 compile 一次,不要直接复用旧示例
← 返回教程中心