写了半年的Prompt,你有没有一种感觉:这玩意儿跟写正则一样——能用,但每次改都心惊胆战。改一个词,效果可能变好也可能崩掉,全靠直觉和手动测试。Stanford NLP的DSPy框架就是来解决这个问题的:把Prompt工程变成真正的软件工程。

为什么手写Prompt注定走不远

大部分LLM应用的开发流程是这样的:

  1. 写一个System Prompt
  2. 拿10个case手动测试
  3. 效果不好,加几条few-shot示例
  4. 还是不好,改措辞、调结构
  5. 换个模型,全部重来

问题的本质在于:Prompt是不可组合、不可迁移、不可自动优化的字符串拼接。你换一个模型,或者加一个新功能,之前调好的Prompt可能瞬间失效。

DSPy的核心思想:把Prompt从"字符串"变成"可编译的程序模块"

DSPy的核心概念

先搞清楚几个关键概念:

import dspy

# 1. 配置LLM(DSPy的统一接口,底层支持OpenAI/Anthropic/本地模型)
lm = dspy.LM("openai/gpt-4o-mini", api_key="sk-xxx")
dspy.configure(lm=lm)

# 2. Signature(签名):声明输入输出,而不是写Prompt
class Sentiment(dspy.Signature):
    """判断文本的情感倾向"""
    text: str = dspy.InputField()
    sentiment: str = dspy.OutputField(desc="positive, negative, or neutral")

# 3. Module(模块):可复用的推理单元,替代硬编码的Prompt Chain
classify = dspy.Predict(Sentiment)

# 使用
result = classify(text="这个产品太好用了!")
print(result.sentiment)  # positive

对比一下传统写法:

# ❌ 传统方式:Prompt硬编码,换模型就废了
prompt = f"""请判断以下文本的情感倾向,只返回positive、negative或neutral三个词之一。
文本:{text}
情感:"""
response = openai.chat(prompt)

# ✅ DSPy方式:声明式签名,框架自动编排
classify = dspy.Predict(Sentiment)
result = classify(text=text)

区别在哪?传统方式你写的是字符串,DSPy方式你写的是接口声明。Prompt的具体措辞让框架去生成和优化。

构建一个RAG管道

用DSPy写RAG,比LangChain简洁得多:

import dspy

lm = dspy.LM("openai/gpt-4o-mini")
dspy.configure(lm=lm)

# 假设有一个向量检索器(DSPy支持接入任何检索后端)
from dspy.retrieve.chromadb_rm import ChromadbRM

retriever = ChromadbRM(
    collection_name="docs",
    persist_path="./chroma_db",
    embedding_function=dspy.SentenceTransformersEmbeddings("BAAI/bge-base-zh"),
)

# 定义RAG签名
class RAG(dspy.Signature):
    """根据检索到的上下文回答问题"""
    context: list[str] = dspy.InputField(desc="检索到的相关文档片段")
    question: str = dspy.InputField()
    answer: str = dspy.OutputField()

# 定义RAG模块
class RAGModule(dspy.Module):
    def __init__(self):
        self.retrieve = dspy.Retrieve(k=5)
        self.generate = dspy.ChainOfThought(RAG)

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

rag = RAGModule()
result = rag(question="什么是向量数据库?")
print(result.answer)

到目前为止看起来没什么特别的——直到你开始编译。

编译:DSPy的杀手级特性

这是DSPy真正牛的地方。你提供一组训练数据(甚至不需要标注),DSPy自动帮你优化整个管道:

from dspy.teleprompt import BootstrapFewShot

# 定义评估指标
def validate_answer(example, prediction, trace=None):
    """简单的评估函数:答案不为空且包含关键词"""
    has_answer = len(prediction.answer.strip()) > 10
    return has_answer

# 准备训练数据(只需要问题+可选的答案)
trainset = [
    dspy.Example(question="什么是向量数据库?", answer="向量数据库...").with_inputs("question"),
    dspy.Example(question="Redis怎么用?", answer="Redis是一个...").with_inputs("question"),
    dspy.Example(question="Python异步编程是什么?", answer="async/await...").with_inputs("question"),
    # 10-20个例子就够
]

# 编译:自动优化few-shot示例的选择和排列
compiler = BootstrapFewShot(metric=validate_answer, max_bootstrapped_demos=4)
optimized_rag = compiler.compile(RAGModule(), trainset=trainset)

# 优化后的模型直接使用,Prompt已经被框架自动调整
result = optimized_rag(question="什么是Kubernetes?")

编译过程中DSPy做了什么:

  1. 用你的训练数据跑一遍管道,自动收集成功的推理轨迹
  2. Bootstrap生成few-shot示例,选择效果最好的组合
  3. 把这些示例嵌入到Prompt中,替换掉你手写的few-shot

你换一个模型,重新compile一下就行。Prompt格式、few-shot策略全部由框架适配。

更强的优化器:MIPROv2

BootstrapFewShot只是入门。生产环境推荐用MIPROv2,它能同时优化指令文本和few-shot示例:

from dspy.teleprompt import MIPROv2

# 准备验证集
devset = [
    dspy.Example(question="什么是Docker?", answer="容器化技术...").with_inputs("question"),
    # 更多验证数据...
]

# MIPROv2:自动优化指令+示例
optimizer = MIPROv2(
    metric=validate_answer,
    num_candidates=10,       # 生成10个候选指令
    init_temperature=1.0,
    verbose=True,
)

optimized = optimizer.compile(
    RAGModule(),
    trainset=trainset,
    num_trials=20,           # 20轮优化实验
    max_bootstrapped_demos=4,
    max_labeled_demos=2,
)

# 保存优化后的模型
optimized.save("optimized_rag.json")

# 加载
loaded = RAGModule()
loaded.load("optimized_rag.json")

MIPROv2的优化流程:

训练数据 → Bootstrap收集成功轨迹 → 生成候选指令集
                                    ↓
            Bayes优化 ← 评估每组指令+示例在验证集上的表现
                                    ↓
                        输出最优的指令+示例组合

多步推理:Chain of Thought + ReAct

复杂任务需要多步推理。DSPy提供了现成的模块:

# Chain of Thought:自动加"Let's think step by step"
class MathQA(dspy.Signature):
    """解决数学应用题"""
    question: str = dspy.InputField()
    reasoning: str = dspy.OutputField(desc="推理过程")
    answer: str = dspy.OutputField(desc="最终数值答案")

solver = dspy.ChainOfThought(MathQA)
result = solver(question="小明有5个苹果,给了小红2个,又买了3个,现在有几个?")
print(result.reasoning)  # 小明开始有5个,给小红2个后剩3个,又买了3个,共6个
print(result.answer)     # 6

# ReAct:带工具调用的推理
class SearchAgent(dspy.Signature):
    """搜索并回答问题"""
    question: str = dspy.InputField()
    answer: str = dspy.OutputField()

def search_wikipedia(query: str) -> list[str]:
    """搜索维基百科"""
    # 实际实现接入Wikipedia API
    return ["相关文档片段..."]

agent = dspy.ReAct(SearchAgent, tools=[search_wikipedia])
result = agent(question="图灵测试是谁提出的?")

生产环境实战:客服意图分类系统

拿一个真实的例子——客服意图分类,对比传统方式和DSPy方式:

import dspy
from pydantic import BaseModel
from enum import Enum
from typing import Optional

# 定义意图类别
class Intent(str, Enum):
    ORDER_STATUS = "订单查询"
    REFUND = "退款申请"
    TECH_SUPPORT = "技术支持"
    COMPLAINT = "投诉建议"
    CHITCHAT = "闲聊"

class IntentClassification(dspy.Signature):
    """客服意图分类器:根据用户消息判断意图类别"""
    message: str = dspy.InputField(desc="用户发送的消息")
    intent: str = dspy.OutputField(desc="意图类别,从以下选择:订单查询、退款申请、技术支持、投诉建议、闲聊")
    confidence: float = dspy.OutputField(desc="置信度,0-1之间")

class CustomerServiceClassifier(dspy.Module):
    def __init__(self):
        self.classify = dspy.ChainOfThought(IntentClassification)

    def forward(self, message):
        result = self.classify(message=message)
        return result

# 准备训练数据
trainset = [
    dspy.Example(message="我的订单到哪了", intent="订单查询").with_inputs("message"),
    dspy.Example(message="买了三天还没发货", intent="订单查询").with_inputs("message"),
    dspy.Example(message="我要退货", intent="退款申请").with_inputs("message"),
    dspy.Example(message="东西坏了能退吗", intent="退款申请").with_inputs("message"),
    dspy.Example(message="App打不开闪退", intent="技术支持").with_inputs("message"),
    dspy.Example(message="你们服务太差了", intent="投诉建议").with_inputs("message"),
    dspy.Example(message="今天天气不错", intent="闲聊").with_inputs("message"),
    # ... 更多训练数据
]

# 评估指标
def evaluate(example, prediction, trace=None):
    return example.intent == prediction.intent

# 编译优化
from dspy.teleprompt import BootstrapFewShot
optimizer = BootstrapFewShot(metric=evaluate, max_bootstrapped_demos=3)
classifier = optimizer.compile(CustomerServiceClassifier(), trainset=trainset)

# 测试
test_messages = [
    "快递怎么还没到",
    "我要退款退货",
    "系统登不上去了",
    "你们公司真垃圾",
    "哈哈今天心情好",
]

for msg in test_messages:
    result = classifier(message=msg)
    print(f"消息: {msg}")
    print(f"意图: {result.intent} (置信度: {result.confidence})")
    print("---")

实测数据(1000条客服消息):

方案 准确率 耗时 维护成本
手写Prompt 82.3% 每次换模型需重写
DSPy + BootstrapFewShot 89.7% recompile即可
DSPy + MIPROv2 93.1% recompile即可
微调BERT 94.5% 需GPU训练

DSPy的性价比极高:不需要GPU,不需要标注海量数据,效果接近微调,维护成本比手写Prompt低得多。

与现有框架的对比

维度 手写Prompt LangChain DSPy
学习曲线 中高
可组合性 链式 模块化
自动优化
模型迁移 重写 部分适配 recompile
调试能力 打印 LangSmith 内置追踪
生产稳定性

DSPy的学习曲线确实比LangChain陡,但它解决的是LangChain没解决的问题:自动优化。LangChain帮你编排流程,DSPy帮你把编排好的流程变得更好。

坑和注意事项

  1. 训练数据质量很重要:垃圾进垃圾出,bootstrap基于你的数据生成示例,数据质量差优化效果也好不了
  2. 不是万能药:简单任务直接用结构化输出就够了,DSPy的价值在复杂多步管道
  3. 编译时间不短:MIPROv2跑20轮实验可能要几分钟到几十分钟,不适合实时场景
  4. 调试不如手写直观:你看不到最终的Prompt长什么样(虽然可以inspect_history查看)
  5. 版本兼容问题:DSPy还在快速迭代,API变动频繁,锁定版本很重要

总结

DSPy的核心价值:把Prompt工程从"玄学调参"变成"可编译的软件工程"。你声明输入输出接口,框架自动找最优的Prompt策略。换模型、加功能,重新编译就行。

对于还在手动调Prompt的团队,DSPy值得认真评估。它不是替代LangChain,而是解决一个更底层的问题:怎样让LLM应用的性能可以系统性地提升,而不是靠碰运气