Skip to content

中间件链机制

本章目标:

  1. 讲清 DeerFlow 为什么用 LangGraph AgentMiddleware 链来组织横切逻辑,而不是把这些逻辑塞进 LeadAgent 本体。
  2. 逐个拆解约 18 个中间件的职责、实现的 hook、启用条件,以及它们在 build_lead_runtime_middlewares_build_middlewares 两处装配点的严格顺序与原因
  3. 给出自定义 AgentMiddleware 的最小模板、hook 签名约束、装配位置选择,以及顺序敏感性等真实存在的踩坑点。

TL;DR

DeerFlow 的 LeadAgent 几乎不含横切逻辑:目录初始化、沙箱获取、上传注入、错误恢复、摘要压缩、循环检测、标题/记忆/Token 统计、澄清中断等全部由一条按 append 顺序构造的 AgentMiddleware 链承担。链分两段装配:共享基础段由 build_lead_runtime_middlewares(实为 _build_runtime_middlewares)产出,Lead 专属段由 _build_middlewares 追加,最后 create_agent(middleware=...) 把它编织进 LangGraph 图。每个中间件按需实现 before_agent / before_model / after_model / after_agent / wrap_model_call / wrap_tool_call 六类 hook,顺序决定 hook 触发的洋葱嵌套层级,因此顺序是语义的一部分而非风格。ClarificationMiddleware 必须最后、ThreadDataMiddleware 必须最先,这类约束在代码注释里写死并被本章源码核实。

Overview

为什么用中间件链,而不是把逻辑写进 Agent

一个生产级 super agent 在「调用模型 → 执行工具 → 再调用模型」的循环外,还需要做大量横切的事情:为线程创建隔离目录、获取沙箱、把上传文件注入对话、把工具异常转成可恢复的 ToolMessage、对过长上下文做摘要、检测重复调用循环、统计 Token、生成标题、异步入队记忆、拦截澄清请求并中断。如果把这些都写进 LeadAgent 工厂,会产生三个问题:(1) Agent 工厂变成上帝函数;(2) 条件开关(plan 模式、vision、tool_search、subagent、guardrails)散落在主流程里难以推理;(3) 这些逻辑无法在 LeadAgent 与 Subagent 之间复用。

DeerFlow 的选择是:LeadAgent 工厂 _make_lead_agent 只负责解析模型名、装配工具、生成系统提示,然后把一条中间件链交给 create_agentbackend/packages/harness/deerflow/agents/lead_agent/agent.py:434-446。所有横切逻辑被拆成独立的 AgentMiddleware 子类,每个只实现自己需要的 hook,启用与否由 AppConfig / RunnableConfig 决定。共享段 _build_runtime_middlewares 同时被 Lead(build_lead_runtime_middlewares)和 Subagent(build_subagent_runtime_middlewares)复用backend/packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py:129-167,Subagent 执行器直接调用同一构造器backend/packages/harness/deerflow/subagents/executor.py:284-297

中间件链是「洋葱模型」:wrap_model_call / wrap_tool_call 类 hook 形成嵌套包裹,链中靠前的中间件包在外层,靠后的包在内层。这意味着顺序即语义——例如 ToolErrorHandlingMiddleware 必须排在 ClarificationMiddleware 之前,否则澄清的 Command(goto=END) 控制流会被错误的异常处理吞掉。

Architecture

两处装配点

中间件链由两个函数协作构造,顺序通过 Python listappend / insert 显式控制:

  1. 共享基础段:_build_runtime_middlewares(对外暴露为 build_lead_runtime_middlewares)backend/packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py:70-137。它产出 ThreadDataMiddlewareSandboxMiddleware,按需 insert UploadsMiddlewareappend DanglingToolCallMiddlewareLLMErrorHandlingMiddleware、可选 GuardrailMiddleware,再固定 append SandboxAuditMiddlewareToolErrorHandlingMiddleware

  2. Lead 专属段:_build_middlewares 先调用上面的共享段,再依次 append DynamicContextMiddleware、可选 DeerFlowSummarizationMiddleware、可选 TodoMiddleware、可选 TokenUsageMiddlewareTitleMiddlewareMemoryMiddleware、可选 ViewImageMiddleware、可选 DeferredToolFilterMiddleware、可选 SubagentLimitMiddleware、可选 LoopDetectionMiddleware、外部注入的 custom_middlewares,最后无条件 append ClarificationMiddlewarebackend/packages/harness/deerflow/agents/lead_agent/agent.py:240-318

注意一处易错点:_build_middlewares 顶部有一段长注释列举顺序约定backend/packages/harness/deerflow/agents/lead_agent/agent.py:230-239,但实际顺序以两个函数的 append/insert 调用为准——注释里的相对顺序与运行时一致,本章已逐行核实。

DeerFlowClient 嵌入式路径复用同一个 _build_middlewares,额外支持 custom_middlewares 注入backend/packages/harness/deerflow/client.py:233

顺序总览(以默认全开为例)

#中间件装配函数Source
1ThreadDataMiddleware_build_runtime_middlewaresagent...:82-85
2UploadsMiddlewareinsert(1, ...)(仅 Lead)...:87-90
3SandboxMiddleware_build_runtime_middlewares...:82-85
4DanglingToolCallMiddlewareappend(条件)...:92-95
5LLMErrorHandlingMiddlewareappend...:97
6GuardrailMiddlewareappend(条件)...:99-120
7SandboxAuditMiddlewareappend...:122-124
7.5ToolErrorHandlingMiddlewareappend...:125
8DynamicContextMiddleware_build_middlewares appendagent.py:263-265
9DeerFlowSummarizationMiddlewareappend(条件)agent.py:268-270
10TodoMiddlewareappend(条件 plan)agent.py:273-277
11TokenUsageMiddlewareappend(条件)agent.py:280-281
12TitleMiddlewareappendagent.py:284
13MemoryMiddlewareappendagent.py:287
14ViewImageMiddlewareappend(条件 vision)agent.py:291-293
15DeferredToolFilterMiddlewareappend(条件)agent.py:296-299
16SubagentLimitMiddlewareappend(条件)agent.py:302-305
17LoopDetectionMiddlewareappend(条件,默认开)agent.py:308-310
custom_middlewaresextend(条件)agent.py:313-314
18ClarificationMiddlewareappend(永远最后)agent.py:316-317

UploadsMiddlewareinsert(1, ...) 插在 ThreadDataMiddleware 之后、SandboxMiddleware 之前;ToolErrorHandlingMiddleware 是共享段最后一个 append,故编号写作 7.5 表示它紧随 SandboxAudit。

Components / Subsystems

按装配顺序逐个说明。每条给出:一句话职责 / 关键类位置 / 实现的 hook / 启用条件。

1. ThreadDataMiddleware

  • 职责:为每个线程计算/创建隔离目录(workspace/uploads/outputs),并把 run_idtimestamp 补进最后一条 HumanMessageadditional_kwargs
  • :thread_data_middleware.py:24-118
  • Hook:before_agent(lazy_init=True 时只算路径不建目录)thread_data_middleware.py:81-118
  • 启用:始终。必须最先,因为后续中间件依赖 thread_id/thread_data

2. UploadsMiddleware

3. SandboxMiddleware

  • 职责:为线程获取沙箱并把 sandbox_id 写入 state;沙箱跨多轮复用,不在每轮释放。
  • :sandbox/middleware.py:21-84
  • Hook:before_agent(lazy_init=True 时延迟到首个工具调用)、after_agent(释放沙箱)sandbox/middleware.py:51-83
  • 启用:始终。

4. DanglingToolCallMiddleware

  • 职责:历史里若 AIMessagetool_calls(含 invalid_tool_calls、原始 additional_kwargs.tool_calls)缺失对应 ToolMessage(如用户中断),在该 AIMessage 后插合成占位 ToolMessage,保证发给模型的对话格式合法。
  • :dangling_tool_call_middleware.py:29-185
  • Hook:wrap_model_call / awrap_model_call(刻意不用 before_model,以保证插入位置正确而非追加到末尾)dangling_tool_call_middleware.py:165-185
  • 启用:include_dangling_tool_call_patch=True(Lead 与 Subagent 均开)。

5. LLMErrorHandlingMiddleware

  • 职责:对模型调用做重试/退避(可解析 Retry-After)、错误分类(quota/auth/transient/busy)、熔断器(closed/open/half_open),失败时返回友好 AIMessage 而非抛出。
  • :llm_error_handling_middleware.py:66-298
  • Hook:wrap_model_call / awrap_model_call;显式 raise GraphBubbleUp 以保留中断/暂停控制流llm_error_handling_middleware.py:208-252
  • 启用:始终(熔断阈值来自 app_config.circuit_breaker)。

6. GuardrailMiddleware

7. SandboxAuditMiddleware

  • 职责:对 bash 工具做安全审计——高危(rm -rf /curl|sh、fork bomb 等)直接拦截返回错误 ToolMessage,中危(pip installchmod 777)放行但追加警告,所有 bash 调用写结构化审计日志。
  • :sandbox_audit_middleware.py:197-364
  • Hook:wrap_tool_call / awrap_tool_call(非 bash 直接透传)sandbox_audit_middleware.py:329-363
  • 启用:始终(_build_runtime_middlewares 固定 append)。

7.5 ToolErrorHandlingMiddleware

8. DynamicContextMiddleware

  • 职责:把 per-user 记忆 + 当前日期作为 <system-reminder> 注入首条 HumanMessage(冻结快照,保系统提示静态以利前缀缓存);跨午夜则在当前轮前补一条日期更新提醒。
  • :dynamic_context_middleware.py:81-204
  • Hook:before_agent / abefore_agentdynamic_context_middleware.py:198-204
  • 启用:始终(记忆注入受 memory.injection_enabled 门控,日期始终注入)。

9. DeerFlowSummarizationMiddleware

  • 职责:接近 Token 上限时压缩历史;扩展了 LangChain 内置 SummarizationMiddleware,带 before_summarization hook 派发(如 memory_flush_hook)、技能 bundle 抢救(保留近期加载的 skill 文件)、保留隐藏的 dynamic-context 提醒。
  • :summarization_middleware.py:98-375
  • Hook:before_model / abefore_model(→ _maybe_summarize)summarization_middleware.py:120-176
  • 启用:app_config.summarization.enabled,由 _create_summarization_middleware 工厂构造agent.py:53-112

10. TodoMiddleware

  • 职责:扩展 LangChain TodoListMiddleware,提供 write_todos;检测摘要后 todo 上下文丢失并注入提醒,阻止有未完成 todo 时过早退出(after_model 注入完成提醒并 jump_to: model,有 _MAX_COMPLETION_REMINDERS=2 上限)。
  • :todo_middleware.py:107-359
  • Hook:before_model/abefore_modelbefore_agent/abefore_agentafter_model/aafter_model(@hook_config(can_jump_to=["model"]))、wrap_model_call/awrap_model_callafter_agent/aafter_agenttodo_middleware.py:116-359
  • 启用:config.configurable.is_plan_mode=True(经 _create_todo_list_middleware)agent.py:115-227

11. TokenUsageMiddleware

  • 职责:记录模型响应 Token 用量日志;把 subagent token(按 tool_call_id 缓存)合并回派发的 AIMessage;在 additional_kwargs 写入 step 归因元数据供前端展示。
  • :token_usage_middleware.py:267-358
  • Hook:after_model / aafter_modeltoken_usage_middleware.py:352-358
  • 启用:app_config.token_usage.enabled

12. TitleMiddleware

  • 职责:首轮完整交互后自动生成线程标题(异步路径调小模型,失败回退到截断的用户消息);归一化结构化消息内容、剥离 <think> 标签。
  • :title_middleware.py:29-184
  • Hook:after_model(同步=本地回退)、aafter_model(异步=LLM 生成)title_middleware.py:178-184
  • 启用:始终(config.title.enabled_should_generate_title 内部判定)。

13. MemoryMiddleware

  • 职责:agent 执行完把对话(过滤为用户输入 + 最终 AI 回复)入队做异步记忆更新;入队时捕获 user_id(因 threading.Timer 不传 ContextVar)。
  • :memory_middleware.py:28-110
  • Hook:after_agentmemory_middleware.py:52-110
  • 启用:始终(memory.enabledafter_agent 内部判定),装配在 TitleMiddleware 之后。

14. ViewImageMiddleware

  • 职责:上一轮 view_image 工具全部完成后,模型调用前把已查看图片的 base64 数据作为 HumanMessage(混合文本+image_url 块)注入,使模型能"看"图。
  • :view_image_middleware.py:19-223
  • Hook:before_model / abefore_modelview_image_middleware.py:190-223
  • 启用:model_config.supports_vision(Lead 用运行时解析的 model_name;Subagent 在 build_subagent_runtime_middlewares 内判定)agent.py:289-293

15. DeferredToolFilterMiddleware

  • 职责:tool_search 开启时,从 request.tools 移除已延迟(deferred)的工具 schema,使模型只 bind 活跃工具;若模型仍调用未提升的延迟工具,返回提示先调 tool_search 的错误 ToolMessage
  • :deferred_tool_filter_middleware.py:26-107
  • Hook:wrap_model_call/awrap_model_call(过滤)、wrap_tool_call/awrap_tool_call(拦截未提升调用)deferred_tool_filter_middleware.py:71-107
  • 启用:app_config.tool_search.enabled

16. SubagentLimitMiddleware

17. LoopDetectionMiddleware

18. ClarificationMiddleware

  • 职责:拦截 ask_clarification 工具调用,格式化问题(含 options/icon),返回 Command(update=..., goto=END) 中断执行把问题抛给用户;用确定性 message id 让重试的澄清替换而非追加。
  • :clarification_middleware.py:25-201
  • Hook:wrap_tool_call / awrap_tool_call(非 ask_clarification 直接透传)clarification_middleware.py:158-201
  • 启用:始终,且永远是最后一个 appendagent.py:316-317

Data Flow

下面是一次完整 model 调用前后,中间件按链顺序触发的时序(只画装配全开时的关键 hook;wrap_* 是嵌套包裹,靠前的在外层):

LangGraph agent 内部的节点级状态机(before_agent/after_agent 包住整轮,before_model/after_model 包住每次模型调用):

速查表

全部中间件 × hook 矩阵。 表示该类实现了对应 hook(含其 a* 异步对偶)。wrap_modelwrap_model_call,wrap_toolwrap_tool_call

中间件before_agentbefore_modelafter_modelafter_agentwrap_modelwrap_tool启用条件来源(装配函数)Source
ThreadDataMiddleware始终共享段L24-118
UploadsMiddlewareLead 专属共享段 insertL66-295
SandboxMiddleware始终共享段L21-84
DanglingToolCallMiddlewarepatch 标志共享段(条件)L29-185
LLMErrorHandlingMiddleware始终共享段L66-298
GuardrailMiddlewareguardrails.enabled共享段(条件)L20-98
SandboxAuditMiddleware始终共享段L197-364
ToolErrorHandlingMiddleware始终共享段L21-67
DynamicContextMiddleware始终Lead 段L81-204
DeerFlowSummarizationMiddlewaresummarization.enabledLead 段(条件)L98-375
TodoMiddlewareis_plan_modeLead 段(条件)L107-359
TokenUsageMiddlewaretoken_usage.enabledLead 段(条件)L267-358
TitleMiddleware始终(内部判 title.enabled)Lead 段L29-184
MemoryMiddleware始终(内部判 memory.enabled)Lead 段L28-110
ViewImageMiddlewaremodel.supports_visionLead 段(条件)L19-223
DeferredToolFilterMiddlewaretool_search.enabledLead 段(条件)L26-107
SubagentLimitMiddlewaresubagent_enabledLead 段(条件)L25-76
LoopDetectionMiddlewareloop_detection.enabled(默认开)Lead 段(条件)L144-440
ClarificationMiddleware始终(永远最后)Lead 段L25-201

ViewImageMiddleware 也出现在 Subagent 共享段(当 subagent 模型支持 vision 时)tool_error_handling_middleware.py:161-167

扩展指南

自定义 AgentMiddleware 最小模板

所有 hook 签名从源码读出:before_agent/after_agent/before_model/after_model(self, state, runtime) -> dict | None;wrap_model_call(self, request: ModelRequest, handler) -> ModelCallResult;wrap_tool_call(self, request: ToolCallRequest, handler) -> ToolMessage | Command。异步对偶在方法名前加 a(abefore_modelawrap_tool_call 等)。返回 dict 即对 state 的增量更新(走 LangGraph reducer);返回 None 表示不改 state。

python
from typing import override
from collections.abc import Callable
from langchain.agents import AgentState
from langchain.agents.middleware import AgentMiddleware
from langchain.agents.middleware.types import ModelCallResult, ModelRequest, ModelResponse
from langchain_core.messages import ToolMessage
from langgraph.errors import GraphBubbleUp
from langgraph.prebuilt.tool_node import ToolCallRequest
from langgraph.runtime import Runtime


class MyMiddleware(AgentMiddleware[AgentState]):
    """一句话职责。"""

    @override
    def before_model(self, state: AgentState, runtime: Runtime) -> dict | None:
        # 改写发给模型前的 state,如注入提醒消息
        return None  # 或 {"messages": [...]}

    @override
    def wrap_tool_call(
        self,
        request: ToolCallRequest,
        handler: Callable[[ToolCallRequest], ToolMessage | Command],
    ) -> ToolMessage | Command:
        try:
            return handler(request)
        except GraphBubbleUp:
            raise  # 必须:保留 interrupt/pause/resume 控制流
        except Exception as exc:
            return ToolMessage(content=f"Error: {exc}", tool_call_id=request.tool_call.get("id", ""), status="error")

约束清单(均从代码读出)

  1. 必须继承 AgentMiddleware[StateSchema];若中间件需要新 state 键,定义 class XxxState(AgentState) 并设类属性 state_schema = XxxState(见 ThreadDataMiddleware/UploadsMiddleware/TitleMiddleware 均用 NotRequired 字段扩展)。
  2. 只实现需要的 hook,空实现会调 super()(如 SandboxMiddleware.before_agent 在 lazy 时 return super().before_agent(...))。
  3. 保留 GraphBubbleUp:wrap_* hook 里所有 except 必须先 except GraphBubbleUp: raise,否则会吞掉 LangGraph 的中断/暂停/恢复信号——ToolError/Guardrail/LLMError 三处都这么做。
  4. 修改 AIMessage 的 tool_calls 时用 clone_ai_message_with_tool_callstool_call_metadata.py:18-50,它会同步清理 additional_kwargs.tool_callsfunction_callresponse_metadata.finish_reason,否则严格的 OpenAI 兼容校验会因结构化/原始 tool_calls 不一致而 400。
  5. after_model 想跳回模型需加 @hook_config(can_jump_to=["model"]) 并返回 {"jump_to": "model"}(见 TodoMiddleware.after_model)。
  6. 装配位置怎么加:把实例 append_build_middlewares——若是工具/模型横切且 Lead/Subagent 共用,放进 _build_runtime_middlewares;若 Lead 专属,放进 _build_middlewares;务必排在 ClarificationMiddleware(永远最后)之前。DeerFlowClient 路径还可通过 custom_middlewares 参数注入,会被 extend 到 Clarification 之前agent.py:312-317

Common Pitfalls / Tips

  • 顺序敏感性是硬约束:ToolErrorHandlingMiddleware 必须在 ClarificationMiddleware 之前。Clarification 通过 wrap_tool_call 返回 Command(goto=END) 中断;若 ToolError 包在它内层(更靠后),澄清的控制流不会被影响,但若顺序反了,异常处理可能把澄清逻辑吞掉——这正是注释明确写「ToolErrorHandlingMiddleware should be before ClarificationMiddleware」的原因agent.py:238-239
  • ThreadData 必须最先:UploadsMiddlewareSandboxMiddleware 等都依赖 thread_id/thread_data,所以它是共享段第一个,UploadsMiddlewareinsert(1, ...) 紧随其后tool_error_handling_middleware.py:82-90
  • DanglingToolCall 刻意不用 before_model:文件头注释明确说,用 before_model + add_messages reducer 会把补丁追加到消息末尾而非紧跟悬空 AIMessage,所以必须用 wrap_model_call 在 request 层重排dangling_tool_call_middleware.py:11-13
  • LoopDetection 警告注入是已知临时方案:_apply 把警告追加进 AIMessage content 而非插独立 HumanMessage,因为在 after_model 时 tools 节点还没跑,插非 tool 消息会破坏 OpenAI/Moonshot 严格的 tool_call 配对校验;副作用是警告文本会泄漏给 Memory/Title/telemetry,正解见 RFC #2517loop_detection_middleware.py:391-415
  • Summarization 必须保留 dynamic-context 提醒:否则 DynamicContextMiddleware 会把摘要后的 summary HumanMessage 误当首条用户消息,在错误位置重注入提醒——_preserve_dynamic_context_reminders 专门防这个summarization_middleware.py:185-201
  • _build_middlewares 顶部注释的顺序仅供参考:运行时顺序以 append/insert 实际调用为准,改顺序要改的是调用序列,不是注释。
  • 共享段被 Subagent 复用:在共享段加中间件会同时影响 Subagent;Subagent 不含 UploadsMiddleware 且 Lead 专属段全部不参与 Subagenttool_error_handling_middleware.py:139-167

References

  1. backend/packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py_build_runtime_middlewares / build_lead_runtime_middlewares / build_subagent_runtime_middlewaresToolErrorHandlingMiddleware
  2. backend/packages/harness/deerflow/agents/lead_agent/agent.py_build_middlewares Lead 专属段装配
  3. backend/packages/harness/deerflow/agents/middlewares/dangling_tool_call_middleware.py — 悬空 tool_call 修补
  4. backend/packages/harness/deerflow/agents/middlewares/loop_detection_middleware.py — 双层循环检测与硬停
  5. backend/packages/harness/deerflow/agents/middlewares/clarification_middleware.py — 澄清拦截与 Command(goto=END)
  6. backend/packages/harness/deerflow/agents/middlewares/summarization_middleware.py — 摘要扩展与技能抢救
  7. backend/packages/harness/deerflow/agents/middlewares/tool_call_metadata.pyclone_ai_message_with_tool_calls
  8. backend/packages/harness/deerflow/subagents/executor.py — Subagent 复用共享中间件段
页面关系
./10-LeadAgent与Agent工厂.md上游:_make_lead_agent_build_middlewares 并把链交给 create_agent,本章是其横切层
./11-ThreadState与状态管理.md中间件 hook 的 stateThreadState,各 XxxState(AgentState) 扩展的键由其 reducer 合并
./23-长期记忆系统.mdMemoryMiddlewareafter_agent 入队、DynamicContextMiddleware 注入记忆,本章给出装配位置
./33-上下文工程-摘要与循环检测.mdDeerFlowSummarizationMiddlewareLoopDetectionMiddleware 的算法细节在该章展开
./35-可观测性-Tracing与Token用量.mdTokenUsageMiddleware 的 Token 统计与 step 归因如何被 tracing 消费
./24-Guardrails安全守卫.mdGuardrailMiddleware 是本链中的一环,其 provider 协议与授权算法在 24 章展开

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