文章

LangGraph 状态设计与节点编排

LangGraph 状态设计与节点编排

参考资料

一句话总结

LangGraph 的核心不是“把一个大 Prompt 交给模型”,而是先把任务拆成一组职责明确的节点,再通过共享状态把这些节点串起来。这个过程中最重要的设计原则之一是:State 里保存原始数据,Prompt 在节点内部按需格式化


从流程出发,而不是从 Prompt 出发

官方文档用“客服邮件处理 Agent”举了一个很典型的例子:一个 Agent 需要读取邮件、识别意图、搜索文档、起草回复、必要时升级给人工,并在最终完成发送。

如果直接把这些步骤塞进一个大模型调用里,虽然看起来简单,但很快会遇到几个问题:

  • 每一步依赖的上下文并不相同
  • 有些步骤是推理,有些步骤是查数据,有些步骤是执行动作
  • 不同节点的错误处理方式不一样
  • 中间结果需要保留,方便后续节点继续使用

所以 LangGraph 的思路是:先还原业务流程,再映射成图结构
图里的每个节点只做一件事,节点之间通过状态传递信息,至于下一步去哪里,则由节点根据当前结果决定。


LangGraph 的五步思考方式

结合文档内容,可以把 LangGraph 的设计过程概括成五步:

1. 先把业务流程拆成离散步骤

每个步骤最终会对应一个节点,例如:

  • Read Email:读取和解析邮件
  • Classify Intent:识别问题类型、紧急程度和主题
  • Doc Search:检索知识库
  • Bug Track:写入或更新缺陷系统
  • Draft Reply:生成回复草稿
  • Human Review:进入人工审核
  • Send Reply:真正发送邮件

这里的关键点不是“节点越少越好”,而是节点职责要单一
如果一个节点同时负责分类、检索、起草回复、决定是否人工审核,那它很快就会变成一个不可维护的黑盒。

2. 明确每个步骤到底在做什么

官方文档把节点大致分成四类:

  • LLM steps:理解、分析、生成文本、做推理判断
  • Data steps:从外部系统或知识库拉取数据
  • Action steps:执行发送邮件、创建工单这类外部动作
  • User input steps:需要人工介入或补充信息

这种分类很实用,因为它直接决定了节点的设计方式:

  • LLM 节点重点关注输入上下文和输出结构
  • 数据节点重点关注参数、重试、缓存
  • 动作节点重点关注幂等性和失败补偿
  • 人工节点重点关注中断与恢复

3. 设计共享状态

State 是整个图的共享记忆。
它的作用不是简单“存点变量”,而是让不同节点都能基于一致的数据视图工作。

文档里给出的判断标准很直接:

  • 如果某段信息需要跨步骤保留,就放进 state
  • 如果某段信息可以从已有数据推导出来,就不要存,按需计算

以邮件 Agent 为例,适合放进 state 的通常有:

  • 原始邮件内容和发件人信息
  • 意图分类结果
  • 知识库检索结果
  • 客户历史信息
  • 回复草稿
  • 执行过程中的元数据

4. 把每个步骤实现成节点函数

LangGraph 节点本质上就是 Python 函数:读取当前状态,做自己的工作,再返回对状态的更新,必要时决定下一跳。

文档中的设计重点不是“函数怎么写”,而是错误怎么分层处理

  • 瞬时错误:交给系统自动重试
  • LLM 可恢复错误:把错误写回 state,让模型重新决策
  • 用户可修复错误:通过 interrupt() 暂停并等待补充信息
  • 未知错误:直接抛出,方便调试
  • 重试后仍失败的场景:走补偿或恢复分支

5. 最后再连线成图

很多人第一次接触 LangGraph 时,会把注意力都放在“边怎么画”。
但文档强调的重点恰恰相反:真正的路由决策通常发生在节点内部,而不是边上

也就是说,图结构可以很简洁:

  • START -> read_email
  • read_email -> classify_intent
  • send_reply -> END

至于分类后是去检索、去建 bug、还是进人工审核,通常通过 Command(update=..., goto=...) 在节点内部决定。


为什么要“State 保存原始数据,Prompt 按需格式化”

这是整篇文档里最值得反复记住的一条原则。

官方建议非常明确:不要把格式化后的 Prompt 文本存进 state,而是把原始数据存进去,真正调用模型时再在节点内部拼 Prompt。

这样设计的价值主要有四点。

1. 同一份数据可以被不同节点以不同方式消费

例如一封用户邮件,分类节点需要的是“意图、紧急程度、主题”;回复生成节点需要的是“问题背景、检索结果、客户历史”;人工审核节点需要的是“原文 + 草稿 + 风险信息”。

如果你在 state 里提前塞了某一种固定格式的 Prompt,其他节点很可能还得重新拆开、重新组织,反而更麻烦。

2. Prompt 模板可以独立演化

Prompt 是经常要调的,但 state schema 往往更稳定。
如果把 Prompt 拼接结果直接存进 state,那么每次改模板,都可能牵动状态结构和兼容逻辑。

反过来,如果 state 只存原始数据,Prompt 只是节点内部的一层视图,那么你可以很自由地优化提示词,而不必频繁改状态定义。

3. 调试会更清晰

调试 Agent 时,一个高频问题是:“模型到底看到了什么数据?”
如果 state 里存的是原始字段,比如 email_contentclassificationsearch_resultscustomer_history,那排查起来会非常直接。

你能清楚看到:

  • 上游节点产出了什么
  • 下游节点消费了什么
  • 是数据本身有问题,还是 Prompt 组装方式有问题

4. 更有利于长期演进

随着 Agent 变复杂,state 结构通常会越来越像“业务事实层”,而 Prompt 更像“解释层”或“渲染层”。

把两者分开之后,Agent 才更容易:

  • 新增节点
  • 替换模型
  • 重写模板
  • 扩展状态字段
  • 保持旧状态兼容

一个更合理的 State 应该长什么样

文档中的邮件 Agent 状态设计很有代表性,大致包含三类内容:

1. 原始输入数据

  • email_content
  • sender_email
  • email_id

这类字段是事实本身,后续节点都会反复用到,必须长期保留。

2. 中间推理或外部查询结果

  • classification
  • search_results
  • customer_history

这些结果可能来自 LLM,也可能来自外部系统,但都属于后续节点会复用的中间产物。

3. 生成内容与过程产物

  • draft_response
  • messages

例如回复草稿需要经过人工审核,就不能只存在某个函数的局部变量里,而必须进 state。

这里最值得注意的一点是:文档中的 state 存的是结构化结果和原始结果,而不是“已经整理好的提示词文本”。


节点内部如何消费这些原始状态

State 存原始数据,并不意味着 Prompt 会更难写。
正确做法是:在节点内部临时把原始数据格式化成当前任务需要的上下文。

比如:

  • 分类节点临时拼出“邮件正文 + 发件人 + 分类要求”
  • 检索节点临时拼出“意图 + topic”作为检索 query
  • 回复节点临时把检索结果格式化成项目符号,把客户信息转成上下文说明
  • 人工审核节点则直接把原始邮件、草稿和风险标签打包给人工

这个思路很像前后端分层:

  • state 更像数据库或领域模型
  • prompt 更像视图层

一旦接受这个分层,整个 Agent 的可维护性会好很多。


LangGraph 里的错误处理也值得单独学

很多人把 Agent 错误处理理解成“失败了就重试”,但 LangGraph 文档给出的思路更细。

瞬时错误:重试

网络抖动、限流、临时不可用,适合交给 RetryPolicy
这种错误本质上和业务语义无关,系统层自动处理即可。

LLM 可恢复错误:写回 state 再回环

如果工具调用失败、结构化解析失败,但模型有机会根据错误信息修正自己的动作,就把错误作为上下文的一部分写回 state,再回到上游节点。

这种方式的重点是:不要把所有失败都当成系统异常,有些失败本身就是模型后续推理的输入。

用户可修复错误:interrupt()

如果缺少账号 ID、订单号、确认信息等内容,就不要让模型硬猜,而是中断图执行,等待人工补充后再恢复。

这也是 LangGraph 很适合做业务 Agent 的原因之一:它天然支持“执行到一半停下来,拿到信息后继续跑”。

真正的未知错误:直接抛出

文档特别强调一点:处理不了的异常不要硬吃掉。
未知异常应该暴露出来,让开发者看到真实问题,而不是在图里悄悄吞掉。


对实际开发最有帮助的几个启发

结合这篇文档,我觉得有几条经验特别适合直接带到工程里:

1. 先设计状态,再写 Prompt

很多 Agent 一开始就急着调提示词,但真正决定系统上限的,往往是状态结构是否清晰。
只要 state 设计混乱,后面加节点、调路由、做恢复都很痛苦。

2. 节点函数一定要“单职责”

每个节点只做一类事情:分类、检索、执行动作、人工审核。
职责越单一,状态越稳定,调试也越容易。

3. 把 Prompt 视为“状态的渲染结果”

这是一种非常重要的思维转变。
Prompt 不是主数据,state 才是;Prompt 只是当前节点为了完成任务,对 state 做的一次格式化投影。

4. 路由逻辑尽量由节点返回结果决定

不要把所有分支逻辑都堆在图外部。
如果节点已经产出了结构化结论,比如 intenturgency,那就让节点直接返回下一跳,更符合 LangGraph 的工作方式。

5. 错误处理要按“谁能修”来分层

不是所有错误都该重试,也不是所有错误都该交给人工。
更合理的做法是先判断:这个错误最适合由系统、模型、用户还是开发者来解决。


总结

Thinking in LangGraph 真正想传达的,不只是“怎么写一个图”,而是怎么用图的方式思考 Agent 系统

其中最关键的设计原则就是:

  • 先拆流程,再建节点
  • 用共享 state 连接节点
  • state 保存原始数据
  • prompt 在节点内部按需格式化
  • 把错误处理、人工介入和路由决策都纳入图的设计中

如果把 LangGraph 只当成一个“多步调用框架”,很容易写出能跑但难维护的 Agent。
但如果把它理解成“围绕状态流转来组织推理、动作和人工协作的执行框架”,很多设计选择就会自然清晰起来。

本文由作者按照 CC BY 4.0 进行授权