基于对 LangGraph 源码的阅读与讨论,整理成一份以「概念 + 入口 + 结构」为主的学习笔记,方便后续翻查和扩展。
一、从 StateGraph 的泛型说起
1.1 这行代码在说什么?
python
class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
- Generic[X, Y, Z] :来自
typing,表示「泛型类」,方括号里是类型参数。 - StateT, ContextT, InputT, OutputT:四个类型变量,分别表示「状态 / 上下文 / 输入 / 输出」的类型。
- 在 Python 里,这些类型主要给类型检查器和 IDE 用 ,运行时不做强制;你传的 TypedDict 类(如
State)会作为state_schema存起来,类型上就对应StateT。
1.2 和 Java 泛型的区别
- Java:泛型在编译期检查、运行期擦除;
List<String>限制元素类型。 - Python:泛型主要是「声明 + 统一含义」------标注「这里/后面都用同一个类型」,由 mypy/Pyright 检查,解释器不强制。
二、几个 Python 语法点
:=(海象运算符) :在表达式里完成赋值并返回该值,例如if (x := get_value()) is not None:。- 参数名与
*:调用时一旦用了「名字=值」,其后的实参也必须带名字;定义里单独一个*表示其后的参数必须用关键字传(keyword-only)。 - 同一参数不能传两次 :既按位置又按关键字传同一参数会触发
TypeError: got multiple values for argument。
三、add_node 在做什么?
- 把
add_node(fn)/add_node("name", fn)统一成「节点名 + action」。 - 校验节点名:不重复、不用 START/END、不含保留字符。
- 确定 input_schema :优先显式传入 → 否则从函数第一个参数的类型注解推断 → 再否则用图的
state_schema。 - 从返回类型注解里解析 Command/Literal,得到
ends(用于图可视化/路由)。 - 用
coerce_to_runnable包装 action,和 input_schema、策略等一起放进 StateNodeSpec ,写入self.nodes[node],并调用_add_schema(input_schema)登记 schema。
四、Channel 和 Trigger 是在哪建的?
- 状态 channel (state 各 key 的 channel/reducer):在建图阶段 ,通过
_add_schema(schema),内部用_get_channels(schema)根据 schema 字段注解生成 channel,写入self.channels。- 调用时机:
StateGraph.__init__(state/input/output schema)和 add_node 末尾(节点的 input_schema)。
- 调用时机:
- 分支/汇聚 channel (如
branch:to:xxx、join:xxx)和 trigger :在 compile() 里,对每个节点/边调用 attach_node / attach_edge 时创建;- 例如:每个非 START 节点有
triggers=[branch_channel],多源汇聚时创建 join channel 并nodes[end].triggers.append(channel_name)。
- 例如:每个非 START 节点有
五、数据流转的入口
- 对外入口 :
Pregel.stream()/invoke()(main.py);invoke内部只是消费stream()的迭代器。 - 第一次把用户 input 写进 channel :在 SyncPregelLoop.enter 里调
_first(),其中map_input(input_keys, self.input)把 input 转成 channel 写入,再apply_writes(..., PregelTaskWrites((), INPUT, input_writes, []), ...)应用到 channel。 - 每一步推进 :
while loop.tick():(main.py 2643/2969)里,tick() 调 prepare_next_tasks 算本拍任务、apply_writes 应用上一拍 writes;runner.tick() 执行本拍节点;after_tick() 把本拍 writes 写入 channel、更新 checkpoint、得到 updated_channels 供下一拍用。
六、每次 stream/astream 都会起一个 tick 流转器
- 每次 stream() / astream() 都会新建一个 SyncPregelLoop / AsyncPregelLoop ,在
with里跑while loop.tick():。 - 同步入口:
main.py约 2582(创建 loop)、2643(while loop.tick():);异步:约 2890、2969。 - tick 的具体逻辑在
_loop.py的 PregelLoop.tick() (约 459 行),sync/async 共用;区别是执行节点时 sync 用runner.tick(),async 用runner.atick()。
七、单次 tick 的完整流程与结构
7.1 主循环
while loop.tick():
match_cached_writes(); runner.tick(...); loop.after_tick()
7.2 tick() 里
- 检查
step > stop→ 若超则返回 False。 - prepare_next_tasks(...) :根据 checkpoint、checkpoint_pending_writes、updated_channels 、trigger_to_nodes 算出本拍 tasks。
- 若 tasks 为空则
status="done"并返回 False。 - 若有 checkpoint_pending_writes 则 _match_writes 挂到对应 task.writes。
- interrupt_before 检查;对已有 writes 的 task 做 output_writes;返回 True。
7.3 after_tick() 里
- apply_writes(checkpoint, channels, tasks.values(), ...) :把本拍 task.writes 写入 channel,更新 channel_versions,返回 updated_channels。
- 若 updated_channels 与 output_keys 有交集则 _emit("values", ...)。
- checkpoint_pending_writes.clear() ;_put_checkpoint(...);interrupt_after 检查。
7.4 关键结构
- channels :
Mapping[str, BaseChannel],channel 名 → 当前 step 的 channel 实例(从 checkpoint 恢复);典型 key 包括__start__、state 各 key、branch:to:{node}、join:...。 - trigger_to_nodes :
dict[str, list[str]],channel 名 → 被该 channel 触发的节点列表,由 PregelNode.triggers 反推(main.py 3243--3248)。 - updated_channels :上一拍 apply_writes 更新过的 channel 集合,供本拍 prepare_next_tasks 用。
- Checkpoint:含 channel_values、channel_versions、versions_seen、updated_channels 等(checkpoint.base)。
- checkpoint_pending_writes :
list[(task_id, channel_name, value)],在 tick 里 _match_writes 挂到 task.writes,在 after_tick 里 apply 后 clear。 - PregelNode:triggers、channels、writers、bound(_read.py)。
八、走到 END 后谁被释放?
- CompiledStateGraph / Pregel 实例 :不会因为一次 run 结束而释放,可重复
invoke/stream。 - 本轮的 SyncPregelLoop / AsyncPregelLoop :退出 with 时 exit 执行,loop 可被 GC;与这次 run 相关的 checkpoint、channels、tasks 随之不再被引用。
九、执行模型:图 + 类 BFS 的按层推进
- 结构:有向图(节点、边、channel/trigger)。
- 执行 :按「拍」(tick/superstep)推进;每拍根据 updated_channels 和 trigger_to_nodes 算出要跑的节点,跑完后 apply_writes 更新 channel,再进入下一拍------类似 BFS 的「按层」,但由**数据流(channel 写入 + trigger)**决定谁跑,不是显式 BFS 队列。整体是 BSP(Bulk Synchronous Parallel)风格。
十、并发链与 join
- 何时需要图上并发链 + join :不同链要不同 retry/cache/中断策略、按链观测或恢复、人审按链、多 agent/多工具并行再合并决策等;若只是「一个节点里并发调几个 API」,用 async/Executor 即可。
- 并发链共享 :同一个 run 里只有一个 loop、一个 step 计数;recursion_limit 也是整图一个。
- 长链拖短链 :若一条链远长于其他,短链会早到 join 前「空等」,并行度下降、延迟被长链拉长。主要在设计层解决:少用不必的 join、平衡链长、用流水线或子图替代「长链+短链同图 join」等。
十一、后端视角:像传统流程编排 + Spring 式默认机制
- 图 + 节点 + 边 对应「流程编排 / 微服务式步骤」;对简单业务,除了 checkpoint(及 interrupt/resume),很多用 async + 状态也能实现。
- 价值一部分来自「默认机制完善」:retry_policy、step_timeout、recursion_limit、config、stream 模式、与 LangSmith 等集成,类似 Spring 的「开箱即用」;另一部分来自针对「有状态、可中断、可流式」agent 的图+state+channel 模型。
十二、部署实践:单例图与 loop 上限
- 单例 :一个进程/worker 一般只 compile 一次 ,所有 API 复用同一个 CompiledStateGraph ;每次请求只调 invoke(input, config) 或 astream(...),各请求的 state/checkpoint 在各自 run 的 loop 内,互不干扰。
- 单次 run 的 tick 数 :有上限,由 recursion_limit 决定;默认 10000 (环境变量 LANGGRAPH_DEFAULT_RECURSION_LIMIT 可覆盖)。
- 并发 run 数 :框架不设上限,由进程与部署决定。
- max_concurrency :限制的是单次 run 内并发执行的 node 数,不限制并发请求数。
- QPS:框架没有「默认 QPS」概念,需按业务与部署压测。
十三、代码入口速查
| 内容 | 文件 | 位置 |
|---|---|---|
| stream 主循环 | pregel/main.py | 2643:while loop.tick(): |
| astream 主循环 | pregel/main.py | 2969:while loop.tick(): |
| tick() | pregel/_loop.py | 459--535 |
| after_tick() | pregel/_loop.py | 537--570 |
| prepare_next_tasks | pregel/_algo.py | 369 起 |
| apply_writes | pregel/_algo.py | 217 起 |
| _trigger_to_nodes | pregel/main.py | 3243--3248 |
| recursion_limit 默认 | _internal/_config.py | DEFAULT_RECURSION_LIMIT = getenv(..., "10000") |
文档路径:libs/langgraph/ 下;checkpoint 类型见 libs/checkpoint/langgraph/checkpoint/base/。