跳到正文

AGENT PROJECT INTERVIEW · Q06 / 25

LangGraph 的 StateGraph 怎么设计?State、Node、Edge 到底各负责什么

从共享 State、Node 状态变换、固定边与条件边出发,讲清 LangGraph StateGraph 的设计顺序、循环控制、失败路由、Trace 与工程验收。

作者与复核:吴师兄内容依据:公开 Agent 教程、项目题资料与工程推导查看关联教程 →查看吴师兄公开作品 →

25 道题目Q06
本页内容17 节

30 秒先说结论

State 保存任务目标、计划、进度、工具结果、错误和控制字段;Node 负责一类可验证的状态变换;固定 Edge 处理确定顺序,Conditional Edge 根据经过校验的状态选择下一步。设计时先写完成条件和失败出口,再做最小 State、拆节点和连边,循环同时设置业务、资源、无进展和风险出口,并保存逐步 Trace。

面试官问:“你说项目用了 LangGraph,那你这张图到底是怎么设计的?”

如果回答只是“State 是状态,Node 是节点,Edge 是边”,这道题基本只答到了框架名词。面试官真正想确认的是:为什么任务需要一张有状态的图,哪些数据应该进入 State,节点之间怎样分工,分支和循环由什么条件决定,以及图跑错时能不能回放到具体一步。

很多 Agent 项目一开始只有一个 while 循环。模型决定工具,程序执行,再把结果交回模型。这个最小循环很重要,因为它让人看清 Agent 的本质。但当流程出现计划、执行、反思、重试、人工确认等多个阶段后,所有判断继续塞在一个循环里,状态会越来越散,异常出口也越来越难解释。StateGraph 的价值,就是把这些隐含在代码里的状态和控制流显式化。

先给出 30 秒回答

LangGraph 的 StateGraph 可以理解为一张会携带共享状态运行的流程图。State 保存任务目标、计划、已完成步骤、工具结果、错误和控制字段;Node 是只负责一类状态变换的处理单元;Edge 定义固定跳转,Conditional Edge 根据当前状态选择下一节点。设计时我会先写业务完成条件和失败出口,再设计最小状态,最后拆节点与边,而不是先按框架示例堆 Planner、Executor、Reflector。关键节点要做到输入输出清楚、重复执行可控,循环必须受步数、时间和业务条件约束,并通过 Trace 记录每次状态变化。

这段回答里有四个关键词:共享状态、状态变换、条件路由、受控循环。把这四件事讲清,才算真正理解 StateGraph。

StateGraph 四个核心部件

这张图的用途,是让状态、责任和跳转条件都能被检查,而不是增加架构装饰。

为什么一个普通循环会逐渐失控

先看一个研究助手。用户给出一个问题,系统先制定计划,再调用搜索或访问工具,检查结果是否足够,最后生成回答。刚开始可以写成下面的逻辑:

while not finished:
    action = model_decide(messages)
    result = execute(action)
    messages.append(result)

这段代码能跑,但很多重要问题没有显式答案:当前执行到计划的第几步?工具失败后是重试、换工具还是结束?已经得到哪些证据?谁判断结果足够?如果模型连续选择同一个动作,在哪里阻止?如果需要人工确认,暂停点放在哪里?

开发者通常会继续加入 if:如果是搜索就走一个分支,如果需要反思就走另一个分支,如果错误次数过多就退出。再过一段时间,计划、消息、工具结果、重试次数散落在不同局部变量里。最终答案错了,只能看到循环结束后的文本,很难确认是哪次状态更新把任务带偏。

StateGraph 没有消除这些复杂度,它只是迫使你把复杂度放在可以命名和检查的位置。共享数据进入 State,业务步骤进入 Node,跳转逻辑进入 Edge,循环出口写成明确条件。这样做的直接收益不是代码更短,而是每一步都能回答“读了什么、改了什么、为什么去下一步”。

State:不是把所有变量塞进一个大字典

State 是整张图共同传递的数据。最容易犯的错误是把所有内容都放进去,包括原始页面、完整模型响应、所有日志和临时对象。状态越大,节点越难理解,持久化和恢复也越贵。

一个面向多步任务的最小 State,通常需要覆盖下面几类信息。

任务事实

包括用户原始目标、经过确认的约束、期望输出和什么算完成。原始问题最好保留,经过改写的任务说明单独存放,避免后续节点只看到模型改写版,逐渐偏离用户真正的要求。

执行计划

包括当前计划、各步骤状态、正在执行哪一步。计划不是一段只能给人看的自然语言,至少要有步骤编号、动作类型、依赖和完成标记。只有结构化后,执行节点才能知道下一项工作,反思节点也才能指出哪一步需要重做。

观察结果

工具返回的数据、来源、错误和摘要都属于 Observation。不要只存一段“搜索成功”的文字。后续判断往往需要知道工具名、输入参数摘要、返回状态、结果位置和是否可重试。

控制字段

例如当前阶段、已执行步数、连续失败次数、是否需要人工、停止原因和状态版本。控制字段看起来不“智能”,却决定系统是否可控。循环能不能停、失败后走哪条边,通常都依赖这些字段。

对外结果

最终答案、引用、仍未解决的问题和执行摘要最好有独立字段。这样最终输出节点不必重新翻完整消息历史,也方便接口层只返回允许暴露的信息。

State 设计有一个实用原则:每个字段都应该能说清由哪个节点写、哪些节点读、是否允许覆盖。如果一个字段任何节点都能随意修改,它很快会变成隐形耦合点。

Node:把每一步写成可验证的状态变换

Node 可以是调用模型、执行工具、检查规则或整理输出的函数。它最重要的边界不是“一个节点只能做一件小事”,而是一次状态变化要有清楚责任。

以常见的五个节点为例。

Router:确认任务走哪条主路径

Router 读取用户目标和必要上下文,输出意图、任务类型或是否需要澄清。它不应该顺手执行搜索,也不应该提前生成最终答案。路由结果最好是有限枚举,而不是任意文本,否则后面的条件边无法稳定判断。

Planner:生成可执行计划

Planner 把目标拆成步骤,并标记每一步需要的信息或工具。计划必须接受校验:步骤是否为空、工具是否存在、依赖是否合理、高风险动作是否出现。模型生成了一个格式正确的计划,不等于计划可执行。

Executor:执行当前步骤

Executor 只处理计划中的当前动作。它调用工具前做参数与权限校验,调用后把结构化结果写回 Observation,同时更新步骤状态。工具异常不应直接让整张图崩溃,而要转成可路由的错误状态。

Reflector:判断继续、修正还是停止

Reflector 读取任务目标、计划和现有结果,判断证据是否足够、是否偏题、是否需要重规划。它输出的是决策信号和理由,而不是悄悄重写所有状态。是否真正跳回 Planner,由条件边决定。

Finalizer:形成对外答案

Finalizer 只在满足完成条件或明确降级时运行。它整合已经验证的结果,标出不确定信息,不再自行调用新工具。把收尾单独做成节点,可以避免模型一边继续探索,一边又声称任务已经完成。

好的节点应该尽量接近“读入一份状态,返回一组增量更新”。不要在节点内部偷偷修改全局变量,也不要让同一工具调用既发生在 Executor,又发生在 Reflector。责任清楚以后,单节点测试和失败回放才有意义。

从目标到答案的状态流

先定义每个节点的输入输出,再连接边,比先画一张漂亮架构图更可靠。

Edge:固定流程和动态决策要分开

Edge 决定节点之间怎么跳转。它不只是一条连线,更是一项控制权声明。

固定 Edge 适合没有争议的顺序。例如 Router 完成后进入 Planner,Finalizer 完成后结束。只要前一步成功,下一步始终相同,就没有必要再调用模型决定。

Conditional Edge 适合结果确实会改变路径的地方。例如 Executor 返回后,可以根据状态走向 Reflector、错误处理或人工确认;Reflector 可以选择继续执行、回到 Planner、生成答案或安全停止。

条件函数应优先读取结构化字段。比如根据 statusretryableneeds_humandone 路由,而不是再让模型读一遍长文本并输出“下一步去哪”。模型可以参与产生这些信号,但系统应校验信号是否合法,并为未知值准备默认出口。

这里有个很重要的工程取舍:不是所有分支都应该交给 LLM。鉴权失败必须停止,达到预算必须停止,高风险写操作必须转人工,这些属于确定性规则。只有“现有证据是否足以回答”“是否需要换一个检索角度”这类难以完全枚举的判断,才适合让模型提供建议。

怎样设计循环,才不会把图变成另一种死循环

图允许有环,是 LangGraph 适合 Agent 的原因之一。Executor 可以回到 Planner,Reflector 可以让任务继续。但“能画环”不代表“环会自动收敛”。

一个受控循环至少需要四类出口。

第一类是业务完成。不是模型说“完成了”,而是必要步骤有结果、输出字段齐全、关键证据满足要求。

第二类是资源上限。包括最大步数、总耗时、模型调用预算和工具调用预算。具体阈值要通过真实任务观察后确定,不能照抄一个固定数字。

第三类是无进展。连续重复相同工具和参数、多个回合没有新增信息、同一错误反复出现,都说明继续循环的边际价值很低。

第四类是风险出口。权限不足、出现高风险动作、状态冲突或关键工具不可用时,系统应暂停、降级或转人工,而不是让模型继续猜。

为了避免循环条件被不同节点各自解释,可以把停止判断收敛到一个守卫函数或专门节点。它读取控制字段和业务结果,输出有限状态:继续、完成、降级、人工、终止。这样后续修改阈值时,不需要在多个节点里寻找重复逻辑。

一份更像工程代码的状态定义

下面的代码只展示结构,字段要按真实任务裁剪:

from typing import TypedDict, Literal

class AgentState(TypedDict):
    task: str
    constraints: list[str]
    plan: list[dict]
    current_step: int
    observations: list[dict]
    status: Literal["planning", "running", "review", "done", "failed"]
    step_count: int
    consecutive_failures: int
    needs_human: bool
    stop_reason: str | None
    final_answer: str | None

设计这份结构时,可以逐字段做三个检查。

第一,能否从 State 还原当前任务进行到了哪里。如果进程中断后只看到一堆 Messages,却不知道哪一步已经完成,状态设计还不够明确。

第二,节点失败后能否安全重试。如果重试 Executor 会重复发送消息或重复写库,就需要加入操作标识、幂等键或执行状态。

第三,旧状态能否被新版本代码理解。项目迭代后字段会变化,持久化状态最好带版本,并准备迁移或拒绝恢复的策略。否则“可以保存 Checkpoint”不等于“可以长期恢复”。

StateGraph 和普通 Workflow、ReAct 循环怎么选

StateGraph 不是所有 Agent 的必选项。简单任务只有一个工具循环,手写 for 往往更容易看清机制。步骤完全固定、没有循环,也可以使用普通 Workflow 或状态机。

当出现以下信号时,图结构开始有价值:多个阶段共享状态;不同错误需要不同出口;存在回退和重规划;需要暂停后恢复;需要对单个节点做重试或观测;团队希望把控制流作为可审查资产。

相反,如果整张图只有“模型节点到工具节点再回模型节点”两条边,却没有明确状态、错误路由和停止条件,那么使用 LangGraph 只是换了框架外壳,系统深度并没有增加。

三种编排方式的适用边界

框架选择取决于控制流和状态复杂度,不取决于项目介绍里想放多少技术名词。

这张图最容易在哪些地方出错

State 越积越大

把每轮完整页面、日志和模型响应都追加到 State,会让序列化、模型输入和回放越来越重。解决方法不是粗暴删除,而是区分运行状态、可检索工件和审计日志。State 保存当前决策需要的内容,大对象放外部存储,只保留引用。

节点边界太粗

一个节点里同时规划、调用工具、判断结果和写最终答案,任何一步失败都只能整体重跑。拆分不是越细越好,而是按失败边界和责任边界拆。需要不同重试、权限或观测策略的步骤,通常值得独立成节点。

条件边读取自由文本

如果路由函数依赖“模型回答里有没有某个词”,提示词轻微变化就可能走错。更稳的做法是要求模型输出枚举字段,再做 Schema 校验,对非法值走安全默认路径。

并行节点覆盖状态

两个节点同时更新同一字段,可能出现顺序不确定或结果丢失。并行前要定义字段合并规则,例如列表追加、按键合并或冲突失败。不能默认“框架会知道哪个结果更重要”。

重试造成重复副作用

只读搜索重复一次通常影响较小,发送通知、创建记录或扣减资源重复一次就可能出事故。图级重试必须结合工具幂等、操作 ID 和执行前状态检查。

怎样证明你的 StateGraph 不是只在演示里跑通

面试里最有说服力的不是展示一张图,而是拿出围绕图的验证证据。

先准备节点级样本。给 Router 一组容易混淆的输入,检查输出是否落在合法枚举;给 Planner 一组边界任务,检查计划能否被执行;给 Executor 注入超时、空结果和格式错误;给 Reflector 准备“信息足够”和“看似完整但缺关键证据”的对照样本。

再准备路径级样本。至少覆盖正常完成、工具失败后换路、计划不可执行后重规划、达到上限后停止、需要人工后暂停。每条样本都应有预期节点序列和最终状态,而不只看最终答案顺不顺眼。

还要检查状态不变量。例如步数只能增加不能倒退,完成状态不能再次进入工具节点,权限失败不能进入写操作,最终答案生成后不能悄悄修改已确认的任务目标。不变量适合由代码断言,不能只靠提示词提醒。

最后保存 Trace。每一步至少记录节点名、输入状态摘要、输出增量、路由结果、模型和工具耗时、错误类型。涉及敏感信息时只保存脱敏摘要或引用。出现失败后,应该能回答:哪个节点第一次产生错误状态,哪条边把它送到了错误路径,后续为什么没有拦住。

StateGraph 上线前验收清单

能回放一条失败路径,往往比顺利跑通十次更能证明你理解这张图。

面试官会怎样连续追问

追问一:State 里为什么不直接放完整 Messages

Messages 适合保存对话轨迹,但不足以表达步骤状态、错误类型、权限、预算和完成条件。可以保留 Messages,同时增加结构化业务状态;大对象和审计日志放外部存储,State 只保留当前决策所需内容和引用。

追问二:Planner 和 Router 能不能合成一个节点

可以,前提是它们的失败处理、输入输出和优化方式一致。若 Router 只做有限路径选择,而 Planner 需要生成和校验多步计划,拆开后更容易独立评估,也能避免所有任务都进入昂贵规划。

追问三:Conditional Edge 为什么不全部交给模型

权限、预算、幂等和高风险操作属于硬规则,模型不应拥有绕过权。模型适合判断语义上难枚举的部分,输出有限决策信号,再由代码校验和路由。

追问四:一个节点失败了怎么重试

先区分可重试与不可重试错误,再判断节点是否有副作用。只读、幂等节点可以按策略重试;写操作要使用幂等键、执行状态和补偿流程。重试次数和间隔应由真实故障特征决定。

追问五:什么时候应该回到 Planner

当前计划不可执行、关键假设被工具结果否定、任务约束发生变化,或继续执行不会满足目标时才重规划。单次工具超时通常先在工具层处理,不必每次都让 Planner 重写全局计划。

追问六:怎样测试一张有环图

同时做节点测试、路由测试和路径测试。固定随机性,替换模型与工具为可控桩,验证预期状态变化与节点序列;再用真实模型跑一组回归任务,检查行为分布和失败 Trace。

用 State、Node 和 Edge 讲清 StateGraph

我把 StateGraph 看成一个显式状态机,不只是把几个模型节点连起来。State 保存任务目标、计划、当前步骤、工具结果、错误和步数等结构化信息;Node 负责单一、可验证的状态变换,比如 Router、Planner、Executor、Reflector 和 Finalizer;固定 Edge 处理确定顺序,Conditional Edge 根据经过校验的状态选择继续、重规划、人工或结束。设计顺序是先写完成条件和失败出口,再做最小 State,然后按失败边界拆节点,最后连接边。循环必须同时有业务完成、资源上限、无进展和风险出口。对高风险动作、权限和预算,我用代码规则控制,不让模型自由路由。验证时不仅看最终答案,还会做节点级样本、整条路径测试、状态不变量和 Trace 回放,确保出了问题能定位到具体节点和状态变化。

StateGraph 是否可控:六项项目证据

  1. 你能否画出真实节点和条件边,而不是照抄框架示例。
  2. 每个 State 字段由谁写、谁读、能否覆盖,是否说得清。
  3. 每个循环出口是否有代码级条件,而不只依赖模型自觉停止。
  4. 工具失败、计划失效、预算耗尽和人工介入是否各有路径。
  5. 写操作重试时是否有幂等与副作用控制。
  6. 是否保存一条可以回放的失败 Trace 和对应修复证据。

如果目前只有一张设计图,没有真实路径测试,就把它诚实地说成设计方案。判断 StateGraph 是否专业,要看状态能否解释、跳转能否约束、失败能否回放,节点数量本身没有说服力。

INTERVIEW FOLLOW-UPS

面试官会怎样继续追问?

这些追问会继续检查方案取舍、验证方法和项目事实。
  1. Q01

    State 里为什么不能只保存完整 Messages?

  2. Q02

    Planner、Executor 和 Reflector 的节点边界怎样划分?

  3. Q03

    Conditional Edge 哪些条件可以交给模型,哪些必须由代码决定?

  4. Q04

    怎样测试一张带循环和回退的 StateGraph?

PROJECT CHECK

换成你的项目,这几件事能否说清楚?

  • 列出每个 State 字段的读写节点和覆盖规则
  • 为每个节点写清输入、输出与失败出口
  • 同时设置业务完成、资源、无进展和风险停止条件
  • 保存一条可以回放的失败路径与状态增量
用自己的项目经历练一次