一架梯子,一头程序猿,仰望星空!
LangChain教程(Python版本) > 内容正文

LangChain表达式语言入门


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,它接受一个模板变量字典并生成一个 PromptValuePromptValue 是一个包装完成提示的对象,可以传递给 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. 整个流程

执行过程如下:

  1. 调用chain.invoke({"topic": "冰淇淋"}),相当于启动我们定义的工作流,传入参数{"topic": "冰淇淋"},要求生成一个主题关于”冰淇淋”的笑话
  2. 将调用参数{"topic": "冰淇淋"}传给chain的第一个组件prompt,prompt通过参数格式化提示模板(prompt template),得到告诉我一个关于冰淇淋的小笑话 提示词(prompt)
  3. 告诉我一个关于冰淇淋的小笑话提示词(prompt)传给model(gpt4模型)
  4. model返回的结果,传给output_parser输出解析器, 输出解析器格式化模型结果,并返回最终的内容。

如果你对任何组件的输出感兴趣,可以随时测试链条的较小版本,如 promptprompt | 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

简单解释一下,上面的提示模板接受 contextquestion 作为要替换在提示(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

流程为:

  1. 首先,创建一个 RunnableParallel 对象,其中包含两个条目。第一个条目 context 将包括检索器提取的文档结果。第二个条目 question 将包含用户原始问题。为传递问题,我们使用 RunnablePassthrough 复制这个条目。
  2. 将上一步的字典传递给 prompt 组件。它接受用户输入(即 question)以及检索到的文档(即 context),构建一个提示并输出一个 PromptValue
  3. model 组件接受生成的提示,并传递给 OpenAI 的 LLM 模型进行评估。模型生成的输出是一个 ChatMessage 对象。
  4. 最后,output_parser 组件接受一个 ChatMessage,将其转换为 Python 字符串,并从 invoke 方法返回。


关联主题