数据截至 (上游 commit 1b7d2e80db9f)
编排骨架:三层 LangGraph 与状态流转
30 秒导读: Open Deep Research 用 LangGraph 把"做一次深度研究"编排成一条主干流水线:先问清楚要研究什么,再把对话压成一份研究提纲,然后交给一个"研究主管"子图去调兵遣将,最后把所有笔记写成报告。本章讲透这条主干怎么走、以及支撑它的三层 State + 一个自定义 reducer 为什么这么设计——研究主管和研究员内部怎么循环,留给 02。
1. 先建立直觉:主图就是一条四步流水线
把整个系统想象成一家研究外包公司接 一单活:
- 前台先跟客户确认需求("你说的'最好的电动车'是指价格、续航、还是充电网络?")——不清楚就打回去问,清楚就放行。
- 项目经理把这段对话整理成一份书面立项书(research brief),交给研究部门。
- 研究总监拿着立项书拆任务、派给若干研究员并行去查,收集笔记。
- 撰稿人把所有笔记汇总成一份最终报告交给客户。
这四步在代码里就是主图(deep_researcher)的四个节点,一条线串下来。它定义在 deep_researcher.py:701-719 的 deep_researcher_builder:
# 示意,非源码 —— 主图的骨架(真实定义见 deep_researcher.py:707-716)
builder.add_node("clarify_with_user", ...) # ① 前台:澄清需求
builder.add_node("write_research_brief", ...) # ② 项目经理:写立项书
builder.add_node("research_supervisor", ...) # ③ 研究总监(一个子图)
builder.add_node("final_report_generation", ...) # ④ 撰稿人:写报告
builder.add_edge(START, "clarify_with_user") # 入口
builder.add_edge("research_supervisor", "final_report_generation") # 研究完 → 写报告
builder.add_edge("final_report_generation", END) # 出口
这节要留意两件"图上看不全"的事:
- ①→② 和 ①→END 这两条边不在上面的静态边里。澄清节点用
Command(goto=...)在运行时动态决定跳哪——这是 clarify-or-skip 路由,下面第 3 节讲。 - ③
research_supervisor不是一个普通函数节点,而是另一个编译好的子图(supervisor_subgraph)整个塞进来当一个节点。这就是"三层"里的中间层,第 5 节讲。
2. 顶层全景图:节点、边、状态怎么流
先看整条主干 怎么转。方向从上到下,实线是静态边,虚线是节点内 Command(goto=...) 的动态跳转:
┌──────────────────────────┐
用户消息 ──▶ │ clarify_with_user │ ① 需要澄清吗?
(messages) │ 澄清 or 跳过 │
└───────┬──────────┬───────┘
need_clar=True │ │ need_clar=False / 关闭澄清
(回一句问题) ▼ ▼ (动态 goto)
END ┌──────────────────────────┐
│ write_research_brief │ ② 对话 → research_brief
│ 定题 + 初始化 supervisor │
└───────────┬──────────────┘
│ (动态 goto,带 override 初始化)
▼
┌──────────────────────────┐
│ research_supervisor │ ③ 子图(见 02)
│ 委派研究 + 收集 notes │
└───────────┬──────────────┘
│ (静态边)
▼
┌──────────────────────────┐
│ final_report_generation │ ④ notes → final_report
└─────── ────┬──────────────┘
│ (静态边)
▼
END
四个节点的一句话职责:
| 节点 | 干什么 | 真实符号 · 位置 |
|---|---|---|
| clarify_with_user | 判断需求是否清晰;不清晰就回一句问题结束,清晰就放行 | clarify_with_user · deep_researcher.py:60-115 |
| write_research_brief | 把整段对话压成一份 research_brief,并初始化 supervisor 的上下文 | write_research_brief · deep_researcher.py:118-175 |
| research_supervisor | 一整个子图:主管拆任务、并行派研究员、汇总 notes/raw_notes | supervisor_subgraph · deep_researcher.py:353-363 |
| final_report_generation | 把所有 notes 拼成 findings,生成 final_report | final_report_generation · deep_researcher.py:607-697 |
怎么读这张图: 关键的分支只有一个——在①,要么打回去问用户(走 END,一次对话到此为止,等用户补充后重新进图),要么放行往下走。②③④ 是一条直线,没有分支。真正的复杂度(拆任务、并行、循环)全藏在③那个子图里。
3. clarify_with_user:澄清 or 跳过的 路由
这节讲第一个节点怎么做"要不要问用户"的决策。
它要解决的小问题
用户开口常常太笼统:"研究一下大模型推理市场"——是指价格?厂商份额?延迟?一头扎进去研究,很可能查了半天方向不对。所以第一步是判断需求是否清晰,不清晰就先问一句。
三种走向
clarify_with_user 返回一个 Command,goto 有三种可能:
| 情况 | 判断依据 | goto | 附带更新 |
|---|---|---|---|
| 配置里关掉了澄清 | allow_clarification 为假 | write_research_brief | 无,直接放行 |
| 需要澄清 | 模型判定 need_clarification=True | END | 把 question 作为 AI 消息回给用户 |
| 不需要澄清 | 模型判定 need_clarification=False | write_research_brief | 把 verification(确认语)作为 AI 消息 |
配置开关是第一道闸门(deep_researcher.py:75-77):
# 真实源码 deep_researcher.py:75-77 —— 关掉澄清就直接放行,连模型都不调
if not configurable.allow_clarification:
return Command(goto="write_research_brief")
用结构化输出逼模型给出干净的三段
判断"要不要问"如果让模型自由发挥文本,下游没法可靠 地解析。这里用 structured output——强制模型按一个 Pydantic 模型填字段。绑定发生在 deep_researcher.py:89-94:configurable_model.with_structured_output(ClarifyWithUser)。
ClarifyWithUser 这个 schema 定在 state.py:30-41,只有三个字段:
# 真实源码 state.py:30-41 —— 结构化输出的契约
class ClarifyWithUser(BaseModel):
need_clarification: bool # 要不要问
question: str # 要问什么(need=True 时用)
verification: str # 确认语(need=False 时用,告诉用户"开始研究了")
拿到这三个字段后,节点就能无歧义地路由——need_clarification 决定 goto,question 或 verification 决定回给用户哪句话(deep_researcher.py:104-115)。
巧妙处:
verification字段看着多余(不需要澄清干嘛还回一句话?),其实是为了给用户一个"我理解对了、这就开始"的回执,同时把这句话写进messages,让最终报告阶段(final_report_generation会读messages)也能看到这段上下文。
4. write_research_brief:把对话压成提纲,并"接管"下游上下文
这节讲第二个节点——它做两件事:定题和初始化 supervisor。