写了半年的Prompt,你有没有一种感觉:这玩意儿跟写正则一样——能用,但每次改都心惊胆战。改一个词,效果可能变好也可能崩掉,全靠直觉和手动测试。Stanford NLP的DSPy框架就是来解决这个问题的:把Prompt工程变成真正的软件工程。
为什么手写Prompt注定走不远
大部分LLM应用的开发流程是这样的:
- 写一个System Prompt
- 拿10个case手动测试
- 效果不好,加几条few-shot示例
- 还是不好,改措辞、调结构
- 换个模型,全部重来
问题的本质在于: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做了什么:
- 用你的训练数据跑一遍管道,自动收集成功的推理轨迹
- Bootstrap生成few-shot示例,选择效果最好的组合
- 把这些示例嵌入到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帮你把编排好的流程变得更好。
坑和注意事项
- 训练数据质量很重要:垃圾进垃圾出,bootstrap基于你的数据生成示例,数据质量差优化效果也好不了
- 不是万能药:简单任务直接用结构化输出就够了,DSPy的价值在复杂多步管道
- 编译时间不短:MIPROv2跑20轮实验可能要几分钟到几十分钟,不适合实时场景
- 调试不如手写直观:你看不到最终的Prompt长什么样(虽然可以
inspect_history查看) - 版本兼容问题:DSPy还在快速迭代,API变动频繁,锁定版本很重要
总结
DSPy的核心价值:把Prompt工程从"玄学调参"变成"可编译的软件工程"。你声明输入输出接口,框架自动找最优的Prompt策略。换模型、加功能,重新编译就行。
对于还在手动调Prompt的团队,DSPy值得认真评估。它不是替代LangChain,而是解决一个更底层的问题:怎样让LLM应用的性能可以系统性地提升,而不是靠碰运气。