本节摘要:程序从单步扩展到多步会发生什么:以多跳问答与检索增强生成为例,看查询改写、证据聚合如何表达为模块组合,理解"结构复杂度与数据规模配平"原则,并掌握多步程序编译时的两个特有陷阱——错误归因漂移与中间分布偏移。
单步程序里,编译器优化的只是一个预测器;多步程序里,一个问题的回答要经过若干预测器接力,编译的复杂性不是线性增长而是组合式增长。本节用两个经典结构讲清这种复杂性:检索增强生成(RAG,检索一次、作答一次)与多跳问答(检索-改写-再检索-作答)。
RAG 是最常用的两步结构,模块写法在 3.2 节已经给过骨架,这里补上编译视角的关键细节:
import dspy class GenerateAnswer(dspy.Signature): """只依据给定上下文回答问题;上下文不足以回答时明确说依据不足。""" context = dspy.InputField(desc="与问题相关的证据文段") question = dspy.InputField() answer = dspy.OutputField(desc="短答案,或说明依据不足") class RAG(dspy.Module): def __init__(self, k: int = 5): super().__init__() self.retrieve = dspy.Retrieve(k=k) self.generate = dspy.ChainOfThought(GenerateAnswer) def forward(self, question): context = self.retrieve(question).passages return self.generate(context="\n".join(context), question=question)
编译视角要注意的是签名描述里那句"上下文不足时明确说依据不足"。这不是措辞装饰——它定义了任务在坏情况下的行为契约。多步程序的指标也应配合这个契约:证据不足时,老实的"依据不足"应该得分,硬编的答案应该扣分。指标若只认"答案与标准一致",编译器会学着无视上下文硬答,检索层的存在感被编译过程主动削弱——多步程序里指标失真的破坏力比单步程序大得多,因为它同时毒害多个预测器的示范池。
多跳问题的形态是"第一部《哈利·波特》小说作者的出生地在哪里"——需要先查作者,再用作者查出生地。手工时代这类问题的提示词要写得很绕;DSPy 里它就是两个预测器的串联:
class HopQuery(dspy.Signature): """根据已有信息,提出回答最终问题还缺的那一步查询。""" question = dspy.InputField(desc="用户的原始问题") known_so_far = dspy.InputField(desc="已经查到的信息摘要") next_query = dspy.OutputField(desc="下一步检索查询词") class FinalAnswer(dspy.Signature): """综合两轮检索证据回答原始问题。""" question = dspy.InputField() context = dspy.InputField(desc="两轮检索的全部证据") answer = dspy.OutputField() class MultiHop(dspy.Module): def __init__(self, k: int = 3): super().__init__() self.hop = dspy.ChainOfThought(HopQuery) self.answer = dspy.ChainOfThought(FinalAnswer) self.retrieve = dspy.Retrieve(k=k) def forward(self, question): first = self.retrieve(question).passages known = "\n".join(first) next_query = self.hop(question=question, known_so_far=known).next_query second = self.retrieve(next_query).passages context = "\n".join(first + second) return self.answer(question=question, context=context)
结构一目了然:第一跳检索原始问题,跳查询预测器生成"还缺什么"的查询,第二跳补证据,最终作答器汇总。注意两个预测器各有签名、各有示范池,编译时优化器会为它们分别搜索——第一跳学到的示范是"如何提出好的追问",第二跳学的是"如何综合证据"。这种分而治之的优化正是多步结构相对"一个巨型提示词硬扛"的根本优势:手工巨型提示词里,两种能力互相干扰;模块化结构里,各学各的。
结构复杂度与数据规模必须配平,这条原则在多步程序上给出具体数字:两个预测器的程序建议训练集不少于一百条;三个以上预测器,两百条起步更稳。道理在自举机制里:每个预测器的示范池都靠训练集轨迹喂养,轨迹要经过全链路才能产生,链路越长、合格轨迹的产出率越低,同样的训练集能供养的示范就越少。
陷阱一:错误归因漂移。编译期自举失败时,你看到的表象往往是"最终答案错",但真实病因可能在任何一跳——查询改写得糟糕、检索没捞到、作答时没用好证据。排查手段是把链路拆开单测:先用人工构造的完美中间结果喂给下游预测器,看它单独的表现是否达标,逐段定位。多步程序编译失败时禁止整体重跑碰运气——链路的每一跳都有自己的病灶。
陷阱二:中间分布偏移。编译后的程序上线,遇到训练分布之外的输入时,中间预测器可能产出畸形查询(比如过长或过泛的检索词),错误沿着链路放大。缓解手段有二:在中间签名的 desc 里明确约束查询形态("不超过十个词的关键词查询");或用 5.3 节的断言机制在中间步骤设卡,畸形中间结果直接回溯重来,不让脏水流进下游。