概述
前面第 5 篇我们讲过 LCEL:
python
chain = prompt | model | parser
result = chain.invoke({"topic": "Runnable"})
这行代码看起来很轻,但背后压着 LangChain 最核心的一层设计:Runnable。
如果只从使用者视角看,它像是一套链式调用 API。
但从源码视角看,Runnable 至少解决了五个问题:
- 不同组件如何拥有统一调用接口?
|管道语法如何把多个组件组合成一个新组件?- 为什么组合后的
chain仍然可以继续invoke()、stream()、batch()? - 普通 Python 函数为什么可以接进 LCEL?
- tracing、metadata、并发控制、异常处理这些运行时信息如何贯穿整个链路?
本文会围绕 langchain-core 的 Runnable 源码展开,重点看:
Runnable抽象类的职责。__or__运算符重载如何生成RunnableSequence。invoke()、stream()、batch()的执行差异。RunnableSequence如何把多个步骤串起来。RunnableLambda如何把普通函数包装成 Runnable。RunnableConfig如何把 callbacks、tags、metadata、max_concurrency 传下去。
LCEL 的本质不是"用竖线少写几行代码",而是用 Runnable 把 LLM 应用中的组件统一成可组合、可批处理、可流式、可追踪的执行单元。
源码入口:先找到 Runnable 在哪里
读这篇源码时,先记住几个路径。
以 LangChain 官方仓库为准,核心代码主要在:
text
libs/core/langchain_core/runnables/base.py
这个文件里包含几类关键对象:
| 对象 | 作用 |
|---|---|
Runnable |
所有可执行组件的抽象基类 |
RunnableSerializable |
支持序列化和配置化的 Runnable 基类 |
RunnableSequence |
顺序组合多个 Runnable |
RunnableParallel |
并行执行多个 Runnable |
RunnableLambda |
把普通函数包装成 Runnable |
RunnableGenerator |
包装生成器函数,适合流式转换 |
coerce_to_runnable() |
把函数、dict、Runnable-like 对象转换成 Runnable |
相关辅助逻辑主要在:
text
libs/core/langchain_core/runnables/config.py
libs/core/langchain_core/runnables/utils.py
libs/core/langchain_core/runnables/graph.py
其中:
config.py: 负责RunnableConfig、callback manager、executor、config 合并。utils.py: 负责函数签名判断、schema 推断、并发工具等。graph.py: 负责把 Runnable 链路转换成图结构,供可视化、调试和 tracing 使用。
源码阅读顺序建议是:
text
Runnable
-> __or__ / pipe
-> coerce_to_runnable
-> RunnableSequence
-> RunnableLambda
-> batch / stream / transform
-> RunnableConfig
不要一上来就读 RunnableSequence.batch(),它里面有 callback、异常、并发、config 分发,会显得很绕。
先读 Runnable 的公共协议,再读组合类和包装类,最后读 config、callback、streaming 这些横切逻辑。
Runnable 的心智模型:一个可执行、可组合、可观测的工作单元
先看一个最小 Runnable:
python
from langchain_core.runnables import RunnableLambda
def add_one(x: int) -> int:
return x + 1
r = RunnableLambda(add_one)
print(r.invoke(1)) # 2
print(r.batch([1, 2, 3])) # [2, 3, 4]
从源码角度看,Runnable 不是"链"的意思,而是"可执行单元"。
这个可执行单元至少有三层能力:
| 能力 | 方法 | 含义 |
|---|---|---|
| 单次调用 | invoke() / ainvoke() |
一个输入变一个输出 |
| 批量调用 | batch() / abatch() |
多个输入变多个输出 |
| 流式调用 | stream() / astream() |
一个输入逐步产出多个 chunk |
| 事件流 | astream_events() |
观察中间步骤和运行事件 |
| 日志流 | astream_log() |
观察运行日志和 patch |
| 组合 | __or__() / pipe() |
拼成新的 Runnable |
| 配置 | with_config() / bind() |
注入配置或绑定参数 |
| 增强 | with_retry() / with_fallbacks() |
增加重试和降级 |
| 工具化 | as_tool() |
转成 tool |
这就是为什么 prompt、model、parser 看起来类型完全不同,却都能被 | 拼起来。
它们都遵守一个最小协议:
text
Input -> Runnable -> Output
一旦遵守这个协议,就可以进入 LCEL 组合系统。
Runnable 抽象类:真正必须实现的是 invoke
从源码结构看,Runnable 是一个泛型抽象类:
python
Runnable[Input, Output]
你可以把它理解成:
text
接收 Input 类型
输出 Output 类型
它的核心抽象方法是 invoke()。
简化理解如下:
python
class Runnable(Generic[Input, Output]):
def invoke(self, input: Input, config: RunnableConfig | None = None, **kwargs) -> Output:
...
源码里的真实实现会更复杂,因为它要处理:
- 类型推断。
- Pydantic schema。
- config schema。
- callback manager。
- tracing。
- sync / async 适配。
- streaming。
- batch 并发。
- 序列化。
但从阅读入口看,先抓住一个重点:
对一个最小 Runnable 来说,
invoke()是必须回答的问题:给我一个输入,我如何产出一个输出?
其他能力很多都有默认实现。
例如:
- 默认
ainvoke()可以把同步invoke()放进 executor 里跑。 - 默认
batch()可以对多个输入并发调用invoke()。 - 默认
stream()可以退化成只产出一次invoke()的结果。
这套默认实现非常关键。
它意味着:只要一个组件实现了最基础的 invoke(),就立刻获得了异步、批处理、流式协议的基本形态。
invoke() 是 Runnable 的最小执行语义,ainvoke()、batch()、stream() 则是在这个语义上的通用运行模式。
__or__:竖线运算符如何生成 RunnableSequence
我们最熟悉的写法是:
python
chain = prompt | model | parser
Python 里 | 是运算符。LangChain 让它能用于 Runnable,是因为 Runnable 重载了 __or__()。
源码可以简化成这样的逻辑:
python
def __or__(self, other):
return RunnableSequence(
self,
coerce_to_runnable(other),
)
也就是说:
python
prompt | model
会变成:
python
RunnableSequence(prompt, model)
再继续:
python
prompt | model | parser
可以理解成:
python
RunnableSequence(
RunnableSequence(prompt, model),
parser,
)
实际源码会对嵌套 sequence 做整理,保证最终步骤列表更平整,但心智模型就是这样。
__ror__:为什么 dict 也能放到管道里?
你可能见过这种写法:
python
chain = {
"question": RunnablePassthrough(),
"context": retriever,
} | prompt | model
这里左边是一个 dict,它本身不是 Runnable。
能工作是因为 Runnable 还实现了反向运算符 __ror__()。
当 Python 发现左操作数处理不了 |,就会尝试右操作数的 __ror__()。
简化理解:
python
def __ror__(self, other):
return RunnableSequence(
coerce_to_runnable(other),
self,
)
所以:
python
some_dict | prompt
会先把 some_dict 转成 Runnable,再和 prompt 组成 RunnableSequence。
coerce_to_runnable():Runnable-like 对象的入口
coerce_to_runnable() 是 LCEL 很重要的"入口转换器"。
它负责把这些对象转换成真正的 Runnable:
| 传入对象 | 转换结果 |
|---|---|
已经是 Runnable |
原样返回 |
| 普通函数 | RunnableLambda |
| 生成器函数 | RunnableGenerator |
dict |
RunnableParallel |
| 不支持的对象 | 抛出类型错误 |
这就是为什么 LCEL 里可以混用:
python
from langchain_core.runnables import RunnableLambda
chain = (
RunnableLambda(lambda x: x + 1)
| (lambda x: x * 2)
| {"value": lambda x: x, "text": str}
)
上面写法中,后两个对象不是显式 Runnable,但都会被转换。
| 的核心不是"把两个对象连起来",而是"先把右侧对象规范化成 Runnable,再生成一个新的 RunnableSequence"。
RunnableSequence:链式组合的内部结构
RunnableSequence 是 LangChain 里最重要的组合类。
它代表:
text
step1 -> step2 -> step3 -> ... -> stepN
每一步都是 Runnable。
例如:
python
chain = prompt | model | parser
可以理解成:
text
RunnableSequence
first: prompt
middle: [model]
last: parser
或者更直观地理解成:
text
steps = [prompt, model, parser]
invoke():顺序执行每一步
RunnableSequence.invoke() 的核心逻辑非常直接。
伪代码如下:
python
def invoke(input, config=None):
value = input
for step in steps:
value = step.invoke(value, child_config)
return value
所以这条链:
python
chain = prompt | model | parser
执行时就是:
text
原始输入 dict
|
v
prompt.invoke(...)
|
v
PromptValue / messages
|
v
model.invoke(...)
|
v
AIMessage
|
v
parser.invoke(...)
|
v
字符串 / 结构化对象
你可以把 RunnableSequence 当成一个"for 循环封装器",但它比普通 for 循环多做了很多事情:
- 给每个 step 分配子 callback。
- 合并和传递 config。
- 维护 run name。
- 捕获错误并上报 tracing。
- 让整个 sequence 仍然是一个 Runnable。
- 让 sequence 自动继承
batch()、stream()、ainvoke()等能力。
这就是 LCEL 的价值:组合之后仍然符合同一个协议。
子 run:为什么 trace 里有 seq:step:1
读源码时会看到类似这样的概念:
text
seq:step:1
seq:step:2
seq:step:3
这不是业务逻辑,而是 tracing 逻辑。
一个 RunnableSequence 本身是一个 root run,它里面的每个 step 都是 child run。
结构类似:
text
RunnableSequence run
|
|-- seq:step:1 prompt
|-- seq:step:2 model
|-- seq:step:3 parser
这也是为什么 LangSmith 或 ConsoleCallbackHandler 可以看到中间步骤。
如果没有这层 callback 分发,chain.invoke() 在外部看起来就只是一个黑盒。
RunnableSequence.invoke() 的本质是顺序调用每个 step,但源码里额外处理了 config、callback、trace、异常和子步骤命名。
stream():流式输出不是所有步骤都天然支持
很多人以为只要调用:
python
for chunk in chain.stream(input):
print(chunk)
就一定会从第一步开始一路流式输出。
源码层面并不是这么简单。
stream() 背后更核心的方法是 transform()。
可以这样理解:
text
stream(input)
-> 把单个 input 包装成 iterator
-> 调用 transform(iterator)
-> 逐个 yield output chunk
对 RunnableSequence 来说,流式数据会沿着每个 step 的 transform() 往后传:
text
input iterator
-> step1.transform(...)
-> step2.transform(...)
-> step3.transform(...)
-> output iterator
关键点在这里:
如果中间某个 step 不支持真正的流式 transform,它可能会先把输入缓存起来,等输入完整后再产出结果。
所以流式链路是否真的"边生成边输出",取决于链上每一步是否支持流式。
例如:
python
chain = prompt | streaming_model | parser
如果:
prompt很快产出完整 messages。streaming_model支持 token/chunk 流式输出。parser支持逐 chunk 解析。
那么整个链路就能比较自然地流式输出。
但如果中间插入一个普通 RunnableLambda:
python
chain = prompt | model | RunnableLambda(lambda x: expensive_parse(x))
它可能需要等模型输出完整后才执行解析,这会打断前面的流式体验。
RunnableLambda 的流式局限
普通函数包装成 RunnableLambda 时,通常是:
text
收完整输入 -> 调函数 -> 产一个输出
它并不天然适合逐 chunk 处理。
如果你需要真正的流式转换,更应该考虑生成器函数或专门实现支持 transform() 的 Runnable。
stream() 是协议层能力,但真实流式效果取决于链上每个步骤是否支持 transform(),普通同步函数经常会让流式链路变成"先缓存、后输出"。
batch():默认并发调用 invoke,而不是神秘批处理
batch() 的使用方式很简单:
python
outputs = chain.batch([
{"topic": "Runnable"},
{"topic": "RunnableSequence"},
{"topic": "RunnableLambda"},
])
很多人会误以为 batch() 一定会调用模型供应商的批量 API。
源码默认逻辑不是这样。
官方 reference 和源码都表明:默认 batch() 会并发执行多次 invoke(),适合 IO 密集型 Runnable。
简化伪代码:
python
def batch(inputs, config=None, return_exceptions=False):
configs = get_config_list(config, len(inputs))
def one(input, config):
return self.invoke(input, config)
return executor.map(one, inputs, configs)
它的关键点有三个:
- 一个 input 对应一个 config。
- 多个 input 默认并发执行。
- 如果底层 API 有真正批量接口,子类应该重写
batch()。
比如对普通 HTTP 调用来说,默认并发已经有价值。
但如果某个 embeddings provider 提供了原生 batch endpoint,那么最佳实现应该直接覆盖 batch(),一次请求处理多个文本,而不是并发发很多小请求。
max_concurrency:并发不是无限开
RunnableConfig 里有一个关键字段:
python
config = {
"max_concurrency": 5,
}
它用于控制并发数量。
例如:
python
results = chain.batch(
[{"topic": t} for t in topics],
config={"max_concurrency": 5},
)
这在调用外部模型、搜索 API、数据库、爬虫工具时很重要。
否则你很容易遇到:
- provider rate limit。
- 数据库连接池耗尽。
- 本地 CPU 线程过多。
- 请求排队导致延迟抖动。
return_exceptions:批处理中如何处理单条失败
batch() 还有一个常见参数:
python
return_exceptions=True
如果它是 False,某个输入失败通常会抛出异常。
如果它是 True,失败项会作为异常对象放回结果列表中。
例如:
python
outputs = chain.batch(inputs, return_exceptions=True)
for item in outputs:
if isinstance(item, Exception):
print("failed:", item)
else:
print("ok:", item)
这在批量处理文档、批量抽取结构化信息、批量生成摘要时很实用。
默认 batch() 不是供应商原生批量 API,而是并发执行多个 invoke();真正高效的批量接口需要具体 Runnable 自己重写。
RunnableSequence.batch:批处理链路如何逐步推进
单个 Runnable 的 batch() 是并发执行多个 invoke()。
但 RunnableSequence 的 batch() 更有意思。
它不是简单地对每个输入完整执行一遍全链路,而是按 step 推进:
text
inputs = [a, b, c]
step1.batch(inputs)
-> [a1, b1, c1]
step2.batch([a1, b1, c1])
-> [a2, b2, c2]
step3.batch([a2, b2, c2])
-> [a3, b3, c3]
这样做有两个好处:
- 每个 step 都有机会使用自己的批处理优化。
- callback 和 tracing 可以按步骤组织。
例如:
python
chain = prompt | model | parser
执行 chain.batch(inputs) 时,大致是:
text
prompt.batch(inputs)
-> messages_list
model.batch(messages_list)
-> ai_messages
parser.batch(ai_messages)
-> parsed_outputs
如果 model.batch() 有供应商侧优化,它就可以在这一层发挥作用。
如果没有,它也会退回到并发 model.invoke()。
批处理中的异常传播
当 return_exceptions=True 时,RunnableSequence.batch() 还需要处理一个问题:
某个输入在 step1 已经失败了,还要不要进入 step2?
合理做法是:失败项不再进入后续 step,但最终结果列表仍然保持输入顺序。
所以源码里会维护失败输入的位置,再把成功结果和异常结果重新组装回原来的顺序。
这也是为什么 RunnableSequence.batch() 的源码比单个 Runnable 的 batch() 更长。
它不仅要"批量执行",还要保证:
- 顺序不乱。
- 异常位置不丢。
- 成功项继续往后跑。
- 每个 step 都有自己的 child callback。
RunnableSequence.batch() 是按步骤批量推进,而不是简单地把整条链复制 N 份并发跑。
RunnableLambda:普通函数如何变成 Runnable
RunnableLambda 是 LCEL 非常实用的一层适配器。
它让普通 Python 函数可以进入 Runnable 世界:
python
from langchain_core.runnables import RunnableLambda
def normalize_topic(topic: str) -> dict:
return {"topic": topic.strip()}
chain = RunnableLambda(normalize_topic) | prompt | model | parser
源码上,它主要做几件事:
- 保存原始函数。
- 判断函数是同步还是异步。
- 推断输入输出类型和 schema。
- 调用时把
input、config、run_manager等按函数签名注入。 - 如果函数返回的还是 Runnable,则继续执行这个 Runnable。
- 如果函数是 generator,则支持按 chunk 产出。
函数签名注入:为什么有些函数能接收 config
你可以写:
python
from langchain_core.runnables import RunnableConfig, RunnableLambda
def add_trace_prefix(x: str, config: RunnableConfig) -> str:
run_name = config.get("run_name", "unnamed")
return f"[{run_name}] {x}"
r = RunnableLambda(add_trace_prefix)
print(r.invoke("hello", config={"run_name": "demo"}))
不是所有函数都必须接收 config。
RunnableLambda 会根据函数签名判断是否传入这些额外参数。
常见可注入参数包括:
configrun_manager
这让普通函数在需要时可以参与 tracing、读取配置、派生 callback。
返回 Runnable:动态路由的基础
RunnableLambda 还有一个很重要的能力:如果函数返回一个 Runnable,它会继续调用返回的 Runnable。
这可以用来做动态路由:
python
from langchain_core.runnables import RunnableLambda
def route(question: str):
if "SQL" in question:
return sql_chain
return rag_chain
router = RunnableLambda(route)
result = router.invoke("请用 SQL 查询订单数量")
心智模型是:
text
route(input) -> 返回某条 chain -> 执行这条 chain
源码里会用 recursion_limit 防止无限返回 Runnable 导致递归失控。
RunnableLambda 是普通 Python 世界和 Runnable 世界之间的桥,它让函数、动态路由、小型转换逻辑都能进入 LCEL。*
RunnableParallel:dict 为什么代表并行分支
虽然第这篇重点是 RunnableSequence,但理解 dict 自动变成 RunnableParallel 很重要。
比如 RAG 里常见写法:
python
from operator import itemgetter
from langchain_core.runnables import RunnablePassthrough
chain = {
"context": itemgetter("question") | retriever,
"question": itemgetter("question"),
} | prompt | model | parser
左侧 dict 会被 coerce_to_runnable() 转成 RunnableParallel。
它的语义是:
text
同一份输入
|
+-- context 分支
|
+-- question 分支
|
v
合并成 dict 输出
也就是:
text
input
-> {
"context": context_runnable.invoke(input),
"question": question_runnable.invoke(input),
}
这解释了为什么 LCEL 里经常用 dict 做字段组装。
它不是普通字典赋值,而是并行执行分支,并把多个分支结果合成一个字典。
在 LCEL 里,dict 通常不是静态数据,而是会被转换成 RunnableParallel 的并行分支声明。
RunnableConfig:为什么 config 能一路传到底
RunnableConfig 是理解生产级 LCEL 的关键。
它不是业务输入,而是运行时配置。
常见字段包括:
| 字段 | 作用 |
|---|---|
tags |
给 run 打标签,方便筛选 trace |
metadata |
附加运行元数据 |
callbacks |
注入 callback handler |
run_name |
指定当前 run 名称 |
max_concurrency |
控制 batch 并发数量 |
recursion_limit |
控制 Runnable 动态递归深度 |
configurable |
给可配置字段传值 |
run_id |
指定运行 ID |
调用时可以这样传:
python
result = chain.invoke(
{"topic": "Runnable"},
config={
"run_name": "runnable_article_demo",
"tags": ["csdn", "source-reading"],
"metadata": {"article": 22},
},
)
在 RunnableSequence 内部,config 不会简单原样塞给每一步。
它会被 patch、merge、派生 child callbacks。
大致结构是:
text
root config
|
+-- step1 child config
|
+-- step2 child config
|
+-- step3 child config
这使得 trace 可以形成树:
text
chain run
|
|-- prompt run
|-- model run
|-- parser run
如果你以后读 create_agent()、middleware、ToolNode、LangSmith tracing,都会看到这套 config/callback 机制。
业务数据走 input,运行控制走 config;Runnable 源码大量复杂性都来自 config、callback 和 tracing 的传递。
用一个例子串起 invoke、stream、batch
下面用一个不依赖模型的例子,把三种调用方式串起来。
python
from langchain_core.runnables import RunnableLambda
def clean_text(text: str) -> str:
return text.strip()
def to_words(text: str) -> list[str]:
return text.split()
def count_words(words: list[str]) -> int:
return len(words)
chain = (
RunnableLambda(clean_text)
| RunnableLambda(to_words)
| RunnableLambda(count_words)
)
print(chain.invoke(" hello langchain runnable "))
print(chain.batch([
"hello langchain",
"runnable sequence",
"invoke stream batch",
]))
执行 invoke():
text
" hello langchain runnable "
-> clean_text
-> "hello langchain runnable"
-> to_words
-> ["hello", "langchain", "runnable"]
-> count_words
-> 3
执行 batch():
text
step1.batch(["hello langchain", "runnable sequence", ...])
-> cleaned_texts
step2.batch(cleaned_texts)
-> word_lists
step3.batch(word_lists)
-> counts
执行 stream() 时,由于这里都是普通函数,实际不会有模型 token 那样的细粒度流式效果。通常会等某一步拿到完整输入后再产出。
如果想观察 stream() 更明显的效果,需要使用支持流式的模型或 generator Runnable。
源码阅读时要抓的关键问题
读 base.py 时,不建议逐行读完。
更高效的方式是带着问题读:
问题一:这个类到底是不是 Runnable?
判断标准:
text
能不能 invoke?
能不能 batch?
能不能 stream?
能不能被 | 组合?
问题二:这个对象是原生 Runnable,还是被转换出来的?
例如:
ChatPromptTemplate通常本身就是 Runnable。ChatModel通常本身就是 Runnable。- 普通函数会被转成
RunnableLambda。 - dict 会被转成
RunnableParallel。
问题三:这个链路的输入输出类型是什么?
源码中有 InputType、OutputType、input_schema、output_schema。
这些信息不仅用于文档,也用于校验、可视化、工具化、部署。
问题四:流式输出是在哪一步断掉的?
如果你发现:
python
chain.stream(input)
没有逐 token 输出,优先检查:
- 模型是否开启 streaming。
- 中间 parser 是否支持流式。
- 是否插入了普通
RunnableLambda。 - 是否某一步只实现了
invoke(),没有真正实现transform()。
问题五:批处理为什么没有变快?
优先检查:
- 是否被
max_concurrency限制。 - 是否底层 provider 本身限流。
- 是否某个 step 是 CPU 密集型。
- 是否具体 Runnable 没有重写真正的原生 batch。
- 是否 callback/tracing 开销过大。
常见误区
最后整理几个常见误区。
误区一:Runnable 等于 Chain
不准确。
Runnable 是统一执行接口,RunnableSequence 才是顺序链。
也就是说:
text
Runnable 是协议
RunnableSequence 是一种组合实现
误区二:| 只是 Python 语法糖
不准确。
| 会触发 __or__(),把右侧对象转换成 Runnable,并返回新的 RunnableSequence。
它改变了对象结构,不只是少写几行代码。
误区三:batch() 一定等于供应商批量 API
不准确。
默认 batch() 是并发调用 invoke()。
只有具体 Runnable 重写了 batch(),才可能使用供应商原生批量能力。
误区四:stream() 一定逐 token 输出
不准确。
stream() 是协议,真实输出粒度取决于链上每个 step 是否支持流式转换。
误区五:普通函数放进 LCEL 没有成本
不准确。
普通函数很方便,但它可能:
- 打断流式输出。
- 隐藏类型信息。
- 让异常位置不直观。
- 把复杂业务逻辑塞进匿名 lambda,降低可读性。
如果函数逻辑变复杂,建议显式命名,并用 RunnableLambda 包装。
总结
本文从源码视角拆了 Runnable 的核心设计。
你需要记住这几条主线:
Runnable是统一执行协议,核心是invoke(input, config)。__or__()会把两个 Runnable-like 对象组合成RunnableSequence。coerce_to_runnable()负责把函数、dict、generator 等对象转换成 Runnable。RunnableSequence.invoke()是顺序执行每个 step。RunnableSequence.batch()是按 step 批量推进,并保持结果顺序和异常位置。stream()依赖transform(),真实流式效果取决于每个 step 是否支持流式。RunnableLambda让普通 Python 函数进入 LCEL,但普通函数不天然等于高质量流式组件。RunnableConfig负责 tags、metadata、callbacks、run_name、max_concurrency 等运行时控制。
最后给一张源码阅读地图:
text
Runnable 抽象
|
|-- invoke / ainvoke
|-- batch / abatch
|-- stream / astream
|-- transform / atransform
|
+-- __or__ / __ror__ / pipe
|
v
RunnableSequence
|
+-- step1.invoke
+-- step2.invoke
+-- step3.invoke
coerce_to_runnable
|
|-- function -> RunnableLambda
|-- generator -> RunnableGenerator
|-- dict -> RunnableParallel
|-- Runnable -> 原样返回
RunnableConfig
|
|-- callbacks
|-- tags / metadata
|-- run_name
|-- max_concurrency
|-- recursion_limit
看懂 Runnable,就看懂了 LangChain 如何把 Prompt、Model、Parser、Retriever、Tool 和普通函数统一成一套可组合的执行系统。