ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从手搓Agent Loop到LangGraph框架:智能体开发效率提升与多智能体协作实战

从手搓Agent Loop到LangGraph框架:智能体开发效率提升与多智能体协作实战 1. 从手搓到框架为什么第六章要讲这个转折做智能体开发的人几乎都经历过一个相似的阶段最开始用最原始的方式把大模型的调用、工具的执行、结果的判断写成一个while循环跑通了觉得不过如此。然后需求稍微复杂一点——要加个记忆、要并行调两个工具、要在某一步失败后重试、要让两个智能体互相审阅——代码就开始失控if-else嵌套到第七层状态变量散落在十几个地方调试的时候自己都看不懂三天前写的逻辑。第六章把从手动实践到框架开发作为分水岭讲的正是这个转折点。前面几章我们一直在手动实现 Agent Loop自己写循环、自己拼消息、自己解析工具调用、自己判断终止条件。这些手动实践不是白做的它让你彻底看清了智能体运行的本质——无非就是模型输出决策、外部执行动作、结果回灌模型、再决策这样一个闭环。但看清本质和能高效地构建复杂系统是两码事。这一章的核心价值在于当你已经理解了 Agent Loop 的底层机制之后如何借助成熟的框架比如 LangGraph 这类以图结构组织智能体流程的工具把开发效率提升一个量级同时让代码具备可维护性、可观测性和可扩展性。适合的读者是那些已经手写过至少一个能跑通的 Agent、但一遇到多步骤或多智能体协作就头疼的开发者。如果你还完全没接触过 Agent Loop建议先回到前面章节把手动版本跑一遍否则这一章很多为什么框架要这样设计的点你会没有体感。我自己的经验是手动实现的价值不在于以后一直手写而在于当框架出问题时你能一眼看出是框架的抽象漏了哪一层而不是对着报错干瞪眼。这一章我们就沿着手动实现的痛点 → 框架的抽象方式 → 实战落地 → 踩坑与优化这条线走一遍。2. 手动 Agent Loop 到底卡在哪四个绕不过去的痛点2.1 状态管理散落各处的变量是维护噩梦手动写 Agent 时最典型的状态管理方式就是定义一堆局部变量messages存对话历史、step_count记步数、tool_results存工具返回、final_answer存最终结果。刚开始只有三五个变量还好一旦加入中间思考过程多轮工具调用的中间结果失败重试的计数变量数量迅速膨胀。更麻烦的是这些变量的生命周期和修改时机全靠开发者自己保证。比如工具执行失败后要不要把错误信息塞回messages塞进去之后step_count要不要加如果加了会不会导致重试次数被误判这些问题在手动实现里没有统一答案全靠你在每个分支里小心翼翼地处理。我见过一个真实的手动实现光是在不同分支里更新messages的代码就有十几处后来加了一个新工具漏改了一处导致工具结果永远回灌不到模型排查了大半天。框架的第一个价值就体现在这里它把状态定义成一个显式的、集中的结构LangGraph 里叫 State所有节点读写同一个状态对象状态的流转由框架统一调度。你不再需要关心这个变量在哪个分支被改了只需要在状态定义里声明清楚有哪些字段、每个节点负责更新哪些字段。2.2 流程控制循环、分支、并行混在一起就是一团乱麻手动 Agent Loop 的骨架通常是一个while循环里面套着模型调用和工具执行。但只要需求稍微复杂这个循环就会长出各种分支如果模型返回的是工具调用就走工具分支如果返回的是最终答案就跳出循环如果工具调用失败就走重试分支如果连续失败三次就走降级分支。当这些分支用if-else堆在一个函数里时代码的形状就完全丢失了。你没法一眼看出整个流程长什么样只能顺着代码一行行读。而智能体流程本质上是一张图——有节点模型调用、工具执行、条件判断、有边节点之间的流转、有分支根据条件走不同的边。用线性的代码去表达图结构本身就是一种错配。LangGraph 这类框架的核心思路就是让你直接用图的方式描述流程定义节点函数、定义边和条件边框架负责按图执行。这样流程的形状在代码里是显式的加一个节点、改一条边都不会影响其他部分。这是从手动到框架最直观的一个跃迁。2.3 多智能体协作手动实现几乎无法优雅处理单个 Agent 手动写还能忍一旦涉及多智能体协作手动实现基本就崩了。比如一个规划者 执行者 审阅者的三智能体结构规划者拆解任务执行者逐步完成审阅者检查结果并决定是否打回重做。手动实现时你需要维护三套消息历史、三个循环、以及它们之间的通信协议代码量翻三倍不说调试难度是指数级上升。多智能体协作的难点在于每个智能体有自己的上下文和状态但它们又要共享部分信息协作的拓扑结构谁调用谁、谁给谁反馈需要清晰表达还要防止无限循环审阅者一直打回、执行者一直重做。这些在手动实现里没有现成的抽象只能硬编码而硬编码的多智能体系统几乎不可维护。框架提供的解法是把每个智能体也抽象成图中的一个节点或子图智能体之间的通信通过共享状态或消息传递完成协作拓扑用图的边来表达。这样多智能体系统的结构就变得清晰可控。2.4 可观测性出问题时你根本不知道哪一步错了手动实现的 Agent 跑起来像个黑盒。你只能看到输入和最终输出中间经过了哪些步骤、每步的耗时、哪一步消耗了多少 token、工具调用的参数是什么、返回是什么全靠自己打日志。而日志打得不全出问题时就抓瞎打得太多又淹没在信息里。框架通常内置了执行轨迹的记录能力每一步的输入输出、状态变化都能被追踪。LangGraph 配合 LangSmith 这类工具可以把整个执行过程可视化出来哪一步慢、哪一步 token 消耗大、哪一步逻辑走错了一目了然。这个能力在手动实现里要自己搭一套成本很高。把这四个痛点摆在一起看框架的价值就清楚了它不是替你写代码而是替你管理状态、组织流程、协调多智能体、记录轨迹。你省下的是管道代码的时间可以把精力放在真正的业务逻辑上。3. LangGraph 的图抽象节点、边与状态到底怎么配合3.1 State所有节点共享的中央账本LangGraph 里最核心的概念是 State。你可以把它理解成一个所有节点都能读写的中央账本。定义 State 时你要声明这个账本里有哪些字段以及每个字段的更新方式。最基础的写法是用一个 TypedDict 定义字段from typing import TypedDict, Annotated from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] step_count: int final_answer: str这里有个关键细节messages字段用了Annotated[list, add_messages]。这个add_messages是一个reducer它规定了当多个节点都想更新messages时不是覆盖而是追加。这是 LangGraph 状态管理的一个精髓——每个字段可以有自己的合并策略。为什么这个设计重要回到手动实现的痛点多个节点往消息列表里加东西时如果直接覆盖就会丢历史。用 reducer 声明追加语义后框架自动帮你合并你不用在每个节点里手动messages.append(...)。而像step_count这种字段默认的 reducer 是覆盖因为每次更新就是要替换成新值。我踩过的一个坑是一开始没给messages加add_messages结果每个节点返回的消息把之前的全冲掉了模型永远只看到最后一条消息行为完全不对。这个坑很隐蔽因为代码不报错只是逻辑悄悄错了。所以定义 State 时凡是需要累积的字段一定要想清楚它的 reducer。3.2 节点一个函数就是一步操作节点在 LangGraph 里就是一个普通的 Python 函数接收当前 State返回要更新的字段。比如一个调用模型的节点def call_model(state: AgentState): response llm.invoke(state[messages]) return {messages: [response]}注意返回的是一个字典只包含要更新的字段。框架会拿这个字典去更新 State具体怎么合并由字段的 reducer 决定。这种只返回增量的设计让节点函数非常干净——它不需要知道 State 的完整内容也不需要关心其他节点做了什么。节点的粒度怎么把握我的经验是一个节点做一件逻辑上完整的事。比如调用模型是一个节点执行工具是一个节点判断是否结束可以是一个节点也可以是一条条件边。粒度太细会导致图很碎节点之间跳来跳去粒度太粗又回到了手动实现的老路。一般来说一个节点对应一次外部交互调模型、调工具、调数据库或者一次决策比较合适。3.3 边与条件边流程的形状在这里显式表达边定义了节点之间的流转。普通边是执行完 A 就执行 B条件边是执行完 A 后根据某个判断决定走 B 还是 C。from langgraph.graph import StateGraph, END graph StateGraph(AgentState) graph.add_node(agent, call_model) graph.add_node(tools, execute_tools) graph.set_entry_point(agent) graph.add_conditional_edges( agent, should_continue, # 一个返回字符串的函数 {continue: tools, end: END} ) graph.add_edge(tools, agent)这段代码把 Agent Loop 的形状完整表达出来了从 agent 开始判断是否继续继续就去 toolstools 执行完回到 agent不继续就结束。对比手动实现的while循环加if-else这个图结构一眼就能看懂加节点改边也不会牵一发动全身。should_continue这个函数就是条件边的路由逻辑它接收 State返回一个字符串框架根据这个字符串决定走哪条边。把路由逻辑单独抽成函数比塞在循环里的if-else清晰得多也方便单独测试。3.4 编译与执行图怎么跑起来定义完图之后要compile()一下才能执行app graph.compile() result app.invoke({messages: [(user, 帮我查一下明天的天气)]})compile()这一步会做图的合法性检查比如有没有孤立节点、条件边的返回值有没有对应的目标。执行时用invoke传入初始状态框架会按图跑完整个流程返回最终状态。这里有个实用技巧compile()时可以传入checkpointer实现状态的持久化和断点续跑。这在长流程或者需要人工介入的场景里非常有用——比如流程跑到一半需要人工审批可以把状态存下来审批完再从中断处继续。手动实现要做到这个得自己设计一套状态序列化和恢复机制工作量不小。4. 把手动版 Agent 迁移到 LangGraph一次完整的重构实录4.1 迁移前的准备先把手动版的行为摸清楚迁移不是重写而是把已有的逻辑映射到框架的抽象上。所以在动手之前我建议先把手动版的行为彻底摸清楚它有哪些步骤、每步的输入输出是什么、状态在哪些地方被修改、有哪些分支和终止条件。最好画一张流程图纸上画就行把整个流程的形状先理出来。这一步很多人会跳过直接上手写 LangGraph 代码结果写着写着发现漏了某个分支或者状态字段对不上。我自己的做法是列一张表左边是手动版的变量和分支右边是 LangGraph 里对应的 State 字段和节点/边。映射清楚了再写代码效率高很多。4.2 状态字段的映射从散落变量到集中 State假设手动版有这些变量history对话历史、current_tool_call当前要执行的工具调用、tool_output工具返回、iterations迭代次数、done是否结束。映射到 LangGraph 的 State手动版变量LangGraph State 字段reducer 策略historymessagesadd_messages追加current_tool_calltool_call覆盖tool_outputtool_result覆盖iterationsstep_count覆盖或自定义累加done不需要用条件边代替-注意最后一行done这个布尔变量在 LangGraph 里不需要作为状态字段因为是否结束是通过条件边来表达的。这是框架思维和手动思维的一个区别——有些控制流变量在框架里变成了结构本身。4.3 节点拆分把手动版的代码块对应成节点手动版的代码通常可以切成几块调模型、解析工具调用、执行工具、判断是否结束。对应到 LangGraph 就是call_model节点调模型把返回加入 messagesexecute_tools节点从最后一条消息里解析工具调用执行把结果加入 messagesshould_continue条件边函数检查最后一条消息里有没有工具调用有就继续没有就结束拆分的时候有个原则节点函数尽量保持纯——给定输入状态产生确定的输出更新不依赖外部可变状态。这样节点容易测试也容易复用。手动版里常见的在函数内部直接改全局变量的做法在框架里要改成返回增量更新。4.4 跑通第一个迁移版本先求对再求好第一次迁移不要追求完美先把主流程跑通。我的建议是先只迁移最核心的模型调用 工具执行 循环判断这条主线把多智能体、重试、降级这些高级特性先放一放。跑通之后对比手动版和框架版的输出确认行为一致再逐步加回其他特性。跑通的过程中最容易出问题的地方是消息格式。手动版里你可能用的是自定义的消息结构而 LangGraph 和底层模型库通常对消息格式有约定比如HumanMessage、AIMessage、ToolMessage。迁移时要把消息格式统一否则模型可能收不到工具结果或者工具调用解析失败。这个坑我在第一次迁移时踩过模型一直重复调用同一个工具就是因为工具返回的消息格式不对模型看不到结果。5. 多智能体协作在 LangGraph 里怎么落地5.1 用子图封装单个智能体多智能体协作的第一步是把每个智能体封装成一个独立的子图。子图本身也是一个完整的图有自己的 State 和节点可以单独测试。然后在主图里把子图当作一个节点使用。def build_worker_agent(): g StateGraph(WorkerState) g.add_node(worker, worker_node) g.set_entry_point(worker) g.add_edge(worker, END) return g.compile() main_graph.add_node(worker_agent, build_worker_agent())用子图的好处是隔离每个智能体的内部逻辑和状态对外部不可见只通过输入输出交互。这样修改一个智能体的内部实现不会影响其他智能体。手动实现多智能体时最大的痛苦就是所有智能体的状态混在一起改一处动全身。5.2 规划者-执行者-审阅者一个可复用的协作拓扑一个经典的多智能体拓扑是规划者-执行者-审阅者规划者把任务拆成步骤执行者逐步完成审阅者检查每步结果并决定是否通过。在 LangGraph 里这个拓扑用三个节点加条件边就能表达planner节点生成任务计划写入 Stateexecutor节点执行当前步骤写入结果reviewer节点检查结果返回通过或打回条件边reviewer 通过就进入下一步或结束打回就回到 executor这个拓扑的关键在于审阅者的判断逻辑和防死循环机制。审阅者不能无限打回所以要设置最大重试次数超过就强制通过或报错。这个计数放在 State 里每次打回加一条件边里检查。5.3 智能体之间的通信共享状态还是消息传递LangGraph 里智能体之间通信有两种方式共享状态和消息传递。共享状态是所有智能体读写同一个 State 的不同字段适合同步协作消息传递是智能体之间通过消息队列异步通信适合松耦合的场景。大多数场景下共享状态就够了也更简单。但要注意字段的隔离——不是所有智能体都需要看到所有字段。可以通过给不同智能体定义不同的 State 子集或者在节点函数里只读取需要的字段来实现隔离。我见过一个项目所有智能体共享一个巨大的 State结果一个智能体不小心改了另一个智能体的字段导致行为异常排查了很久。字段隔离这件事宁可一开始就做好。5.4 防止协作失控超时、重试上限与人工兜底多智能体协作最容易失控的地方是无限循环A 等 BB 等 A或者审阅者一直打回。LangGraph 本身提供了递归限制recursion_limit超过就抛异常这是最后一道防线。但更好的做法是在业务逻辑里主动设置上限每个智能体的重试次数、整个流程的最大步数、单步的最大耗时。对于关键流程还要留人工兜底的入口。比如审阅者连续打回三次后不是继续自动重试而是把状态挂起等人工介入。LangGraph 的 checkpointer 配合中断机制可以实现这个——在需要人工介入的节点前设置中断状态存下来人工处理完再恢复执行。6. 迁移过程中踩过的坑与优化经验6.1 状态字段的 reducer 写错导致消息丢失前面提过add_messages的坑这里再展开说。LangGraph 里每个字段的 reducer 决定了多个节点更新同一字段时的合并方式。默认是覆盖只有显式声明了 reducer 的字段才会用自定义合并。消息列表必须用add_messages否则后一个节点的更新会覆盖前一个。更隐蔽的是如果你自定义了一个 reducer 但逻辑写错了问题会更难查。比如你想让某个计数字段累加写了个 reducer 但忘了处理初始值第一次更新时就会报错或者得到错误结果。我的建议是能用内置 reducer 就用内置的自定义 reducer 一定要写单元测试。6.2 条件边返回值不匹配导致流程卡死条件边函数返回的字符串必须和add_conditional_edges里映射字典的 key 完全一致。如果返回了一个字典里没有的字符串LangGraph 会报错。但有时候错误信息不够直观你会以为是图结构的问题其实是返回值拼写错了。我踩过的坑是条件边函数在某些分支下返回了None而映射字典里没有None对应的目标导致流程卡住。后来养成的习惯是条件边函数一定要有明确的默认返回值覆盖所有可能的分支并且在函数开头就处理好边界情况。6.3 子图状态与主图状态的字段对齐用子图时子图的 State 和主图的 State 可能字段不一致。LangGraph 在把子图当作节点时会把主图 State 传给子图子图执行完再把更新传回主图。如果字段对不上就会出现子图改了但主图没变或者主图传了但子图收不到的情况。解决办法是显式定义输入输出的映射。LangGraph 支持在添加子图节点时指定输入输出 schema把主图的字段映射到子图的字段。这个映射要仔细核对尤其是字段名不一致的时候。我一般会在子图单独测试通过后再接入主图接入后先跑一个最小用例验证状态传递正确。6.4 用 checkpointer 实现断点续跑与人工介入checkpointer 是 LangGraph 里我觉得最实用的特性之一。它把每一步执行后的状态持久化下来可以随时恢复。这在几个场景下特别有用长流程中途失败不用从头跑、需要人工审批时挂起、调试时可以回放某一步的状态。配置 checkpointer 很简单compile(checkpointer...)传入一个 checkpointer 实例即可。但要注意checkpointer 存储的状态可能包含敏感信息生产环境要做好存储的安全和清理。另外恢复执行时要传入正确的 thread_id否则会恢复到错误的会话。6.5 性能优化并行节点与流式输出LangGraph 支持并行执行节点——如果多个节点之间没有依赖关系可以同时跑。这在多工具调用的场景下能显著降低延迟。实现方式是把多个节点连到同一个起点框架会自动并行调度。流式输出是另一个优化点。默认情况下invoke是等整个流程跑完才返回用户要等很久。用stream方法可以逐步返回每个节点的输出用户能实时看到进展。对于交互式应用流式输出几乎是必须的否则用户体验很差。7. 从框架再回到本质什么时候该用框架什么时候不该聊了这么多框架的好处最后说点反向的经验。框架不是银弹有些场景下手动实现反而更合适。如果你的 Agent 逻辑非常简单——就是调模型、执行工具、循环三步没有多智能体、没有复杂分支、没有持久化需求那手动实现可能更轻量。引入框架会带来额外的学习成本和抽象层对于简单场景是过度设计。另一个判断标准是团队情况。如果团队里没人熟悉 LangGraph引入它意味着所有人都要学一套新的概念和调试方式。这时候要么先做技术验证、培养一两个熟悉的人要么就继续手动实现等痛点足够痛了再迁移。我自己的经验是当手动实现的代码超过 500 行、或者开始出现改一处要动三处的情况、或者需要多智能体协作时就是迁移到框架的合适时机。太早迁移是过度设计太晚迁移是技术债堆积。第六章把这个转折点放在这里我觉得节奏是对的——前面手动实践打底这里框架开发提速后面再深入高级特性。框架的本质是把智能体开发中反复出现的模式状态管理、流程控制、协作协调、可观测性抽象成可复用的结构。理解这些模式比记住某个框架的 API 更重要。因为框架会变但这些模式是稳定的。你手动实现过一遍再用框架就能看清框架每个设计决策背后的为什么而不是照着文档抄代码。这也是我一直建议的先手搓再上框架顺序不能反。
返回列表