第 3 章:请求生命周期 —— 一条消息如何变成可恢复的运行
本章选择 Web 聊天的主路径,从点击发送一直追到最终状态提交。理解这条链路后,其他入口只是换了前半段。
阶段一:前端构造运行请求
聊天页把用户输入交给 thread hooks。请求包含三类数据:
input:HumanMessage 及附件/结构化回复元数据;context:模型名、思考开关、计划模式、子代理、agent_name 等;streamMode:通常需要values、messages-tuple和custom。
前端在发送前通过 sanitizeRunStreamOptions() 验证 stream mode。Gateway 支持的集合是 values、messages-tuple、updates、debug、tasks、checkpoints、custom;未知值直接抛错,避免“客户端以为订阅了,服务器却静默忽略”。
阶段二:Nginx 改写 LangGraph 兼容路由
浏览器只访问 :2026。Nginx 将 /api/langgraph/* 改写到 Gateway 原生 /api/*,其他 /api/* 也直接代理 Gateway,页面资源则交给 Frontend。
这一层同时处理 SSE 必需的代理设置:关闭缓冲、延长读取超时、保留连接语义。否则 Agent 在工作,用户却要等 Nginx 缓冲区攒满才看到输出。
阶段三:Gateway 做运行准入
POST /threads/{thread_id}/runs/stream 最终进入 thread-runs router。这里先验证线程归属和请求,再交给运行服务创建记录。
RunManager 的 create_or_reject() / reserve_thread_operation() 解决一个关键问题:同一线程上的状态变更必须串行化。两个运行若同时从相同 checkpoint 开始,各自提交新消息,会造成历史分叉或后写覆盖前写。系统通过数据库唯一约束与运行状态共同仲裁,而不只靠进程内锁。
可选策略决定遇到活跃运行怎么办:拒绝、等待、打断或按特定编辑重放路径处理。成功后会得到一个持久 RunRecord,SSE 连接与后台执行从此通过 run_id 关联。
阶段四:Worker 冻结运行上下文
run_agent() 接收:
StreamBridge 发布实时事件
RunManager 更新状态与所有权
RunRecord 本次运行的身份
RunContext checkpointer/store/event store 等依赖
agent_factory 通常是 make_lead_agent
graph_input 写入图的消息或 Command
config/context LangGraph 配置与可信运行上下文2
3
4
5
6
7
Worker 在调用 Agent 前会完成几项容易忽略的准备:读取运行前 checkpoint、捕获工作区快照、注入可信 user_id/run_id/role、安装 RunJournal、设置 recursion limit,并把 process-wide checkpoint channel mode 冻结。
“冻结”很重要:客户端不能通过 configurable 临时把消息通道从 full 改成 delta。进程一旦以某种 checkpoint 模式启动,后续配置不一致就失败关闭,避免同一数据库被两种 reducer 语义混写。
阶段五:Agent Graph 开始流式执行
Worker 调用图的 astream()。Lead Agent 由模型、工具、中间件链、系统提示词和 ThreadState schema 组装。一次典型循环是:
中间件可修改消息、过滤工具、发出 Command(update=...)、终止循环或注入动态上下文。LangChain create_agent() 提供循环骨架,DeerFlow 的差异化集中在这条中间件链。
阶段六:事件被发布,而不是直接写给 HTTP
Worker 观察 graph stream 并把 frame 写进 StreamBridge。常见数据面:
| 事件 | 作用 |
|---|---|
messages-tuple | token/message chunk,适合即时文字和工具参数预览 |
values | 完整 ThreadState 快照,校正瞬态增量 |
custom | task_started/running/completed、进度、控制信号 |
gap | 保留窗口出现缺口,要求客户端重载耐久状态 |
SSE handler 只负责读取 bridge。这样即使原请求断开,后台 Agent 仍可继续;用户刷新后可用同一个 run_id join,并通过 Last-Event-ID 从游标续读。
阶段七:结束不是一个瞬间
运行结束要经过“finalizing”阶段:
- 合并最后的 checkpoint 与消息;
- 记录 workspace changes;
- 验证模型是否真的产生可见终答;
- 记录 token、stop reason、错误 fallback;
- 写入 run event / delivery receipt;
- 将状态置为成功、失败、中断或取消;
- 发布终态并安排资源清理。
为什么不在 astream() 结束时立即标 completed?因为模型循环结束不等于所有旁路数据已经持久化。前端如果过早刷新历史,可能看见“运行成功但最后消息还没写好”的短暂矛盾。
失败路径
- 模型报错:LLM error middleware 生成标准化错误,worker 记录 fallback;
- 客户端断线:运行继续,重连 join;
- 事件缺口:前端收到
gap,加载 durable state,再从最新可用游标续; - 取消:RunManager 先写 durable cancel intent,再向本地任务或远程 owner 发信号;
- 进程崩溃:租约过期后由 orphan reconciliation 判定并恢复/终结;
- 工具写了一半:运行前 checkpoint 与 workspace snapshot 为编辑重放和变化记录提供回滚基准。
源码锚点
backend/app/gateway/routers/thread_runs.pybackend/packages/harness/deerflow/runtime/runs/manager.pybackend/packages/harness/deerflow/runtime/runs/worker.pyfrontend/src/core/api/api-client.ts
下一章进入 make_lead_agent(),看这台机器的发动机如何组装。