第 4 章 · 状态图:用什么承载协调

一句话结论:有了「角色」和「Plan 契约」,还得回答「这些角色按什么顺序、什么规则跑起来」。DeerFlow 的答案是——用 LangGraph 的「状态图」(stateful graph)来承载协调,而不是手写 if-else,也不是搞一堆 agent 自由聊天。


1. 先把问题说清楚:协调的「流程」该写在哪?

角色、Plan 都是「静态的零件」。但协调还有一层动态的东西:什么时刻谁先跑、跑完把控制权交给谁、什么条件下回头重来。这层「流程」有几种写法:

方向 A:手写 if-else / 函数调用链。

# 手写流程:直接、但脆弱
plan = planner(question)
if user_wants_review(plan):
    plan = revise(plan)
results = research_team(plan)
report = reporter(results)
  • 优点:简单直白,Go 程序员一眼懂。
  • 致命伤:流程写死在代码里。想加一个「失败重试」「中途暂停」「并行分支」,就得改代码结构;而且运行到一半的状态(跑了几步、每步结果)没有地方存——进程一崩,全没了。

方向 B:agent 自由对话(群聊,谁想说谁说)。

  • 优点:灵活。
  • 致命伤:没有确定的控制流,也就是没有「收敛条件」。前面已经反复强调过,这里不展开。

方向 C:显式的状态图(LangGraph)。

  • 把「协调流程」建模成一张有节点、有边、有共享状态的图。每个角色是一个节点,边定义了「谁跑完接着谁跑」,所有角色通过一份共享的 State 读写数据。

为什么 DeerFlow 选 C:因为状态图同时给了三样手写流程给不了的东西——① 可中断(checkpoint),② 可追溯(可视化),③ 可持久化(状态存下来能恢复)。对一个「要跑几分钟到几小时」的长任务来说,这三样是刚需,不是锦上添花。


2. 状态图的三要素

一张 LangGraph 状态图,本质就三样:

# ① 状态(State):所有角色共享的「共享黑板上的一份数据」
class ResearchState(TypedDict):
    question: str                 # 用户原始问题
    plan: Plan | None             # 契约:研究计划(第 3 章)
    step_results: dict            # 每一步的执行结果,随执行填充
    final_report: str | None      # 最终报告
 
# ② 节点(Node):一个角色/一个处理单元,接收 State、返回 State 的增量
def planner_node(state: ResearchState):
    state["plan"] = planner_run(state["question"])
    return {"plan": state["plan"]}
 
# ③ 边(Edge):控制流——谁跑完指向谁
graph.add_edge("coordinator", "planner")
graph.add_edge("planner", "research_team")
graph.add_edge("research_team", "reporter")

关键心智:节点之间不直接互相调用,而是「读写同一份 State」。A 节点把结果写进 State 的某个字段,B 节点从 State 里读。这就是「协调的载体」——所有 agent 通过一份共享 State 对齐,而不是通过函数参数传来传去。


3. 思考的分叉:共享 State vs 消息传递

这里有个值得深挖的选择——agent 之间交换信息,到底用共享 State,还是用消息队列(message passing)?

维度共享 State(LangGraph 的做法)消息传递(Actor 模型的做法)
数据流所有角色读写同一份结构角色之间点对点发消息
一致性单份数据,天然一致各持副本,需处理时序/乱序
追溯状态就是历史快照,好查要靠消息日志重建,麻烦
心智负担「一块黑板」「收发信件」

为什么 DeerFlow(及 LangGraph 生态)选共享 State:

  • 多 agent 研究里,所有角色的工作都围绕同一份 Plan 展开,天然是「大家往同一张表上填」的模式,而不是「互相寄信」。
  • 共享 State 让「检查点/恢复」变得简单:状态就是一个快照,存下来就能随时恢复。
  • 消息传递更适合「角色间松耦合、异步、需要隔离」的场景(比如 2.0 里跨 worker 的租约协调),但对 1.x 这种「围绕一份 Plan 的强耦合流水线」而言,共享 State 最直白。

类比:共享 State 是「会议室里的一块白板」,大家往上写、往上读;消息传递是「各自办公桌,靠递纸条」。当所有人都在为同一个项目(同一份 Plan)工作、且需要随时看到全貌时,白板比递纸条高效得多。


4. 伪代码:把整个研究流程画成一张图

把前面所有角色串起来,就是 DeerFlow 1.x 的完整图:

graph = StateGraph(ResearchState)
 
graph.add_node("coordinator", coordinator_node)      # 入口:路由
graph.add_node("planner", planner_node)              # 出 Plan
graph.add_node("human_feedback", human_feedback_node)# 人工闸门(第 8 章)
graph.add_node("research_team", research_team_node)  # 派活给研究员/程序员(第 5 章)
graph.add_node("reporter", reporter_node)            # 汇总成稿
 
graph.set_entry_point("coordinator")
 
# 控制流(边),注意有两个「条件边」,这是状态图比 if-else 强的地方
graph.add_edge("coordinator", "planner")
graph.add_conditional_edges(
    "planner", should_pause_for_human,               # 计划要不要人工过目
    {"review": "human_feedback", "accept": "research_team"}
)
graph.add_conditional_edges(
    "research_team", need_replan,                    # 执行完要不要重规划(第 9 章)
    {"replan": "planner", "done": "reporter"}
)
graph.add_edge("reporter", END)
 
app = graph.compile(checkpointer=checkpointer)       # 关键:带 checkpointer

注意最后一行 checkpointer——它让整张图的每个中间状态都能存盘。这正是「状态图」相对「手写 if-else」最硬的差异:手写流程里,跑到一半的进度只存在于进程内存里,一崩就没;而带 checkpoint 的状态图,随时可以「从第 N 步的现场」恢复。这是长任务 agent 系统的命根子,第 9 章会展开。


思考题

Q:上面这段代码里,我故意用了两个 add_conditional_edges(条件边)。如果不用条件边,而是用普通边把流程写死成「planner → research_team → reporter」,会丢掉什么能力?为什么「条件边」是协调的关键?

参考答案:

条件边本质是把「下一步去哪」这个决策,从写死的代码里,交还给运行时状态。

写死成普通边,等于假设「计划一定对、执行一定够、不需要回退」。但真实研究里,这两条假设经常不成立:

  • 计划可能需要人工改(所以 planner 之后要能拐去 human_feedback);
  • 执行完可能发现资料不够(所以 research_team 之后要能拐回 planner 重新规划)。

条件边让你能基于当前 State 动态决定走向,于是整张图才从一个「流水线」升级成一个「能回环、能分叉的活系统」。协调的精髓,正在于「控制流本身可以随状态变化」,而不是一套固定死的顺序。 这也是为什么用图(而不是 if-else)——图把「控制流的动态性」变成了一等公民。