LangChain 表达式语言 (LCEL)
LangChain Expression Language (LCEL) 采用一种声明式方法,通过现有的 Runnable 来构建新的 Runnable。
这意味着你描述的是应该发生什么,而不是如何发生,这使得 LangChain 能够优化链的运行时执行。
我们经常将使用 LCEL 创建的 Runnable 称为“链”。需要记住的是,“链”是一个 Runnable,它实现了完整的Runnable 接口。
- LCEL 备忘单 展示了涉及 Runnable 接口和 LCEL 表达式的常见模式。
- 请参阅以下如何操作指南列表,其中涵盖了 LCEL 的常见任务。
- 可以在LangChain Core API 参考中找到内置
Runnable的列表。在 LangChain 中使用 LCEL 组合自定义“链”时,许多这些Runnable都很有用。
LCEL 的优势
LangChain 以多种方式优化了使用 LCEL 构建的链的运行时执行:
- 优化的并行执行:使用 RunnableParallel 并行运行
Runnable,或使用 Runnable Batch API 并行处理多个输入。并行执行可以显着降低延迟,因为处理可以并行进行而不是顺序进行。 - 保证的异步支持:使用 Runnable Async API 可以异步运行使用 LCEL 构建的任何链。这在服务器环境中运行链时非常 有用,因为您希望并发处理大量请求。
- 简化流式传输: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 链是通过组合现有的 Runnable 来构建的。两个主要的组合原语是 RunnableSequence 和 RunnableParallel。
许多其他组合原语(例如 RunnableAssign)可以被视为这两个原语的变体。
你可以在LangChain Core API 参考中找到所有组合原语的列表。
RunnableSequence
RunnableSequence 是一个组合原语,允许你将多个 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 支持同步和异步执行(就像所有 Runnable 一样)。
- 对于同步执行,
RunnableParallel使用 ThreadPoolExecutor 并行运行Runnable。 - 对于异步执行,
RunnableParallel使用 asyncio.gather 并行运行Runnable。
组合语法
RunnableSequence 和 RunnableParallel 的使用非常普遍,以至于我们为其创建了简写语法。这有助于使代码更具可读性和简洁性。
| 运算符
我们已经重载了 | 运算符以从两个 Runnable 创建 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 执行特定任务的指南,请查看相关操作指南。