Chapter 01

为什么需要 DSPy:从 Prompt 工程到 Prompt 编程

手写 prompt 是手工业,DSPy 是工业化。用签名声明任务,让编译器替你挑示例、调提示、甚至换模型——这章讲清楚 DSPy 到底解决了什么问题。

LLM 应用开发的真实痛点

在真实项目里调 LLM,你一定遇到过这些问题:

DSPy 的核心主张
不要再"写 prompt",而是"声明程序"。你定义模块的输入输出,DSPy 来把模块编译成具体的 prompt——就像 Python 代码由编译器变成字节码,你不用关心字节码长什么样。

传统 Prompt 工程 vs DSPy

传统写法:字符串堆砌

# 判断情感:好/中/差
prompt = f"""你是一个情感分析专家。请判断下面这段评论的情感。
规则:
1. 只输出 好、中、差 三个字之一
2. 不要输出多余内容
3. 如果文本模糊,选"中"

示例:
评论: 这个东西真好用,非常推荐
情感: 好

评论: {review}
情感: """

result = llm.complete(prompt).strip()

问题很多:

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)   # "好"

差别一目了然:

DSPy 三件套

概念类比作用
Signature函数签名声明"输入字段 → 输出字段",DSPy 据此生成 prompt
Module层(nn.Module)把 Signature 封装成可调用对象,可组合、可嵌套
OptimizerPyTorch 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 的主场

本章小结