每个 Agent 系统开始回答之前,都要先解决一个朴素问题:谁负责接请求?你在前端敲下的一句话,不能直接交给模型。它要先被登记成一次有 ID、有生命周期、可流式观察的 run,模型才有回复你。这一章看 DeerFlow 如何拿到输入、创建 run、装入运行时上下文,再把执行事件持续流回前端。
先记住一条边界:DeerFlow 先托管一次运行,才轮到 agent 做推理。 Gateway 就是这个运行时宿主:它负责接入、run 生命周期、上下文注入、事件流和结束判定。推理发生在后续的图执行阶段。
flowchart TD
U["前端 / IM / LangGraph SDK"] --> EP["POST /threads/{id}/runs/stream"]
EP --> SR["stream_run() · start_run()"]
SR --> RM["RunManager.create_or_reject()"]
RM --> CFG["build_run_config() · 兼容适配"]
CFG --> TASK["create_task(run_agent)"]
TASK -. 后台 .-> RA["run_agent()"]
RA --> RT["Runtime(context=...)"]
RT --> MK["make_lead_agent(config)"]
MK --> ASM["agent.astream(...)"]
ASM --> SB["StreamBridge"]
SR --> SSE["sse_consumer() · SSE 事件"]
SB --> SSE 三种「运行时」,先分清
读这一段最容易误解的地方,是把三样东西混成一样:
LangGraph = agent 用的执行内核(图怎么跑)
LangGraph Server = 官方可选的 HTTP 运行时
DeerFlow Gateway = 当前主要的 HTTP 运行时,兼容 LangGraph API
backend/langgraph.json 里还登记着 lead_agent,那是为了让官方 LangGraph Server / Studio / CLI 还能照常工作。但主链上 Gateway 是直接 import 并调用图工厂的,没有经过官方 server。所以常规服务路径走的是 Gateway 这套自己实现的运行时,不是 LangGraph Server。
请求落地的四个零件
打开 thread_runs.py 和 services.py ,你会反复碰到四个角色。这里不需要先背字段,先记住它们各自回答哪个问题。
RunCreateRequest —— 请求长什么样。它带着 input、command、config、context、metadata、stream_mode,还有 interrupt_before / interrupt_after、multitask_strategy、on_disconnect 这些控制位。它是「这次 run 想怎么跑」的完整声明。
RunManager —— run 的登记处。create_or_reject() 决定这次请求能不能创建 run(比如同一 thread 已经在跑、按 multitask_strategy 该拒就拒),之后负责 set_status / cancel / 持久化 run 元数据。
StreamBridge —— 一道解耦闸。后台图在产事件,HTTP 这边在消费事件,两边不直接互相阻塞:run_agent() 往 bridge 里发,sse_consumer() 从 bridge 里取、再格式化成 SSE 事件。断连后是否继续跑,不由 bridge 自己决定,而由 on_disconnect 决定:cancel 会取消后台 run,continue 才会让它继续执行。
run_agent() —— 执行 run 的后台 worker( worker.py )。注意:它负责托管 agent 执行,不是 agent 本身。
configurable,还是 context?
build_run_config() 的职责是做入口层「翻译」:把 HTTP 请求里的字段,转换成 LangGraph 认识的 RunnableConfig 形状。这里有整篇最该记的一处兼容债:
config["configurable"] 旧的运行时选项通道
config["context"] 新的 LangGraph runtime context 通道
DeerFlow 在兼容模式下,会把关键字段同时写进两边——model_name、mode、thinking_enabled、reasoning_effort、is_plan_mode、subagent_enabled、max_concurrent_subagents、agent_name、is_bootstrap 这些白名单字段,两个通道各放一份。这是兼容成本:老的 DeerFlow 代码从 configurable 读,新的 ToolRuntime.context 从 context 读;只写一边,就会有一类消费者读不到。
随后 run_agent() 还会建一个 LangGraph Runtime,把 thread_id / run_id / user_id / app_config 这些 run 级数据装进 runtime.context,再塞回 config["configurable"]["__pregel_runtime"],让下游中间件和工具都能按 LangGraph 的运行时 API 拿到它。/runs/wait 也不再只是直接 await 后台 task;它会消费同一条 bridge,并按 on_disconnect 处理等待中的断连,避免把半途断开的 run 误当成正常完成。
自己当宿主 · Gateway
- 自己掌握鉴权 / 元数据 / 回滚 / 持久化
- 前端、IM、SDK 多宿主统一接入
- run 的生命周期完全可控
代价 · 兼容适配
- 字段要翻成 LangGraph 风格的 config
configurable/context双写,历史债run_agent()职责集中,局部修改成本高
易误解的要点
configurable和context是历史兼容债。 某个字段只写了一边,老代码或新ToolRuntime.context消费者就可能读空。加新字段时,先确认它该进哪个白名单、是不是要双写。run_agent()的职责偏重。 它把图调用、持久化、流式发布、回滚、tracing、清理放在同一条路径上——功能很全,但也因此更难局部修改。改它之前,先认清你动的是哪一段。- run 在后台 task 中执行。 断连、取消、回滚、最终 checkpoint 落盘都要和后台任务状态对齐,否则可能把「跑了一半的状态」当成成功返回。
- 它不直接改
ThreadState。 run 记录、thread 元数据、流事件是 Gateway 写的;agent 的图状态由图执行自己写。看到状态变化,先分清是哪一层负责写入。
入口这一章只做一件事:把用户输入登记成一次正在执行的 run,把上下文交给图执行,再把事件流回前端。完成这些准备后,run 会进入 make_lead_agent():请求数据会继续被装配成一张能运行的 agent 图。