在前面几篇文章中,我们已经掌握了LangChain的核心组件:提示词模板、大语言模型、输出解析器。细心的读者可能发现,在使用这些组件时,经常会看到类似 prompt | llm | parser 这样的链式操作。这就是今天重点介绍的LCEL(LangChain Expression Language)表达式。

在平时开发中,经常需要将多个组件组合起来形成完整的处理流程,将上一个组件的输出作为下一个组件的输入,在不使用LCEL表达式之前,就会写出这种代码:使用 invoke() 进行层层嵌套,这就好比早期 JS 中的回调地狱,结构混乱、难以维护,并且出现错误很难判断是哪一步出了问题。

# 1.构建提示词
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个资深文学家"),
("human", "请简短赏析{name}这首诗,并给出评价")
]) # 2.创建模型
llm = ChatOpenAI()
# 3.创建字符串输出解析器
parser = StrOutputParser() # 4.调用模型返回结果
result = parser.invoke(
llm.invoke(
prompt.invoke({"name": "江雪"})
)
)

而LCEL让这个过程变得简洁直观,通过管道符号进行连接,可以很轻松地构建出功能强大的AI应用。

文中所有示例代码:https://github.com/wzycoding/langchain-study

一、什么是LCEL表达式

LCEL(LangChain Expression Language)是LangChain框架的表达式语言,它提供了一种声明式的方式来构建复杂的数据处理链。通过LCEL,我们可以使用管道符号 | 将不同的组件连接起来,形成一个完整的数据处理链。

LCEL有以下优点:

1、代码更加简洁:用管道符号连接组件,代码更加简洁易读

2、可任意组合:任意Runnable组件都可以自由组合,构建复杂的处理逻辑

3、统一接口规范:所有Runnable组件都遵循统一的接口规范

4、方便监控与调试:LangChain内置了日志和监控功能,方便调试和优化

下面是使用LCEL表达式的案例:

# 1.构建提示词
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个资深文学家"),
("human", "请简短赏析{name}这首诗,并给出评价")
]) # 2.创建模型
llm = ChatOpenAI()
# 3.创建字符串输出解析器
parser = StrOutputParser() # 4.构建链
chain = prompt | llm | parser # 5.执行链
print(f"输出结果:{chain.invoke({'name': '题西林壁'})}")

显而易见,LCEL写法更加简洁,而且表达了清晰的数据流向:输入经过提示词模板处理,然后将PromptValue传递给大语言模型,最后将大语言模型输出的Message传递给输出解析器,经过输出解析器解析得到最终结果。

二、什么是Runnable组件

在深入LCEL之前,首先需要理解Runnable接口。Runnable是LangChain中所有可执行组件的基础接口,它定义了组件应该具备的标准方法。前面介绍的LangChain组件如提示词模板、模型、输出解析器等,都实现了Runnable接口,这就是为什么这些组件可以使用管道符进行连接的原因。

在Runnable接口中定义了以下核心方法:

invoke(input):同步执行,处理单个输入,最常用的方法

batch(inputs):批量执行,处理多个输入,提升处理效率

stream(input):流式执行,逐步返回结果,经典的使用场景是大模型是一点点输出的,不是一下返回整个结果,可以通过 stream() 方法,进行流式输出

ainvoke(input):异步执行,用于高并发场景

三、RunnableBranch条件分支

在LangChain中提供了类RunnableBranch来完成LCEL中的条件分支判断,它可以根据输入的不同采用不同的处理逻辑,具体示例如下,在下方示例中程序会根据用户输入中是否包含‘日语’、‘韩语’等关键词,来选择对应的提示词进行处理。根据判断结果,再执行不同的逻辑分支。

import dotenv
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableBranch
from langchain_openai import ChatOpenAI # 读取env配置
dotenv.load_dotenv() def judge_language(inputs):
"""判断语言种类"""
query = inputs["query"]
if "日语" in query:
return "japanese"
elif "韩语" in query:
return "korean"
else:
return "english" # 1.构建提示词
english_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个英语翻译专家,你叫小英"),
("human", "{query}")
]) japanese_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个日语翻译专家,你叫小日"),
("human", "{query}")
]) korean_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个韩语翻译专家,你叫小韩"),
("human", "{query}")
]) # 2.创建模型
llm = ChatOpenAI()
# 3.创建字符串输出解析器
parser = StrOutputParser() # 4.构建链分支结构,默认分支为英语
chain = RunnableBranch(
(lambda x: judge_language(x) == "japanese", japanese_prompt | llm | parser),
(lambda x: judge_language(x) == "korean", korean_prompt | llm | parser),
(english_prompt | llm | parser)
) # 5.执行链
print(f"输出结果:{chain.invoke({'query': '请你用韩语翻译这句话:“我爱你”。并且告诉我你叫什么'})}")

执行结果如下,根据执行结果,执行的是韩语分支。

输出结果:“我爱你”用韩语是:“사랑해” (Saranghae)。

我叫小韩,很高兴为你服务!

四、RunnableLambda函数转换为可执行组件

LangChain还提供了类RunnableLambda,它可以非常方便的将函数转换为可执行组件,如下示例,将字符个数统计函数包装成一个RunnableLambda,并参与链执行。

import dotenv
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda
from langchain_openai import ChatOpenAI # 读取env配置
dotenv.load_dotenv() def character_counter(text):
"""统计输出字符个数"""
return len(text) # 1.构建提示词
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个资深文学家"),
("human", "请以{subject}为主题写一首古诗")
]) # 2.创建模型
llm = ChatOpenAI()
# 3.创建字符串输出解析器
parser = StrOutputParser() # 4.构建链
chain = prompt | llm | parser | RunnableLambda(character_counter) # 5.执行链
print(f"输出结果:{chain.invoke({'subject': '大雪'})}")

执行结果:

输出结果:67

五、RunnableParallel并行处理

在某些需求中,为了提高执行效率,可能会有两个链并行执行的情况,比如同时进行古诗创作和解答数学题。RunnableParallel能让多个链并行处理,最终同时返回结果。

5.1 并行处理

RunnableParallel基础用法示例如下,RunnableParallel中需要传入一个字典结构,key是这个链的标识,value是具体链信息,RunnableParallel本身也是一个可执行组件,因此也可以调用invoke方法,最终执行后,返回的依然是一个字典,key依然是链的标识,value是链执行的结果。

import dotenv
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel
from langchain_openai import ChatOpenAI # 读取env配置
dotenv.load_dotenv() # 1.构建提示词
chinese_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个资深文学家"),
("human", "请以{subject}为主题写一首古诗")
]) math_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个资深数学家"),
("human", "请你给出数学问题:{question}的答案")
]) # 2.创建模型
llm = ChatOpenAI()
# 3.创建字符串输出解析器
parser = StrOutputParser() # 4.创建并行链
parallel_chain = RunnableParallel({
"chinese": chinese_prompt | llm | parser,
"math": math_prompt | llm | parser
}) # 5.执行链
print(f"输出结果:{parallel_chain.invoke({'subject': '春天', 'question': '24和16最大公约数是多少?'})}")

执行结果:

输出结果:{'chinese': '好的,以下是我为春天主题创作的古诗:\n\n**春晨**\n\n柳垂翠影映江天,  \n风拂桃花气馥兰。  \n溪水悠悠行不息,  \n莺歌燕舞入梦间。\n\n朝霞初照翠峰低,  \n芳草萋萋染绿池。  \n心随春光漫游远,  \n醉卧花间梦未已。\n\n这首诗描绘了春天早晨的景象,柳树垂枝,桃花盛开,百鸟欢歌,心随春风游走的宁静与美好。你觉得怎么样?', 'math': '24 和 16 的最大公约数 (GCD) 可以通过辗转相除法求得。我们可以一步一步地计算:\n\n1. 24 ÷ 16 = 1 余 8\n2. 16 ÷ 8 = 2 余 0\n\n当余数为 0 时,除数 8 就是最大公约数。\n\n所以,24 和 16 的最大公约数是 **8**。'}

5.2 RunnableParallel参数传递

上面介绍了RunnableParallel如何进行链的并行执行,下面示例展示了模拟在和大语言模型交互之前,先检索文档的操作,通过RunnableParallel将执行结果作为提示词模板的输入参数,将输出结果继续向下传递。

相当于传递给提示词模板的参数从最开始的一个question,又增加了一个检索文档结果的参数retrieval_info,并且,这里使用了简写方式,在LCEL表达式中,使用字典结构包裹并在管道符两侧的,都会自动包装成RunnableParallel。

from operator import itemgetter

import dotenv
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI # 读取env配置
dotenv.load_dotenv() def retrieval_doc(question):
"""模拟知识库检索"""
print(f"检索器接收到用户提出问题:{question}")
return "你是一个愤怒的语文老师,你叫Bob" # 1.构建提示词
prompt = ChatPromptTemplate.from_messages([
("system", "{retrieval_info}"),
("human", "{question}")
]) # 2.创建模型
llm = ChatOpenAI()
# 3.创建字符串输出解析器
parser = StrOutputParser() # 4.构建链
chain = {
"retrieval_info": lambda x: retrieval_doc(x["question"]),
"question": itemgetter("question")
} | prompt | llm | parser # 5.执行链
print(f"输出结果:{chain.invoke({'question': '你是谁,能否帮我写一首诗?'})}")

执行结果:

检索器接收到用户提出问题:你是谁,能否帮我写一首诗?
输出结果:我是谁?我是Bob,一个愤怒的语文老师!你要写诗?我看看你的水平如何,来来来,给我个主题吧,最好能高大上一点,不然我真的会很生气的!

六、RunnablePassthrough数据传递

RunnablePassthrough是一个相对特殊的组件,它的作用是将输入数据原样传递到下一个可执行组件,同时还能对传递的数据进行数据重组。在构建复杂链时非常有用。

6.1 数据传递基础用法

RunnablePassthrough()进行原样输出很简单,乍一看起来这个类看起来作用不大,实际上它在用来占位、调试中,都有一定作用,如下示例,将参数直接原样传递给下一个可运行组件。

import dotenv
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI # 读取env配置
dotenv.load_dotenv() # 1.构建提示词
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个资深文学家"),
("human", "请简短赏析{name}这首诗,并给出评价")
]) # 2.创建模型
llm = ChatOpenAI()
# 3.创建字符串输出解析器
parser = StrOutputParser() # 4.构建链
chain = RunnablePassthrough() | prompt | llm | parser # 5.执行链
print(f"输出结果:{chain.invoke({'name': '题西林壁'})}")

执行结果:

输出结果:《题西林壁》是苏轼的经典之作,通过描绘西林寺的景象,表达了作者对于自然、人生以及自身处境的深刻感悟。

诗中写道:“不识庐山真面目,只缘身在此山中。”这两句通过庐山的景象,传达了一个哲理:人常常因为局限于眼前的事物,无法看清事物的全貌。用庐山作为象征,既反映了自然的壮丽,也暗示了人生的复杂与迷茫。作者通过这句诗,提出了“跳出事物的框架,方能看到真相”的思想,极富哲理。

整首诗结构简洁,语言凝练,感情真挚,既描写了景色,又引发了对人生和思维局限的深刻反思。它不仅是对庐山美景的写照,更是对人生困境的警示。

**评价:**这首诗具有很高的哲理性和艺术性,语言简练却富有深意,值得每一位读者细细品味。苏轼以“庐山”作比,既能展现山水的美,又能寄托哲理思考,展现了其深厚的文化底蕴。

6.2 数据重组

RunnablePassthrough最强大的功能是可以重新组织数据结构,为后续链执行做准备,示例如下,我们改写了之前使用RunnableParallel进行检索的示例,通过RunnablePassthrough.assign()方法也能达到目的,可以向入参中添加新的属性,下面示例添加了检索结果属性retrieval_info,将新的数据继续向下传递。

import dotenv
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI # 读取env配置
dotenv.load_dotenv() def retrieval_doc(inputs):
"""模拟知识库检索"""
print(f"检索器接收到用户提出问题:{inputs['question']}")
return "你是一个愤怒的语文老师,你叫Bob" # 1.构建提示词
prompt = ChatPromptTemplate.from_messages([
("system", "{retrieval_info}"),
("human", "{question}")
]) # 2.创建模型
llm = ChatOpenAI()
# 3.创建字符串输出解析器
parser = StrOutputParser() # 4.构建链
chain = RunnablePassthrough.assign(retrieval_info=retrieval_doc) | prompt | llm | parser # 5.执行链
print(f"输出结果:{chain.invoke({'question': '你是谁,能否帮我写一首诗?'})}")

执行结果:

检索器接收到用户输入信息:你是谁,能否帮我写一首诗?
输出结果:我是Bob,一个愤怒的语文老师!你敢让我写诗?这可是件严肃的事,不能随便糊弄!好吧,既然你要我写,那我就写。写诗,得有情感,得有深度。你给我一个主题,看看你能承受我给你带来的震撼!

七、总结

通过本文的学习,我们深入了解了LCEL表达式的强大功能。LCEL不仅仅是一种语法糖,更代表了LangChain框架的设计思想:通过标准化的接口和组合式的设计,让复杂的AI应用开发变得简单便捷。掌握了LCEL表达式,你已经具备了构建复杂AI应用的基础能力,后续将继续深入介绍LangChain的核心模块和高级用法,敬请期待。

LangChain框架入门06:手把手带你玩转LCEL表达的更多相关文章

  1. Java开发不懂Docker,学尽Java也枉然,阿里P8架构师手把手带你玩转Docker实战

    转: Java开发不懂Docker,学尽Java也枉然,阿里P8架构师手把手带你玩转Docker实战 Docker简介 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一 ...

  2. [转帖]从零开始入门 K8s | 手把手带你理解 etcd

    从零开始入门 K8s | 手把手带你理解 etcd https://zhuanlan.zhihu.com/p/96721097 导读:etcd 是用于共享配置和服务发现的分布式.一致性的 KV 存储系 ...

  3. 手把手带你玩转Linux

    今天这篇文章带你走进Linux世界的同时,带你手把手玩转Linux,加深对Linux系统的认识. 一.搞好Linux工作必须得不断折腾,说白了,只是动手力量必须强.我在初学Linux的那片,家中三台计 ...

  4. Android 手把手带你玩转自己定义相机

    本文已授权微信公众号<鸿洋>原创首发,转载请务必注明出处. 概述 相机差点儿是每一个APP都要用到的功能,万一老板让你定制相机方不方?反正我是有点方. 关于相机的两天奋斗总结免费送给你. ...

  5. 从零开始入门 K8s | 手把手带你理解 etcd

    作者 | 曾凡松(逐灵) 阿里云容器平台高级技术专家 本文整理自<CNCF x Alibaba 云原生技术公开课>第 16 讲. 导读:etcd 是用于共享配置和服务发现的分布式.一致性的 ...

  6. 手把手带你玩转 DialogFragment

    前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 思维导图 一.为什么要学习 DialogFragment 你还在用 Dialog 吗? 你还在经常烦恼 ...

  7. 手把手教你玩转 CSS3 3D 技术

    css3的3d起步 要玩转css3的3d,就必须了解几个词汇,便是透视(perspective).旋转(rotate)和移动(translate).透视即是以现实的视角来看屏幕上的2D事物,从而展现3 ...

  8. 手把手教你玩转CSS3 3D技术

    手把手教你玩转 CSS3 3D 技术   要玩转css3的3d,就必须了解几个词汇,便是透视(perspective).旋转(rotate)和移动(translate).透视即是以现实的视角来看屏幕上 ...

  9. Java自带RPC实现,RMI框架入门

    Java自带RPC实现,RMI框架入门 首先RMI(Remote Method Invocation)是Java特有的一种RPC实现,它能够使部署在不同主机上的Java对象进行通信与方法调用,它是一种 ...

  10. 完毕port(CompletionPort)具体解释 - 手把手教你玩转网络编程系列之三

       手把手叫你玩转网络编程系列之三    完毕port(Completion Port)具体解释                                                    ...

随机推荐

  1. linux下使用动态壁纸

    让你的linux桌面动起来(幻梦动态壁纸) 我也是突发奇想,做了这么一个程序,目前在多个linux下可以运行,支持双屏 理论上说支持mpv >=29.0 qt>=5.8.0的系统版本 ub ...

  2. RBMQ与odoo15的集成

    背景:在对接物联网设备时候常用的协议就是:MQTT.AMQ.https.还有WebSocket,此案例就是针对接物联网设备传输的消息的消费 原理:通过新建守护线程的方式来启动mq服务,来消费设备平台端 ...

  3. Spring Boot微服务设置logback日志打印级别并关闭kafka debug日志

    摘要:以关闭Spring Boot微服务kafka日志为例,介绍logback日志框架中logger标签的属性. 问题描述   在Spring Boot整合kafka的时候,日志配置使用 logbac ...

  4. golang遍历处理map时的常见性能陷阱

    最近一直在重构优化老系统,所以性能优化相关的文章会比较多. 这次的是有关循环处理map时的性能优化.预分配内存之类的大家都知道的就不多说了,今天来讲点大伙不知道的. 要讲的一共有三点,而且都和循环处理 ...

  5. EasyExcel读取多个sheet表数据,自定义监听器

    接口 /** * 导入 * * @param file * @return */ @PostMapping("/waitimport") public Result waitImp ...

  6. 「Note」数论方向 - 组合数学

    1. 容斥原理 1.1 介绍 解决集合内计数问题. \(S\) 为集合编号集合. \[\left | \bigcup_{i\in S}A_i \right | =\sum_{T\subseteq S\ ...

  7. Spring扩展接口-BeanPostProcessor

    .markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...

  8. Django2.2版本迁移数据库报错问题解决方案

    在迁移的时候系统会抛出异常,提示我们安装mysqlclient. 这时候我们可以使用pymysql进行伪装,在项目的__init__.py中添加如下代码即可.(如果是2.2以前的版本) import ...

  9. Java编码小技巧

    你在写一个方法的时候, 例如传入 两个数组,而你要写的方法代码块又恰好有一种判断方式会导致你要写两个相同代码块, 你就可以自己调用自己,并把传参顺序 换一下 public int[] intersec ...

  10. C# WinFomr 组合快捷键

    private void 控件名称_KeyDown(object sender, KeyEventArgs e) { //如果只是按了回车,而不是按组合快捷键就执行 if (e.KeyCode == ...