之前在业务迭代里做客户评论分析模块时,我踩了不少多阶段 LLM 应用的坑:第一阶段的情绪判断只是换个说法,第二阶段的要点抽取结果就会跟着漂移;手工维护的 Prompt 攒了十几个,改了一个反而影响另一个环节。后来把整套流程迁移到 DSPy 上,把每个处理环节定义成独立的签名和模块,再交给优化器自动挑选提示词和示例,整体效果和可维护性都有了明显提升。
这篇文章就基于这套重构经验,完整拆解一条基于 DSPy 的级联流水线(Cascading Pipeline)从设计、建模、编译到评估的过程。适合已经写过 ChatGPT 或大模型 API 调用、想系统化组织多阶段 LLM 任务的开发者;也适合使用 LangChain 等框架,但对 Prompt 调试和可维护性有困惑的同学。读完你会掌握 DSPy 的 Signature、Module、Optimizer、Metric 四个核心概念,拿到一条可以直接改用的二级到三级流水线代码,并且能按文中的排查清单处理大多数常见问题。
1. DSPy 是什么,为什么级联流水线值得关注
1.1 多阶段任务的真实痛点
先看一个常见的多阶段任务:客服工单系统拿到一条用户反馈,“快递晚了两天,也没人解释,包装倒是完整”。如果只调用一次大模型,让它同时输出情感、问题类别、责任方和处理建议,Prompt 会变得非常长,而且每个字段的准确性互相制约。更麻烦的是,线上数据分布一变化,整个长 Prompt 就可能要重新调一遍。
如果拆成多个独立 Prompt 串行调用,又会遇到几个新问题:
- 中间结果格式不稳定,下一阶段很难解析;
- 每一级 Prompt 都是手工维护,改动成本很高;
- 出了问题很难定位,不知道错误到底来自哪一级;
- 没有统一机制为每个阶段自动生成“最优示例”。
DSPy 的设计目标就是解决这些问题。它不是又一种 Prompt 模板框架,而是把“用语言模型编程”这件事抽象成一套可编程、可组合、可自动优化的范式。开发者只负责描述任务的目标输入输出,剩下的提示词构造和示例选择交给框架完成。
1.2 DSPy 的核心思想
DSPy 由斯坦福 NLP 团队开源,核心思路是:不要让开发者手写一段又一段的长 Prompt,而是把任务声明为“签名(Signature)”,把处理逻辑封装成“模块(Module)”,然后交给“优化器(Optimizer,旧版本里称为 Teleprompter)”,由优化器在给定数据集和评估指标的基础上,自动寻找更好的提示词组合。
一句话概括:传统 Prompt 工程是人去找 Prompt,DSPy 是机器基于数据去找 Prompt。这个差异非常关键,因为它改变了调优方式。手写 Prompt 时,你只能凭经验反复试;DSPy 优化器则会在签名约束下搜索指令和示例,结果更可控,也更容易迁移到新任务上。
1.3 级联流水线的典型场景
级联流水线指多个 DSPy 模块按顺序执行,前一个模块的输出作为后一个模块的部分输入。典型场景包括:
- RAG 类应用:查询改写 → 检索 → 重排 → 生成 → 校验;
- 内容分析系统:分类 → 要素抽取 → 摘要 → 建议生成;
- 自动化报告:数据清洗 → 指标计算 → 文案撰写 → 格式转换;
- 智能客服:意图识别 → 槽位抽取 → 回答生成 → 安全过滤。
这类任务的共同特征是:单个模型调用很难一次做好,或者业务需要中间产物用于追踪、审核和审计。级联的价值在于把复杂任务拆成小任务,小任务的签名更清晰,优化器也更容易为每个小任务找到稳定提示词。这也是标题里说“kind of good”的原因:级联并不是银弹,但在需要结构化和可控性的场景下,它的工程收益通常大于维护成本。
2. 环境准备与版本说明
2.1 安装 DSPy
本文示例的运行环境如下,版本需要根据你的项目实际情况调整,重点演示配置思路。
| 组件 | 说明 |
|---|---|
| Python | 建议 3.9 及以上版本 |
| DSPy | 以 2.x 版本为例 |
| LLM | OpenAI 兼容接口,本文用 gpt-4o-mini 示例 |
| 操作系统 | macOS / Linux / Windows 均可 |
安装命令:
python -m venv .venv source .venv/bin/activate # Windows 使用 .venv\Scripts\activate pip install -U dspy历史版本中库名曾叫 dspy-ai,新版本统一为 dspy。如果你的环境已经装过旧包,建议先卸载再安装,避免 import 时出现路径冲突。
2.2 配置语言模型
DSPy 需要配置一个默认语言模型。最基础的方式是直接传入 OpenAI 兼容的模型和密钥:
import dspy lm = dspy.OpenAI( model="gpt-4o-mini", api_key="sk-你的密钥", temperature=0, ) dspy.configure(lm=lm)如果安装的是较新版本,也可以写成dspy.LM(model="openai/gpt-4o-mini", api_key="sk-..."),具体写法请以当前版本的官方文档为准。除了 OpenAI,还可以通过 OpenAI 兼容接口接入 DeepSeek、通义千问、智谱等国内模型服务,不同 DSPy 版本对api_base参数的支持方式有差异,实际配置时建议先用一个最小调用验证连通性,再进入完整开发。
2.3 示例项目结构
为了便于阅读,示例项目按功能拆分为以下文件:
customer_review_pipeline/ ├── main.py # 主入口:训练、编译、评估、推理 ├── pipeline.py # 签名与模块定义 ├── data.py # 样例数据 └── metrics.py # 验证指标小项目直接合并成单文件也可以,拆分只是为了说明每一层代码的职责。后面的讲解会按文件路径给出完整代码。
3. DSPy 核心概念拆解
3.1 Signature:声明式输入输出
Signature 是 DSPy 的最小任务描述单位,用来定义“输入什么、输出什么”。它由 InputField 和 OutputField 组成,类的 docstring 会被框架当作整体任务说明。看一个最简单的例子:
class ReviewTone(dspy.Signature): """判断一条用户评论的情感倾向。""" review = dspy.InputField(desc="要分析的评论原文") tone = dspy.OutputField(desc="只能返回:正面 / 中性 / 负面")这里的重点是 desc。desc不是摆设,它会进入自动生成的 Prompt,是优化器调整提示词的基础。字段描述写得越具体,模型和优化器越不容易跑偏。比如输出字段如果只写“情感”,模型可能返回“比较好”“一般般”这类描述;写成“只能返回:正面 / 中性 / 负面”,输出空间就被约束住了。
需要注意,Signature 并不是 Python dataclass,它的字段只是声明格式,真正调用时由 DSPy 转换为基础模型的输入输出参数。所以不要试图在里面写复杂校验逻辑,校验应该放到后面的 Metric 部分。
3.2 Module:可组合的处理单元
Module 是具体的执行单元,封装一个或多个 Signature 调用。简单场景可以直接用dspy.Predict或dspy.ChainOfThought,复杂场景则继承dspy.Module并实现forward。
class ToneStage(dspy.Module): def __init__(self): super().__init__() self.tone = dspy.Predict(ReviewTone) def forward(self, review): result = self.tone(review=review) return dspy.Prediction(tone=result.tone)dspy.Predict是直接根据签名生成 Prompt 并输出结果;dspy.ChainOfThought则在任务复杂时让模型先输出一段推理过程再给出最终答案,相当于自动添加了“Let's think step by step”对应机制,适合抽取、判断、生成等需要中间推理的任务。两者返回的都是 Prediction 对象,可以直接访问签名中声明的字段名。
Module 的意义在于复用和组合。一个级联流水线本质上就是多个 Module 像函数一样被串起来,每个forward的输入输出可以作为下一级的输入。
3.3 Optimizer:自动优化提示词
Optimizer 是 DSPy 最核心的模块,负责在给定训练集和评估指标的基础上,自动生成并挑选提示词示例组合。它不需要开发者手工写 few-shot 示例,而是从标注数据中引导生成。
optimizer = dspy.teleprompt.BootstrapFewShot( metric=my_metric, max_bootstrapped_demos=4, ) optimized_pipeline = optimizer.compile( student=my_pipeline, trainset=trainset, )BootstrapFewShot会先用基础 Pipeline 在训练集上尝试运行,把预测正确的路径追出来,再把这些路径中的输入输出作为示例放进提示词。较新的版本还提供了 MIPROv2 这类指令级优化器,它会在指令文本和示例两方面同时搜索。不同版本中优化器的导入路径可能有变化,旧版本是dspy.teleprompt.*,新版本可能迁移到dspy.optimizers.*,运行时以实际安装版本的 import error 提示为准即可。
3.4 Metric:优化器眼中的“对错”
优化器不能凭空判断好坏,它必须依赖一个可执行的评估函数 Metric。Metric 的输入通常有两个:一个是训练集或开发集里的 Example,一个是 Pipeline 输出的 Prediction。返回 True 表示该条数据预测正确。
def simple_metric(example, pred, trace=None): return pred.tone == example.tone简单任务可以直接做字符串比对,复杂任务可以用 LLM-as-judge,即再写一个 Signature 让模型判断回答质量。Metric 的选择直接决定优化方向:如果只看情感字段,优化器就不会优化问题抽取;如果同时检查多个字段,优化器才会朝着“全字段正确”的方向努力。所以实战中 Metric 要尽量贴合业务验收标准。
4. 完整实战:客户评论分析级联流水线
4.1 需求拆解与流水线设计
现在开始搭建一条三级级联流水线,任务是:给定一条客户评论,输出情感倾向、问题要点和客服回复建议。
阶段设计如下:
| 阶段 | 输入 | 输出 | 目标 |
|---|---|---|---|
| 情感判断 | 评论原文 | sentiment、confidence | 判断整体情感倾向 |
| 问题抽取 | 评论原文 + sentiment | issues | 列出具体问题要点 |
| 回复生成 | 评论原文 + sentiment + issues | reply | 生成面向客户的回复 |
顺序有明确理由:情感是全局信息,先算出来能给后续抽取提供聚焦方向;问题要点是结构化中间产物,直接喂给回复生成,能避免大模型凭空总结原文。流程可以用下面的 ASCII 图表示:
review -> [情感判断] -> sentiment/confidence | v [问题抽取] <- sentiment | v [回复生成] <- sentiment + issues | v reply4.2 定义签名
文件路径:pipeline.py
import dspy class SentimentAnalyze(dspy.Signature): """分析一条客户评论的整体情感倾向。""" review = dspy.InputField(desc="客户评论原文,可能是混杂语气的中文短文本") sentiment = dspy.OutputField(desc="情感倾向,只能从以下三个值中选择:正面 / 中性 / 负面") confidence = dspy.OutputField(desc="对该判断的置信度,取值范围 0.0 到 1.0") class IssueExtract(dspy.Signature): """从客户评论中抽取具体问题要点。""" review = dspy.InputField(desc="客户评论原文") sentiment = dspy.InputField(desc="第一步得到的情感倾向") issues = dspy.OutputField(desc="问题要点,多个要点用中文分号分隔,按重要性从大到小排列,每条不超过 20 字") class ReplySuggest(dspy.Signature): """根据情感倾向和问题要点生成面向客户的回复。""" review = dspy.InputField(desc="客户评论原文") sentiment = dspy.InputField(desc="情感倾向") issues = dspy.InputField(desc="问题要点列表") reply = dspy.OutputField(desc="客服回复,语气诚恳具体,先道歉或共情,再说明处理动作,不超过 150 字")注意issues的输出被定义成用分号分隔的字符串。这样做是为了保证解析稳定,后续无论模型输出列表还是字符串,我们都能用统一方式处理。
4.3 实现模块并组装流水线
文件路径:pipeline.py
class SentimentModule(dspy.Module): def __init__(self): super().__init__() self.analyze = dspy.ChainOfThought(SentimentAnalyze) def forward(self, review): out = self.analyze(review=review) return out class IssueModule(dspy.Module): def __init__(self): super().__init__() self.extract = dspy.ChainOfThought(IssueExtract) def forward(self, review, sentiment): out = self.extract(review=review, sentiment=sentiment) return out class ReviewPipeline(dspy.Module): def __init__(self): super().__init__() self.sentiment_stage = SentimentModule() self.issue_stage = IssueModule() self.reply_stage = dspy.ChainOfThought(ReplySuggest) def forward(self, review): sentiment_out = self.sentiment_stage(review=review) issue_out = self.issue_stage( review=review, sentiment=sentiment_out.sentiment, ) reply_out = self.reply_stage( review=review, sentiment=sentiment_out.sentiment, issues=issue_out.issues, ) return dspy.Prediction( sentiment=sentiment_out.sentiment, confidence=sentiment_out.confidence, issues=issue_out.issues, reply=reply_out.reply, )这里展示了级联的核心模式:前一个forward的返回值作为后一个forward的参数。ReviewPipeline本身也是一个 Module,因此它可以继续被上层组合,也可以直接交给优化器编译。如果后续要加“内容审核”“敏感信息过滤”等阶段,只需要在forward中插入新模块,签名保持独立即可。
4.4 构造训练数据
文件路径:data.py
import dspy TRAIN = [ dspy.Example( review="快递晚了两天,客服也没解释原因,包装倒是完好。", sentiment="负面", issues="物流延误;客服未答复", ).with_inputs("review"), dspy.Example( review="音质不错,低音很足,就是戴久了耳朵疼。", sentiment="正面", issues="佩戴舒适度一般", ).with_inputs("review"), dspy.Example( review="客服态度很好,问题当天就解决了,点赞。", sentiment="正面", issues="无", ).with_inputs("review"), dspy.Example( review="买了两件衣服,颜色和图片差太多,申请退款还被驳回。", sentiment="负面", issues="色差严重;退款被拒", ).with_inputs("review"), ]with_inputs("review")声明了推理时只需要传入 review 字段,sentiment 和 issues 只作为标注出现在训练阶段。DSPy 不会要求训练数据量很大,十几条高质量样本就能让 BootstrapFewShot 跑起来;但样本越多、覆盖越广,优化后的稳定性越好。
4.5 编写验证指标
文件路径:metrics.py
def policy_metric(example, pred, trace=None): # 1) 情感必须完全匹配 if pred.sentiment != example.sentiment: return False # 2) 参考问题要点应出现在预测要点中 pred_issues = [ p.strip() for p in pred.issues.replace(";", ";").split(";") if p.strip() ] for expected in example.issues.replace(";", ";").split(";"): if expected.strip() == "无": continue if not any(expected.strip()[:4] in p for p in pred_issues): return False # 3) 回复不应过短,且应包含处理动作 if len(pred.reply) < 20: return False action_words = ["抱歉", "核实", "处理", "补偿", "反馈", "优化", "跟进"] if not any(w in pred.reply for w in action_words): return False return True这个指标把业务验收标准具体化了:情感字段必须正确、问题要点必须覆盖、回复必须足够长且有处理动作。如果真实业务有更严格的标准,比如不能出现敏感词、回复必须包含工单号,都应该写进 metric。指标不是可有可无的装饰,它决定了优化器会把 Pipeline 推向哪个方向。
4.6 编译优化流水线
文件路径:main.py
import dspy from dspy.teleprompt import BootstrapFewShot from pipeline import ReviewPipeline from data import TRAIN from metrics import policy_metric optimizer = BootstrapFewShot( metric=policy_metric, max_bootstrapped_demos=4, max_labeled_demos=4, ) compiled_pipeline = optimizer.compile( student=ReviewPipeline(), trainset=TRAIN, )编译过程会真实调用语言模型,因此需要消耗一定的 token 和等待时间。编译完成后,compiled_pipeline就是一条“带优化提示词”的级联流水线,可以直接用于推理。如果安装版本中dspy.teleprompt路径不可用,可以尝试from dspy.optimizers import BootstrapFewShot,参数基本一致。
4.7 运行验证与结果对比
先用一条开发集评论测试编译后的流水线:
result = compiled_pipeline( review="收到的杯子有划痕,客服说可以换,但要我自己出邮费。" ) print("情感:", result.sentiment) print("置信度:", result.confidence) print("问题要点:", result.issues) print("回复:", result.reply)输出效果类似如下(不同模型和版本下可能略有差异):
情感: 负面 置信度: 0.95 问题要点: 商品有划痕;退换货运费争议 回复: 非常抱歉给您带来不好的体验,我们会立即核实商品划痕情况,并为您协调处理退换货运费问题,请保持联系方式畅通。为了验证优化是否真的有效,可以在开发集上分别评估未编译和编译后的 Pipeline:
from dspy.evaluate import Evaluate DEV = [ dspy.Example( review="鞋子偏小半码,客服推荐换大一码,其他都还可以。", sentiment="中性", issues="尺码偏小", ).with_inputs("review"), # 更多开发集样本... ] evaluator = Evaluate(metric=policy_metric, devset=DEV, num_threads=4) base_score = evaluator(ReviewPipeline()) compiled_score = evaluator(compiled_pipeline) print("未编译得分:", base_score) print("编译后得分:", compiled_score)在大多数任务中,编译后得分都会优于未编译版本。这并不意味着 DSPy 给模型增加了神秘能力,而是优化器从训练数据中挑选出了更合适的示例,并把这些示例组织成了更稳定的提示词结构。如果你遇到得分没有提升的情况,优先检查指标定义、训练数据质量和级联层级数量,而不是急着换更大的模型。
5. 常见问题与排查思路
级联 Pipeline 的报错往往不复杂,难的是定位错误到底发生在链路哪一段。下面整理了一份高频问题表。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 导入优化器时报 ImportError | DSPy 版本升级后模块路径变化 | 尝试from dspy.optimizers import BootstrapFewShot,或统一升级/降级版本 |
| 调用大模型返回 401 Invalid API key | 密钥配置错误或环境变量未生效 | 检查 api_key 参数,确认没有多余空格;优先使用环境变量注入 |
| 访问 pred.issues 报 AttributeError | 签名输出字段名与模块返回字段名不一致 | 检查 OutputField 名称,字段名必须大小写完全一致 |
| 编译后效果反而变差 | Metric 定义不准确、训练样本太少或分布单一 | 扩充并清洗训练集,简化 Metric,减少级联层级 |
| 流水线响应太慢 | 每一级都调用大模型,链路过长 | 合并可并行阶段,分类任务换小模型,生成任务才用大模型 |
| 中间结果格式不稳定 | 没有在 desc 中明确分隔符和取值域 | 在字段描述中写死格式,例如“用中文分号分隔每条不超过20字” |
| token 消耗明显偏高 | 编译阶段重复调用 + 链路过深 | 先在小数据集编译;推理时合理设置缓存,减少重复请求 |
遇到异常时,推荐按下面的顺序排查:
- 先跑通一个最小 Predict,确认 LM 配置和网络连通性没有问题。
- 单独测试每一个 Stage,确认问题出在哪一级,而不是直接怀疑整个链路。
- 打印中间结果,检查前一级输出是否符合后一级的输入要求。
- 使用
lm.inspect_history()查看实际发给模型的 Prompt 内容,判断提示词是否被优化器正确构造。 - 最后检查 Metric 是否过于严格或过于宽松,导致优化器找不到正确的搜索方向。
6. 最佳实践与工程建议
6.1 级联层级保持精简
级联流水线最大的陷阱是层级无限膨胀。每增加一级,就会新增一次模型调用,也会新增一层错误传播。经验法则是:只把真正需要独立观察和独立优化的环节拆出来,能用规则判断的不要交给模型,能合并的简单字段不要单独成级。比如“情感判断”和“置信度判断”可以放同一个签名里,而“回复生成”需要单独控制语气,拆出来才合理。
6.2 签名描述直接影响优化上限
优化器只能在签名给定的边界内搜索 Prompt,所以 Signature 写不好,后面怎么调都受限。建议做到三点:输出字段明确给取值范围;描述中写清分隔符和格式;docstring 交代任务背景。字段名尽量不要使用含义模糊的词,比如text、info,换成review_text、issue_points这类语义清晰的名称。
6.3 指标先行,数据先行
不要先把 Pipeline 写完再去想评估指标。更合理的顺序是:先收集 20 到 50 条真实样例,定义清楚“什么算对”,再写签名,再搭建流水线,最后编译。开发集和训练集必须隔离,不能有重复样本,否则评估得分会虚高。如果业务需要多字段全部正确才计分,Metric 就应该按全字段阈值返回布尔值,而不是给部分正确加分。
6.4 每一级都要可观测
级联链路越长,可观测性越重要。上线到生产环境时,建议把每一级的输入输出都记录到日志,至少包括 sentiment、confidence、issues、reply 四个字段,并附带原始评论和耗时。这样即使线上效果变差,也能迅速判断是哪一级出了问题。DSPy 的lm.inspect_history()在开发期很适合查看 Prompt,但生产环境要依赖自己的日志系统,不要把调试接口暴露到线上。
6.5 控制成本与延迟
级联流水线天然比单次调用贵,因为链路中多次调用模型。工程上可以这样控制成本:情感分类这类低难度任务,优先使用小模型或规则模型;只有回复生成这类创造性任务才使用大模型;编译优化只在小数据集上执行,不要在生产流量上反复触发。对于相似请求,建议在业务层做缓存,避免同一段评论反复经过完整链路。
6.6 不要为了用 DSPy 而用 DSPy
单轮简单分类、翻译、改写这类任务,直接调一次模型可能更好,引入 DSPy 反而增加复杂度。DSPy 真正值得投入的场景是:多阶段、多字段、需要反复调优、需要把提示词沉淀为可复用模块的系统。如果你的项目只有两三个 Prompt,手工维护完全够用,那就不需要框架化。
7. 总结与下一步学习路线
本文实现了一条三级级联流水线,覆盖情感判断、问题抽取和回复生成三个环节,并完成了签名定义、模块组装、训练数据构造、指标编写和优化器编译的完整流程。你至少应该掌握:Signature 如何声明输入输出,Module 如何组合成链路,Optimizer 如何替代手工调 Prompt,Metric 为什么决定优化方向,以及级联链路出问题时从哪里开始排查。
下一步可以沿着三个方向继续深入:一是接入dspy.Retrieve构建 RAG 级联流水线,让流水线具备检索能力;二是尝试 MIPROv2 这种指令级优化器,观察指令搜索带来的效果变化;三是把流水线封装成服务,配合日志和缓存体系上线,把开发期的优化能力转化为生产环境的稳定输出。
刚开始实践时,建议先拿一条两级到三级的小流水线在 20 条样例上跑通编译,再逐步加 Stage,不要一上来就设计七八层的复杂链路。如果本文对你有帮助,可以收藏备用,后续我会继续更新 DSPy 在 RAG 和 Agent 场景下的实战笔记。