
从最简单的LLM调用到ReAct Agent再到多Agent协作一路走过来的人应该都有同感代码慢慢变得不可控了。if-else越叠越厚状态散落在一堆全局变量里想加一个分支就要动半天逻辑最后自己都看不懂自己写了什么。LangGraph就是在这个痛点下被设计出来的——它把Agent流程从线性代码升级成了图结构。这一篇我把最核心的原理和第一条Graph的构建过程记录下来希望能帮到那些正卡在会用LangChain但不知道怎么编排复杂Agent的朋友。1. 为什么说线性Agent走不远三个绕不开的墙1.1 循环、分支与恢复代码行数掩盖的真实复杂度先看一个常见的场景。你用LangChain或手写Prompt做一个研究助手Agent拿到用户问题后先搜索、再总结、然后给答案。写成代码大概是这样用户输入进LLMLLM决定调用工具拿到工具结果之后再喂回LLM直到LLM说不需要工具了就输出。这个循环用while写死看起来也就那么十几行。问题出在产品需求总是悄悄变复杂这件事上。当你需要如果搜索结果置信度低就再追问用户一句或步骤一失败就直接走人工流程时线性while循环就要塞if-else。塞了三五个之后你会发现分支之间的状态靠全局变量传递一个地方忘了重置下一个用户就会拿到上一个人的残留数据想在某个环节插入一个人工审核节点要么打断整个循环要么用标志位跳来跳去流程一旦中间崩溃你想从断点恢复几乎不可能因为没有任何机制记录当前走到了哪一步。这不是代码风格问题是模型问题。线性结构天然只擅长顺序处理而真实Agent场景里充满了条件跳转、并行、回退、事件等待。1.2 状态被大多数人忽略的Agent基础设施很多人做Agent第一版只考虑模型选择和Prompt设计状态管理压根不考虑。一开始还行因为你就一个会话、一个用户、一次性往返。一旦做到多用户、长会话、多轮工具调用状态就变成了唯一主角。最典型的崩溃多个用户同时用同一个Agent各自的上文聊着聊着就串了。你排查到最后会发现某个全局dict里存着上一个人的查询结果所有用户进程共享了这个dict。LangGraph的一等公民就是State它把状态这个隐式前提变成显式设计——每一步节点都接收一个显式的state对象返回一个更新后的state。节点之间不共享可变全局变量所有信息都通过state流动。这个概念听起来平淡但它是整个框架的地基。1.3 LangChain的AgentExecutor和LangGraph的差异到底在哪LangChain的老方案AgentExecutor本质上是个通用循环执行器给一个agent给一组tools它自己循环跑。好处是上手快坏处是你觉得它有魔法但它只是个可配置的while循环你想定制跑完第一步之后先判断再决定要不要跑第二步就得去改内部实现。LangGraph的定位完全不同。它不内置循环循环在LangGraph里是你自己画的边——节点A到节点BB有条件地回到A这就是循环。你想要先搜索判断结果再决定是否人工介入就是在图上加一个条件节点由模型或规则来决定走哪条边。有人会说这不就是流程图框架吗对它就是。但它是一个专门为LLM应用设计的、带状态管理、带持久化、带检查点的流程图框架。2. LangGraph里的几个核心概念用地铁图来理解2.1 State车厢里传递的信息包State在LangGraph里就是一个数据类通常用TypedDict定义比如from typing import TypedDict, Annotated class AgentState(TypedDict): messages: Annotated[list, operator.add] current_step: str final_answer: str这里Annotated的第二个参数是状态归约器它规定了这个字段怎么合并——operator.add表示新增的messages会追加到原列表而不是覆盖。没有归约器的字段默认是覆盖。你可以把State想象成地铁车厢每一站节点上车的人会不断被收集到车厢里车厢永远是同一节但里面的乘客越装越多。每个节点只负责读取当前车厢的信息往车厢里加自己的输出然后再传给下一个节点。最容易被忽略的地方是不要让State无限膨胀。有人把所有中间结果都往state里塞token开销越来越大调试也越来越痛苦。好的习惯是State只存必要信息中间产物用临时变量或者外部存储。2.2 Node只在需要时才干活的函数Node就是普通的Python函数接收state返回一个dict表示要更新的字段。比如def search_node(state: AgentState): query state[messages][-1].content results search_tool(query) return {search_results: results}这个函数不关心谁调用它、也不关心下一步去哪。它只做自己的事从state取输入产生输出更新state。这个设计最大的好处是单个节点可以单独测试。你写一个search_node不需要跑整个图直接构造一个state传进去看返回结果就行了。这是线性Agent代码很难做到的——你没法单独测一个循环中间的一次迭代。2.3 Edge连节点、画条件分支、形成循环的线Edge定义图中节点的连接关系。普通边是无条件从一个节点到另一个节点条件边是根据state里的某些值动态决定下一个节点。def router(state: AgentState): if state[current_step] search: return search_node else: return final_node条件边是LangGraph的灵魂。你的Agent智能决策在哪里体现一部分在模型掐prompt时的思考另一部分就在这个条件边——它是显式、可调试、可预测的。地铁图类比节点是站台边是轨道条件边就是那个去往不同方向的分岔口。你到底是去二次确认站还是直接到输出站取决于你当前携带的信息包state。3. 一小时上手从零手写一个搜索确认回答的图3.1 第一步用StateGraph搭建空图装好langgraph包所有基础功能都在langgraph.graph里pip install langgraph langchain-openai然后开始构图from langgraph.graph import StateGraph, END from langgraph.graph.graph import START graph StateGraph(AgentState) graph.add_node(search, search_node) graph.add_node(confirm, confirm_node) graph.add_node(generate, generate_node) graph.add_edge(START, search)注意START这个伪节点——图的入口是一条指向第一个真实节点的边而不是直接调用第一个节点。这套设计让图可以随时替换入口节点比如你以后想从confirm直接开始改一条边就行了不用动代码逻辑。3.2 第二步加条件边让Agent学会自我判断核心需求是搜索之后如果结果没问题就直接生成答案如果有问题就走人工确认节点。graph.add_conditional_edges( search, router, {answer: generate, ask: confirm} ) graph.add_edge(generate, END) graph.add_edge(confirm, generate)router函数根据搜索结果质量判断返回值是answer还是ask条件边的字典参数把router的返回值映射到具体节点。这里的routing逻辑完全透明测试时只要能构造不同state就能直接验证router行为。3.3 第三步编译并运行第一条Graphapp graph.compile() initial_state {messages: [{role: user, content: XX公司2025年的收入预期是什么}], current_step: search, final_answer: } result app.invoke(initial_state) print(result[final_answer])运行起来之后你会发现整个流程每次走到哪个节点、停在了哪里、state长什么样全都在Graph的调试面板里一目了然。这在以前线性Agent时代是很难得的——运行一次之后的中间状态几乎是黑匣子。3.4 一个小案例让图学会根据搜索结果决定追问还是直接答我建议第一次练习用最简单但最贴近实战的场景用户提一个问题你让搜索节点去查然后让质量判断节点决定搜索结果足够就直接回答不够就生成一句追问。这样你就可以观察条件边到底在做什么模型生成追问的节点无论如何都会调用LLM但它会给用户一个语义上下文——帮你确认信息而不是直接报搜索失败错误。不是所有Agent都必须用LangGraph但一旦流程复杂度超过单次工具调用LLM输出图结构带来的优势就很明显了。4. 从一个Agent到多个Agent协作图结构的真正威力4.1 多Agent不是人多力量大是专业分工流程持有很多人一想到Multi-Agent就以为要开多个线程、多个模型同时跑。LangGraph的Multi-Agent压根不关心你同时跑几个进程它关心的是一个复杂任务怎么拆分、怎么编排、怎么让每个环节的产出被下一个环节消费。简单来说每个Agent节点还是一个普通Node但它内部可以是一个完整的子循环。比如一个研究团队图里有四个Agent节点规划Agent把用户问题拆解成子任务搜索Agent逐个完成搜索子任务审查Agent对搜索结果做质量判断回答Agent基于审查结果写最终答案。这里面每一个Agent都可以被画成一张独立的小图大图里的节点调用小图时本质上就是一次子图调用——状态会传入子图子图会把处理好的状态返回给父图。4.2 让多个Agent真正聊起来的边和让人工审核自动切入的边多Agent协作的核心不难每个节点只负责自己的职责节点之间的边决定沟通方式。难的是你怎么画一条边让用户介入这个环节自然发生。工程上的做法是审查节点里用人路由节点里用模型。比如在做内容编写Agent时可以让一个审阅Agent负责把初稿文本和一个是否需人工复核的决定一起写入state然后条件边根据这个决定把流程路由到人工复核节点或直接到发布节点。人工审核节点本身甚至可以不接LLM只是把state里的信息展示在后台等操作员点了通过整个图再继续往下走。LangGraph的checkpoint机制保证这个等待期间所有状态都不会丢整套流程可以停几天再恢复。4.3 状态共享、信息流和全局变量的心里话当你真的开始设计几个Agent协作时会发现一个隐性问题并不是所有信息都适合放在共享的全局State里。比如单次搜索API返回的原始response这个又大又容易过期明显应该由搜索Agent内部临时处理处理完只把结构化结果塞回共享State。我的实践原则是跨Agent传递的最终结论、用户意图、已确认信息、待决策事项Agent内部消费的原始api返回、中间计算结果、临时的上下文列表。这两个混在一起状态会乱得发疯。遵守这个区分的团队做Multi-Agent时基本不会出大问题。5. 踩过的几个坑提前帮你排雷5.1 条件边返回的键值对必须和节点名称完全一致这是一个很蠢但常见的错。add_conditional_edges里你写了{answer: generator_node}但你在add_node时写的是generate_node一运行就报错Invalid state: node generator_node not found。这类问题由于不会在编译时报错而是到运行到那一步才崩溃所以排查起来特别费劲。我的经验是把所有节点名集中放到一个常量类里用类属性引用而不是手写字符串。5.2 没有正确设置归约器状态被静默覆盖如果你在AgentState里定义了messages: list而没有用Annotated[list, operator.add]那么每次节点返回的新messages都会覆盖旧messages——之前的对话历史悄无声息地消失了。如果你没注意这个特性会在某个时刻突然发现Agent智商掉线因为它失忆了。不仅是messages任何需要累加的字段都要考虑operator.add或自定义归约器。自定义归约器接收两个参数返回合并后的结果——current是当前state里的值new是节点返回的新值。def merge_dict(current: dict, new: dict) - dict: merged current.copy() merged.update(new) return merged5.3 图不是越大越好过长的链式容易让token爆炸有人一开始觉得LangGraph能画图就拼命画把每一个if判断都变成节点。结果每个节点都调一次LLM20个节点跑下来光context传递就烧掉几万token。这不是框架的问题是设计问题。我的建议是有状态就画节点没状态就留在函数内。如果一个判断不需要跟其他节点共享信息、不需要被其他节点触发、也不需要中途暂停等待那就写成一个普通的if-else放在一个节点函数里面。节点是给需要被外部看见、被外部控制的操作留的。另一方面其实LangGraph支持并行执行节点当两个节点没有依赖关系时图引擎可以自动并行处理。我这里不多展开等你跑过流水线就会理解这个特性有多值钱。6. 写在最后的一点感受做Agent开发这一年多我最大的体感是模型能力已经不是最大的瓶颈编排能力才是。同一个模型、同一批工具用线性代码还是用图结构去组织它们产出的稳定性和可维护性可以差一个等级。LangGraph的本事不是帮你凭空变出更聪明的Agent而是让你那些本来就很聪明的节点能够在一个不失控的结构里按你想好的方式运转。第一篇就先聊到这里。下一篇我打算写一写LangGraph的持久化检查点机制以及怎么用它做出中断一天还能恢复的长时间运行Agent希望对你有用。