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 | parserresult = 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 Chain | RunnableSequence |
| Prompt → Model → Parser | `prompt |
| 函数式预处理 / 后处理 | RunnableLambda |
| 多路并行检索 / 多路模型调用 | RunnableParallel |
| 条件分支 | RunnableBranch |
| 原样透传输入 / 字典字段保留 | RunnablePassthrough |
| 统一配置、追踪、重试 | RunnableConfig / with_config / with_retry |
后面学习 Tool Calling、Retriever、Agent、LangGraph Node 时,都会反复遇到 Runnable。所以第一篇必须先把 Runnable 打透。
1. 本篇问题、学习目标与能力边界
本章定义本文必须解决的问题,不提前展开实现细节。
掌握 LangChain 中最核心的统一执行抽象:Runnable。
你需要理解:
- 为什么
prompt | model | parser可以组合; - 为什么每个组件都能
invoke / ainvoke / stream / astream / batch / abatch; - 为什么普通函数可以变成
RunnableLambda; - 为什么字典可以隐式变成
RunnableParallel; - 为什么 LangChain 能把 Prompt、Model、Parser、Retriever、Tool 放在同一套执行协议下;
RunnableConfig如何把callbacks / tags / metadata / max_concurrency / configurable传给下游子调用;RunnableSequence如何逐个调用子 Runnable;RunnableParallel如何把同一个输入并行送给多个 Runnable;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_runnable2. 核心概念与最小心智模型
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 调用 | Chain | RunnableSequence |
| 输入模板化 | Prompt | ChatPromptTemplate.invoke |
| 模型生成 | Model | BaseChatModel.invoke |
| 输出后处理 | Parser | StrOutputParser.invoke |
| 链式组合 | LCEL | Runnable.or |
这说明:Runnable 是 LangChain 里最小的“可执行单元”,RunnableSequence 是最常见的“顺序编排单元”。
为什么这是学习 Agent 框架的第一站
在 Agent 系统里,即使你后来使用的是 LangGraph,节点内部仍然大量出现:
intent_chain = intent_prompt | model | intent_parserplanner_chain = planner_prompt | model.with_structured_output(Plan)reflection_chain = reflection_prompt | model | parser这些链路本质上都是 RunnableSequence。
所以你在旅行规划助手里看到的:
prompt | modelprompt | model | parserretriever | formatter | prompt | model | parser都应该被标注为:
这是一个 RunnableSequence。它的输入类型是什么?每一步输出类型是什么?最终输出类型是什么?它是否支持 batch?它是否支持 stream?它是否能挂 callbacks / tags / metadata?它是否可以加 with_retry?3. 完整执行链路
最小代码只作为执行链证据,代码、对象流转与执行顺序在同一章闭环。
从最小组合观察执行协议
标准 Prompt → Model → Parser 链
from langchain_core.prompts import ChatPromptTemplatefrom langchain_core.output_parsers import StrOutputParserfrom 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())你关注的不是输出好不好看,而是验证三件事:
chain的类型是不是RunnableSequence;chain是否暴露 input_schema / output_schema;chain.get_graph()是否能把链路还原成图结构。
4. 源码地图、关键文件与阅读顺序
核心文件
langchain_core/runnables/base.pylangchain_core/runnables/config.pylangchain_core/runnables/utils.pylangchain_core/runnables/passthrough.pylangchain_core/runnables/branch.py本篇重点类
RunnableRunnableSequenceRunnableParallelRunnableLambdaRunnableConfig扩展类
RunnableSerializableRunnableBindingRunnablePassthroughRunnableAssignRunnablePickRunnableBranchRunnableGenerator推荐阅读顺序
不要一上来从文件头读到文件尾。建议按问题倒推:
第一轮:看 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 是一个可以被调用、批处理、流式输出、转换、组合的工作单元。这句话有四层含义:
- 可调用:单输入单输出,
invoke(input); - 可异步调用:
ainvoke(input); - 可批处理:多输入多输出,
batch(inputs); - 可流式输出:逐块输出,
stream(input); - 可组合:通过
|或pipe()组合成更大的 Runnable; - 可配置:每次运行可携带 tags、metadata、callbacks、max_concurrency 等配置;
- 可追踪:通过 callback manager / LangSmith 记录中间步骤;
- 可模式化:暴露 input_schema、output_schema、config_schema。
Runnable 的核心方法
源码中最重要的方法可以分成四类。
执行方法
单次调用:invoke(input, config=None, **kwargs) -> outputainvoke(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_schemaoutput_schemaconfig_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 | parserPython 实际执行顺序是:
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 会通过工具函数判断函数是否接受 config 或 run_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 输出:PromptValuePromptValue 可转 messages:[ SystemMessage(content="你是一个旅行规划助手。"), HumanMessage(content="请为 东京 规划 5 天行程。")]
ChatModel 输出:AIMessageAIMessage(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 = inputx1 = 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 内部通常会维护:
firstmiddlelast这是为了类型推断和执行优化。你可以理解为:
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 / atransformRunnableConfig 传播
RunnableConfig 是什么
RunnableConfig 是每次执行 Runnable 时携带的运行配置。
常见字段:
tags: list[str]metadata: dict[str, Any]callbacks: callback handlers 或 callback managerrun_name: 当前运行名称run_id: 当前运行 IDmax_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 + evalmetadata: component + case_idcallbacks: 合并或继承configurable: 合并工程建议:
链路级 tags:标注模块,如 travel / planner / parser请求级 metadata:标注业务请求,如 user_id / session_id / case_id评估级 metadata:标注测试样本,如 dataset / case_id / expected_intentCallback 生命周期
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: StrOutputParseron_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源码证据:
- https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/base.py
- https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/config.py
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 | parsercoerce_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 的区别
| 对比项 | RunnableBranch | LangGraph conditional edge |
|---|---|---|
| 所在层级 | LangChain Core Runnable | LangGraph 图编排 |
| 状态模型 | 单次输入输出 | 共享 State |
| 路由粒度 | 链内部轻量分支 | 节点级流程控制 |
| 可观测性 | Runnable trace | Graph 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)源码证据:
- https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/base.py
- https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/branch.py
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、合并配置、委托调用、处理异常”四个位置,就能快速判断它属于协议包装还是业务变换。
源码证据:
- https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/base.py
- https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/retry.py
- https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/fallbacks.py
扩展协作总结、伪代码与源码证据
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def extend(runnable, config=None, retry=None): bound = runnable.with_config(config or {}) return bound.with_retry(**retry) if retry else bound源码证据:
- https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/base.py
- https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/fallbacks.py
11. 工程决策与适用场景
搜索目标
在旅行规划助手项目中查找所有类似代码:
prompt | modelprompt | model | parserretriever | 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 | `prompt | model | parser` | RunnableSequence | user_request | AIMessage | IntentSchema | 否 / 可选 |
planner_chain | `prompt | model.with_structured_output(…)` | RunnableSequence | TravelState | AIMessage | Plan | 否 / 可选 | 是 |
final_chain | `prompt | model | StrOutputParser()` | RunnableSequence | full_state | AIMessage | str | 是 |
research_chain | `dict | prompt | model` | RunnableParallel + RunnableSequence | destination | dict | AIMessage | 部分支持 |
推荐项目目录补充
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.pyrunnable_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. 参考资料与下一篇衔接
官方资料
-
LangChain Core Runnables Reference
https://reference.langchain.com/python/langchain-core/runnables/ -
Runnable API Reference
https://reference.langchain.com/python/langchain-core/runnables/base/Runnable/ -
RunnableSequence API Reference
https://reference.langchain.com/python/langchain-core/runnables/base/RunnableSequence/ -
RunnableParallel API Reference
https://reference.langchain.com/python/langchain-core/runnables/base/RunnableParallel/ -
RunnableConfig API Reference
https://reference.langchain.com/python/langchain-core/runnables/config/RunnableConfig/ -
LangChain GitHub Source:
base.py
https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/base.py -
LangChain GitHub Source:
config.py
https://github.com/langchain-ai/langchain/blob/15b0a4930b5306bb2174f16d92fe4b7837147d0a/libs/core/langchain_core/runnables/config.py -
LangChain Models Documentation
https://docs.langchain.com/oss/python/langchain/models -
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 源码解剖打基础。