LangChain 表达式语言(LCEL)
LangChain Expression Language (LCEL) 采用一种 声明式 方法,从现有的 Runnables 构建新的 Runnables。
这意味着您描述的是应该发生什么,而不是如何发生,从而使 LangChain 能够优化链的运行时执行。
我们通常将使用 LCEL 创建的 Runnable 称为“链”。需要记住的是,“链”是一个 Runnable,并且实现了完整的 Runnable 接口。
- LCEL 速查表 展示了涉及 Runnable 接口和 LCEL 表达式的常见模式。
- 请参阅以下涵盖 LCEL 常用任务的操操作指南列表。
- 在 LangChain 核心 API 参考 中可以找到内置的
Runnables列表。这些可运行对象中的许多在使用 LCEL 于 LangChain 中构建自定义“链”时非常有用。
LCEL的优势
LangChain 通过多种方式优化了使用 LCEL 构建的链在运行时的执行效率:
- 优化的并行执行: 使用 RunnableParallel 并行运行 Runnables,或使用 Runnable 批处理 API 并行处理多个输入。并行执行可显著降低延迟,因为处理过程可以并行而非串行进行。
- 保证异步支持: 使用 LCEL 构建的任何链都可以通过 Runnable 异步 API 异步运行。这在服务器环境中非常有用,当您需要并发处理大量请求时。
- 简化流式传输: LCEL 链可以进行流式传输,允许在链执行过程中逐步输出结果。LangChain 可以优化输出的流式传输,以最小化首个标记的时间(即从开始到聊天模型或大语言模型输出第一个输出块所经过的时间)。
其他优势包括:
- 无缝集成 LangSmith 跟踪 随着您的链式结构变得越来越复杂,了解每一步的具体操作变得愈发重要。 通过 LCEL,所有步骤都会自动记录到 LangSmith,以实现最大程度的可观测性和可调试性。
- 标准 API: 由于所有链都是使用 Runnable 接口构建的,因此它们可以以与任何其他 Runnable 相同的方式使用。
- 可通过 LangServe 部署: 使用 LCEL 构建的链可以使用 LangServe 进行部署,适用于生产环境。
我应该使用 LCEL 吗?
LCEL 是一种 编排解决方案 —— 它使 LangChain 能够以优化的方式处理链在运行时的执行。
尽管我们已经看到用户在生产环境中运行包含数百个步骤的链条,但我们通常建议使用LCEL来处理更简单的编排任务。当应用程序需要复杂的状态管理、分支、循环或多个代理时,我们建议用户利用LangGraph。
在 LangGraph 中,用户定义图来指定应用程序的流程。这使得用户在需要时可以在各个节点中继续使用 LCEL,同时轻松定义更复杂、更易读且更易维护的编排逻辑。
以下是几点指南:
- 如果您只进行一次大型语言模型调用,就不需要使用LCEL;而是可以直接调用底层的 聊天模型。
- 如果你有一个简单的链(例如,提示 + 大型语言模型 + 解析器,或简单的检索设置等),并且你正在利用 LCEL 的优势,那么 LCEL 是一个合理的选项。
- 如果您正在构建一个复杂的链(例如,具有分支、循环、多个代理等),请改用 LangGraph。请记住,您可以在 LangGraph 的各个节点中始终使用 LCEL。
组合原语
LCEL 通过组合现有的 Runnables 来构建。两种主要的组合原语是 RunnableSequence 和 RunnableParallel。
许多其他组合原语(例如 RunnableAssign)可以被视为这两种原语的变体。
您可以在 LangChain Core API 参考 中找到所有组合原语的列表。
RunnableSequence
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
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),
}
回想一下,可运行对象是并行执行的,因此虽然结果与上面所示的字典推导式相同,但执行时间要快得多。
RunnableParallel同时支持同步和异步执行(所有 Runnables 均如此)。
- 对于同步执行,
RunnableParallel使用 ThreadPoolExecutor 来并发运行可运行对象。 - 对于异步执行,
RunnableParallel使用 asyncio.gather 来并发运行可运行对象。
组合语法
RunnableSequence 和 RunnableParallel 的使用非常普遍,因此我们创建了一种简写语法来使用它们。这有助于使代码更易读且更简洁。
| 运算符
我们已重载 | 运算符,通过两个 Runnables 创建一个 RunnableSequence。
chain = runnable1 | runnable2
相当于:
chain = RunnableSequence([runnable1, runnable2])
.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执行特定任务的指南,请参阅 相关操操作指南。