我仔细阅读了DSPy的“深入探索”文档——一共13篇。从签名、适配器、模块、度量、优化器、GEPA、工具、ReAct、设置、保存、灵活性、RLM和内置模块变体。以下是我得到的结论。
大多数人误解的思维模型
DSPy并不是一个提示工程库。它是一个用于语言模型程序的编译器。这个区别很重要。
一个提示工程库帮助你写出更好的提示。DSPy让你声明你想要什么(签名),组合如何获取它(模块),然后自动优化提示、演示甚至模型权重,以达到你定义的度量标准(优化器)。你只需编写一次程序。编译器会找到最佳版本。
签名:类型系统
一切从签名开始。它是一个Pydantic模型,声明输入字段、输出字段和任务指令。字符串形式 "question -> answer" 是一种简写——实际上,每个签名都是一个带有类型字段、描述和文档字符串的类。文档字符串是唯一被优化器重写的内容。字段名称、类型和描述保持不变。这是有意为之——字段名称是程序的公共接口的一部分。其他模块、调用代码和下游消费者通过名称读取 result.answer。一个重写字段名称的优化器会破坏周围的全部内容。
每个突变方法——with_instructions、with_updated_fields、prepend、append、delete——都会进行深拷贝并返回一个新的类。没有任何内容会原地修改。这使得优化器能够运行数百个候选变体而互不干扰。
适配器:提示层
适配器是将签名转化为实际提示的地方。ChatAdapter 使用 [[ ## field ## ]] 标记。JSONAdapter 使用结构化的 JSON。XMLAdapter 使用标签。TwoStepAdapter 为格式化不稳定的推理模型拆分生成和提取。
适配器的生命周期是固定的:预处理 → 格式化 → LM 调用 → 后处理 → 解析。你可以通过走这五个步骤中的任何一个来调试任何适配器。
类型转换集中在单个函数 parse_value(value, annotation) 中。每个适配器都委托给它。当类型字段行为异常时,就在那里查找。
模块:正确的组合
一个模块很简单:继承它,定义 __init__ 中的子模块,在 forward() 中编写逻辑。子模块通过遍历 self.__dict__ 发现——赋值即注册。无需 register_module() 调用,无需装饰器,无需继承技巧。
__call__ 方法在 forward() 上包裹基础设施:使用跟踪、回调和调用模块堆栈。总是以 module(...) 的形式调用模块,而不是直接调用 forward()。设置通过上下文传播,而不是构造函数参数——with dspy.context(lm=other_lm) 会为每个子模块内部自动切换 LM,无需重新布线。
_compiled 标志是多阶段优化的关键。在优化器编译一个子模块后,它会设置 _compiled = True。未来的优化器会跳过它。这使得你可以优化内部 → 嵌入外部 → 优化外部。
模块动物园
除了预测、链式思维和ReAct之外,还有令人惊讶的深度集合:
BestOfN 和 Refine 多次以不同的 rollout ID 和温度 1.0 采样同一个模块,然后使用奖励函数评分。Refine 在尝试之间添加反馈——它构建模块的源代码和 I/O 的快照,将这些信息传递给内部预测器生成建议,并在下一次尝试中将这些建议作为提示字段注入。
ProgramOfThought 和 CodeAct 生成 Python 代码并在沙盒 Deno/WASM 解释器中运行。CodeAct 继承自 ReAct 和 ProgramOfThought——它是一个工具使用的循环,其中的工具调用是 Python 代码。
RLM(递归语言模型)适用于大上下文。与其将一切塞入提示,它将上下文作为变量传递给模型的 Python REPL。模型编写代码来探索数据并调用 llm_query() 以询问子问题。一个长上下文问题被拆分为许多短上下文问题。
Flex 是最激进的。它将模块的源代码放入优化搜索空间。GEPA 不仅重写提示——它重写整个实现。不同的预测器、不同的控制流、更多的或更少的 Python。模块的结构本身是被优化的对象。
度量:唯一重要的东西
度量是任何可调用的 (gold, pred) -> score。鸭子类型,没有基类,没有装饰器。这种简洁是故意的——任何现有的 NLP 库中的评分函数都可以通过一行代码转换为 DSPy 度量。
返回类型可以是 bool、float 或 dspy.Prediction(score, feedback)。每种类型都以不同的方式处理。布尔值聚合为百分比,浮点数求平均,Prediction 形状将反馈字符串传递给 GEPA 的反思循环。
trace 参数是使一个度量服务于两个目的的杠杆。当 trace is None(评估)时,SemanticF1 返回连续评分。当 trace is not None(优化)时,它返回二元通过/未通过。同一个函数,两种模式。
优化器景观
DSPy 随附了十几种优化器。选择指南归结为两个问题:瓶颈是什么,预算是多少?
演示调优(BootstrapFewShot 家族):收集示例跟踪,其中度量通过,然后将它们填充到预测器的演示中。BootstrapRS 用不同的种子运行此操作 N 次并选择获胜者。KNNFewShot 在推理时间通过嵌入检索选择演示。
指令调优(COPRO、GEPA、MIPROv2):在每个预测器的签名上重写文档字符串。COPRO 是广度优先的。MIPROv2 在指令 + 演示的联合空间上使用贝叶斯优化。GEPA 使用由度量的自然语言反馈引导的进化搜索。
权重调优(BootstrapFinetune):从成功的跟踪中启动,将它们写入训练数据,微调 LM。最后一个杠杆。
一般建议:从 BootstrapFewShot 开始。如果指令看起来不对,添加 COPRO 或 GEPA。如果两者都需要调优,使用 MIPROv2。如果仅提示优化已达到平台期且模型可调优,尝试 BootstrapFinetune。大多数团队从未需要超越仅提示优化。
GEPA:反馈驱动的优化器
GEPA 是最有趣的优化器。它维护一个候选程序的种群,对每个程序在验证集上进行评分,并使用一个反思 LM 提出受每个预测器反馈启发的指令编辑建议。
关键在于:你的度量必须返回 Prediction(score, feedback),而不仅仅是浮点数。反馈字符串原封不动地传递给反思提示。一个裸浮点数可以工作,但会给你一个较弱的优化器——反思 LM 只能看到“这个得分为 0.6”而不是“答案事实正确但错过了时间限定”。
GEPA 从帕累托前沿采样进行探索,并返回选择的最高综合评分。反思 LM 的成本占主导地位——据此预算。在两个预测器、100 个示例的任务上,中等预算意味着大约 12 到 36 次对反思 LM 的调用。
工具和 ReAct
一个 dspy.Tool 是一个 Pydantic 模型,覆盖一个可调用函数。默认情况下,inspect.signature 从其元数据中反向工程出模式、类型和描述。ReAct 模块运行一个思考 → 工具名称 → 工具参数 → 观察的循环,其中 finish 工具终止它。
工具执行错误会成为观察,而不是崩溃。LM 在下一次迭代中看到错误文本并可以恢复。轨迹截断通过丢弃最旧的工具调用来处理上下文溢出。
MCP 工具通过 Tool.from_mcp_tool(session, tool) 跨接进来。始终异步,因为 MCP 客户端会话是原生异步的。
设置:隐式但可组合
dspy.configure() 用于全局设置,dspy.context() 用于作用域覆盖。设置通过 Python 的 contextvars 传播——跨越 await 和 asyncio.create_task。
注意事项:普通的 threading.Thread 不继承覆盖。使用 dspy.Parallel 或手动快照并重新应用 thread_local_overrides。
API 密钥从不序列化。allow_pickle 默认为 False。allow_unsafe_lm_state 默认剥离端点。安全模型是故意增加摩擦——在调用点上显式做出信任决策。
保存和加载
两条路径:状态仅(JSON,可读,可差异)和完整程序(cloudpickle,当加载方没有你的源代码时)。load_state 是事务性的——它首先加载到一个深拷贝中,只有在试验成功后才提交。
编译一次,保存,重新加载。只有当你将优化器成本摊销到许多推理调用时,经济才有效。
最后总结
DSPy 是一个编译器。你声明程序的结构(签名 + 模块),定义什么是“好”(度量),然后让优化器找到最佳版本。三个旋钮——指令、演示和权重——涵盖了框架暴露的每一个搜索空间。
最令我印象深刻的部分:通过深拷贝实现的不可变性模式,使优化器并行安全,基于跟踪的度量切换,用一个函数服务于评估和优化,以及 Flex——模块的源代码本身是可优化的参数。最后一个功能尚处于实验阶段,接口仍在变化,但它真正实现了“编程,而不是提示”的概念。
如果你正在使用 LLM 构建任何非平凡的东西,停止手动编写提示。编写一个签名,定义一个度量,让编译器完成工作。