9249 字
46 分钟
LangChain Runnable 源码解剖:从 prompt | model | parser 到统一执行协议

LangChain Core 源码学习路线:第 1 篇 Runnable 源码解剖#

核心问题: LangChain 如何用 Runnable 统一组件执行、组合、配置传播与分支调度?

源码主线: Runnable → RunnableSequence → invoke / batch / stream → Config / Callback

前置文章:

依赖基线: langchain-core==1.4.8

源码基线: https://github.com/langchain-ai/langchain/tree/15b0a4930b5306bb2174f16d92fe4b7837147d0a ,commit 15b0a4930b5306bb2174f16d92fe4b7837147d0a

阅读边界: 本篇覆盖 Runnable 协议及组合运行机制,不展开 Prompt、ChatModel、Tool 与 LangGraph 调度器内部实现。


0. 本篇在源码学习主线中的位置#

在 LangChain / LangGraph 的学习中,很多人会先背 API:

chain = prompt | model | parser
result = chain.invoke(input)

但源码级学习不能停留在“这样写能跑”。你真正要理解的是:

为什么 Prompt、Model、Parser、Retriever、Tool、普通 Python 函数,都可以被放进同一条链路?
为什么这些组件都能 invoke / stream / batch / ainvoke?
为什么一个 `|` 运算符就能把多个对象组合成一个可执行对象?
为什么 chain 本身组合后仍然是一个 Runnable,还能继续被组合、重试、打标签、追踪、并行执行?

答案就是 LangChain Core 的最核心抽象:Runnable

从 Agent 范式角度看,Runnable 不是一个具体 Agent 范式,而是 LangChain 里承载各种范式的“执行协议层”:

上层范式 / 能力LangChain Core 落点
普通 LLM ChainRunnableSequence
Prompt → Model → Parser`prompt
函数式预处理 / 后处理RunnableLambda
多路并行检索 / 多路模型调用RunnableParallel
条件分支RunnableBranch
原样透传输入 / 字典字段保留RunnablePassthrough
统一配置、追踪、重试RunnableConfig / with_config / with_retry

后面学习 Tool Calling、Retriever、Agent、LangGraph Node 时,都会反复遇到 Runnable。所以第一篇必须先把 Runnable 打透。


1. 本篇问题、学习目标与能力边界#

本章定义本文必须解决的问题,不提前展开实现细节。

掌握 LangChain 中最核心的统一执行抽象:Runnable

你需要理解:

  1. 为什么 prompt | model | parser 可以组合;
  2. 为什么每个组件都能 invoke / ainvoke / stream / astream / batch / abatch
  3. 为什么普通函数可以变成 RunnableLambda
  4. 为什么字典可以隐式变成 RunnableParallel
  5. 为什么 LangChain 能把 Prompt、Model、Parser、Retriever、Tool 放在同一套执行协议下;
  6. RunnableConfig 如何把 callbacks / tags / metadata / max_concurrency / configurable 传给下游子调用;
  7. RunnableSequence 如何逐个调用子 Runnable;
  8. RunnableParallel 如何把同一个输入并行送给多个 Runnable;
  9. with_retry / with_config / with_types / assign / pick 这些增强能力为什么能作用在整个 chain 上。

本篇目标不是让你“会写 chain”,而是让你看到这行代码背后的运行时结构:

chain = prompt | model | parser

它本质上不是三段代码顺序执行,而是构造了一个新的对象:

RunnableSequence(
first=ChatPromptTemplate,
middle=[ChatModel],
last=StrOutputParser
)

这个 RunnableSequence 自己仍然是 Runnable,所以它又能继续:

chain.with_config(...)
chain.with_retry(...)
chain.batch([...])
chain.stream(...)
chain | another_runnable

2. 核心概念与最小心智模型#

Runnable 的核心价值是统一执行协议,而不是提供某一种具体业务能力。

普通 LLM Chain → RunnableSequence#

普通链路:

User Input
Prompt
Model
Parser
Output

框架表达:

chain = prompt | model | parser

源码抽象:

ChatPromptTemplate 是 Runnable[dict, PromptValue]
ChatModel 是 Runnable[PromptValue/messages, AIMessage]
StrOutputParser 是 Runnable[AIMessage, str]
prompt | model | parser
= RunnableSequence(prompt, model, parser)

范式位置:

范式层语言框架层语言源码层语言
单步 LLM 调用ChainRunnableSequence
输入模板化PromptChatPromptTemplate.invoke
模型生成ModelBaseChatModel.invoke
输出后处理ParserStrOutputParser.invoke
链式组合LCELRunnable.or

这说明:Runnable 是 LangChain 里最小的“可执行单元”,RunnableSequence 是最常见的“顺序编排单元”。

为什么这是学习 Agent 框架的第一站#

在 Agent 系统里,即使你后来使用的是 LangGraph,节点内部仍然大量出现:

intent_chain = intent_prompt | model | intent_parser
planner_chain = planner_prompt | model.with_structured_output(Plan)
reflection_chain = reflection_prompt | model | parser

这些链路本质上都是 RunnableSequence。

所以你在旅行规划助手里看到的:

prompt | model
prompt | model | parser
retriever | formatter | prompt | model | parser

都应该被标注为:

这是一个 RunnableSequence。
它的输入类型是什么?
每一步输出类型是什么?
最终输出类型是什么?
它是否支持 batch?
它是否支持 stream?
它是否能挂 callbacks / tags / metadata?
它是否可以加 with_retry?

3. 完整执行链路#

最小代码只作为执行链证据,代码、对象流转与执行顺序在同一章闭环。

从最小组合观察执行协议#

标准 Prompt → Model → Parser 链#

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4.1-mini")
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个旅行规划助手。"),
("user", "请为 {destination} 规划 {days} 天行程。")
])
chain = prompt | model | StrOutputParser()
result = chain.invoke({
"destination": "东京",
"days": 5
})
print(result)

你应该如何读这段代码#

不要只读成:

先格式化 prompt,再调用模型,再转成字符串。

源码级读法应该是:

1. ChatPromptTemplate 是一个 Runnable。
2. ChatModel 是一个 Runnable。
3. StrOutputParser 是一个 Runnable。
4. `|` 调用 Runnable.__or__。
5. __or__ 会调用 coerce_to_runnable,把右侧对象转成 Runnable。
6. __or__ 返回 RunnableSequence。
7. 第二个 `| parser` 继续构造更长的 RunnableSequence。
8. chain.invoke(input) 实际调用 RunnableSequence.invoke(input)。
9. RunnableSequence.invoke 按 steps 顺序调用每个子 Runnable。
10. 每一步输出成为下一步输入。

推荐增加调试代码#

print(type(prompt))
print(type(model))
print(type(StrOutputParser()))
print(type(chain))
print(chain.get_graph())
print(chain.input_schema.model_json_schema())
print(chain.output_schema.model_json_schema())

你关注的不是输出好不好看,而是验证三件事:

  1. chain 的类型是不是 RunnableSequence
  2. chain 是否暴露 input_schema / output_schema;
  3. chain.get_graph() 是否能把链路还原成图结构。

4. 源码地图、关键文件与阅读顺序#

核心文件#

langchain_core/runnables/base.py
langchain_core/runnables/config.py
langchain_core/runnables/utils.py
langchain_core/runnables/passthrough.py
langchain_core/runnables/branch.py

本篇重点类#

Runnable
RunnableSequence
RunnableParallel
RunnableLambda
RunnableConfig

扩展类#

RunnableSerializable
RunnableBinding
RunnablePassthrough
RunnableAssign
RunnablePick
RunnableBranch
RunnableGenerator

推荐阅读顺序#

不要一上来从文件头读到文件尾。建议按问题倒推:

第一轮:看 Runnable 的协议
Runnable.invoke / ainvoke / stream / batch
第二轮:看 `|` 是怎么组合的
Runnable.__or__ / __ror__ / pipe / coerce_to_runnable
第三轮:看 RunnableSequence 如何执行
RunnableSequence.__init__ / invoke / batch / transform / stream
第四轮:看 RunnableLambda 如何包装普通函数
RunnableLambda.__init__ / invoke / ainvoke
第五轮:看 RunnableConfig 如何传递
ensure_config / patch_config / merge_configs / get_config_list
第六轮:看并行与分支
RunnableParallel / RunnableBranch / RunnablePassthrough

推荐源码链接#

  • LangChain Core Runnable reference:https://reference.langchain.com/python/langchain-core/runnables/
  • Runnable API:https://reference.langchain.com/python/langchain-core/runnables/base/Runnable/
  • RunnableSequence API:https://reference.langchain.com/python/langchain-core/runnables/base/RunnableSequence/
  • RunnableParallel API:https://reference.langchain.com/python/langchain-core/runnables/base/RunnableParallel/
  • RunnableConfig API:https://reference.langchain.com/python/langchain-core/runnables/config/RunnableConfig/
  • GitHub 源码:https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/base.py
  • RunnableConfig 源码:https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/config.py

5. 对象模型、继承关系与协议边界#

Runnable 统一执行协议#

Runnable 的定位#

源码中 Runnable 的核心定义可以概括为:

Runnable 是一个可以被调用、批处理、流式输出、转换、组合的工作单元。

这句话有四层含义:

  1. 可调用:单输入单输出,invoke(input)
  2. 可异步调用ainvoke(input)
  3. 可批处理:多输入多输出,batch(inputs)
  4. 可流式输出:逐块输出,stream(input)
  5. 可组合:通过 |pipe() 组合成更大的 Runnable;
  6. 可配置:每次运行可携带 tags、metadata、callbacks、max_concurrency 等配置;
  7. 可追踪:通过 callback manager / LangSmith 记录中间步骤;
  8. 可模式化:暴露 input_schema、output_schema、config_schema。

Runnable 的核心方法#

源码中最重要的方法可以分成四类。

执行方法#
单次调用:
invoke(input, config=None, **kwargs) -> output
ainvoke(input, config=None, **kwargs) -> output
批量调用:
batch(inputs, config=None, **kwargs) -> list[output]
abatch(inputs, config=None, **kwargs) -> list[output]
流式调用:
stream(input, config=None, **kwargs) -> Iterator[chunk]
astream(input, config=None, **kwargs) -> AsyncIterator[chunk]

读源码时要先分清这三组方法的职责:invoke 是最基础的单次执行入口;batch 是多输入入口;stream 是流式输出入口。后两者很多时候不是子类“原生实现”的能力,而是 Runnable 基类用 invoke 包出来的默认外观。

组合方法#
__or__(other)
__ror__(other)
pipe(*others)
assign(**kwargs)
pick(keys)

其中 __or__prompt | model 的根源。

配置增强方法#
with_config(...)
with_retry(...)
with_fallbacks(...)
with_types(...)
with_listeners(...)
configurable_fields(...)
configurable_alternatives(...)

这些方法不是只给 LLM 用的,而是给所有 Runnable 用的。这就是为什么整个 chain 可以直接:

chain = (prompt | model | parser).with_retry(stop_after_attempt=3)
Schema / Trace 方法#
input_schema
output_schema
config_schema()
get_graph()
get_prompts()

这些方法让 Runnable 不只是可执行,还可以被:

可视化
可验证
可追踪
可调试
可序列化

Runnable 的默认实现关系#

Runnable 里最重要的设计是:只要子类实现最核心的同步调用能力,框架就能给它补齐其他执行模式的默认实现。

简化理解:

class Runnable:
def invoke(self, input, config=None, **kwargs):
# 子类最应该实现的核心能力
raise NotImplementedError
async def ainvoke(self, input, config=None, **kwargs):
# 默认异步:把同步 invoke 放进 executor
return await run_in_executor(
config,
self.invoke,
input,
config,
**kwargs,
)
def batch(self, inputs, config=None, **kwargs):
# 默认批处理:每个 input 仍然走 invoke,只是由 executor 并发调度
configs = get_config_list(config, len(inputs))
with get_executor_for_config(configs[0]) as executor:
return list(
executor.map(
lambda pair: self.invoke(pair[0], pair[1], **kwargs),
zip(inputs, configs),
)
)
def stream(self, input, config=None, **kwargs):
# 默认流式:没有真正的 chunk 转换,只是最后 yield 一次完整结果
yield self.invoke(input, config, **kwargs)

因此,很多组件只需要实现 invoke,就天然拥有 ainvoke / batch / stream 的统一外观。

这段伪代码的重点是:config 会跟着每一次默认包装继续传下去,batch 会为每个输入准备对应 config,stream 的默认实现只是“把最终结果包装成一个迭代器”。所以源码里看到某个 Runnable 有 stream() 方法,不等于它一定能逐 token 输出。

但是要注意:

“支持 stream 方法”不等于“真正 token 级流式输出”。

如果某个组件没有实现原生 transform 或流式逻辑,它虽然也能被 stream() 调用,但可能只是等整个结果生成后一次性吐出。

这对旅行规划助手很关键:

如果你希望前端 SSE 逐 token 展示最终行程,链路中的中间步骤不能随便放阻塞型 RunnableLambda。
否则流式输出会被阻塞在该步骤之后才开始。

核心对象关系#

Runnable
├─ RunnableSequence
├─ RunnableParallel
├─ RunnableLambda
├─ RunnablePassthrough
└─ RunnableBranch

协议边界#

对象负责什么不负责什么
Runnable统一调用和组合协议定义具体业务语义
RunnableSequence按顺序传递中间结果自动提供并行语义
RunnableParallel并行执行独立分支处理有依赖关系的步骤
RunnableConfig携带 callback、tag、metadata 和并发配置保存业务状态

6. 源码阅读策略与证据标准#

阅读顺序#

公开入口
输入输出类型
构建期对象
运行时主链
分支、异常与停止条件
扩展接口

证据标准#

标记使用条件
源码事实当前正式版源码可以直接证明
官方契约官方文档或 API Reference 明确承诺
简化伪代码压缩真实控制流,且明确不是逐字源码
作者推断根据调用关系得出,必须标注为推断
工程建议说明适用条件,不写成框架保证

7. 构建期源码解剖#

本章回答普通对象如何被转换为 Runnable,以及组合表达式如何构造可执行结构。

RunnableSequence 的构造#

表面现象#

你写:

chain = prompt | model | parser

Python 实际执行顺序是:

chain_1 = prompt.__or__(model)
chain_2 = chain_1.__or__(parser)

最终得到:

RunnableSequence(prompt, model, parser)

源码关键逻辑#

源码中 Runnable.__or__ 的核心逻辑可以简化为:

class Runnable:
def __or__(self, other):
return RunnableSequence(
self,
coerce_to_runnable(other),
)
def __ror__(self, other):
return RunnableSequence(
coerce_to_runnable(other),
self,
)
def pipe(self, *others):
chain = self
for other in others:
chain = chain | other
return chain

__ror__ 处理的是右结合场景,比如:

{"context": retriever, "question": RunnablePassthrough()} | prompt

当左侧是 dict,不是 Runnable 时,Python 会尝试调用右侧对象的 __ror__

这里要注意:| 发生在“组合阶段”,不是“执行阶段”。prompt | model | parser 不会立刻调用模型,它只是把三个节点包装成一个新的 RunnableSequence。真正执行要等到后面调用 invoke / stream / batch

coerce_to_runnable:为什么普通函数和 dict 也能进链#

coerce_to_runnable 是理解 LCEL 的关键函数。

它的逻辑可以概括为:

def coerce_to_runnable(thing):
if isinstance(thing, Runnable):
# 已经是 Runnable,直接进入链
return thing
if is_async_generator(thing) or is_generator_function(thing):
# 生成器函数更适合表达 chunk-by-chunk 的转换
return RunnableGenerator(thing)
if callable(thing):
# 普通函数包装成 RunnableLambda
return RunnableLambda(thing)
if isinstance(thing, dict):
# dict 的每个 value 会继续递归转换成 Runnable
return RunnableParallel({
key: coerce_to_runnable(value)
for key, value in thing.items()
})
raise TypeError

这段伪代码里最容易被忽略的是 dict 分支:dict 不是普通参数对象,而是会被解释成“并行 map”。所以 {"food": food_chain, "weather": weather_chain} 的语义不是把字典传给下游,而是先构造一个 RunnableParallel,等执行时再把同一个输入分发给每个 key 对应的子 Runnable。

这解释了三个常见现象。

现象一:普通函数可以直接写进链#
def format_plan(ai_message):
return ai_message.content.strip()
chain = prompt | model | format_plan

本质是:

chain = prompt | model | RunnableLambda(format_plan)
现象二:dict 可以触发并行#
chain = {
"food": food_chain,
"attractions": attraction_chain,
} | merge_prompt | model

本质是:

chain = RunnableParallel({
"food": food_chain,
"attractions": attraction_chain,
}) | merge_prompt | model
现象三:不支持的类型会报错#
chain = prompt | "not runnable"

会报类似错误:

Expected a Runnable, callable or dict.
Instead got an unsupported type: <class 'str'>

这个错误不是模型问题,而是 LCEL 组合阶段 coerce_to_runnable 不知道如何把字符串变成 Runnable。

工程理解#

| 的本质不是“把 Python 函数拼起来”,而是做了两件事:

1. 把每个对象统一包装成 Runnable;
2. 把多个 Runnable 注册进 RunnableSequence。

所以 prompt | model | parser 的工程含义是:

声明一条可执行、可追踪、可批处理、可流式、可配置的执行管线。

RunnableLambda 的构造与函数适配#

表面用法#

from langchain_core.runnables import RunnableLambda
def normalize_destination(input: dict) -> dict:
return {
**input,
"destination": input["destination"].strip()
}
normalize_node = RunnableLambda(normalize_destination)
chain = normalize_node | prompt | model | StrOutputParser()

也可以直接写:

chain = normalize_destination | prompt | model | StrOutputParser()

因为 coerce_to_runnable 会把 callable 转成 RunnableLambda

RunnableLambda 解决什么问题#

它解决的是:

如何把普通 Python 业务逻辑放进 LangChain 的统一执行协议?

比如:

字段清洗
输入归一化
输出格式修正
日志补充
规则判断
轻量数据转换

这些逻辑不需要 LLM,但需要接入 chain 的执行、追踪、配置和批处理体系。

RunnableLambda 的源码机制#

RunnableLambda 的核心行为:

1. 接收一个 Python callable;
2. 检查它是同步函数、异步函数、生成器函数还是异步生成器函数;
3. invoke 时调用同步函数;
4. ainvoke 时优先调用异步函数,否则委托线程池;
5. batch 时继承 Runnable 默认 batch;
6. 如果函数返回另一个 Runnable,则继续调用返回的 Runnable。

简化伪源码:

class RunnableLambda(Runnable):
def __init__(self, func, afunc=None, name=None):
self.func = func
self.afunc = afunc
self.name = name or func.__name__
def invoke(self, input, config=None, **kwargs):
config = ensure_config(config)
callback_manager = get_callback_manager_for_config(config)
run_manager = callback_manager.on_chain_start(
serialized=self.get_serialized(),
inputs=input,
name=config.get("run_name") or self.name,
)
try:
output = call_func_with_variable_args(
self.func,
input,
config,
run_manager=run_manager,
**kwargs,
)
if isinstance(output, Runnable):
# 函数可以动态返回下一个 Runnable,形成运行时分支
child_config = patch_config(
config,
callbacks=run_manager.get_child(),
)
output = output.invoke(input, child_config)
run_manager.on_chain_end(output)
return output
except BaseException as e:
run_manager.on_chain_error(e)
raise

这段伪代码比“调用普通函数”多了三层意思:先创建当前 lambda 的 trace;再按函数签名注入 config / run_manager;最后处理“函数返回 Runnable”的动态链路。也就是说,RunnableLambda 不是简单的 func(input),而是把普通函数纳入了 LangChain 的配置、追踪和递归执行协议。

call_func_with_variable_args 可以继续展开成:

def call_func_with_variable_args(func, input, config, run_manager, **kwargs):
if accepts_config(func):
kwargs["config"] = config
if accepts_run_manager(func):
kwargs["run_manager"] = run_manager
return func(input, **kwargs)

所以这一步真正做的是“根据函数签名决定注入什么”,而不是要求所有业务函数都写成同一种固定格式。

call_func_with_variable_args:函数为什么可以接收 config / run_manager#

普通函数可能有不同签名:

def f(x): ...
def f(x, config): ...
def f(x, run_manager): ...
def f(x, run_manager, config): ...

LangChain 会通过工具函数判断函数是否接受 configrun_manager,然后按需注入。

这让你可以写出更工程化的业务函数:

from langchain_core.runnables import RunnableConfig
def normalize_request(input: dict, config: RunnableConfig) -> dict:
metadata = config.get("metadata", {})
trace_id = metadata.get("trace_id")
return {
**input,
"trace_id": trace_id,
"destination": input["destination"].strip()
}

这样函数仍然是普通函数,但进入 RunnableLambda 后可以读取运行配置。

RunnableLambda 的流式限制#

源码文档中明确提醒:RunnableLambda 更适合不需要流式处理的普通函数。如果需要 chunk-by-chunk 的流式转换,应使用 RunnableGenerator 或自定义 Runnable。

所以旅行规划助手里:

chain = prompt | model | RunnableLambda(clean_text) | parser

如果 model 原本可以 token streaming,RunnableLambda(clean_text) 可能成为流式阻塞点。

更推荐:

非流式预处理:放在 prompt 前
非流式后处理:放在最终输出后
需要流式处理:实现 transform / RunnableGenerator

构建期总结、伪代码与源码证据#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def compose(left, right):
first = coerce_to_runnable(left)
second = coerce_to_runnable(right)
return RunnableSequence(first, second)

源码证据:


8. 运行时主链源码解剖#

本章沿一次调用追踪步骤执行、配置传播与 callback 生命周期。

从调用入口追踪完整流程#

用户看到的执行流程#

chain.invoke(input)
ChatPromptTemplate.invoke(input)
生成 PromptValue / messages
ChatModel.invoke(messages)
返回 AIMessage
StrOutputParser.invoke(AIMessage)
返回字符串

源码视角的执行流程#

更接近源码的执行过程是:

chain.invoke(input, config=None)
RunnableSequence.invoke(input, config)
ensure_config(config)
创建 root run / callback manager
遍历 steps:
step_1 = ChatPromptTemplate
step_2 = ChatModel
step_3 = StrOutputParser
每一步执行前 patch_config:
为子步骤创建 child callback manager
写入 seq:step:1 / seq:step:2 / seq:step:3
output_1 = step_1.invoke(input, child_config)
output_2 = step_2.invoke(output_1, child_config)
output_3 = step_3.invoke(output_2, child_config)
callback_manager.on_chain_end(output_3)
return output_3

类型流转#

以旅行规划助手为例,输入输出不是一直都是字符串:

输入:dict
{
"destination": "东京",
"days": 5
}
ChatPromptTemplate 输出:PromptValue
PromptValue 可转 messages:
[
SystemMessage(content="你是一个旅行规划助手。"),
HumanMessage(content="请为 东京 规划 5 天行程。")
]
ChatModel 输出:AIMessage
AIMessage(content="以下是东京 5 天行程...")
StrOutputParser 输出:str
"以下是东京 5 天行程..."

所以你后面学习 Tool Calling 时要记住:

模型输出不是普通字符串,而是 AIMessage。
AIMessage 既可以有 content,也可以有 tool_calls、usage_metadata、response_metadata。
Parser 只是把 AIMessage 进一步转成你需要的业务结构。

RunnableSequence 运行主链#

RunnableSequence 的定位#

RunnableSequence 是 LangChain 里最重要的组合算子之一。它的语义是:

按顺序调用多个 Runnable,前一个 Runnable 的输出作为下一个 Runnable 的输入。

即:

x0 = input
x1 = step1.invoke(x0)
x2 = step2.invoke(x1)
x3 = step3.invoke(x2)
return x3

构造阶段#

当你写:

chain = prompt | model | parser

构造阶段大致发生:

1. prompt.__or__(model)
2. coerce_to_runnable(model)
3. RunnableSequence(prompt, model)
4. sequence.__or__(parser)
5. coerce_to_runnable(parser)
6. RunnableSequence(prompt, model, parser)

RunnableSequence 内部通常会维护:

first
middle
last

这是为了类型推断和执行优化。你可以理解为:

steps = [first, *middle, last]

invoke 执行阶段#

简化伪源码:

def invoke(self, input, config=None, **kwargs):
# 1. 标准化 config,并把父级上下文配置合进来
config = config_with_context(
ensure_config(config),
self.steps,
)
# 2. 当前 sequence 自己先创建一个 root run
callback_manager = get_callback_manager_for_config(config)
run_manager = callback_manager.on_chain_start(
serialized=self.get_serialized(),
inputs=input,
name=config.get("run_name") or self.get_name(),
run_id=config.get("run_id"),
)
try:
for i, step in enumerate(self.steps):
# 3. 每个 step 派生一个 child config
child_config = patch_config(
config,
callbacks=run_manager.get_child(f"seq:step:{i + 1}")
)
# 4. 把 child_config 写入当前上下文,内部嵌套调用也能读到
context = copy_context()
context.run(_set_config_context, child_config)
# 5. 第一个 step 接收外部 kwargs,后续 step 只接收上一步输出
if i == 0:
input = context.run(step.invoke, input, child_config, **kwargs)
else:
input = context.run(step.invoke, input, child_config)
run_manager.on_chain_end(input)
return input
except BaseException as e:
run_manager.on_chain_error(e)
raise

这段逻辑解释了几个重要细节。

先看变量流转:input 在循环里不断被覆盖,它不是原始输入的永久引用,而是“当前步骤的输出”。如果 step1 是 prompt,input 会从业务 dict 变成 PromptValue;如果 step2 是 model,input 会从 PromptValue 变成 AIMessage;如果 step3 是 parser,input 才会变成最终字符串或结构化对象。

细节一:kwargs 只传给第一个 step#

如果你执行:

chain.invoke(input, some_kwarg="value")

通常只有第一个 step 会接收额外 kwargs,后续 step 接收的是前一步输出。

细节二:每个 step 都有 child callback#

每一步都会通过:

run_manager.get_child("seq:step:n")

生成子运行记录。

这就是为什么 LangSmith 或 ConsoleCallbackHandler 能看到:

chain
├── seq:step:1 ChatPromptTemplate
├── seq:step:2 ChatModel
└── seq:step:3 StrOutputParser
细节三:异常会进入 on_chain_error#

任何一步失败,都会:

触发 callback on_chain_error
中断后续步骤
向外抛出异常

所以调试时你要定位:

是 prompt 格式化失败?
是 model 调用失败?
是 parser 解析失败?

不要只看最终报错。

batch 执行阶段#

RunnableSequence.batch(inputs) 的语义不是简单地:

[chain.invoke(x) for x in inputs]

更准确的理解是:

先把所有 inputs 批量送进 step1;
再把 step1 的所有 outputs 批量送进 step2;
再把 step2 的所有 outputs 批量送进 step3。

即:

inputs_0 = [case1, case2, case3]
inputs_1 = step1.batch(inputs_0)
inputs_2 = step2.batch(inputs_1)
inputs_3 = step3.batch(inputs_2)
return inputs_3

这样设计的好处是:

如果某个底层模型 API 支持真正 batch,它可以在自己的 batch 中优化;
如果不支持,Runnable 默认 batch 会用线程池并发执行 invoke。

这对评估很重要。比如你做旅行助手测试集:

results = chain.batch([
{"destination": "东京", "days": 5},
{"destination": "大阪", "days": 3},
{"destination": "京都", "days": 4},
])

你不是手写 for 循环,而是让每个 Runnable 自己决定是否优化 batch。

stream 执行阶段#

RunnableSequence.stream(input) 更复杂,因为它要考虑链路中每个 step 是否支持流式转换。

源码层核心概念是 transform

transform: Iterator[InputChunk] -> Iterator[OutputChunk]

如果链路中每个组件都实现了 transform,那么数据可以像管道一样一边输入一边输出:

input stream
step1.transform
step2.transform
step3.transform
output stream

但如果中间某个组件不支持 transform,例如普通 RunnableLambda,就可能阻塞流式输出:

step1 支持流式
step2 不支持流式,需要等完整输入
step3 支持流式
结果:stream 会从 step2 完成后才开始继续往下游输出。

工程结论:

如果你的旅行规划助手要做 SSE 流式响应,
不要在最终模型输出前随便插入阻塞型 RunnableLambda。

如果确实需要自定义流式逻辑,优先考虑:

RunnableGenerator
或自定义 Runnable 并实现 transform / atransform

RunnableConfig 传播#

RunnableConfig 是什么#

RunnableConfig 是每次执行 Runnable 时携带的运行配置。

常见字段:

tags: list[str]
metadata: dict[str, Any]
callbacks: callback handlers 或 callback manager
run_name: 当前运行名称
run_id: 当前运行 ID
max_concurrency: 最大并发数
recursion_limit: 最大递归深度
configurable: 运行时可配置字段

典型用法:

result = chain.invoke(
{"destination": "东京", "days": 5},
config={
"run_name": "travel_plan_chain",
"tags": ["travel", "runnable-sequence", "demo"],
"metadata": {
"user_id": "u_001",
"trace_id": "trace_20260608_001"
},
"max_concurrency": 5,
}
)

RunnableConfig 为什么重要#

在 Demo 里你可以忽略 config,但在生产 Agent 系统里它非常关键:

字段作用旅行助手例子
tags给 trace 打标签travel, planner, eval
metadata记录业务元信息user_id, session_id, request_id
callbacks接入日志、LangSmith、自定义监控输出每一步耗时
run_name当前链路名称itinerary_generation_chain
max_concurrency控制 batch / parallel 并发并发评估 100 条 case
configurable运行时切换模型、温度、策略llm=openai / llm=qwen

ensure_config:为什么 config 可以自动继承#

ensure_config(config) 做的事情可以理解为:

def ensure_config(config=None):
empty = {
"tags": [],
"metadata": {},
"callbacks": None,
"recursion_limit": DEFAULT_RECURSION_LIMIT,
"configurable": {},
}
# 1. 先继承 ContextVar 里的父级配置
if var_child_runnable_config.get():
empty = merge_configs(empty, var_child_runnable_config.get())
# 2. 再合并当前调用显式传入的 config
if config is not None:
for key, value in config.items():
if key in CONFIG_KEYS:
empty[key] = merge_config_value(empty.get(key), value)
else:
# 未知字段不会丢,通常进入 configurable
empty["configurable"][key] = value
# 3. 简单 configurable 字段同步到 metadata,方便 trace 检索
for key, value in empty["configurable"].items():
if is_simple_value(value) and not key.startswith("__"):
empty["metadata"].setdefault(key, value)
return empty

这解释了为什么父 chain 设置的 tags / metadata 可以传给子步骤。

读这段逻辑时,重点是合并顺序:先拿父级上下文,再叠加本次调用。这样外层 chain 的 tags=["travel"] 能被子步骤继承,而本次请求里的 metadata={"case_id": "C001"} 也能继续向下游传递。

patch_config:为什么每个子步骤能有独立 callback#

RunnableSequence 执行每个 step 时,会基于父 config 派生 child config:

child_config = patch_config(
config,
callbacks=run_manager.get_child("seq:step:1")
)

展开理解就是:

def patch_config(config, **overrides):
copied = config.copy()
for key, value in overrides.items():
if value is not None:
copied[key] = value
return ensure_config(copied)

这意味着:

父运行:travel_plan_chain
子运行:seq:step:1 prompt
子运行:seq:step:2 model
子运行:seq:step:3 parser

这些子运行共享父级 tags / metadata,但 callbacks 指向各自的 child run manager。

merge_configs:with_config 为什么能叠加#

你可以这样写:

base_chain = prompt | model | parser
chain = base_chain.with_config(
tags=["travel"],
metadata={"component": "planner"}
)
chain.invoke(
input,
config={
"tags": ["eval"],
"metadata": {"case_id": "C001"}
}
)

最终配置不是简单覆盖,而是合并:

tags: travel + eval
metadata: component + case_id
callbacks: 合并或继承
configurable: 合并

工程建议:

链路级 tags:标注模块,如 travel / planner / parser
请求级 metadata:标注业务请求,如 user_id / session_id / case_id
评估级 metadata:标注测试样本,如 dataset / case_id / expected_intent

Callback 生命周期#

callback 的本质#

Callback 是 LangChain 的生命周期钩子。它让你能观察:

chain 开始
chain 结束
chain 报错
LLM 开始
LLM 结束
tool 开始
tool 结束
retriever 开始
retriever 结束

Runnable 层通过 RunnableConfig 传递 callbacks。

from langchain_core.tracers import ConsoleCallbackHandler
chain.invoke(
{"destination": "东京", "days": 5},
config={
"callbacks": [ConsoleCallbackHandler()],
"tags": ["debug", "travel"],
"metadata": {"case_id": "debug_001"},
}
)

callback 在 RunnableSequence 中的层级#

执行一个 RunnableSequence 时,callback trace 大致是:

on_chain_start: RunnableSequence
on_chain_start: ChatPromptTemplate
on_chain_end: ChatPromptTemplate
on_chat_model_start: ChatModel
on_llm_end: ChatModel
on_parser_start: StrOutputParser
on_parser_end: StrOutputParser
on_chain_end: RunnableSequence

不同组件的 callback 类型可能不同,但父子层级由 run_manager.get_child(...) 保持。

对应到 RunnableSequence.invoke,callback 的流向可以压缩成这段伪代码:

root_run = callback_manager.on_chain_start("RunnableSequence", input)
for i, step in enumerate(steps):
child_config = patch_config(
config,
callbacks=root_run.get_child(f"seq:step:{i + 1}"),
)
output = step.invoke(input, child_config)
root_run.on_chain_end(output)

这里的 root_run 是整条链的父节点,get_child(...) 生成每个步骤自己的子节点。模型、Prompt、Parser 触发的 callback 类型可以不同,但它们都挂在同一棵运行树下面。

旅行助手中的建议#

给每条 RunnableSequence 都配置明确名称:

intent_chain = (intent_prompt | model | intent_parser).with_config(
run_name="intent_chain",
tags=["travel", "intent", "runnable"]
)
planner_chain = (planner_prompt | model | planner_parser).with_config(
run_name="planner_chain",
tags=["travel", "planner", "runnable"]
)

这样后续 LangSmith / 日志里才不会只看到一堆匿名 RunnableSequence。


运行时总结、伪代码与源码证据#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def invoke_sequence(steps, input_value, config):
value = input_value
for index, step in enumerate(steps):
child_config = patch_config(config, step=index)
value = step.invoke(value, child_config)
return value

源码证据:


9. 关键分支、异常与边界#

本章解释并行、条件路由及其并发和失败边界。

RunnableParallel 并行分支#

表面用法#

from langchain_core.runnables import RunnableParallel
parallel = RunnableParallel({
"food": food_chain,
"attractions": attraction_chain,
"weather": weather_chain,
})
result = parallel.invoke({
"destination": "东京",
"days": 5
})

结果:

{
"food": "东京美食推荐...",
"attractions": "东京景点推荐...",
"weather": "东京天气建议..."
}

dict 隐式转换#

你也可以写:

parallel = {
"food": food_chain,
"attractions": attraction_chain,
"weather": weather_chain,
}

只要它出现在 | 组合里:

chain = {
"food": food_chain,
"attractions": attraction_chain,
"weather": weather_chain,
} | merge_prompt | model | parser

coerce_to_runnable 会把 dict 转为 RunnableParallel

RunnableParallel 的语义#

RunnableParallel 的语义是:

给每个子 Runnable 同一个输入,
并行执行,
最后把每个子 Runnable 的输出按 key 合并成 dict。

简化伪源码:

def invoke(self, input, config=None):
config = ensure_config(config)
callback_manager = get_callback_manager_for_config(config)
run_manager = callback_manager.on_chain_start(
serialized=self.get_serialized(),
inputs=input,
name=config.get("run_name") or self.get_name(),
)
steps = self.steps__
futures = {}
try:
with get_executor_for_config(config) as executor:
for key, step in steps.items():
child_config = patch_config(
config,
callbacks=run_manager.get_child(f"map:key:{key}"),
)
futures[key] = executor.submit(
step.invoke,
input,
child_config,
)
output = {
key: future.result()
for key, future in futures.items()
}
run_manager.on_chain_end(output)
return output
except BaseException as e:
run_manager.on_chain_error(e)
raise

这里的关键不是“用了线程池”这么简单,而是三点:每个 step 拿到的是同一个原始 input;每个 key 都有自己的 child callback;最终输出按 key 重新组装成 dict。也就是说,RunnableParallel 不是 step1 -> step2 -> step3,而是 input -> 多个分支 -> dict output

max_concurrency 如何影响并行#

parallel.invoke(
input,
config={"max_concurrency": 3}
)

max_concurrency 会影响内部 executor 的最大并发数。

对旅行助手来说:

景点检索、餐厅检索、天气查询、交通建议
可以并行。
但创建订单、扣款、退款、写数据库
不能盲目并行,必须看副作用和事务顺序。

RunnableParallel 和 Agent 范式的关系#

它不是 Multi-Agent,但可以承载“多路信息收集”:

并行检索多个来源
并行调用多个轻量模型
并行生成多个候选方案
并行做多个维度评分

在旅行规划助手里,典型结构是:

research_chain = {
"attractions": attraction_retriever | format_docs,
"food": food_retriever | format_docs,
"weather": weather_tool,
"transport": transport_tool,
} | synthesis_prompt | model | parser

范式映射:

信息收集阶段:RunnableParallel
综合生成阶段:RunnableSequence
整体任务范式:Plan-and-Execute / Workflow 中的某个节点

RunnableBranch 条件分支#

RunnableBranch 的定位#

RunnableBranch 是 LangChain Core 里的条件分支 Runnable。它适合轻量路由:

如果满足条件 A,执行 chain_a;
如果满足条件 B,执行 chain_b;
否则执行 default_chain。

表面代码:

from langchain_core.runnables import RunnableBranch
branch = RunnableBranch(
(lambda x: "预算" in x["user_request"], budget_chain),
(lambda x: "景点" in x["user_request"], attraction_chain),
general_chain,
)

它的执行逻辑可以直接理解成 first-match:

def invoke(self, input, config=None):
config = ensure_config(config)
callback_manager = get_callback_manager_for_config(config)
run_manager = callback_manager.on_chain_start(
serialized=self.get_serialized(),
inputs=input,
)
try:
for index, (condition, branch) in enumerate(self.branches):
condition_config = patch_config(
config,
callbacks=run_manager.get_child(f"condition:{index + 1}"),
)
matched = condition.invoke(input, condition_config)
if matched:
branch_config = patch_config(
config,
callbacks=run_manager.get_child(f"branch:{index + 1}"),
)
output = branch.invoke(input, branch_config)
run_manager.on_chain_end(output)
return output
default_config = patch_config(
config,
callbacks=run_manager.get_child("branch:default"),
)
output = self.default.invoke(input, default_config)
run_manager.on_chain_end(output)
return output
except BaseException as e:
run_manager.on_chain_error(e)
raise

这段伪代码要抓住两个点:第一,分支按声明顺序判断,命中第一个就返回,后面的不会再执行;第二,condition 和 branch 都会拿到自己的 child callback,所以路由判断本身也可以被追踪。

与 LangGraph Router 的区别#

对比项RunnableBranchLangGraph conditional edge
所在层级LangChain Core RunnableLangGraph 图编排
状态模型单次输入输出共享 State
路由粒度链内部轻量分支节点级流程控制
可观测性Runnable traceGraph trace / checkpoint
适合场景简单分支复杂业务流程

旅行助手中:

简单:根据用户请求走预算 chain / 景点 chain → RunnableBranch
复杂:多轮状态、工具失败重试、人工确认 → LangGraph conditional edge

工程建议#

不要把所有路由都塞进 RunnableBranch。

如果只是链内部的轻量条件选择,用 RunnableBranch。
如果需要状态持久化、循环、回滚、HITL,用 LangGraph。

分支语义总结、伪代码与源码证据#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def route_or_parallel(input_value, branches, condition):
if condition is not None:
return select_branch(condition(input_value)).invoke(input_value)
return run_independent_branches(branches, input_value)

源码证据:


10. 扩展机制与框架协作#

本章解释透传、字段增强、重试与其他 Runnable 的组合边界。

RunnablePassthrough、assign 与 pick#

为什么需要 Passthrough#

在链式执行中,一个常见问题是:

经过某个步骤后,原始输入丢了。

例如:

chain = retriever | prompt | model | parser

执行到 prompt 时,可能只剩下检索结果,没有原始 question。

这时需要 RunnablePassthrough 保留输入。

RAG 常见写法#

from langchain_core.runnables import RunnablePassthrough
rag_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough(),
}
| prompt
| model
| StrOutputParser()
)

执行语义:

输入 question
RunnableParallel:
context = retriever(question) | format_docs
question = 原样透传 question
prompt 接收 {context, question}
model
parser

如果把这段链写成普通 Python 伪代码,大概是:

def rag_chain(question):
parallel_output = {
"context": format_docs(retriever.invoke(question)),
"question": question,
}
prompt_value = prompt.invoke(parallel_output)
message = model.invoke(prompt_value)
return parser.invoke(message)

这段伪代码能看出 RunnablePassthrough() 的价值:它不是做复杂计算,而是在 context 分支把 question 变成 docs 的同时,给 prompt 保留一份原始 question。

assign 的语义#

assign 用于在 dict 输出上追加字段:

chain = base_chain.assign(
total_chars=lambda x: len(x["answer"]),
source_count=lambda x: len(x["sources"]),
)

语义:

保留原 dict 字段;
并行计算新字段;
把新字段合并回原 dict。

更贴近源码语义的伪代码是:

def assign(input_dict, **mappers):
assigned = {
key: mapper.invoke(input_dict)
for key, mapper in mappers.items()
}
return {
**input_dict,
**assigned,
}

所以 assign 的输入最好是 dict。它不是“替换输出”,而是“在原输出上追加派生字段”。

pick 的语义#

pick 用于从 dict 输出中抽取字段:

answer_only = chain.pick("answer")

语义:

输入:{"answer": "...", "sources": [...], "score": 0.9}
输出:"..."

如果传入多个 key,它的效果更像字段裁剪:

def pick(input_dict, keys):
if isinstance(keys, str):
return input_dict[keys]
return {
key: input_dict[key]
for key in keys
}

因此 assign 常用于中间态“加字段”,pick 常用于末尾“收口输出形状”。

旅行助手应用#

travel_context_chain = (
{
"user_request": RunnablePassthrough(),
"attractions": attraction_chain,
"food": food_chain,
"weather": weather_chain,
}
| RunnablePassthrough.assign(
evidence_count=lambda x: len(x["attractions"]) + len(x["food"])
)
)

这个链路的作用是:

并行收集多类旅行信息;
保留原始用户请求;
追加 evidence_count;
把结构化上下文交给 planner prompt。

RunnableBinding、Retry 与 Fallback 如何保持协议稳定#

with_config()with_retry()with_fallbacks() 不会把原对象改造成另一套调用接口,而是返回新的 Runnable 包装对象。包装层保存额外策略,运行时仍把 invoke / batch / stream 委托给内部 Runnable,因此上层组合器不需要识别每一种扩展类型。

# 简化伪代码,不是源码逐字复制
def invoke_bound(input_value, caller_config):
effective_config = merge_configs(bound_config, caller_config)
try:
return inner.invoke(input_value, config=effective_config)
except retryable_errors:
return retry_or_call_fallback(input_value, effective_config)

这条边界很关键:配置、重试和降级属于执行策略,输入输出协议仍由被包装的 Runnable 决定。扩展机制因此可以层层组合,但业务代码仍只依赖统一的 Runnable 接口。

阅读包装类源码时,可以用下面四个判据确认扩展是否破坏原协议:

判据应观察的源码行为
输入是否透传包装层是否把原始输入完整交给内部 Runnable
配置如何合并绑定配置与调用配置的优先级是否明确
异常在哪里截获仅捕获声明为可重试、可降级的异常
输出是否改形若输出类型变化,包装类必须显式声明新协议

因此,看到一个新 Runnable 扩展类时,不必先逐行阅读全部实现。先定位“保存内部 Runnable、合并配置、委托调用、处理异常”四个位置,就能快速判断它属于协议包装还是业务变换。

源码证据:

扩展协作总结、伪代码与源码证据#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def extend(runnable, config=None, retry=None):
bound = runnable.with_config(config or {})
return bound.with_retry(**retry) if retry else bound

源码证据:


11. 工程决策与适用场景#

搜索目标#

在旅行规划助手项目中查找所有类似代码:

prompt | model
prompt | model | parser
retriever | formatter | prompt | model | parser
{
"context": retriever,
"question": RunnablePassthrough()
} | prompt | model

标注模板#

给每一条链补充如下源码理解注释:

travel_plan_chain = prompt | model | StrOutputParser()
"""
RunnableSequence 标注:
- 范式层:普通 LLM Chain / 行程生成节点
- 框架层:ChatPromptTemplate | ChatModel | StrOutputParser
- 源码层:Runnable.__or__ 构造 RunnableSequence
- 输入:dict(destination: str, days: int, preferences: list[str])
- Step1 输出:PromptValue / messages
- Step2 输出:AIMessage
- Step3 输出:str
- 支持 invoke:是
- 支持 batch:是,默认逐 step batch
- 支持 stream:取决于 model 和 parser 是否支持 transform
- 可加 with_retry:是,建议加在 model 或整个 chain 上
- 可加 with_config:是,建议添加 run_name / tags / metadata
"""

推荐整理表#

Chain 名称代码形式Runnable 类型输入中间输出最终输出是否 stream是否 with_retry是否 with_config
intent_chain`promptmodelparser`RunnableSequenceuser_requestAIMessageIntentSchema否 / 可选
planner_chain`promptmodel.with_structured_output(…)`RunnableSequenceTravelStateAIMessagePlan否 / 可选
final_chain`promptmodelStrOutputParser()`RunnableSequencefull_stateAIMessagestr
research_chain`dictpromptmodel`RunnableParallel + RunnableSequencedestinationdictAIMessage部分支持

推荐项目目录补充#

docs/framework_source_notes/
01_runnable_source_deep_dive.md
runnable_chain_inventory.md
src/travel_agent/
chains/
intent_chain.py
planner_chain.py
research_chain.py
final_chain.py

runnable_chain_inventory.md 专门记录项目中所有 Runnable 链路。


12. 常见误区与源码纠正#

误区一:把 | 当成普通管道#

错误理解:

`|` 就是把函数一个接一个调用。

正确理解:

`|` 是 Runnable 的组合运算符,会构造 RunnableSequence;
组合发生在构建阶段,执行发生在 invoke / stream / batch 阶段。

误区二:以为所有 stream 都是 token 级流式#

错误理解:

只要 chain.stream 就一定逐 token 输出。

正确理解:

只有链路中组件支持 transform / 原生流式时,才能保持真正流式;
中间阻塞组件会延迟流式输出。

误区三:把 RunnableLambda 用成万能节点#

错误理解:

任何逻辑都用 RunnableLambda 包一下。

正确理解:

RunnableLambda 适合轻量同步/异步函数;
复杂状态、重试、副作用、流式逻辑不建议全塞进去。

误区四:忽略 RunnableConfig#

错误理解:

config 只是可选参数,不重要。

正确理解:

config 是生产级观测、追踪、并发控制、动态配置的入口。

误区五:把 RunnableParallel 当 Multi-Agent#

错误理解:

并行几个 chain 就是 Multi-Agent。

正确理解:

RunnableParallel 是并行执行原语;
Multi-Agent 还需要角色边界、共享状态、协作协议、失败处理和评估。

13. 最终心智模型与掌握检查#

你最终应该形成这张图:

Runnable
是所有 LangChain 组件的统一执行协议
Prompt / Model / Parser / Retriever / Tool / Function
都可以被看作 Runnable
Runnable.__or__
把多个 Runnable 组合成 RunnableSequence
RunnableSequence.invoke
按 step 顺序执行,前一步输出作为后一步输入
RunnableConfig
把 callbacks / tags / metadata / concurrency 传给每个 step
CallbackManager
记录父子运行轨迹
stream / batch / retry / config
成为所有 Runnable 的通用能力

一句话总结:

Runnable 是 LangChain Core 的“统一执行协议”;LCEL 的 | 不是语法糖那么简单,而是把 Prompt、Model、Parser、Retriever、Tool、普通函数统一注册成可执行、可追踪、可组合、可批处理、可流式的运行单元。


14. 参考资料与下一篇衔接#

官方资料#

  1. LangChain Core Runnables Reference
    https://reference.langchain.com/python/langchain-core/runnables/

  2. Runnable API Reference
    https://reference.langchain.com/python/langchain-core/runnables/base/Runnable/

  3. RunnableSequence API Reference
    https://reference.langchain.com/python/langchain-core/runnables/base/RunnableSequence/

  4. RunnableParallel API Reference
    https://reference.langchain.com/python/langchain-core/runnables/base/RunnableParallel/

  5. RunnableConfig API Reference
    https://reference.langchain.com/python/langchain-core/runnables/config/RunnableConfig/

  6. LangChain GitHub Source: base.py
    https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/base.py

  7. LangChain GitHub Source: config.py
    https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/config.py

  8. LangChain Models Documentation
    https://docs.langchain.com/oss/python/langchain/models

  9. LangChain v1 Release Notes
    https://docs.langchain.com/oss/python/releases/langchain-v1

下一篇衔接#

下一篇建议进入:

第 2 篇:Prompt / Message / ChatModel 源码解剖

核心问题:

为什么 ChatPromptTemplate.invoke 返回的不是字符串?
PromptValue、BaseMessage、HumanMessage、SystemMessage、AIMessage 的关系是什么?
为什么 AIMessage 可以携带 tool_calls?
ChatModel.invoke 内部如何标准化输入并返回 AIMessage?

这会为后续 Tool Calling 和 ReAct Agent 源码解剖打基础。


LangChain Runnable 源码解剖:从 prompt | model | parser 到统一执行协议
https://jupiter-ws.cn/posts/agent-frameworks/01-langchain-runnable-source-deep-dive/
作者
Jupiter
发布于
2026-03-05
许可协议
CC BY-NC-SA 4.0