系列导航
- 第一篇:什么是 Agent?一个成熟 Agent 平台的 8 个核心组件
- 第二篇:工具系统设计 — 从全量绑定到按需加载
- 第三篇:五个核心中间件深度解析
- 本篇:Agent 生命周期与状态管理
摘要
本文阐明 Agent 系统中三个容易混淆的层次概念(Thread、Run、Loop iteration),详细说明中间件 hook 的触发时序,分析 Clarification 中断后通过 Checkpoint 恢复执行的完整机制,并讨论 Docker-compose 部署场景下 AioSandboxProvider 的 DooD 方案。
一、三层概念:Thread、Run、Loop Iteration
理解 Agent 的生命周期,首先需要区分三个层次的概念。
1.1 Thread(线程/会话)
Thread 代表一个持续的对话线程,由唯一的thread_id标识。一个 Thread 可以跨越多天、包含多次用户交互。Thread 的状态通过 Checkpointer 持久化存储。
1.2 Run(执行/请求)
Run 代表一次完整的用户请求处理过程——从接收到用户消息到返回最终响应。一个 Thread 中包含多个 Run(每次用户发消息触发一个新 Run)。
1.3 Loop Iteration(循环迭代)
Loop iteration 代表 Agent 核心 ReAct 循环中的一次模型调用。一个 Run 中可能包含多次循环迭代——模型可能需要调用多个工具后才能给出最终回答。
1.4 层次关系
Thread(会话,跨多次交互,由 Checkpointer 持久化) │ ├── Run 1(用户发送第一条消息) │ ├── Loop 1: 模型调用 → tool_call("web_search") → 工具执行 │ ├── Loop 2: 模型调用 → tool_call("read_file") → 工具执行 │ └── Loop 3: 模型调用 → 最终文本回复(无 tool_call) │ ├── Run 2(用户发送第二条消息) │ ├── Loop 1: 模型调用 → tool_call("bash") → 执行 │ ├── Loop 2: 模型调用 → tool_call("write_file") → 执行 │ ├── Loop 3: 模型调用 → tool_call("bash") → 验证 │ ├── Loop 4: 模型调用 → tool_call("str_replace") → 修复 │ └── Loop 5: 模型调用 → 最终回复 │ └── Run 3(用户发送第三条消息) └── Loop 1: 模型调用 → 直接文本回复二、中间件 Hook 的触发时序
2.1 Hook 与层次的对应关系
| Hook | 对应层次 | 执行频率 |
|---|---|---|
before_agent/after_agent | Run | 一次/Run |
before_model/after_model/wrap_model_call | Loop iteration | 每轮循环 |
wrap_tool_call | 工具调用 | 每次工具调用 |
DeerFlow 项目文档中有明确表述:
before_agent/after_agent只跑一次。before_model/after_model/wrap_model_call每轮循环都跑。
2.2 执行顺序规则
before_*正序执行:列表位置 0 → Nafter_*反序执行:列表位置 N → 0
这意味着列表最后的中间件,其after_model最先执行。
2.3 完整时序示例
以一个包含两轮工具调用的 Run 为例:
一次 Run ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ before_agent [正序,一次] │ │ [0] ThreadData: 创建线程目录 │ │ [1] Uploads: 扫描上传文件 │ │ [2] Sandbox: 分配沙箱 │ │ [12] LoopDetection: 清理旧 run 的 pending warning │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Loop 1 │ │ │ │ before_model [正序] │ │ │ │ [6] Summarization: 检查 token 是否需要压缩 │ │ │ │ [10] ViewImage: 注入图片 base64 │ │ │ │ wrap_model_call │ │ │ │ [3] DanglingToolCall: 补悬空 ToolMessage │ │ │ │ [12] LoopDetection: 注入当前 run warning │ │ │ │ ─── MODEL 调用 → 返回 tool_call ─── │ │ │ │ after_model [反序] │ │ │ │ [12] LoopDetection: 检测循环 │ │ │ │ [11] SubagentLimit: 截断多余 task │ │ │ │ [8] Title: 生成标题 │ │ │ │ wrap_tool_call │ │ │ │ [5] ToolErrorHandling: 异常转 ToolMessage │ │ │ │ [13] Clarification: 检查是否 ask_clarification │ │ │ │ ─── 工具执行 → 返回结果 ─── │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Loop 2 │ │ │ │ before_model → wrap_model_call → MODEL → after_model │ │ │ │ (模型返回最终文本,无 tool_call,循环终止) │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ after_agent [反序,一次] │ │ [12] LoopDetection: 清理当前 run 未消费 warning │ │ [9] Memory: 入队记忆更新 │ │ [2] Sandbox: 释放沙箱 │ │ │ └─────────────────────────────────────────────────────────────────────┘2.4 各中间件选择 Hook 的逻辑
| 需求场景 | 选择的 Hook | 原因 |
|---|---|---|
| 整个 Run 只需做一次的初始化/清理 | before_agent / after_agent | 避免每轮重复执行 |
| 每次模型调用前都需要检查 | before_model | 消息列表每轮都在增长 |
| 需要修改发给模型的 messages/tools | wrap_model_call | 在模型调用的最后一刻修改请求 |
| 需要修改或拦截模型返回 | after_model | 第一时间处理模型输出 |
| 需要拦截特定工具调用 | wrap_tool_call | 选择性中断 |
| Run 结束后做异步副作用 | after_agent | 不阻塞响应返回 |
三、Checkpoint 与中断恢复
3.1 Checkpoint 机制
LangGraph 的 Checkpointer 在每个图节点执行完毕后自动将当前完整状态序列化并持久化存储。DeerFlow 使用 SQLite 作为默认 Checkpointer 后端。
Checkpoint 中保存的内容包括ThreadState的所有字段:
messages:完整对话历史sandbox:沙箱 IDtitle:自动生成的标题artifacts:生成的文件列表todos:任务列表promoted:已提升的延迟工具
3.2 Clarification 中断的完整流程
当模型调用ask_clarification工具时:
步骤 1:中间件拦截并中断
# ClarificationMiddleware.wrap_tool_callreturnCommand(update={"messages":[ToolMessage(content="❓ 你要删除哪个文件?",...)]},goto=END,)Command(goto=END)使图执行跳转到__end__节点。此时图正常终止,Checkpointer 保存最终状态:
# Checkpoint 中保存的状态{"messages":[HumanMessage("帮我删除文件"),AIMessage(tool_calls=[{"name":"ask_clarification","args":{...}}]),ToolMessage("❓ 你要删除哪个文件?")],"sandbox":{"sandbox_id":"local:thread-123"},"title":"文件操作",...}步骤 2:SSE 流结束,前端展示澄清消息
Gateway 发出end事件,前端检测到ask_clarification类型的 ToolMessage,将其渲染为交互式提问 UI。
步骤 3:用户回复,发起新 Run
POST /api/langgraph/threads/thread-123/runs { "input": {"messages": [{"role": "user", "content": "删除 temp.txt"}]} }关键:请求携带同一个 thread_id(thread-123)。
步骤 4:Checkpointer 恢复状态
Worker 在执行agent.astream()时,LangGraph 自动从 Checkpointer 加载该 thread_id 的最新状态,并将新消息追加到messages列表:
# 恢复后的完整状态{"messages":[HumanMessage("帮我删除文件"),AIMessage(tool_calls=[{"name":"ask_clarification",...}]),ToolMessage("❓ 你要删除哪个文件?"),HumanMessage("删除 temp.txt")# ← 新追加],...}步骤 5:Agent 继续执行
图从入口节点开始执行,模型接收到完整的对话历史——包括之前的澄清问题和用户的回答——自然理解上下文并继续完成任务。
3.3 常见误解澄清
误解:中断恢复后 Agent 不是从头执行吗?
图确实从入口节点重新开始。但"从头开始"指的是图的执行流程,不是对话上下文。状态从 Checkpoint 恢复后,模型看到的 messages 列表包含了完整的历史对话,因此模型具备所有必要的上下文来继续工作。
类比:每次向 ChatGPT 发送消息时,整个对话历史都会发送给模型。LangGraph 的 Checkpoint 机制本质上就是这个"对话历史"的持久化存储。
误解:before_agent 的中间件会重新初始化所有资源吗?
是的,新 Run 的before_agent会重新执行。但中间件设计为幂等——例如 SandboxMiddleware 的acquire()对同一 thread_id 会复用已有沙箱而非重新创建。
四、Run 级别的状态管理
4.1 Pre-run Snapshot 与 Rollback
DeerFlow 在每次 Run 开始前保存一份 Checkpoint 快照(pre_run_snapshot)。如果 Run 执行过程中发生异常,可以回滚到 Run 开始前的状态:
# worker.pypre_run_checkpoint_id=Nonepre_run_snapshot=NoneifcheckpointerisnotNone:ckpt_tuple=awaitcheckpointer.aget_tuple(config_for_check)ifckpt_tupleisnotNone:pre_run_checkpoint_id=ckpt_config.get("checkpoint_id")pre_run_snapshot={"checkpoint_ns":...,"checkpoint":copy.deepcopy(ckpt_tuple.checkpoint),"metadata":copy.deepcopy(ckpt_tuple.metadata),"pending_writes":copy.deepcopy(ckpt_tuple.pending_writes),}回滚确保失败的 Run 不会污染 Thread 的持久化状态——用户下次发消息时看到的是上次成功 Run 后的状态。
4.2 Run 状态机
每个 Run 有明确的状态流转:
PENDING → RUNNING → COMPLETED → FAILED → CANCELLED状态由RunManager管理,通过StreamBridge将状态变更实时推送给前端。
五、Docker-compose 部署下的沙箱方案
5.1 问题:容器内如何创建容器?
当 DeerFlow 通过 docker-compose 部署时,Gateway 服务本身运行在容器内。此时AioSandboxProvider需要创建新的 Docker 容器作为代码执行沙箱——这涉及"容器内创建容器"的问题。
业界有两种解决方案:
| 方案 | 原理 | 优劣 |
|---|---|---|
| DinD (Docker-in-Docker) | 容器内运行完整 Docker daemon | 性能差、安全风险高、存储驱动嵌套问题 |
| DooD (Docker-outside-of-Docker) | 容器通过宿主机 Docker socket 调用宿主机 daemon | 轻量、新容器与调用者平级 |
5.2 DeerFlow 的 DooD 方案
DeerFlow 采用 DooD 方案。docker-compose.yaml 中:
gateway:volumes:# 将宿主机 Docker socket 挂载进 Gateway 容器-${DEER_FLOW_DOCKER_SOCKET}:/var/run/docker.sockGateway 容器内的AioSandboxProvider通过挂载的 socket 与宿主机 Docker daemon通信。新创建的沙箱容器与 Gateway 容器是同级兄弟关系,不存在容器嵌套:
宿主机 Docker Daemon ├── deer-flow-nginx (反向代理) ├── deer-flow-frontend (Next.js) ├── deer-flow-gateway (Agent 运行时) │ └── 通过 /var/run/docker.sock 调用宿主机 Docker API ├── sandbox-thread-abc123 ← 沙箱容器(与 gateway 平级) └── sandbox-thread-def456 ← 另一个沙箱容器5.3 Provisioner 模式
对于更大规模的部署(多节点、K8s 环境),DeerFlow 支持 Provisioner 模式:
sandbox:use:deerflow.community.aio_sandbox:AioSandboxProviderprovisioner_url:http://provisioner:8002此模式下,Gateway 不直接操作 Docker/K8s API,而是通过 HTTP 接口请求 Provisioner 服务分配和管理沙箱容器。Provisioner 作为独立服务运行,负责容器的生命周期管理、资源池化、和调度策略。
5.4 孤儿容器回收
AioSandboxProvider实现了启动时孤儿回收机制——扫描上次进程崩溃后遗留的沙箱容器,将其纳入 warm pool 或标记为待清理:
def_reconcile_orphans(self):"""启动时扫描遗留容器,防止资源泄漏。"""running=self._backend.list_running()forinfoinrunning:self._warm_pool[info.sandbox_id]=info logger.info(f"Adopted container{info.sandbox_id}into warm pool")这确保了即使 Gateway 异常退出,下次启动时不会出现"幽灵容器"持续占用资源。
5.5 部署前检查
DeerFlow 提供了部署前检查机制。deploy.sh脚本在启动前验证 Docker socket 可用性:
if[!-S"$DEER_FLOW_DOCKER_SOCKET"];thenecho"⚠ Docker socket not found at$DEER_FLOW_DOCKER_SOCKET"echo" AioSandboxProvider (DooD) will not work."exit1fiscripts/doctor.py提供更全面的环境诊断,检查 Docker/Container CLI 可用性、socket 权限等。
六、中间件执行顺序为何重要
6.1 SafetyFinishReason 与 LoopDetection 的顺序
这两个中间件都在after_modelhook 工作。注册顺序为:
middlewares=[...,LoopDetectionMiddleware,SafetyFinishReasonMiddleware,ClarificationMiddleware]LangChain 的after_model按反序执行。因此 SafetyFinishReason(后注册)先执行:
执行顺序: SafetyFinishReason.after_model → 先检测安全截断,清除 tool_calls LoopDetection.after_model → 后检测循环,看到的是已清理的消息如果顺序颠倒(LoopDetection 先执行),它会看到被截断的 tool_calls 并可能误判为循环模式,发出虚假的循环警告。
6.2 ClarificationMiddleware 在列表最后
位置最后使其wrap_tool_call最先拦截(LangChain 工具调用包装也是反序)。这确保ask_clarification调用被第一时间拦截,不会被其他中间件(如 ToolErrorHandlingMiddleware)错误地当作异常处理。
6.3 SandboxMiddleware 的对称性
before_agent(列表位置 2)获取沙箱,after_agent(反序执行,倒数第 3 个)释放沙箱。这种外层进入/外层退出的模式确保了:如果中间执行过程中发生异常,after_agent仍能正确释放资源。
七、总结
本文讨论的几个核心问题在 Agent 平台工程化中具有普遍性:
| 问题 | DeerFlow 的解决方案 |
|---|---|
| Agent 执行状态跨请求持久化 | LangGraph Checkpointer + SQLite |
| 中断后恢复执行 | Checkpoint 状态恢复 + 消息追加 |
| 容器化部署下的代码隔离 | DooD 方案 + Provisioner 可选扩展 |
| 中间件执行顺序依赖 | 显式列表排序 + 反序规则 |
| Run 失败后的状态回滚 | Pre-run snapshot + 条件回滚 |
理解这些机制后,在使用 LangGraph 或类似框架构建 Agent 系统时,能够更准确地预判状态管理中的潜在问题,并选择合适的持久化和恢复策略。
系列总结
本系列四篇文章从不同维度分析了 DeerFlow Agent 平台的架构设计:
- 全局认知:Agent 的定义与 8 个核心组件的系统性拆解
- 工具系统:三层工具架构与延迟加载的创新设计
- 中间件模式:五个核心中间件的动机、机制与决策
- 生命周期管理:状态持久化、中断恢复与部署实践
DeerFlow 作为当前开源 Agent 框架中架构完整度较高的项目之一,其源码中蕴含的工程决策——从 tool_search 的按需加载、到 Skill Rescue 的摘要保护、到 Safety 中间件的防截断循环——对 Agent 平台的设计与实现具有较高的参考价值。