LangChain 表达式语言 (LCEL)
LangChain Expression Language (LCEL) 采用声明式方法,从现有的可运行对象构建新的可运行对象。
这意味着你描述的是应该发生什么,而不是如何发生,从而允许 LangChain 优化链的运行时执行。
我们通常将使用 LCEL 创建的 Runnable
称为“链”。重要的是要记住,“链”是 Runnable
,它实现了完整的 Runnable 接口。
- LCEL 速查表展示了涉及 Runnable 接口和 LCEL 表达式的常见模式。
- 请参阅以下 操作指南列表,其中涵盖了 LCEL 的常见任务。
- 内置
Runnables
的列表可以在 LangChain Core API 参考中找到。当使用 LCEL 在 LangChain 中组合自定义“链”时,许多这些 Runnables 都很有用。
LCEL 的优势
LangChain 以多种方式优化使用 LCEL 构建的链的运行时执行
- 优化的并行执行:使用 RunnableParallel 并行运行 Runnables,或使用 Runnable Batch API 并行运行多个输入通过给定的链。并行执行可以显著减少延迟,因为可以并行而不是顺序地进行处理。
- 保证异步支持:可以使用 Runnable Async API 异步运行使用 LCEL 构建的任何链。当在服务器环境中运行链时,这可能很有用,您希望同时处理大量请求。
- 简化流式传输:LCEL 链可以进行流式传输,允许在链执行时进行增量输出。LangChain 可以优化输出的流式传输,以尽量减少首个令牌的时间(从 聊天模型或 llm 输出的第一个区块的时间)。
其他优势包括
- 无缝 LangSmith 跟踪 随着您的链变得越来越复杂,了解每一步到底发生了什么变得越来越重要。使用 LCEL,所有 步骤都会自动记录到 LangSmith 中,以实现最大的可观察性和可调试性。
- 标准 API:由于所有链都是使用 Runnable 接口构建的,因此它们可以像任何其他 Runnable 一样使用。
- 可使用 LangServe 部署:使用 LCEL 构建的链可以使用 LangServe 部署以供生产使用。
我应该使用 LCEL 吗?
LCEL 是一种 编排解决方案 - 它允许 LangChain 以优化的方式处理链的运行时执行。
虽然我们已经看到用户在生产环境中运行了数百个步骤的链,但我们通常建议将 LCEL 用于更简单的编排任务。当应用程序需要复杂的状态管理、分支、循环或多个代理时,我们建议用户利用 LangGraph。
在 LangGraph 中,用户定义指定应用程序流程的图。这允许用户在需要 LCEL 时在各个节点中使用 LCEL,同时使其易于定义更易读且更易于维护的复杂编排逻辑。
以下是一些准则
- 如果您要进行单个 LLM 调用,则不需要 LCEL;而是直接调用底层 聊天模型。
- 如果您有一个简单的链(例如,提示 + llm + 解析器、简单的检索设置等),那么如果您正在利用 LCEL 的优势,则 LCEL 是一个合理的选择。
- 如果您正在构建一个复杂的链(例如,具有分支、循环、多个代理等),请改用 LangGraph。请记住,您始终可以在 LangGraph 中的各个节点中使用 LCEL。
组合原语
LCEL
链是通过将现有的 Runnables
组合在一起构建的。两个主要的组合原语是 RunnableSequence 和 RunnableParallel。
许多其他组合原语(例如,RunnableAssign)可以被认为是这两个原语的变体。
您可以在 LangChain Core API 参考中找到所有组合原语的列表。
RunnableSequence
RunnableSequence
是一种组合原语,允许您按顺序“链接”多个 runnable,一个 runnable 的输出作为下一个 runnable 的输入。
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
是一种组合原语,允许您并发运行多个 runnable,每个 runnable 都提供相同的输入。
from langchain_core.runnables import RunnableParallel
chain = RunnableParallel({
"key1": runnable1,
"key2": runnable2,
})
使用某些输入调用 chain
final_output = chain.invoke(some_input)
将生成一个 final_output
字典,该字典具有与输入字典相同的键,但值替换为相应 runnable 的输出。
{
"key1": runnable1.invoke(some_input),
"key2": runnable2.invoke(some_input),
}
回想一下,runnable 是并行执行的,因此,虽然结果与上面显示的字典理解相同,但执行时间要快得多。
RunnableParallel
支持同步和异步执行(所有 Runnables
都支持)。
- 对于同步执行,
RunnableParallel
使用 ThreadPoolExecutor 并发运行 runnable。 - 对于异步执行,
RunnableParallel
使用 asyncio.gather 并发运行 runnable。
组合语法
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 执行特定任务的指南,请查看 相关的操作指南。