微信扫码
与创始人交个朋友
我要投稿
Prompt(提示),实质上是一段自然语言文本,它极大地降低了操纵模型的复杂度,对于AI应用普及带来了极大的推动。然而,当下以langchain、llamaindex为代表的过程式的LLM应用开发框架却也存在着一些问题。
首先,LLM 对不同的Prompt反应非常敏感,再加上应用Pipeline或者Chain围绕prompt构建,因此提示间微小的差异都会带来很大的影响(比如加个尊称),十分脆弱,开发过程中不得不小心进行调试验证。其次,由于自然语言的模式导致代码繁琐,维护困难,当然基于此也出现了prompt模板这样的缓解措施,但仍然不够优雅。再次,随着模型及提示技术发展,原来的提示需要同步迭代优化,而原来的方式想要完成这样的工作,却非常困难。因为,往往LLM应用的业务流程与Prompt本身耦合在一起,每次更改Pipeline、LLM 或数据时,所有Prompt,甚至微调流程都需要更改,牵一发而动全身。LangChain等更是引入了langsmith这样的系统来监控每一个步骤,来确保到底问题出现在哪里。
基于上面的问题,受到pytorch的声明式、模块化的设计思想启发,斯坦福研究团队提出了DSPy(https://github.com/stanfordnlp/dspy),它引入了一种声明式的方法来开发和优化 LM(语言模型)应用。它将 LLM 视为一个模块,根据它与流水线中其他组件的交互方式自动调整行为。其理念是将围绕如何设计Prompt转变为如何设计好一个系统,而无需关注细节,每当修改代码、数据、断言或指标时,都可以重新编译程序,DSPy 会生成新的有效提示来适应变更。
官方指出它的三大特点:
Dspy应用的开发过程如下:
它相较于langchain等围绕Prompt构建应用chain/Pipeline的过程式开发,DSPy 引入了签名(用于抽象Prompt)、模块(用于抽象Prompt技巧)和优化器(可以调整模块的提示(或LM权重))将应用结构和流程与模型权重及Prompt调整分离。
下面是它三个重要的领域概念的详细介绍:
签名(Signature)是对 DSPy 模块的输入/输出行为的声明式规范。签名使得只需要告诉语言模型它需要做什么,而不是描述语言模型如何去做。引入签名是避免直接手写Prompt,而是通过框架自动优化生成高质量Prompt。这样有助于编写模块化且干净的代码,其中对于LM 调用可以被优化成高质量的Prompt(或自动微调)。
模块(Module)是使用LM的程序的Block,类似于Pytorch中的Convolution等。每个内置模块都抽象化了一种提示技术(例如COT或 ReAct)。最重要的是,它们被泛化以处理任何 DSPy 签名。DSPy 模块具有可学习的参数(即构造提示和 LM可微调的少量权重),可以被调用来处理输入并返回输出。多个模块可以组合成更大的模块(程序)。常见的Module有dspy.Predict、dspy.Retrieve、dspy.ChainOfThought、dspy.ReAct等。
优化器(Optimizer,原名:Teleprompter)是一种优化算法,类似于Pytorch中的优化器,如SGD或Adam,可以调整 DSPy 程序的参数(例如提示或语言模型权重),以最大限度地提高指定的指标(Metric),例如准确性。DSPy 有很多内置的优化器,它们采用不同的策略,如LabeledFewShot、BootstrapFewShotWithRandomSearch、KNNFewShot、COPRO、BootstrapFinetune等。
一个典型的 DSPy 优化器编译执行需要三个输入:
DSPy 程序。由一个或者多个模块(例如,dspy.Predict)组成的LM应用(模块)。
指标(Metric)。这是一个评估程序输出并为其分配分数(分数越高越好)的函数,类似于定义loss function。如:
def validate_answer(example, pred, trace=None):return example.answer.lower() == pred.answer.lower(
训练样本数据。可以很少(例如,只有 5 或 10 个例子)甚至没有标签(如果有大量数据,DSPy 也可以利用它。但推荐从小一点点迭代,进而取得更好的效果。)
from dspy.teleprompt import BootstrapFewShotWithRandomSearch
# Set up the optimizer: we want to "bootstrap" (i.e., self-generate) 8-shot examples of your program's steps.
# The optimizer will repeat this 10 times (plus some initial attempts) before selecting its best attempt on the devset.
config = dict(max_bootstrapped_demos=4, max_labeled_demos=4, num_candidate_programs=10, num_threads=4)
teleprompter = BootstrapFewShotWithRandomSearch(metric=YOUR_METRIC_HERE, **config)
optimized_program = teleprompter.compile(YOUR_PROGRAM_HERE, trainset=YOUR_TRAINSET_HERE)
下面是一个利用DSPy实现RAG的官方例子:
1)配置LM和RM(Retrieve Model)。
import dspy
turbo = dspy.OpenAI(model='gpt-3.5-turbo')
colbertv2_wiki17_abstracts = dspy.ColBERTv2(url='http://20.102.90.50:2017/wiki17_abstracts')
dspy.settings.configure(lm=turbo, rm=colbertv2_wiki17_abstracts)
2)加载数据集
from dspy.datasets import HotPotQA
# Load the dataset.
dataset = HotPotQA(train_seed=1, train_size=20, eval_seed=2023, dev_size=50, test_size=0)
# Tell DSPy that the 'question' field is the input. Any other fields are labels and/or metadata.
trainset = [x.with_inputs('question') for x in dataset.train]
devset = [x.with_inputs('question') for x in dataset.dev]
len(trainset), len(devset)
3)构造签名
因为是构建 RAG Pipeline,故定义签名为:context,question --> answer。
class GenerateAnswer(dspy.Signature):
"""Answer questions with short factoid answers."""
context = dspy.InputField(desc="may contain relevant facts")
question = dspy.InputField()
answer = dspy.OutputField(desc="often between 1 and 5 words")
4)构建RAG Pipeline。
class RAG(dspy.Module):
def __init__(self, num_passages=3):
super().__init__()
self.retrieve = dspy.Retrieve(k=num_passages)
self.generate_answer = dspy.ChainOfThought(GenerateAnswer)
def forward(self, question):
context = self.retrieve(question).passages
prediction = self.generate_answer(context=context, question=question)
return dspy.Prediction(context=context, answer=prediction.answer)
5)编译优化该Pipeline。
在已经定义了这个程序,现在让我们编译它。编译程序会更新存储在每个模块中的参数。编译程序时还依赖下面三点:
一个训练集。使用上面训练集中 20 个问答示例。
一个用于验证的指标。定义一个简单的Metric(validate_context_and_answer),它检查预测的答案是否正确,以及检索的上下文是否实际包含答案。
一个特定的优化器。
from dspy.teleprompt import BootstrapFewShot
# Validation logic: check that the predicted answer is correct.
# Also check that the retrieved context does actually contain that answer.
def validate_context_and_answer(example, pred, trace=None):
answer_EM = dspy.evaluate.answer_exact_match(example, pred)
answer_PM = dspy.evaluate.answer_passage_match(example, pred)
return answer_EM and answer_PM
# Set up a basic teleprompter, which will compile our RAG program.
teleprompter = BootstrapFewShot(metric=validate_context_and_answer)
# Compile!
compiled_rag = teleprompter.compile(RAG(), trainset=trainset)
编译好的程序可以保存在本地:
compiled_rag.save(YOUR_SAVE_PATH)
在使用时可按如下方式加载:
loaded_program = YOUR_PROGRAM_CLASS()loaded_program.load(path=YOUR_SAVE_PATH)
6)执行应用Pipeline
# Ask any question you like to this simple RAG program.
my_question = "What castle did David Gregory inherit?"
# Get the prediction. This contains `pred.context` and `pred.answer`.
pred = compiled_rag(my_question)
# Print the contexts and the answer.
print(f"Question: {my_question}")
print(f"Predicted Answer: {pred.answer}")
print(f"Retrieved Contexts (truncated): {[c[:200] + '...' for c in pred.context]}")
可利用下面命令查看最近的Prompt。
turbo.inspect_history(n=1)
输出:
Answer questions with short factoid answers.
---
Question: At My Window was released by which American singer-songwriter?
Answer: John Townes Van Zandt
Question: "Everything Has Changed" is a song from an album released under which record label ?
Answer: Big Machine Records
...(truncated)
可以看出,DSPy自动生成了带有COT的示例(shot)的Prompt。
7)评估Pipeline
可以用开发集(devset)来评估我们的compiled_rag程序。
from dspy.evaluate.evaluate import Evaluate
# Set up the `evaluate_on_hotpotqa` function. We'll use this many times below.
evaluate_on_hotpotqa = Evaluate(devset=devset, num_threads=1, display_progress=False, display_table=5)
# Evaluate the `compiled_rag` program with the `answer_exact_match` metric.
metric = dspy.evaluate.answer_exact_match
evaluate_on_hotpotqa(compiled_rag, metric=metric)
输出:
Average Metric: 22 / 50(44.0): 100%|██████████| 50/50 [00:00<00:00, 116.45it/s]
Average Metric: 22 / 50(44.0%)
44.0
8)评估检索结果
同时,也可以评估检索结果,以判定检索的结果中是否包含答案。
def gold_passages_retrieved(example, pred, trace=None):
gold_titles = set(map(dspy.evaluate.normalize_text, example['gold_titles']))
found_titles = set(map(dspy.evaluate.normalize_text, [c.split(' | ')[0] for c in pred.context]))
return gold_titles.issubset(found_titles)
compiled_rag_retrieval_score = evaluate_on_hotpotqa(compiled_rag, metric=gold_passages_retrieved)
输出:
Average Metric: 13 / 50(26.0): 100%|██████████| 50/50 [00:00<00:00, 671.76it/s]Average Metric: 13 / 50(26.0%)
可在colab上体验:https://colab.research.google.com/github/stanfordnlp/dspy/blob/main/intro.ipynb
小结
本文介绍新的大模型应用开发的新方式DSPy,通过介绍和案例,可以看出,开发者希望通过类似于使用Pytorch开发NN应用的方式开发大模型应用。其在设计思想及开发流程步骤上都有Pytorch应用的影子,这对于熟悉Pytorch模型应用开发的开发者来讲是容易上手的。另一方面,就如同开发NN模型程序一样,我们可以专注于流程结构,而无需过度关注模型权重及Prompt具体内容,这对于开发和后期优化来讲都是非常优雅的设计。在我们之前介绍的LLM应用成熟度(Google总监提出生成式AI应用架构和成熟度模型,一步步指导进阶)中也提到,DSPy是L6最高级的技术。随着LLM应用不断深入,它也将被越来越多的开发者所熟知。
53AI,企业落地应用大模型首选服务商
产品:大模型应用平台+智能体定制开发+落地咨询服务
承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2024-05-28
2024-08-13
2024-04-26
2024-08-21
2024-06-13
2024-07-09
2024-08-04
2024-04-11
2024-07-18
2024-07-01