【可能是全网最丝滑的LangChain教程】七、LCEL表达式语言

系列文章地址

【可能是全网最丝滑的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字符串。

image.png

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字符串。

image.png

总结

以上就是 LCEL 的简介以及基本使用。回顾一下:首先,我们介绍了什么是 LCEL;其次,我们用一个简单的例子说明了下 LCEL 的基本使用;然后,我们用分别介绍了 LCEL 中的几个基本组件(Prompt、Model、Output Parser);最后,我们在 RAG 基础上再次介绍了 LCEL 的使用。

以上内容依据官方文档编写,官方地址:LCEL

Love & Peace~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容