第 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)——图把「控制流的动态性」变成了一等公民。