论文精读 03|DSPy:把 LM 流程变成可编译程序
论文档案
| 项目 | 内容 |
|---|---|
| 论文 | DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines |
| 作者 | Omar Khattab 等 |
| 发表 | ICLR 2024 |
| 原文 | OpenReview / arXiv |
| 代码 | stanfordnlp/dspy |
| 本文定位 | 把 prompt 从手工字符串提升为可声明、可组合、可优化的程序参数 |
这篇论文为何属于 Harness
DSPy 通常被归为 prompting 或 LM programming,但它对 Harness 有直接意义:当 Agent 包含检索、推理、工具、反思等多个模型调用时,prompt 不再是一段文本,而是一个有结构的执行图。每个节点有输入输出契约,整个图有任务指标,编译器可以在固定程序结构下优化示例、指令或模型。
它要消除的脆弱工作流是:
改 prompt 字符串 -> 手工跑几个例子 -> 感觉变好 -> 上线替换成:
声明模块契约 -> 组合程序 -> 定义指标和训练样本
-> 编译器搜索参数 -> 在验证集选择 -> 固化产物三个核心抽象
Signature:描述“做什么”
Signature 是自然语言类型化的输入输出声明,例如:
"question -> answer"
"context, question -> answer"
"task, observation -> action"它刻意不写具体 prompt 模板,而是声明字段、角色和可选说明。编译器可以依据 signature 生成提示、选择 demonstrations 或用于微调。对 Harness 来说,这相当于模型调用层的接口契约。
Signature 的价值还在于减少手工解析。自由文本 prompt 常把格式说明、业务语义和示例揉在一起;字段化后,输入输出边界至少变得显式。但论文当时的类型系统仍较弱,不能替代 JSON Schema、权限检查或业务验证。
Module:把提示技术封装成可组合算子
最基础的 Predict 接收 signature,生成 prompt,调用 LM,再解析输出。更高层模块包括:
ChainOfThought:给 signature 增加 rationale 字段;ProgramOfThought:借助程序执行完成推理;MultiChainComparison:生成并比较多条推理链;ReAct:把工具使用循环封装为模块;Retrieve:连接 ColBERTv2、Pyserini、Pinecone 等检索器。
模块可以像普通代码一样放进 if、循环、异常处理和任意 forward 控制流。论文借鉴 PyTorch 的 define-by-run 思路:程序结构由开发者写,LM 调用参数由编译器优化。
Teleprompter:根据指标优化模块参数
论文把优化策略称作 teleprompter。它接收程序、少量训练样本和 metric,输出编译后的程序。典型流程分三步:
- 找出程序里所有
Predict节点; - 运行 teacher 或当前程序,收集完整多阶段 trace;
- 用 metric 过滤成功 trace,将中间输入输出变成各模块 demonstrations,再搜索组合。
BootstrapFewShot 类似带拒绝采样的轨迹蒸馏:坏轨迹丢掉,好轨迹给流水线各节点补标签。BootstrapFewShotWithRandomSearch 进一步搜索 demonstration 组合;BootstrapFinetune 则可让昂贵 teacher 产生数据,微调较小模型。
一个最小例子
论文中的简单 RAG 程序只需要两个模块:
class RAG(dspy.Module):
def __init__(self, k=3):
self.retrieve = dspy.Retrieve(k=k)
self.answer = dspy.ChainOfThought("context, question -> answer")
def forward(self, question):
context = self.retrieve(question).passages
return self.answer(context=context, question=question)程序声明了数据流,却没有手写长 prompt。给出训练集与 exact-match 指标后,编译器运行完整 RAG,只有最终答对的 trace 才可能成为检索和回答模块的 few-shot 示例。
这揭示一个重要观念:优化对象可以是整个 LM 程序的行为,而不是孤立的一次模型调用。
实验一:GSM8K 数学题
论文从 GSM8K 官方训练集抽取 200 个训练样本、300 个开发样本,并在约 1,300 个官方测试样本上报告结果。比较三个程序:
- 单步
Predict; ChainOfThought;- 生成 5 条链再用
MultiChainComparison汇总的 reflection 程序。
编译设置包括 zero-shot、随机 8-shot、bootstrap、二次 bootstrap 和 top-7 program ensemble。论文报告在不同程序与编译配置下,GPT-3.5 的简单程序可从约 33% 提升到 82%,Llama2-13B-chat 可从约 9% 提升到 47%;具体数字依赖所选程序、开发/测试切分与 ensemble,不能只摘最高值比较模型。
更值得注意的是,自动 bootstrap 在若干设置下能接近或超过使用人工 CoT 示例,说明 demonstration 的轨迹匹配和模块归属可能比单纯手写“优质答案”更重要。
实验二:HotpotQA 多跳检索
HotpotQA 实验比较 vanilla、单跳 CoT-RAG、ReAct 和显式两跳 multihop 程序。后者先生成查询、检索,再结合已有 passages 生成下一跳查询,最后回答。
| 设置 | GPT-3.5 Test Answer EM | Llama2-13B Test Answer EM |
|---|---|---|
| vanilla few-shot | 31.5 | 21.8 |
| multihop few-shot | 31.2 | 31.3 |
| multihop bootstrap | 39.6 | 36.4 |
| multihop ensemble | 45.6(半数测试集) | 41.0 |
配对 passage retrieval accuracy 也有明显变化。论文还用 200 个有标签、800 个无标签问题,通过 teacher 编译 T5-Large(770M),在开发集达到 39.3% answer EM 和 46.0% passage accuracy。
这组结果支持两点:流水线拓扑很重要,编译器也很重要。一个只会优化 prompt、但程序结构选错的系统,仍可能被检索上限卡住。
对 Agent Harness 最重要的五个启示
1. Prompt 应有源码与编译产物之分
Signature、程序结构、metric 和训练集是“源码”;最终 instructions、demonstrations、模型选择是“编译产物”。二者都应版本化。线上事故排查时,只记录原始 DSPy 代码而不保存编译后的 prompt,无法复现真实调用。
2. Metric 实际上定义了 Agent 的优化方向
如果 metric 只有 exact match,编译器不会主动优化证据忠实、延迟或成本。DSPy 支持把 metric 写成程序,这很灵活,也意味着评估器错误会被系统性放大。Harness 的 evaluator 必须纳入审查、测试和版本管理。
3. 成功轨迹筛选会产生幸存者偏差
Bootstrap 丢弃失败 trace,再把成功 trace 作为示例。这可能学到真正有效的步骤,也可能学到偶然相关的表面模式。需要独立验证集、多个随机种子和分布外测试;不能在同一小样本上反复编译后再报告训练表现。
4. 编译有成本,运行时也有成本
论文中的编译从数分钟到数十分钟,复杂设置运行程序数千次。Ensemble 进一步提高线上调用量。实际系统应同时跟踪质量、token、延迟、金钱和失败恢复,而不是只最大化单一准确率。
5. 可组合不等于可治理
DSPy 模块能调用检索器、SQL 或 Python,但论文的核心抽象并不负责沙箱、权限、审批与副作用。把 DSPy 用作策略层时,工具执行仍需经过独立 Harness policy gate。
与传统 Agent 框架的边界
| 需求 | DSPy 擅长 | 仍需 Harness 补齐 |
|---|---|---|
| 模型调用契约 | signature 与字段化 I/O | 严格 schema、兼容版本 |
| 多步骤逻辑 | Python 控制流和模块组合 | checkpoint、恢复、幂等 |
| 自动优化 | metric 驱动的 prompt/示例/微调 | 数据治理、上线门禁 |
| 工具接入 | Retrieve、ReAct 等模块 | 权限、隔离、审计 |
| 评估 | 可编程 metric | 环境级真实状态验证 |
| 复现 | 编译程序可固化 | 模型快照、数据快照、完整 trace |
建议的复现实验
用一个两跳检索任务就能验证核心主张:
- 写
query -> passages与passages, question -> answer两个 signature; - 做单跳 RAG 与两跳 RAG 两种程序;
- 每种程序比较手写 few-shot、bootstrap、随机搜索;
- 固定训练/验证/测试切分,编译只看训练和验证;
- 同时报告 answer EM、证据召回、总调用数、token 与延迟;
- 保存编译前后每个模块的 instructions 和 demonstrations;
- 用一个换主题的分布外集合测试是否只记住了演示风格。
若要把它用于 Agent,额外做工具失败注入:超时、空结果、格式变化。观察编译出的 demonstrations 是帮助恢复,还是让模型过拟合正常路径。
局限与阅读警告
- 论文重点是编程模型与初步实证,不是成熟的全局最优编译算法;
- 训练、验证样本规模较小,反复优化容易过拟合;
- 不同文献的 HotpotQA 抽样、检索器和评测协议并不一致;
- 高分配置常包含二次 bootstrap 或 ensemble,成本不可忽略;
- 自动生成的中间标签仍来自模型,不能替代真实监督;
- DSPy 解决可优化性,不自动解决 Agent 工具执行的安全性。
DSPy 对 Harness 的长期意义,是让“模型外系统”也拥有类似软件工程的抽象:接口、模块、程序、编译和指标。下一步不是把所有 prompt 都塞进 DSPy,而是建立完整产物链:可读源码、可重现编译、可审计运行时、独立验收和可回滚发布。