LangChain:LCEL与管道符(|)
LCEL(LangChain Expression Language—表达式语言)是 LangChain 框架中构建 Agent 智能体的核心与灵魂。它提供了一套声明式的语法,能用管道符(|)像拼接积木一样,将各种功能组件(模型、提示、解析器、工具等)组合成一个清晰的数据处理流水线。
LCEL概念
LCEL是什么
LCEL(LangChain Expression Language ) 是LangChain的声明式组合语法,使用**管道符(|)**将各种组件(如模型、提示、解析器、工具等)连接成复杂的执行链。
设计理念是:**声明式编排 **和 **一切皆可运行(Everything is a Runnable)**。
- 声明式编排:只需要清晰地描述“我想做什么”(例如:用这个提示词,调用那个模型,然后解析结果),而不是一步步地指示“我该如何做”。这让代码逻辑变得极其直观。
- 一切皆 Runnable:在 LCEL 中,无论是提示词模板、大模型、输出解析器,还是自定义组件,都被抽象为
Runnable对象,拥有invoke、stream、batch等统一接口。它们就像标准化的积木块,可以通过统一的接口被轻松组合和调用。
通过统一接口(Runnable)+ 管道组合(|),将复杂的工作流表示为简单的管道操作。
管道符(|)是什么
管道符 | 的灵感来自 Unix 管道,它将左边组件的输出作为右边组件的输入。
管道符 |是LCEL的连接运算符,它将多个 Runnable 组件按顺序连接起来,形成一个数据流。前一个组件的输出会自动作为下一个组件的输入。整个链条本身也是一个 Runnable,可以被调用。
例如,A | B意味着“将A的输出作为B的输入”。这种语法极其直观,让代码读起来就像描述一个数据处理流水线。
1 | from langchain_core.prompts import ChatPromptTemplate |
执行流程图解
1 | 用户输入: {"topic": "人工智能"} |
组件与管道符(|)
- 组件:任何实现了
Runnable接口的对象(如ChatPromptTemplate、ChatOpenAI、StrOutputParser等)。 - **管道符 (
|)**:将一个Runnable的输出传递给下一个Runnable作为输入,从而构建工作流。 - 工作流:用户输入 → Prompt 模板(格式化)→ LLM 模型(生成回答)→ 输出解析器(提取纯文本)→ 最终输出。
可以把管道符 | 想象成工厂里的传送带,而每个组件则是传送带上的一个工作站。原料(用户输入)从传送带一端进入,依次经过每个工作站(提示模板、语言模型、输出解析器等)的加工,最终在另一端产出成品(最终答案)。
LCEL组件
关键组件详解
| 组件 | 作用 | 示例 |
|---|---|---|
RunnablePassthrough |
原样传递输入 | {"query": RunnablePassthrough()} |
RunnableParallel |
并行执行多个分支 | RunnableParallel(a=chain1, b=chain2) |
RunnableLambda |
包装自定义函数 | RunnableLambda(lambda x: x.upper()) |
RunnableBranch |
条件分支 | RunnableBranch((condition, chain1), default) |
itemgetter |
提取字典字段 | itemgetter("messages") |
常用组件的输入输出类型
PromptTemplate:input: dict→output: PromptValueChatModel:input: PromptValue / str / list[BaseMessage]→output: BaseMessageStrOutputParser:input: BaseMessage→output: strRunnableLambda:任意可调用对象,需要你手动保证类型匹配。
LCEL优势
- 代码更简洁:从嵌套调用变为线性的管道式组合,自动处理组件间的数据传递和格式转换,大幅提升可读性,让代码更优雅。
- 原生支持高级级特性:LCEL 链本身具备流式输出(
stream)、并行处理(batch)、异步调用(ainvoke)和自动重试、回退等机制,也是生产环境所需的能力,可以轻松附加到链上,无需额外代码。 - 强大的组合性与灵活性:通过
RunnableParallel、RunnableBranch等工具,能轻松构建并行、分支、循环等复杂的数据流。 - Agent的本质: 一个复杂的Agent本身就是由多个LCEL链(工具调用链、思考链、响应生成链)组合而成的。
- 统一的接口与生态:
Runnable协议统一了所有组件的行为,所有链都有.invoke(),.ainvoke(),.stream(),.batch()方法,使得复用和集成变得异常简单。
LCEL写法
等价写法
可以用更显式的 RunnableLambda 写出同样的功能(不推荐,但便于理解):
1 | from langchain_core.runnables import RunnableLambda |
或者如果 summarize_chain 已经输出字符串:
1 | full_chain = summarize_chain | (lambda s: {"summary": s}) | translate_chain |
优雅写法
如果需要保留原始输入的同时添加新字段,或者不想写 lambda,可以使用 RunnablePassthrough.assign,如下:
1 | from langchain_core.runnables import RunnablePassthrough |
但 lambda 方式已经足够清晰,是 LCEL 中非常常见的模式。
LCE使用示例
示例1:基础使用
最经典的用法: prompt | llm | output_parser
语种翻译链:
1 | # 1. 导入所需组件 |
一个简单的问答链:
1 | from langchain_openai import ChatOpenAI |
简单的问答链,提示词指定角色:
1 | from langchain_core.prompts import ChatPromptTemplate |
关键解读:
prompt | llm | output_parser创建了一个清晰的数据流:输入字典->提示模板->LLM模型->输出解析器->最终字符串。- 每个组件都实现了
Runnable接口,知道如何接收上游输入,并产生下游能理解的输出。
示例2:复杂逻辑
LCEL 的强大之处在于它能轻松处理分支和并行等复杂逻辑,而无需编写复杂的 if/else 或循环。
1. 并行处理 (RunnableParallel)
当需要同时处理多个独立任务时(例如,从多个知识库检索信息),RunnableParallel 并发执行可以显著提升效率。
假设想同时获取一个城市的简介和最佳旅游时间:
1 | from langchain_core.runnables import RunnableParallel |
假设想同时生成摘要和提取关键词:
1 | from langchain_core.runnables import RunnableParallel |
并行处理三个任务:
1 | from langchain_core.runnables import RunnableParallel |
2. 条件分支 (RunnableBranch)
RunnableBranch 可以根据输入的条件,动态选择不同的处理路径,实现“if-else”逻辑。
根据问的是天气还是新闻路由:
1 | from langchain_core.runnables import RunnableBranch |
根据文档内容长度路由:
1 | from langchain_core.runnables import RunnableBranch |
判断值条件路由:
1 | from langchain_core.runnables import RunnableBranch |
链嵌套使用路由:
1 | from langchain_core.runnables import RunnableBranch, RunnableLambda |
3. 数据传递 (RunnablePassthrough)
RunnablePassthrough 就像一个“数据通道”,在复杂的字典流中,它可以原封不动地传递某个值,常与 RunnableParallel 配合使用。
1 | from langchain_core.runnables import RunnablePassthrough |
4. 自定义逻辑 (RunnableLambda)
RunnableLambda 允许你将任意的 Python 函数包装成一个 Runnable 对象,无缝嵌入 LCEL 链中,实现复杂的数据转换或逻辑判断。
带检索的RAG链:
1 | from langchain_community.vectorstores import FAISS |
使用 RunnableLambda 把普通函数包装成 Runnable 对象。
1 | from langchain_core.runnables import RunnableLambda |
5. 错误处理 (RunnableWithFallback)
RunnableWithFallback 可以为一个 Runnable 组件配置一个或多个“备胎”。当主组件执行失败时,会自动调用备用组件,提升系统的健壮性。
1 | from langchain_core.runnables import RunnableWithFallback |
使用注意事项
LCEL表达式语言的管道符|是有严格的先后顺序要求,顺序决定了数据流的类型和结构,错误的顺序会导致运行时错误。
LCEL 中的 | 管道操作符并不是简单的“拼接”,而是将前一个组件的输出作为后一个组件的输入。因此,顺序必须遵循各组件之间 输入/输出类型的匹配规则。
两侧都是Runnable对象
|操作符两侧都必须是Runnable对象。最常见的错误是直接将一个普通 Python 对象(如字符串)放在 | 后面。LCEL 期望每一个操作单元都是一个 Runnable,这是它的设计原则。
输入输出类型匹配
核心规则:类型必须匹配
每个 Runnable 组件(如 PromptTemplate、ChatModel、OutputParser)都有其预期的输入类型和输出类型,所以在写 LCEL 链时,首先明确每个组件的输入类型和输出类型。管道传递的本质就是:A | B 要能工作,必须满足 A.output_type ⊆ B.input_type(即 A 的输出是 B 能接受的输入)。
| 连接的两个组件,左边组件的输出类型必须是**右边组件的输入类型。例如,ChatPromptTemplate 输出 PromptValue,ChatOpenAI 接收 PromptValue 并输出 AIMessage,而 StrOutputParser 接收 AIMessage 并输出 str。
最常见的:PromptTemplate → ChatModel → OutputParser
1 | from langchain_core.prompts import ChatPromptTemplate |
- 输入
{"topic": "程序员"}→PromptTemplate期望一个dict,输出一个PromptValue(或字符串化的消息列表)。 PromptValue→ChatModel期望PromptValue或消息序列,输出BaseMessage。BaseMessage→StrOutputParser期望BaseMessage,输出str。
错误1:ChatModel 在前,PromptTemplate 在后
1 | # 错误:类型不匹配 |
model输出BaseMessage,但prompt期望输入dict(包含模板变量)。BaseMessage无法直接转换为dict,因此抛出TypeError。
错误2:OutputParser 在前,ChatModel 在后
1 | # 错误示例 |
parser输出str(或其他解析后的类型),而model通常期望PromptValue、字符串或消息序列。直接传递str在某些模型下可能勉强工作(如旧版LLM接口),但不推荐,且会丢失 prompt 模板的能力。更重要的是,如果你的 parser 输出的是复杂结构(如dict),model很可能无法处理。
错误3:两个 PromptTemplate 直接连接
1 | prompt1 = ChatPromptTemplate.from_template("先总结:{text}") |
prompt1输出PromptValue,但prompt2期望dict。除非你手动包装一个适配器,否则类型不匹配。
字典格式的正确使用
LCEL 链条中的数据通常以字典(dict)的形式在组件间传递。确保每个组件的输入变量名与上一步输出的键(key)匹配。例如,prompt 需要 {topic},那么前一个组件的输出或 invoke 的输入就必须包含 topic 这个键。
1 | # 正确:RunnableParallel 或 字典字面量 |
提示词模板变量匹配
这是最常见的错误之一。如果 PromptTemplate 中定义了 {variable_name},那么在调用链时,输入的字典中必须包含 variable_name 这个键,否则会抛出 ValueError。
输出解析器失败
输出解析器(如 JsonOutputParser)期望模型输出特定格式。如果模型输出不符合预期,解析会失败。调试时,可以先移除解析器,打印模型的原始输出,检查格式问题,然后通过优化提示词(明确告知模型输出格式)来解决。
上下文窗口超限
如果输入文本过长,加上提示词模板后可能会超出模型的最大 token 限制。可以使用 LangChain 的 TextSplitter 将长文档切分成小块来处理。
版本兼容性与调试
遇到类型错误,可以分段执行(如先 chain1 = prompt | model,再单独 chain1.invoke(...) 观察输出类型)来调试。
LCEL 链本身就像一个“黑盒”,内部的中间结果不容易直接查看。可以使用 langchain_core.callbacks 模块中的 ConsoleCallbackHandler,它能在执行时打印每个组件的输入和输出。
LangChain 迭代非常快,不同版本间可能存在不兼容。建议使用 langchain-core 和 langchain-community 等拆分后的包,并锁定版本。
对于复杂的链条,调试可能比较困难,可以考虑使用 LangSmith 这样的工具进行全链路跟踪和可视化。
优先使用管道符|
优先使用 | 而非 .pipe():管道符 | 是组合 LCEL 链的首选和推荐方式,因为它更简洁、直观,符合 Python 语言习惯。.pipe() 方法作为备选,主要在需要动态构建链时使用,例如在循环中根据条件添加组件。
拥抱现代化API
拥抱现代化API,舍弃旧用法:在 langchain.chains 模块中的 LLMChain、SequentialChain 等旧式链(Legacy Chains)已被标记为过时(deprecated),并将在未来的版本中被移除。请确保学习和实践都基于最新的 LCEL 语法。
异步支持
1 | # LCEL 链自动支持异步 |
调试与追踪
1 | # 查看链的结构 |
状态传递
1 | # 使用 RunnablePassthrough.assign 保留原始输入 |
调整顺序或适配类型
当需要实现非标准顺序时,可以使用 LCEL 提供的适配器组件:
| 场景 | 解决方案 | 示例 |
|---|---|---|
| 前一步输出类型不满足下一步输入 | RunnableLambda 或 RunnablePassthrough 进行转换 |
`chain = prompt1 |
| 需要同时传递多个输入(分支合并) | RunnableParallel |
`{“summary”: chain1, “topic”: chain2} |
| 在管道中插入一个无输入的函数 | RunnablePassthrough.assign() |
保留原有输入并添加新字段 |
从 BaseMessage 中提取字符串内容 |
使用 StrOutputParser() |
`model |
LCEL 的 | 管道要求前后类型匹配。当不匹配时,使用 RunnableLambda、RunnablePassthrough、StrOutputParser 等适配器进行类型转换,或重新设计管道逻辑。
示例:修复“两个 prompt 不能直接连”的问题
1 | from langchain_core.runnables import RunnableLambda, RunnablePassthrough |
LCEL示例解读
示例代码
1 | # 用LCEL完成 总结 → 翻译 的两步任务 |
详细拆解这段代码,理解它在 LCEL(LangChain 表达式语言)中的精妙设计。
1 | full_chain = summarize_chain | (lambda x: {"summary": x.content}) | translate_chain |
这段代码构建了一个两步骤的流水线:先总结,再翻译。它体现了 LCEL 用 | 管道符连接不同组件时的类型转换思想。
三个组成部分
| 组件 | 类型 | 输入类型 | 输出类型 |
|---|---|---|---|
summarize_chain |
一个 LCEL 链(通常是 `PromptTemplate | ChatModel | StrOutputParser`) |
lambda x: {"summary": x.content} |
一个 Python 函数,被 RunnableLambda 自动包装 |
取决于上游输出:这里上游是 summarize_chain 输出 str,所以 x 是 str |
dict(例如 {"summary": "这是总结内容"}) |
translate_chain |
另一个 LCEL 链,通常期望输入是一个包含 summary 键的字典 |
dict(必须包含 summary 字段) |
str(翻译后的文本) |
数据流动过程
Step1:执行summarize_chain
假设我们最终调用:
1 | result = full_chain.invoke({"text": "LangChain 是一个用于构建 LLM 应用的框架..."}) |
{"text": "..."}作为输入传给summarize_chain。summarize_chain内部可能是这样的:1
2
3summarize_prompt = PromptTemplate.from_template("用中文总结以下内容:\n{text}")
model = ChatOpenAI()
summarize_chain = summarize_prompt | model | StrOutputParser()- 它输出一个 字符串,例如:这个字符串会作为
1
"LangChain 是一个用于构建大语言模型应用的框架。"
lambda函数的输入x。
Step2:通过lambda函数进行类型转换
1 | lambda x: {"summary": x.content} |
注意,这里有一个隐藏陷阱:x 是字符串,而字符串没有 .content 属性! 实际上,更常见的写法应该是:
1 | lambda x: {"summary": x} # 因为 x 已经是纯文本字符串 |
但原代码写的是 x.content,这暗示 x 可能是一个 BaseMessage 对象(例如 AIMessage)。
这说明 summarize_chain 的输出很可能**没有经过 StrOutputParser**,而是直接输出了 ChatModel 的原始 AIMessage。
修正理解(更符合实际场景),通常我们写两步链时,会这样设计:
1 | summarize_chain = prompt | model # 输出 AIMessage |
summarize_chain输出AIMessage,其content属性是总结文本。- lambda 函数接收
msg(一个AIMessage对象),提取msg.content,并包装成字典{"summary": ...}。 - 这个字典正好是
translate_chain所期望的输入(它的 prompt 模板中有{summary}变量)。
Step3:执行translate_chain
translate_chain收到{"summary": "LangChain 是一个用于构建大语言模型应用的框架。"}- 它内部的 prompt 模板类似:
1
translate_prompt = PromptTemplate.from_template("将以下内容翻译成英文:\n{summary}")
- 经过模型和解析器后,输出一个字符串,例如:
1
"LangChain is a framework for building large language model applications."
最终 full_chain.invoke(...) 返回这个翻译后的字符串。
lambda函数
核心原因:连接两个对数据结构要求不同的组件。
summarize_chain输出的是纯文本(或AIMessage)。translate_chain期望的输入是一个字典,键名为summary。
直接连接 summarize_chain | translate_chain 会失败,因为类型不匹配:
- 如果
translate_chain的第一个组件是PromptTemplate,它要求输入为dict(提供模板变量summary)。 - 而
summarize_chain输出的是str或AIMessage,不能直接塞进PromptTemplate。
lambda 函数充当了一个适配器(Adapter),在 LCEL 中被自动提升为 RunnableLambda,负责将上游的输出转换成下游需要的格式。
示例总结
| 代码片段 | 作用 |
|---|---|
summarize_chain |
第一步:生成总结(输出文本或消息对象) |
| ` | (lambda x: {“summary”: x.content})` |
translate_chain |
第二步:翻译字典中的 summary 字段内容 |
最佳实践总结
- 从简单开始:先掌握
prompt | model | parser基础模式 - **善用
RunnableParallel**:需要多任务并行时优先考虑 - 保持链的可读性:复杂逻辑拆分为多个子链
- **利用
.bind()**:固定参数如model.bind(temperature=0) - 使用 LangSmith:生产环境务必开启追踪调试
LangChain:LCEL与管道符(|)
http://blog.gxitsky.com/2026/04/11/AI-LangChain-034-Chain-LCEL/

