跳到主要内容

01 · 状态与图:骨架长什么样

这一章讲整个循环的骨架:贯穿全程的状态是怎么设计的,五个节点怎么连成一张会自我回环的图,循环靠什么变量、在哪里停。读完你就能在脑子里跑一遍这张图。

1.1 共享状态:一本被反复改写的笔记本

LangGraph 的每个节点都是纯函数:读状态 → 返回一个「状态更新」字典。所以先看状态。整个项目的状态就一个 dataclass,SummaryState:

# state.py:7 —— 真实源码(略去注释)
@dataclass(kw_only=True)
class SummaryState:
research_topic: str = field(default=None)
search_query: str = field(default=None)
web_research_results: Annotated[list, operator.add] = field(default_factory=list)
sources_gathered: Annotated[list, operator.add] = field(default_factory=list)
research_loop_count: int = field(default=0)
running_summary: str = field(default=None)

各字段的角色:

字段作用谁写它
research_topic用户给的话题,全程只读输入
search_query当前这一轮要搜的查询generate_query / reflect_on_summary
web_research_results每轮格式化后的搜索文本(累加)web_research
sources_gathered每轮的来源清单(累加)web_research
research_loop_count已经搜了几轮web_research
running_summary那本「不断被改写的笔记本」,最终即报告summarize_sources / finalize_summary

关键设计:两个字段带 Annotated[list, operator.add] 这是 LangGraph 的 reducer(归并器) 机制——当一个节点返回 {"web_research_results": [x]} 时,LangGraph 不会覆盖旧值,而是用 operator.add 把新列表拼到旧列表后面(state.py:10-11)。所以每轮搜到的东西是累积的,而不是互相覆盖。

running_summary 是普通 str 字段,没有 reducer——所以 summarize_sources 每次返回的新摘要是整体替换旧摘要。这正是我们想要的:笔记本被改写(replace),来源被追加(append)。

1.2 输入/输出裁剪:对外只露两个字段

虽然内部状态有 6 个字段,但对图的调用方只暴露一个入、一个出:

# state.py:16-23 —— 真实源码
@dataclass(kw_only=True)
class SummaryStateInput:
research_topic: str = field(default=None) # 只需要给话题

@dataclass(kw_only=True)
class SummaryStateOutput:
running_summary: str = field(default=None) # 只拿回报告

这两个 schema 在 graph.py:445-450 建图时通过 input= / output= 传给 StateGraph。好处:调用方不必了解 research_loop_countsources_gathered 这些内部机械,接口干净。

1.3 把节点连成图

建图代码集中在 graph.py:444-465,读起来几乎就是那张全景图的文字版:

# graph.py:445-463 —— 真实源码(节选)
builder = StateGraph(
SummaryState, input=SummaryStateInput,
output=SummaryStateOutput, config_schema=Configuration,
)
builder.add_edge(START, "generate_query")
builder.add_edge("generate_query", "web_research")
builder.add_edge("web_research", "summarize_sources")
builder.add_edge("summarize_sources", "reflect_on_summary")
builder.add_conditional_edges("reflect_on_summary", route_research) # ← 回环在这
builder.add_edge("finalize_summary", END)
graph = builder.compile()

注意有两种边:

  • add_edge(A, B):无条件边,A 走完必定去 B。前四步是一条直线。
  • add_conditional_edges("reflect_on_summary", route_research):条件边,reflect 走完后调用 route_research 函数,由它返回的字符串决定去哪。这就是「再来一轮 vs 收尾」的岔路口。

graph 这个编译好的对象,正是 langgraph.json:4 里注册给 LangGraph Studio 的入口("ollama_deep_researcher": "./src/ollama_deep_researcher/graph.py:graph")。

1.4 循环靠什么转、在哪停

岔路口的逻辑非常短:

# graph.py:421-441 —— 真实源码(节选)
def route_research(state, config) -> Literal["finalize_summary", "web_research"]:
configurable = Configuration.from_runnable_config(config)
if state.research_loop_count <= configurable.max_web_research_loops:
return "web_research" # 回到搜网,带着 reflect 生成的新 query
else:
return "finalize_summary" # 够了,收尾

计数器 research_loop_countweb_research 每次 +1(graph.py:260)。这里有个容易数错、值得点明的细节:判断用的是 <=(小于等于),而计数在每次搜网后才递增。把默认 max_web_research_loops = 3(configuration.py:19-23)代进去逐拍推演:

时刻research_loop_countroute 判断结果
第 1 次 web_research 后11 ≤ 3 真再搜
第 2 次后22 ≤ 3 真再搜
第 3 次后33 ≤ 3 真再搜
第 4 次后44 ≤ 3 假收尾

所以 max_web_research_loops=3 实际会执行 4 次搜索(不是 3 次)。这是 <= 加「先搜后计数」共同造成的,阅读或改这段时要留意。

1.5 一次完整回合的数据流

把状态和图合起来,一轮循环里数据这样流动:

search_query ──▶ [web_research] ──▶ 追加 web_research_results[]
└─▶ 追加 sources_gathered[]
└─▶ research_loop_count += 1

web_research_results[-1] ──▶ [summarize] ──▶ 替换 running_summary

running_summary ──▶ [reflect] ──▶ 覆盖 search_query(新的追问)

[route] ──▶ 回到 web_research 或去 finalize

注意 summarize_sources 只吃最新一轮的资料(state.web_research_results[-1],graph.py:284),不是每次重读全部——因为「历史」已经沉淀进 running_summary 那本笔记本里了。这就是「滚动摘要」省上下文的关键。

下一章我们钻进每个节点,看它们各自的真实实现。→ 02-nodes-walkthrough.md

代码地图

主题文件符号
共享状态src/ollama_deep_researcher/state.pySummaryState
累加字段 reducersrc/ollama_deep_researcher/state.pyAnnotated[list, operator.add]
输入/输出 schemasrc/ollama_deep_researcher/state.pySummaryStateInput / SummaryStateOutput
建图与边src/ollama_deep_researcher/graph.pybuilder / add_conditional_edges
循环判停src/ollama_deep_researcher/graph.pyroute_research
Studio 入口注册langgraph.jsongraphs.ollama_deep_researcher