专栏名称: 张铁蕾
老程序猿,全栈攻城狮,CTO,与你一起讨论技术干货和个人成长。
目录
相关文章推荐
爱可可-爱生活  ·  【AI-Powered Podcast ... ·  22 小时前  
国际金融报  ·  马斯克寻求收购OpenAI,奥尔特曼回应 ·  昨天  
金融早实习  ·  平安资管2025社会招聘 ·  2 天前  
51好读  ›  专栏  ›  张铁蕾

浅谈DSPy和自动化提示词工程(中)

张铁蕾  · 公众号  ·  · 2024-12-04 07:55

正文

一张琴,一壶酒,一溪云。

书接上回,接着写点技术

在上一篇文章《 浅谈DSPy和自动化提示词工程(上) 》中,我们解析了一个典型DSPy优化程序的骨架代码。本篇我们继续分析两个遗留的关键问题:

  • 从Signature到Prompt的过程。

  • MIPROv2的具体实现。

从Signature到Prompt

在DSPy的官网[1]和Github主页[2]上,第一句话是这样介绍DSPy的:

DSPy is the framework for programming—rather than prompting—language models.

(译文) DSPy是通过「编程」的方式——而不是写提示词的方式——来使用语言模型的框架。

所谓 programming 的方式,我们在上篇文章分析代码的时候已经发现这个现象了:

cot = dspy.ChainOfThought('context, question -> response')

这行代码创建了一个Module实例 cot ,但没有明确指明任何提示词或提示词模板。 'context, question -> response' 的形式类似于一个函数签名,它的意思就表示,这个Module接收两个输入 context question ,调用LLM后则输出一个 response

这种编程风格是DSPy的一种设计选择,故意把真正的prompt「隐藏」在了后面。这种方式可能有利有弊,我们眼下先不讨论。在这里,我们看一下背后真正的prompt是什么,有助于更好地理解后面的优化过程。

我们运行一下前面的 cot Module,如下图:

注意上图中这一行代码:

cot(context=context, question=response)

我们看到调用 cot 这个Module的时候传入了两个参数: context question ,这与前面指定的Signature的形式是相符的,即 'context, question -> response'

这行代码背后做了很多事情:它根据输入的参数组装成了一个prompt,然后调用了LLM,并返回了响应 response 。而且,对于 dspy.ChainOfThought 来说,它还多返回了一个输出字段 reasoning ,这是LLM输出的推理过程。

cot Module与LLM的交互过程,可以调用dspy.inspect_history方法来查看。这个交互过程包含了输入给LLM的prompt和LLM输出的completion。如下图(点击看大图):

好了,现在我们看到了真正的prompt了。上图中「Response:」那行之前是prompt,「Response:」那行之后则是LLM的输出。

在DSPy框架中,凡是调用LLM地方,prompt基本上都是遵循类似的这样一个模板格式。这也包括使用LLM计算Metric的时候,以及DSPy优化器在工作时调用LLM生成新的prompt的时候。正是因为DSPy使用了这样一种相对「固定」的内部prompt模板格式,才使得它能够让开发者在写程序时不需要指定具体的prompt,实现了框架所宣称的「programming—rather than prompting—language models」。

上图还有一个值得注意的地方,在System message最后一行:

Given the fields `context`, `question`, produce the fields `response`.

这一行的内容,就是prompt中的 instruction (指令) 。当DSPy的优化器工作的时候,其中的一个步骤就是会重写这个instruction。

MIPROv2的优化过程详解

在看到了DSPy中真正的prompt之后,我们来仔细审视一下DSPy优化器的工作过程。

回忆一下上篇文章中调用优化器的代码:

...

rag = RAG()

# Part 5: 初始化Teleprompter并完成编译/优化
# dspy.MIPROv2是Teleprompter的子集
tp = dspy.MIPROv2(
metric=metric,
auto="light",
num_threads=8
)
optimized_rag = tp.compile(rag, trainset=trainset, valset=valset,
max_bootstrapped_demos=2, max_labeled_demos=2,
requires_permission_to_run=False)

MIPROv2是DSPy中一类比较重要的优化器。它的具体算法实现出自论文“Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programs”[3]。MIPROv2同时优化prompt中的两个部分:一个是在上一节我们看到的 instruction ,另一个是few-shot部分。在Heiko Hotz发表的一篇博客文章中[4],优化few-shot的做法在APE工程中被称为 exemplar selection

概括来看,MIPROv2的执行过程可以分为三个大的步骤:

  • Step 1: 通过Bootstrap的方式,生成few-shot候选集。

  • Step 2: 生成prompt中的instruction候选集。

  • Step 3: 从候选集中选出最佳的few-shot和instruction组合。

下面我们通过实际运行的例子来介绍这三个步骤的详细情况。

Step 1: 生成few-shot候选集

这一步的目的是,基于trainset的数据,为DSPy程序的每一个子Module生成few-shot例子。用于生成的策略主要有两种。

第一种策略,是从trainset中直接采样,采样得到的example直接作为每个子Module的few-shot候选。采样的数目,就是由前面代码中的 max_labeled_demos 来指定的。

第二种策略,真正地被称为 Bootstrap 。它的做法是这样:从trainset中随机选择一些example,放到DSPy程序( rag Module)中去执行,并在执行过程中记录程序的各个子Module的输入和输出。如果DSPy程序执行完端到端的输出能够通过由Metric所定义的评测标准,那么各个子Module就将它们各自的输入、输出作为few-shot候选。

之所以需要执行这种Bootstrap策略,是因为在DSPy中,优化过程是针对整个DSPy程序的,而不是仅针对单个LLM Module的。**DSPy把一个程序看成是由多个Module组成的,而且整个程序的执行是一个多阶段 (Multi-Stage) 的pipeline。**trainset是针对整个程序的端到端的标注,里面通常没有针对中间结果的标注信息。因此,Bootstrap策略相当于是利用端到端的trainset标注数据,通过程序自动化地对中间结果产生了部分标注数据,从而省去了对中间结果进行人工标注的大量工作。

由Bootstrap策略为每个Module生成的few-shot例子的数目,是由前面代码中的 max_bootstrapped_demos 来指定的。

这样说来,前面第一种策略,从trainset中直接采样作为few-shot候选,当DSPy程序包含多个子Module时,这种策略很可能发挥不了真正的效力。因为,中间Module预期的输出标注信息,在trainset中很可能没有。

Bootstrap策略可以执行多次。只要trainset足够大,MIPROv2算法会每次把trainset进行一次shuffle,然后重复执行Bootstrap策略就得到一个新的few-shot候选集。

回到前面代码的执行,经过Step 1之后,生成的few-shot候选集如下所示:

简单解释一下上图的结果:

demo_candidates的结果显示了 rag 程序中只有一个Module(实际是个 dspy.ChainOfThought 实例),其编号为0,且Step 1为这个Module生成了5个few-shot候选集。

demo_candidates[0]的结果显示了这个唯一的Module的5个few-shot候选集,其中每个候选集包含2个example(由参数 max_bootstrapped_demos max_labeled_demos 指定的)。如果大家仔细观察的话,会发现example有两种形式:

  • 一种是带 augmented 字段的。表示这个example是由Bootstrap策略生成的。

  • 另一种是不带 augmented 字段的。表示这个example是直接从trainset中采样得到的。

这5个few-shot候选集,并非都会用在最后的prompt中。最后还会进行一次筛选。

Step 2: 生成instruction候选集

这一步针对程序的每个Module的每个few-shot候选集,都生成一个全新的instruction候选。

新的instruction具体是如何生成?我们来查看其中的一个实际例子。下图展示了其中一次生成instruction时与LLM的交互过程(包含meta-prompt和LLM输出):

上图显示meta-prompt中包含了非常多的信息,这非常有启发性。现在我们来理解一下上图中的很多信息。

首先,上图中「Response:」那行之前就是我们在上篇文章中提到的meta-prompt,而「Response:」之后则是LLM的输出。

在这个meta-prompt中,我们可以看到,为了生成新的instruction,DSPy喂给了LLM很多参考信息,包括:

  • 对数据集的描述,即 [[ ## dataset_description ## ]] 字段。这里实际上是对 trainset 的一个自然语言描述,这个描述也是由LLM来总结生成的。

  • 程序的代码和描述,即 [[ ## program_code ## ]] [[ ## program_description ## ]] 字段。

  • 作为参考起点的基础instruction,即 [[ ## basic_instruction ## ]] 字段。

  • 指导生成的tip,它用于引导生成instruction的方向以及风格,即 [[ ## tip ## ]] 字段。

生成instruction的tip,DSPy框架提供了一些预置的配置,如下:

TIPS = {
"none": "",
"creative": "Don't be afraid to be creative when creating the new instruction!",
"simple": "Keep the instruction clear and concise.",
"description": "Make sure your instruction is very informative and descriptive.",
"high_stakes": "The instruction should include a high stakes scenario in which the LM must solve the task!",
"persona": 'Include a persona that is relevant to the task in the instruction (ie. "You are a ...")',
}

在上图中,生成instruction时使用了“creative”的tip,它鼓励LLM生成更有创造性的instruction。 [[ ## proposed_instruction ## ]] 字段的值就是在引导下最终生成的instruction。

经过完整的Step 2执行之后,生成的instruction候选集如下所示:







请到「今天看啥」查看全文