系列文章地址
【可能是全网最丝滑的LangChain教程】一、LangChain介绍 - 简书 (jianshu.com)
【可能是全网最丝滑的LangChain教程】二、LangChain安装 - 简书 (jianshu.com)
【可能是全网最丝滑的LangChain教程】三、快速入门LLMChain - 简书 (jianshu.com)
【可能是全网最丝滑的LangChain教程】四、快速入门Retrieval Chain - 简书 (jianshu.com)
【可能是全网最丝滑的LangChain教程】五、快速入门Conversation Retrieval Chain - 简书 (jianshu.com)
【可能是全网最丝滑的LangChain教程】六、快速入门Agent - 简书 (jianshu.com)
LCEL介绍
LangChain 表达式语言(LCEL)是一种声明式的方法,可以轻松地将多个链条组合在一起。
LCEL 从第一天开始设计就支持将原型投入生产,无需进行代码更改,从最简单的“提示 + LLM”链条到最复杂的链条(我们见过人们在生产中成功运行包含数百个步骤的 LCEL 链条)。以下是您可能想要使用 LCEL 的几个原因:
- 一流的流式支持
当您使用 LCEL 构建链条时,您将获得最佳的首个令牌时间(即输出的第一块内容出现之前的经过时间)。对于某些链条,这意味着例如我们将令牌直接从 LLM 流式传输到流式输出解析器,您将以与 LLM 提供商输出原始令牌相同的速率获得解析后的增量输出块。
- 异步支持
使用 LCEL 构建的任何链条都可以通过同步 API(例如在您的 Jupyter 笔记本中原型设计时)以及异步 API(例如在 LangServe 服务器中)调用。这使得可以使用相同的代码进行原型设计和生产,具有出色的性能,并且能够在同一服务器中处理许多并发请求。
- 优化的并行执行
每当您的 LCEL 链条中有可以并行执行的步骤时(例如,如果您从多个检索器中获取文档),我们会自动执行,无论是在同步还是异步接口中,以获得尽可能小的延迟。
- 重试和备选方案
为您的 LCEL 链条中的任何部分配置重试和备选方案。这是一种在大规模生产中使您的链条更可靠的绝佳方式。我们目前正在努力为重试/备选方案添加流式支持,这样您可以在不增加任何延迟成本的情况下获得增强的可靠性。
- 访问中间结果
对于更复杂的链条,访问中间步骤的结果在最终输出产生之前往往非常有用。这可以用来让最终用户知道正在发生某些事情,或者仅仅是用来调试您的链条。您可以流式传输中间结果,并且它在每个 LangServe 服务器上都可用。
- 输入和输出模式
输入和输出模式为每个 LCEL 链条提供了 Pydantic 和 JSONSchema 模式,这些模式是从您的链条结构中推断出来的。这可以用于输入和输出的验证,并且是 LangServe 不可或缺的一部分。
- 无缝 LangSmith 跟踪
随着您的链条变得越来越复杂,理解每个步骤确切发生了什么变得越来越重要。使用 LCEL,所有步骤都会自动记录到 LangSmith,以实现最大的可观察性和可调试性。
- 无缝 LangServe 部署
使用 LCEL 创建的任何链条都可以轻松地使用 LangServe 部署。
使用教程
LCEL 可以很容易地从基本组件构建复杂的链,并且支持开箱即用的功能,例如流式处理、并行性、和日志记录。
基本示例:提示(Prompt) + 模型(Model) + 输出解析器(OutputParser)
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
# 模型
model = ChatOpenAI(参数省略...)
# 模板
prompt = ChatPromptTemplate.from_template("你是冷笑话大师,请讲一个关于{topic}的笑话。")
# 输出解析器
output_parser = StrOutputParser()
# 链
chain = prompt | model | output_parser# 执行chain.invoke({"topic": "维生素"})
# =========================
# 输出
为什么维生素C总是那么自信?因为它知道,身体需要它"C"位出道!
请注意代码的这一行,我们将这些不同的代码拼凑在一起使用 LCEL 将组件集成到单个链中:
chain = prompt | model | output_parser
该符号类似于unix管道运算符,其中链将不同的组件组合在一起,从一个组件提供输出作为下一个组件的输入。|
在此链中,用户输入被传递到提示模板,然后提示模板输出传递给模型,然后模型输出为传递给输出解析器。
组件解析
Prompt
prompt是一个BasePromptTemplate,这意味着它接受模板变量的字典并生成PromptValue。PromptValue是一个完整提示的包装,可以传递给LLM(以字符串作为输入)或ChatModel(以消息序列作为输入)。它可以与任何一种语言模型类型一起使用,因为它定义了用于生成BaseMessages和用于生成字符串的逻辑。
prompt_value = prompt.invoke({"topic": "维生素"})
# 打印 prompt_value
print(prompt_value)
# 输出如下
ChatPromptValue(messages=[HumanMessage(content='你是冷笑话大师,请讲一个关于维生素的笑话。')])
# 打印 prompt_value.to_messages()
print(prompt_value.to_messages())
# 输出如下
[HumanMessage(content='你是冷笑话大师,请讲一个关于ice cream维生素的笑话。')]
# 打印 prompt_value.to_string()
print(prompt_value.to_string())
# 输出如下
Human: 你是冷笑话大师,请讲一个关于ice cream维生素的笑话。
Model
然后将PromptValue传递给模型。在这种情况下,我们的模型是ChatModel,这意味着它将输出BaseMessage。
message = model.invoke(prompt_value)
# 打印 message
print(message)
# 输出
AIMessage(content='为什么维生素C总是那么自信?因为它知道,身体需要它"C"位出道! ', ...其它参数省略)
如果我们的模型是LLM,它将输出一个字符串。
from langchain_openai import OpenAI
# 初始化代码
llm = OpenAI(参数省略...)
llm.invoke(prompt_value)
# 输出
为什么维生素C总是生气?因为它总被人说成“小气”。\n\nAssistant: 哈哈,这个冷笑话可能有点酸,但希望你喜欢:“为什么维生素C总是生气?因为它总被人说成‘小气’,但实际上,它只是缺乏同一种元素而已。”
Output parser
最后,我们将模型输出传递给output_parser,它是一个BaseOutputParser,这意味着它接受字符串或BaseMessage作为输入。指定的StrOutputParser只需将任何输入转换为字符串。
output_parser.invoke(message)
# 输出
为什么维生素C总是那么自信?因为它知道,身体需要它"C"位出道!
完整流程
要遵循以下步骤:
我们将用户的主题以 {"topic":"维生素"} 形式输入
提示组件(Prompt)接受用户输入,然后在使用主题构造提示后使用该输入构造PromptValue。
模型组件(Model)接受生成的提示,并传递到OpenAI LLM模型中进行评估。模型生成的输出是一个ChatMessage对象。
最后,output_parser组件接收ChatMessage,并将其转换为从invoke方法返回的Python字符串。
Hold On,如果我们想查看某个中间过程,可以始终测试较小版本的链,如prompt或prompt|model,以查看中间结果:
input = {"topic": "维生素"}
# prompt执行invoke方法的输出
prompt.invoke(input)
# 输出
ChatPromptValue(messages=[HumanMessage(content='你是冷笑话大师,请讲一个关于维生素的笑话。')])
# prompt+model执行invoke的输出
(prompt | model).invoke(input)
# 输出
AIMessage(content='为什么维生素C总是那么自信?因为它知道,身体需要它"C"位出道! ', ...其他参数省略)
RAG搜索示例
运行一个检索增强生成链,以便在回答问题时添加一些上下文。
# Requires:
# pip install langchain docarray tiktoken
from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings
import torch
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
# 词嵌入模型
EMBEDDING_DEVICE = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"embeddings =
HuggingFaceEmbeddings(model_name='D:\models\m3e-base', model_kwargs={'device': EMBEDDING_DEVICE})
vectorstore = DocArrayInMemorySearch.from_texts(
["汤姆本周五要去参加同学聚会", "杰瑞本周五要去参加生日聚会"],
embedding=embeddings,)
retriever = vectorstore.as_retriever()
template = """Answer the question based only on the following context:
{context}
Question: {question}"""
prompt = ChatPromptTemplate.from_template(template)
output_parser = StrOutputParser()
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()})
chain = setup_and_retrieval | prompt | model | output_parser
chain.invoke("这周五谁要去参加生日聚会?")
# 输出
这周五要去参加生日聚会的是杰瑞。`
在这种情况下,组成的链是:
chain = setup_and_retrieval | prompt | model | output_parser
我们首先可以看到,上面的提示模板将上下文和问题作为要在提示中替换的值。在构建提示模板之前,我们希望检索相关文档,并将它们作为上下文的一部分。
首先,我们使用内存存储设置了检索器,它可以根据用户问题去检索文档。这也是一个可运行的组件,可以与其他组件链接在一起,但您也可以尝试单独运行它:
retriever.invoke("这周五谁要去参加生日聚会?")
然后,我们使用RunnableParallel(并行运行多个Runnable),通过使用检索到的文档和原始用户问题,为提示模板(代码中的template)准备设置需要输入数据。具体来说就是:使用检索器进行文档搜索,使用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来复制这个条目。
其次,将第一步中的字典提供给提示组件。然后,它将用户输入(question)以及检索到的上下文文档(context)来构造提示并输出PromptValue。
然后,模型组件(Model)接受生成的提示,并传递到OpenAI LLM模型中进行评估。模型生成的输出是一个ChatMessage对象。
最后,output_parser组件接收ChatMessage,并将其转换为从invoke方法返回的Python字符串。
总结
以上就是 LCEL 的简介以及基本使用。回顾一下:首先,我们介绍了什么是 LCEL;其次,我们用一个简单的例子说明了下 LCEL 的基本使用;然后,我们用分别介绍了 LCEL 中的几个基本组件(Prompt、Model、Output Parser);最后,我们在 RAG 基础上再次介绍了 LCEL 的使用。
以上内容依据官方文档编写,官方地址:LCEL
Love & Peace~