Skip to content

Agent 中间件链机制

本章目标:

  1. 理解中间件链是什么、为什么 Agent 的横切关注点全靠它而不是塞进 Agent 主逻辑
  2. 拿到一张完整的中间件矩阵速查表(顺序、是否条件挂载、触发开关)
  3. 会写一个自定义 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 ThreadDataMiddlewareSandboxMiddleware(: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
1ThreadDataMiddleware基础始终建 per-thread 目录(workspace/uploads/outputs)tool_error_handling_middleware.py:80
2UploadsMiddleware基础始终(insert@1)新上传文件注入对话tool_error_handling_middleware.py:87
3SandboxMiddleware基础始终取 sandbox,写 sandbox_id 入 statetool_error_handling_middleware.py:81
4DanglingToolCallMiddleware基础始终给无响应的 tool_call 补占位 ToolMessagetool_error_handling_middleware.py:92
5LLMErrorHandlingMiddleware基础始终provider 错误规范化为可恢复错误tool_error_handling_middleware.py:94
6GuardrailMiddleware基础条件guardrails.enabled工具调用前授权检查tool_error_handling_middleware.py:119
7SandboxAuditMiddleware基础始终shell/文件操作安全审计tool_error_handling_middleware.py:123
8ToolErrorHandlingMiddleware基础始终工具异常 → 错误 ToolMessage(续行)tool_error_handling_middleware.py:124
9SummarizationMiddleware功能条件summarization.enabledtoken 压力时压缩历史agent.py:230-233
10TodoListMiddleware功能条件is_plan_modewrite_todos 任务跟踪agent.py:235-238
11TokenUsageMiddleware功能条件token_usage.enabled记 token 用量agent.py:240-242
12SkillUsageMiddleware功能始终记技能调用次数agent.py:245
13TitleMiddleware功能始终首轮后自动生成标题agent.py:248
14MemoryMiddleware功能条件agent_name=None 或 user-/project-/usermem- 前缀异步记忆更新队列agent.py:258-259
15ViewImageMiddleware功能条件model supports_visionLLM 前注入 base64 图agent.py:264-266
16DeferredToolFilterMiddleware功能条件tool_search.enabled隐藏延迟工具 schemaagent.py:269-272
17SubagentLimitMiddleware功能条件subagent_enabled截断超限并发 task 调用agent.py:275-277
18LoopDetectionMiddleware功能始终检测打断 tool-call 死循环agent.py:280
19(custom_middlewares)功能条件传入参数自定义注入点agent.py:283-284
20ClarificationMiddleware功能始终ask_clarificationCommand(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 永远不会执行;MemoryMiddlewareTitleMiddleware 之后(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_agentcustom_middlewares 路径传入,位置固定在 LoopDetection 之后、Clarification 之前(agent.py:280-288),不能插到基础段(那由 build_lead_runtime_middlewares 硬编码)。
  • 想在工具调用前做授权,优先用官方 GuardrailProvider(guardrails.enabled)而非自写 middleware——那是专为此设计的扩展点。
  • hook 返回 dict 会按 ThreadState reducer 合并进状态(注意 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 — 必须最后的中断 middleware
  • deer-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 工厂
PageRelationship
Lead Agent 设计与图构建本章链由该章 make_lead_agent 装配
沙箱架构与文件工具本章 Sandbox/SandboxAudit middleware 操作该章的沙箱
长期记忆机制本章 MemoryMiddleware 把对话喂给该章记忆队列
子代理委派与确定性 Pipeline本章 SubagentLimitMiddleware 限制该章子代理并发
SKILL 技能系统本章 SkillUsageMiddleware 记录该章技能调用

公司内部参考 · 由 claude-wiki-gen 基于源码自动生成的二次分析