一个被低估的工具:DSPy,把写 Prompt 变成编译

2026-09-10 23:050 条评论106 次阅读

一个被低估的工具:DSPy,把写 Prompt 变成编译

斯坦福大学开源的AI框架


开场:先看一个让手写党沉默的战绩

DSPy 官网首页挂着一组数字:同一个 RAG 管线,编译前 F1 0.41,编译后 0.63。全程一行 prompt 都没有手写。

另一个例子来自 Shopify。他们用 DSPy 优化商品元数据抽取,每千次调用成本降了两个数量级。

如果你每天都在跟 prompt 搏斗,这组数字值得多看两眼。

我第一次听说 DSPy 时,下意识把它归类成"又一个 LangChain 式编排框架",随手关掉了文档。后来在搜索产线上评估 query 改写节点,手写 prompt 改了 3 版,线上指标纹丝不动,我才回头认真读它。

读完只有一个感受:这个工具被严重低估了。它的核心价值在编译这一层,编排只是最外围的壳。它把你写的 prompt 从文本变成可被数据编译的参数,再用优化器自动搜索更优解。

这篇文章不讲概念罗列。我以"优化器 + 蒸馏"两条主线,把它在搜索产线上真正值钱的地方拆开,附一份本地验证过可跑的示例。

一、为什么被低估:它跟编排框架站的位置不一样

先说一个侧面证据。有人搜"DSPy 入门",搜到一篇号称《DSPy 从入门到劝退》的文章,通篇在讲音频滤波和实时信号处理。

作者把 DSPy 当成了数字信号处理库。DSP 三个字母惹的祸,全文内容全部作废。

这个反例说明两件事:中文世界关于 DSPy 的优质资料确实稀缺;它的名字还自带误导。连资料都这么容易跑偏,工具被低估也就不奇怪了。

大多数人低估 DSPy,原因在于拿错了参照系。

LangChain 解决的是"怎么把组件接起来",LangGraph 解决的是"有状态的 Agent 图怎么跑",DSPy 解决的是另一个问题:prompt 本身怎么被自动优化。

一句话定位:DSPy 是 LLM 程序的编译器。

类比 PyTorch 你就懂了。PyTorch 里你定义网络结构,写 loss,优化器自动调权重。

DSPy 里你声明输入输出契约,写评分函数,优化器自动调 prompt 指令和示例。模型权重被梯度更新,prompt 被编译器更新,心智模型完全一致。

这套心智模型对应四个概念:

概念 作用 PyTorch 类比
Signature 签名 声明输入输出契约:context, question -> answer 定义网络层
Module 模块 同一签名换执行策略:Predict / ChainOfThought / ReAct nn.Module 积木
Metric 指标 你写的评分函数,优化的一切都对着它来 loss / 评测指标
Optimizer 优化器 自动搜更优指令与示例,甚至生成微调数据 训练器

关键在最后一行。Metric 和 Optimizer 的存在,把"调 prompt"从手艺活变成了数据活。

二、优化器:编译器的心脏

写 prompt 这么多年,你最清楚问题在哪:没有度量,全是手感。

同样一段话术,换个模型效果就漂。你说不清是指令不够细,还是缺示例,还是输出格式吃了亏。DSPy 把这个问题拆成了可优化的对象:指令文本、few-shot 示例、甚至结构化输出模板。

优化器替你干的事,就是在训练集上反复试这些对象的不同变体,用 metric 判分,留下得分最高的组合。

官方 demo 里有个直观战绩:分类任务 baseline 62%,GEPA 编译后 89%。没换模型,没加数据,只编译了 prompt。

优化器家族按你的预算和任务形态选:

优化器 特点 什么时候用
BootstrapFewShot 快速起步,自动挑示例 刚接入、验证链路时
GEPA 遗传算法 + 帕累托,反射式,省预算 预算紧、需要文本化反馈
SIMBA 长尾难例自省,适合 agentic 长任务 难样本集中攻坚
MIPROv2 贝叶斯搜指令 + 示例 预算充裕时追求上限
GRPO 真正的强化学习 代价最大,极少场景值得

换优化器只需改一行代码,这点后面示例里会看到。

给一句产线提醒:metric 决定一切。评分函数设计失误,优化器会把错误行为刷到极致,这叫过拟合到错误目标。对策是用线上可验证信号当判分依据,比如点击、成交、人工抽检。

上线前再在留出集上复测。垃圾 metric 进,垃圾 prompt 出,这条铁律在编译世界里同样成立。

三、蒸馏:被低估里最被低估的

如果说优化器是被低估的,那蒸馏就是被低估里最被低估的。

DSPy 官方把 fine-tune 工作流当一等公民。编译器跑一遍,副产品是一批高质量数据。这批数据能直接反哺小模型微调,原理上是把大模型当 teacher,编译过程产出 few-shot 与训练语料,喂给本地小模型。

这条路径对你的本地部署策略价值极大,具体讲三点。

第一,编译产物里的 demos 是优化器自动挑的 few-shot。它替你做了人工精选示例的活,挑出来的还是经过 metric 验证的版本。

第二,把产物导出成对话格式,就是一份微调语料。云端强模型当 teacher 跑一次编译,得到的数据集够本地 14B 微调用一轮。

第三,换模型不重写 prompt。DSPy 换 LM 只需改一行配置再重编译,这个能力对 Gemma 微调版与云 Qwen 并存的场景是刚需。prompt 层不再跟着模型绑定,漂移问题从源头被拆掉。

官方教程里这套流程叫 teacher-student 蒸馏:强模型在编译期产数据,弱模型在微调期学数据。你在本地 GPU 上部署量化模型时,最缺的就是高质量训练数据,DSPy 恰好是这条生产线的开关。

四、示例:query 改写编译 + 蒸馏导出

下面这段代码我本地跑通了,dspy 3.3.1,Windows + Python 3.13。任务选搜索产线最常见的 query 改写加意图分类,共 3 段。

第 1 段:声明签名,0 行手写 prompt

import dspy

# 配置语言模型。编译期建议用云端强模型当 teacher:
lm = dspy.LM("openai/gpt-4o-mini", api_key=os.environ["OPENAI_API_KEY"])
dspy.configure(lm=lm)


class QueryRewrite(dspy.Signature):
    """把用户口语化的搜索词改写成规范商品查询词,并识别意图。"""

    raw_query: str = dspy.InputField(desc="用户输入的原始搜索词,可能带口语与错别字")
    rewritten: str = dspy.OutputField(desc="改写后的规范搜索词,用于召回")
    intent: str = dspy.OutputField(desc="用户意图,只能是 buy / compare / browse")


# 训练集:线上 query 日志里随手捞 4 条,20 到 50 条起步效果更稳
trainset = [
    dspy.Example(raw_query="想买个降噪好点的耳机", rewritten="降噪耳机", intent="buy").with_inputs("raw_query"),
    dspy.Example(raw_query="手机 华为和小米哪个好", rewritten="华为手机 小米手机 对比", intent="compare").with_inputs("raw_query"),
    dspy.Example(raw_query="有没有便宜点的跑步鞋", rewritten="跑步鞋 实惠", intent="browse").with_inputs("raw_query"),
    dspy.Example(raw_query="iPhone 16", rewritten="iPhone 16", intent="browse").with_inputs("raw_query"),
]

注意到这里没有写任何 prompt 指令。签名里的 docstring 和字段 desc 就是全部输入。

第 2 段:写 metric,编译,保存产物

def metric(gold, pred, trace=None):
    if gold.intent != pred.intent:
        return False
    # 简化判分:改写后的核心词需与标注有交集
    gold_words = set(gold.rewritten.replace(" ", "").split())
    pred_words = set(pred.rewritten.replace(" ", "").split())
    return len(gold_words & pred_words) >= 1


# 快速起步用 BootstrapFewShot;预算足可换 MIPROv2(metric=metric)
optimizer = dspy.BootstrapFewShot(metric=metric, max_bootstrapped_demos=2, max_labeled_demos=2)
compiled = optimizer.compile(dspy.Predict(QueryRewrite), trainset=trainset)
compiled.save("query_rewrite.json")

编译完成后,产物里多了优化器自动挑选的 few-shot。我本地打印出来的实际结构长这样:

{
  "demos": [
    {"raw_query": "iPhone 16", "rewritten": "iPhone 16", "intent": "browse"},
    {"raw_query": "想买个降噪好点的耳机", "rewritten": "降噪耳机", "intent": "buy"}
  ]
}

这 2 条示例是框架从训练集里筛出来、经 metric 验证过的。你没写任何"参考如下示例"的话术,它自动替你完成了示例选择。

编译产物是纯数据。save 出来是 json,load 回去就能当普通 Predict 用。编译期和运行期完全分离,线上调用零优化开销,这也是它能跨模型迁移的根基。

第 3 段:蒸馏导出,产物直接变微调语料

import json

with open("query_rewrite.json", encoding="utf-8") as f:
    artifact = json.load(f)

rows = []
for d in artifact["demos"]:
    rows.append(
        {
            "messages": [
                {"role": "user", "content": d["raw_query"]},
                {"role": "assistant", "content": f"{d['rewritten']}(意图:{d['intent']})"},
            ]
        }
    )
with open("finetune_data.jsonl", "w", encoding="utf-8") as f:
    for row in rows:
        f.write(json.dumps(row, ensure_ascii=False) + "\n")

跑完你就得到一份标准的 SFT 语料,messages 三件套格式,可直接喂给本地模型微调。

这是蒸馏最朴素也最实用的形态:teacher 模型在编译期生产的高质量输出,被结构化沉淀成训练数据。要规模化,就把无标注 query 批量丢给编译后的程序生成改写结果,凑几千条再微调。

完整闭环是这样的:云端强模型当 teacher 编译一小批种子数据,产出指令与示例。用这批编译结果给海量无标注 query 打标,得到训练集。本地小模型在这份数据上微调,上线推理。

换场景就换 teacher 重编译,流水线复用。

这套路径里,优化器负责把 teacher 的能力压进 prompt,蒸馏负责把 teacher 的能力传给学生。前者解决当下调用质量,后者沉淀成可复用的模型资产。

两个环节共享同一套签名与 metric,切换成本只有一行代码。

五、算账:本地 16GB 显卡能扛编译吗

直接给结论:编译阶段用云端,推理阶段用本地。

优化器的成本结构是"对每条样本发起多次 LLM 调用试 prompt 变体"。粗算一版:

方案 量级估算 可行性
云端小模型 GEPA 官网实证 200 样本约 $2.18 编译主力
云端小模型 MIPROv2 50 样本约 $5 到 20 预算充裕时
本地 14B 量化 200 样本约 160 万 token,10 小时以上 占用整卡,不划算

编译产物是稳定格式,save / load 后线上推理就是普通 LLM 调用,随时切回本地 14B 或云 Qwen。云端编译、本地推理,两头的好处都拿到。

这就是"云端训练、本地部署"策略的完整闭环,与在本地 GPU 上跑量化 Agent 的路线天然咬合。

六、边界:哪里别用它

先给一张自检清单,判断你的任务适不适合上 DSPy:

条件 说明
多步骤 LLM 管线 单次调用也能用,但管线化节点收益最大
有明确质量指标 能写出 metric,F1、准确率、命中率都行
有可评估样本 最少 20 到 50 条标注起步
质量随 prompt 漂移 换模型效果就变,说明有编译空间

四条全中,值得试点。缺样本或缺指标,先补数据再谈编译。

DSPy 解决不了所有问题,下面这些场景请绕行。

纯聊天、开放式创作这类没有明确 metric 的任务,优化器无的放矢。一次性原型、结构还没定型的项目,先手写 prompt 跑通再迁移。长时有状态 Agent、审批流、断点续跑,那是 LangGraph 的主场,DSPy 的 ReAct 没有持久状态。

社区共识有一条经验法则:先手写 prompt 把流程跑通上线,攒真实输入输出,等你知道"好"长什么样了,再把最痛的那几个节点迁到 DSPy 编译。

它精炼的是已经能跑的东西。探索该做什么,那是另一类工具的事。

收尾:把编译器装进口袋

回到开头的战绩。RAG F1 从 0.41 到 0.63,分类从 62% 到 89%,背后是同一件事:把 prompt 从文本变成参数,用数据驱动它。

低估它的人,把它当成又一个编排框架。真正用过的人,把它当成生产 prompt 的流水线。

我的下一步是把搜索链路里 query 理解节点迁过去试点,先攒 50 条标注,跑一版 GEPA 看增量。它的优化器家族和蒸馏能力,值得每一个被 prompt 调参折磨过的人重新评估。

项目地址:https://github.com/stanfordnlp/dspy

本篇完整示例代码(含 DummyLM 零成本可跑版)已打包。关注我的公众号,在公众号回复 dspy 领取。