中间件
中间件包裹 Lead Agent 中的每次 LLM 调用。它们是添加跨领域行为(如记忆、摘要压缩、澄清和 token 追踪)的主要扩展点。
每次 Lead Agent 调用 LLM 时,都会先后执行一条中间件链。中间件可以读取和修改 Agent 的状态、向系统提示注入内容、拦截工具调用,并对模型输出做出反应。
这种设计使 Agent 核心保持简单稳定,同时允许丰富的可组合行为分层叠加。
每个子 Agent 运行各自的 Agent 循环,并拥有自己的中间件链。下方的循环检测、token 预算和摘要压缩防护已镜像到子 Agent 链(#3875);记忆、标题生成和澄清等其他 Lead Agent 专属中间件不会在子 Agent 链中运行。参见子 Agent → 失控行为防护。
链的工作方式
中间件链在每次 Agent 调用时根据当前配置和请求参数构建一次。中间件按定义的顺序运行:
- 运行时中间件(
InputSanitizationMiddleware输入清理 →ToolOutputBudgetMiddleware输出预算截断 →ToolResultSanitizationMiddleware工具结果清理,随后是线程数据、上传、沙箱、悬空工具调用修补和 LLM 错误处理;工具回执(如启用)、授权/guardrail(如启用)、沙箱审计、读前写后(如启用)、工具进度(如启用)和工具错误处理随后执行) DynamicContextMiddleware— 当前日期和可选记忆上下文SkillActivationMiddleware— slash skill 激活SkillToolPolicyMiddleware— 将技能工具限制为当前激活的技能DurableContextMiddleware— 捕获持久摘要、委托和技能引用状态SummarizationMiddleware— 上下文压缩(如果启用)TodoMiddleware— 任务列表管理(仅计划模式)TokenUsageMiddleware— token 追踪(如果启用)TitleMiddleware— 自动生成线程标题MemoryMiddleware— 跨会话记忆注入和队列ViewImageMiddleware— 图像细节注入(如果模型支持视觉)DeferredToolFilterMiddleware— 隐藏延迟工具 schema(如果启用工具搜索)SystemMessageCoalescingMiddleware— 合并面向 provider 的 system messagesSubagentLimitMiddleware— 限制并行子 Agent 调用(如果启用子 Agent)LoopDetectionMiddleware— 打破重复工具调用循环TokenBudgetMiddleware— 每次运行的 token 预算限制(如果启用)- 自定义中间件(如有)
- 配置的扩展中间件(如有)
TerminalResponseMiddleware— 重试一次空的最终响应ModelLengthFinishReasonMiddleware— 记录受长度限制的完成SafetyFinishReasonMiddleware— provider 安全终止后抑制工具执行(如果启用)ClarificationMiddleware— 拦截澄清请求(始终最后)
顺序很重要。Durable context 会在摘要压缩之前捕获已委托任务的派发状态、终态结果和已加载的技能引用,确保它们不会随原始 transcript 压缩而丢失。澄清总是最后运行,这样它可以在所有其他中间件完成后拦截。
中间件参考
ClarificationMiddleware
拦截澄清工具调用,并将其转换为面向用户的信息请求。当模型决定在继续之前需要询问用户某事时,此中间件会将该请求呈现出来。
配置:由 guardrails.clarification 设置控制。
LoopDetectionMiddleware
检测 Agent 是否在没有取得进展的情况下重复进行相同的工具调用。检测到循环时,中间件会介入打破循环,防止 Agent 无限消耗轮次。
Warning 介入会按 thread 和 run 排队,并在下一次模型调用时合并为一条隐藏的 HumanMessage(name="loop_warning"),追加到已有工具结果之后。这样不会破坏 provider 对 tool-call/tool-message 配对的校验。Run 开始和结束时会清理过期或未送达的 warning;达到 hard stop 时仍会清空 tool calls 并强制生成最终文本回复。
此中间件同样挂载到子 Agent 链——子 Agent 不允许调用 task,因此这里只会触发工具循环启发式判定,退化的子 Agent 循环会被同样地打破。
配置:内置,无需用户配置。
MemoryMiddleware
在每次对话开始时读取持久化记忆事实并将其注入到系统提示中。对话结束后,将后台更新排队,以将新信息纳入记忆存储。
配置:参见记忆系统页面和 config.yaml 中的 memory: 部分。
memory:
enabled: true
injection_enabled: true
max_injection_tokens: 2000
debounce_seconds: 30SubagentLimitMiddleware
限制 Agent 在单次轮次中可以进行的并行子 Agent 任务调用数量,也限制一次 Lead Agent run 内的子 Agent 委派总数。这防止 Agent 在重复规划 checkpoint 中持续启动无边界批次。
配置:每次请求配置中的 subagent_enabled、max_concurrent_subagents,以及可选的 max_total_subagents。总量上限默认使用 config.yaml 中的 subagents.max_total_per_run。
TitleMiddleware
在第一次交互后自动为线程生成标题。默认从用户的第一条消息生成快速本地 fallback 标题;只有设置 title.model_name 时,才启用可选的 LLM 标题路径。
配置:config.yaml 中的 title: 部分。
title:
enabled: true
max_words: 6
max_chars: 60
model_name: null # 本地 fallback;填模型名才启用 LLM 标题TodoMiddleware
当计划模式激活时,维护一个对用户可见的结构化任务列表。Agent 使用 write_todos 工具,随着完成复杂目标,将任务标记为 pending、in_progress 或 completed。
激活:在请求配置中设置 is_plan_mode: true 时自动启用。不需要 config.yaml 条目。
TokenUsageMiddleware
追踪每次模型调用的 LLM token 消耗,并以 info 级别记录。对于监控成本和了解长时序任务中 token 的使用位置很有帮助。
配置:config.yaml 中的 token_usage: 部分。
token_usage:
enabled: trueDurableContextMiddleware
在摘要压缩原始 transcript 之前,将长期运行事实捕获到明确的线程状态 channel 中。它记录压缩摘要、已委托任务的状态/结果,以及已加载的 SKILL.md 引用,并在后续模型调用中作为隐藏 durable context 数据投影进去。
后端会在 durable context 捕获之前写入有界的结构化 task 结果和技能读取元数据。前端 task card 读取结构化 task status/result 字段;task 结果文本只作为展示内容,不再作为 wire protocol 解析。
配置:内置。summarization.skill_file_read_tool_names 控制哪些读取工具会被视为技能引用读取;设为 [] 可关闭 durable 技能引用捕获。
SummarizationMiddleware
当对话变长时,对旧消息进行摘要以减少上下文大小。生成的摘要存入线程状态,并在后续模型调用中作为隐藏的 durable context 数据投影进去,在不把旧消息继续留在活跃 transcript 中的情况下保留含义。
同一个中间件与同一个 summarization.enabled 开关也会压缩子 Agent transcript,使一份配置同时覆盖 Lead Agent 与子 Agent 两条链(#3875)。
配置:config.yaml 中的 summarization: 部分。详见下方详细配置。
ViewImageMiddleware
当当前模型支持视觉(supports_vision: true)时,此中间件拦截 view_image 工具调用,并将图像内容直接注入到模型的上下文中以便分析。
激活:当解析的模型具有 supports_vision: true 时自动启用。
DeferredToolFilterMiddleware
当工具搜索启用时,此中间件从模型上下文中隐藏延迟工具 schema。工具通过 tool_search 工具按需发现,而不是预先全部列出,从而减少上下文使用。
配置:config.yaml 中的 tool_search.enabled: true。
对于 create_deerflow_agent,@Next 或 @Prev 锚点必须指向这个较小链中实际存在的中间件;否则使用完整中间件列表中的锚点会解析失败。
摘要压缩配置详解
SummarizationMiddleware 是长时序任务中影响最大的中间件之一。完整配置参考如下:
summarization:
enabled: true
# 摘要使用的模型(null = 使用默认模型)
# 推荐使用轻量级、经济的模型,如 "gpt-4o-mini"
model_name: null
# 触发条件——满足任意一个条件时运行摘要
trigger:
- type: tokens # 当上下文超过 N 个 token 时触发
value: 32000
# - type: messages # 当消息数超过 N 时触发
# value: 50
# - type: fraction # 当上下文达到模型最大输入的 X% 时触发
# value: 0.8
# 摘要后保留多少最近历史
keep:
type: messages
value: 10 # 保留最近 10 条消息
# 或者按 token 保留:
# type: tokens
# value: 3000
# 为摘要器准备消息时要裁剪的最大 token 数
trim_tokens_to_summarize: 15564
# 自定义摘要提示词(null = 使用默认 LangChain 提示词)
summary_prompt: null触发类型:
tokens:当对话中总 token 数超过value时触发。messages:当消息数超过value时触发。fraction:当上下文达到模型最大输入 token 限制的value比例时触发。
保留类型:
messages:摘要后保留最后value条消息。tokens:保留最近value个 token 的历史。fraction:保留模型最大输入 token 限制的value比例的最近历史。
编写自定义中间件
自定义中间件可以注入到链中用于专业用途。中间件必须实现 langchain.agents.middleware 中的 AgentMiddleware 接口:
from langchain.agents import AgentState
from langchain.agents.middleware import AgentMiddleware
from langgraph.runtime import Runtime
class MyMiddleware(AgentMiddleware[AgentState]):
def before_model(self, state: AgentState, runtime: Runtime) -> dict | None:
print(f"模型输入包含 {len(state.get('messages', []))} 条消息")
return None
def after_model(self, state: AgentState, runtime: Runtime) -> dict | None:
messages = state.get("messages", [])
last_message = messages[-1] if messages else None
print(f"最后一条消息类型:{type(last_message).__name__ if last_message else 'none'}")
return None生命周期钩子可以返回状态更新字典,LangChain 会将其合并到 Agent 状态中;仅观察时返回 None。
对于运维配置的部署,请通过导入路径注册零参数构造的类:
extensions:
middlewares:
- my_company.deerflow_middlewares:MyMiddleware配置的中间件会在内置中间件及可选的循环/token 保护之后加载。对于主 Agent 链,它位于终态响应、模型长度、安全和澄清尾部之前;子 Agent 链没有终态响应、模型长度或澄清阶段,因此配置中间件之后会继续执行可选的安全防护、DurableContextMiddleware、可选的 SummarizationMiddleware,随后是 SubagentDateContextMiddleware 和 SystemMessageCoalescingMiddleware。中间件路径会执行 Python 代码,因此应视为可信配置。嵌入式调用方也可以使用 DeerFlowClient(middlewares=[...]);它构建完整的主 Agent 链,并将中间件放在其终态响应、模型长度、安全和澄清尾部之前。create_deerflow_agent(extra_middleware=[...]) 则构建较小的按功能组装的主 Agent 链;未锚定的额外中间件会紧接在 ClarificationMiddleware 之前放置(锚定中间件遵循其 @Next/@Prev 位置)。两个 API 均不会将中间件转发给子 Agent。
应根据配置的所有者和插入位置选择注册方式。固定槽位(并未弃用)extensions.middlewares 可写在 config.yaml 或 extensions_config.json 中(前者优先),并同时作用于主 Agent 和子 Agent。通过顶层 plugins: 注册的打包扩展可在语义化扩展点贡献中间件。若贡献代码需要可提交、可编程且仅作用于主 Agent 的接线,请使用 build_middlewares(..., custom_middlewares=[MyMiddleware()])。