ARTICLE · 1228027
【LangGraph源码精读】第05期|线程池还是进程池?LangGraph的并发选择藏着性能秘密
【LangGraph源码精读】第05期|线程池还是进程池?LangGraph的并发选择藏着性能秘密
30篇源码精读系列·第05篇(执行引擎层第五篇)。你配置了并行分支,以为两个节点会同时跑,结果总耗时还是两个节点的和。为什么?因为你用了invoke而不是ainvoke?因为节点是同步函数?因为同步屏障在等慢节点?这篇把LangGraph的并发执行器彻底拆开:PregelRunner怎么调度、Sync和Async有什么区别、超时和重试怎么配合、慢节点怎么拖垮整个Super-step。
一、开头:你的并行Agent为什么没有变快?
你写了一个Agent,里面有"搜索"和"代码生成"两个独立节点,配置成并行执行。你以为总耗时是max(搜索, 代码生成),结果实际耗时是搜索+代码生成。
排查了半天,发现可能是这些原因:
- ● 你用了invoke(),节点是同步函数,并发用的是线程池,但节点里有CPU密集型计算,Python GIL导致串行
- ● 你用了ainvoke(),但节点函数忘了写async,还是同步执行
- ● 两个节点确实并行跑了,但其中一个特别慢,同步屏障导致快节点必须等慢节点
- ● 你以为ToolNode里的多个工具调用是并行的,但用的是v1版本,内部是串行的
并发是LangGraph中最容易"看起来对但实际不对"的地方。 配置了并行分支不代表真的并行,用了ainvoke不代表性能一定好。这篇把并发执行器的每一层都拆开,让你知道你的Agent到底在怎么跑。
二、PregelRunner:并发执行的核心
PregelRunner在pregel/_runner.py里,是Execute阶段的执行者。它不决定图结构,只负责把已经选好的任务执行起来。
2.1 PregelRunner.tick()的6步执行流程
fast path是一个重要的性能优化。 如果当前Super-step只有一个任务(顺序图的常见情况),PregelRunner不会创建线程池或asyncio任务,直接同步执行,避免并发开销。这意味着顺序图的性能和普通函数调用几乎一样。
2.2 commit():并发安全的关键
回顾上一期讲的:commit()不直接修改共享Channel,而是把结果写入task.writes缓冲区,调用loop.put_writes(task_id, writes)。
这就是"状态副本+输出缓冲"在并发层的具体实现。 PregelRunner只负责"跑任务",不负责"合并结果"。合并是after_tick的事,在所有任务完成后单线程进行。这种职责分离让并发执行变得简单且安全。
三、Sync vs Async:两条完全不同的执行路径
LangGraph有两套执行循环,对应同步和异步调用。
3.1 为什么Sync不支持超时?
这是一个常见的困惑。Python的线程没有可靠的取消机制——你不能安全地强制终止一个正在运行的线程。所以LangGraph在编译时就拒绝给同步节点配置timeout。
最佳实践:调用LLM或外部API的节点,永远写成async def。 这样你才能用timeout防止慢API拖垮整个Agent。同步节点只适合纯本地计算且确定不会卡住的场景。
3.2 什么时候用Sync,什么时候用Async?
一个常见误区:用了ainvoke就一定更快。 如果你的节点都是同步函数,ainvoke内部还是会用线程池跑,和invoke没有本质区别。只有节点本身是async def,ainvoke才能发挥协程的优势。
四、并发的边界:什么能并行,什么不能
这是理解LangGraph并发最关键的一点。
4.1 可以并行的情况
4.2 不能并行的情况
核心原则:同一Super-step内无数据依赖的节点可以并行,有依赖的必须串行。 LangGraph通过Channel订阅关系自动判断哪些节点可以并行——你不需要手动指定,编译时trigger_to_nodes索引已经决定了。
4.3 ToolNode的v1和v2:并发粒度的区别
v2是create_react_agent的默认版本。 它的优势是每个工具调用有独立的task identity,可以单独重试、单独追踪、单独超时。这在生产环境中非常重要——一个工具调用失败不会影响其他已经成功的工具调用。
五、超时机制:v1.2的生产级能力
超时是v1.2新增的重要功能,专门解决"慢节点拖垮整个Agent"的问题。
5.1 两种超时类型
run_timeout是"最多跑多久",idle_timeout是"多久没动静就算挂了"。 两者可以同时配置,哪个先触发就取消节点。
5.2 idle_timeout的进度信号
idle_timeout的时钟在以下情况自动刷新(默认refresh_on="auto"):
这意味着:只要节点在"干活"(输出token、调用工具、写状态),idle_timeout就不会触发。只有节点真正卡住了(比如死锁、无限等待),才会超时。
5.3 heartbeat模式:更严格的空闲检测
heartbeat模式适合"节点可能产生很多噪音输出但实际没进展"的场景。你需要在节点的关键步骤手动调用runtime.heartbeat(),告诉运行时"我还在干活"。
5.4 超时后的处理
注意:超时的节点的部分写入会被清除。 这意味着如果节点执行了一半超时了,它已经产生的输出不会保留——下一次重试会从头开始。这是"节点必须幂等"的另一个原因。
六、重试机制:RetryPolicy详解
重试是生产级Agent的标配。LangGraph的RetryPolicy在v1.2做了重大升级。
6.1 RetryPolicy参数
退避策略: 第1次重试等0.5秒,第2次等1秒,第3次等2秒...直到max_interval。加jitter是为了避免多个Agent同时重试导致下游服务被打垮。
6.2 default_retry_on:默认重试哪些异常
HTTP库(requests/httpx)只重试5xx状态码,不重试4xx。 这是合理的——4xx是客户端错误(比如参数不对、鉴权失败),重试也没用;5xx是服务端错误,重试可能成功。
6.3 重试的粒度
重试的粒度是节点级,不是graph级。 这是BSP模型的优势——一个节点失败重试,其他已经成功的节点结果保留在pending writes里,不会浪费。
七、错误处理:error_handler和Saga模式
重试解决不了所有问题。有些错误重试也没用(比如支付失败、库存不足),这时候需要error_handler做补偿。
7.1 error_handler的执行流程
7.2 Saga补偿模式
error_handler最经典的用法是Saga模式——分布式事务中的补偿机制。
error_handler可以返回Command(goto="compensation_node"),把执行流路由到补偿节点。 这比"整个graph失败回滚"灵活得多——你可以精确控制失败后做什么。
7.3 error_handler的限制
八、慢节点问题:同步屏障的代价
这是BSP模型最被诟病的缺点:同步屏障。
8.1 什么是慢节点问题
假设一个Super-step有3个并行节点:A(1秒)、B(1秒)、C(30秒)。
A和B在1秒时就完成了,但必须等C到30秒才能进入Update阶段。 这29秒的等待就是同步屏障的代价。
8.2 怎么缓解慢节点问题
核心观点:对于AI Agent,同步屏障的代价通常可以接受。 因为Agent的瓶颈在LLM调用(秒级),而不是执行引擎的同步屏障(毫秒级)。只有当你有特别慢的非LLM节点时,才需要专门优化。
九、性能优化建议
十、常见坑:并发的5个隐蔽问题
十一、面试关联:并发执行器的5道高频题
十二、本期小结
并发执行器,核心就6句话:
- 1. PregelRunner只负责"跑任务",不负责"合并结果"。 commit()把结果写入task.writes缓冲区,不直接改Channel;合并是after_tick的事,单线程确定序进行。这种职责分离让并发安全且简单。单任务走fast path直接执行,多任务才用并发。
- 2. Sync和Async是两条完全不同的执行路径。 Sync用线程池+Runnable.invoke,Async用asyncio+Runnable.ainvoke。只有Async支持超时——因为Python线程无法安全取消,asyncio协程可以。调用LLM和外部API的节点永远写成async def。
- 3. 并发的边界由Channel订阅关系自动决定。 同一Super-step内无数据依赖的节点可以并行,有依赖的必须分两个Super-step。你不需要手动指定并行,编译时trigger_to_nodes索引已经决定了。ToolNode v2让每个工具调用有独立task identity。
- 4. 超时机制(v1.2)有两种:run_timeout和idle_timeout。 run_timeout是硬墙钟上限不刷新;idle_timeout是空闲超时,有进度信号时自动刷新。超时后抛出NodeTimeoutError,清除部分写入,默认可重试。Send可以动态覆盖单个调用的超时。
- 5. 重试和error_handler配合构成完整的容错体系。 RetryPolicy用指数退避+抖动,默认重试网络异常和5xx,不重试代码错误。重试耗尽后error_handler执行补偿,可以返回Command路由到补偿节点(Saga模式)。重试粒度是节点级,不影响同Super-step其他已成功节点。
- 6. 同步屏障是BSP的固有代价,但对AI Agent通常可接受。 快节点必须等慢节点完成才能进入Update阶段。缓解方案包括拆分慢节点、超时重试、缓存结果。Agent的瓶颈通常在LLM调用(秒级),同步屏障的等待(毫秒级)相对很小。
十三、下一期预告
下一期(源码06),执行引擎层最后一篇,失败重试和容错的深入:
【LangGraph源码精读】第06期|节点失败了怎么办?LangGraph的重试和部分失败恢复
从RetryPolicy的源码实现到pending writes恢复机制,从error_handler的Saga补偿到drain优雅关闭,搞懂LangGraph怎么保证"节点失败不丢状态、部分失败不重跑全部"。
你在使用LangGraph时遇到过并发性能或节点超时的问题吗?评论区聊聊,我优先写你最关心的场景。
本文为《LangGraph源码精读30讲》系列第05篇。内容基于LangGraph v1.2.11源码结构梳理,核心文件位于libs/langgraph/langgraph/pregel/目录下的_runner.py(PregelRunner)、_loop.py(Sync/AsyncPregelLoop)、_retry.py(重试逻辑)。超时和error_handler为v1.2新增功能。版本更新后部分细节可能变化。如有错漏欢迎指正。















































































