文章目录
- 一、写并行工作流踩过的破防大坑
- 1.1 defer到底是个啥逻辑
- 二、完整可运行实战代码,一眼看懂执行顺序
- 2.1 拆解三层执行流程
- 第一层:普通并行节点优先调度
- 第二层:所有常规节点全部闭环
- 第三层:延迟队列节点统一唤醒执行
- 三、defer解决的核心痛点,对比两种写法差距
- 3.1 不使用defer的传统写法有多折磨人
- 3.2 使用defer后的极简结构
- 四、defer适用场景避坑清单
- 五、高频面试考点,开发必掌握细节
- 5.1 标记defer=True和手动把节点放在流程末尾有什么本质区别?
- 5.2 多个同时标记defer=True的节点,执行顺序可控吗?
- 5.3 defer节点运行抛出异常,前面节点的结果会回滚吗?
- 5.4 延迟节点能不能修改、更新全局状态?
- 六、一句话总结核心逻辑
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。
一、写并行工作流踩过的破防大坑
咱先唠唠实打实的开发场景,谁写LangGraph没搞过并行分支?
需求很简单:同时开两个分支,一个生成主题诗歌,一个写同主题段子,两边全部跑完之后,统一校验两份内容有没有正常产出。
最开始我老老实实连线,写诗节点连审计、写笑话节点也连审计。
分支少的时候看着还行,一旦业务加需求,并行节点干到十几个,那连线画得跟蜘蛛网一样,改一次逻辑要删一堆边,改到眼晕。
上次迭代加了5个并行任务,光是新增连线花了半小时,改完跑测试还漏连一个,审计节点提前执行,一半数据没读到,线上直接报错,被产品追着问半小时。
有没有不用疯狂连线的偷懒办法?真有,一行参数defer=True直接搞定所有收尾逻辑。
1.1 defer到底是个啥逻辑
字面意思就是延迟、延后,框架内部有一套专属调度规则。
只要给节点标记defer=True,框架自动给它挂个“压轴标签”,不管你从START连了多少条边指向它,它都会原地待命,等图里所有不带defer的普通节点全部执行完毕,才会启动这个延迟节点。
代码写法极简,只需要在add_node时追加参数:
builder.add_node("audit_node", audit_node, defer=True)就这一行,收尾节点自动排到全流程最后,不用手动维护任何分支连线。
二、完整可运行实战代码,一眼看懂执行顺序
直接上可复制运行的完整demo,用DeepSeek大模型生成内容,搭配状态存储和日志打印,跑一遍就能直观看到延迟节点的执行时机。
from typing import TypedDict from langgraph.graph import StateGraph, START, END from langchain_core.messages import HumanMessage from langchain_deepseek import ChatDeepSeek from loguru import logger from dotenv import load_dotenv load_dotenv(override=True) model = ChatDeepSeek( model="deepseek-v4-flash", extra_body={"thinking": {"type": "disabled"}} ) class OverAllState(TypedDict): topic: str poem: str joke: str # 并行节点1:生成七言绝句 def node_a(state: OverAllState) -> OverAllState: poem = model.invoke([HumanMessage(f"写一首关于 {state['topic']} 的七言绝句")]).content return {"poem": poem} # 并行节点2:生成对应主题笑话 def node_b(state: OverAllState) -> OverAllState: joke = model.invoke([HumanMessage(f"写一个关于 {state['topic']} 的笑话")]).content return {"joke": joke} # 延迟审计节点,defer=True def audit_node(state: OverAllState) -> OverAllState: logger.info( f"全部任务执行完毕," f"诗歌 {'✅ 已生成' if state.get('poem') else '❌ 未生成'}," f"笑话 {'✅ 已生成' if state.get('joke') else '❌ 未生成'}" ) return {} # 图构建流程 builder = StateGraph(state_schema=OverAllState) builder.add_node("node_a", node_a) builder.add_node("node_b", node_b) builder.add_node("audit_node", audit_node, defer=True) # 全部节点统一从START触发 builder.add_edge(START, "node_a") builder.add_edge(START, "node_b") builder.add_edge(START, "audit_node") builder.add_edge("node_a", END) builder.add_edge("node_b", END) builder.add_edge("audit_node", END) graph = builder.compile() res = graph.invoke({"topic": "布偶狗"}) print(res)运行输出日志能清晰看到,诗歌和笑话先完成打印,审计日志永远最后输出,不会提前读取残缺状态。
2.1 拆解三层执行流程
第一层:普通并行节点优先调度
START同时触发三个节点,node_a、node_b正常进入执行队列,audit_node因为带defer标记,直接存入延迟队列,原地等待,不会抢占调度资源。
第二层:所有常规节点全部闭环
写诗、写笑话两个并行分支全部跑完,状态里完整存入poem、joke字段,没有遗漏数据。
第三层:延迟队列节点统一唤醒执行
普通节点无剩余待执行任务,框架自动取出延迟队列内的节点运行,此时状态数据完整,审计、汇总逻辑不会出现缺字段问题。
三、defer解决的核心痛点,对比两种写法差距
3.1 不使用defer的传统写法有多折磨人
传统汇聚逻辑,每一个并行分支都必须单独连线到收尾节点:
START → node_a → audit_node → END
START → node_b → audit_node → END
一旦新增并行任务,就要新增一条指向审计节点的边,并行节点数量涨到10个,就要手动维护10条连线,后期迭代、删改分支时极易漏连线,产生数据不全bug。
之前有次迭代加了3个并行节点,忘了补连线,审计节点只能读到一半数据,校验逻辑直接失效,排查bug花了快一小时。
3.2 使用defer后的极简结构
所有并行节点直接连END,审计节点仅从START触发并标记延迟,无论新增多少并行任务,都不用修改审计相关代码和连线,完全零改动。
代码可读性直接拉满,新人接手看图就能分清业务节点和后置收尾节点,不用梳理错综复杂的汇聚连线。
四、defer适用场景避坑清单
| 使用场景 | 适配度 | 实操说明 |
|---|---|---|
| 全流程日志统计、耗时记录 | 极高适配 | 统一统计所有节点运行时长、执行状态,无需提前读取数据 |
| 并行结果统一审计校验 | 极高适配 | 确保所有分支产出完整后再做合规、完整性检查 |
| 多分支数据汇总整合 | 适配 | 收集所有并行输出,拼接、整理成统一返回格式 |
| 资源释放、临时文件清理 | 适配 | 全流程结束后统一关闭会话、删除临时缓存文件 |
| 核心业务校验、数据落库 | 严禁使用 | 延迟节点报错不会回滚前面所有节点的执行结果,数据不一致风险极高 |
五、高频面试考点,开发必掌握细节
5.1 标记defer=True和手动把节点放在流程末尾有什么本质区别?
手动后置节点相当于排队,必须所有前驱节点挨个连线,一旦新增分支就要改图结构,维护成本极高。
defer是全局调度标记,和连线顺序无关,无论多少并行分支,只需要给节点加一个参数,自动后置执行,不用修改任何边逻辑。
举个生活化例子:手动后置是排队买票,每个人都要站在前一个人身后;defer是专属压轴通道,不管前面多少人,永远最后登场。
5.2 多个同时标记defer=True的节点,执行顺序可控吗?
完全不可控,同一批延迟节点会在同一时间被唤醒,调度先后由框架底层队列决定,没有固定顺序。
如果两个延迟节点存在数据依赖,比如B节点需要读取A节点更新后的状态,千万不要同时加defer,改用普通有向边定义先后执行顺序。
之前踩过这个坑,两个延迟节点一个汇总、一个导出,偶尔导出先执行,汇总数据还没更新,导出文件内容残缺,排查半天才找到根源。
5.3 defer节点运行抛出异常,前面节点的结果会回滚吗?
不会回滚,所有普通节点执行完成后状态已经持久化存储,延迟节点报错只会中断自身逻辑,前面生成的所有数据全部保留。
这也是为什么核心业务逻辑不能放在延迟节点,比如订单落库、数据校验,一旦校验失败,数据已经存入,还要额外写补偿逻辑处理异常,增加开发工作量。
日志、清理这类非核心逻辑放延迟节点完全没问题,就算日志写入失败,业务数据不受任何影响。
5.4 延迟节点能不能修改、更新全局状态?
完全可以,defer只是控制执行时机,不限制节点读写state的能力。
延迟节点return返回的字典,同样会合并到全局状态里,只是它执行完成后,图内没有剩余普通节点,不会再触发其他业务节点,仅剩其余延迟节点会依次运行。
六、一句话总结核心逻辑
defer=True就是给节点打上压轴标签,所有常规业务节点全部执行完毕,它才会启动,专门用来处理并行工作流的后置收尾、汇总、审计逻辑,大幅简化图连线结构,降低复杂分支的维护成本。
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。