LLM 应用开发的真实痛点
在真实项目里调 LLM,你一定遇到过这些问题:
- Prompt 脆弱:换一个词,精度从 85% 掉到 72%
- 不可迁移:GPT-4 调好的 prompt,拿到 Claude 就完全不灵
- 无法复用:三个业务场景,写了三套几乎一样但细节不同的 prompt
- 优化靠玄学:"加上 Let's think step by step 分数涨了 3 分"——不知道为什么,也不敢改
DSPy 的核心主张
不要再"写 prompt",而是"声明程序"。你定义模块的输入输出,DSPy 来把模块编译成具体的 prompt——就像 Python 代码由编译器变成字节码,你不用关心字节码长什么样。
不要再"写 prompt",而是"声明程序"。你定义模块的输入输出,DSPy 来把模块编译成具体的 prompt——就像 Python 代码由编译器变成字节码,你不用关心字节码长什么样。
传统 Prompt 工程 vs DSPy
传统写法:字符串堆砌
# 判断情感:好/中/差 prompt = f"""你是一个情感分析专家。请判断下面这段评论的情感。 规则: 1. 只输出 好、中、差 三个字之一 2. 不要输出多余内容 3. 如果文本模糊,选"中" 示例: 评论: 这个东西真好用,非常推荐 情感: 好 评论: {review} 情感: """ result = llm.complete(prompt).strip()
问题很多:
- 示例写死了,加一个就要改字符串
- 换模型(从 GPT-4 到 Llama-3)要重调整个 prompt
- 想加"先推理再给结论"的 CoT,要再改一次
- 想 A/B 测试哪个 prompt 更准,还要把 prompt 参数化
DSPy 写法:声明签名
import dspy dspy.configure(lm=dspy.LM("openai/gpt-4o-mini")) class Sentiment(dspy.Signature): """判断评论情感(好/中/差)""" review: str = dspy.InputField() label: Literal["好", "中", "差"] = dspy.OutputField() classify = dspy.Predict(Sentiment) result = classify(review="这个东西真好用,非常推荐") print(result.label) # "好"
差别一目了然:
- 没有字符串拼接,类型安全(Literal 保证只有三个值)
- 换模型只改
configure,不动业务代码 - 想升级到 CoT,一行:
dspy.ChainOfThought(Sentiment) - 可以
compile(),用训练集自动找最佳示例和提示
DSPy 三件套
| 概念 | 类比 | 作用 |
|---|---|---|
| Signature | 函数签名 | 声明"输入字段 → 输出字段",DSPy 据此生成 prompt |
| Module | 层(nn.Module) | 把 Signature 封装成可调用对象,可组合、可嵌套 |
| Optimizer | PyTorch Optimizer | 用训练数据和 metric 自动调整 Module 的提示与示例 |
流程图
┌─────────────┐ ┌─────────────┐ ┌────────────┐
│ Signature │ ───▶ │ Module │ ───▶ │ compiled │
│ (声明) │ │ (未优化) │ │ (已优化) │
└─────────────┘ └──────┬──────┘ └────────────┘
│
┌─────▼──────┐
│ Optimizer │◀── trainset + metric
└────────────┘
DSPy 能做到的四件"魔术"
1. 自动挑 few-shot 示例
from dspy.teleprompt import BootstrapFewShot bootstrap = BootstrapFewShot(metric=accuracy_metric, max_bootstrapped_demos=4) compiled = bootstrap.compile(student=classify, trainset=train)
给一批带标签的样本,DSPy 自己跑一轮,挑出"能让学生答对"的示例塞进 prompt——你甚至没写示例,DSPy 从数据里自己挖。
2. 自动优化指令文本
from dspy.teleprompt import MIPROv2 mipro = MIPROv2(metric=accuracy_metric, auto="light") compiled = mipro.compile(classify, trainset=train, valset=val)
MIPROv2 会 propose 多个候选指令(比如把"判断情感"改成"作为资深情感分析师,判断下列文本的情绪倾向"),用验证集挑最好的。
3. 多步骤程序统一优化
class MultiHop(dspy.Module): def __init__(self): self.retrieve = dspy.Retrieve(k=3) self.hop1 = dspy.ChainOfThought("context, question -> subquery") self.hop2 = dspy.ChainOfThought("context, question -> answer") def forward(self, question): ctx = self.retrieve(question).passages sub = self.hop1(context=ctx, question=question).subquery ctx2 = self.retrieve(sub).passages return self.hop2(context=ctx + ctx2, question=question)
这个 pipeline 里两个 hop 各自的 prompt 会被 DSPy 一起优化——手写 prompt 时这几乎是不可能的。
4. 蒸馏到小模型
from dspy.teleprompt import BootstrapFinetune # 先用 GPT-4o 编译出高质量 demo teacher = MultiHop(); teacher.set_lm(dspy.LM("openai/gpt-4o")) # 再让 Llama-3 8B 学 finetune = BootstrapFinetune(metric=accuracy, num_threads=8) student = finetune.compile(teacher, trainset=train, target="meta-llama/Llama-3-8B-Instruct")
成本降一个数量级,同时精度只降一点——生产里最实用的场景之一。
环境准备
pip install -U dspy # 可选依赖 pip install openai anthropic # 用商用模型 pip install transformers torch # 用本地 HF 模型 pip install chromadb qdrant-client # 用向量检索
import dspy import os # 方式 1:OpenAI 兼容 lm = dspy.LM("openai/gpt-4o-mini", api_key=os.environ["OPENAI_API_KEY"]) # 方式 2:Anthropic lm = dspy.LM("anthropic/claude-3-5-sonnet-20241022") # 方式 3:Ollama 本地 lm = dspy.LM("ollama_chat/llama3.1", api_base="http://localhost:11434", api_key="") dspy.configure(lm=lm)
DSPy 不适合做什么
诚实面对 DSPy 的边界
✗ 一次性的小脚本(开销太大,还不如写个 prompt 完事)
✗ 没有任何评估指标的任务(没有 metric 就没法 compile,DSPy 就是个普通框架)
✗ 强依赖完全自由生成的创作类(诗歌、剧本,优化器的方向感很弱)
✓ 分类、抽取、多跳 QA、RAG、Agent——有明确指标,就是 DSPy 的主场
✗ 一次性的小脚本(开销太大,还不如写个 prompt 完事)
✗ 没有任何评估指标的任务(没有 metric 就没法 compile,DSPy 就是个普通框架)
✗ 强依赖完全自由生成的创作类(诗歌、剧本,优化器的方向感很弱)
✓ 分类、抽取、多跳 QA、RAG、Agent——有明确指标,就是 DSPy 的主场
本章小结
- 手写 prompt 在生产里脆弱、难复用、无法跨模型迁移
- DSPy 用 Signature 声明任务、Module 组合逻辑、Optimizer 自动优化
- 核心能力:自动挑 few-shot、自动调指令、多步骤统一优化、蒸馏小模型
- 有评估指标的任务 DSPy 最强;无指标或纯创作不适合