主题
Agent 中间件链机制
本章目标:
- 理解中间件链是什么、为什么 Agent 的横切关注点全靠它而不是塞进 Agent 主逻辑
- 拿到一张完整的中间件矩阵速查表(顺序、是否条件挂载、触发开关)
- 会写一个自定义 middleware 并知道它该插在链的哪个位置
TL;DR
Lead Agent 的所有横切能力(线程目录、上传注入、沙箱、错误恢复、guardrail、审计、摘要、待办、记忆、视觉、循环检测、澄清中断)都做成 AgentMiddleware,在 build_lead_runtime_middlewares() + _build_middlewares() 里按严格顺序装配进链。链分两段:固定基础段(ThreadData→Uploads→Sandbox→...→ToolError)+ 条件功能段(按 config.configurable 开关追加)。ClarificationMiddleware 永远最后(它要 Command(goto=END) 中断)。
Overview(为什么用中间件链而不是把逻辑写进 Agent)
一个 Agent 跑一轮要做的事远不止"调模型":得先建好线程工作目录、拿到 sandbox、把新上传文件注入对话、处理上一轮没回应的 tool_call、规范化 provider 错误、做工具调用授权、审计 shell 操作、token 超了要摘要、记录待办、抽取记忆、注入图片、检测死循环、拦截澄清请求……
如果这些都写进 Agent 主循环,主逻辑会被淹没,而且每个能力都"可选/有顺序依赖"——摘要必须在加消息后、澄清必须最后、记忆必须在标题后。deer-flow 的解法是 middleware chain:每个横切能力是一个 AgentMiddleware,实现 before_agent/after_model/before_model 等 hook,框架按列表顺序在 Agent 执行的不同阶段调它们。这让"加一个能力"=写一个 middleware 插进链,"关一个能力"=条件不 append,Agent 主逻辑保持干净。
Architecture:两段装配
链的装配分两个函数,合起来决定最终顺序:
第一段(固定基础)——build_lead_runtime_middlewares()(agents/middlewares/tool_error_handling_middleware.py:78-128): 先 append ThreadDataMiddleware、SandboxMiddleware(:80-81),再把 UploadsMiddleware insert 到 index 1(:87,使顺序变成 ThreadData→Uploads→Sandbox),然后 append DanglingToolCallMiddleware(:92)、LLMErrorHandlingMiddleware(:94)、GuardrailMiddleware(:119,仅 guardrails 启用)、SandboxAuditMiddleware(:123)、ToolErrorHandlingMiddleware(:124)。
第二段(条件功能)——_build_middlewares()(agents/lead_agent/agent.py:216-289)在第一段基础上按开关 append,最后强制 ClarificationMiddleware()(agent.py:289)。
中间件矩阵速查表
| # | 中间件 | 段 | 条件挂载 | 触发开关 | 职责 | Source |
|---|---|---|---|---|---|---|
| 1 | ThreadDataMiddleware | 基础 | 始终 | — | 建 per-thread 目录(workspace/uploads/outputs) | tool_error_handling_middleware.py:80 |
| 2 | UploadsMiddleware | 基础 | 始终(insert@1) | — | 新上传文件注入对话 | tool_error_handling_middleware.py:87 |
| 3 | SandboxMiddleware | 基础 | 始终 | — | 取 sandbox,写 sandbox_id 入 state | tool_error_handling_middleware.py:81 |
| 4 | DanglingToolCallMiddleware | 基础 | 始终 | — | 给无响应的 tool_call 补占位 ToolMessage | tool_error_handling_middleware.py:92 |
| 5 | LLMErrorHandlingMiddleware | 基础 | 始终 | — | provider 错误规范化为可恢复错误 | tool_error_handling_middleware.py:94 |
| 6 | GuardrailMiddleware | 基础 | 条件 | guardrails.enabled | 工具调用前授权检查 | tool_error_handling_middleware.py:119 |
| 7 | SandboxAuditMiddleware | 基础 | 始终 | — | shell/文件操作安全审计 | tool_error_handling_middleware.py:123 |
| 8 | ToolErrorHandlingMiddleware | 基础 | 始终 | — | 工具异常 → 错误 ToolMessage(续行) | tool_error_handling_middleware.py:124 |
| 9 | SummarizationMiddleware | 功能 | 条件 | summarization.enabled | token 压力时压缩历史 | agent.py:230-233 |
| 10 | TodoListMiddleware | 功能 | 条件 | is_plan_mode | write_todos 任务跟踪 | agent.py:235-238 |
| 11 | TokenUsageMiddleware | 功能 | 条件 | token_usage.enabled | 记 token 用量 | agent.py:240-242 |
| 12 | SkillUsageMiddleware | 功能 | 始终 | — | 记技能调用次数 | agent.py:245 |
| 13 | TitleMiddleware | 功能 | 始终 | — | 首轮后自动生成标题 | agent.py:248 |
| 14 | MemoryMiddleware | 功能 | 条件 | agent_name=None 或 user-/project-/usermem- 前缀 | 异步记忆更新队列 | agent.py:258-259 |
| 15 | ViewImageMiddleware | 功能 | 条件 | model supports_vision | LLM 前注入 base64 图 | agent.py:264-266 |
| 16 | DeferredToolFilterMiddleware | 功能 | 条件 | tool_search.enabled | 隐藏延迟工具 schema | agent.py:269-272 |
| 17 | SubagentLimitMiddleware | 功能 | 条件 | subagent_enabled | 截断超限并发 task 调用 | agent.py:275-277 |
| 18 | LoopDetectionMiddleware | 功能 | 始终 | — | 检测打断 tool-call 死循环 | agent.py:280 |
| 19 | (custom_middlewares) | 功能 | 条件 | 传入参数 | 自定义注入点 | agent.py:283-284 |
| 20 | ClarificationMiddleware | 功能 | 始终 | — | 拦 ask_clarification → Command(goto=END) 中断 | agent.py:288 |
注:这是从源码核实的实际顺序(
SkillUsageMiddleware是项目文档未列的较新中间件)。MemoryMiddleware的条件挂载是 a-cdm 重要定制:默认 chat(agent_name=None)挂、user-/project-/usermem-前缀挂,其他原型 agent(consultant-agent/ba-expert 等)不挂,防跨用户跨项目套话泄漏(对齐 OpenSpec "专家无记忆"方案,agent.py:250-258注释)。
Data Flow:hook 调用时机
顺序蕴含的关键不变量:ClarificationMiddleware 必须最后——它通过 Command(goto=END) 中断整个 run,排在它后面的 middleware 永远不会执行;MemoryMiddleware 在 TitleMiddleware 之后(agent.py:248 后)——保证记忆抽取时标题已生成;ToolErrorHandlingMiddleware 把工具异常转成 ToolMessage 让 run 继续而非崩(tool_error_handling_middleware.py:19)。
一次 run 在中间件链上的生命周期状态:
扩展指南:写一个自定义 middleware
_build_middlewares 接受 custom_middlewares 参数,在 ClarificationMiddleware 之前注入(agent.py:283-284):
python
# 最小自定义 middleware 模板
from langchain.agents.middleware import AgentMiddleware
from deerflow.agents.thread_state import ThreadState
class MyAuditMiddleware(AgentMiddleware[ThreadState]):
async def before_model(self, state, runtime):
# 在每次调模型前做你的事(如打点/校验)
return None # 返回 None = 不改 state;返回 dict = 状态更新约束(从代码读出):
- 泛型参数用
ThreadState(本项目状态 schema,见 Lead Agent 章)。 - 自定义 middleware 通过
make_lead_agent的custom_middlewares路径传入,位置固定在 LoopDetection 之后、Clarification 之前(agent.py:280-288),不能插到基础段(那由build_lead_runtime_middlewares硬编码)。 - 想在工具调用前做授权,优先用官方
GuardrailProvider(guardrails.enabled)而非自写 middleware——那是专为此设计的扩展点。 - hook 返回
dict会按ThreadStatereducer 合并进状态(注意 artifacts/viewed_images 是合并语义)。
Common Pitfalls / 实战 Tips
- 顺序即正确性:把 middleware 插错位置(如 Clarification 前后)会破坏中断/摘要语义。改链要理解每个 hook 在哪个阶段跑。
- MemoryMiddleware 的"专家无记忆"是有意的:别给 consultant-agent/ba-expert 加记忆"修复",那会导致跨用户套话泄漏(
agent.py:250-258)。 - 基础段不可从 custom 注入:
build_lead_runtime_middlewares是硬编码顺序,自定义只能进功能段尾部。 - 条件中间件靠 configurable:plan mode/subagent/vision 都靠
config.configurable,不是全局配置——前端不传开关就不挂。
References
deer-flow/backend/packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py:78-128— 基础段装配(本章主源)deer-flow/backend/packages/harness/deerflow/agents/lead_agent/agent.py:216-289— 功能段条件装配deer-flow/backend/packages/harness/deerflow/agents/middlewares/— 18 个 middleware 实现目录deer-flow/backend/packages/harness/deerflow/agents/middlewares/clarification_middleware.py— 必须最后的中断 middlewaredeer-flow/backend/packages/harness/deerflow/agents/middlewares/memory_middleware.py— 条件挂载的记忆队列deer-flow/backend/packages/harness/deerflow/agents/lead_agent/agent.py:45-91— 摘要/todo middleware 工厂
Related Pages
| Page | Relationship |
|---|---|
| Lead Agent 设计与图构建 | 本章链由该章 make_lead_agent 装配 |
| 沙箱架构与文件工具 | 本章 Sandbox/SandboxAudit middleware 操作该章的沙箱 |
| 长期记忆机制 | 本章 MemoryMiddleware 把对话喂给该章记忆队列 |
| 子代理委派与确定性 Pipeline | 本章 SubagentLimitMiddleware 限制该章子代理并发 |
| SKILL 技能系统 | 本章 SkillUsageMiddleware 记录该章技能调用 |