LangGraph零基础入门:用State、Node与Edge构建可控Agent工作流

LangGraph零基础入门:用State、Node与Edge构建可控Agent工作流 去年做一个客服工单自动分类的 Agent 时我第一次产生了“AI 流程失控”的感觉。第一版用 LangChain 的 AgentExecutor 串起大模型和几个工具跑通 Demo 只花了一个晚上但到了第二版业务方要求“先判断工单类型如果涉及退款就进入人工审批其他类型自动回复并且每一步都要能追溯”。这个需求一加进来AgentExecutor 的对话式循环就非常难精细控制流程走到了哪一步、当前状态是什么、为什么没有走退款分支全都像一个黑盒。那段时间我被迫给链路里加各种临时 log最后几乎是用 if-else 硬拼出来的。后来我把流程迁到 LangGraph 上才真正意识到问题不在“模型不够聪明”而在“执行框架不够可控”。LangGraph 的出发点不是让 Agent 更灵活而是让 Agent 的执行过程变成一张可绘制、可检查、可复现的图节点负责干活边负责决定下一步去哪State 负责把每一步的状态完整地记录下来。这篇内容就是从一个零基础开发者的视角把 LangGraph 的 State、Node、Edge 和多 Agent 协作从头捋一遍。1. 先搞清楚 LangGraph 解决的是哪一类失控1.1 LangChain 跑得很顺为什么还需要 LangGraph很多人看到“LangGraph”的第一反应是用 LangChain 不就行了确实如果只是做一个简单的对话式 AgentLangChain 的 AgentExecutor 或 create_react_agent 都很方便。把系统提示词、工具列表和模型传进去它自己会决定“下一步调用哪个工具”跑起来效果也不错。但问题在于这种“对话式循环”把控制权交给了模型自己。模型每轮只会做一个动作要么调工具要么给最终回复。从表面看很灵活但一旦业务流程里有确定的规则情况就变得别扭。比如客服场景里工单进入系统后先判断类型退款类必须转人工审批技术咨询类走自动回复自动回复前还需要查知识库查不到知识库答案时要落到“人工待处理”队列。这套流程如果用 AgentExecutor 写模型很可能跳过“先分类”这一步或者把退款工单直接自动回复了。你当然可以写很多提示词去约束但提示词约束的本质是概率不是确定性。只要业务流程不能被概率性行为覆盖你就需要一个“流程大于模型”的执行引擎。LangGraph 的出现核心就是解决这个“失控”问题。它不强求你按模型自己的想法走而是把整个 Agent 任务画成一张有向图。模型仍然是干活的主力但“接下来去哪”这件事既可以通过模型判断也可以通过规则代码决定。底层执行器只认图的结构不认模型的“自由意志”。1.2 State、Node、Edge 三件套先说清楚各自边界LangGraph 上手后你会发现它反复强调的就三样东西组件作用一句话理解State保存整个任务过程中的所有数据一张公开的工作台谁都能读写但写入有规则Node执行具体动作的函数单元一个节点只做一件事完成后把结果写回 StateEdge决定下一步执行哪个节点普通边直接走条件边像 if-else 一样选择分支这三样拼起来LangGraph 就像一个“带流程图的机器人流水线”State 是当前流水线上的工件Node 是每个工位Edge 是工位之间的传送带和分拣闸机。很多人容易把 Node 当成普通 Python 函数写一个函数就算完事。实际上 Node 在 LangGraph 里有明确边界它接收当前 State执行任务返回一份“要更新的状态片段”。它不应该自己偷偷维护一个私有变量也不应该直接修改外部全局变量。原因后面会细说。另一个容易混淆的点是 Edge。这里的 Edge 不是浏览器而是图里连接节点的边。LangGraph 的灵活之处正在于它支持普通边也支持条件边后者会根据 State 内容动态决定往哪个节点走。这一步是 LangGraph 和很多“工具调用循环”框架拉开差距的关键。2. State从“到处传参”到“全局状态容器”2.1 先定义一份 State而不是先想着写节点LangGraph 里最先要设计的不是节点的函数体而是 State。State 决定了整张图“能看到什么数据、能保存什么结果”。如果 State 设计得不对后面所有节点都会跟着别扭。常见的写法是用 TypedDict 声明from typing import TypedDict class State(TypedDict): query: str # 用户原始输入 category: str # 工单分类结果 answer: str # 最终回复 needs_review: bool # 是否需要人工审核在 LangGraph 的图里State 是一个共享容器。每个节点都能读取它节点执行完可以返回一份“更新”更新里的字段会被合并回 State。听起来很像一个全局字典但它比普通字典严格得多节点不能直接改 State只能通过返回值声明“我想改哪几个字段”。这样做有个明显好处所有数据变更都有固定出口图每次执行前后的 State 都能被完整记录下来。你可以在任何一步打印 State看到当前任务的全部上下文。这一点在调试和复盘时价值极大。2.2 节点函数如何更新 State不是深合并是按键覆盖在 LangGraph 里节点函数最常见的形式是“入参一个 State返回一个 dict”def classify_node(state: State): # 这里假装调用了 LLM 分类 return {category: refund}当这个节点执行完LangGraph 会把返回值里的category字段更新到 State 中其他字段保持不变。这里有一个非常关键的默认行为LangGraph 默认的 state 更新逻辑是“浅覆盖”不是“深合并”。如果你在 State 里放了一个列表字段比如messages: [{role: user, content: 你好}]然后节点返回{messages: [{role: assistant, content: 你好}]}默认情况下新的列表会直接替换旧的列表而不是追加。这就是新手最常踩的坑之一。你以为返回了一条新消息之前的对话历史会自动保留但实际它是被整体覆盖了。想要“追加”语义必须给字段配置 Reducer。2.3 消息列表必须用 Reducer否则对话历史会丢LangGraph 内置了一个非常常用的 Reduceradd_messages。它的作用就是把新消息追加到旧消息后面而不是覆盖from typing import Annotated from langgraph.graph.message import add_messages class State(TypedDict): messages: Annotated[list, add_messages]之后所有节点只要返回{messages: [新消息]}LangGraph 就会自动把新消息追加到历史消息列表尾部。从设计角度理解Reducer 是 LangGraph 里管理“复杂状态更新”的机制。默认的覆盖策略适合大多数基础字段但凡是“累积、追加、合并”类的数据都需要自己定义 Reducer。这其实是在提醒你在将状态写入 State 之前想清楚这个字段的更新语义是“替换”还是“累加”。2.4 把 State 当成唯一事实来源而不是变量中转站我建议把 State 理解成整个流程的“项目白板”而不是一段临时变量。所有节点之间需要共享的信息都应该显式地放在 State 上。比如 A 节点算出了一个中间分数B 节点要用到它那就把分数写进 State而不是通过类成员变量或全局变量传递。这样设计的原因是 LangGraph 有“可重放”能力。配合检查点机制你可以保存每一步的 State出问题时回放到某一步重新执行。如果信息藏在节点内部的全局变量里State 记录得再完整也是残缺的。长期使用 LangGraph 后你会发现可重放能力比“跑得快”重要得多因为 Agent 应用一旦进入生产最难的事不是让它跑通而是让它出问题时能被定位和修复。建议定义 State 时把所有会在多个节点之间流动的字段都写出来。哪怕暂时不确定用不用也比后面补字段时回头改节点逻辑简单。3. Node把它理解成可挂起的任务单元而不是普通 Python 函数3.1 Node 的通用形态接收 State干活返回更新LangGraph 的 Node 本质上是一个可调用对象最常见的是普通函数def query_node(state: State): query state[query] return {answer: f已收到问题{query}}把函数加入图的方式也直观from langgraph.graph import StateGraph, START, END graph StateGraph(State) graph.add_node(classify, classify_node) graph.add_node(answer, query_node) graph.add_edge(START, classify) graph.add_edge(classify, answer) graph.add_edge(answer, END)运行图的入口在大多数版本里是编译后调用app graph.compile() result app.invoke({query: 我买的商品想退款}) print(result[answer])这个例子虽然简单但已经包含 LangGraph 的最小骨架定义 State、创建图、添加节点、添加边、编译执行。需要提醒的是LangGraph 不同版本的 API 有过调整。早期版本和当前版本在StateGraph、compile、invoke的细节上可能略有差异。如果你在运行时发现某个方法名对不上先打开官方文档确认当前版本对应的写法。这不算坑而是框架迭代期的常态。3.2 Node 内部适合做什么不适合做什么一个 Node 应该像一个专注的工位。它适合做的是调用 LLM生成文本、分类、抽取信息调用工具或 API比如查天气、查订单、查数据库执行确定性的业务逻辑比如校验字段、过滤列表、计算得分在 human-in-the-loop 场景下暂停流程等待人工审批。不太适合做的是在节点里隐式修改外部全局变量把大量临时数据挂在 self 或模块级变量上一个节点里塞了“分类 查询 生成 审核”等多个动作。为什么建议一个节点只做一件事因为 LangGraph 的价值之一是可观测性。节点粒度越细你定位问题时越清楚“是哪一个环节坏了”。如果一个节点内部塞了十几个操作出问题时你仍然要回到代码里慢慢打断点图的优势就荡然无存。3.3 异步、并行分支和子图是 Node 的进阶能力除了普通函数LangGraph 还支持异步节点适合处理 I/O 密集操作async def fetch_order_node(state: State): order_info await fetch_order(state[order_id]) return {order_info: order_info}如果一张图里有多个互不依赖的节点LangGraph 可以并行执行它们。这时候 Edge 的语义会变成“同时前往多个节点”。比如一个售后工单既要查订单状态又要查物流信息两个节点之间没有依赖就可以并行graph.add_edge(parse_query, fetch_order) graph.add_edge(parse_query, fetch_logistics)子图则更像是把一张图当作一个节点嵌入另一张图。当业务特别复杂时比如“工单处理”里又包含“退货审批”和“补偿判断”两个子流程你可以把子流程各自封装成子图再在主图里引用。子图的优点是隔离复杂度缺点是跨子图调试时会多一层上下文。新手阶段不建议一上来就拆子图先把平铺流程图跑通再逐步模块化。4. Edge真正决定 Agent 走向的是路由不是模型心情4.1 普通边和条件边的区别Edge 在 LangGraph 里承担的是“流程转向”职责。普通边很直白节点 A 执行完无条件进入节点 B。但真实业务流程几乎都带条件所以 LangGraph 最核心的能力是条件边。条件边通常由一个路由函数加上 conditional_edge 完成def route_by_category(state: State): if state[category] refund: return manual_review if state[category] tech: return auto_reply return manual_review graph.add_conditional_edge( classify, route_by_category, { manual_review: manual_review, auto_reply: auto_reply, }, )这个路由函数输入当前 State返回的是“下一个节点名”。LangGraph 会根据返回值去匹配映射表决定下一步实际走向哪个节点。你可以把语义理解成前一个节点干完活流程来到分叉口它只看 State 里的关键字段来决定把工件送到哪条流水线。4.2 conditional_edge 的返回语义和常见误区条件边有几个容易出错的地方第一路由函数返回的字符串必须能在映射表中找到对应节点。如果你返回了refund但映射表里键是manual_review运行时会报错。这时候排查的重点不是逻辑对不对而是“返回值和映射表之间是否完全对齐”。第二条件边不是只能返回一个节点。某些场景下比如需要并行分发任务路由函数可以返回一个节点列表让流程同时进入多个节点。不过新手阶段建议先把它当成“单选的 if-else”来用避免一上来就引入并发复杂度。第三条件边里如果逻辑复杂不要在路由函数里写一堆副作用。路由函数最好只读 State做轻量判断返回目标节点名。它不该临时调 LLM也不该修改外部状态否则会让流程的“可预测性”大幅下降。4.3 循环不是 bug是图的一种形态LangGraph 支持循环结构。这件事需要专门强调因为很多人在理解“图”时默认它是单向流动的实际上 LangGraph 的图允许形成闭环。比如一个 Agent 需要“多次调用工具直到拿到最终结果”节点顺序可以是入口节点生成下一步计划工具节点执行动作判断节点检查结果是否满足结束条件不满足就回到计划节点满足就走向结束。这种结构用条件边实现时路由函数会在“继续循环的节点”和“终止后进入的节点”之间二选一。但循环也带来了新的要求你必须保证循环有明确的退出条件并且设置合理的最大轮数。否则一旦模型反复判断“还需要再来一次”你的图就会像 while True 没有 break 一样卡死。曾有不止一个同事问过我“LangGraph 任务为什么一直不结束”最后发现是循环判断节点里没有加入“最多执行 N 次的检查”。这里的经验是即使模型没有告诉你要退出你也要从工程上给它一个保底终止条件。建议给循环类节点设计一个“最大尝试次数”字段放进 State 里每次循环累加一次达到上限就强制转向结束分支。这个小改动能在生产环境中救很多次命。5. 多 Agent 协作本质上是图的分工与路由5.1 多 Agent 不是多个模型一起聊天而是多个职责节点协同很多人一听到“多 Agent”第一反应是让多个大模型互相聊天。真正的多 Agent 协作尤其是在 LangGraph 里通常是指把不同职责的 Agent 作为节点放进同一张图通过 State 和 Edge 来协调它们的工作。比如一个内容生成系统可能由三个 Agent 组成编导 Agent分析用户需求制定内容大纲写作 Agent按大纲生成初稿审查 Agent检查合规和事实错误返回修改意见。这三个 Agent 各自是一个节点它们的输入和输出都写在 State 上。编导 Agent 完成后把大纲放到 State写作 Agent 读取大纲生成初稿审查 Agent 读取初稿决定通过还是打回。这里的核心不是“模型数量多”而是“职责边界清楚”。每个 Agent 只需要关注自己那一环不需要在提示词里同时处理几十个要求。相比于一个巨大提示词驱动一个模型多个小 Agent 的流程可控性会更好也更容易定位问题。5.2 用 State 做共享黑板Node 做职责隔离Edge 做连接如果用一句话概括 LangGraph 里的多 Agent 实践那就是State 是各 Agent 之间唯一的交流方式Node 是各个 Agent 的执行边界Edge 是它们之间的协作关系。假设一个“客服主管 两个客服专员”的层次结构。主 Agent 负责判断工单类型然后派发维修工单给维修专员 Agent派发退款工单给退款专员 Agent。在代码层面你可能只需要一个主管路由节点加上两个子 Agent 节点。主 Agent 不直接处理售后技术细节它只做“分诊”。维修 Agent 只处理设备故障类问题退款 Agent 只处理退换货和财务补偿。每个 Agent 节点的提示词都很短因为它不需要知道全流程只需要在自己职责范围内做判断。这种“主管 专员”的分层模式是 LangGraph 官方文档里多次出现的一种设计范式也是多 Agent 协作里最容易理解的一种。它比“让多个通用 Agent 自由发言”要稳定得多因为图的 Edge 已经给流程画好了边界。5.3 什么时候不需要多 Agent多 Agent 是有代价的更多提示词、更多节点、更多状态字段、更多意外路口。如果你只是做一个“根据用户问题直接回答”的简单 Agent拆成多 Agent 只会让流程更重不会有明显收益。我的判断标准有三个任务是否需要多个不同的专业身份如果所有步骤都是同一个角色在做就没有拆分的必要是否需要分阶段、可追溯地处理任务如果只是一轮问答用普通 Agent 就够了是否需要人工审核或独立子流程介入如果需要才值得用图把多环节串起来。多 Agent 协作的正确打开方式不是因为“多 Agent 很火”而是因为你的业务流程里确实存在多个可以独立把关的环节。把这个环节拆成节点用 State 传数据用 Edge 定方向LangGraph 才能发挥出真正的优势。6. 零基础上手路径与常见问题排查6.1 建议的上手顺序先小图再条件再循环最后多 Agent在带新人接触 LangGraph 时我通常会建议按下面的阶梯来不要一上来就奔着“多 Agent 大项目”去阶段目标核心练习第一阶段跑通最小图定义 State一个节点一条边invoke 一次第二阶段掌握状态更新至少一个字段被覆盖、一个消息列表用 add_messages 追加第三阶段掌握条件路由做一个分类节点根据结果走不同分支第四阶段掌握循环与终止做一个带最大轮数的 Agent 循环第五阶段掌握持久化与子图加入检查点能把一个子流程抽成子图第六阶段多 Agent 协作主管节点 多个专员节点通过 State 协作这个顺序背后的逻辑是每个阶段都建立在前一个阶段的地基上。你如果不理解 State 的覆盖语义后面条件路由里数据丢失时根本不知道原因你如果不理解 Node 的返回值机制多 Agent 之间很难正确传递信息。6.2 最容易踩的五个坑从大量实际操作经验看LangGraph 新手的高频问题集中在下面几个地方State 字段被覆盖而不是累加。解决方式是用Annotated[list, add_messages]或者自定义 Reducer。condition_edge 返回值和映射关系不匹配。解决方式是在路由函数里打印返回值确认和映射表键一致。图进入了死循环。解决方式是在 State 里加最大轮次字段在循环节点里判断并强制退出。在节点内部依赖外部全局变量。这会让调用不透明也影响重放和调试所有共享数据都放 State。装了新版 LangGraph 但参考旧版教程。不同版本 API 有差异比如导入路径、StateGraph的初始化方式、add_conditional_edge的写法。遇到 AttributeError 时优先查文档。6.3 一个标准的排查链路如果任务执行结果不对不要急着改节点逻辑。我一般按这个顺序排查先看最终 State。result里最终状态是什么哪些字段是对的哪些字段是空的先知道“输出哪里不对”。再检查中间 State。分步打印或观察节点执行后的 State找出“字段从第几步开始不对”。检查边和路由。如果某个分支没有走到预期节点重点看条件边函数返回了什么是不是匹配了错误的分支。检查节点函数输入。确认节点拿到的 State 字段是否符合预期尤其是“前一个节点是否真的把结果写回 State”了。最后才是检查 LLM 提示词。很多问题表面上看是模型回答不对实际是流程里的数据没有传对。流程没串好之前先不要怪模型。这个排查链路适合绝大多数基于 LangGraph 的 Agent 应用。它的核心思想是先确认图结构执行正确再确认 State 数据流转正确最后才进入模型能力层。如果一开始就钻进提示词里调参反而容易把真正的问题掩盖掉。6.4 适用边界LangGraph 不是什么场景都值得上LangGraph 适合的场景通常有这几个特征任务流程是多步骤的且步骤之间有明确依赖需要精确控制流程走向不能完全交给模型随机发挥需要保留历史状态支持回放、恢复或人工介入需要多个角色或模块协作完成同一个任务需要把流程做到可观测、可审计。反过来说如果只是单轮问答、简单工具调用或者一次性的脚本任务用 LangGraph 反而会显得重。它带来的 State、Edge、图编译这些概念对简单场景都是额外负担。学习 LangGraph 的正确态度是先掌握它再判断什么时候不用它。从长期价值看LangGraph 真正的意义不在于“多了一个能写 Agent 的框架”而在于它把 Agent 从“模型输出文本的随机过程”变成了“可设计、可控制、可维护的软件流程”。这几乎是 Agent 应用走向生产环境时必须跨过的一道门槛。对零基础同学来说不用急着把所有功能都学会先把 State 的更新规则、Node 的边界、Edge 的路由逻辑想透再逐步往上叠加循环、子图和多 Agent 协作会稳得多。建议你今天的下一步就是从一段最简单的 State 定义和两个节点开始把第一张图真正跑起来。