每个 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、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 事件。即使前端断开,后台任务也不会被迫中断。
run_agent() —— 真正干活的后台 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、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 拿到它。
自己当宿主 · Gateway
- 自己掌握鉴权 / 元数据 / 回滚 / 持久化
- 前端、IM、SDK 多宿主统一接入
- run 的生命周期完全可控
代价 · 兼容适配
- 字段要翻成 LangGraph 风格的 config
configurable/context双写,历史债run_agent()职责集中,局部修改成本高
会绊倒你的地方
configurable和context是历史兼容债。 某个字段只写了一边,老代码或新ToolRuntime.context消费者就可能读空。加新字段时,先确认它该进哪个白名单、是不是要双写。run_agent()的职责偏重。 它把图调用、持久化、流式发布、回滚、tracing、清理放在同一条路径上——功能很全,但也因此更难局部修改。改它之前,先认清你动的是哪一段。- 它走后台 task。 断连、取消、回滚、最终 checkpoint 落盘都得小心处理,否则可能把「跑了一半的状态」当成功返回。
- 它不直接改
ThreadState。 run 记录、thread 元数据、流事件是 Gateway 写的;agent 的图状态由图执行自己写。看到状态变化,先分清是哪一层负责写入。
入口这一章只做一件事:把用户输入登记成一次正在执行的 run,把上下文交给图执行,再把事件流回前端。它接住请求、创建记录、装好上下文,然后退到幕后。接下来,run 会进入 make_lead_agent():它不再只是请求数据,而要被装配成一张能运行的 agent 图。