LangChain 实战课-基础篇
03|模型I/O:输入提示、调用模型、解析输出
从这节课开始,我们将对LangChain中的六大核心组件一一进行详细的剖析。
模型,位于LangChain框架的最底层,它是基于语言模型构建的应用的 核心元素,因为所谓LangChain应用开发,就是以LangChain作为框架,通过API调用大模型来解决具体问题的过程。
可以说,整个LangChain框架的逻辑都是由LLM这个发动机来驱动的。没有模型,LangChain这个框架也就失去了它存在的意义。那么这节课我们就详细讲讲模型,最后你会收获一个能够自动生成鲜花文案的应用程序。

Model I/O
我们可以把对模型的使用过程拆解成三块,分别是 输入提示(对应图中的Format)、 调用模型(对应图中的Predict)和 输出解析(对应图中的Parse)。这三块形成了一个整体,因此在LangChain中这个过程被统称为 Model I/O(Input/Output)。

在模型 I/O的每个环节,LangChain都为咱们提供了模板和工具,快捷地形成调用各种语言模型的接口。
- 提示模板:使用模型的第一个环节是把提示信息输入到模型中,你可以创建LangChain模板,根据实际需求动态选择不同的输入,针对特定的任务和应用调整输入。
- 语言模型:LangChain允许你通过通用接口来调用语言模型。这意味着无论你要使用的是哪种语言模型,都可以通过同一种方式进行调用,这样就提高了灵活性和便利性。
- 输出解析:LangChain还提供了从模型输出中提取信息的功能。通过输出解析器,你可以精确地从模型的输出中获取需要的信息,而不需要处理冗余或不相关的数据,更重要的是还可以把大模型给回的非结构化文本,转换成程序可以处理的结构化数据。
下面我们用示例的方式来深挖一下这三个环节。先来看看LangChain中提示模板的构建。
提示模板
语言模型是个无穷无尽的宝藏,人类的知识和智慧,好像都封装在了这个“魔盒”里面了。但是,怎样才能解锁其中的奥秘,那可就是仁者见仁智者见智了。所以,现在“提示工程”这个词特别流行,所谓Prompt Engineering,就是专门研究对大语言模型的提示构建。
我的观点是,使用大模型的场景千差万别,因此肯定不存在那么一两个神奇的模板,能够骗过所有模型,让它总能给你最想要的回答。然而,好的提示(其实也就是好的问题或指示啦),肯定能够让你在调用语言模型的时候事半功倍。
那其中的具体原则,不外乎吴恩达老师在他的 提示工程课程 中所说的:
- 给予模型清晰明确的指示
- 让模型慢慢地思考
说起来很简单,对吧?是的,道理总是简单,但是如何具体实践这些原则,又是个大问题。让我从创建一个简单的LangChain提示模板开始。
这里,我们希望为销售的每一种鲜花生成一段简介文案,那么每当你的员工或者顾客想了解某种鲜花时,调用该模板就会生成适合的文字。
这个提示模板的生成方式如下:
## 导入LangChain中的提示模板
from langchain import PromptTemplate
## 创建原始模板
template = """您是一位专业的鲜花店文案撰写员。\n
对于售价为 {price} 元的 {flower_name} ,您能提供一个吸引人的简短描述吗?
"""
## 根据原始模板创建LangChain提示模板
prompt = PromptTemplate.from_template(template)
## 打印LangChain提示模板的内容
print(prompt)
提示模板的具体内容如下:
input_variables=['flower_name', 'price']
output_parser=None partial_variables={}
template='/\n您是一位专业的鲜花店文案撰写员。
\n对于售价为 {price} 元的 {flower_name} ,您能提供一个吸引人的简短描述吗?\n'
template_format='f-string'
validate_template=True
在这里,所谓“模板”就是一段描述某种鲜花的文本格式,它是一个 f-string,其中有两个变量 {flower_name} 和 {price} 表示花的名称和价格,这两个值是模板里面的占位符,在实际使用模板生成提示时会被具体的值替换。
代码中的from_template是一个类方法,它允许我们直接从一个字符串模板中创建一个PromptTemplate对象。打印出这个PromptTemplate对象,你可以看到这个对象中的信息包括输入的变量(在这个例子中就是 flower_name 和 price)、输出解析器(这个例子中没有指定)、模板的格式(这个例子中为 'f-string')、是否验证模板(这个例子中设置为 True)。
因此PromptTemplate的from_template方法就是将一个原始的模板字符串转化为一个更丰富、更方便操作的PromptTemplate对象,这个对象就是LangChain中的提示模板。LangChain 提供了多个类和函数,也为各种应用场景设计了很多内置模板,使构建和使用提示变得容易。我们下节课还会对提示工程的基本原理和LangChain中的各种提示模板做更深入的讲解。
下面,我们将会使用这个刚刚构建好的提示模板来生成提示,并把提示输入到大语言模型中。
语言模型
LangChain中支持的模型有三大类。
- 大语言模型(LLM) ,也叫Text Model,这些模型将文本字符串作为输入,并返回文本字符串作为输出。Open AI的text-davinci-003、Facebook的LLaMA、ANTHROPIC的Claude,都是典型的LLM。
- 聊天模型(Chat Model),主要代表Open AI的ChatGPT系列模型。这些模型通常由语言模型支持,但它们的 API 更加结构化。具体来说,这些模型将聊天消息列表作为输入,并返回聊天消息。
- 文本嵌入模型(Embedding Model),这些模型将文本作为输入并返回浮点数列表,也就是Embedding。而文本嵌入模型如OpenAI的text-embedding-ada-002,我们之前已经见过了。文本嵌入模型负责把文档存入向量数据库,和我们这里探讨的提示工程关系不大。
然后,我们将调用语言模型,让模型帮我们写文案,并且返回文案的结果。
## 设置OpenAI API Key
import os
os.environ["OPENAI_API_KEY"] = '你的Open AI API Key'
## 导入LangChain中的OpenAI模型接口
from langchain import OpenAI
## 创建模型实例
model = OpenAI(model_name='text-davinci-003')
## 输入提示
input = prompt.format(flower_name=["玫瑰"], price='50')
## 得到模型的输出
output = model(input)
## 打印输出内容
print(output)
input = prompt.format(flower_name=["玫瑰"], price='50') 这行代码的作用是将模板实例化,此时将 {flower_name} 替换为 "玫瑰", {price} 替换为 '50',形成了具体的提示:“您是一位专业的鲜花店文案撰写员。对于售价为 50 元的玫瑰,您能提供一个吸引人的简短描述吗?”
接收到这个输入,调用模型之后,得到的输出如下:
让你心动!50元就可以拥有这支充满浪漫气息的玫瑰花束,让TA感受你的真心爱意。
复用提示模板,我们可以同时生成多个鲜花的文案。
## 导入LangChain中的提示模板
from langchain import PromptTemplate
## 创建原始模板
template = """您是一位专业的鲜花店文案撰写员。\n
对于售价为 {price} 元的 {flower_name} ,您能提供一个吸引人的简短描述吗?
"""
## 根据原始模板创建LangChain提示模板
prompt = PromptTemplate.from_template(template)
## 打印LangChain提示模板的内容
print(prompt)
## 设置OpenAI API Key
import os
os.environ["OPENAI_API_KEY"] = '你的Open AI API Key'
## 导入LangChain中的OpenAI模型接口
from langchain import OpenAI
## 创建模型实例
model = OpenAI(model_name='text-davinci-003')
## 多种花的列表
flowers = ["玫瑰", "百合", "康乃馨"]
prices = ["50", "30", "20"]
## 生成多种花的文案
for flower, price in zip(flowers, prices):
## 使用提示模板生成输入
input_prompt = prompt.format(flower_name=flower, price=price)
## 得到模型的输出
output = model(input_prompt)
## 打印输出内容
print(output)
模型的输出如下:
这支玫瑰,深邃的红色,传递着浓浓的深情与浪漫,令人回味无穷!
百合:美丽的花朵,多彩的爱恋!30元让你拥有它!
康乃馨—20元,象征爱的祝福,送给你最真挚的祝福。
你也许会问我,在这个过程中,使用LangChain的意义究竟何在呢?我直接调用Open AI的API,不是完全可以实现相同的功能吗?
的确如此,让我们来看看直接使用Open AI API来完成上述功能的代码。
import openai ## 导入OpenAI
openai.api_key = 'Your-OpenAI-API-Key' ## API Key
prompt_text = "您是一位专业的鲜花店文案撰写员。对于售价为{}元的{},您能提供一个吸引人的简短描述吗?" ## 设置提示
flowers = ["玫瑰", "百合", "康乃馨"]
prices = ["50", "30", "20"]
## 循环调用Text模型的Completion方法,生成文案
for flower, price in zip(flowers, prices):
prompt = prompt_text.format(price, flower)
response = openai.Completion.create(
engine="text-davinci-003",
prompt=prompt,
max_tokens=100
)
print(response.choices[0].text.strip()) ## 输出文案
上面的代码是直接使用Open AI和带有 {} 占位符的提示语,同时生成了三种鲜花的文案。看起来也是相当简洁。
不过,如果你深入思考一下,你就会发现LangChain的优势所在。 我们只需要定义一次模板,就可以用它来生成各种不同的提示。 对比单纯使用 f-string 来格式化文本,这种方法更加简洁,也更容易维护。而LangChain在提示模板中,还整合了output_parser、template_format 以及是否需要validate_template等功能。
更重要的是,使用LangChain提示模板,我们还可以很方便地把程序切换到不同的模型,而不需要修改任何提示相关的代码。
下面,我们用完全相同的提示模板来生成提示,并发送给HuggingFaceHub中的开源模型来创建文案。(注意:需要注册HUGGINGFACEHUB_API_TOKEN)

## 导入LangChain中的提示模板
from langchain import PromptTemplate
## 创建原始模板
template = """You are a flower shop assitiant。\n
For {price} of {flower_name} ,can you write something for me?
"""
## 根据原始模板创建LangChain提示模板
prompt = PromptTemplate.from_template(template)
## 打印LangChain提示模板的内容
print(prompt)
import os
os.environ['HUGGINGFACEHUB_API_TOKEN'] = '你的HuggingFace API Token'
## 导入LangChain中的OpenAI模型接口
from langchain import HuggingFaceHub
## 创建模型实例
model= HuggingFaceHub(repo_id="google/flan-t5-large")
## 输入提示
input = prompt.format(flower_name=["rose"], price='50')
## 得到模型的输出
output = model(input)
## 打印输出内容
print(output)
输出:
i love you
真是一分钱一分货,当我使用较早期的开源模型T5,得到了很粗糙的文案 “i love you”(哦,还要注意T5还没有支持中文的能力,我把提示文字换成英文句子,结构其实都没变)。
当然,这里我想要向你传递的信息是:你可以重用模板,重用程序结构,通过LangChain框架调用任何模型。如果你熟悉机器学习的训练流程的话,这LangChain是不是让你联想到PyTorch和TensorFlow这样的框架—— 模型可以自由选择、自主训练,而调用模型的框架往往是有章法、而且可复用的。
因此,使用LangChain和提示模板的好处是:
- 代码的可读性:使用模板的话,提示文本更易于阅读和理解,特别是对于复杂的提示或多变量的情况。
- 可复用性:模板可以在多个地方被复用,让你的代码更简洁,不需要在每个需要生成提示的地方重新构造提示字符串。
- 维护:如果你在后续需要修改提示,使用模板的话,只需要修改模板就可以了,而不需要在代码中查找所有使用到该提示的地方进行修改。
- 变量处理:如果你的提示中涉及到多个变量,模板可以自动处理变量的插入,不需要手动拼接字符串。
- 参数化:模板可以根据不同的参数生成不同的提示,这对于个性化生成文本非常有用。
那我们就接着介绍模型 I/O的最后一步,输出解析。
输出解析
LangChain提供的解析模型输出的功能,使你能够更容易地从模型输出中获取结构化的信息,这将大大加快基于语言模型进行应用开发的效率。
为什么这么说呢?请你思考一下刚才的例子,你只是让模型生成了一个文案。这段文字是一段字符串,正是你所需要的。但是,在开发具体应用的过程中,很明显 我们不仅仅需要文字,更多情况下我们需要的是程序能够直接处理的、结构化的数据。
比如说,在这个文案中,如果你希望模型返回两个字段:
- description:鲜花的说明文本
- reason:解释一下为何要这样写上面的文案
那么,模型可能返回的一种结果是:
A:“文案是:让你心动!50元就可以拥有这支充满浪漫气息的玫瑰花束,让TA感受你的真心爱意。为什么这样说呢?因为爱情是无价的,50元对应热恋中的情侣也会觉得值得。”
上面的回答并不是我们在处理数据时所需要的,我们需要的是一个类似于下面的Python字典。
B:{description: “让你心动!50元就可以拥有这支充满浪漫气息的玫瑰花束,让TA感受你的真心爱意。” ; reason: “因为爱情是无价的,50元对应热恋中的情侣也会觉得值得。”}
那么从A的笼统言语,到B这种结构清晰的数据结构,如何自动实现?这就需要LangChain中的输出解析器上场了。
下面,我们就通过LangChain的输出解析器来重构程序,让模型有能力生成结构化的回应,同时对其进行解析,直接将解析好的数据存入CSV文档。
## 通过LangChain调用模型
from langchain import PromptTemplate, OpenAI
## 导入OpenAI Key
import os
os.environ["OPENAI_API_KEY"] = '你的OpenAI API Key'
## 创建原始提示模板
prompt_template = """您是一位专业的鲜花店文案撰写员。
对于售价为 {price} 元的 {flower_name} ,您能提供一个吸引人的简短描述吗?
{format_instructions}"""
## 创建模型实例
model = OpenAI(model_name='text-davinci-003')
## 导入结构化输出解析器和ResponseSchema
from langchain.output_parsers import StructuredOutputParser, ResponseSchema
## 定义我们想要接收的响应模式
response_schemas = [
ResponseSchema(name="description", description="鲜花的描述文案"),
ResponseSchema(name="reason", description="问什么要这样写这个文案")
]
## 创建输出解析器
output_parser = StructuredOutputParser.from_response_schemas(response_schemas)
## 获取格式指示
format_instructions = output_parser.get_format_instructions()
## 根据原始模板创建提示,同时在提示中加入输出解析器的说明
prompt = PromptTemplate.from_template(prompt_template,
partial_variables={"format_instructions": format_instructions})
## 数据准备
flowers = ["玫瑰", "百合", "康乃馨"]
prices = ["50", "30", "20"]
## 创建一个空的DataFrame用于存储结果
import pandas as pd
df = pd.DataFrame(columns=["flower", "price", "description", "reason"]) ## 先声明列名
for flower, price in zip(flowers, prices):
## 根据提示准备模型的输入
input = prompt.format(flower_name=flower, price=price)
## 获取模型的输出
output = model(input)
## 解析模型的输出(这是一个字典结构)
parsed_output = output_parser.parse(output)
## 在解析后的输出中添加“flower”和“price”
parsed_output['flower'] = flower
parsed_output['price'] = price
## 将解析后的输出添加到DataFrame中
df.loc[len(df)] = parsed_output
## 打印字典
print(df.to_dict(orient='records'))
## 保存DataFrame到CSV文件
df.to_csv("flowers_with_descriptions.csv", index=False)
输出:
[{'flower': '玫瑰', 'price': '50', 'description': 'Luxuriate in the beauty of this 50 yuan rose, with its deep red petals and delicate aroma.', 'reason': 'This description emphasizes the elegance and beauty of the rose, which will be sure to draw attention.'},
{'flower': '百合', 'price': '30', 'description': '30元的百合,象征着坚定的爱情,带给你的是温暖而持久的情感!', 'reason': '百合是象征爱情的花,写出这样的描述能让顾客更容易感受到百合所带来的爱意。'},
{'flower': '康乃馨', 'price': '20', 'description': 'This beautiful carnation is the perfect way to show your love and appreciation. Its vibrant pink color is sure to brighten up any room!', 'reason': 'The description is short, clear and appealing, emphasizing the beauty and color of the carnation while also invoking a sense of love and appreciation.'}]
这段代码中,首先定义输出结构,我们希望模型生成的答案包含两部分:鲜花的描述文案(description)和撰写这个文案的原因(reason)。所以我们定义了一个名为response_schemas的列表,其中包含两个ResponseSchema对象,分别对应这两部分的输出。
根据这个列表,我通过StructuredOutputParser.from_response_schemas方法创建了一个输出解析器。
然后,我们通过输出解析器对象的get_format_instructions()方法获取输出的格式说明(format_instructions),再根据原始的字符串模板和输出解析器格式说明创建新的提示模板(这个模板就整合了输出解析结构信息)。再通过新的模板生成模型的输入,得到模型的输出。此时模型的输出结构将尽最大可能遵循我们的指示,以便于输出解析器进行解析。
对于每一个鲜花和价格组合,我们都用 output_parser.parse(output) 把模型输出的文案解析成之前定义好的数据格式,也就是一个Python字典,这个字典中包含了description 和 reason 这两个字段的值。
parsed_output
{'description': 'This 50-yuan rose is... feelings.', 'reason': 'The description is s...y emotion.'}
len(): 2
最后,把所有信息整合到一个pandas DataFrame对象中(需要安装Pandas库)。这个DataFrame对象中包含了flower、price、description 和 reason 这四个字段的值。其中,description 和 reason 是由 output_parser 从模型的输出中解析出来的,flower 和 price 是我们自己添加的。
我们可以打印出DataFrame的内容,也方便地在程序中处理它,比如保存为下面的CSV文件。因为此时数据不再是模糊的、无结构的文本,而是结构清晰的有格式的数据。 输出解析器在这个过程中的功劳很大。

到这里,我们今天的任务也就顺利完成了。
总结时刻
这样,你就从头到尾利用大模型开发出来了一个能够自动生成鲜花文案的应用程序!怎么样,是不是感觉和我们平时所做的基于SQL和数据库表以及固定业务逻辑的应用开发很不一样?
你看,每一次运行都有不同的结果,而我们完全不知道大模型下一次会给我们带来怎样的新东西。因此,基于大模型构建的应用可以说充满了创造力。
总结一下使用LangChain框架的好处,你会发现它有这样几个优势。
- 模板管理:在大型项目中,可能会有许多不同的提示模板,使用 LangChain 可以帮助你更好地管理这些模板,保持代码的清晰和整洁。
- 变量提取和检查:LangChain 可以自动提取模板中的变量并进行检查,确保你没有忘记填充任何变量。
- 模型切换:如果你想尝试使用不同的模型,只需要更改模型的名称就可以了,无需修改代码。
- 输出解析:LangChain的提示模板可以嵌入对输出格式的定义,以便在后续处理过程中比较方便地处理已经被格式化了的输出。
在下节课中,我们将继续深入探索LangChain中的提示模板,看一看如何通过高质量的提示工程让模型创造出更为精准、更高质量的输出。
思考题
- 请你用自己的理解,简述LangChain调用大语言模型来做应用开发的优势。
- 在上面的示例中,format_instructions,也就是输出格式是怎样用output_parser构建出来的,又是怎样传递到提示模板中的?
- 加入了partial_variables,也就是输出解析器指定的format_instructions之后的提示,为什么能够让模型生成结构化的输出?你可以打印出这个提示,一探究竟。
- 使用输出解析器后,调用模型时有没有可能仍然得不到所希望的输出?也就是说,模型有没有可能仍然返回格式不够完美的输出?
题目较多,可以选择性思考,期待在留言区看到你的分享。如果你觉得内容对你有帮助,也欢迎分享给有需要的朋友!最后如果你学有余力,可以进一步学习下面的延伸阅读。
延伸阅读
04|提示工程(上):用少样本FewShotTemplate和ExampleSelector创建应景文案
上节课我给你留了一个思考题: 在提示模板的构建过程中加入了partial_variables,也就是输出解析器指定的format_instructions之后,为什么能够让模型生成结构化的输出?
当你用print语句打印出最终传递给大模型的提示时,一切就变得非常明了。
您是一位专业的鲜花店文案撰写员。
对于售价为 50 元的 玫瑰 ,您能提供一个吸引人的简短描述吗?
The output should be a markdown code snippet formatted in the following schema, including the leading and trailing "```json" and "```":
```json
{
"description": string // 鲜花的描述文案
"reason": string // 问什么要这样写这个文案
}
秘密在于,LangChain的输出解析器偷偷的在提示中加了一段话,也就是 {format_instructions} 中的内容。这段由LangChain自动添加的文字,就清楚地指示着我们希望得到什么样的回答以及回答的具体格式。提示指出,模型需要根据一个schema来格式化输出文本,这个schema从 ```json 开始,到 ``` 结束。
这就是在告诉模型,你就follow这个schema(schema,可以理解为对数据结构的描述)的格式,就行啦!
这就是一个很棒、很典型的 提示工程。有了这样清晰的提示,智能程度比较高的模型(比如GPT3.5及以上版本),肯定能够输出可以被解析的数据结构,如JSON格式的数据。
那么这节课我就带着你进一步深究,如何利用LangChain中的提示模板,做好提示工程。

上节课我说过,针对大模型的提示工程该如何做,吴恩达老师在他的 ChatGPT Prompt Engineering for Developers 公开课中,给出了两个大的原则:第一条原则是写出清晰而具体的指示,第二条原则是给模型思考的时间。
无独有偶,在Open AI的官方文档 GPT 最佳实践 中,也给出了和上面这两大原则一脉相承的6大策略。分别是:
- 写清晰的指示
- 给模型提供参考(也就是示例)
- 将复杂任务拆分成子任务
- 给GPT时间思考
- 使用外部工具
- 反复迭代问题
怎么样,这些原则和策略是不是都是大白话?这些原则其实不仅能够指导大语言模型,也完全能够指导你的思维过程,让你处理问题时的思路更为清晰。所以说,大模型的思维过程和我们人类的思维过程,还是蛮相通的。
提示的结构
当然了,从大原则到实践,还是有一些具体工作需要说明,下面我们先看一个实用的提示框架。

在这个提示框架中:
- 指令(Instuction)告诉模型这个任务大概要做什么、怎么做,比如如何使用提供的外部信息、如何处理查询以及如何构造输出。这通常是一个提示模板中比较固定的部分。一个常见用例是告诉模型“你是一个有用的XX助手”,这会让他更认真地对待自己的角色。
- 上下文(Context)则充当模型的额外知识来源。这些信息可以手动插入到提示中,通过矢量数据库检索得来,或通过其他方式(如调用API、计算器等工具)拉入。一个常见的用例时是把从向量数据库查询到的知识作为上下文传递给模型。
- 提示输入(Prompt Input)通常就是具体的问题或者需要大模型做的具体事情,这个部分和“指令”部分其实也可以合二为一。但是拆分出来成为一个独立的组件,就更加结构化,便于复用模板。这通常是作为变量,在调用模型之前传递给提示模板,以形成具体的提示。
- 输出指示器(Output Indicator)标记要生成的文本的开始。这就像我们小时候的数学考卷,先写一个“解”,就代表你要开始答题了。如果生成 Python 代码,可以使用 “import” 向模型表明它必须开始编写 Python 代码(因为大多数 Python 脚本以import开头)。这部分在我们和ChatGPT对话时往往是可有可无的,当然LangChain中的代理在构建提示模板时,经常性的会用一个“Thought:”(思考)作为引导词,指示模型开始输出自己的推理(Reasoning)。
下面,就让我们看看如何使用 LangChain中的各种提示模板做提示工程,将更优质的提示输入大模型。
LangChain 提示模板的类型
LangChain中提供String(StringPromptTemplate)和Chat(BaseChatPromptTemplate)两种基本类型的模板,并基于它们构建了不同类型的提示模板:

这些模板的导入方式如下:
from langchain.prompts.prompt import PromptTemplate
from langchain.prompts import FewShotPromptTemplate
from langchain.prompts.pipeline import PipelinePromptTemplate
from langchain.prompts import ChatPromptTemplate
from langchain.prompts import (
ChatMessagePromptTemplate,
SystemMessagePromptTemplate,
AIMessagePromptTemplate,
HumanMessagePromptTemplate,
)
我发现有时候不指定 .prompts,直接从LangChain包也能导入模板。
from langchain import PromptTemplate
下面我们通过示例来介绍上面这些模版,前两个我们简单了解就好,其中最典型的FewShotPromptTemplate会重点讲。至于PipelinePrompt和自定义模板,使用起来比较简单,请你参考LangChain文档自己学习。
使用 PromptTemplate
下面通过示例简单说明一下PromptTemplate的使用。
from langchain import PromptTemplate
template = """\
你是业务咨询顾问。
你给一个销售{product}的电商公司,起一个好的名字?
"""
prompt = PromptTemplate.from_template(template)
print(prompt.format(product="鲜花"))
输出:
你是业务咨询顾问。
你给一个销售鲜花的电商公司,起一个好的名字?
这个程序的主要功能是生成适用于不同场景的提示,对用户定义的一种产品或服务提供公司命名建议。
在这里, "你是业务咨询顾问。你给一个销售{product}的电商公司,起一个好的名字?" 就是原始提示模板,其中 {product} 是占位符。
然后通过PromptTemplate的from_template方法,我们创建了一个提示模板对象,并通过prompt.format方法将模板中的 {product} 替换为 "鲜花"。
这样,就得到了一句具体的提示: 你是业务咨询顾问。你给一个销售鲜花的电商公司,起一个好的名字?——这就要求大语言模型,要有的放矢。
在上面这个过程中,LangChain中的模板的一个方便之处是from_template方法可以从传入的字符串中自动提取变量名称(如product),而无需刻意指定。 上面程序中的product自动成为了format方法中的一个参数。
当然,也可以通过提示模板类的构造函数,在创建模板时手工指定input_variables,示例如下:
prompt = PromptTemplate(
input_variables=["product", "market"],
template="你是业务咨询顾问。对于一个面向{market}市场的,专注于销售{product}的公司,你会推荐哪个名字?"
)
print(prompt.format(product="鲜花", market="高端"))
输出:
你是业务咨询顾问。对于一个面向高端市场的,专注于销售鲜花的公司,你会推荐哪个名字?
上面的方式直接生成了提示模板,并没有通过from_template方法从字符串模板中创建提示模板。二者效果是一样的。
使用 ChatPromptTemplate
对于OpenAI推出的ChatGPT这一类的聊天模型,LangChain也提供了一系列的模板,这些模板的不同之处是它们有对应的角色。
下面代码展示了OpenAI的Chat Model中的各种消息角色。
import openai
openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Who won the world series in 2020?"},
{"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
{"role": "user", "content": "Where was it played?"}
]
)
OpenAI对传输到gpt-3.5-turbo和GPT-4的messsage格式说明如下:
消息必须是消息对象的数组,其中每个对象都有一个角色(系统、用户或助理)和内容。对话可以短至一条消息,也可以来回多次。
通常,对话首先由系统消息格式化,然后是交替的用户消息和助理消息。
系统消息有助于设置助手的行为。例如,你可以修改助手的个性或提供有关其在整个对话过程中应如何表现的具体说明。但请注意,系统消息是可选的,并且没有系统消息的模型的行为可能类似于使用通用消息,例如“你是一个有用的助手”。
用户消息提供助理响应的请求或评论。
助理消息存储以前的助理响应,但也可以由你编写以给出所需行为的示例。
LangChain的ChatPromptTemplate这一系列的模板,就是 跟着这一系列角色而设计的。
下面,我给出一个示例。
## 导入聊天消息类模板
from langchain.prompts import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
## 模板的构建
template="你是一位专业顾问,负责为专注于{product}的公司起名。"
system_message_prompt = SystemMessagePromptTemplate.from_template(template)
human_template="公司主打产品是{product_detail}。"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)
prompt_template = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])
## 格式化提示消息生成提示
prompt = prompt_template.format_prompt(product="鲜花装饰", product_detail="创新的鲜花设计。").to_messages()
## 下面调用模型,把提示传入模型,生成结果
import os
os.environ["OPENAI_API_KEY"] = '你的OpenAI Key'
from langchain.chat_models import ChatOpenAI
chat = ChatOpenAI()
result = chat(prompt)
print(result)
输出:
content='1. 花语创意\n2. 花韵设计\n3. 花艺创新\n4. 花漾装饰\n5. 花语装点\n6. 花翩翩\n7. 花语之美\n8. 花馥馥\n9. 花语时尚\n10. 花之魅力'
additional_kwargs={}
example=False
好吧,尽管模型成功地完成了任务,但是感觉没有咱“易速鲜花”响亮!
讲完上面两种简单易用的提示模板,下面开始介绍今天的重点内容,FewShotPromptTemplate。FewShot,也就是少样本这一概念,是提示工程中非常重要的部分,对应着OpenAI提示工程指南中的第2条——给模型提供参考(也就是示例)。
FewShot的思想起源
讲解概念之前,我先分享个事儿哈,帮助你理解。
今天我下楼跑步时,一个老爷爷教孙子学骑车,小孩总掌握不了平衡,蹬一两下就下车。
- 爷爷说:“宝贝,你得有毅力!”
- 孙子说:“爷爷,什么是毅力?”
- 爷爷说:“你看这个叔叔,绕着楼跑了10多圈了,这就是毅力,你也得至少蹬个10几趟才能骑起来。”
这老爷爷就是给孙子做了一个One-Shot学习。如果他的孙子第一次听说却上来就明白什么是毅力,那就神了,这就叫Zero-Shot,表明这孩子的语言天赋不是一般的高,从知识积累和当前语境中就能够推知新词的涵义。有时候我们把Zero-Shot翻译为“顿悟”,聪明的大模型,某些情况下也是能够做到的。
Few-Shot(少样本)、One-Shot(单样本)和与之对应的 Zero-Shot(零样本)的概念都起源于机器学习。如何让机器学习模型在极少量甚至没有示例的情况下学习到新的概念或类别,对于许多现实世界的问题是非常有价值的,因为我们往往无法获取到大量的标签化数据。
这几个重要概念并非在某一篇特定的论文中首次提出,而是在机器学习和深度学习的研究中逐渐形成和发展的。
- 对于Few-Shot Learning,一个重要的参考文献是2016年Vinyals, O.的论文《小样本学习的匹配网络》。
- 这篇论文提出了一种新的学习模型——匹配网络(Matching Networks),专门针对单样本学习(One-Shot Learning)问题设计, 而 One-Shot Learning 可以看作是一种最常见的 Few-Shot 学习的情况。
- 对于Zero-Shot Learning,一个代表性的参考文献是Palatucci, M.在2009年提出的《基于语义输出编码的零样本学习(Zero-Shot Learning with semantic output codes)》,这篇论文提出了零次学习(Zero-Shot Learning)的概念,其中的学习系统可以根据类的语义描述来识别之前未见过的类。
在提示工程(Prompt Engineering)中,Few-Shot 和 Zero-Shot 学习的概念也被广泛应用。
- 在Few-Shot学习设置中,模型会被给予几个示例,以帮助模型理解任务,并生成正确的响应。
- 在Zero-Shot学习设置中,模型只根据任务的描述生成响应,不需要任何示例。
而OpenAI在介绍GPT-3模型的重要论文《Language models are Few-Shot learners(语言模型是少样本学习者)》中,更是直接指出:GPT-3模型,作为一个大型的自我监督学习模型,通过提升模型规模,实现了出色的Few-Shot学习性能。
这篇论文为大语言模型可以进行Few-Shot学习提供了扎实的理论基础。

下图就是OpenAI的GPT-3论文给出的GPT-3在翻译任务中,通过FewShot提示完成翻译的例子。

以上,就是ZeroShot、OneShot、FewShot这些重要概念的起源。
使用 FewShotPromptTemplate
下面,就让我们来通过LangChain中的FewShotPromptTemplate构建出最合适的鲜花文案。
1. 创建示例样本
首先,创建一些示例,作为提示的样本。其中每个示例都是一个字典,其中键是输入变量,值是这些输入变量的值。
## 1. 创建一些示例
samples = [
{
"flower_type": "玫瑰",
"occasion": "爱情",
"ad_copy": "玫瑰,浪漫的象征,是你向心爱的人表达爱意的最佳选择。"
},
{
"flower_type": "康乃馨",
"occasion": "母亲节",
"ad_copy": "康乃馨代表着母爱的纯洁与伟大,是母亲节赠送给母亲的完美礼物。"
},
{
"flower_type": "百合",
"occasion": "庆祝",
"ad_copy": "百合象征着纯洁与高雅,是你庆祝特殊时刻的理想选择。"
},
{
"flower_type": "向日葵",
"occasion": "鼓励",
"ad_copy": "向日葵象征着坚韧和乐观,是你鼓励亲朋好友的最好方式。"
}
]
samples这个列表,它包含了四个字典,每个字典代表了一种花的类型、适合的场合,以及对应的广告文案。 这些示例样本,就是构建FewShotPrompt时,作为例子传递给模型的参考信息。
2. 创建提示模板
配置一个提示模板,将一个示例格式化为字符串。这个格式化程序应该是一个PromptTemplate对象。
## 2. 创建一个提示模板
from langchain.prompts.prompt import PromptTemplate
template="鲜花类型: {flower_type}\n场合: {occasion}\n文案: {ad_copy}"
prompt_sample = PromptTemplate(input_variables=["flower_type", "occasion", "ad_copy"],
template=template)
print(prompt_sample.format(**samples[0]))
提示模板的输出如下:
鲜花类型: 玫瑰
场合: 爱情
文案: 玫瑰,浪漫的象征,是你向心爱的人表达爱意的最佳选择。
在这个步骤中,我们创建了一个PromptTemplate对象。这个对象是根据指定的输入变量和模板字符串来生成提示的。在这里,输入变量包括 "flower_type"、 "occasion"、 "ad_copy",模板是一个字符串,其中包含了用大括号包围的变量名,它们会被对应的变量值替换。
到这里,我们就把字典中的示例格式转换成了提示模板,可以形成一个个具体可用的LangChain提示。比如我用samples[0]中的数据替换了模板中的变量,生成了一个完整的提示。
3. 创建 FewShotPromptTemplate 对象
然后,通过使用上一步骤中创建的prompt_sample,以及samples列表中的所有示例, 创建一个FewShotPromptTemplate对象,生成更复杂的提示。
## 3. 创建一个FewShotPromptTemplate对象
from langchain.prompts.few_shot import FewShotPromptTemplate
prompt = FewShotPromptTemplate(
examples=samples,
example_prompt=prompt_sample,
suffix="鲜花类型: {flower_type}\n场合: {occasion}",
input_variables=["flower_type", "occasion"]
)
print(prompt.format(flower_type="野玫瑰", occasion="爱情"))
输出:
鲜花类型: 玫瑰
场合: 爱情
文案: 玫瑰,浪漫的象征,是你向心爱的人表达爱意的最佳选择。
鲜花类型: 康乃馨
场合: 母亲节
文案: 康乃馨代表着母爱的纯洁与伟大,是母亲节赠送给母亲的完美礼物。
鲜花类型: 百合
场合: 庆祝
文案: 百合象征着纯洁与高雅,是你庆祝特殊时刻的理想选择。
鲜花类型: 向日葵
场合: 鼓励
文案: 向日葵象征着坚韧和乐观,是你鼓励亲朋好友的最好方式。
鲜花类型: 野玫瑰
场合: 爱情
可以看到,FewShotPromptTemplate是一个更复杂的提示模板,它包含了多个示例和一个提示。这种模板可以使用多个示例来指导模型生成对应的输出。目前我们创建一个新提示,其中包含了根据指定的花的类型“野玫瑰”和场合“爱情”。
4. 调用大模型创建新文案
最后,把这个对象输出给大模型,就可以根据提示,得到我们所需要的文案了!
## 4. 把提示传递给大模型
import os
os.environ["OPENAI_API_KEY"] = '你的Open AI Key'
from langchain.llms import OpenAI
model = OpenAI(model_name='text-davinci-003')
result = model(prompt.format(flower_type="野玫瑰", occasion="爱情"))
print(result)
输出:
文案: 野玫瑰代表着爱情的坚贞,是你向心爱的人表达爱意的最佳礼物。
好!模型成功地模仿了我们的示例,写出了新文案,从结构到语气都蛮相似的。
使用示例选择器
如果我们的示例很多,那么一次性把所有示例发送给模型是不现实而且低效的。另外,每次都包含太多的Token也会浪费流量(OpenAI是按照Token数来收取费用)。
LangChain给我们提供了示例选择器,来选择最合适的样本。(注意,因为示例选择器使用向量相似度比较的功能,此处需要安装向量数据库,这里我使用的是开源的Chroma,你也可以选择之前用过的Qdrant。)
下面,就是使用示例选择器的示例代码。
## 5. 使用示例选择器
from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
## 初始化示例选择器
example_selector = SemanticSimilarityExampleSelector.from_examples(
samples,
OpenAIEmbeddings(),
Chroma,
k=1
)
## 创建一个使用示例选择器的FewShotPromptTemplate对象
prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=prompt_sample,
suffix="鲜花类型: {flower_type}\n场合: {occasion}",
input_variables=["flower_type", "occasion"]
)
print(prompt.format(flower_type="红玫瑰", occasion="爱情"))
输出:
鲜花类型: 玫瑰
场合: 爱情
文案: 玫瑰,浪漫的象征,是你向心爱的人表达爱意的最佳选择。
鲜花类型: 红玫瑰
场合: 爱情
在这个步骤中,它首先创建了一个SemanticSimilarityExampleSelector对象,这个对象可以根据语义相似性选择最相关的示例。然后,它创建了一个新的FewShotPromptTemplate对象,这个对象使用了上一步创建的选择器来选择最相关的示例生成提示。
然后,我们又用这个模板生成了一个新的提示,因为我们的提示中需要创建的是红玫瑰的文案,所以,示例选择器example_selector会根据语义的相似度(余弦相似度)找到最相似的示例,也就是“玫瑰”,并用这个示例构建了FewShot模板。
这样,我们就避免了把过多的无关模板传递给大模型,以节省Token的用量。
总结时刻
好的,到这里,今天这一讲就结束了。我们介绍了提示工程的原理,几种提示模板的用法,以及最重要的FewShot的思路。其实说白了,就是给模型一些示例做参考,模型才能明白你要什么。

总的来说,提供示例对于解决某些任务至关重要,通常情况下,FewShot的方式能够显著提高模型回答的质量。不过,当少样本提示的效果不佳时,这可能表示模型在任务上的学习不足。在这种情况下,我们建议对模型进行微调或尝试更高级的提示技术。
下一节课,我们将在探讨输出解析的同时,讲解另一种备受关注的提示技术,被称为“思维链提示”(Chain of Thought,简称CoT)。这种技术因其独特的应用方式和潜在的实用价值而引人注目。
思考题
- 如果你观察LangChain中的prompt.py中的PromptTemplate的实现代码,你会发现除了我们使用过的input_variables、template等初始化参数之外,还有template_format、validate_template等参数。举例来说,template_format可以指定除了f-string之外,其它格式的模板,比如jinja2。请你查看LangChain文档,并尝试使用这些参数。
template_format: str = "f-string"
"""The format of the prompt template. Options are: 'f-string', 'jinja2'."""
validate_template: bool = True
"""Whether or not to try validating the template."""
-
请你尝试使用PipelinePromptTemplate和自定义Template。
-
请你构想一个关于鲜花店运营场景中客户服务对话的少样本学习任务。在这个任务中,模型需要根据提供的示例,学习如何解答客户的各种问题,包括询问花的价格、推荐鲜花、了解鲜花的保养方法等。最好是用ChatModel完成这个任务。
from langchain.chat_models import ChatOpenAI
from langchain import PromptTemplate
from langchain.prompts.chat import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
AIMessagePromptTemplate,
HumanMessagePromptTemplate)
题目较多,可以选择性思考,期待在留言区看到你的分享。如果你觉得内容对你有帮助,也欢迎分享给有需要的朋友!最后如果你学有余力,可以进一步学习下面的延伸阅读。
延伸阅读
- 论文: Open AI的GPT-3模型: 大模型是少样本学习者, Brown, T. B., Mann, B., Ryder, N., Subbiah, M., Kaplan, J., Dhariwal, P., … & Agarwal, S. (2020). Language models are few-shot learners. arXiv preprint arXiv:2005.14165.
- 论文: 单样本学习的匹配网络,Vinyals, O., Blundell, C., Lillicrap, T., & Wierstra, D. (2016). Matching networks for one shot learning. In Advances in neural information processing systems (pp. 3630-3638).
- 论文: 用语义输出编码做零样本学习,Palatucci, M., Pomerleau, D., Hinton, G. E., & Mitchell, T. M. (2009). Zero-shot learning with semantic output codes. In Advances in neural information processing systems (pp. 1410-1418).
- 论文: 对示例角色的重新思考:是什么使得上下文学习有效? Min, S., Lyu, X., Holtzman, A., Artetxe, M., Lewis, M., Hajishirzi, H., & Zettlemoyer, L. (2022). Rethinking the Role of Demonstrations: What Makes In-Context Learning Work? Proceedings of the 2022 Conference on Empirical Methods in Natural Language Processing (EMNLP 2022).
- 论文: 微调后的语言模型是零样本学习者,Wei, J., Bosma, M., Zhao, V. Y., Guu, K., Yu, A. W., Lester, B., Du, N., Dai, A. M., & Le, Q. V. (2022). Finetuned Language Models Are Zero-Shot Learners. Proceedings of the International Conference on Learning Representations (ICLR 2022).
05|提示工程(下):用思维链和思维树提升模型思考质量
我在 第4课 的结尾时说了,你可以尝试用思维链也就是CoT(Chain of Thought)的概念来引导模型的推理,让模型生成更详实、更完备的文案,今天我们就一起看一看CoT的使用。
什么是 Chain of Thought
CoT这个概念来源于学术界,是谷歌大脑的Jason Wei等人于2022年在论文《 Chain-of-Thought Prompting Elicits Reasoning in Large Language Models(自我一致性提升了语言模型中的思维链推理能力)》中提出来的概念。它提出,如果生成一系列的中间推理步骤,就能够显著提高大型语言模型进行复杂推理的能力。
Few-Shot CoT
Few-Shot CoT 简单的在提示中提供了一些链式思考示例(Chain-of-Thought Prompting),足够大的语言模型的推理能力就能够被增强。简单说,就是给出一两个示例,然后在示例中写清楚推导的过程。

论文中给出了一个大模型通过思维链做数学题的示例。图左和图右,大模型都读入了OneShot示例,但是图左只给出了答案,而图右则在OneShot示例中给出了解题的具体思路。结果,只给出了答案的模型推理错误,而给出解题思路后,同一个模型生成了正确的答案。
在三种大型语言模型的实验中,CoT在一系列的算术、常识和符号推理任务中都提高了性能。在GSM8K数学问题基准测试中,通过CoT指导后,大模型的表现可以达到当时最先进的准确性。
CoT从概念上非常容易理解,从应用上非常容易操作。虽然简单,但这种思想可以给我们的开发过程带来很多启发。
比如,假设我们正在开发一个AI花店助手,它的任务是帮助用户选择他们想要的花,并生成一个销售列表。在这个过程中,我们可以使用CoT来引导AI的推理过程。
👉 整体指导:你需要跟着下面的步骤一步步的推理。
-
问题理解:首先,AI需要理解用户的需求。例如,用户可能会说:“今天要参加朋友的生日Party,想送束花祝福她。”我们可以给AI一个提示模板,里面包含示例:“ 遇到XX问题,我先看自己有 没有 相关知识,有的话,就提供答案;没有,就调用工具搜索,有了知识后再试图解决。”—— 这就是给了AI一个思维链的示例。
-
信息搜索:接下来,AI需要搜索相关信息。例如,它可能需要查找哪些花最适合生日派对。
-
决策制定:基于收集到的信息,AI需要制定一个决策。我们可以通过思维链让他详细思考决策的流程,先做什么后做什么。例如,我们可以给它一个示例:“ 遇到生日派对送花的情况,我先考虑用户的需求,然后查看鲜花的库存,最后决定推荐一些玫瑰和百合,因为这些花通常适合生日派对。”—— 那么有了生日派对这个场景做示例,大模型就能把类似的思维流程运用到其它场景。
-
生成销售列表:最后,AI使用OutputParser生成一个销售列表,包括推荐的花和价格。
在这个过程中,整体上,思维链引导AI从理解问题,到搜索信息,再到制定决策,最后生成销售列表。这种方法不仅使AI的推理过程更加清晰,也使得生成的销售列表更加符合用户的需求。具体到每一个步骤,也可以通过思维链来设计更为详细的提示模板,来引导模型每一步的思考都遵循清晰准确的逻辑。
其实LangChain的核心组件Agent的本质就是进行好的提示工程,并大量地使用预置的FewShot和CoT模板。这个在之后的课程学习中我们会理解得越来越透彻。
Zero-Shot CoT
下面的这两个CoT提示模板的例子,来自于Google Research和东京大学的论文《 大语言模型是零样本推理者》。
图中的(d)示例非常非常有意思,在Zero-Shot CoT中,你只要简单地告诉模型“ 让我们一步步的思考(Let’s think step by step)”,模型就能够给出更好的答案!

哈哈哈,这样太神奇了吧,这不由得让我联想起最简单的提示工程,角色设定——模型回答之前,先告诉它“你是一个很有经验的XX专家”,模型应该就会在开始胡说八道之前三思。
简单总结一下:Few-Shot CoT,指的就是在带有示例的提示过程中,加入思考的步骤,从而引导模型给出更好的结果。而Zero-Shot CoT,就是直接告诉模型要一步一步地思考,慢慢地推理。
Chain of Thought 实战
现在,就让我带着你完成一次Chain of Thought的LangChain应用开发实战。
项目需求:在这个示例中,你正在开发一个AI运营助手,我们要展示AI如何根据用户的需求推理和生成答案。然后,AI根据当前的用户请求进行推理,提供了具体的花卉建议并解释了为什么选择这些建议。
在这个过程中,AI需要理解客户的需求之后,按部就班的思考,然后给出最符合逻辑的回答。
CoT的模板设计
针对这个聊天机器人的需求,我设计了下面这样的思维链模板。
作为一个为花店电商公司工作的AI助手,我的目标是帮助客户根据他们的喜好做出明智的决定。
我会按部就班的思考,先理解客户的需求,然后考虑各种鲜花的涵义,最后根据这个需求,给出我的推荐。
同时,我也会向客户解释我这样推荐的原因。
示例 1:
人类:我想找一种象征爱情的花。
AI:首先,我理解你正在寻找一种可以象征爱情的花。在许多文化中,红玫瑰被视为爱情的象征,这是因为它们的红色通常与热情和浓烈的感情联系在一起。因此,考虑到这一点,我会推荐红玫瑰。红玫瑰不仅能够象征爱情,同时也可以传达出强烈的感情,这是你在寻找的。
示例 2:
人类:我想要一些独特和奇特的花。
AI:从你的需求中,我理解你想要的是独一无二和引人注目的花朵。兰花是一种非常独特并且颜色鲜艳的花,它们在世界上的许多地方都被视为奢侈品和美的象征。因此,我建议你考虑兰花。选择兰花可以满足你对独特和奇特的要求,而且,兰花的美丽和它们所代表的力量和奢侈也可能会吸引你。
AI的模板开始于对其角色的阐述,并给出了一些先前的对话示例(Few-Shot Learning)来帮助AI理解如何处理这种类型的请求。这些示例展示了AI如何根据思维链进行思考,给出深思熟虑之后的答案。
程序的完整框架
程序的完整代码如下:
## 设置环境变量和API密钥
import os
os.environ["OPENAI_API_KEY"] = '你的OpenAI API Key'
## 创建聊天模型
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(temperature=0)
## 设定 AI 的角色和目标
role_template = "你是一个为花店电商公司工作的AI助手, 你的目标是帮助客户根据他们的喜好做出明智的决定"
## CoT 的关键部分,AI 解释推理过程,并加入一些先前的对话示例(Few-Shot Learning)
cot_template = """
作为一个为花店电商公司工作的AI助手,我的目标是帮助客户根据他们的喜好做出明智的决定。
我会按部就班的思考,先理解客户的需求,然后考虑各种鲜花的涵义,最后根据这个需求,给出我的推荐。
同时,我也会向客户解释我这样推荐的原因。
示例 1:
人类:我想找一种象征爱情的花。
AI:首先,我理解你正在寻找一种可以象征爱情的花。在许多文化中,红玫瑰被视为爱情的象征,这是因为它们的红色通常与热情和浓烈的感情联系在一起。因此,考虑到这一点,我会推荐红玫瑰。红玫瑰不仅能够象征爱情,同时也可以传达出强烈的感情,这是你在寻找的。
示例 2:
人类:我想要一些独特和奇特的花。
AI:从你的需求中,我理解你想要的是独一无二和引人注目的花朵。兰花是一种非常独特并且颜色鲜艳的花,它们在世界上的许多地方都被视为奢侈品和美的象征。因此,我建议你考虑兰花。选择兰花可以满足你对独特和奇特的要求,而且,兰花的美丽和它们所代表的力量和奢侈也可能会吸引你。
"""
from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate
system_prompt_role = SystemMessagePromptTemplate.from_template(role_template)
system_prompt_cot = SystemMessagePromptTemplate.from_template(cot_template)
## 用户的询问
human_template = "{human_input}"
human_prompt = HumanMessagePromptTemplate.from_template(human_template)
## 将以上所有信息结合为一个聊天提示
chat_prompt = ChatPromptTemplate.from_messages([system_prompt_role, system_prompt_cot, human_prompt])
prompt = chat_prompt.format_prompt(human_input="我想为我的女朋友购买一些花。她喜欢粉色和紫色。你有什么建议吗?").to_messages()
## 接收用户的询问,返回回答结果
response = llm(prompt)
print(response)
程序中,首先设置环境变量OpenAI的API密钥,以便能够使用OpenAI的GPT-4模型。然后创建聊天模型:通过调用 ChatOpenAI 类,创建了一个聊天模型。设置 temperature=0 可以让模型生成更确定性的回答,即输出更倾向于最可能的结果。
接着定义了AI的角色和目标,该AI为花店电商公司的助手,其目标是根据客户的喜好来提供购买建议。紧接着,定义 CoT 模板,其中包括了AI的角色和目标描述、思考链条以及遵循思考链条的一些示例,显示了AI如何理解问题,并给出建议。
之后,我使用了PromptTemplate的from_template方法,来生成相应的询问模板。其中包括用于指导模型的SystemMessagePromptTemplate和用于传递人类问题的HumanMessagePromptTemplate。
然后,我使用了ChatPromptTemplate.from_messages方法,整合上述定义的角色,CoT模板和用户询问,生成聊天提示。
最后,将生成的聊天提示输入模型中,获得模型的回答,并打印出来。
在Few-Shot CoT提示的指引之下,模型针对我们的问题,从问题中的具体需求出发,返回了不错的建议。
现在,根据你的需求:你正在寻找你的女朋友喜欢的粉色和紫色的花。
首先,我从理解你的需求出发,只会推荐粉色或紫色,或者两者的组合的花。这些可能包括粉色的玫瑰,紫色的兰花,或者是粉色和紫色的花的混合花束。玫瑰是象征爱情和亲情的经典符号,而兰花象征着美丽和力量。这两种花都蕴含很棒的内涵。当然了,无论你选择哪种花卉,重要的是表达出你对她的爱和关心。记得附上一张温馨的贺卡,写下你的真挚祝福。
Tree of Thought
CoT这种思想,为大模型带来了更好的答案,然而,对于需要探索或预判战略的复杂任务来说,传统或简单的提示技巧是不够的。基于CoT的思想,Yao和Long等人几乎在同一时间在论文《 思维之树:使用大型语言模型进行深思熟虑的问题解决》和《 大型语言模型指导的思维之树》中,进一步提出了思维树(Tree of Thoughts,ToT)框架,该框架基于思维链提示进行了总结,引导语言模型探索把思维作为中间步骤来解决通用问题。
ToT是一种解决复杂问题的框架,它在需要多步骤推理的任务中,引导语言模型搜索一棵由连贯的语言序列(解决问题的中间步骤)组成的思维树,而不是简单地生成一个答案。ToT框架的核心思想是:让模型生成和评估其思维的能力,并将其与搜索算法(如广度优先搜索和深度优先搜索)结合起来,进行系统性地探索和验证。

ToT 框架为每个任务定义具体的思维步骤和每个步骤的候选项数量。例如,要解决一个数学推理任务,先把它分解为3个思维步骤,并为每个步骤提出多个方案,并保留最优的5个候选方案。然后在多条思维路径中搜寻最优的解决方案。
这种方法的优势在于,模型可以通过观察和评估其自身的思维过程,更好地解决问题,而不仅仅是基于输入生成输出。这对于需要深度推理的复杂任务非常有用。此外,通过引入强化学习、集束搜索等技术,可以进一步提高搜索策略的性能,并让模型在解决新问题或面临未知情况时有更好的表现。
下面我们应用ToT的思想,给出一个鲜花运营方面的示例。
假设一个顾客在鲜花网站上询问:“我想为我的妻子购买一束鲜花,但我不确定应该选择哪种鲜花。她喜欢淡雅的颜色和花香。”
AI(使用ToT框架):
思维步骤1:理解顾客的需求。
顾客想为妻子购买鲜花。
顾客的妻子喜欢淡雅的颜色和花香。
思维步骤2:考虑可能的鲜花选择。
候选1:百合,因为它有淡雅的颜色和花香。
候选2:玫瑰,选择淡粉色或白色,它们通常有花香。
候选3:紫罗兰,它有淡雅的颜色和花香。
候选4:桔梗,它的颜色淡雅但不一定有花香。
候选5:康乃馨,选择淡色系列,它们有淡雅的花香。
思维步骤3:根据顾客的需求筛选最佳选择。
百合和紫罗兰都符合顾客的需求,因为它们都有淡雅的颜色和花香。
淡粉色或白色的玫瑰也是一个不错的选择。
桔梗可能不是最佳选择,因为它可能没有花香。
康乃馨是一个可考虑的选择。
思维步骤4:给出建议。
“考虑到您妻子喜欢淡雅的颜色和花香,我建议您可以选择百合或紫罗兰。淡粉色或白色的玫瑰也是一个很好的选择。希望这些建议能帮助您做出决策!”
这个例子,可以作为FewShot示例之一,传递给模型,让他学着实现ToT。
通过在具体的步骤中产生多条思考路径,ToT 框架为解决复杂问题提供了一种新的方法,这种方法结合了语言模型的生成能力、搜索算法以及强化学习,以达到更好的效果。
总结时刻
这节课我们介绍了Chain of Thought(CoT,即“思维链”)和Tree of Thoughts(ToT,即“思维树”)这两个非常有趣的概念,并探讨了如何利用它们引导大型语言模型进行更深入的推理。
-
CoT的核心思想是通过生成一系列中间推理步骤来增强模型的推理能力。在Few-Shot CoT和Zero-Shot CoT两种应用方法中,前者通过提供链式思考示例传递给模型,后者则直接告诉模型进行要按部就班的推理。
-
ToT进一步扩展了CoT的思想,通过搜索由连贯的语言序列组成的思维树来解决复杂问题。我通过一个鲜花选择的实例,展示了如何在实际应用中使用ToT框架。
有朋友在GitHub上开了一个 Repo,专门给大家介绍ToT的应用方法和实例,他们还给出了几个非常简单的通用ToT提示语,就像下面这样。
请你模拟三位出色、逻辑性强的专家合作回答一个问题。每个人都详细地解释他们的思考过程,考虑到其他人之前的解释,并公开承认错误。在每一步,只要可能,每位专家都会在其他人的思考基础上进行完善和建设,并承认他们的贡献。他们继续,直到对问题有一个明确的答案。为了清晰起见,您的整个回应应该是一个Markdown表格。
问题是…

如果你有兴趣,可以去这个Repo里面看一看。
思考题
- 我们的CoT实战示例中使用的是Few-Shot CoT提示,请你把它换为Zero-Shot CoT,跑一下程序,看看结果。
- 请你设计一个你工作场景中的任务需求,然后用ToT让大语言模型帮你解决问题。
期待在留言区看到你的分享,我们一起交流探讨,共创一个好的学习氛围。如果你觉得内容对你有帮助,也欢迎分享给有需要的朋友!最后如果你学有余力,可以进一步学习下面的延伸阅读。
延伸阅读
- 论文,自我一致性提升了语言模型中的思维链推理能力, Chain-of-Thought Prompting Elicits Reasoning in Large Language Models,Wang, X., Wei, J., Schuurmans, D., Le, Q., Chi, E., Narang, S., Chowdhery, A., & Zhou, D. (2023). Self-Consistency Improves Chain of Thought Reasoning in Language Models. Proceedings of the International Conference on Learning Representations (ICLR). arXiv preprint arXiv:2203.11171.
- 论文,大语言模型是零样本推理者, Large Language Models are Zero-Shot Reasoners,Kojima, T., Gu, S. S., Reid, M., Matsuo, Y., & Iwasawa, Y. (2023). Large Language Models are Zero-Shot Reasoners. arXiv preprint arXiv:2205.11916v4.
- 论文,思维之树:使用大型语言模型进行深思熟虑的问题解决, Tree of Thoughts: Deliberate Problem Solving with Large Language Models,Yao, S., Yu, D., Zhao, J., Shafran, I., Griffiths, T. L., Cao, Y., & Narasimhan, K. (2023). Tree of Thoughts: Deliberate Problem Solving with Large Language Models. arXiv preprint arXiv:2305.10601.
- 论文,大型语言模型指导的思维之树, Large Language Model Guided Tree-of-Thought,Long, J. (2023). Large Language Model Guided Tree-of-Thought. arXiv preprint arXiv:2305.08291.
- GitHub链接, tree-of-thoughts,把ToT算法导入你的大模型应用,目前3.3K颗星
06|调用模型:使用OpenAI API还是微调开源Llama2/ChatGLM?
之前,我们花了两节课的内容讲透了提示工程的原理以及LangChain中的具体使用方式。今天,我们来着重讨论Model I/O中的第二个子模块,LLM。

让我们带着下面的问题来开始这一节课的学习。大语言模型,不止ChatGPT一种。调用OpenAI的API,当然方便且高效,不过,如果我就是想用其他的模型(比如说开源的Llama2或者ChatGLM),该怎么做?再进一步,如果我就是想在本机上从头训练出来一个新模型,然后在LangChain中使用自己的模型,又该怎么做?
关于大模型的微调(或称精调)、预训练、重新训练、乃至从头训练,这是一个相当大的话题,不仅仅需要足够的知识和经验,还需要大量的语料数据、GPU硬件和强大的工程能力。别说一节课了,我想两三个专栏也不一定能讲全讲透。不过,我可以提纲挈领地把大模型的训练流程和使用方法给你缕一缕。这样你就能体验到,在LangChain中使用自己微调的模型是完全没问题的。
大语言模型发展史
说到语言模型,我们不妨先从其发展史中去了解一些关键信息。
Google 2018 年的论文名篇Attention is all you need,提出了Transformer架构,也给这一次AI的腾飞点了火。Transformer是几乎所有预训练模型的核心底层架构。基于Transformer预训练所得的大规模语言模型也被叫做“基础模型”(Foundation Model 或Base Model)。
在这个过程中,模型学习了词汇、语法、句子结构以及上下文信息等丰富的语言知识。这种在大量数据上学到的知识,为后续的下游任务(如情感分析、文本分类、命名实体识别、问答系统等)提供了一个通用的、丰富的语言表示基础,为解决许多复杂的NLP问题提供了可能。
在预训练模型出现的早期,BERT毫无疑问是最具代表性的,也是影响力最大的模型。BERT通过同时学习文本的前向和后向上下文信息,实现对句子结构的深入理解。BERT之后,各种大型预训练模型如雨后春笋般地涌现,自然语言处理(NLP)领域进入了一个新时代。这些模型推动了NLP技术的快速发展,解决了许多以前难以应对的问题,比如翻译、文本总结、聊天对话等等,提供了强大的工具。

当然,现今的预训练模型的趋势是参数越来越多,模型也越来越大,训练一次的费用可达几百万美元。这样大的开销和资源的耗费,只有世界顶级大厂才能够负担得起,普通的学术组织和高等院校很难在这个领域继续引领科技突破,这种现象开始被普通研究人员所诟病。

预训练+微调的模式
不过,话虽如此,大型预训练模型的确是工程师的福音。因为,经过预训练的大模型中所习得的语义信息和所蕴含的语言知识,能够非常容易地向下游任务迁移。NLP应用人员可以对模型的头部或者部分参数根据自己的需要进行适应性的调整,这通常涉及在相对较小的有标注数据集上进行有监督学习,让模型适应特定任务的需求。
这就是对预训练模型的微调(Fine-tuning)。微调过程相比于从头训练一个模型要快得多,且需要的数据量也要少得多,这使得作为工程师的我们能够更高效地开发和部署各种NLP解决方案。

图中的“具体任务”,其实也可以更换为“具体领域”。那么总结来说:
- 预训练:在大规模无标注文本数据上进行模型的训练,目标是让模型学习自然语言的基础表达、上下文信息和语义知识,为后续任务提供一个通用的、丰富的语言表示基础。
- 微调:在预训练模型的基础上,可以根据特定的下游任务对模型进行微调。现在你经常会听到各行各业的人说: 我们的优势就是领域知识嘛!我们比不过国内外大模型,我们可以拿开源模型做垂直领域嘛!做垂类模型!—— 啥叫垂类?指的其实就是根据领域数据微调开源模型这件事儿。
这种预训练+微调的大模型应用模式优势明显。首先,预训练模型能够将大量的通用语言知识迁移到各种下游任务上,作为应用人员,我们不需要自己寻找语料库,从头开始训练大模型,这减少了训练时间和数据需求;其次,微调过程可以快速地根据特定任务进行优化,简化了模型部署的难度;最后,预训练+微调的架构具有很强的可扩展性,可以方便地应用于各种自然语言处理任务,大大提高了NLP技术在实际应用中的可用性和普及程度,给我们带来了巨大的便利。
好,下面咱们开始一步步地使用开源模型。今天我要带你玩的模型主要是Meta(Facebook)推出的Llama2。当然你可以去Llama的官网下载模型,然后通过Llama官方 GitHub 中提供的方法来调用它。但是,我还是会推荐你从HuggingFace下载并导入模型。因为啊,前天百川,昨天千问,今天流行Llama,明天不就流行别的了嘛。模型总在变,但是HuggingFace一直在那里,支持着各种开源模型。我们学东西,尽量选择学一次能够复用的知识。
用 HuggingFace 跑开源模型
注册并安装 HuggingFace
第一步,还是要登录 HuggingFace 网站,并拿到专属于你的Token。(如果你做了前面几节课的实战案例,那么你应该已经有这个API Token了)
第二步,用 pip install transformers 安装HuggingFace Library。详见 这里。
第三步,在命令行中运行 huggingface-cli login,设置你的API Token。

当然,也可以在程序中设置你的API Token,但是这不如在命令行中设置来得安全。
## 导入HuggingFace API Token
import os
os.environ['HUGGINGFACEHUB_API_TOKEN'] = '你的HuggingFace API Token'
申请使用 Meta 的 Llama2 模型
在HuggingFace的Model中,找到 meta-llama/Llama-2-7b。注意,各种各样版本的Llama2模型多如牛毛,我们这里用的是最小的7B版。此外,还有13b\70b\chat版以及各种各样的非Meta官方版。

选择meta-llama/Llama-2-7b这个模型后,你能够看到这个模型的基本信息。如果你是第一次用Llama,你需要申请Access,因为我已经申请过了,所以屏幕中间有句话:“You have been granted access to this model”。从申请到批准,大概是几分钟的事儿。

通过 HuggingFace 调用 Llama
好,万事俱备,现在我们可以使用HuggingFace的Transformers库来调用Llama啦!
## 导入必要的库
from transformers import AutoTokenizer, AutoModelForCausalLM
## 加载预训练模型的分词器
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
## 加载预训练的模型
## 使用 device_map 参数将模型自动加载到可用的硬件设备上,例如GPU
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-chat-hf",
device_map = 'auto')
## 定义一个提示,希望模型基于此提示生成故事
prompt = "请给我讲个玫瑰的爱情故事?"
## 使用分词器将提示转化为模型可以理解的格式,并将其移动到GPU上
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
## 使用模型生成文本,设置最大生成令牌数为2000
outputs = model.generate(inputs["input_ids"], max_new_tokens=2000)
## 将生成的令牌解码成文本,并跳过任何特殊的令牌,例如[CLS], [SEP]等
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
## 打印生成的响应
print(response)
这段程序是一个很典型的HuggingFace的Transformers库的用例,该库提供了大量预训练的模型和相关的工具。
- 导入AutoTokenizer:这是一个用于自动加载预训练模型的相关分词器的工具。分词器负责将文本转化为模型可以理解的数字格式。
- 导入AutoModelForCausalLM:这是用于加载因果语言模型(用于文本生成)的工具。
- 使用from_pretrained方法来加载预训练的分词器和模型。其中,
device_map = 'auto'是为了自动地将模型加载到可用的设备上,例如GPU。 - 然后,给定一个提示(prompt):
"请给我讲个玫瑰的爱情故事?",并使用分词器将该提示转换为模型可以接受的格式,return_tensors="pt"表示返回PyTorch张量。语句中的.to("cuda")是GPU设备格式转换,因为我在GPU上跑程序,不用这个的话会报错,如果你使用CPU,可以试一下删掉它。 - 最后使用模型的
.generate()方法生成响应。max_new_tokens=2000限制生成的文本的长度。使用分词器的.decode()方法将输出的数字转化回文本,并且跳过任何特殊的标记。
因为是在本地进行推理,耗时时间比较久。在我的机器上,大概需要30s~2min产生结果。

这样的回答肯定不能直接用做商业文案,而且,我的意思是玫瑰花相关的故事,它明显把玫瑰理解成一个女孩的名字了。所以,开源模型,尤其是7B的小模型和Open AI的ChatGPT还是有一定差距的。
LangChain 和 HuggingFace 的接口
讲了半天,LangChain未出场。下面让我们看一看,如何把HuggingFace里面的模型接入LangChain。
通过 HuggingFace Hub
第一种集成方式,是通过HuggingFace Hub。HuggingFace Hub 是一个开源模型中心化存储库,主要用于分享、协作和存储预训练模型、数据集以及相关组件。
我们给出一个HuggingFace Hub 和LangChain集成的代码示例。
## 导入HuggingFace API Token
import os
os.environ['HUGGINGFACEHUB_API_TOKEN'] = '你的HuggingFace API Token'
## 导入必要的库
from langchain import PromptTemplate, HuggingFaceHub, LLMChain
## 初始化HF LLM
llm = HuggingFaceHub(
repo_id="google/flan-t5-small",
#repo_id="meta-llama/Llama-2-7b-chat-hf",
)
## 创建简单的question-answering提示模板
template = """Question: {question}
Answer: """
## 创建Prompt
prompt = PromptTemplate(template=template, input_variables=["question"])
## 调用LLM Chain --- 我们以后会详细讲LLM Chain
llm_chain = LLMChain(
prompt=prompt,
llm=llm
)
## 准备问题
question = "Rose is which type of flower?"
## 调用模型并返回结果
print(llm_chain.run(question))
可以看出,这个集成过程非常简单,只需要在HuggingFaceHub类的repo_id中指定模型名称,就可以直接下载并使用模型,模型会自动下载到HuggingFace的Cache目录,并不需要手工下载。
初始化LLM,创建提示模板,生成提示的过程,你已经很熟悉了。这段代码中有一个新内容是我通过llm_chain来调用了LLM。这段代码也不难理解,有关Chain的概念我们以后还会详述。
不过,我尝试使用meta-llama/Llama-2-7b-chat-hf这个模型时,出现了错误,因此我只好用比较旧的模型做测试。我随便选择了google/flan-t5-small,问了它一个很简单的问题,想看看它是否知道玫瑰是哪一种花。

模型告诉我,玫瑰是花。对,答案只有一个字,flower。这…不得不说,2023年之前的模型,和2023年之后的模型,水平没得比。以前的模型能说话就不错了。

通过 HuggingFace Pipeline
既然HuggingFace Hub还不能完成Llama-2的测试,让我们来尝试另外一种方法,HuggingFace Pipeline。HuggingFace 的 Pipeline 是一种高级工具,它简化了多种常见自然语言处理(NLP)任务的使用流程,使得用户不需要深入了解模型细节,也能够很容易地利用预训练模型来做任务。
让我来看看下面的示例:
## 指定预训练模型的名称
model = "meta-llama/Llama-2-7b-chat-hf"
## 从预训练模型中加载词汇器
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(model)
## 创建一个文本生成的管道
import transformers
import torch
pipeline = transformers.pipeline(
"text-generation",
model=model,
torch_dtype=torch.float16,
device_map="auto",
max_length = 1000
)
## 创建HuggingFacePipeline实例
from langchain import HuggingFacePipeline
llm = HuggingFacePipeline(pipeline = pipeline,
model_kwargs = {'temperature':0})
## 定义输入模板,该模板用于生成花束的描述
template = """
为以下的花束生成一个详细且吸引人的描述:
花束的详细信息:
```{flower_details}```
"""
## 使用模板创建提示
from langchain import PromptTemplate, LLMChain
prompt = PromptTemplate(template=template,
input_variables=["flower_details"])
## 创建LLMChain实例
from langchain import PromptTemplate
llm_chain = LLMChain(prompt=prompt, llm=llm)
## 需要生成描述的花束的详细信息
flower_details = "12支红玫瑰,搭配白色满天星和绿叶,包装在浪漫的红色纸中。"
## 打印生成的花束描述
print(llm_chain.run(flower_details))
这里简单介绍一下代码中使用到的transformers pipeline的配置参数。

生成的结果之一如下:

此结果不敢恭维。但是,后续的测试告诉我,这很有可能是7B这个模型太小,尽管有形成中文的相应能力,但是能力不够强大,也就导致了这样的结果。
至此,通过HuggingFace接口调用各种开源模型的尝试成功结束。下面,我们进行最后一个测试,看看LangChain到底能否直接调用本地模型。
用 LangChain 调用自定义语言模型
最后,我们来尝试回答这节课开头提出的问题,假设你就是想训练属于自己的模型。而且出于商业秘密的原因,不想开源它,不想上传到HuggingFace,就是要在本机运行模型。此时应该如何利用LangChain的功能?
我们可以创建一个LLM的衍生类,自己定义模型。而LLM这个基类,则位于langchain.llms.base中,通过from langchain.llms.base import LLM语句导入。
这个自定义的LLM类只需要实现一个方法:
- _call方法:用于接收输入字符串并返回响应字符串。
以及一个可选方法:
- _identifying_params方法:用于帮助打印此类的属性。
下面,让我们先从HuggingFace的 这里,下载一个llama-2-7b-chat.ggmlv3.q4_K_S.bin模型,并保存在本地。

你可能会质疑我,不是说自己训练,自己微调,不再用HuggingFace了吗?
不好意思,容许我解释一下。自己训练一个能用的模型没那么容易。这个模型,它并不是原始的Llama模型,而是TheBloke这位老兄用他的手段为我们量化过的新模型,你也可以理解成,他已经为我们压缩或者说微调了Llama模型。
量化是AI模型大小和性能优化的常用技术,它将模型的权重简化到较少的位数,以减少模型的大小和计算需求,让大模型甚至能够在CPU上面运行。当你看到模型的后缀有GGML或者GPTQ,就说明模型已经被量化过,其中GPTQ 是一种仅适用于 GPU 的特定格式。GGML 专为 CPU 和 Apple M 系列设计,但也可以加速 GPU 上的某些层。llama-cpp-python这个包就是为了实现GGML而制作的。
所以,这里你就假设,咱们下载下来的llama-2-7b-chat.ggmlv3.q4_K_S.bin这个模型,就是你自己微调过的。将来你真的微调了Llama2、ChatGLM、百川或者千问的开源版,甚至是自己从头训练了一个mini-ChatGPT,你也可以保存为you_own_model.bin的格式,就按照下面的方式加载到LangChain之中。
然后,为了使用llama-2-7b-chat.ggmlv3.q4_K_S.bin这个模型,你需要安装 pip install llama-cpp-python 这个包。
## 导入需要的库
from llama_cpp import Llama
from typing import Optional, List, Mapping, Any
from langchain.llms.base import LLM
## 模型的名称和路径常量
MODEL_NAME = 'llama-2-7b-chat.ggmlv3.q4_K_S.bin'
MODEL_PATH = '/home/huangj/03_Llama/'
## 自定义的LLM类,继承自基础LLM类
class CustomLLM(LLM):
model_name = MODEL_NAME
## 该方法使用Llama库调用模型生成回复
def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str:
prompt_length = len(prompt) + 5
## 初始化Llama模型,指定模型路径和线程数
llm = Llama(model_path=MODEL_PATH+MODEL_NAME, n_threads=4)
## 使用Llama模型生成回复
response = llm(f"Q: {prompt} A: ", max_tokens=256)
## 从返回的回复中提取文本部分
output = response['choices'][0]['text'].replace('A: ', '').strip()
## 返回生成的回复,同时剔除了问题部分和额外字符
return output[prompt_length:]
## 返回模型的标识参数,这里只是返回模型的名称
@property
def _identifying_params(self) -> Mapping[str, Any]:
return {"name_of_model": self.model_name}
## 返回模型的类型,这里是"custom"
@property
def _llm_type(self) -> str:
return "custom"
## 初始化自定义LLM类
llm = CustomLLM()
## 使用自定义LLM生成一个回复
result = llm("昨天有一个客户抱怨他买了花给女朋友之后,两天花就枯了,你说作为客服我应该怎么解释?")
## 打印生成的回复
print(result)
代码中需要解释的内容不多,基本上就是CustomLLM类的构建和使用,类内部通过Llama类来实现大模型的推理功能,然后直接返回模型的回答。

似乎Llama经过量化之后,虽然仍读得懂中文,但是不会讲中文了。
翻译成中文,他的回答是这样的。
当客户抱怨他们为女朋友买的花在两天内就枯萎了,我会以礼貌和专业的方式这样解释:
“感谢您把这个问题告诉我们。对于给您带来的任何不便,我深感抱歉。有可能这些花没有被正确地存储或照料,这可能影响了它们的生命期。我们始终以提供高质量的产品为荣,但有时可能会出现意外的问题。请您知道,我们非常重视您的满意度并随时为您提供帮助。您希望我为您提供替换或退款吗?”
看上去,除了中文能力不大灵光之外,Llama2的英文表现真的非常完美,和GPT3.5差距不是很大,要知道:
- 这可是开源模型,而且是允许商业的免费模型。
- 这是在本机 CPU 的环境下运行的,模型的推理速度还是可以接受的。
- 这仅仅是Llama的最小版本,也就是7B的量化版,就达到了这么好的效果。
基于上述三点原因,我给Llama2打98.5分。
总结时刻
今天的课程到此就结束了,相信你学到了很多新东西吧。的确,进入大模型开发这个领域,就好像打开了通往新世界的一扇门,有太多的新知识,等待着你去探索。
现在,你已经知道大模型训练涉及在大量数据上使用深度学习算法,通常需要大量计算资源和时间。训练后,模型可能不完全适合特定任务,因此需要微调,即在特定数据集上继续训练,以使模型更适应该任务。为了减小部署模型的大小和加快推理速度,模型还会经过量化,即将模型参数从高精度格式减少到较低精度。
如果你想继续深入学习大模型,那么有几个工具你不得不接着研究。
- PyTorch是一个流行的深度学习框架,常用于模型的训练和微调。
- HuggingFace是一个开源社区,提供了大量预训练模型和微调工具,尤其是NLP任务。
- LangChain则擅长于利用大语言模型的推理功能,开发新的工具或应用,完成特定的任务。
这些工具和库在AI模型的全生命周期中起到关键作用,使研究者和开发者更容易开发和部署高效的AI系统。
思考题
-
现在请你再回答一下,什么时候应该使用OpenAI的API?什么时候应该使用开源模型?或者自己开发/微调的模型?
提示:的确,文中没有给出这个问题的答案。因为这个问题并没有标准答案。
-
请你使用HuggingFace的Transformers库,下载新的模型进行推理,比较它们的性能。
-
请你在LangChain中,使用HuggingFaceHub和HuggingFace Pipeline这两种接口,调用当前最流行的大语言模型。
提示:HuggingFace Model 页面,有模型下载量的当月排序,当月下载最多的模型就是最流行的模型。
期待在留言区看到你的分享,我们一起交流探讨,共创一个良好的学习氛围。如果你觉得内容对你有帮助,也欢迎分享给有需要的朋友!最后如果你学有余力,可以进一步学习下面的延伸阅读。
延伸阅读
- Llama2,开源的可商用类ChatGPT模型, Facebook链接、 GitHub链接
- HuggingFace Transformer 文档
- PyTorch 官方 教程、 文档
- AutoGPTQ 基于GPTQ算法的大模型量化工具包
- Llama CPP 支持 GGML,目标是在MacBook(或类似的非GPU的普通家用硬件环境)上使用4位整数量化运行Llama模型
07|输出解析:用OutputParser生成鲜花推荐列表
首先请你回忆一下 第4课 中我们学了什么: 为一些花和价格生成吸引人的描述,并将这些描述和原因存储到一个CSV文件中。为了实现这个目标,程序调用了OpenAI模型,并利用了结构化输出解析器,以及一些数据处理和存储的工具。
今天我要带着你深入研究一下LangChain中的输出解析器,并用一个新的解析器——Pydantic 解析器来重构第4课中的程序。这节课也是模型I/O框架的最后一讲。

下面先来看看LangChain中的输出解析器究竟是什么,有哪些种类。
LangChain 中的输出解析器
语言模型输出的是文本,这是给人类阅读的。但很多时候,你可能想要获得的是程序能够处理的结构化信息。这就是输出解析器发挥作用的地方。
输出解析器是 一种专用于处理和构建语言模型响应的类。一个基本的输出解析器类通常需要实现两个核心方法。
- get_format_instructions:这个方法需要返回一个字符串,用于指导如何格式化语言模型的输出,告诉它应该如何组织并构建它的回答。
- parse:这个方法接收一个字符串(也就是语言模型的输出)并将其解析为特定的数据结构或格式。这一步通常用于确保模型的输出符合我们的预期,并且能够以我们需要的形式进行后续处理。
还有一个可选的方法。
- parse_with_prompt:这个方法接收一个字符串(也就是语言模型的输出)和一个提示(用于生成这个输出的提示),并将其解析为特定的数据结构。这样,你可以根据原始提示来修正或重新解析模型的输出,确保输出的信息更加准确和贴合要求。
下面是一个基于上述描述的简单伪代码示例:
class OutputParser:
def __init__(self):
pass
def get_format_instructions(self):
## 返回一个字符串,指导如何格式化模型的输出
pass
def parse(self, model_output):
## 解析模型的输出,转换为某种数据结构或格式
pass
def parse_with_prompt(self, model_output, prompt):
## 基于原始提示解析模型的输出,转换为某种数据结构或格式
pass
在LangChain中,通过实现get_format_instructions、parse 和 parse_with_prompt 这些方法,针对不同的使用场景和目标,设计了各种输出解析器。让我们来逐一认识一下。
- 列表解析器(List Parser):这个解析器用于处理模型生成的输出,当需要模型的输出是一个列表的时候使用。例如,如果你询问模型“列出所有鲜花的库存”,模型的回答应该是一个列表。
- 日期时间解析器(Datetime Parser):这个解析器用于处理日期和时间相关的输出,确保模型的输出是正确的日期或时间格式。
- 枚举解析器(Enum Parser):这个解析器用于处理预定义的一组值,当模型的输出应该是这组预定义值之一时使用。例如,如果你定义了一个问题的答案只能是“是”或“否”,那么枚举解析器可以确保模型的回答是这两个选项之一。
- 结构化输出解析器(Structured Output Parser):这个解析器用于处理复杂的、结构化的输出。如果你的应用需要模型生成具有特定结构的复杂回答(例如一份报告、一篇文章等),那么可以使用结构化输出解析器来实现。
- Pydantic(JSON)解析器:这个解析器用于处理模型的输出,当模型的输出应该是一个符合特定格式的JSON对象时使用。它使用Pydantic库,这是一个数据验证库,可以用于构建复杂的数据模型,并确保模型的输出符合预期的数据模型。
- 自动修复解析器(Auto-Fixing Parser):这个解析器可以自动修复某些常见的模型输出错误。例如,如果模型的输出应该是一段文本,但是模型返回了一段包含语法或拼写错误的文本,自动修复解析器可以自动纠正这些错误。
- 重试解析器(RetryWithErrorOutputParser):这个解析器用于在模型的初次输出不符合预期时,尝试修复或重新生成新的输出。例如,如果模型的输出应该是一个日期,但是模型返回了一个字符串,那么重试解析器可以重新提示模型生成正确的日期格式。
上面的各种解析器中,前三种很容易理解,而结构化输出解析器你已经用过了。所以接下来我们重点讲一讲Pydantic(JSON)解析器、自动修复解析器和重试解析器。
Pydantic(JSON)解析器实战
Pydantic (JSON) 解析器应该是最常用也是最重要的解析器,我带着你用它来重构鲜花文案生成程序。
Pydantic 是一个 Python 数据验证和设置管理库,主要基于 Python 类型提示。尽管它不是专为 JSON 设计的,但由于 JSON 是现代 Web 应用和 API 交互中的常见数据格式,Pydantic 在处理和验证 JSON 数据时特别有用。
第一步:创建模型实例
先通过环境变量设置OpenAI API密钥,然后使用LangChain库创建了一个OpenAI的模型实例。这里我们仍然选择了text-davinci-003作为大语言模型。
## ------Part 1
## 设置OpenAI API密钥
import os
os.environ["OPENAI_API_KEY"] = '你的OpenAI API Key'
## 创建模型实例
from langchain import OpenAI
model = OpenAI(model_name='text-davinci-003')
第二步:定义输出数据的格式
先创建了一个空的DataFrame,用于存储从模型生成的描述。接下来,通过一个名为FlowerDescription的Pydantic BaseModel类,定义了期望的数据格式(也就是数据的结构)。
## ------Part 2
## 创建一个空的DataFrame用于存储结果
import pandas as pd
df = pd.DataFrame(columns=["flower_type", "price", "description", "reason"])
## 数据准备
flowers = ["玫瑰", "百合", "康乃馨"]
prices = ["50", "30", "20"]
## 定义我们想要接收的数据格式
from pydantic import BaseModel, Field
class FlowerDescription(BaseModel):
flower_type: str = Field(description="鲜花的种类")
price: int = Field(description="鲜花的价格")
description: str = Field(description="鲜花的描述文案")
reason: str = Field(description="为什么要这样写这个文案")
在这里我们用到了负责数据格式验证的Pydantic库来创建带有类型注解的类FlowerDescription,它可以自动验证输入数据,确保输入数据符合你指定的类型和其他验证条件。
Pydantic有这样几个特点。
- 数据验证:当你向Pydantic类赋值时,它会自动进行数据验证。例如,如果你创建了一个字段需要是整数,但试图向它赋予一个字符串,Pydantic会引发异常。
- 数据转换:Pydantic不仅进行数据验证,还可以进行数据转换。例如,如果你有一个需要整数的字段,但你提供了一个可以转换为整数的字符串,如
"42",Pydantic会自动将这个字符串转换为整数42。 - 易于使用:创建一个Pydantic类就像定义一个普通的Python类一样简单。只需要使用Python的类型注解功能,即可在类定义中指定每个字段的类型。
- JSON支持:Pydantic类可以很容易地从JSON数据创建,并可以将类的数据转换为JSON格式。
下面,我们基于这个Pydantic数据格式类来创建LangChain的输出解析器。
第三步:创建输出解析器
在这一步中,我们创建输出解析器并获取输出格式指示。先使用LangChain库中的PydanticOutputParser创建了输出解析器,该解析器将用于解析模型的输出,以确保其符合FlowerDescription的格式。然后,使用解析器的get_format_instructions方法获取了输出格式的指示。
## ------Part 3
## 创建输出解析器
from langchain.output_parsers import PydanticOutputParser
output_parser = PydanticOutputParser(pydantic_object=FlowerDescription)
## 获取输出格式指示
format_instructions = output_parser.get_format_instructions()
## 打印提示
print("输出格式:",format_instructions)
程序输出如下:
输出格式: The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.
Here is the output schema:
{"properties": {"flower_type": {"title": "Flower Type", "description": "\u9c9c\u82b1\u7684\u79cd\u7c7b", "type": "string"}, "price": {"title": "Price", "description": "\u9c9c\u82b1\u7684\u4ef7\u683c", "type": "integer"}, "description": {"title": "Description", "description": "\u9c9c\u82b1\u7684\u63cf\u8ff0\u6587\u6848", "type": "string"}, "reason": {"title": "Reason", "description": "\u4e3a\u4ec0\u4e48\u8981\u8fd9\u6837\u5199\u8fd9\u4e2a\u6587\u6848", "type": "string"}}, "required": ["flower_type", "price", "description", "reason"]}
上面这个输出,这部分是通过output_parser.get_format_instructions()方法生成的,这是Pydantic (JSON) 解析器的核心价值,值得你好好研究研究。同时它也算得上是一个很清晰的提示模板,能够为模型提供良好的指导,描述了模型输出应该符合的格式。(其中description中的中文被转成了UTF-8编码。)
它指示模型输出JSON Schema的形式,定义了一个有效的输出应该包含哪些字段,以及这些字段的数据类型。例如,它指定了 "flower_type" 字段应该是字符串类型, "price" 字段应该是整数类型。这个指示中还提供了一个例子,说明了什么是一个格式良好的输出。
下面,我们会把这个内容也传输到模型的提示中, 让输入模型的提示和输出解析器的要求相互吻合,前后就呼应得上。
第四步:创建提示模板
我们定义了一个提示模板,该模板将用于为模型生成输入提示。模板中包含了你需要模型填充的变量(如价格和花的种类),以及之前获取的输出格式指示。
## ------Part 4
## 创建提示模板
from langchain import PromptTemplate
prompt_template = """您是一位专业的鲜花店文案撰写员。
对于售价为 {price} 元的 {flower} ,您能提供一个吸引人的简短中文描述吗?
{format_instructions}"""
## 根据模板创建提示,同时在提示中加入输出解析器的说明
prompt = PromptTemplate.from_template(prompt_template,
partial_variables={"format_instructions": format_instructions})
## 打印提示
print("提示:", prompt)
输出:
提示:
input_variables=['flower', 'price']
output_parser=None
partial_variables={'format_instructions': 'The output should be formatted as a JSON instance that conforms to the JSON schema below.\n\n
As an example, for the schema {
"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}},
"required": ["foo"]}}\n
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema.
The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.\n\n
Here is the output schema:\n```\n
{"properties": {
"flower_type": {"title": "Flower Type", "description": "\\u9c9c\\u82b1\\u7684\\u79cd\\u7c7b", "type": "string"},
"price": {"title": "Price", "description": "\\u9c9c\\u82b1\\u7684\\u4ef7\\u683c", "type": "integer"},
"description": {"title": "Description", "description": "\\u9c9c\\u82b1\\u7684\\u63cf\\u8ff0\\u6587\\u6848", "type": "string"},
"reason": {"title": "Reason", "description": "\\u4e3a\\u4ec0\\u4e48\\u8981\\u8fd9\\u6837\\u5199\\u8fd9\\u4e2a\\u6587\\u6848", "type": "string"}},
"required": ["flower_type", "price", "description", "reason"]}\n```'}
template='您是一位专业的鲜花店文案撰写员。
\n对于售价为 {price} 元的 {flower} ,您能提供一个吸引人的简短中文描述吗?\n
{format_instructions}'
template_format='f-string'
validate_template=True
这就是包含了format_instructions信息的提示模板。
input_variables=['flower', 'price']:这是一个包含你想要在模板中使用的输入变量的列表。我们在模板中使用了'flower'和'price'两个变量,后面我们会用具体的值(如玫瑰、20元)来替换这两个变量。output_parser=None:这是你可以选择在模板中使用的一个输出解析器。在此例中,我们并没有选择在模板中使用输出解析器,而是在模型外部进行输出解析,所以这里是None。partial_variables:包含了你想要在模板中使用,但在生成模板时无法立即提供的变量。在这里,我们通过'format_instructions'传入输出格式的详细说明。template:这是模板字符串本身。它包含了你想要模型生成的文本的结构。在此例中,模板字符串是你询问鲜花描述的问题,以及关于输出格式的说明。template_format='f-string':这是一个表示模板字符串格式的选项。此处是f-string格式。validate_template=True:表示是否在创建模板时检查模板的有效性。这里选择了在创建模板时进行检查,以确保模板是有效的。
总的来说,这个提示模板是一个用于生成模型输入的工具。你可以在模板中定义需要的输入变量,以及模板字符串的格式和结构,然后使用这个模板来为每种鲜花生成一个描述。
后面,我们还要把实际的信息,循环传入提示模板,生成一个个的具体提示。下面让我们继续。
第五步:生成提示,传入模型并解析输出
这部分是程序的主体,我们循环来处理所有的花和它们的价格。对于每种花,都根据提示模板创建了输入,然后获取模型的输出。然后使用之前创建的解析器来解析这个输出,并将解析后的输出添加到DataFrame中。最后,你打印出了所有的结果,并且可以选择将其保存到CSV文件中。
## ------Part 5
for flower, price in zip(flowers, prices):
## 根据提示准备模型的输入
input = prompt.format(flower=flower, price=price)
## 打印提示
print("提示:", input)
## 获取模型的输出
output = model(input)
## 解析模型的输出
parsed_output = output_parser.parse(output)
parsed_output_dict = parsed_output.dict() ## 将Pydantic格式转换为字典
## 将解析后的输出添加到DataFrame中
df.loc[len(df)] = parsed_output.dict()
## 打印字典
print("输出的数据:", df.to_dict(orient='records'))
这一步中,你使用你的模型和输入提示(由鲜花种类和价格组成)生成了一个具体鲜花的文案需求(同时带有格式描述),然后传递给大模型,也就是说,提示模板中的 flower 和 price,此时都被具体的花取代了,而且模板中的 {format_instructions},也被替换成了 JSON Schema 中指明的格式信息。
具体来说,输出的一个提示是这样的:
提示: 您是一位专业的鲜花店文案撰写员。
对于售价为 20 元的 康乃馨 ,您能提供一个吸引人的简短中文描述吗?
The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {“properties”: {“foo”: {“title”: “Foo”, “description”: “a list of strings”, “type”: “array”, “items”: {“type”: “string”}}}, “required”: [“foo”]}}
the object {“foo”: [“bar”, “baz”]} is a well-formatted instance of the schema. The object {“properties”: {“foo”: [“bar”, “baz”]}} is not well-formatted.
Here is the output schema:
{"properties": {"flower_type": {"title": "Flower Type", "description": "\u9c9c\u82b1\u7684\u79cd\u7c7b", "type": "string"}, "price": {"title": "Price", "description": "\u9c9c\u82b1\u7684\u4ef7\u683c", "type": "integer"}, "description": {"title": "Description", "description": "\u9c9c\u82b1\u7684\u63cf\u8ff0\u6587\u6848", "type": "string"}, "reason": {"title": "Reason", "description": "\u4e3a\u4ec0\u4e48\u8981\u8fd9\u6837\u5199\u8fd9\u4e2a\u6587\u6848", "type": "string"}}, "required": ["flower_type", "price", "description", "reason"]}
下面,程序解析模型的输出。在这一步中,你使用你之前定义的输出解析器(output_parser)将模型的输出解析成了一个FlowerDescription的实例。FlowerDescription是你之前定义的一个Pydantic类,它包含了鲜花的类型、价格、描述以及描述的理由。
然后,将解析后的输出添加到DataFrame中。在这一步中,你将解析后的输出(即FlowerDescription实例)转换为一个字典,并将这个字典添加到你的DataFrame中。这个DataFrame是你用来存储所有鲜花描述的。
模型的最后输出如下:
输出的数据:
[{'flower_type': 'Rose', 'price': 50, 'description': '玫瑰是最浪漫的花,它具有柔和的粉红色,有着浓浓的爱意,价格实惠,50元就可以拥有一束玫瑰。', 'reason': '玫瑰代表着爱情,是最浪漫的礼物,以实惠的价格,可以让您尽情体验爱的浪漫。'},
{'flower_type': '百合', 'price': 30, 'description': '这支百合,柔美的花蕾,在你的手中摇曳,仿佛在与你深情的交谈', 'reason': '营造浪漫氛围'},
{'flower_type': 'Carnation', 'price': 20, 'description': '艳丽缤纷的康乃馨,带给你温馨、浪漫的气氛,是最佳的礼物选择!', 'reason': '康乃馨是一种颜色鲜艳、芬芳淡雅、具有浪漫寓意的鲜花,非常适合作为礼物,而且20元的价格比较实惠。'}]
因此,Pydantic的优点就是容易解析,而解析之后的字典格式的列表在进行数据分析、处理和存储时非常方便。每个字典代表一条记录,它的键( 即 "flower_type"、 "price"、 "description" 和 "reason")是字段名称,对应的值是这个字段的内容。这样一来,每个字段都对应一列,每个字典就是一行,适合以DataFrame的形式来表示和处理。
自动修复解析器(OutputFixingParser)实战
下面咱们来看看如何使用自动修复解析器。
首先,让我们来设计一个解析时出现的错误。
## 导入所需要的库和模块
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from typing import List
## 使用Pydantic创建一个数据格式,表示花
class Flower(BaseModel):
name: str = Field(description="name of a flower")
colors: List[str] = Field(description="the colors of this flower")
## 定义一个用于获取某种花的颜色列表的查询
flower_query = "Generate the charaters for a random flower."
## 定义一个格式不正确的输出
misformatted = "{'name': '康乃馨', 'colors': ['粉红色','白色','红色','紫色','黄色']}"
## 创建一个用于解析输出的Pydantic解析器,此处希望解析为Flower格式
parser = PydanticOutputParser(pydantic_object=Flower)
## 使用Pydantic解析器解析不正确的输出
parser.parse(misformatted)
这段代码如果运行,会出现错误。
langchain.schema.output_parser.OutputParserException: Failed to parse Flower from completion {'name': '康乃馨', 'colors': ['粉红色','白色']}. Got: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
这个错误消息来自Python的内建JSON解析器发现我们输入的JSON格式不正确。程序尝试用PydanticOutputParser来解析JSON字符串时,Python期望属性名称被双引号包围,但在给定的JSON字符串中是单引号。
当这个错误被触发后,程序进一步引发了一个自定义异常:OutputParserException,它提供了更多关于错误的上下文。这个自定义异常的消息表示在尝试解析flower对象时遇到了问题。
刚才说了,问题在于misformatted字符串的内容:
"{'name': '康乃馨', 'colors': ['粉红色','白色','红色','紫色','黄色']}"
应该改为:
'{"name": "康乃馨", "colors": ["粉红色","白色","红色","紫色","黄色"]}'
这样,你的JSON字符串就会使用正确的双引号格式,应该可以被正确地解析。
不过,这里我并不想这样解决问题,而是尝试使用OutputFixingParser来帮助咱们自动解决类似的格式错误。
## 从langchain库导入所需的模块
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers import OutputFixingParser
## 设置OpenAI API密钥
import os
os.environ["OPENAI_API_KEY"] = '你的OpenAI API Key'
## 使用OutputFixingParser创建一个新的解析器,该解析器能够纠正格式不正确的输出
new_parser = OutputFixingParser.from_llm(parser=parser, llm=ChatOpenAI())
## 使用新的解析器解析不正确的输出
result = new_parser.parse(misformatted) ## 错误被自动修正
print(result) ## 打印解析后的输出结果
用上面的新的new_parser来代替Parser进行解析,你会发现,JSON格式的错误问题被解决了,程序不再出错。
输出如下:
name='Rose' colors=['red', 'pink', 'white']
这里的秘密在于,在OutputFixingParser内部,调用了原有的PydanticOutputParser,如果成功,就返回;如果失败,它会将格式错误的输出以及格式化的指令传递给大模型,并要求LLM进行相关的修复。
神奇吧,大模型不仅给我们提供知识,还随时帮助分析并解决程序出错的信息。
重试解析器(RetryWithErrorOutputParser)实战
OutputFixingParser不错,但它只能做简单的格式修复。如果出错的不只是格式,比如,输出根本不完整,有缺失内容,那么仅仅根据输出和格式本身,是无法修复它的。
此时,通过实现输出解析器中parse_with_prompt方法,LangChain提供的重试解析器可以帮助我们利用大模型的推理能力根据原始提示找回相关信息。
我们通过分析一个重试解析器的用例来理解上面的这段话。
首先还是设计一个解析过程中的错误。
## 定义一个模板字符串,这个模板将用于生成提问
template = """Based on the user question, provide an Action and Action Input for what step should be taken.
{format_instructions}
Question: {query}
Response:"""
## 定义一个Pydantic数据格式,它描述了一个"行动"类及其属性
from pydantic import BaseModel, Field
class Action(BaseModel):
action: str = Field(description="action to take")
action_input: str = Field(description="input to the action")
## 使用Pydantic格式Action来初始化一个输出解析器
from langchain.output_parsers import PydanticOutputParser
parser = PydanticOutputParser(pydantic_object=Action)
## 定义一个提示模板,它将用于向模型提问
from langchain.prompts import PromptTemplate
prompt = PromptTemplate(
template="Answer the user query.\n{format_instructions}\n{query}\n",
input_variables=["query"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
prompt_value = prompt.format_prompt(query="What are the colors of Orchid?")
## 定义一个错误格式的字符串
bad_response = '{"action": "search"}'
parser.parse(bad_response) ## 如果直接解析,它会引发一个错误
由于bad_response只提供了action字段,而没有提供action_input字段,这与Action数据格式的预期不符,所以解析会失败。
我们首先尝试用OutputFixingParser来解决这个错误。
from langchain.output_parsers import OutputFixingParser
from langchain.chat_models import ChatOpenAI
fix_parser = OutputFixingParser.from_llm(parser=parser, llm=ChatOpenAI())
parse_result = fix_parser.parse(bad_response)
print('OutputFixingParser的parse结果:',parse_result)
OutputFixingParser的parse结果: action='search' action_input='query'
我们来看看这个尝试解决了什么问题,没解决什么问题。
解决的问题有:
- 不完整的数据:原始的bad_response只提供了action字段而没有action_input字段。OutputFixingParser已经填补了这个缺失,为action_input字段提供了值
'query'。
没解决的问题有:
- 具体性:尽管OutputFixingParser为action_input字段提供了默认值
'query',但这并不具有描述性。真正的查询是 “Orchid(兰花)的颜色是什么?”。所以,这个修复只是提供了一个通用的值,并没有真正地回答用户的问题。 - 可能的误导:
'query'可能被误解为一个指示,要求进一步查询某些内容,而不是作为实际的查询输入。
当然,还有更鲁棒的选择,我们最后尝试一下RetryWithErrorOutputParser这个解析器。
## 初始化RetryWithErrorOutputParser,它会尝试再次提问来得到一个正确的输出
from langchain.output_parsers import RetryWithErrorOutputParser
from langchain.llms import OpenAI
retry_parser = RetryWithErrorOutputParser.from_llm(
parser=parser, llm=OpenAI(temperature=0)
)
parse_result = retry_parser.parse_with_prompt(bad_response, prompt_value)
print('RetryWithErrorOutputParser的parse结果:',parse_result)
这个解析器没有让我们失望,成功地还原了格式,甚至也根据传入的原始提示,还原了action_input字段的内容。
RetryWithErrorOutputParser的parse结果: action='search' action_input='colors of Orchid'
总结时刻
结构化解析器和Pydantic解析器都旨在从大型语言模型中获取格式化的输出。结构化解析器更适合简单的文本响应,而Pydantic解析器则提供了对复杂数据结构和类型的支持。选择哪种解析器取决于应用的具体需求和输出的复杂性。
自动修复解析器主要适用于纠正小的格式错误,它更加“被动”,仅在原始输出出现问题时进行修复。重试解析器则可以处理更复杂的问题,包括格式错误和内容缺失。它通过重新与模型交互,使得输出更加完整和符合预期。
在选择哪种解析器时,需要考虑具体的应用场景。如果仅面临格式问题,自动修复解析器可能足够;但如果输出的完整性和准确性至关重要,那么重试解析器可能是更好的选择。
思考题
- 到目前为止,我们已经使用了哪些LangChain输出解析器?请你说一说它们的用法和异同。同时也请你尝试使用其他类型的输出解析器,并把代码与大家分享。
- 为什么大模型能够返回JSON格式的数据,输出解析器用了什么魔法让大模型做到了这一点?
- 自动修复解析器的“修复”功能具体来说是怎样实现的?请做debug,研究一下LangChain在调用大模型之前如何设计“提示”。
- 重试解析器的原理是什么?它主要实现了解析器类的哪个可选方法?
题目较多,可以选择性思考,期待在留言区看到你的分享。如果你觉得内容对你有帮助,也欢迎分享给有需要的朋友!最后如果你学有余力,可以进一步学习下面的延伸阅读。
延伸阅读
- 工具: Pydantic 是一个Python库,用于数据验证,可以确保数据符合特定的格式
- 文档:LangChain中的各种 Output Parsers
08|链(上):写一篇完美鲜花推文?用SequencialChain链接不同的组件
到这节课,我们已经学到了不少LangChain的应用,也体会到了LangChain功能的强大。但也许你心里开始出现了一个疑问:LangChain,其中的 Chain 肯定是关键组件,为什么我们还没有讲到呢?
这的确是个好问题。对于简单的应用程序来说,直接调用LLM就已经足够了。因此,在前几节课的示例中,我们主要通过LangChain中提供的提示模板、模型接口以及输出解析器就实现了想要的功能。
什么是 Chain
但是,如果你想开发更复杂的应用程序,那么就需要通过 “Chain” 来链接LangChain的各个组件和功能——模型之间彼此链接,或模型与其他组件链接。

这种将多个组件相互链接,组合成一个链的想法简单但很强大。它简化了复杂应用程序的实现,并使之更加模块化,能够创建出单一的、连贯的应用程序,从而使调试、维护和改进应用程序变得容易。
说到链的实现和使用,也简单。
- 首先LangChain通过设计好的接口,实现一个具体的链的功能。例如,LLM链(LLMChain)能够接受用户输入,使用 PromptTemplate 对其进行格式化,然后将格式化的响应传递给 LLM。这就相当于把整个Model I/O的流程封装到链里面。
- 实现了链的具体功能之后,我们可以通过将多个链组合在一起,或者将链与其他组件组合来构建更复杂的链。
所以你看,链在内部把一系列的功能进行封装,而链的外部则又可以组合串联。 链其实可以被视为LangChain中的一种基本功能单元。
LangChain中提供了很多种类型的预置链,目的是使各种各样的任务实现起来更加方便、规范。

我们先使用一下最基础也是最常见的LLMChain。
LLMChain:最简单的链
LLMChain围绕着语言模型推理功能又添加了一些功能,整合了PromptTemplate、语言模型(LLM或聊天模型)和 Output Parser,相当于把Model I/O放在一个链中整体操作。它使用提示模板格式化输入,将格式化的字符串传递给 LLM,并返回 LLM 输出。
举例来说,如果我想让大模型告诉我某种花的花语,如果不使用链,代码如下:
#----第一步 创建提示
## 导入LangChain中的提示模板
from langchain import PromptTemplate
## 原始字符串模板
template = "{flower}的花语是?"
## 创建LangChain模板
prompt_temp = PromptTemplate.from_template(template)
## 根据模板创建提示
prompt = prompt_temp.format(flower='玫瑰')
## 打印提示的内容
print(prompt)
#----第二步 创建并调用模型
## 导入LangChain中的OpenAI模型接口
from langchain import OpenAI
## 创建模型实例
model = OpenAI(temperature=0)
## 传入提示,调用模型,返回结果
result = model(prompt)
print(result)
输出:
玫瑰的花语是?
爱情、浪漫、美丽、永恒、誓言、坚贞不渝。
此时Model I/O的实现分为两个部分,提示模板的构建和模型的调用独立处理。
如果使用链,代码结构则显得更简洁。
## 导入所需的库
from langchain import PromptTemplate, OpenAI, LLMChain
## 原始字符串模板
template = "{flower}的花语是?"
## 创建模型实例
llm = OpenAI(temperature=0)
## 创建LLMChain
llm_chain = LLMChain(
llm=llm,
prompt=PromptTemplate.from_template(template))
## 调用LLMChain,返回结果
result = llm_chain("玫瑰")
print(result)
输出:
{'flower': '玫瑰', 'text': '\n\n爱情、浪漫、美丽、永恒、誓言、坚贞不渝。'}
在这里,我们就把提示模板的构建和模型的调用封装在一起了。
链的调用方式
链有很多种调用方式。
直接调用
刚才我们是直接调用的链对象。当我们像函数一样调用一个对象时,它实际上会调用该对象内部实现的__call__方法。
如果你的提示模板中包含多个变量,在调用链的时候,可以使用字典一次性输入它们。
prompt = PromptTemplate(
input_variables=["flower", "season"],
template="{flower}在{season}的花语是?",
)
llm_chain = LLMChain(llm=llm, prompt=prompt)
print(llm_chain({
'flower': "玫瑰",
'season': "夏季" }))
输出:
{'flower': '玫瑰', 'season': '夏季', 'text': '\n\n玫瑰在夏季的花语是爱的誓言,热情,美丽,坚定的爱情。'}
通过run方法
通过run方法,也等价于直接调用_call_函数。
语句:
llm_chain("玫瑰")
等价于:
llm_chain.run("玫瑰")
通过predict方法
predict方法类似于run,只是输入键被指定为关键字参数而不是 Python 字典。
result = llm_chain.predict(flower="玫瑰")
print(result)
通过apply方法
apply方法允许我们针对输入列表运行链,一次处理多个输入。
示例如下:
## apply允许您针对输入列表运行链
input_list = [
{"flower": "玫瑰",'season': "夏季"},
{"flower": "百合",'season': "春季"},
{"flower": "郁金香",'season': "秋季"}
]
result = llm_chain.apply(input_list)
print(result)
输出:
'''[{'text': '\n\n玫瑰在夏季的花语是“恋爱”、“热情”和“浪漫”。'},
{'text': '\n\n百合在春季的花语是“爱情”和“友谊”。'},
{'text': '\n\n郁金香在秋季的花语表达的是“热情”、“思念”、“爱恋”、“回忆”和“持久的爱”。'}]'''
通过generate方法
generate方法类似于apply,只不过它返回一个LLMResult对象,而不是字符串。LLMResult通常包含模型生成文本过程中的一些相关信息,例如令牌数量、模型名称等。
result = llm_chain.generate(input_list)
print(result)
输出:
generations=[[Generation(text='\n\n玫瑰在夏季的花语是“热情”、“爱情”和“幸福”。',
generation_info={'finish_reason': 'stop', 'logprobs': None})],
[Generation(text='\n\n春季的花语是爱情、幸福、美满、坚贞不渝。',
generation_info={'finish_reason': 'stop', 'logprobs': None})],
[Generation(text='\n\n秋季的花语是“思念”。银色的百合象征着“真爱”,而淡紫色的郁金香则象征着“思念”,因为它们在秋天里绽放的时候,犹如在思念着夏天的温暖。',
generation_info={'finish_reason': 'stop', 'logprobs': None})]]
llm_output={'token_usage': {'completion_tokens': 243, 'total_tokens': 301, 'prompt_tokens': 58}, 'model_name': 'text-davinci-003'}
run=[RunInfo(run_id=UUID('13058cca-881d-4b76-b0cf-0f9c831af6c4')),
RunInfo(run_id=UUID('7f38e33e-bab5-4d03-b77c-f50cd195affb')),
RunInfo(run_id=UUID('7a1e45fd-77ee-4133-aab0-431147186db8'))]
Sequential Chain:顺序链
好,到这里,你已经掌握了最基本的LLMChain的用法。下面,我要带着你用Sequential Chain 把几个LLMChain串起来,形成一个顺序链。

这个示例中,我们的目标是这样的:
- 第一步,我们假设大模型是一个植物学家,让他给出某种特定鲜花的知识和介绍。
- 第二步,我们假设大模型是一个鲜花评论者,让他参考上面植物学家的文字输出,对鲜花进行评论。
- 第三步,我们假设大模型是易速鲜花的社交媒体运营经理,让他参考上面植物学家和鲜花评论者的文字输出,来写一篇鲜花运营文案。
下面我们就来一步步地实现这个示例。
首先,导入所有需要的库。
## 设置OpenAI API密钥
import os
os.environ["OPENAI_API_KEY"] = '你的OpenAI API Key'
from langchain.llms import OpenAI
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.chains import SequentialChain
然后,添加第一个LLMChain,生成鲜花的知识性说明。
## 这是第一个LLMChain,用于生成鲜花的介绍,输入为花的名称和种类
llm = OpenAI(temperature=.7)
template = """
你是一个植物学家。给定花的名称和类型,你需要为这种花写一个200字左右的介绍。
花名: {name}
颜色: {color}
植物学家: 这是关于上述花的介绍:"""
prompt_template = PromptTemplate(input_variables=["name", "color"], template=template)
introduction_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="introduction")
接着,添加第二个LLMChain,根据鲜花的知识性说明生成评论。
## 这是第二个LLMChain,用于根据鲜花的介绍写出鲜花的评论
llm = OpenAI(temperature=.7)
template = """
你是一位鲜花评论家。给定一种花的介绍,你需要为这种花写一篇200字左右的评论。
鲜花介绍:
{introduction}
花评人对上述花的评论:"""
prompt_template = PromptTemplate(input_variables=["introduction"], template=template)
review_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="review")
接着,添加第三个LLMChain,根据鲜花的介绍和评论写出一篇自媒体的文案。
## 这是第三个LLMChain,用于根据鲜花的介绍和评论写出一篇自媒体的文案
template = """
你是一家花店的社交媒体经理。给定一种花的介绍和评论,你需要为这种花写一篇社交媒体的帖子,300字左右。
鲜花介绍:
{introduction}
花评人对上述花的评论:
{review}
社交媒体帖子:
"""
prompt_template = PromptTemplate(input_variables=["introduction", "review"], template=template)
social_post_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="social_post_text")
最后,添加SequentialChain,把前面三个链串起来。
## 这是总的链,我们按顺序运行这三个链
overall_chain = SequentialChain(
chains=[introduction_chain, review_chain, social_post_chain],
input_variables=["name", "color"],
output_variables=["introduction","review","social_post_text"],
verbose=True)
## 运行链,并打印结果
result = overall_chain({"name":"玫瑰", "color": "黑色"})
print(result)
最终的输出如下:
> Entering new chain...
> Finished chain.
{'name': '玫瑰', 'color': '黑色',
'introduction': '\n\n黑色玫瑰,这是一种对传统玫瑰花的独特颠覆,它的出现挑战了我们对玫瑰颜色的固有认知。它的花瓣如煤炭般黑亮,反射出独特的微光,而花蕊则是金黄色的,宛如夜空中的一颗星,强烈的颜色对比营造出一种前所未有的视觉效果。在植物学中,黑色玫瑰的出现无疑提供了一种新的研究方向,对于我们理解花朵色彩形成的机制有着重要的科学价值。',
'review': '\n\n黑色玫瑰,这不仅仅是一种花朵,更是一种完全颠覆传统的艺术表现形式。黑色的花瓣仿佛在诉说一种不可言喻的悲伤与神秘,而黄色的蕊瓣犹如漆黑夜空中的一抹亮色,给人带来无尽的想象。它将悲伤与欢乐,神秘与明亮完美地结合在一起,这是一种全新的视觉享受,也是一种对生活理解的深度表达。',
'social_post_text': '\n欢迎来到我们的自媒体平台,今天,我们要向您展示的是我们的全新产品——黑色玫瑰。这不仅仅是一种花,这是一种对传统观念的挑战,一种视觉艺术的革新,更是一种生活态度的象征。
这种别样的玫瑰花,其黑色花瓣宛如漆黑夜空中闪烁的繁星,富有神秘的深度感,给人一种前所未有的视觉冲击力。这种黑色,它不是冷酷、不是绝望,而是充满着独特的魅力和力量。而位于黑色花瓣之中的金黄色花蕊,则犹如星星中的灵魂,默默闪烁,给人带来无尽的遐想,充满活力与生机。
黑色玫瑰的存在,不仅挑战了我们对于玫瑰传统颜色的认知,它更是一种生动的生命象征,象征着那些坚韧、独特、勇敢面对生活的人们。黑色的花瓣中透露出一种坚韧的力量,而金黄的花蕊则是生活中的希望,二者的结合恰好象征了生活中的喜怒哀乐,体现了人生的百态。'}
至此,我们就通过两个LLM链和一个顺序链,生成了一篇完美的文案。
总结时刻
LangChain为我们提供了好用的“链”,帮助我们把多个组件像链条一样连接起来。这个“链条”其实就是一系列组件的调用顺序,这个顺序里还可以包括其他的“链条”。
我们可以使用多种方法调用链,也可以根据开发时的需求选择各种不同的链。

除去最常见的LLMChain和SequenceChain之外,LangChain中还自带大量其他类型的链,封装了各种各样的功能。你可以看一看这些链的实现细节,并尝试着使用它们。
下一节课,我们会继续介绍另外一种好用的链,RouterChain。
思考题
- 在 第3课 中,我们曾经用提示模板生成过一段鲜花的描述,代码如下:
for flower, price in zip(flowers, prices):
## 根据提示准备模型的输入
input = prompt.format(flower_name=flower, price=price)
## 获取模型的输出
output = model(input)
## 解析模型的输出
parsed_output = output_parser.parse(output)
请你使用LLMChain重构提示的format和获取模型输出部分,完成相同的功能。
提示:
llm_chain = LLMChain(
llm=model,
prompt=prompt)
- 上一道题目中,我要求你把提示的format和获取模型输出部分整合到LLMChain中,其实你还可以更进一步,把output_parser也整合到LLMChain中,让程序结构进一步简化,请你尝试一下。
提示:
llm_chain = LLMChain(
llm=model,
prompt=prompt,
output_parser=output_parser)
- 选择一个LangChain中的链(我们没用到的类型),尝试使用它解决一个问题,并分享你的用例和代码。
题目较多,可以选择性思考,期待在留言区看到你的分享。如果你觉得内容对你有帮助,也欢迎分享给有需要的朋友!最后如果你学有余力,可以进一步学习下面的延伸阅读。
延伸阅读
09|链(下):想学“育花”还是“插花”?用RouterChain确定客户意图
上一节课中,我带着你学习了Chain的基本概念,还使用了LLMChain和SequencialChain,这一节课,我们再来看看其他类型的一些Chain的用法。
任务设定
首先,还是先看一下今天要完成一个什么样的任务。
这里假设咱们的鲜花运营智能客服ChatBot通常会接到两大类问题。
- 鲜花养护(保持花的健康、如何浇水、施肥等)
- 鲜花装饰(如何搭配花、如何装饰场地等)
你的需求是, 如果接到的是第一类问题,你要给ChatBot A指示;如果接到第二类的问题,你要给ChatBot B指示。

我们可以根据这两个场景来构建两个不同的目标链。遇到不同类型的问题,LangChain会通过RouterChain来自动引导大语言模型选择不同的模板。
当然我们的运营过程会遇到更多种类的问题,你只需要通过同样的方法扩充逻辑即可。
整体框架
RouterChain,也叫路由链,能动态选择用于给定输入的下一个链。我们会根据用户的问题内容,首先使用路由器链确定问题更适合哪个处理模板,然后将问题发送到该处理模板进行回答。如果问题不适合任何已定义的处理模板,它会被发送到默认链。
在这里,我们会用LLMRouterChain和MultiPromptChain(也是一种路由链)组合实现路由功能,该MultiPromptChain会调用LLMRouterChain选择与给定问题最相关的提示,然后使用该提示回答问题。
具体步骤如下:
- 构建处理模板:为鲜花护理和鲜花装饰分别定义两个字符串模板。
- 提示信息:使用一个列表来组织和存储这两个处理模板的关键信息,如模板的键、描述和实际内容。
- 初始化语言模型:导入并实例化语言模型。
- 构建目标链:根据提示信息中的每个模板构建了对应的LLMChain,并存储在一个字典中。
- 构建LLM路由链:这是决策的核心部分。首先,它根据提示信息构建了一个路由模板,然后使用这个模板创建了一个LLMRouterChain。
- 构建默认链:如果输入不适合任何已定义的处理模板,这个默认链会被触发。
- 构建多提示链:使用MultiPromptChain将LLM路由链、目标链和默认链组合在一起,形成一个完整的决策系统。
具体实现
下面,就是用路由链自动选择处理模板的具体代码实现。
构建提示信息的模板
首先,我们针对两种场景,构建两个提示信息的模板。
## 构建两个场景的模板
flower_care_template = """你是一个经验丰富的园丁,擅长解答关于养花育花的问题。
下面是需要你来回答的问题:
{input}"""
flower_deco_template = """你是一位网红插花大师,擅长解答关于鲜花装饰的问题。
下面是需要你来回答的问题:
{input}"""
## 构建提示信息
prompt_infos = [
{
"key": "flower_care",
"description": "适合回答关于鲜花护理的问题",
"template": flower_care_template,
},
{
"key": "flower_decoration",
"description": "适合回答关于鲜花装饰的问题",
"template": flower_deco_template,
}]
初始化语言模型
接下来,我们初始化语言模型。
## 初始化语言模型
from langchain.llms import OpenAI
import os
os.environ["OPENAI_API_KEY"] = '你的OpenAI Key'
llm = OpenAI()
构建目标链
下面,我们循环prompt_infos这个列表,构建出两个目标链,分别负责处理不同的问题。
## 构建目标链
from langchain.chains.llm import LLMChain
from langchain.prompts import PromptTemplate
chain_map = {}
for info in prompt_infos:
prompt = PromptTemplate(template=info['template'],
input_variables=["input"])
print("目标提示:\n",prompt)
chain = LLMChain(llm=llm, prompt=prompt,verbose=True)
chain_map[info["key"]] = chain
这里,目标链提示是这样的:
目标提示:
input_variables=['input']
output_parser=None partial_variables={}
template='你是一个经验丰富的园丁,擅长解答关于养花育花的问题。\n 下面是需要你来回答的问题:\n
{input}' template_format='f-string'
validate_template=True
目标提示:
input_variables=['input']
output_parser=None partial_variables={}
template='你是一位网红插花大师,擅长解答关于鲜花装饰的问题。\n 下面是需要你来回答的问题:\n
{input}' template_format='f-string'
validate_template=True
对于每个场景,我们创建一个 LLMChain(语言模型链)。每个链会根据其场景模板生成对应的提示,然后将这个提示送入语言模型获取答案。
构建路由链
下面,我们构建路由链,负责查看用户输入的问题,确定问题的类型。
## 构建路由链
from langchain.chains.router.llm_router import LLMRouterChain, RouterOutputParser
from langchain.chains.router.multi_prompt_prompt import MULTI_PROMPT_ROUTER_TEMPLATE as RounterTemplate
destinations = [f"{p['key']}: {p['description']}" for p in prompt_infos]
router_template = RounterTemplate.format(destinations="\n".join(destinations))
print("路由模板:\n",router_template)
router_prompt = PromptTemplate(
template=router_template,
input_variables=["input"],
output_parser=RouterOutputParser(),)
print("路由提示:\n",router_prompt)
router_chain = LLMRouterChain.from_llm(llm,
router_prompt,
verbose=True)
输出:
路由模板:
Given a raw text input to a language model select the model prompt best suited for the input. You will be given the names of the available prompts and a description of what the prompt is best suited for. You may also revise the original input if you think that revising it will ultimately lead to a better response from the language model.
<< FORMATTING >>
Return a markdown code snippet with a JSON object formatted to look like:
```json
{{
"destination": string \ name of the prompt to use or "DEFAULT"
"next_inputs": string \ a potentially modified version of the original input
}}
```
REMEMBER: "destination" MUST be one of the candidate prompt names specified below OR it can be "DEFAULT" if the input is not well suited for any of the candidate prompts.
REMEMBER: "next_inputs" can just be the original input if you don't think any modifications are needed.
<< CANDIDATE PROMPTS >>
flower_care: 适合回答关于鲜花护理的问题
flower_decoration: 适合回答关于鲜花装饰的问题
<< INPUT >>
{input}
<< OUTPUT >>
路由提示:
input_variables=['input'] output_parser=RouterOutputParser(default_destination='DEFAULT', next_inputs_type=<class 'str'>, next_inputs_inner_key='input')
partial_variables={}
template='Given a raw text input to a language model select the model prompt best suited for the input. You will be given the names of the available prompts and a description of what the prompt is best suited for. You may also revise the original input if you think that revising it will ultimately lead to a better response from the language model.\n\n
<< FORMATTING >>\n
Return a markdown code snippet with a JSON object formatted to look like:\n```json\n{{\n "destination": string \\ name of the prompt to use or "DEFAULT"\n "next_inputs": string \\ a potentially modified version of the original input\n}}\n```\n\n
REMEMBER: "destination" MUST be one of the candidate prompt names specified below OR it can be "DEFAULT" if the input is not well suited for any of the candidate prompts.\n
REMEMBER: "next_inputs" can just be the original input if you don\'t think any modifications are needed.\n\n<< CANDIDATE PROMPTS >>\n
flower_care: 适合回答关于鲜花护理的问题\n
flower_decoration: 适合回答关于鲜花装饰的问题\n\n
<< INPUT >>\n{input}\n\n<< OUTPUT >>\n'
template_format='f-string'
validate_template=True
这里我说一下路由器链是如何构造提示信息,来引导大模型查看用户输入的问题并确定问题的类型的。
先看路由模板部分,这段模板字符串是一个指导性的说明,目的是引导语言模型正确处理用户的输入,并将其定向到适当的模型提示。
1. 路由模板的解释
路由模板是路由功能得以实现的核心。我们来详细分解一下这个模板的每个部分。
引言
Given a raw text input to a language model select the model prompt best suited for the input.
这是一个简单的引导语句,告诉模型你将给它一个输入,它需要根据这个输入选择最适合的模型提示。
You will be given the names of the available prompts and a description of what the prompt is best suited for.
这里进一步提醒模型,它将获得各种模型提示的名称和描述。
You may also revise the original input if you think that revising it will ultimately lead to a better response from the language model.
这是一个可选的步骤,告诉模型它可以更改原始输入以获得更好的响应。
格式说明(<< FORMATTING >>)
指导模型如何格式化其输出,使其以特定的方式返回结果。
Return a markdown code snippet with a JSON object formatted to look like:
表示模型的输出应该是一个 Markdown 代码片段,其中包含一个特定格式的 JSON 对象。
下面的代码块显示了期望的 JSON 结构,其中 destination 是模型选择的提示名称(或“DEFAULT”),而 next_inputs 是可能被修订的原始输入。
额外的说明和要求
REMEMBER: “destination” MUST be one of the candidate prompt names specified below OR it can be “DEFAULT”…
这是一个重要的指导,提醒模型 “destination” 字段的值必须是下面列出的提示之一或是 “DEFAULT”。
REMEMBER: “next_inputs” can just be the original input if you don’t think any modifications are needed.
这再次强调,除非模型认为有必要,否则原始输入不需要修改。
候选提示(<< CANDIDATE PROMPTS >>)
列出了两个示例模型提示及其描述:
- “flower_care: 适合回答关于鲜花护理的问题”,适合处理与花卉护理相关的问题。
- “flower_decoration: 适合回答关于鲜花装饰的问题”,适合处理与花卉装饰相关的问题。
输入/输出部分
<< INPUT >>\n{input}\n\n<< OUTPUT >>\n:
这部分为模型提供了一个格式化的框架,其中它将接收一个名为 {input} 的输入,并在此后的部分输出结果。
总的来说,这个模板的目的是让模型知道如何处理用户的输入,并根据提供的提示列表选择一个最佳的模型提示来回应。
2. 路由提示的解释
路由提示 (router_prompt)则根据路由模板,生成了具体传递给LLM的路由提示信息。
- 其中input_variables 指定模板接收的输入变量名,这里只有
"input"。 - output_parser 是一个用于解析模型输出的对象,它有一个默认的目的地和一个指向下一输入的键。
- template 是实际的路由模板,用于给模型提供指示。这就是刚才详细解释的模板内容。
- template_format 指定模板的格式,这里是
"f-string"。 - validate_template 是一个布尔值,如果为 True,则会在使用模板前验证其有效性。
简而言之,这个构造允许你将用户的原始输入送入路由器,然后路由器会决定将该输入发送到哪个具体的模型提示,或者是否需要对输入进行修订以获得最佳的响应。
构建默认链
除了处理目标链和路由链之外,我们还需要准备一个默认链。如果路由链没有找到适合的链,那么,就以默认链进行处理。
## 构建默认链
from langchain.chains import ConversationChain
default_chain = ConversationChain(llm=llm,
output_key="text",
verbose=True)
构建多提示链
最后,我们使用MultiPromptChain类把前几个链整合在一起,实现路由功能。这个MultiPromptChain类是一个多路选择链,它使用一个LLM路由器链在多个提示之间进行选择。
MultiPromptChain中有三个关键元素。
- router_chain(类型RouterChain):这是用于决定目标链和其输入的链。当给定某个输入时,这个router_chain决定哪一个destination_chain应该被选中,以及传给它的具体输入是什么。
- destination_chains(类型Mapping[str, LLMChain]):这是一个映射,将名称映射到可以将输入路由到的候选链。例如,你可能有多种处理文本输入的方法(或“链”),每种方法针对特定类型的问题。destination_chains可以是这样一个字典:
{'weather': weather_chain, 'news': news_chain}。在这里,weather_chain可能专门处理与天气相关的问题,而news_chain处理与新闻相关的问题。 - default_chain(类型LLMChain):当 router_chain 无法将输入映射到destination_chains中的任何一个链时,LLMChain 将使用此默认链。这是一个备选方案,确保即使路由器不能决定正确的链,也总有一个链可以处理输入。
它的工作流程如下:
- 输入首先传递给router_chain。
- router_chain根据某些标准或逻辑决定应该使用哪一个destination_chain。
- 输入随后被路由到选定的destination_chain,该链进行处理并返回结果。
- 如果router_chain不能决定正确的destination_chain,则输入会被传递给default_chain。
这样,MultiPromptChain就为我们提供了一个在多个处理链之间动态路由输入的机制,以得到最相关或最优的输出。
实现代码如下:
## 构建多提示链
from langchain.chains.router import MultiPromptChain
chain = MultiPromptChain(
router_chain=router_chain,
destination_chains=chain_map,
default_chain=default_chain,
verbose=True)
运行路由链
好了,至此我们的链路已经准备好了。现在开始提出各种问题,测试一下我们的链。
测试A:
print(chain.run(“如何为玫瑰浇水?”))
输出:

测试B:
print(chain.run(“如何为婚礼场地装饰花朵?”))
输出:

测试C:
print(chain.run(“如何考入哈佛大学?”))
输出:

这三个测试,分别被路由到了三个不同的目标链,其中两个是我们预设的“专家类型”目标链,而第三个问题:如何考入哈佛大学?被模型一眼看穿,并不属于任何鲜花运营业务场景,路由链把它抛入了一个 “default chain” —— ConversationChain 去解决。
总结时刻
在这个示例中,我们看到了LLMRouterChain以及MultiPromptChain。其中,LLMRouterChain继承自RouterChain;而MultiPromptChain则继承自MultiRouteChain。
整体上,我们通过MultiPromptChain把其他链组织起来,完成了路由功能。
chain = MultiPromptChain(
router_chain=router_chain,
destination_chains=chain_map,
default_chain=default_chain,
verbose=True)
在LangChain的 chains -> router -> base.py 文件中,可以看到RouterChain和MultiRouteChain的代码实现。
思考题
- 通过verbose=True这个选项的设定,在输出时显示了链的开始和结束日志,从而得到其相互调用流程。请你尝试把该选项设置为False,看一看输出结果有何不同。
- 在这个例子中,我们使用了ConversationChain作为default_chain,这个Chain是LLMChain的子类,你能否把这个Chain替换为LLMChain?
期待在留言区看到你的分享。如果你觉得内容对你有帮助,也欢迎分享给有需要的朋友!最后如果你学有余力,可以进一步学习下面的延伸阅读。
延伸阅读
10|记忆:通过Memory记住客户上次买花时的对话细节
在默认情况下,无论是LLM还是代理都是无状态的,每次模型的调用都是独立于其他交互的。也就是说,我们每次通过API开始和大语言模型展开一次新的对话,它都不知道你其实昨天或者前天曾经和它聊过天了。
你肯定会说,不可能啊,每次和ChatGPT聊天的时候,ChatGPT明明白白地记得我之前交待过的事情。

的确如此,ChatGPT之所以能够记得你之前说过的话,正是因为它使用了 记忆(Memory)机制,记录了之前的对话上下文,并且把这个上下文作为提示的一部分,在最新的调用中传递给了模型。在聊天机器人的构建中,记忆机制非常重要。

使用ConversationChain
不过,在开始介绍LangChain中记忆机制的具体实现之前,先重新看一下我们上一节课曾经见过的ConversationChain。
这个Chain最主要的特点是,它提供了包含AI 前缀和人类前缀的对话摘要格式,这个对话格式和记忆机制结合得非常紧密。
让我们看一个简单的示例,并打印出ConversationChain中的内置提示模板,你就会明白这个对话格式的意义了。
from langchain import OpenAI
from langchain.chains import ConversationChain
## 初始化大语言模型
llm = OpenAI(
temperature=0.5,
model_name="text-davinci-003"
)
## 初始化对话链
conv_chain = ConversationChain(llm=llm)
## 打印对话的模板
print(conv_chain.prompt.template)
输出:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.
Current conversation:
{history}
Human: {input}
AI:
这里的提示为人类(我们)和人工智能( text-davinci-003 )之间的对话设置了一个基本对话框架:这是 人类和 AI 之间的友好对话。AI 非常健谈并从其上下文中提供了大量的具体细节。 (The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. )
同时,这个提示试图通过说明以下内容来减少幻觉,也就是尽量减少模型编造的信息:
“如果 AI 不知道问题的答案,它就会如实说它不知道。”(If the AI does not know the answer to a question, it truthfully says it does not know.)
之后,我们看到两个参数 {history} 和 {input}。
- {history} 是存储会话记忆的地方,也就是人类和人工智能之间对话历史的信息。
- {input} 是新输入的地方,你可以把它看成是和ChatGPT对话时,文本框中的输入。
这两个参数会通过提示模板传递给 LLM,我们希望返回的输出只是对话的延续。

那么当有了 {history} 参数,以及 Human 和 AI 这两个前缀,我们就能够把历史对话信息存储在提示模板中,并作为新的提示内容在新一轮的对话过程中传递给模型。—— 这就是记忆机制的原理。
下面就让我们来在ConversationChain中加入记忆功能。
使用ConversationBufferMemory
在LangChain中,通过ConversationBufferMemory( 缓冲记忆)可以实现最简单的记忆机制。
下面,我们就在对话链中引入ConversationBufferMemory。
from langchain import OpenAI
from langchain.chains import ConversationChain
from langchain.chains.conversation.memory import ConversationBufferMemory
## 初始化大语言模型
llm = OpenAI(
temperature=0.5,
model_name="text-davinci-003")
## 初始化对话链
conversation = ConversationChain(
llm=llm,
memory=ConversationBufferMemory()
)
## 第一天的对话
## 回合1
conversation("我姐姐明天要过生日,我需要一束生日花束。")
print("第一次对话后的记忆:", conversation.memory.buffer)
输出:
第一次对话后的记忆:
Human: 我姐姐明天要过生日,我需要一束生日花束。
AI: 哦,你姐姐明天要过生日,那太棒了!我可以帮你推荐一些生日花束,你想要什么样的?我知道有很多种,比如玫瑰、康乃馨、郁金香等等。
在下一轮对话中,这些记忆会作为一部分传入提示。
## 回合2
conversation("她喜欢粉色玫瑰,颜色是粉色的。")
print("第二次对话后的记忆:", conversation.memory.buffer)
输出:
第二次对话后的记忆:
Human: 我姐姐明天要过生日,我需要一束生日花束。
AI: 哦,你姐姐明天要过生日,那太棒了!我可以帮你推荐一些生日花束,你想要什么样的?我知道有很多种,比如玫瑰、康乃馨、郁金香等等。
Human: 她喜欢粉色玫瑰,颜色是粉色的。
AI: 好的,那我可以推荐一束粉色玫瑰的生日花束给你。你想要多少朵?我可以帮你定制一束,比如说十朵、二十朵或者更多?
下面,我们继续对话,同时打印出此时提示模板的信息。
## 回合3 (第二天的对话)
conversation("我又来了,还记得我昨天为什么要来买花吗?")
print("/n第三次对话后时提示:/n",conversation.prompt.template)
print("/n第三次对话后的记忆:/n", conversation.memory.buffer)
模型输出:
Human: 我姐姐明天要过生日,我需要一束生日花束。
AI: 哦,你姐姐明天要过生日,那太棒了!我可以帮你推荐一些生日花束,你想要什么样的?我知道有很多种,比如玫瑰、康乃馨、郁金香等等。
Human: 她喜欢粉色玫瑰,颜色是粉色的。
AI: 好的,那我可以推荐一束粉色玫瑰的生日花束给你,你想要多少朵?
Human: 我又来了,还记得我昨天为什么要来买花吗?
AI: 是的,我记得你昨天来买花是因为你姐姐明天要过生日,你想要买一束粉色玫瑰的生日花束给她。
实际上,这些聊天历史信息,都被传入了ConversationChain的提示模板中的 {history} 参数,构建出了包含聊天记录的新的提示输入。
有了记忆机制,LLM能够了解之前的对话内容,这样简单直接地存储所有内容为LLM提供了最大量的信息,但是新输入中也包含了更多的Token(所有的聊天历史记录),这意味着响应时间变慢和更高的成本。而且,当达到LLM的令牌数(上下文窗口)限制时,太长的对话无法被记住(对于text-davinci-003和gpt-3.5-turbo,每次的最大输入限制是4096个Token)。
下面我们来看看针对Token太多、聊天历史记录过长的一些解决方案。
使用ConversationBufferWindowMemory
说到记忆,我们人类的大脑也不是无穷无尽的。所以说,有的时候事情太多,我们只能把有些遥远的记忆抹掉。毕竟,最新的经历最鲜活,也最重要。
ConversationBufferWindowMemory 是 缓冲窗口记忆,它的思路就是只保存最新最近的几次人类和AI的互动。因此,它在之前的“缓冲记忆”基础上增加了一个窗口值 k。这意味着我们只保留一定数量的过去互动,然后“忘记”之前的互动。
下面看一下示例。
from langchain import OpenAI
from langchain.chains import ConversationChain
from langchain.chains.conversation.memory import ConversationBufferWindowMemory
## 创建大语言模型实例
llm = OpenAI(
temperature=0.5,
model_name="text-davinci-003")
## 初始化对话链
conversation = ConversationChain(
llm=llm,
memory=ConversationBufferWindowMemory(k=1)
)
## 第一天的对话
## 回合1
result = conversation("我姐姐明天要过生日,我需要一束生日花束。")
print(result)
## 回合2
result = conversation("她喜欢粉色玫瑰,颜色是粉色的。")
## print("\n第二次对话后的记忆:\n", conversation.memory.buffer)
print(result)
## 第二天的对话
## 回合3
result = conversation("我又来了,还记得我昨天为什么要来买花吗?")
print(result)
第一回合的输出:
{'input': '我姐姐明天要过生日,我需要一束生日花束。',
'history': '',
'response': ' 哦,你姐姐明天要过生日!那太棒了!你想要一束什么样的花束呢?有很多种类可以选择,比如玫瑰花束、康乃馨花束、郁金香花束等等,你有什么喜欢的吗?'}
第二回合的输出:
{'input': '她喜欢粉色玫瑰,颜色是粉色的。',
'history': 'Human: 我姐姐明天要过生日,我需要一束生日花束。\nAI: 哦,你姐姐明天要过生日!那太棒了!你想要一束什么样的花束呢?有很多种类可以选择,比如玫瑰花束、康乃馨花束、郁金香花束等等,你有什么喜欢的吗?',
'response': ' 好的,那粉色玫瑰花束怎么样?我可以帮你找到一束非常漂亮的粉色玫瑰花束,你觉得怎么样?'}
第三回合的输出:
{'input': '我又来了,还记得我昨天为什么要来买花吗?',
'history': 'Human: 她喜欢粉色玫瑰,颜色是粉色的。\nAI: 好的,那粉色玫瑰花束怎么样?我可以帮你找到一束非常漂亮的粉色玫瑰花束,你觉得怎么样?',
'response': ' 当然记得,你昨天来买花是为了给你喜欢的人送一束粉色玫瑰花束,表达你对TA的爱意。'}
在给定的例子中,设置 k=1,这意味着窗口只会记住与AI之间的最新的互动,即只保留上一次的人类回应和AI的回应。
在第三个回合,当我们询问“还记得我昨天为什么要来买花吗?”,由于我们只保留了最近的互动(k=1),模型已经忘记了正确的答案。所以,虽然它说记得,但只能模糊地说出“喜欢的人”,而没有说关键字“姐姐”。不过,如果(我是说如果哈)在第二个回合,模型能回答“我可以帮你 为你姐姐 找到…”,那么,尽管我们没有第一回合的历史记录,但凭着上一个回合的信息,模型还是有可能推断出昨天来的人买花的真实意图。
尽管这种方法不适合记住遥远的互动,但它非常擅长限制使用的Token数量。如果只需要记住最近的互动,缓冲窗口记忆是一个很好的选择。但是,如果需要混合远期和近期的互动信息,则还有其他选择。
使用ConversationSummaryMemory
上面说了,如果模型在第二轮回答的时候,能够说出“我可以帮你为你姐姐找到…”,那么在第三轮回答时,即使窗口大小 k=1,还是能够回答出正确答案。
这是为什么?
因为模型 在回答新问题的时候,对之前的问题进行了总结性的重述。
ConversationSummaryMemory( 对话总结记忆)的思路就是将对话历史进行汇总,然后再传递给 {history} 参数。这种方法旨在通过对之前的对话进行汇总来避免过度使用 Token。
ConversationSummaryMemory有这么几个核心特点。
- 汇总对话:此方法不是保存整个对话历史,而是每次新的互动发生时对其进行汇总,然后将其添加到之前所有互动的“运行汇总”中。
- 使用LLM进行汇总:该汇总功能由另一个LLM驱动,这意味着对话的汇总实际上是由AI自己进行的。
- 适合长对话:对于长对话,此方法的优势尤为明显。虽然最初使用的 Token 数量较多,但随着对话的进展,汇总方法的增长速度会减慢。与此同时,常规的缓冲内存模型会继续线性增长。
下面,我们来看看使用ConversationSummaryMemory的代码示例。
from langchain.chains.conversation.memory import ConversationSummaryMemory
## 初始化对话链
conversation = ConversationChain(
llm=llm,
memory=ConversationSummaryMemory(llm=llm)
)
第一回合的输出:
{'input': '我姐姐明天要过生日,我需要一束生日花束。',
'history': '',
'response': ' 我明白,你需要一束生日花束。我可以为你提供一些建议吗?我可以推荐一些花束给你,比如玫瑰,康乃馨,百合,仙客来,郁金香,满天星等等。挑选一束最适合你姐姐的生日花束吧!'}
第二回合的输出:
{'input': '她喜欢粉色玫瑰,颜色是粉色的。',
'history': "\nThe human asked what the AI thinks of artificial intelligence. The AI thinks artificial intelligence is a force for good because it will help humans reach their full potential. The human then asked the AI for advice on what type of flower bouquet to get for their sister's birthday, to which the AI provided a variety of suggestions.",
'response': ' 为了为你的姐姐的生日准备一束花,我建议你搭配粉色玫瑰和白色康乃馨。你可以在玫瑰花束中添加一些紫色的满天星,或者添加一些绿叶以增加颜色对比。这将是一束可爱的花束,让你姐姐的生日更加特别。'}
第三回合的输出:
{'input': '我又来了,还记得我昨天为什么要来买花吗?',
'history': "\n\nThe human asked what the AI thinks of artificial intelligence. The AI thinks artificial intelligence is a force for good because it will help humans reach their full potential. The human then asked the AI for advice on what type of flower bouquet to get for their sister's birthday, to which the AI suggested pink roses and white carnations with the addition of purple aster flowers and green leaves for contrast. This would make a lovely bouquet to make the sister's birthday extra special.",
'response': ' 确实,我记得你昨天想买一束花给你的姐姐作为生日礼物。我建议你买粉红色的玫瑰花和白色的康乃馨花,再加上紫色的雏菊花和绿叶,这样可以让你的姐姐的生日更加特别。'}
看得出来,这里的 'history',不再是之前人类和AI对话的简单复制粘贴,而是经过了总结和整理之后的一个综述信息。
这里,我们 不仅仅利用了LLM来回答每轮问题,还利用LLM来对之前的对话进行总结性的陈述,以节约Token数量。这里,帮我们总结对话的LLM,和用来回答问题的LLM,可以是同一个大模型,也可以是不同的大模型。
ConversationSummaryMemory的优点是对于长对话,可以减少使用的 Token 数量,因此可以记录更多轮的对话信息,使用起来也直观易懂。不过,它的缺点是,对于较短的对话,可能会导致更高的 Token 使用。另外,对话历史的记忆完全依赖于中间汇总LLM的能力,还需要为汇总LLM使用 Token,这增加了成本,且并不限制对话长度。
通过对话历史的汇总来优化和管理 Token 的使用,ConversationSummaryMemory 为那些预期会有多轮的、长时间对话的场景提供了一种很好的方法。然而,这种方法仍然受到 Token 数量的限制。在一段时间后,我们仍然会超过大模型的上下文窗口限制。
而且,总结的过程中并没有区分近期的对话和长期的对话(通常情况下近期的对话更重要),所以我们还要继续寻找新的记忆管理方法。
使用ConversationSummaryBufferMemory
我要为你介绍的最后一种记忆机制是ConversationSummaryBufferMemory,即 对话总结缓冲记忆,它是一种 混合记忆 模型,结合了上述各种记忆机制,包括ConversationSummaryMemory 和 ConversationBufferWindowMemory的特点。这种模型旨在在对话中总结早期的互动,同时尽量保留最近互动中的原始内容。
它是通过max_token_limit这个参数做到这一点的。当最新的对话文字长度在300字之内的时候,LangChain会记忆原始对话内容;当对话文字超出了这个参数的长度,那么模型就会把所有超过预设长度的内容进行总结,以节省Token数量。
from langchain.chains.conversation.memory import ConversationSummaryBufferMemory
## 初始化对话链
conversation = ConversationChain(
llm=llm,
memory=ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=300))
第一回合的输出:
{'input': '我姐姐明天要过生日,我需要一束生日花束。',
'history': '',
'response': ' 哇,你姐姐要过生日啊!那太棒了!我建议你去买一束色彩鲜艳的花束,因为这样可以代表你给她的祝福和祝愿。你可以去你家附近的花店,或者也可以从网上订购,你可以看看有没有特别的花束,比如彩色玫瑰或者百合花,它们会更有特色。'}
第二回合的输出:
{'input': '她喜欢粉色玫瑰,颜色是粉色的。',
'history': 'Human: 我姐姐明天要过生日,我需要一束生日花束。\nAI: 哇,你姐姐要过生日啊!那太棒了!我建议你去买一束色彩鲜艳的花束,因为这样可以代表你给她的祝福和祝愿。你可以去你家附近的花店,或者也可以从网上订购,你可以看看有没有特别的花束,比如彩色玫瑰或者百合花,它们会更有特色。',
'response': ' 好的,那粉色玫瑰就是一个很好的选择!你可以买一束粉色玫瑰花束,这样你姐姐会很开心的!你可以在花店里找到粉色玫瑰,也可以从网上订购,你可以根据你的预算,选择合适的数量。另外,你可以考虑添加一些装饰,比如细绳、彩带或者小礼品'}
第三回合的输出:
{'input': '我又来了,还记得我昨天为什么要来买花吗?',
'history': "System: \nThe human asked the AI for advice on buying a bouquet for their sister's birthday. The AI suggested buying a vibrant bouquet as a representation of their wishes and blessings, and recommended looking for special bouquets like colorful roses or lilies for something more unique.\nHuman: 她喜欢粉色玫瑰,颜色是粉色的。\nAI: 好的,那粉色玫瑰就是一个很好的选择!你可以买一束粉色玫瑰花束,这样你姐姐会很开心的!你可以在花店里找到粉色玫瑰,也可以从网上订购,你可以根据你的预算,选择合适的数量。另外,你可以考虑添加一些装饰,比如细绳、彩带或者小礼品",
'response': ' 是的,我记得你昨天来买花是为了给你姐姐的生日。你想买一束粉色玫瑰花束来表达你的祝福和祝愿,你可以在花店里找到粉色玫瑰,也可以从网上订购,你可以根据你的预算,选择合适的数量。另外,你可以考虑添加一些装饰,比如细绳、彩带或者小礼品}
不难看出,在第二回合,记忆机制完整地记录了第一回合的对话,但是在第三回合,它察觉出前两轮的对话已经超出了300个字节,就把早期的对话加以总结,以节省Token资源。
ConversationSummaryBufferMemory的优势是通过总结可以回忆起较早的互动,而且有缓冲区确保我们不会错过最近的互动信息。当然,对于较短的对话,ConversationSummaryBufferMemory也会增加Token数量。
总体来说,ConversationSummaryBufferMemory为我们提供了大量的灵活性。它是我们迄今为止的唯一记忆类型,可以回忆起较早的互动并完整地存储最近的互动。在节省Token数量方面,ConversationSummaryBufferMemory与其他方法相比,也具有竞争力。
总结时刻
好的,今天我给你介绍了一种对话链和四种类型的对话记忆机制,那么我们可以通过一个表格对这四种类型的记忆做一个整体比较。
四种记忆机制的比较如下:

网上还有人总结了一个示意图,体现出了当对话轮次逐渐增加时,各种记忆机制对Token的消耗数量。意图向我们表达的是:有些记忆机制,比如说ConversationSummaryBufferMemory和ConversationSummaryMemory,在对话轮次较少的时候可能会浪费一些Token,但是多轮对话过后,Token的节省就逐渐体现出来了。
当然ConversationBufferWindowMemory对于Token的节省最为直接,但是它会完全遗忘掉K轮之前的对话内容,因此对于某些场景也不是最佳选择。

思考题
- 在你的客服聊天机器人设计中,你会首先告知客户:“亲,我的记忆能力有限,只能记住和你的最近10次对话哦。如果我忘了之前的对话,请你体谅我。” 当有了这样的预设,你会为你的ChatBot选择那种记忆机制?
- 尝试改变示例程序ConversationBufferWindowMemory中的k值,并增加对话轮次,看看记忆效果。
- 尝试改变示例程序ConversationSummaryBufferMemory中的max_token_limit值,看看记忆效果。
期待在留言区看到你的分享。如果你觉得内容对你有帮助,也欢迎分享给有需要的朋友!最后如果你学有余力,可以进一步学习下面的延伸阅读。
延伸阅读
11|代理(上):ReAct框架,推理与行动的协同
在之前介绍的思维链(CoT)中,我向你展示了 LLMs 执行推理轨迹的能力。在给出答案之前,大模型通过中间推理步骤(尤其是与少样本提示相结合)能够实现复杂的推理,获得更好的结果,以完成更具挑战的任务。
然而,仅仅应用思维链推理并不能解决大模型的固有问题: 无法主动更新自己的知识,导致出现事实幻觉。也就是说,因为缺乏和外部世界的接触,大模型只拥有训练时见过的知识,以及提示信息中作为上下文提供的附加知识。如果你问的问题超出它的知识范围,要么大模型向你坦白:“我的训练时间截至XXXX年XX月XX日”,要么它就会开始一本正经地胡说。
下面这张图就属于第二种情况,我制作的一个Prompt骗过了大模型,它会误以为我引述的很多虚构的东西是事实,而且它还会顺着这个思路继续胡编乱造。

这个问题如何解决呢?
也不难。你可以让大模型先在本地知识库中进行搜索,检查一下提示中的信息的真实性,如果真实,再进行输出;如果不真实,则进行修正。如果本地知识库找不到相应的信息,可以调用工具进行外部搜索,来检查提示信息的真实性。

上面所说的无论本地知识库还是搜索引擎,都不是封装在大模型内部的知识,我们把它们称为“外部工具”。
代理的作用
每当你遇到这种需要模型做自主判断、自行调用工具、自行决定下一步行动的时候,Agent(也就是代理)就出场了。

代理就像一个多功能的接口,它能够接触并使用一套工具。根据用户的输入,代理会决定调用哪些工具。它不仅可以同时使用多种工具,而且可以将一个工具的输出数据作为另一个工具的输入数据。
在LangChain中使用代理,我们只需要理解下面三个元素。
- 大模型:提供逻辑的引擎,负责生成预测和处理输入。
- 与之交互的 外部工具:可能包括数据清洗工具、搜索引擎、应用程序等。
- 控制交互的 代理:调用适当的外部工具,并管理整个交互过程的流程。

上面的思路看似简单,其实很值得我们仔细琢磨。
这个过程有很多地方需要大模型自主判断下一步行为(也就是操作)要做什么,如果不加引导,那大模型本身是不具备这个能力的。比如下面这一系列的操作:
- 什么时候开始在本地知识库中搜索(这个比较简单,毕竟是第一个步骤,可以预设)?
- 怎么确定本地知识库的检索已经完成,可以开始下一步?
- 调用哪一种外部搜索工具(比如Google引擎)?
- 如何确定外部搜索工具返回了想要找的内容?
- 如何确定信息真实性的检索已经全部完成,可以开始下一步?
那么,LangChain中的代理是怎样自主计划、自行判断,并执行行动的呢?
ReAct框架
这里我要请你思考一下:如果你接到一个新任务,你将如何做出决策并完成下一步的行动?
比如说,你在运营花店的过程中,经常会经历天气变化而导致的鲜花售价变化,那么,每天早上你会如何为你的鲜花定价?
也许你会告诉我,我会去Google上面查一查今天的鲜花成本价啊( 行动),也就是我预计的进货的价格,然后我会根据这个价格的高低( 观察),来确定我要加价多少( 思考),最后计算出一个售价( 行动)!

你看,在这个简单的例子中,你有观察、有思考,然后才会具体行动。这里的观察和思考,我们统称为推理(Reasoning)过程,推理指导着你的行动(Acting)。
我们今天要讲的 ReAct 框架的灵感正是来自“行动”和“推理”之间的协同作用,这种协同作用使得咱们人类能够学习新任务并做出决策或推理。这个框架,也是大模型能够作为“智能代理”,自主、连续、交错地生成推理轨迹和任务特定操作的理论基础。
先和你说明一点,此 ReAct 并非指代流行的前端开发框架React,它在这里专指如何指导大语言模型推理和行动的一种思维框架。这个思维框架是Shunyu Yao等人在ICLR 2023会议论文《 ReAct: Synergizing Reasoning and Acting in Language Models》(ReAct:在语言模型中协同推理和行动)中提出的。
这篇文章的一个关键启发在于: 大语言模型可以通过生成推理痕迹和任务特定行动来实现更大的协同作用。
具体来说,就是引导模型生成一个任务解决轨迹:观察环境-进行思考-采取行动,也就是观察-思考-行动。那么,再进一步进行简化,就变成了推理-行动,也就是Reasoning-Acting框架。
其中,Reasoning包括了对当前环境和状态的观察,并生成推理轨迹。这使模型能够诱导、跟踪和更新操作计划,甚至处理异常情况。Acting在于指导大模型采取下一步的行动,比如与外部源(如知识库或环境)进行交互并且收集信息,或者给出最终答案。
ReAct的每一个推理过程都会被详细记录在案,这也改善大模型解决问题时的可解释性和可信度,而且这个框架在各种语言和决策任务中都得到了很好的效果。
下面让我们用一个具体的示例来说明这一点。比如我给出大模型这样一个任务:在一个虚拟环境中找到一个胡椒瓶并将其放在一个抽屉里。
在这个任务中,没有推理能力的模型不能够在房间的各个角落中进行寻找,或者在找到胡椒瓶之后不能够判断下一步的行动,因而无法完成任务。如果使用ReAct,这一系列子目标将被具体地捕获在每一个思考过程中。
现在,让我们回到开始的时候我们所面临的问题。仅仅使用思维链(CoT)提示,LLMs能够执行推理轨迹,以完成算术和常识推理等问题,但这样的模型因为缺乏和外部世界的接触或无法更新自己的知识,会导致幻觉的出现。

而将 ReAct框架和思维链(CoT)结合使用,则能够让大模型在推理过程同时使用内部知识和获取到的外部信息,从而给出更可靠和实际的回应,也提高了 LLMs 的可解释性和可信度。
LangChain正是通过Agent类,将ReAct框架进行了完美封装和实现,这一下子就赋予了大模型极大的自主性(Autonomy), 你的大模型现在从一个仅仅可以通过自己内部知识进行对话聊天的 Bot , 飞升为了一个有手有脚能使用工具的智能代理。
ReAct框架会提示 LLMs 为任务生成推理轨迹和操作,这使得代理能系统地执行动态推理来创建、维护和调整操作计划,同时还支持与外部环境(例如Google搜索、Wikipedia)的交互,以将额外信息合并到推理中。
通过代理实现ReAct框架
下面,就让我们用LangChain中最为常用的 ZERO_SHOT_REACT_DESCRIPTION ——这种常用代理类型,来剖析一下LLM是如何在ReAct框架的指导之下进行推理的。
此处,我们要给代理一个任务,这个任务是找到玫瑰的当前市场价格,然后计算出加价15%后的新价格。
在开始之前,有一个准备工作,就是你需要在 serpapi.com 注册一个账号,并且拿到你的 SERPAPI_API_KEY,这个就是我们要为大模型提供的 Google 搜索工具。

先安装SerpAPI的包。
pip install google-search-results
设置好OpenAI和SerpAPI的API密钥。
## 设置OpenAI和SERPAPI的API密钥
import os
os.environ["OPENAI_API_KEY"] = 'Your OpenAI API Key'
os.environ["SERPAPI_API_KEY"] = 'Your SerpAPI API Key'
再导入所需的库。
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.llms import OpenAI
然后加载将用于控制代理的语言模型。
llm = OpenAI(temperature=0)
接下来,加载一些要使用的工具,包括serpapi(这是调用Google搜索引擎的工具)以及llm-math(这是通过LLM进行数学计算的工具)。
tools = load_tools(["serpapi", "llm-math"], llm=llm)
最后,让我们使用工具、语言模型和代理类型来初始化代理。
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
好了,现在我们让代理来回答我刚才提出的问题了!目前市场上玫瑰花的平均价格是多少?如果我在此基础上加价15%卖出,应该如何定价?
agent.run("目前市场上玫瑰花的平均价格是多少?如果我在此基础上加价15%卖出,应该如何定价?")
大模型成功遵循了ReAct框架,它输出的思考与行动轨迹如下:
> Entering new chain...
I need to find the current market price of roses and then calculate the new price with a 15% markup.
Action: Search
Action Input: "Average price of roses"
Observation: According to the study, the average price for a dozen roses in the United States is $80.16. The Empire State hovers closer to that number than its neighbors, with a bouquet setting back your average New Yorker $78.33.
Thought: I need to calculate the new price with a 15% markup.
Action: Calculator
Action Input: 80.16 * 1.15
Observation: Answer: 92.18399999999998
Thought: I now know the final answer.
Final Answer: The new price with a 15% markup would be $92.18.
> Finished chain.

可以看到,ZERO_SHOT_REACT_DESCRIPTION类型的智能代理在LangChain中,自动形成了一个完善的思考与行动链条,而且给出了正确的答案。
你可以对照下面这个表格,再巩固一下这个链条中的每一个环节。

这个思维链条中,智能代理有思考、有观察、有行动,成功通过搜索和计算两个操作,完成了任务。在下一讲中,我们将继续深入剖析LangChain中的不同类型的代理,并利用它完成更为复杂的任务。
总结时刻
这节课我们介绍了什么是LangChain中的代理,更重要的是,我们介绍了代理自主行动的驱动力—— ReAct框架。
通过ReAct框架,大模型将被引导生成一个任务解决轨迹,即观察环境-进行思考-采取行动。观察和思考阶段被统称为推理(Reasoning),而实施下一步行动的阶段被称为行动(Acting)。在每一步推理过程中,都会详细记录下来,这也改善了大模型解决问题时的可解释性和可信度。
- 在推理阶段,模型对当前环境和状态进行观察,并生成推理轨迹,从而使模型能够诱导、跟踪和更新操作计划,甚至处理异常情况。
- 在行动阶段,模型会采取下一步的行动,如与外部源(如知识库或环境)进行交互并收集信息,或给出最终答案。
ReAct框架的这些优点,使得它在未来的发展中具有巨大的潜力。随着技术的进步,我们可以期待ReAct框架将能够处理更多、更复杂的任务。特别是随着具身智能的发展,ReAct框架将能够使智能代理在虚拟或实际环境中进行更复杂的交互。例如,智能代理可能会在虚拟环境中进行导航,或者在实际环境中操作物理对象。这将大大扩展AI的应用范围,使得它们能够更好地服务于我们的生活和工作。
思考题
- 在ReAct框架中,推理和行动各自代表什么?其相互之间的关系如何?
- 为什么说ReAct框架能改善大模型解决问题时的可解释性和可信度?
- 你能否说一说LangChain中的代理和链的核心差异?
期待在留言区看到你的思考,如果你觉得内容对你有帮助,也欢迎分享给有需要的朋友!最后如果你学有余力,可以进一步学习下面延伸阅读的两篇论文。
延伸阅读
- 论文, ReAct:在语言模型中协同推理和行动,Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K., & Cao, Y. (2023). ReAct: Synergizing Reasoning and Acting in Language Models. arXiv preprint arXiv:2210.03629
- 论文, ART:大型语言模型的自动多步推理和工具使用, Paranjape, B., Lundberg, S., Singh, S., Hajishirzi, H., Zettlemoyer, L., & Ribeiro, M. T. (2023). ART: Automatic multi-step reasoning and tool-use for large language models. arXiv preprint arXiv:2303.09014.
12|代理(中):AgentExecutor究竟是怎样驱动模型和工具完成任务的?
上节课中,你了解了ReAct框架的原理,最后我给你留了一道思考题,让你说一说LangChain中的“代理”和“链”的差异究竟是什么。
我的答案是: 在链中,一系列操作被硬编码(在代码中)。在代理中,语言模型被用作推理引擎来确定要采取哪些操作以及按什么顺序执行这些操作。
下面这个图,就展现出了Agent接到任务之后,自动进行推理,然后自主调用工具完成任务的过程。

origin_url=images%2F708048%2Faeb7497d833b0b3188fbc7152282b0e3.jpg&pos_id=img-DhKADb8L-1764311999053)
那么,你看LangChain,乃至整个大模型应用开发的核心理念就呼之欲出了。这个核心理念就是 操作的序列并非硬编码在代码中,而是使用语言模型(如GPT-3或GPT-4)来选择执行的操作序列。
这里,我又一次重复了上一段话,显得有点啰嗦,但是这个思路真的是太重要了,它也凸显了LLM作为AI自主决定程序逻辑这个编程新范式的价值,我希望你仔细认真地去理解。
Agent 的关键组件
在LangChain的代理中,有这样几个关键组件。
-
代理(Agent):这个类决定下一步执行什么操作。它由一个语言模型和一个提示(prompt)驱动。提示可能包含代理的性格(也就是给它分配角色,让它以特定方式进行响应)、任务的背景(用于给它提供更多任务类型的上下文)以及用于激发更好推理能力的提示策略(例如ReAct)。LangChain中包含很多种不同类型的代理。
-
工具(Tools):工具是代理调用的函数。这里有两个重要的考虑因素:一是让代理能访问到正确的工具,二是以最有帮助的方式描述这些工具。如果你没有给代理提供正确的工具,它将无法完成任务。如果你没有正确地描述工具,代理将不知道如何使用它们。LangChain提供了一系列的工具,同时你也可以定义自己的工具。
-
工具包(Toolkits):工具包是一组用于完成特定目标的彼此相关的工具,每个工具包中包含多个工具。比如LangChain的Office365工具包中就包含连接Outlook、读取邮件列表、发送邮件等一系列工具。当然LangChain中还有很多其他工具包供你使用。
-
代理执行器(AgentExecutor):代理执行器是代理的运行环境,它调用代理并执行代理选择的操作。执行器也负责处理多种复杂情况,包括处理代理选择了不存在的工具的情况、处理工具出错的情况、处理代理产生的无法解析成工具调用的输出的情况,以及在代理决策和工具调用进行观察和日志记录。
总的来说,代理就是一种用语言模型做出决策、调用工具来执行具体操作的系统。通过设定代理的性格、背景以及工具的描述,你可以定制代理的行为,使其能够根据输入的文本做出理解和推理,从而实现自动化的任务处理。而代理执行器(AgentExecutor)就是上述机制得以实现的引擎。
在这一讲中,我们将深入LangChain源代码的内部,揭示代理是如何通过代理执行器来自动决策的。
深挖 AgentExcutor 的运行机制
让我们先来回顾一下上一讲中的关键代码。
llm = OpenAI(temperature=0) ## 大语言模型
tools = load_tools(["serpapi", "llm-math"], llm=llm) ## 工具-搜索和数学运算
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True) ## 代理
agent.run("目前市场上玫瑰花的平均价格是多少?如果我在此基础上加价15%卖出,应该如何定价?") ## 运行代理
在这段代码中,模型、工具、代理的初始化,以及代理运行的过程都极为简洁。但是,LangChain的内部封装的逻辑究竟是怎样的?我希望带着你至少弄清楚两个问题。
- 代理每次给大模型的具体提示是什么样子的?能够让模型给出下一步的行动指南,这个提示的秘密何在?
- 代理执行器是如何按照ReAct框架来调用模型,接收模型的输出,并根据这个输出来调用工具,然后又根据工具的返回结果生成新的提示的。
运行代码后我们得到下面的日志。

要回答上面的两个问题,仅仅观察LangChain输出的Log是不够的。我们需要深入到LangChain的程序内部,深挖一下AgentExcutor的运行机制。
开始 Debug
现在,请你用你的代码编辑工具(比如VS Code)在agent.run这个语句设置一个断点,用 “Step Into” 功能深入几层LangChain内部代码,直到我们进入了 agent.py文件的AgentExecutor类的内部方法 _take_next_step。
这个 _take_next_step 方法掌控着下一步要调用什么的计划,你可以看到self.agent.plan方法被调用,这是计划开始之处。

注意:如果使用VS Code,要把launch.json的justMycode项设置为false才可以debug LangChain包中的代码。

第一轮思考:模型决定搜索
在AgentExecutor 的_take_next_step 方法的驱动下,我们进一步Debug, 深入self.agent.plan方法,来到了整个行为链条的第一步—— Plan,这个Plan的具体细节是由Agent类的Plan方法来完成的,你可以看到,输入的问题将会被传递给llm_chain,然后接收llm_chain调用大模型的返回结果。

再往前进一步,我们就要开始调用大模型了,那么,LangChain到底传递给了大模型什么具体的提示信息,让大模型能够主动进行工具的选择呢?秘密在 LLMChain类的generate方法 中,我们可以看到提示的具体内容。

在Debug过程中,你可以观察prompt,也就是提示的具体内容,这里我把这个提示Copy出来,你可以看一下。
0: StringPromptValue(text='Answer the following questions as best you can. You have access to the following tools:\n\nSearch: A search engine. Useful for when you need to answer questions about current events. Input should be a search query.\nCalculator: Useful for when you need to answer questions about math.\n\nUse the following format:\n\nQuestion: the input question you must answer\nThought: you should always think about what to do\nAction: the action to take, should be one of [Search, Calculator]\nAction Input: the input to the action\nObservation: the result of the action\n... (this Thought/Action/Action Input/Observation can repeat N times)\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n\nBegin!\n\nQuestion: 目前市场上玫瑰花的平均价格是多少?如果我在此基础上加价15%卖出,应该如何定价?\nThought:
我来给你详细拆解一下这个prompt。注意,下面的解释文字不是原始提示,而是我添加的说明。
0: StringPromptValue(text='Answer the following questions as best you can. You have access to the following tools:\n\n
这句提示是让模型尽量回答问题,并告诉模型拥有哪些工具。
Search: A search engine. Useful for when you need to answer questions about current events. Input should be a search query.\n
这是向模型介绍第一个工具:搜索。
Calculator: Useful for when you need to answer questions about math.\n\n
这是向模型介绍第二个工具:计算器。
Use the following format:\n\n (指导模型使用下面的格式)
Question: the input question you must answer\n (问题)
Thought: you should always think about what to do\n (思考)
Action: the action to take, should be one of [Search, Calculator]\n (行动)
Action Input: the input to the action\n (行动的输入)
Observation: the result of the action\n… (观察:行动的返回结果)
(this Thought/Action/Action Input/Observation can repeat N times)\n (上面这个过程可以重复多次)
Thought: I now know the final answer\n (思考:现在我知道最终答案了)
Final Answer: the final answer to the original input question\n\n (最终答案)
上面,就是给模型的思考框架。具体解释可以看一下括号中的文字
Begin!\n\n
现在开始!
Question: 目前市场上玫瑰花的平均价格是多少?如果我在此基础上加价15%卖出,应该如何定价?\nThought:')
具体问题,也就是具体任务。
上面我一句句拆解的这个提示词,就是Agent之所以能够趋动大模型,进行 思考-行动-观察行动结果-再思考-再行动-再观察 这个循环的核心秘密。有了这样的提示词,模型就会不停地思考、行动,直到模型判断出问题已经解决,给出最终答案,跳出循环。

那么,调用大模型之后,模型具体返回了什么结果呢?

在Debug过程中,我们发现调用模型之后的outputs中包含下面的内容。
0: LLMResult(generations=[[Generation(text=' I need to find the current market price of roses and then calculate the new price with a 15% markup.\n
Action: Search\nAction Input: "Average price of roses"', generation_info={'finish_reason': 'stop', 'logprobs': None})]],
llm_output={'token_usage': {'completion_tokens': 36, 'total_tokens': 294, 'prompt_tokens': 258}, 'model_name': 'text-davinci-003'}, run=None)
把上面的内容拆解如下:
‘text’: ’ I need to find the current market price of roses and then calculate the new price with a 15% markup.\n (Text:问题文本)
Action: Search\n (行动:搜索)
Action Input: “Average price of roses”’ (行动的输入:搜索玫瑰平均价格)
看来,模型知道面对这个问题, 它自己根据现有知识解决不了,下一步行动是需要选择工具箱中的搜索工具。而此时,命令行中也输出了模型的第一步计划——调用搜索工具。


现在模型知道了要调用什么工具,第一轮的Plan部分就结束了。下面,我们就来到了AgentExecutor 的_take_next_step 的 工具调用部分。
在这里,因为模型返回了Action为Search,OutputParse解析了这个结果之后,LangChain很清楚地知道,Search工具会被调用。

工具调用完成之后,我们就拥有了一个对当前工具调用的 Observation,也就是当前工具调用的结果。

下一步,我们要再次调用大模型,形成新的 Thought,看看任务是否已经完成了,或者仍需要再次调用工具(新的工具或者再次调用同一工具)。
第二轮思考:模型决定计算
因为任务尚未完成,第二轮思考开始,程序重新进入了Plan环节。

此时,LangChain的LLM Chain根据目前的input,也就是历史对话记录生成了新的提示信息。


0: StringPromptValue(text='Answer the following questions as best you can. You have access to the following tools:\n\nSearch: A search engine. Useful for when you need to answer questions about current events. Input should be a search query.\nCalculator: Useful for when you need to answer questions about math.\n\nUse the following format:\n\nQuestion: the input question you must answer\nThought: you should always think about what to do\nAction: the action to take, should be one of [Search, Calculator]\nAction Input: the input to the action\nObservation: the result of the action\n... (this Thought/Action/Action Input/Observation can repeat N times)\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n\nBegin!\n\nQuestion: 目前市场上玫瑰花的平均价格是多少?如果我在此基础上加价15%卖出,应该如何定价?\nThought: I need to find the current market price of roses and then calculate the new price with a 15% markup.\nAction: Search\nAction Input: "Average price of roses"\nObservation: The average price for a dozen roses in the U.S. is $80.16. The state where a dozen roses cost the most is Hawaii at $108.33. That\'s 35% more expensive than the national average. A dozen roses are most affordable in Pennsylvania, costing $66.15 on average.\nThought:
我们再来拆解一下这个prompt。
0: StringPromptValue(text='Answer the following questions as best you can. You have access to the following tools:\n\n
这句提示是让模型尽量回答问题,并告诉模型拥有哪些工具。
Search: A search engine. Useful for when you need to answer questions about current events. Input should be a search query.\n
这是向模型介绍第一个工具:搜索。
Calculator: Useful for when you need to answer questions about math.\n\n
这是向模型介绍第二个工具:计算器。
Use the following format:\n\n (指导模型使用下面的格式)
Question: the input question you must answer\n (问题)
Thought: you should always think about what to do\n (思考)
Action: the action to take, should be one of [Search, Calculator]\n (行动)
Action Input: the input to the action\n (行动的输入)
Observation: the result of the action\n… (观察:行动的返回结果)
(this Thought/Action/Action Input/Observation can repeat N times)\n (上面这个过程可以重复多次)
Thought: I now know the final answer\n (思考:现在我知道最终答案了)
Final Answer: the final answer to the original input question\n\n (最终答案)
上面是一段比较细节的解释说明,看一下括号中的文字。
Begin!\n\n
现在开始!
Question: 目前市场上玫瑰花的平均价格是多少?如果我在此基础上加价15%卖出,应该如何定价?\n
具体问题,也就是具体任务。
这句之前的提示,与我们在第一轮思考时看到的完全相同。
Thought: I need to find the current market price of roses and then calculate the new price with a 15% markup.\n (思考:我需要找到玫瑰花的价格,并加入15%的加价)
Action: Search\nAction (行动:搜索)
Input: “Average price of roses”\n (行动的输入:玫瑰花的平均价格)
Observation: The average price for a dozen roses in the U.S. is $80.16. The state where a dozen roses cost the most is Hawaii at $108.33. That’s 35% more expensive than the national average. A dozen roses are most affordable in Pennsylvania, costing $66.15 on average.\n (观察:这里时搜索工具返回的玫瑰花价格信息)
Thought:’
思考:后面是大模型应该进一步推理的内容。
大模型根据上面这个提示,返回了下面的output信息。

AgentAction(tool='Calculator', tool_input='80.16 * 1.15', log=' I need to calculate the new price with a 15% markup.\nAction: Calculator\nAction Input: 80.16 * 1.15')
这个输出显示,模型告诉自己,“我需要计算新的Price,在搜索结果的基础上加价15%”,并确定Action为计算器,输入计算器工具的指令为80.16*1.15。 这是一个非常有逻辑性的思考。
经过解析之后的Thought在命令行中的输出如下:

有了上面的Thought做指引,AgentExecutor调用了第二个工具:LLMMath。现在开始计算。

因为这个数学工具也是调用LLM,我们可以看一下内部的提示,看看这个工具是怎样指导LLM做数学计算的。


这个提示,我把它拷贝出来,也拆解一下。
0: StringPromptValue(text='Translate a math problem into a expression that can be executed using Python’s numexpr library. Use the output of running this code to answer the question.\n\n
指定模型用Python的数学库来编程解决数学问题,而不是自己计算。这就规避了大模型数学推理能力弱的局限。
Question: ${Question with math problem.}\n (问题)
text\n${single line mathematical expression that solves the problem} n```\n (问题的数学描述)
…numexpr.evaluate(text)…\n``` (通过Python库运行问题的数学描述)
output\n${Output of running the code}\n```\n (输出的Python代码运行结果)
Answer: ${Answer}\n\n (问题的答案)
Begin.\n\n (开始)
从这里开始是两个数学式的解题示例。
Question: What is 37593 * 67?\n
```text\n37593 * 67\n```
\n…numexpr.evaluate(“37593 * 67”)…\n
```output\n2518731\n```\n
Answer: 2518731\n\n
Question: 37593^(1/5)\n
```text\n37593**(1/5)\n```\n…
numexpr.evaluate(“37593**(1/5)”)…\n
```output\n8.222831614237718\n```\n
Answer: 8.222831614237718\n\n
两个数学式的解题示例结束。
Question: 80.16 * 1.15\n’)
这里是玫瑰花问题的具体描述。
下面,就是模型返回结果。

在LLMChain内部,根据Python代码进行了计算,因此final_ansewer也已经算好了。

至此,LangChain的BaseTool返回的Observation如下:

observation
'Answer: 92.18399999999998'

命令行中也输出了当前数学工具调用后的Observation结果:92.18。

第三轮思考:模型完成任务
第三轮思考开始。此时,Executor的Plan应该进一步把当前的新结果传递给大模型,不出所料的话,大模型应该有足够的智慧判断出任务此时已经成功地完成了。


下面是目前最新的 prompt。
0: StringPromptValue(text='Answer the following questions as best you can. You have access to the following tools:\n\nSearch: A search engine. Useful for when you need to answer questions about current events. Input should be a search query.\nCalculator: Useful for when you need to answer questions about math.\n\nUse the following format:\n\nQuestion: the input question you must answer\nThought: you should always think about what to do\nAction: the action to take, should be one of [Search, Calculator]\nAction Input: the input to the action\nObservation: the result of the action\n... (this Thought/Action/Action Input/Observation can repeat N times)\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n\nBegin!\n\nQuestion: 目前市场上玫瑰花的平均价格是多少?如果我在此基础上加价15%卖出,应该如何定价?\nThought: I need to find the current market price of roses and then calculate the new price with a 15% markup.\nAction: Search\nAction Input: "Average price of roses"\nObservation: The average price for a dozen roses in the U.S. is $80.16. The state where a dozen roses cost the most is Hawaii at $108.33. That\'s 35% more expensive than the national average. A dozen roses are most affordable in Pennsylvania, costing $66.15 on average.\nThought: I need to calculate the new price with a 15% markup.\nAction: Calculator\nAction Input: 80.16 * 1.15\nObservation: Answer: 92.18399999999998\nThought:')
我们再来拆解一下这个最终的prompt。
0: StringPromptValue(text='Answer the following questions as best you can. You have access to the following tools:\n\n
这句提示是让模型尽量回答问题,并告诉模型拥有哪些工具。
Search: A search engine. Useful for when you need to answer questions about current events. Input should be a search query.\n
这是向模型介绍第一个工具:搜索。
Calculator: Useful for when you need to answer questions about math.\n\n
这是向模型介绍第二个工具:计算器。
Use the following format:\n\n (指导模型使用下面的格式)
Question: the input question you must answer\n (问题)
Thought: you should always think about what to do\n (思考)
Action: the action to take, should be one of [Search, Calculator]\n (行动)
Action Input: the input to the action\n (行动的输入)
Observation: the result of the action\n… (观察:行动的返回结果)
(this Thought/Action/Action Input/Observation can repeat N times)\n (上面这个过程可以重复多次)
Thought: I now know the final answer\n (思考:现在我知道最终答案了)
Final Answer: the final answer to the original input question\n\n (最终答案)
仍然是比较细节的说明,看括号文字。
Begin!\n\n
现在开始!
Question: 目前市场上玫瑰花的平均价格是多少?如果我在此基础上加价15%卖出,应该如何定价?\n
具体问题,也就是具体任务。
Thought: I need to find the current market price of roses and then calculate the new price with a 15% markup.\n (思考:我需要找到玫瑰花的价格,并加入15%的加价)
Action: Search\nAction (行动:搜索)
Input: “Average price of roses”\n (行动的输入:玫瑰花的平均价格)
Observation: The average price for a dozen roses in the U.S. is $80.16. The state where a dozen roses cost the most is Hawaii at $108.33. That’s 35% more expensive than the national average. A dozen roses are most affordable in Pennsylvania, costing $66.15 on average.\n (观察:这里时搜索工具返回的玫瑰花价格信息)
这句之前的提示,与我们在第二轮思考时看到的完全相同。
Thought: I need to calculate the new price with a 15% markup.\n (思考:我需要计算玫瑰花15%的加价)
Action: Calculator\n (行动:计算器工具)
Action Input: 80.16 * 1.15\n (行动输入:一个数学式)
Observation: Answer: 92.18399999999998\n (观察:计算得到的答案)
Thought:’ (思考)
可见,每一轮的提示都跟随着模型的思维链条,逐步递进,逐步完善。环环相扣,最终结果也就呼之欲出了。
继续Debug,发现模型在这一轮思考之后的输出中终于包含了 “ I now know the final answer.”,这说明模型意识到任务已经成功地完成了。


此时,AgentExcutor的plan方法返回一个 AgentFinish 实例,这表示代理经过对输出的检查,其内部逻辑判断出任务已经完成,思考和行动的循环要结束了。

至此,整个链条完成,AgentExecutor 的任务结束。

在命令行中,模型输出 Thought: I now know the final answer. (我已经知道最终的答案)。

最终答案:玫瑰的平均价格是 80.16 美元,加价15%后,是 92.18 美元。
总结时刻
这一课中,我们深入到AgentExecutor的代码内部,深挖其运行机制,了解了AgentExecutor是如何通过计划和工具调用,一步一步完成Thought、Action和Observation的。
如果我们审视一下AgentExecutor 的代码实现,会发现AgentExecutor这个类是作为链(Chain)而存在,同时也为代理执行各种工具,完成任务。它会接收代理的计划,并执行代理思考链路中每一步的行动。
AgentExecutor中最重要的方法是步骤处理方法,_take_next_step方法。它用于在思考-行动-观察的循环中采取单步行动。先调用代理的计划,查找代理选择的工具,然后使用选定的工具执行该计划(此时把输入传给工具),从而获得观察结果,然后继续思考,直到输出是 AgentFinish 类型,循环才会结束。
思考题
-
请你在 agent.py 文件中找到AgentExecutor类。
-
请你在AgentExecutor类中找到_take_next_step方法,对应本课的内容,分析AgentExecutor类是怎样实现Plan和工具调用的。
期待在留言区看到你的分享,如果你觉得内容对你有帮助,也欢迎分享给有需要的朋友!最后如果你学有余力,可以进一步学习下面的延伸阅读。
延伸阅读
13|代理(下):结构化工具对话、Self-Ask with Search以及Plan and execute代理
在上一讲中,我们深入LangChain程序内部机制,探索了AgentExecutor究竟是如何思考(Thought)、执行(Execute/Act)和观察(Observe)的,这些步骤之间的紧密联系就是代理在推理(Reasoning)和工具调用过程中的“生死因果”。
现在我们趁热打铁,再学习几种更为复杂的代理:Structured Tool Chat(结构化工具对话)代理、Self-Ask with Search(自主询问搜索)代理、Plan and execute(计划与执行) 代理。
什么是结构化工具
LangChain的第一个版本是在 2022 年 11 月推出的,当时的设计是基于 ReAct 论文构建的,主要围绕着代理和工具的使用,并将二者集成到提示模板的框架中。
早期的工具使用十分简单,AgentExecutor引导模型经过推理调用工具时,仅仅能够生成两部分内容:一是工具的名称,二是输入工具的内容。而且,在每一轮中,代理只被允许使用一个工具,并且输入内容只能是一个简单的字符串。这种简化的设计方式是为了让模型的任务变得更简单,因为进行复杂的操作可能会使得执行过程变得不太稳定。
不过,随着语言模型的发展,尤其是出现了如 gpt-3.5-turbo 和 GPT-4 这样的模型,推理能力逐渐增强,也为代理提供了更高的稳定性和可行性。这就使得 LangChain 开始考虑放宽工具使用的限制。
2023年初,LangChain 引入了“多操作”代理框架,允许代理计划执行多个操作。在此基础上,LangChain 推出了结构化工具对话代理,允许更复杂、多方面的交互。通过指定AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION 这个代理类型,代理能够调用包含一系列复杂工具的“ 结构化工具箱”,组合调用其中的多个工具,完成批次相关的任务集合。
举例来说,结构化工具的示例包括:
- 文件管理工具集:支持所有文件系统操作,如写入、搜索、移动、复制、列目录和查找。
- Web 浏览器工具集:官方的 PlayWright 浏览器工具包,允许代理访问网站、点击、提交表单和查询数据。
下面,我们就以 PlayWright 工具包为例,来实现一个结构化工具对话代理。
先来看一看什么是 PlayWright 工具包。
什么是 Playwright
Playwright是一个开源的自动化框架,它可以让你模拟真实用户操作网页,帮助开发者和测试者自动化网页交互和测试。用简单的话说,它就像一个“机器人”,可以按照你给的指令去浏览网页、点击按钮、填写表单、读取页面内容等等,就像一个真实的用户在使用浏览器一样。
Playwright支持多种浏览器,比如Chrome、Firefox、Safari等,这意味着你可以用它来测试你的网站或测试应用在不同的浏览器上的表现是否一致。
下面我们先用 pip install playwright 安装Playwright工具。
不过,如果只用pip安装Playwright工具安装包,就使用它,还不行,会得到下面的信息。

因此我们还需要通过 playwright install 命令来安装三种常用的浏览器工具。

现在,一切就绪,我们可以通过Playwright浏览器工具来访问一个测试网页。
from playwright.sync_api import sync_playwright
def run():
## 使用Playwright上下文管理器
with sync_playwright() as p:
## 使用Chromium,但你也可以选择firefox或webkit
browser = p.chromium.launch()
## 创建一个新的页面
page = browser.new_page()
## 导航到指定的URL
page.goto('https://langchain.com/')
## 获取并打印页面标题
title = page.title()
print(f"Page title is: {title}")
## 关闭浏览器
browser.close()
if __name__ == "__main__":
run()
这个简单的Playwright脚本,它打开了一个新的浏览器实例。过程是:导航到指定的URL;获取页面标题并打印页面的标题;最后关闭浏览器。
输出如下:
Page title is: LangChain
这个脚本展示了Playwright的工作方式,一切都是在命令行里面直接完成。它不需要我们真的去打开Chome网页,然后手工去点击菜单栏、拉动进度条等。
下面这个表,我列出了使用命令行进行自动化网页测试的优势。

现在你了解了Playwright这个工具包的基本思路,下面我们就开始使用它来作为工具集,来实现结构化工具对话代理。
使用结构化工具对话代理
在这里,我们要使用的Agent类型是STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION。要使用的工具则是PlayWrightBrowserToolkit,这是LangChain中基于PlayWrightBrowser包封装的工具箱,它继承自 BaseToolkit类。
PlayWrightBrowserToolkit 为 PlayWright 浏览器提供了一系列交互的工具,可以在同步或异步模式下操作。
其中具体的工具就包括:

下面,我们就来看看结构化工具对话代理是怎样通过组合调用PlayWrightBrowserToolkit中的各种工具,自动完成我们交给它的任务。
from langchain.agents.agent_toolkits import PlayWrightBrowserToolkit
from langchain.tools.playwright.utils import create_async_playwright_browser
async_browser = create_async_playwright_browser()
toolkit = PlayWrightBrowserToolkit.from_browser(async_browser=async_browser)
tools = toolkit.get_tools()
print(tools)
from langchain.agents import initialize_agent, AgentType
from langchain.chat_models import ChatAnthropic, ChatOpenAI
## LLM不稳定,对于这个任务,可能要多跑几次才能得到正确结果
llm = ChatOpenAI(temperature=0.5)
agent_chain = initialize_agent(
tools,
llm,
agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
)
async def main():
response = await agent_chain.arun("What are the headers on python.langchain.com?")
print(response)
import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
在这个示例中,我们询问大模型,网页python.langchain.com中有哪些标题目录?
很明显,大模型不可能包含这个网页的内部信息,因为ChatGPT完成训练的那一年(2021年9月),LangChain还不存在。因此,大模型不可避免地需要通过PlayWrightBrowser工具来解决问题。
第一轮思考
代理进入AgentExecutor Chain之后的第一轮思考如下:

这里,我对上述思考做一个具体说明。
I can use the “navigate_browser” tool to visit the website and then use the “get_elements” tool to retrieve the headers. Let me do that.
这是第一轮思考,大模型知道自己没有相关信息,决定使用PlayWrightBrowserToolkit工具箱中的 navigate_browser 工具。
Action:```{“action”: “navigate_browser”, “action_input”: {“url”: “ https://python.langchain.com”}}```
行动:通过Playwright浏览器访问这个网站。
Observation: Navigating to https://python.langchain.com returned status code 200
观察:成功得到浏览器访问的返回结果。
在第一轮思考过程中,模型决定使用PlayWrightBrowserToolkit中的navigate_browser工具。
第二轮思考
下面是大模型的第二轮思考。

还是对上述思考做一个具体说明。
Thought:Now that I have successfully navigated to the website, I can use the “get_elements” tool to retrieve the headers. I will specify the CSS selector for the headers and retrieve their text.
第二轮思考:模型决定使用PlayWrightBrowserToolkit工具箱中的另一个工具 get_elements,并且指定CSS selector只拿标题的文字。
Action: ```{“action”: “get_elements”, “action_input”: {“selector”: “h1, h2, h3, h4, h5, h6”, “attributes”: [“innerText”]}}```
行动:用Playwright的 get_elements 工具去拿网页中各级标题的文字。
Observation: [{“innerText”: “Introduction”}, {“innerText”: “Get started”}, {“innerText”: “Modules”}, {“innerText”: “Model I/O”}, {“innerText”: “Data connection”}, {“innerText”: “Chains”}, {“innerText”: “Agents”}, {“innerText”: “Memory”}, {“innerText”: “Callbacks”}, {“innerText”: “Examples, ecosystem, and resources”}, {“innerText”: “Use cases”}, {“innerText”: “Guides”}, {“innerText”: “Ecosystem”}, {“innerText”: “Additional resources”}, {“innerText”: “Support”}, {“innerText”: “API reference”}]
观察:成功地拿到了标题文本。
在第二轮思考过程中,模型决定使用PlayWrightBrowserToolkit中的get_elements工具。
第三轮思考
下面是大模型的第三轮思考。

对上述思考做一个具体说明。
Thought:The headers on python.langchain.com are:
Introduction
… …
API reference
第三轮思考:模型已经找到了网页中的所有标题。
Action:
{ "action": "Final Answer", "action_input": "The headers on python.langchain.com are: 1. Introduction 2. Get started 3. Modules 4. Model I/O 5. Data connection 6. Chains 7. Agents 8. Memory 9. Callbacks 10. Examples, ecosystem, and resources 11. Use cases 12. Guides 13. Ecosystem 14. Additional resources 15. Support 16. API reference" }
行动:给出最终答案。
AgentExecutor Chain结束之后,成功输出python.langchain.com这个页面中各级标题的具体内容。

在这个过程中,结构化工具代理组合调用了Playwright工具包中的两种不同工具,自主完成了任务。
使用 Self-Ask with Search 代理
讲完了Structured Tool Chat代理,我们再来看看Self-Ask with Search代理。
Self-Ask with Search 也是LangChain中的一个有用的代理类型(SELF_ASK_WITH_SEARCH)。它利用一种叫做 “Follow-up Question(追问)”加“Intermediate Answer(中间答案)”的技巧,来辅助大模型寻找事实性问题的过渡性答案,从而引出最终答案。
这是什么意思?让我通过示例来给你演示一下,你就明白了。在这个示例中,我们使用SerpAPIWrapper作为工具,用OpenAI作为语言模型,创建Self-Ask with Search代理。
from langchain import OpenAI, SerpAPIWrapper
from langchain.agents import initialize_agent, Tool
from langchain.agents import AgentType
llm = OpenAI(temperature=0)
search = SerpAPIWrapper()
tools = [
Tool(
name="Intermediate Answer",
func=search.run,
description="useful for when you need to ask with search",
)
]
self_ask_with_search = initialize_agent(
tools, llm, agent=AgentType.SELF_ASK_WITH_SEARCH, verbose=True
)
self_ask_with_search.run(
"使用玫瑰作为国花的国家的首都是哪里?"
)
该代理对于这个问题的输出如下:

其实,细心的你可能会发现,“ 使用玫瑰作为国花的国家的首都是哪里?”这个问题不是一个简单的问题,它其实是一个多跳问题—— 在问题和最终答案之间,存在中间过程。
多跳问题(Multi-hop question)是指为了得到最终答案,需要进行多步推理或多次查询。这种问题不能直接通过单一的查询或信息源得到答案,而是需要跨越多个信息点,或者从多个数据来源进行组合和整合。
也就是说,问题的答案依赖于另一个子问题的答案,这个子问题的答案可能又依赖于另一个问题的答案。这就像是一连串的问题跳跃,对于人类来说,解答这类问题可能需要从不同的信息源中寻找一系列中间答案,然后结合这些中间答案得出最终结论。
“使用玫瑰作为国花的国家的首都是哪里?”这个问题并不直接询问哪个国家使用玫瑰作为国花,也不是直接询问英国的首都是什么。而是先要推知使用玫瑰作为国花的国家(英国)之后,进一步询问这个国家的首都。这就需要多跳查询。
为什么 Self-Ask with Search 代理适合解决多跳问题呢?有下面几个原因。
- 工具集合:代理包含解决问题所必须的搜索工具,可以用来查询和验证多个信息点。这里我们在程序中为代理武装了SerpAPIWrapper工具。
- 逐步逼近:代理可以根据第一个问题的答案,提出进一步的问题,直到得到最终答案。这种逐步逼近的方式可以确保答案的准确性。
- 自我提问与搜索:代理可以自己提问并搜索答案。例如,首先确定哪个国家使用玫瑰作为国花,然后确定该国家的首都是什么。
- 决策链:代理通过一个决策链来执行任务,使其可以跟踪和处理复杂的多跳问题,这对于解决需要多步推理的问题尤为重要。
在上面的例子中,通过大模型的两次follow-up追问,搜索工具给出了两个中间答案,最后给出了问题的最终答案——伦敦。
使用 Plan and execute 代理
在这节课的最后,我再给你介绍一种比较新的代理类型:Plan and execute 代理。
计划和执行代理通过首先计划要做什么,然后执行子任务来实现目标。这个想法是受到 Plan-and-Solve 论文的启发。论文中提出了计划与解决(Plan-and-Solve)提示。它由两部分组成:首先,制定一个计划,并将整个任务划分为更小的子任务;然后按照该计划执行子任务。
这种代理的独特之处在于,它的计划和执行不再是由同一个代理所完成,而是:
- 计划由一个大语言模型代理(负责推理)完成。
- 执行由另一个大语言模型代理(负责调用工具)完成。
因为这个代理比较新,它隶属于LangChain的实验包langchain_experimental,所以你需要先安装langchain_experimental这个包。
pip install -U langchain langchain_experimental
下面我们来使用一下这个代理。在这里,我们创建了Plan and execute代理,这个代理和之前看到的代理不同,它有一个Planner,有一个Executor,它们可以是不同的模型。
当然,在这个示例中,我们都使用了ChatOpenAI模型。
from langchain.chat_models import ChatOpenAI
from langchain_experimental.plan_and_execute import PlanAndExecute, load_agent_executor, load_chat_planner
from langchain.llms import OpenAI
from langchain import SerpAPIWrapper
from langchain.agents.tools import Tool
from langchain import LLMMathChain
search = SerpAPIWrapper()
llm = OpenAI(temperature=0)
llm_math_chain = LLMMathChain.from_llm(llm=llm, verbose=True)
tools = [
Tool(
name = "Search",
func=search.run,
description="useful for when you need to answer questions about current events"
),
Tool(
name="Calculator",
func=llm_math_chain.run,
description="useful for when you need to answer questions about math"
),
]
model = ChatOpenAI(temperature=0)
planner = load_chat_planner(model)
executor = load_agent_executor(model, tools, verbose=True)
agent = PlanAndExecute(planner=planner, executor=executor, verbose=True)
agent.run("在纽约,100美元能买几束玫瑰?")
输出如下:


在上面输出中,PlanAndExecute 链的调用流程以及代理的思考过程,我就留给你来做分析了,相信你可以把握住Plan and execute代理解决问题的基本脉络。
总结时刻
这节课是Agent的最后一课,也是LangChain所有基础知识的最后一课。我给你总结了两张的表。
第一个表,是LangChain中常见的代理类型和它们的介绍。在这些代理中,有很多我们已经一起使用过了,有些则需要你自己去阅读相关文档,自己去探索它的使用方法。

第二个表,是我对LangChain各个组件的一个简明总结。


上面这个图片,相信此时你已经不再陌生了,也掌握了它们的精髓所在。
最后还有一个问题值得讲一讲,就是图中的 Indexes,到底是什么,其实这个Indexes是LangChang早期版本的一个组件,现在已经被整合到Retrieval(数据检索)这个单元中了。而Retrieval(包括Indexes),讲的其实就是如何把离散的文档及其他信息做嵌入,存储到向量数据库中,然后再提取的过程。这个过程我们在 第3课 已经讲过,在后面的课程中还会再深入介绍。

此外,在LangChain文档中,新的6大组件中其实还有一个模块——Callbacks,目前我们尚未涉及,在后续的课程中也会介绍。
好了,LangChain的基础知识就讲到这里,从下节课起,我们将整合以前学过的各个组件的内容,为你讲解更多偏重具体应用的内容。
思考题
- 在结构化工具对话代理的示例中,请你打印出PlayWrightBrowserToolkit中的所有具体工具名称的列表。
提示:
tools = toolkit.get_tools()
print(tools)
- 在Plan and execute代理的示例中,请你分析PlanAndExecute、AgentExecutor和LLMMathChain链的调用流程以及代理的思考过程。
期待在留言区看到你的分享,如果你觉得内容对你有帮助,也欢迎分享给有需要的朋友!最后如果你学有余力,可以进一步学习下面的延伸阅读。
延伸阅读
- 代码 Github Playwright 工具包
- 论文 "计划与解决"提示:通过大型语言模型改进Zero-Shot链式思考推理 Wang, L., Xu, W., Lan, Y., Hu, Z., Lan, Y., Lee, R. K.-W., & Lim, E.-P. (2023). Plan-and-Solve Prompting: Improving Zero-Shot Chain-of-Thought Reasoning by Large Language Models. arXiv preprint arXiv:2305.04091.
放假通知
相信细心的同学已经发现了,我们这个专栏的更新节奏还是很快的,前面的内容基本接近工作日日更。从内容的重要程度来说,基础篇其实相当重要,值此中秋&国庆双节长假来临之际,希望大家能好好休息,也能空出一段时间好好复习前面所学,所以我们的专栏计划停更一周,10月9日恢复正常更新,也期待你能把前面的思考题都做一做,我会在留言区等你的分享,与你交流探讨。最后祝大家小长假愉快,中秋阖家团圆!
更多推荐
所有评论(0)