你有没有过这种经历:提示词改了一版又一版,RAG 召回率还是不稳,换一个模型效果就崩?问题往往不是模型不行,而是提示词、示例、检索策略这些"软参数"全靠手调。DSPy 的思路是:把这些东西当成程序里的可优化参数,用一台"编译器"在真实样本上自动搜索最优配置。本教程从装包到接入你自己的 RAG 管线,每一步都能复制运行。
先搞懂:DSPy 不是在"写提示词",是在"声明接口"
传统做法是把提示词写成一段长字符串;DSPy 反过来——你只声明输入和输出长什么样(叫 Signature),再选一个推理模块(叫 Module),由 DSPy 在后台把 Signature 编译成具体提示词,并在你的样本上自动挑选 few-shot 示例、甚至重写指令。你的代码只描述"做什么",不写"怎么问"。
| 你写出什么 | DSPy 替你做什么 |
|---|---|
| Signature:输入/输出字段 | 生成提示词骨架 |
| Module:推理流程 | 决定调用几次模型、是否工具调用 |
| Trainset / Devset | 自动选示例、调指令(teleprompt) |
先把"模型名"和"版本"当变量。DSPy 的 LM 接口和模型代号随版本变动较快,本教程的模型字符串以你安装时官方文档为准,不要照抄就上线。
Step 1:安装 DSPy 并连上模型
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)
Step 2:用 Signature 声明输入输出契约
Signature 就是"输入字段 → 输出字段",字段上的文档字符串会被 DSPy 用来生成指令。下面声明一个"给问题生成答案"的最简契约:
class BasicQA(dspy.Signature):
"""根据用户问题给出简洁准确的答案。"""
question: str = dspy.InputField()
answer: str = dspy.OutputField(desc="一句话答案,不确定就明说")
Step 3:用 Module 把推理流程拼起来
最简单的 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:准备训练集与验证集
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
Step 5:用 BootstrapFewShot 自动挑 few-shot 示例
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 自动搜索指令与示例(进阶)
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")
Step 7:把优化后的程序接到你的 RAG 管线
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 一次,不要直接复用旧示例 |