论文精读 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,输出编译后的程序。典型流程分三步:

  1. 找出程序里所有 Predict 节点;
  2. 运行 teacher 或当前程序,收集完整多阶段 trace;
  3. 用 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 EMLlama2-13B Test Answer EM
vanilla few-shot31.521.8
multihop few-shot31.231.3
multihop bootstrap39.636.4
multihop ensemble45.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

建议的复现实验

用一个两跳检索任务就能验证核心主张:

  1. 写 query -> passages 与 passages, question -> answer 两个 signature;
  2. 做单跳 RAG 与两跳 RAG 两种程序;
  3. 每种程序比较手写 few-shot、bootstrap、随机搜索;
  4. 固定训练/验证/测试切分,编译只看训练和验证;
  5. 同时报告 answer EM、证据召回、总调用数、token 与延迟;
  6. 保存编译前后每个模块的 instructions 和 demonstrations;
  7. 用一个换主题的分布外集合测试是否只记住了演示风格。

若要把它用于 Agent,额外做工具失败注入:超时、空结果、格式变化。观察编译出的 demonstrations 是帮助恢复,还是让模型过拟合正常路径。

局限与阅读警告

  • 论文重点是编程模型与初步实证,不是成熟的全局最优编译算法;
  • 训练、验证样本规模较小,反复优化容易过拟合;
  • 不同文献的 HotpotQA 抽样、检索器和评测协议并不一致;
  • 高分配置常包含二次 bootstrap 或 ensemble,成本不可忽略;
  • 自动生成的中间标签仍来自模型,不能替代真实监督;
  • DSPy 解决可优化性,不自动解决 Agent 工具执行的安全性。

DSPy 对 Harness 的长期意义,是让“模型外系统”也拥有类似软件工程的抽象:接口、模块、程序、编译和指标。下一步不是把所有 prompt 都塞进 DSPy,而是建立完整产物链:可读源码、可重现编译、可审计运行时、独立验收和可回滚发布。

延伸阅读