概述:为什么 LCEL 是 LangChain 的核心能力?
前几篇我们已经多次写过这样的代码:
python
chain = prompt | model | parser
result = chain.invoke({"topic": "LangChain"})
这行代码看起来像语法糖,但它其实是 LangChain 很核心的一层抽象:LCEL。
LCEL 全称是 LangChain Expression Language,可以理解成 LangChain 用来声明、组合和执行调用流程的表达式语言。
它要解决的问题是:
- 如何把 Prompt、Model、Parser、Retriever、Tool、普通函数串成一个流程。
- 如何让这个流程天然支持
invoke、batch、stream、异步调用。 - 如何把顺序执行、并行执行、字段透传、字段追加这些数据流操作表达清楚。
- 如何让复杂链路可以被追踪、调试、复用和测试。
如果只写一次模型调用,LCEL 的价值不明显。但只要你的应用进入 RAG、工具调用、结构化输出、多模型路由、Agent 工作流,LCEL 就会变成基础设施。
LCEL 的本质,是用统一的 Runnable 抽象,把 LLM 应用里的多个处理步骤组合成可执行、可追踪、可复用的数据流。
先看一个最小链:prompt | model | parser
先从最熟悉的例子开始:
python
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
load_dotenv()
model = init_chat_model(
"gpt-4o-mini",
model_provider="openai",
temperature=0,
)
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个严谨的技术导师。"),
("human", "请解释这个概念:{topic}"),
])
parser = StrOutputParser()
chain = prompt | model | parser
result = chain.invoke({"topic": "LCEL"})
print(result)
这条链的数据流是:
#mermaid-svg-OPJTCGoFa8EdzM6f{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OPJTCGoFa8EdzM6f .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OPJTCGoFa8EdzM6f .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OPJTCGoFa8EdzM6f .error-icon{fill:#552222;}#mermaid-svg-OPJTCGoFa8EdzM6f .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OPJTCGoFa8EdzM6f .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OPJTCGoFa8EdzM6f .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OPJTCGoFa8EdzM6f .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OPJTCGoFa8EdzM6f .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OPJTCGoFa8EdzM6f .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OPJTCGoFa8EdzM6f .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OPJTCGoFa8EdzM6f .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OPJTCGoFa8EdzM6f .marker.cross{stroke:#333333;}#mermaid-svg-OPJTCGoFa8EdzM6f svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OPJTCGoFa8EdzM6f p{margin:0;}#mermaid-svg-OPJTCGoFa8EdzM6f .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OPJTCGoFa8EdzM6f .cluster-label text{fill:#333;}#mermaid-svg-OPJTCGoFa8EdzM6f .cluster-label span{color:#333;}#mermaid-svg-OPJTCGoFa8EdzM6f .cluster-label span p{background-color:transparent;}#mermaid-svg-OPJTCGoFa8EdzM6f .label text,#mermaid-svg-OPJTCGoFa8EdzM6f span{fill:#333;color:#333;}#mermaid-svg-OPJTCGoFa8EdzM6f .node rect,#mermaid-svg-OPJTCGoFa8EdzM6f .node circle,#mermaid-svg-OPJTCGoFa8EdzM6f .node ellipse,#mermaid-svg-OPJTCGoFa8EdzM6f .node polygon,#mermaid-svg-OPJTCGoFa8EdzM6f .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OPJTCGoFa8EdzM6f .rough-node .label text,#mermaid-svg-OPJTCGoFa8EdzM6f .node .label text,#mermaid-svg-OPJTCGoFa8EdzM6f .image-shape .label,#mermaid-svg-OPJTCGoFa8EdzM6f .icon-shape .label{text-anchor:middle;}#mermaid-svg-OPJTCGoFa8EdzM6f .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OPJTCGoFa8EdzM6f .rough-node .label,#mermaid-svg-OPJTCGoFa8EdzM6f .node .label,#mermaid-svg-OPJTCGoFa8EdzM6f .image-shape .label,#mermaid-svg-OPJTCGoFa8EdzM6f .icon-shape .label{text-align:center;}#mermaid-svg-OPJTCGoFa8EdzM6f .node.clickable{cursor:pointer;}#mermaid-svg-OPJTCGoFa8EdzM6f .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OPJTCGoFa8EdzM6f .arrowheadPath{fill:#333333;}#mermaid-svg-OPJTCGoFa8EdzM6f .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OPJTCGoFa8EdzM6f .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OPJTCGoFa8EdzM6f .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OPJTCGoFa8EdzM6f .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OPJTCGoFa8EdzM6f .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OPJTCGoFa8EdzM6f .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OPJTCGoFa8EdzM6f .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OPJTCGoFa8EdzM6f .cluster text{fill:#333;}#mermaid-svg-OPJTCGoFa8EdzM6f .cluster span{color:#333;}#mermaid-svg-OPJTCGoFa8EdzM6f div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OPJTCGoFa8EdzM6f .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OPJTCGoFa8EdzM6f rect.text{fill:none;stroke-width:0;}#mermaid-svg-OPJTCGoFa8EdzM6f .icon-shape,#mermaid-svg-OPJTCGoFa8EdzM6f .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OPJTCGoFa8EdzM6f .icon-shape p,#mermaid-svg-OPJTCGoFa8EdzM6f .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OPJTCGoFa8EdzM6f .icon-shape .label rect,#mermaid-svg-OPJTCGoFa8EdzM6f .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OPJTCGoFa8EdzM6f .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OPJTCGoFa8EdzM6f .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OPJTCGoFa8EdzM6f :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输入字典: topic
ChatPromptTemplate
PromptValue / Messages
ChatModel
AIMessage
StrOutputParser
字符串
对应关系:
| 步骤 | 输入 | 输出 |
|---|---|---|
prompt |
{"topic": "LCEL"} |
messages |
model |
messages | AIMessage |
parser |
AIMessage |
str |
| 的含义不是"字符串拼接",而是把左边组件的输出交给右边组件作为输入。
也就是说:
python
chain = prompt | model | parser
可以近似理解成:
python
prompt_output = prompt.invoke({"topic": "LCEL"})
model_output = model.invoke(prompt_output)
result = parser.invoke(model_output)
但 LCEL 不只是帮你少写几行代码。它真正的价值是:组合之后的 chain 自己也是一个 Runnable,因此它也能继续被组合、批处理、流式输出和追踪。
Runnable:LCEL 的统一接口
官方参考文档里,Runnable 被定义为一个可以被调用、批处理、流式处理、转换和组合的工作单元。
你可以把 Runnable 理解成 LangChain 世界里的"可执行组件"。
常见 Runnable 包括:
ChatPromptTemplateChatModelStrOutputParserRetrieverRunnableLambdaRunnableParallelRunnablePassthrough- 由多个 Runnable 组合出来的
RunnableSequence
它们看起来功能不同,但都有相似的调用方式:
python
output = runnable.invoke(input_data)
很多 Runnable 还支持:
python
runnable.batch(list_of_inputs)
runnable.stream(input_data)
await runnable.ainvoke(input_data)
await runnable.abatch(list_of_inputs)
只要一个组件实现了 Runnable 接口,它就可以进入 LCEL 管道。
RunnableSequence:顺序执行的数据管道
最常见的 LCEL 链是顺序执行:
python
chain = prompt | model | parser
这背后构造的是 RunnableSequence。
RunnableSequence 的规则很简单:
上一步的输出,作为下一步的输入。
例如:
text
input -> step1 -> step2 -> step3 -> output
用纯函数演示会更直观:
python
from langchain_core.runnables import RunnableLambda
def add_one(x: int) -> int:
return x + 1
def multiply_by_two(x: int) -> int:
return x * 2
chain = RunnableLambda(add_one) | RunnableLambda(multiply_by_two)
print(chain.invoke(3))
输出:
text
8
执行过程:
text
3 -> add_one -> 4 -> multiply_by_two -> 8
这和 LLM 没有关系,但它说明了 LCEL 的核心:每个步骤只关心自己的输入输出,组合逻辑交给 RunnableSequence。
RunnableLambda:把普通函数放进链里
很多时候,我们需要在链中间做一些轻量处理:
- 清洗输入。
- 提取字段。
- 转换格式。
- 追加默认值。
- 对模型输出做后处理。
这时可以用 RunnableLambda 把普通 Python 函数包装成 Runnable。
示例:把用户输入转成统一字典。
python
from langchain_core.runnables import RunnableLambda
def normalize_input(question: str) -> dict:
return {
"question": question.strip(),
"language": "中文",
}
normalizer = RunnableLambda(normalize_input)
print(normalizer.invoke(" LangChain 是什么? "))
输出:
python
{"question": "LangChain 是什么?", "language": "中文"}
放进 chain:
python
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "请使用 {language} 回答。"),
("human", "{question}"),
])
chain = normalizer | prompt
数据流:
text
" LangChain 是什么? "
-> normalize_input
{"question": "LangChain 是什么?", "language": "中文"}
-> prompt
messages
RunnableLambda 的适用边界
RunnableLambda 很方便,但不要滥用。
适合:
- 纯数据转换。
- 字段提取。
- 小型业务判断。
- 简单格式化。
不适合:
- 大量复杂业务逻辑。
- 有副作用的写数据库操作。
- 需要完整流式转换的场景。
- 应该封装成工具、服务或 LangGraph 节点的复杂流程。
官方参考中也提醒,RunnableLambda 默认不实现流式 transform,所以如果你把它放在流式链路中间,可能会影响后续流式输出开始的时机。
RunnableLambda 是把普通函数接入 LCEL 的桥,但复杂业务不要都塞进 lambda。
RunnableParallel:并行执行多个分支
顺序链解决的是"一步接一步"。
但很多场景需要"一份输入,同时走多个分支"。
例如:
- 同一个问题,同时生成摘要和关键词。
- 同一个主题,同时生成标题、简介和大纲。
- 同一个查询,同时走向量检索和关键词检索。
- 同一个用户问题,同时做意图识别和情绪识别。
这时可以用 RunnableParallel。
纯函数示例:
python
from langchain_core.runnables import RunnableLambda, RunnableParallel
def add_one(x: int) -> int:
return x + 1
def multiply_by_two(x: int) -> int:
return x * 2
parallel = RunnableParallel(
plus_one=RunnableLambda(add_one),
times_two=RunnableLambda(multiply_by_two),
)
print(parallel.invoke(3))
输出:
python
{"plus_one": 4, "times_two": 6}
数据流:
#mermaid-svg-8Yh8LdWfPQir0LrU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8Yh8LdWfPQir0LrU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8Yh8LdWfPQir0LrU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8Yh8LdWfPQir0LrU .error-icon{fill:#552222;}#mermaid-svg-8Yh8LdWfPQir0LrU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8Yh8LdWfPQir0LrU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8Yh8LdWfPQir0LrU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8Yh8LdWfPQir0LrU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8Yh8LdWfPQir0LrU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8Yh8LdWfPQir0LrU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8Yh8LdWfPQir0LrU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8Yh8LdWfPQir0LrU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8Yh8LdWfPQir0LrU .marker.cross{stroke:#333333;}#mermaid-svg-8Yh8LdWfPQir0LrU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8Yh8LdWfPQir0LrU p{margin:0;}#mermaid-svg-8Yh8LdWfPQir0LrU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8Yh8LdWfPQir0LrU .cluster-label text{fill:#333;}#mermaid-svg-8Yh8LdWfPQir0LrU .cluster-label span{color:#333;}#mermaid-svg-8Yh8LdWfPQir0LrU .cluster-label span p{background-color:transparent;}#mermaid-svg-8Yh8LdWfPQir0LrU .label text,#mermaid-svg-8Yh8LdWfPQir0LrU span{fill:#333;color:#333;}#mermaid-svg-8Yh8LdWfPQir0LrU .node rect,#mermaid-svg-8Yh8LdWfPQir0LrU .node circle,#mermaid-svg-8Yh8LdWfPQir0LrU .node ellipse,#mermaid-svg-8Yh8LdWfPQir0LrU .node polygon,#mermaid-svg-8Yh8LdWfPQir0LrU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8Yh8LdWfPQir0LrU .rough-node .label text,#mermaid-svg-8Yh8LdWfPQir0LrU .node .label text,#mermaid-svg-8Yh8LdWfPQir0LrU .image-shape .label,#mermaid-svg-8Yh8LdWfPQir0LrU .icon-shape .label{text-anchor:middle;}#mermaid-svg-8Yh8LdWfPQir0LrU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8Yh8LdWfPQir0LrU .rough-node .label,#mermaid-svg-8Yh8LdWfPQir0LrU .node .label,#mermaid-svg-8Yh8LdWfPQir0LrU .image-shape .label,#mermaid-svg-8Yh8LdWfPQir0LrU .icon-shape .label{text-align:center;}#mermaid-svg-8Yh8LdWfPQir0LrU .node.clickable{cursor:pointer;}#mermaid-svg-8Yh8LdWfPQir0LrU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8Yh8LdWfPQir0LrU .arrowheadPath{fill:#333333;}#mermaid-svg-8Yh8LdWfPQir0LrU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8Yh8LdWfPQir0LrU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8Yh8LdWfPQir0LrU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8Yh8LdWfPQir0LrU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8Yh8LdWfPQir0LrU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8Yh8LdWfPQir0LrU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8Yh8LdWfPQir0LrU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8Yh8LdWfPQir0LrU .cluster text{fill:#333;}#mermaid-svg-8Yh8LdWfPQir0LrU .cluster span{color:#333;}#mermaid-svg-8Yh8LdWfPQir0LrU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8Yh8LdWfPQir0LrU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8Yh8LdWfPQir0LrU rect.text{fill:none;stroke-width:0;}#mermaid-svg-8Yh8LdWfPQir0LrU .icon-shape,#mermaid-svg-8Yh8LdWfPQir0LrU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8Yh8LdWfPQir0LrU .icon-shape p,#mermaid-svg-8Yh8LdWfPQir0LrU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8Yh8LdWfPQir0LrU .icon-shape .label rect,#mermaid-svg-8Yh8LdWfPQir0LrU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8Yh8LdWfPQir0LrU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8Yh8LdWfPQir0LrU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8Yh8LdWfPQir0LrU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输入 3
add_one
multiply_by_two
plus_one: 4
times_two: 6
输出字典
在 LCEL 里,字典字面量也经常被自动解释为并行结构:
python
chain = RunnableLambda(add_one) | {
"times_two": RunnableLambda(multiply_by_two),
"as_text": RunnableLambda(lambda x: f"当前值是 {x}"),
}
这等价于先执行 add_one,再把结果同时送到两个分支。
LLM 示例:同一主题生成多个结果
python
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel
load_dotenv()
model = init_chat_model(
"gpt-4o-mini",
model_provider="openai",
temperature=0,
)
title_chain = (
ChatPromptTemplate.from_template("为主题《{topic}》生成 3 个技术文章标题。")
| model
| StrOutputParser()
)
summary_chain = (
ChatPromptTemplate.from_template("用 100 字介绍主题《{topic}》。")
| model
| StrOutputParser()
)
outline_chain = (
ChatPromptTemplate.from_template("为主题《{topic}》生成 5 点文章大纲。")
| model
| StrOutputParser()
)
chain = RunnableParallel(
titles=title_chain,
summary=summary_chain,
outline=outline_chain,
)
result = chain.invoke({"topic": "LangChain LCEL"})
print(result["titles"])
print(result["summary"])
print(result["outline"])
返回值是一个字典:
python
{
"titles": "...",
"summary": "...",
"outline": "..."
}
RunnableParallel 让同一份输入同时进入多个 Runnable 分支,并把分支结果合并成字典。
RunnablePassthrough:保留原始输入
很多链路中,我们不只是要处理后的结果,还要保留原始输入。
例如:
text
输入问题 -> 检索文档 -> 生成答案
生成答案时,Prompt 同时需要:
- 原始问题
question - 检索上下文
context
这时就会用到 RunnablePassthrough。
一个典型写法:
python
from langchain_core.runnables import RunnablePassthrough
chain = {
"question": RunnablePassthrough(),
"context": retriever,
} | prompt | model | parser
数据流是:
#mermaid-svg-HI0j3LsZRwtf3F6D{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HI0j3LsZRwtf3F6D .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HI0j3LsZRwtf3F6D .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HI0j3LsZRwtf3F6D .error-icon{fill:#552222;}#mermaid-svg-HI0j3LsZRwtf3F6D .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HI0j3LsZRwtf3F6D .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HI0j3LsZRwtf3F6D .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HI0j3LsZRwtf3F6D .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HI0j3LsZRwtf3F6D .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HI0j3LsZRwtf3F6D .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HI0j3LsZRwtf3F6D .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HI0j3LsZRwtf3F6D .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HI0j3LsZRwtf3F6D .marker.cross{stroke:#333333;}#mermaid-svg-HI0j3LsZRwtf3F6D svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HI0j3LsZRwtf3F6D p{margin:0;}#mermaid-svg-HI0j3LsZRwtf3F6D .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HI0j3LsZRwtf3F6D .cluster-label text{fill:#333;}#mermaid-svg-HI0j3LsZRwtf3F6D .cluster-label span{color:#333;}#mermaid-svg-HI0j3LsZRwtf3F6D .cluster-label span p{background-color:transparent;}#mermaid-svg-HI0j3LsZRwtf3F6D .label text,#mermaid-svg-HI0j3LsZRwtf3F6D span{fill:#333;color:#333;}#mermaid-svg-HI0j3LsZRwtf3F6D .node rect,#mermaid-svg-HI0j3LsZRwtf3F6D .node circle,#mermaid-svg-HI0j3LsZRwtf3F6D .node ellipse,#mermaid-svg-HI0j3LsZRwtf3F6D .node polygon,#mermaid-svg-HI0j3LsZRwtf3F6D .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HI0j3LsZRwtf3F6D .rough-node .label text,#mermaid-svg-HI0j3LsZRwtf3F6D .node .label text,#mermaid-svg-HI0j3LsZRwtf3F6D .image-shape .label,#mermaid-svg-HI0j3LsZRwtf3F6D .icon-shape .label{text-anchor:middle;}#mermaid-svg-HI0j3LsZRwtf3F6D .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HI0j3LsZRwtf3F6D .rough-node .label,#mermaid-svg-HI0j3LsZRwtf3F6D .node .label,#mermaid-svg-HI0j3LsZRwtf3F6D .image-shape .label,#mermaid-svg-HI0j3LsZRwtf3F6D .icon-shape .label{text-align:center;}#mermaid-svg-HI0j3LsZRwtf3F6D .node.clickable{cursor:pointer;}#mermaid-svg-HI0j3LsZRwtf3F6D .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HI0j3LsZRwtf3F6D .arrowheadPath{fill:#333333;}#mermaid-svg-HI0j3LsZRwtf3F6D .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HI0j3LsZRwtf3F6D .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HI0j3LsZRwtf3F6D .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HI0j3LsZRwtf3F6D .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HI0j3LsZRwtf3F6D .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HI0j3LsZRwtf3F6D .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HI0j3LsZRwtf3F6D .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HI0j3LsZRwtf3F6D .cluster text{fill:#333;}#mermaid-svg-HI0j3LsZRwtf3F6D .cluster span{color:#333;}#mermaid-svg-HI0j3LsZRwtf3F6D div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HI0j3LsZRwtf3F6D .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HI0j3LsZRwtf3F6D rect.text{fill:none;stroke-width:0;}#mermaid-svg-HI0j3LsZRwtf3F6D .icon-shape,#mermaid-svg-HI0j3LsZRwtf3F6D .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HI0j3LsZRwtf3F6D .icon-shape p,#mermaid-svg-HI0j3LsZRwtf3F6D .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HI0j3LsZRwtf3F6D .icon-shape .label rect,#mermaid-svg-HI0j3LsZRwtf3F6D .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HI0j3LsZRwtf3F6D .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HI0j3LsZRwtf3F6D .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HI0j3LsZRwtf3F6D :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户问题
RunnablePassthrough
Retriever
question
context
Prompt
Model
Parser
意思是:
RunnablePassthrough()原样保留输入,作为question。retriever使用同一个输入检索文档,作为context。- 两个字段合并成字典,交给
prompt。
如果不用 RunnablePassthrough,你可能会丢掉原始问题,只剩检索结果。
用 assign 追加字段
RunnablePassthrough.assign(...) 常用来在已有字典上追加字段。
例如输入已经是:
python
{
"question": "LCEL 是什么?",
"context": "LCEL 是 LangChain 的表达式语言..."
}
你想追加一个字段 question_length:
python
from langchain_core.runnables import RunnableLambda, RunnablePassthrough
chain = RunnablePassthrough.assign(
question_length=RunnableLambda(lambda x: len(x["question"]))
)
result = chain.invoke({
"question": "LCEL 是什么?",
"context": "LCEL 是 LangChain 的表达式语言..."
})
print(result)
输出类似:
python
{
"question": "LCEL 是什么?",
"context": "LCEL 是 LangChain 的表达式语言...",
"question_length": 8
}
RunnablePassthrough 用来保留输入,assign 用来在原有输入字典上追加新字段。
数据流视角:LCEL 里最重要的是输入输出形状
学 LCEL,不能只盯着 API 名字。
最重要的是看每一步的输入输出形状。
例如:
python
chain = prompt | model | parser
输入输出形状:
text
dict -> PromptValue/messages -> AIMessage -> str
再看 RAG 链:
python
chain = (
{
"context": retriever,
"question": RunnablePassthrough(),
}
| prompt
| model
| parser
)
输入输出形状:
text
str
-> {
"context": list[Document],
"question": str
}
-> PromptValue/messages
-> AIMessage
-> str
如果某一步报错,通常就是输入输出形状没接上。
例如 Prompt 需要 {question}:
python
prompt = ChatPromptTemplate.from_template("问题:{question}")
但你传进去的是字符串:
python
prompt.invoke("LCEL 是什么?")
就不如传字典清晰:
python
prompt.invoke({"question": "LCEL 是什么?"})
LCEL 调试的第一原则,是画清楚每一步的输入类型和输出类型。
invoke:单次调用
invoke 是最基础的执行方式。
python
result = chain.invoke({"topic": "LCEL"})
它表示:给链一个输入,等待完整输出。
适合:
- 普通问答。
- 数据抽取。
- 分类。
- 单次摘要。
- 后端同步任务。
invoke 的返回值取决于链最后一个步骤。
例如:
python
chain = prompt | model
返回通常是 AIMessage。
如果加了:
python
chain = prompt | model | StrOutputParser()
返回就是字符串。
所以不要机械地认为 chain.invoke() 一定返回字符串。要看链的最后一步是什么。
batch:批量处理
batch 可以一次处理多个输入:
python
inputs = [
{"topic": "LCEL"},
{"topic": "Runnable"},
{"topic": "RAG"},
]
results = chain.batch(inputs)
for result in results:
print(result)
如果你的链是:
python
chain = prompt | model | StrOutputParser()
那么 results 就是字符串列表。
适合:
- 批量生成摘要。
- 批量分类。
- 批量生成标签。
- 批量清洗文本。
控制并发
批量调用模型时要小心限流。可以通过 config 控制并发:
python
results = chain.batch(
inputs,
config={"max_concurrency": 5},
)
不要把几千条数据直接一次性扔给 batch。生产环境还需要考虑:
- 供应商限流。
- 失败重试。
- 断点续跑。
- 成本控制。
- 日志和追踪。
- 输出落库。
stream:流式输出
如果你做聊天界面,通常不希望用户等完整答案生成完才看到内容。
可以使用 stream:
python
for chunk in chain.stream({"topic": "LCEL"}):
print(chunk, end="", flush=True)
如果链最后是 StrOutputParser(),chunk 通常就是逐步解析出的字符串片段。
流式输出的数据流可以理解为:
text
输入 -> Prompt -> Model 持续吐 token -> Parser 持续解析 -> 前端逐步显示
但要注意:不是所有 Runnable 都天然保持流式。
如果中间插入了一个普通 RunnableLambda:
python
chain = prompt | model | RunnableLambda(post_process) | parser
它可能会等上游完整输出结束后才执行,从而让流式效果变差。
所以设计流式链路时,要格外关注中间组件是否支持流式 transform。
ainvoke / abatch / astream:异步调用
异步版本适合 Web 服务、并发任务和异步框架。
python
result = await chain.ainvoke({"topic": "LCEL"})
批量异步:
python
results = await chain.abatch([
{"topic": "LCEL"},
{"topic": "Runnable"},
])
异步流:
python
async for chunk in chain.astream({"topic": "LCEL"}):
print(chunk, end="", flush=True)
如果你在 FastAPI、Starlette、异步任务队列里使用 LangChain,异步接口会更自然。
但不要为了异步而异步。简单脚本和学习 Demo,用同步 invoke 更容易调试。
with_config:给链加运行配置
Runnable 支持运行配置。最常见的是:
run_nametagsmetadatamax_concurrency
示例:
python
result = chain.invoke(
{"topic": "LCEL"},
config={
"run_name": "explain_lcel_chain",
"tags": ["tutorial", "lcel"],
"metadata": {
"article": "05",
"env": "dev",
},
},
)
如果你接入 LangSmith,这些信息会帮助你追踪和过滤运行记录。
也可以提前绑定配置:
python
named_chain = chain.with_config(
run_name="explain_lcel_chain",
tags=["tutorial", "lcel"],
)
result = named_chain.invoke({"topic": "LCEL"})
with_config 不改变业务逻辑,但会让链更容易观测和调试。
with_retry:给链加重试
模型调用可能因为网络抖动、限流、服务端错误失败。
可以给 Runnable 添加重试:
python
reliable_chain = chain.with_retry()
result = reliable_chain.invoke({"topic": "LCEL"})
更完整的重试策略要结合具体错误类型、次数和业务幂等性。
适合重试的场景:
- 临时网络错误。
- 服务端 5xx。
- 供应商限流后等待重试。
- 偶发超时。
不适合盲目重试的场景:
- API Key 错误。
- 模型名写错。
- Prompt 变量缺失。
- 输入数据格式错误。
- 触发外部副作用的工具调用。
重试只能提高临时故障下的成功率,不能修复代码错误和业务设计错误。
with_fallbacks:主链失败时走备用链
如果主模型失败,可以切备用模型。
python
primary_chain = prompt | primary_model | StrOutputParser()
backup_chain = prompt | backup_model | StrOutputParser()
chain = primary_chain.with_fallbacks([backup_chain])
result = chain.invoke({"topic": "LCEL"})
这比自己写一个简单 try-except 更贴近 Runnable 体系。
但 fallback 也要谨慎:
- 主模型和备用模型输出格式要一致。
- 如果链里有工具调用,要确认备用模型也支持工具调用。
- 需要记录最终命中了哪个模型。
- 只应该对可恢复错误 fallback。
- 不要把 prompt 变量错误这种开发期问题隐藏掉。
fallback 的目的是提高服务可用性,不是掩盖系统缺陷。
bind:给模型绑定固定参数
有些参数你希望在某条链里固定。
例如给模型绑定停止词:
python
bound_model = model.bind(stop=["\n\n"])
chain = prompt | bound_model | StrOutputParser()
bind 会返回一个新的 Runnable,不会修改原始模型对象。
适合:
- 绑定供应商特定参数。
- 绑定停止词。
- 绑定某些调用配置。
但供应商参数差异较大,生产环境要确认目标模型是否支持对应参数。
pick 和 assign:操作字典字段
LCEL 里经常处理字典。
假设输入是:
python
data = {
"question": "LCEL 是什么?",
"user_id": "u_001",
"trace_id": "t_001",
}
如果 Prompt 只需要 question,可以用 pick:
python
question_chain = RunnablePassthrough().pick("question")
print(question_chain.invoke(data))
输出:
text
LCEL 是什么?
如果要在原始字典上追加字段,用 assign:
python
chain = RunnablePassthrough.assign(
question_length=RunnableLambda(lambda x: len(x["question"]))
)
print(chain.invoke(data))
输出:
python
{
"question": "LCEL 是什么?",
"user_id": "u_001",
"trace_id": "t_001",
"question_length": 8,
}
这类字段操作在 RAG、日志、权限、引用来源处理里非常常见。
RAG 预演:用 LCEL 串起检索和生成
虽然 RAG 会在下一篇详细讲,这里先看一个 LCEL 视角的 RAG 骨架。
python
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
prompt = ChatPromptTemplate.from_messages([
(
"system",
"你是一个知识库问答助手。只能根据提供的资料回答。"
"如果资料中没有答案,请说无法确定。"
),
(
"human",
"资料:\n{context}\n\n"
"问题:{question}"
),
])
rag_chain = (
{
"context": retriever,
"question": RunnablePassthrough(),
}
| prompt
| model
| StrOutputParser()
)
answer = rag_chain.invoke("LangChain 的 Runnable 是什么?")
print(answer)
这条链最关键的是第一段:
python
{
"context": retriever,
"question": RunnablePassthrough(),
}
同一个字符串输入会分成两路:
- 交给
retriever检索文档,得到context。 - 原样透传,得到
question。
然后合并成:
python
{
"context": [...],
"question": "LangChain 的 Runnable 是什么?"
}
再送给 Prompt。
这就是 LCEL 的威力:RAG 看起来是复杂流程,但在数据流层面就是"分支、合并、顺序执行"。
多步骤处理:先改写问题,再检索,再回答
真实 RAG 里,用户问题可能不适合直接检索。
例如多轮对话中:
text
用户:LangChain 的 Runnable 是什么?
用户:那它和 Chain 有什么区别?
第二句里的"它"需要被改写成完整问题。
LCEL 可以这样组织:
python
rewrite_prompt = ChatPromptTemplate.from_messages([
(
"system",
"你负责把用户问题改写成适合检索的独立问题。只输出改写后的问题。"
),
("human", "历史对话:{history}\n当前问题:{question}"),
])
rewrite_chain = rewrite_prompt | model | StrOutputParser()
然后把改写结果送入检索:
python
retrieval_chain = (
{
"standalone_question": rewrite_chain,
"original_question": RunnablePassthrough(),
}
| RunnablePassthrough.assign(
context=RunnableLambda(lambda x: retriever.invoke(x["standalone_question"]))
)
)
再接回答 Prompt:
python
answer_prompt = ChatPromptTemplate.from_messages([
("system", "你只能根据资料回答问题。"),
(
"human",
"资料:\n{context}\n\n"
"原始问题:{original_question}\n"
"改写问题:{standalone_question}"
),
])
answer_chain = retrieval_chain | answer_prompt | model | StrOutputParser()
这个例子不一定是最优实现,但它展示了 LCEL 的表达力:
- 先生成中间字段。
- 再基于中间字段执行检索。
- 保留原始问题。
- 最后组合上下文生成答案。
当流程继续变复杂,或者需要循环、条件边、人工审批、持久状态时,就应该考虑 LangGraph。
LCEL 和普通 Python 函数有什么区别?
你当然可以不用 LCEL,直接写 Python:
python
messages = prompt.invoke(input_data)
response = model.invoke(messages)
result = parser.invoke(response)
这没有错。
但 LCEL 提供了一些额外收益:
| 能力 | 普通 Python 串调用 | LCEL |
|---|---|---|
| 顺序组合 | 手写中间变量 | `prompt |
| 并行分支 | 手写并发和合并 | RunnableParallel / dict |
| 批处理 | 自己写循环和并发 | batch / abatch |
| 流式输出 | 自己处理每层 stream | stream / astream |
| 重试 | 自己封装 | with_retry |
| fallback | 自己写 try-except | with_fallbacks |
| 追踪 | 自己打日志 | 与 LangSmith / callbacks 体系集成 |
| 复用 | 需要手动封装函数 | 组合结果仍是 Runnable |
如果只是两三行脚本,普通 Python 更直接。
如果你在构建一个会持续演进的 LLM 应用,LCEL 会让流程结构更清晰。
LCEL 和 LangGraph 怎么选?
LCEL 适合线性或有少量分支的无状态流程。
典型场景:
- Prompt -> Model -> Parser。
- 检索 -> Prompt -> Model。
- 多个分支并行生成结果。
- 批量文本处理。
- 简单模型路由。
LangGraph 适合更复杂的状态图和循环控制。
典型场景:
- Agent 多轮工具调用。
- 条件跳转。
- 循环反思。
- 人工审批。
- 长时间运行任务。
- 持久化状态。
- 多 Agent 协作。
可以这样判断:
| 问题 | 更适合 |
|---|---|
| 流程基本固定,只是串几个步骤 | LCEL |
| 有并行分支,但没有复杂状态机 | LCEL |
| 需要循环、条件边、状态持久化 | LangGraph |
| 需要人工中断和恢复 | LangGraph |
| 需要多 Agent 协作 | LangGraph |
LCEL 是数据管道,LangGraph 是状态图引擎。
调试技巧一:逐段 invoke
链出问题时,不要只看最终错误。
逐段调试:
python
input_data = {"topic": "LCEL"}
prompt_output = prompt.invoke(input_data)
print(prompt_output)
model_output = model.invoke(prompt_output)
print(model_output)
parser_output = parser.invoke(model_output)
print(parser_output)
确认每一步的输出都符合下一步的输入要求。
如果逐段都没问题,再组合:
python
chain = prompt | model | parser
print(chain.invoke(input_data))
这种方式对排查 Prompt 变量缺失、模型输出格式异常、Parser 解析失败特别有用。
调试技巧二:打印中间数据形状
可以插入 RunnableLambda 打印中间值:
python
from langchain_core.runnables import RunnableLambda
def debug_print(x):
print("DEBUG:", type(x), x)
return x
debug = RunnableLambda(debug_print)
chain = prompt | debug | model | parser
这适合本地调试。
生产环境不要用 print 打敏感数据,应该接入日志系统,并注意脱敏。
调试技巧三:给关键步骤命名
用 with_config 给链或子链命名:
python
prompt_step = prompt.with_config(run_name="build_prompt")
model_step = model.with_config(run_name="call_model")
parser_step = parser.with_config(run_name="parse_output")
chain = prompt_step | model_step | parser_step
如果使用 LangSmith,命名清楚的步骤更容易定位问题。
不要等链路复杂到看不懂时才开始命名。关键步骤从一开始就应该有语义化名称。
常见错误一:Prompt 输入变量对不上
Prompt 定义:
python
prompt = ChatPromptTemplate.from_template("解释一下:{topic}")
错误调用:
python
chain.invoke({"question": "LCEL"})
这里传的是 question,但 Prompt 需要的是 topic。
正确调用:
python
chain.invoke({"topic": "LCEL"})
排查方法:
python
print(prompt.input_variables)
先确认 Prompt 到底需要哪些变量。
常见错误二:并行分支输出字段名和 Prompt 不一致
例如:
python
chain = {
"docs": retriever,
"query": RunnablePassthrough(),
} | prompt
但 Prompt 写的是:
python
prompt = ChatPromptTemplate.from_template(
"资料:{context}\n问题:{question}"
)
这就对不上。
要么改分支字段:
python
chain = {
"context": retriever,
"question": RunnablePassthrough(),
} | prompt
要么改 Prompt 变量:
python
prompt = ChatPromptTemplate.from_template(
"资料:{docs}\n问题:{query}"
)
LCEL 调试时,字段名比你想象中更重要。
常见错误三:把 RunnableLambda 写得太重
错误倾向:
python
chain = (
prompt
| model
| RunnableLambda(lambda x: save_to_db(call_api(parse_json(x))))
)
这类链很难测试、很难追踪,也容易把副作用藏起来。
更好的方式:
- 解析逻辑单独写函数。
- 外部 API 调用封装成 Tool 或服务。
- 写数据库操作放在明确的业务层。
- 对副作用操作做幂等设计。
LCEL 适合表达数据流,不应该变成隐藏复杂业务副作用的地方。
常见错误四:过早追求一行链
很多人喜欢把所有东西写成一行:
python
chain = {"context": retriever, "question": RunnablePassthrough()} | prompt | model | StrOutputParser()
这没错,但链复杂后可读性会下降。
可以拆开:
python
prepare_inputs = {
"context": retriever,
"question": RunnablePassthrough(),
}
generation_chain = prompt | model | StrOutputParser()
rag_chain = prepare_inputs | generation_chain
拆开之后更容易:
- 单独测试输入准备。
- 单独替换生成链。
- 给每段加
with_config。 - 定位错误。
LCEL 不是为了炫一行代码,而是为了清晰表达数据流。
常见错误五:以为 batch 一定更快
batch 通常比自己简单循环更方便,也可能更快。
但它不保证在所有场景都更快。
影响因素包括:
- 模型供应商限流。
- 网络延迟。
max_concurrency设置。- 每条输入长度。
- 链中是否有阻塞组件。
- 是否触发重试。
如果你要处理大批量数据,建议小批次实验:
python
for i in range(0, len(inputs), 20):
batch_inputs = inputs[i:i + 20]
results = chain.batch(
batch_inputs,
config={"max_concurrency": 5},
)
生产环境还要记录失败项,支持重跑。
完整 Demo:LCEL 多分支文章助手
下面给一个完整 Demo:输入一个技术主题,同时生成标题、简介、提纲,最后组装成一个字典。
目录:
text
lcel-demo/
.env
main.py
.env:
bash
OPENAI_API_KEY=sk-xxxx
main.py:
python
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda, RunnableParallel, RunnablePassthrough
load_dotenv()
model = init_chat_model(
"gpt-4o-mini",
model_provider="openai",
temperature=0,
)
parser = StrOutputParser()
title_chain = (
ChatPromptTemplate.from_template(
"请为技术主题《{topic}》生成 3 个 CSDN 文章标题,只输出标题列表。"
)
| model
| parser
)
intro_chain = (
ChatPromptTemplate.from_template(
"请为技术主题《{topic}》写一段 120 字以内的文章简介。"
)
| model
| parser
)
outline_chain = (
ChatPromptTemplate.from_template(
"请为技术主题《{topic}》生成 5 个二级标题。"
)
| model
| parser
)
parallel_chain = RunnableParallel(
topic=RunnablePassthrough(),
titles=title_chain,
intro=intro_chain,
outline=outline_chain,
)
def add_metadata(data: dict) -> dict:
return {
**data,
"series": "LangChain 从入门到源码",
"stage": "核心能力篇",
}
chain = parallel_chain | RunnableLambda(add_metadata)
result = chain.invoke("LCEL 表达式语言")
print(result["topic"])
print(result["titles"])
print(result["intro"])
print(result["outline"])
print(result["series"])
print(result["stage"])
这条链的数据流:
#mermaid-svg-EVCx1w0YL3nvmXfp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EVCx1w0YL3nvmXfp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EVCx1w0YL3nvmXfp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EVCx1w0YL3nvmXfp .error-icon{fill:#552222;}#mermaid-svg-EVCx1w0YL3nvmXfp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EVCx1w0YL3nvmXfp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EVCx1w0YL3nvmXfp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EVCx1w0YL3nvmXfp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EVCx1w0YL3nvmXfp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EVCx1w0YL3nvmXfp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EVCx1w0YL3nvmXfp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EVCx1w0YL3nvmXfp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EVCx1w0YL3nvmXfp .marker.cross{stroke:#333333;}#mermaid-svg-EVCx1w0YL3nvmXfp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EVCx1w0YL3nvmXfp p{margin:0;}#mermaid-svg-EVCx1w0YL3nvmXfp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EVCx1w0YL3nvmXfp .cluster-label text{fill:#333;}#mermaid-svg-EVCx1w0YL3nvmXfp .cluster-label span{color:#333;}#mermaid-svg-EVCx1w0YL3nvmXfp .cluster-label span p{background-color:transparent;}#mermaid-svg-EVCx1w0YL3nvmXfp .label text,#mermaid-svg-EVCx1w0YL3nvmXfp span{fill:#333;color:#333;}#mermaid-svg-EVCx1w0YL3nvmXfp .node rect,#mermaid-svg-EVCx1w0YL3nvmXfp .node circle,#mermaid-svg-EVCx1w0YL3nvmXfp .node ellipse,#mermaid-svg-EVCx1w0YL3nvmXfp .node polygon,#mermaid-svg-EVCx1w0YL3nvmXfp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EVCx1w0YL3nvmXfp .rough-node .label text,#mermaid-svg-EVCx1w0YL3nvmXfp .node .label text,#mermaid-svg-EVCx1w0YL3nvmXfp .image-shape .label,#mermaid-svg-EVCx1w0YL3nvmXfp .icon-shape .label{text-anchor:middle;}#mermaid-svg-EVCx1w0YL3nvmXfp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EVCx1w0YL3nvmXfp .rough-node .label,#mermaid-svg-EVCx1w0YL3nvmXfp .node .label,#mermaid-svg-EVCx1w0YL3nvmXfp .image-shape .label,#mermaid-svg-EVCx1w0YL3nvmXfp .icon-shape .label{text-align:center;}#mermaid-svg-EVCx1w0YL3nvmXfp .node.clickable{cursor:pointer;}#mermaid-svg-EVCx1w0YL3nvmXfp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EVCx1w0YL3nvmXfp .arrowheadPath{fill:#333333;}#mermaid-svg-EVCx1w0YL3nvmXfp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EVCx1w0YL3nvmXfp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EVCx1w0YL3nvmXfp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EVCx1w0YL3nvmXfp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EVCx1w0YL3nvmXfp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EVCx1w0YL3nvmXfp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EVCx1w0YL3nvmXfp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EVCx1w0YL3nvmXfp .cluster text{fill:#333;}#mermaid-svg-EVCx1w0YL3nvmXfp .cluster span{color:#333;}#mermaid-svg-EVCx1w0YL3nvmXfp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EVCx1w0YL3nvmXfp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EVCx1w0YL3nvmXfp rect.text{fill:none;stroke-width:0;}#mermaid-svg-EVCx1w0YL3nvmXfp .icon-shape,#mermaid-svg-EVCx1w0YL3nvmXfp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EVCx1w0YL3nvmXfp .icon-shape p,#mermaid-svg-EVCx1w0YL3nvmXfp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EVCx1w0YL3nvmXfp .icon-shape .label rect,#mermaid-svg-EVCx1w0YL3nvmXfp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EVCx1w0YL3nvmXfp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EVCx1w0YL3nvmXfp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EVCx1w0YL3nvmXfp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 主题字符串
RunnableParallel
topic 原样透传
title_chain
intro_chain
outline_chain
结果字典
RunnableLambda: add_metadata
最终字典
这个 Demo 覆盖了:
RunnablePassthrough保留原始输入。RunnableParallel并行执行多个分支。prompt | model | parser构造子链。RunnableLambda做后处理。- 组合后的链继续使用
invoke。
这就是 LCEL 的典型使用方式。
总结
LCEL 到底解决了什么?如果只记住一句话:
LCEL 是 LangChain 用来声明和组合数据流的表达式语言,Runnable 是其中最核心的统一执行接口。
再具体一点:
Runnable是可调用、可批处理、可流式、可组合的工作单元。RunnableSequence表示顺序执行,上一步输出接下一步输入。|运算符是构造顺序链的常见写法。RunnableParallel表示并行分支,同一份输入进入多个 Runnable。- 字典字面量在 LCEL 链里经常被用来表达并行字段构造。
RunnablePassthrough用来保留原始输入。assign用来在原始字典上追加字段。RunnableLambda可以把普通 Python 函数接入链路。invoke用于单次调用,batch用于批量处理,stream用于流式输出。with_config、with_retry、with_fallbacks让链更容易观测和增强可靠性。- 学 LCEL 最重要的是画清楚每一步的输入输出形状。