LangChain 表达式语言 (LCEL)
LangChain Expression Language (LCEL) 采用了一种声明式的方法,从现有的Runnables构建新的Runnables。
这意味着你描述的是应该发生什么,而不是如何发生,从而允许LangChain优化链的运行时执行。
我们通常将使用LCEL创建的Runnable
称为“链”。重要的是要记住,“链”是Runnable
,并且它实现了完整的Runnable接口。
- LCEL 速查表展示了涉及 Runnable 接口和 LCEL 表达式的常见模式。
- 请参阅以下操作指南列表,这些指南涵盖了使用LCEL的常见任务。
- 内置的
Runnables
列表可以在LangChain Core API 参考中找到。这些Runnables中的许多在使用LCEL在LangChain中组合自定义“链”时非常有用。
LCEL的优势
LangChain 通过多种方式优化了使用 LCEL 构建的链的运行时执行:
- 优化的并行执行:使用RunnableParallel并行运行Runnables,或使用Runnable Batch API通过给定链并行运行多个输入。并行执行可以显著减少延迟,因为处理可以并行进行而不是顺序进行。
- 保证的异步支持:任何使用LCEL构建的链都可以通过Runnable Async API异步运行。这在服务器环境中运行链时非常有用,因为您可能希望同时处理大量请求。
- 简化流式处理:LCEL链可以进行流式处理,允许在链执行时逐步输出。LangChain可以优化输出的流式处理,以最小化首次输出时间(从聊天模型或llm输出的第一块内容所经过的时间)。
其他好处包括:
- 无缝的LangSmith追踪 随着你的链变得越来越复杂,了解每一步到底发生了什么变得越来越重要。 使用LCEL,所有步骤都会自动记录到LangSmith,以实现最大的可观察性和可调试性。
- 标准API:因为所有的链都是使用Runnable接口构建的,所以它们可以像任何其他Runnable一样使用。
- 可使用LangServe部署: 使用LCEL构建的链可以部署用于生产环境。
我应该使用LCEL吗?
LCEL 是一种编排解决方案——它允许 LangChain 以优化的方式处理链的运行时执行。
虽然我们见过用户在生产环境中运行包含数百个步骤的链,但我们通常建议使用LCEL来处理更简单的编排任务。当应用程序需要复杂的状态管理、分支、循环或多个代理时,我们建议用户利用LangGraph。
在LangGraph中,用户定义指定应用程序流程的图。这使得用户在需要LCEL时,可以继续在单个节点中使用LCEL,同时更容易定义更易读和可维护的复杂编排逻辑。
以下是一些指导原则:
- 如果您正在进行一次LLM调用,您不需要LCEL;而是直接调用底层的聊天模型。
- 如果你有一个简单的链(例如,prompt + llm + parser,简单的检索设置等),并且你正在利用LCEL的优势,那么LCEL是一个合理的选择。
- 如果您正在构建一个复杂的链(例如,带有分支、循环、多个代理等),请使用LangGraph。请记住,您始终可以在LangGraph的各个节点中使用LCEL。
组合原语
LCEL
链是通过将现有的 Runnables
组合在一起构建的。两个主要的组合原语是 RunnableSequence 和 RunnableParallel。
许多其他组合原语(例如,RunnableAssign)可以被视为这两种原语的变体。
你可以在LangChain Core API 参考中找到所有组合原语的列表。
可运行序列
RunnableSequence
是一个组合原语,允许你按顺序“链式”连接多个可运行对象,其中一个可运行对象的输出作为下一个可运行对象的输入。
from langchain_core.runnables import RunnableSequence
chain = RunnableSequence([runnable1, runnable2])
使用一些输入调用chain
:
final_output = chain.invoke(some_input)
对应以下内容:
output1 = runnable1.invoke(some_input)
final_output = runnable2.invoke(output1)
runnable1
和 runnable2
是用于链接任何 Runnable
的占位符。
可运行并行
RunnableParallel
是一个组合原语,允许您并发运行多个可运行对象,并为每个对象提供相同的输入。
from langchain_core.runnables import RunnableParallel
chain = RunnableParallel({
"key1": runnable1,
"key2": runnable2,
})
使用一些输入调用chain
:
final_output = chain.invoke(some_input)
将生成一个final_output
字典,其键与输入字典相同,但值被替换为相应可运行对象的输出。
{
"key1": runnable1.invoke(some_input),
"key2": runnable2.invoke(some_input),
}
回想一下,runnables 是并行执行的,因此虽然结果与上面显示的字典推导相同,但执行时间要快得多。
RunnableParallel
支持同步和异步执行(就像所有的Runnables
一样)。
- 对于同步执行,
RunnableParallel
使用 ThreadPoolExecutor 来并发运行可运行对象。 - 对于异步执行,
RunnableParallel
使用 asyncio.gather 来并发运行可运行对象。
组合语法
RunnableSequence
和 RunnableParallel
的使用非常普遍,因此我们为它们创建了一种简写语法。这有助于使代码更具可读性和简洁性。
The |
操作符
我们已经重载了|
操作符,以便从两个Runnables
创建一个RunnableSequence
。
chain = runnable1 | runnable2
等同于:
chain = RunnableSequence([runnable1, runnable2])
The .pipe
方法`
如果你对操作符重载有道德上的顾虑,你可以使用.pipe
方法代替。这等同于|
操作符。
chain = runnable1.pipe(runnable2)
强制转换
LCEL 应用自动类型强制转换,使组合链更容易。
如果你不理解类型强制转换,你可以直接使用RunnableSequence
和RunnableParallel
类。
这将使代码更加冗长,但也会使其更加明确。
字典到RunnableParallel
在LCEL表达式中,字典会自动转换为RunnableParallel
。
例如,以下代码:
mapping = {
"key1": runnable1,
"key2": runnable2,
}
chain = mapping | runnable3
它会自动转换为以下内容:
chain = RunnableSequence([RunnableParallel(mapping), runnable3])
你必须小心,因为mapping
字典不是一个RunnableParallel
对象,它只是一个字典。这意味着以下代码将引发AttributeError
:
mapping.invoke(some_input)
函数到RunnableLambda
在LCEL表达式中,函数会自动转换为RunnableLambda
。
def some_func(x):
return x
chain = some_func | runnable1
它会自动转换为以下内容:
chain = RunnableSequence([RunnableLambda(some_func), runnable1])
你必须小心,因为lambda函数不是一个RunnableLambda
对象,它只是一个函数。这意味着以下代码将引发一个AttributeError
:
lambda x: x + 1.invoke(some_input)
遗留链
LCEL 旨在提供一致的行为和定制性,超越传统的子类链,如 LLMChain
和
ConversationalRetrievalChain
。许多这些传统链隐藏了重要的细节,如提示,随着更多可行的模型的出现,定制变得越来越重要。
如果您目前正在使用这些旧版链之一,请参阅此指南以获取迁移指导。
有关如何使用LCEL执行特定任务的指南,请查看相关操作指南。