LCEL 入门
LCEL(LangChain Expression Language) 是一种强大的工作流编排工具,可以从基本组件构建复杂任务链条(chain),并支持诸如流式处理、并行处理和日志记录等开箱即用的功能。
基本示例:提示(prompt) + 模型 + 输出解析器
在这个示例中,我们将展示如何通过LCEL(LangChain Expression Language) ,将提示模板(prompt)、模型和输出解析器三个组件链接在一起形成一个完整的工作流,用于实现”讲笑话”的任务。通过代码演示了如何创建链条(chain)、使用管道符号 |
连接不同组件,并介绍了每个组件的作用以及输出结果。
首先,让我们看一下如何将提示模板(prompt template)和模型连接在一起,完成生成一个关于特定主题的笑话:
安装依赖库
%pip install --upgrade --quiet langchain-core langchain-community langchain-openai
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# 定义一个提示模板,包含topic模板参数,用于设置"笑话"的主题
prompt = ChatPromptTemplate.from_template("告诉我一个关于{topic}的小笑话")
# 定义对话模型实例,选择gpt-4模型
model = ChatOpenAI(model="gpt-4")
# 定义字符串输出解析器,这个解析器只是简单的将模型返回的内容转成字符串
output_parser = StrOutputParser()
# 注意这里,这里通过LCEL表达式,定义一个工作流,生成一个chain(任务链条)
chain = prompt | model | output_parser
# 通过prompt模板参数调用工作流,又或者叫chain
chain.invoke({"topic": "冰淇淋"})
返回结果
“为什么派对从不邀请冰淇淋?”因为东西一热,就会滴水!”
在这段代码中,我们使用 LCEL 将不同组件连接成一个链条(chain):
chain = prompt | model | output_parser
这里的 |
符号类似于 unix 管道操作符),它将不同组件连接在一起,将一个组件的输出作为下一个组件的输入。
在这个链条中,用户输入被传递到提示模板,然后提示模板的输出被传递到模型,最后模型的输出被传递到输出解析器。让我们分别看一下每个组件,以更好地理解发生了什么。
1. 提示(prompt)
prompt
是一个 BasePromptTemplate
,它接受一个模板变量字典并生成一个 PromptValue
。PromptValue
是一个包装完成提示的对象,可以传递给 LLM
(以字符串作为输入)或 ChatModel
(以消息序列作为输入)。它可以与任何一种语言模型类型配合使用,因为它定义了生成 BaseMessage
和生成字符串的逻辑。
# 下面我们手动调用prompt传入模板参数,格式化提示词模板(prompt template)
prompt_value = prompt.invoke({"topic": "冰淇淋"})
prompt_value
输出结果
ChatPromptValue(messages=[HumanMessage(content='告诉我一个关于冰淇淋的小笑话')])
下面将prompt格式结果转成对话模型(chat models)使用的消息格式
prompt_value.to_messages()
输出结果
[HumanMessage(content='告诉我一个关于冰淇淋的小笑话')]
也可以直接转成字符串
prompt_value.to_string()
输出结果
'Human: 告诉我一个关于冰淇淋的小笑话。'
2. 模型(model)
然后将 PromptValue
传递给 model
。在本例中,我们的 model
是一个 ChatModel
,这意味着它将输出一个 BaseMessage
。
下面尝试直接调用model
message = model.invoke(prompt_value)
message
返回
AIMessage(content="为什么冰淇淋从不被邀请参加派对?\n\n因为它们总是在事情变热时滴!")
如果我们的 model
定义的是一个 LLM
类型,它将输出一个字符串。
from langchain_openai.llms import OpenAI
# 定义一个OpenAI模型实例,使用gpt-3.5-turbo-instruct模型
llm = OpenAI(model="gpt-3.5-turbo-instruct")
# 通过前面定义的提示词(prompt)调用模型
llm.invoke(prompt_value)
模型返回结果
'\n\n机器人: 为什么冰淇淋车坏了?因为它发生了熔毁!'
3. 输出解析器
最后,将我们的 model
输出传递给 output_parser
,它是一个 BaseOutputParser
,意味着它接受一个字符串或 BaseMessage
作为输入。StrOutputParser
具体地将任何输入简单转换为一个字符串。
output_parser.invoke(message)
为什么冰淇淋从不被邀请参加派对?\n\n因为它们总是在事情变热时滴!
4. 整个流程
执行过程如下:
- 调用
chain.invoke({"topic": "冰淇淋"})
,相当于启动我们定义的工作流,传入参数{"topic": "冰淇淋"}
,要求生成一个主题关于”冰淇淋”的笑话 - 将调用参数
{"topic": "冰淇淋"}
传给chain的第一个组件prompt,prompt通过参数格式化提示模板(prompt template),得到告诉我一个关于冰淇淋的小笑话
提示词(prompt) - 将
告诉我一个关于冰淇淋的小笑话
提示词(prompt)传给model
(gpt4模型) - 将
model
返回的结果,传给output_parser
输出解析器, 输出解析器格式化模型结果,并返回最终的内容。
如果你对任何组件的输出感兴趣,可以随时测试链条的较小版本,如 prompt
或 prompt | model
,以查看中间结果:
input = {"topic": "冰淇淋"}
# 直接调试prompt组件
prompt.invoke(input)
# > ChatPromptValue(messages=[HumanMessage(content='tell me a short joke about ice cream')])
# 定义一个由prompt和model组成的任务链条,然后通过invoke进行调用测试
(prompt | model).invoke(input)
# > AIMessage(content="为什么冰淇淋要去心理治疗?\n因为它有太多的配料,找不到锥-控自己!")
RAG 搜索示例
接下来,讲解一个稍微复杂点的LCEL例子,我们将运行一个检索增强生成链条(chain)的示例,以在回答问题时添加一些背景信息。
# 需要安装:
# pip install langchain docarray tiktoken
from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai.chat_models import ChatOpenAI
from langchain_openai.embeddings import OpenAIEmbeddings
# 定义一个向量存储用于测试相似度搜索,后续的章节会单独讲解向量存储,这里简单了解即可
# 下面导入两条文本信息作为测试数据
vectorstore = DocArrayInMemorySearch.from_texts(
["harrison worked at kensho", "bears like to eat honey"],
embedding=OpenAIEmbeddings(),
)
# 通过向量存储获取检索对象,用于支持根据问题查询相似的文本数据,作为背景信息
retriever = vectorstore.as_retriever()
# 定义prompt模板
template = """仅基于以下背景回答问题:
{context}
问题:{question}
"""
prompt = ChatPromptTemplate.from_template(template)
# 定义对话模型
model = ChatOpenAI()
# 定义输出解析器
output_parser = StrOutputParser()
# 自定义一个任务步骤,用于通过`retriever`查询向量数据库中的相似数据,然后赋值给context字段
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
# 通过lcel表达式,定义工作流,生成一个链条(chain)
chain = setup_and_retrieval | prompt | model | output_parser
# 调用工作流
chain.invoke("harrison 在哪工作?")
在这种情况下,组成的链条是:
chain = setup_and_retrieval | prompt | model | output_parser
简单解释一下,上面的提示模板接受 context
和 question
作为要替换在提示(prompt)中的值。在构建提示模板之前,我们希望检索相关文档以用作上下文的一部分。
作为测试,我们使用DocArrayInMemorySearch
模拟一个基于内存的向量数据库,定义了一个检索器,它可以根据查询检索相似文档。这也是一个可链式连接的可运行组件,但你也可以尝试单独运行它:
retriever.invoke("harrison 在哪工作?")
然后,我们使用 RunnableParallel
准备提示(prompt)所需的输入,使用检索器进行文档搜索,并使用 RunnablePassthrough
传递用户的问题:
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
综上所述,完整的链条是:
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser
流程为:
- 首先,创建一个
RunnableParallel
对象,其中包含两个条目。第一个条目context
将包括检索器提取的文档结果。第二个条目question
将包含用户原始问题。为传递问题,我们使用RunnablePassthrough
复制这个条目。 - 将上一步的字典传递给
prompt
组件。它接受用户输入(即question
)以及检索到的文档(即context
),构建一个提示并输出一个PromptValue
。 model
组件接受生成的提示,并传递给 OpenAI 的 LLM 模型进行评估。模型生成的输出是一个ChatMessage
对象。- 最后,
output_parser
组件接受一个ChatMessage
,将其转换为 Python 字符串,并从invoke
方法返回。