
1. 为什么我放弃了手写Prompt改用DSPy优化提示词先交代一下背景。过去一年我大部分时间都在跟大模型打交道做各种自然语言处理相关的原型和落地项目。早期阶段我的工作模式非常简单粗暴把任务描述清楚丢给模型看输出然后根据输出不断微调提示词。这个流程听起来合理实际跑起来却非常折磨人——尤其当任务稍微复杂一点比如涉及多步骤推理、工具调用、结构化输出时提示词的任何一个措辞变化都可能导致结果剧烈波动。我试过各种技巧few-shot示例轮换、CoT链路改写、格式约束、自我一致性投票全都有效果但效率极低。每次调整一个词、换一个示例都要手动跑一遍完整流程然后肉眼对比输出质量。项目里的提示词越攒越多维护成本越来越高最后几乎变成一团乱麻。后来我接触到了DSPy一个专门针对提示词和推理管道做系统性优化的框架。它的核心思路和传统方式完全相反不再靠人肉调Prompt而是把提示词当作可优化的参数通过编译、评估、自动迭代来找到最优组合。说白了DSPy是在用工程化的方式替代手工调词让提示词优化变成一条可量化的流水线。这篇文章我不会去复述官方文档而是想把我在实际项目里用DSPy优化提示词的完整过程、踩过的坑、以及最终沉淀下来的方法论一次性讲清楚。适合谁看如果你也在做基于大模型的应用开发尤其是那些提示词反复改、效果不稳定、评估标准模糊的项目这篇文章应该能帮你省下不少时间。2. 从手工调词到声明式编程DSPy的核心设计思想2.1 传统提示词工程最大的问题不是“写”而是“调”先说一个很多人忽略的事实提示词工程的瓶颈从来不在于写第一版Prompt而在于后续的持续调优。比如你要做一个文本分类器给模型写了一段指令测试下来准确率不够。你会怎么办大概率是改几个示例、调整一下输出格式要求、或者把任务描述换一种说法然后重新测试。这个循环看起来简单但每次只能验证一个变量的影响而且不同变量之间还会互相干扰。你今天把示例从3个加到5个准确率提升了2%但明天换了一批测试数据这2%可能就消失了。你以为是提示词的问题实际可能是示例选取偏差导致的偶然波动。手工调词的另一个致命问题是不可复现。你的调优经验全部存在脑子里换一个人来接手同一个项目他完全不知道你为什么用这个句式、为什么选这些示例只能从头开始踩坑。项目一多这种隐性知识的流失非常严重。2.2 DSPy把提示词变成了程序的参数DSPy解决这个问题的思路很彻底。它不再把你写的自然语言指令当成“最终答案”而是当成一个初始化状态。你定义好任务、数据、评估函数然后框架会自动生成多个候选Prompt在训练集上评估效果再通过优化算法迭代出最优版本。换句话说DSPy把“调Prompt”这件事从直觉驱动变成了数据驱动。你不是在“写”提示词而是在“声明”任务的结构剩下的交给编译器去探索。我个人的理解是DSPy的核心贡献是用一套类似传统机器学习的框架来治理LLM应用开发中的不确定性。它把LLM调用封装成可组合的模块把提示词和思维链示例当作可调参数然后用优化器自动搜索优质参数组合。这个设计理念本质上是在告诉开发者与其跟模型的随机性对抗不如把对抗过程变成算法的一部分。2.3 DSPy的三个核心抽象Signature、Module、Optimizer如果你第一次接触DSPy我建议先把这三个概念吃透因为它们就是整个框架的骨架。Signature签名描述输入输出之间的语义映射。你不需要写完整的Prompt只需要定义“输入是什么、输出是什么、任务目标是什么”。DSPy会根据这个签名自动生成Prompt的骨架。举个例子做情感分类时签名可以这样定义class SentimentClassification(dspy.Signature): 判断一段文本的情感倾向。 text: str dspy.InputField(desc待分类的文本内容) sentiment: str dspy.OutputField(desc只能是positive、neutral或negative之一)你看这里完全没有写“请你仔细阅读以下文本然后判断情感倾向你的回答必须……”之类的长指令。DSPy会自动把这些描述组织成完整的Prompt。Module模块是任务的执行单元。你可以用预置的dspy.Predict直接调用签名也可以用dspy.ChainOfThought在预测前插入推理步骤或者用dspy.ReAct实现带工具调用的Agent行为。模块可以嵌套组合构建成复杂的管道。classify dspy.ChainOfThought(SentimentClassification)一行代码就让模型在输出答案之前先进行一步思考不需要你手动写“让我们一步一步思考”。Optimizer优化器是自动调优的核心。它负责生成候选Prompt、评估结果、迭代更新。常用的优化器包括BootstrapFewShot、BootstrapFewShotWithRandomSearch、MIPROv2等各自适用于不同场景。这里需要强调一点DSPy优化的核心是少样本示例而不是像微调那样更新模型权重。它通过自动构造高质量示例来“教”模型完成任务所以速度快、成本低不需要GPU训练。3. 工具选型解析为什么选择DSPy而不是其他方案3.1 与手写Prompt工程的对比很多开发者会问我手写Prompt已经能解决80%的问题为什么还要学DSPy我的回答是如果你的任务非常简单比如单一分类、简单抽取、固定格式生成手写Prompt完全够用没必要上框架。但一旦你的任务满足以下任一条件手写Prompt的维护成本会急剧上升任务涉及多个处理步骤中间结果需要传递给后续模块输出格式必须严格结构化且字段较多需要定期在新增数据上更新示例团队成员需要协作维护同一套Prompt逻辑评估指标不单一需要综合多个维度判断效果DSPy的价值在于把这些复杂场景工程化。你定义一次管道之后数据更新、示例优化都可以自动完成。手写Prompt是手工作坊DSPy是流水线生产两者定位不同不是谁替代谁的问题。3.2 与LangChain的定位差异有人会把DSPy和LangChain放在一起比较其实它们解决的问题不一样。LangChain的核心是编排——管理LLM调用、工具、记忆、外部API的连接关系相当于给你一套乐高积木。而DSPy的核心是优化——在给定管道结构的前提下自动寻找最优的Prompt参数相当于给你一个自动调音器。实践中两者可以配合使用用LangChain搭建应用骨架把DSPy优化好的模块嵌入其中。不过我自己更倾向于在一个项目里尽量保持技术栈的统一避免引入过多抽象层。如果主任务是提示词优化直接用DSPy就够了。3.3 可选方案横向对比方案核心机制适用场景局限性手写Prompt调优人工迭代经验驱动简单任务、快速原型效率低、不可复现、变量耦合DSPy声明式编程 自动优化复杂管道、持续迭代、团队协作学习曲线陡峭、抽象层次多LangChain模块编排、工具集成Agent应用、多工具协同不提供Prompt自动优化能力微调更新模型权重需要深度改变模型行为成本高、数据要求大、周期长DSPy在“成本可控”和“自动化程度高”之间取得了不错的平衡这也是它在开发者社区快速升温的原因。4. 实操过程与核心环节实现用DSPy优化一个多分类任务的完整记录4.1 项目背景与任务定义我拿一个实际做过的项目来演示。项目目标是做一个“技术问题工单自动分类”系统输入是用户提交的技术工单描述输出是5个类别之一网络故障、账号权限、软件异常、硬件故障、数据处理问题。这个任务看起来很简单但实际做起来有几个难点用户描述口语化严重很多还夹杂错别字和英文缩写类别之间存在语义重叠比如“软件异常”和“数据处理问题”边界模糊数据分布极不均衡网络故障类样本非常多硬件故障类样本很少需要输出置信度信息便于后续人工复核如果完全手写Prompt这些难点每个都需要单独设计应对策略。而用DSPy我可以把大部分策略转换成数据约束和评估函数。4.2 环境准备与基础配置首先安装DSPy我使用的是Python 3.10环境pip install dspy-ai然后需要配置语言模型和后端。目前DSPy支持OpenAI、Anthropic、本地模型等多种后端。我演示时用的是OpenAI接口配置方式如下import dspy # 使用gpt-4o-mini作为主模型 lm dspy.LM( modelopenai/gpt-4o-mini, api_key你的API_KEY, temperature0.3 ) dspy.configure(lmlm)这里有几个关键选择需要说明。temperature设置为0.3而不是0是因为0虽然能最大程度保证确定性但会让模型缺乏多样性影响Bootstrap阶段采集候选示例的有效性。我实测下来0.2到0.4之间是比较合适的区间。api_key不建议硬编码在代码里建议使用环境变量管理方便多人协作和部署切换。4.3 定义签名把任务说清楚接下来定义工单分类的签名。这一步是整个流程的地基因为DSPy会基于签名自动生成Prompt。如果签名描述不够清晰生成的Prompt就先天不足后期优化也很难弥补。class TicketClassification(dspy.Signature): 对技术工单进行精确分类。 注意类别之间可能存在语义重叠请综合判断最合理的类别。 ticket_text: str dspy.InputField(desc用户提交的技术工单完整内容) category: str dspy.OutputField(desc分类结果必须是以下之一网络故障、账号权限、软件异常、硬件故障、数据处理问题) confidence: float dspy.OutputField(desc置信度打分0到1之间表示本次分类的确信程度)这里有个值得注意的细节我在Docstring里写了“类别之间可能存在语义重叠请综合判断”这句话不是随便写的。根据我之前的测试如果不在文档字符串里显式提示重叠问题模型在遇到边界模糊样本时更容易给出错误分类。DSPy生成的Prompt会很自然地把这个提示融入指令中。我最初也试过在OutputField的desc里写“必须返回JSON格式”但效果反而不稳定。DSPy有自己的结构化输出机制它会自动将多个输出字段组合成JSON格式不需要你手动要求。过度指定格式约束反而会限制优化器的搜索空间。4.4 准备数据集与评估函数在DSPy的体系里数据分为两类用于优化的训练集和用于验证效果的开发集。我整理了约400条带标注的工单数据其中300条作为训练集100条作为开发集。数据格式如下# 每条数据是一个dspy.Example对象 train_dataset [ dspy.Example( ticket_text公司内网无法访问ping网关不通同事电脑也有同样问题, category网络故障, confidence0.95 ), dspy.Example( ticket_text登录ERP系统提示密码过期但三天前刚改过密码, category账号权限, confidence0.9 ), # ... 共300条 ].with_inputs(ticket_text)特别注意with_inputs(ticket_text)这一行。它标记了哪些字段是输入、哪些字段是输出。在DSPy v2.8以上的版本里训练集必须明确指定输入字段否则后续Bootstrap阶段会报错或行为异常。这个坑我踩过一开始没加结果优化器在生成示例时把标准答案也当成输入喂给模型导致性能非常差。评估函数的设计也很重要。对于分类任务准确率是最直观的指标但如果只算准确率模型可能会倾向于把模糊样本分到占比最高的类别来“蒙混过关”。所以我额外加了一个惩罚机制对“软件异常”和“数据处理问题”这两个易混淆类别之间的误判扣分加倍。def evaluate(example, pred, traceNone): if example.category pred.category: return True # 易混淆类别之间的误判额外扣分 confusing_pairs {(软件异常, 数据处理问题), (数据处理问题, 软件异常)} if (example.category, pred.category) in confusing_pairs: return False return False这个评估函数只返回布尔值DSPy在编译时会统计所有样本的真假结果来计算整体性能。4.5 选择优化器从零样本到自动少样本的完整链路定义好模块之后可以先做一个零样本版本的基准测试这一步很有价值它告诉你“不优化”时的效果底线。classify_module dspy.Predict(TicketClassification) # 在开发集上评估 evaluator dspy.Evaluate(metricevaluate, devsetdev_dataset, num_threads4) baseline_score evaluator(classify_module) print(零样本基线准确率:, baseline_score)我在这个任务上测出来的基线准确率大约是78%。对于这个结果我并不意外因为类别重叠问题天然难处理。接下来进入核心环节使用优化器自动生成和筛选少样本示例。这里我选择了BootstrapFewShotWithRandomSearch它会在训练集上进行随机搜索找到最优的示例组合。from dspy.teleprompt import BootstrapFewShotWithRandomSearch optimizer BootstrapFewShotWithRandomSearch( metricevaluate, max_bootstrapped_demos8, max_labeled_demos16, num_candidate_programs6, num_threads4 ) compiled_predictor optimizer.compile( studentclassify_module, teacherclassify_module, trainsettrain_dataset )参数含义说明max_bootstrapped_demos最多生成8个由模型推理得到的示例。这类示例包含完整的推理过程信息量更大。max_labeled_demos最多16个直接从训练集选取的标注示例。num_candidate_programs尝试6组不同的候选程序每组对应一套不同的示例组合和Prompt表述最终保留效果最好的一组。teacher参数指向一个效果更好的教师模型。如果只有学生模型可以让学生自己生成示例如果你有更强模型的API可以用它来生成更高质量的Bootstrap示例。编译完成后再跑一次开发集评估compiled_score evaluator(compiled_predictor) print(优化后准确率:, compiled_score)在我的测试数据上优化后准确率从78%提升到了86.5%。别小看这8.5个百分点的提升在工单分类场景里这相当于减少了约四成的错分工单对后续自动流转环节的影响非常大。4.6 用MIPROv2进一步探索Prompt指令如果你的任务更复杂或者BootstrapFewShot的示例优化效果已经到瓶颈可以尝试MIPROv2优化器。它不仅能优化示例还会对Prompt的描述指令本身进行搜索优化。MIPROv2的用法类似但编译时间明显更长因为它要遍历更多候选指令和示例组合。from dspy.teleprompt import MIPROv2 optimizer MIPROv2( metricevaluate, num_candidates10, max_bootstrapped_demos8 ) compiled_predictor optimizer.compile( studentclassify_module, trainsettrain_dataset, eval_setdev_dataset )如果你是API按次数计费需要注意MIPROv2的token消耗非常大因为它会为每条候选指令和每个示例组合都跑一次完整的评估。建议先用小数据集试跑确定参数可行性再扩大规模。4.7 核心代码完整串联把整个流程串联起来核心代码结构如下import dspy from dspy.teleprompt import BootstrapFewShotWithRandomSearch # 配置模型 lm dspy.LM(modelopenai/gpt-4o-mini, temperature0.3) dspy.configure(lmlm) # 定义签名 class TicketClassification(dspy.Signature): 对技术工单进行精确分类。 类别之间可能存在语义重叠请综合判断最合理的类别。 ticket_text: str dspy.InputField(desc用户提交的技术工单完整内容) category: str dspy.OutputField(desc分类结果网络故障、账号权限、软件异常、硬件故障、数据处理问题) confidence: float dspy.OutputField(desc置信度打分0到1之间) # 定义评估指标 def evaluate(example, pred, traceNone): if example.category pred.category: return True confusing_pairs {(软件异常, 数据处理问题), (数据处理问题, 软件异常)} if (example.category, pred.category) in confusing_pairs: return False return False # 创建基础模块跑零样本基线 base_predictor dspy.Predict(TicketClassification) evaluator dspy.Evaluate(metricevaluate, devsetdev_dataset, num_threads4) print(基线:, evaluator(base_predictor)) # 编译优化 optimizer BootstrapFewShotWithRandomSearch( metricevaluate, max_bootstrapped_demos8, max_labeled_demos16, num_candidate_programs6, num_threads4 ) compiled optimizer.compile( studentbase_predictor, trainsettrain_dataset ) # 评估优化结果 print(优化后:, evaluator(compiled))这套代码跑下来一次编译在300条训练数据上大约耗时5到8分钟取决于API响应速度token消耗大约在15万到30万之间。相比手工调Prompt消耗的几小时精力这个成本完全可以接受。5. 常见问题与排查技巧实录5.1 优化器编译后效果反而变差了怎么办这是最常遇到的问题。很多人在第一次使用DSPy时满怀期待结果编译完一测准确率反而比零样本基线更低。先说一个最容易忽略的原因训练集数据质量参差或者标注标准不一致。DSPy的优化器会从训练集中采样示例如果某条示例的标注本身就是错的Bootstrap阶段还会把错误扩展成带推理过程的错误示例误导模型。解决方法是先检查训练集标注一致性剔除明显错误和边缘争议样本。第二个原因是评估函数设计不当。如果你的指标是准确率但样本类别不均衡优化器会倾向于把所有样本都归类到多数类因为这样准确率最高。解决办法是换用F1分数或者像我前面那样对易混淆类别增加惩罚。第三个原因是关键示例数量设置过大。很多人以为示例越多越好但模型能容纳的有效上下文是有限的。示例占了太多上下文空间反而挤压了指令信息的比重导致效果下降。根据我的经验对于普通分类任务6到10个示例通常足够超过12个效果就开始边际递减甚至下降。你可以用小步调整示例数量跑几次对比。5.2 编译过程Token消耗太高如何控制成本Token消耗是DSPy编译绕不开的问题尤其在MIPROv2里。这里分享几个实测有效的控制手段先用小训练集试跑比如50条数据验证参数设置合理后再扩大到完整集降低num_candidate_programs比如从10降到4以搜索次数换取成本降低使用更便宜的模型做Bootstrap阶段的示例生成最终推理时才用主力模型如果任务不复杂优先使用BootstrapFewShot而不是随机搜索版本消耗能减少一半以上5.3 模型输出字段格式不规范频繁解析失败这通常不是DSPy的问题而是签名中OutputField的描述不够明确。如果你发现返回的JSON字段经常缺少或格式不对优先检查desc是否写清楚了每个字段的含义和约束。另一个常见原因是你使用了过高的temperature比如0.7以上时模型可能输出发散。建议将temperature控制在0.2到0.4之间。5.4 训练集和开发集指标差异很大是不是过拟合了如果你发现开发集上的准确率远低于训练集大概率是训练集中的数据分布和真实场景分布不一致而不是模型的过拟合。因为DSPy优化的是Prompt示例不是模型权重真正的过拟合风险比微调低很多。此时更重要的是检查数据集的分布是否覆盖了真实场景的边界情况建议从线上采一些真实数据加入训练集。5.5 一个容易被忽略的坑版本差异导致API行为不同DSPy的版本迭代非常快不同版本之间的API变动很大。我在一个项目中遇到的情况是同事用的旧版本dspy.teleprompt路径和我的新版完全不同导致代码无法直接复用。如果你参考网络上的教程务必先确认教程对应的版本号最好在自己的环境里查看当前版本的API签名。检查方式import dspy print(dspy.__version__)6. 从分类任务到复杂管道DSPy的进阶玩法6.1 用ChainOfThought模块构建推理管道分类任务只是DSPy的入门级应用它的真正威力在于构建多步骤推理管道。比如我之前做过一个“工单自动解决方案生成器”流程是先判断工单类别再提取关键信息最后生成推荐解决方案。这三个步骤如果分开写Prompt需要维护三套提示词中间结果的传递还要写大量解析代码。用DSPy可以直接声明一条管道class SolutionGenerator(dspy.Module): def __init__(self): super().__init__() self.classify dspy.ChainOfThought(TicketClassification) self.extract dspy.ChainOfThought(InfoExtraction) self.solution dspy.ChainOfThought(SolutionGeneration) def forward(self, ticket_text): cls_result self.classify(ticket_textticket_text) ext_result self.extract(ticket_textticket_text, categorycls_result.category) sol_result self.solution(ticket_textticket_text, infoext_result.info) return sol_result.solution你只需要定义好每一步的签名DSPy会自动把中间结果拼接成后续步骤的Prompt上下文。整个管道可以在训练数据上统一优化不用一个模块一个模块地单独调。6.2 人机协同的评估闭环虽然DSPy实现了自动化优化但我仍然建议在流程中加入一部分人工审核环节。我的做法是优化完成后随机抽取50条开发集样本输出模型的分类和置信度然后由人工快速复核。重点看两类——高置信度但分类错误的和低置信度但分类正确的。这两类数据最能反映问题前者说明模型在某些情况下“过于自信”后者说明模型可能漏掉了一些它实际掌握的信息。把这类错误案例补充进训练集再做一轮编译效果往往立竿见影。6.3 成本与延迟的平衡策略优化不是越充分越好尤其是线上场景模型调用成本和使用延迟都是需要平衡的约束。我建议在项目初期用较强的模型做优化探索确定任务的“性能上限”。这个上限的意义在于如果强模型优化后准确率只有82%说明任务本身的标注一致性或数据质量有问题靠调提示词是突破不了的。这时候应该先提升数据质量而不是继续堆优化资源。一旦确认任务可行再考虑用更小、更便宜的模型做压缩。DSPy的Bootstrap机制很适合这个场景用强模型生成高质量的示例集然后把这些示例固定下来换用小模型配合固定示例推理往往能在成本和效果之间找到不错的平衡点。7. 最后分享一点个人的实际体会从接触DSPy到现在我在三个不同类型的项目里实践过它工单分类、信息抽取、多步骤解决方案生成。总的来说DSPy不是万能的它解决的是“提示词优化”这个特定问题而不是整个LLM应用开发的所有问题。我最深的一个体会是DSPy的学习曲线虽然陡峭但它的核心思想其实很简单——把调Prompt从玄学变成科学。一旦习惯了“定义签名、准备数据、评估、编译、评估”这个循环你会觉得手工调Prompt的时代真的过去了。如果你现在的项目正卡在“Prompt怎么改都不稳定”的阶段我建议你拿一个典型的任务准备几百条带标注的数据花半天时间跑一遍DSPy的流程。哪怕最终效果提升不多你也会对自己的任务有更清晰的认知——因为评估函数会逼你想清楚“到底什么算好的输出”而这本身就是提示词工程里经常被忽略的关键环节。最后再分享一个小技巧DSPy编译完成后会把最终使用的Prompt和示例保存下来。你可以打印出来看看很多时候你会惊讶于优化器生成的Prompt比你手写的版本要简洁得多但效果反而更好。这恰恰印证了那句老话——好的Prompt不是写出来的是选出来的。