DeerFlow 2.0 Lead Agent 中间件分析

Lead Agent 中间件

基于 _build_middlewares() 函数(backend/packages/harness/deerflow/agents/lead_agent/agent.py:269-363


一、中间件组装总览

中间件链分为 两个阶段 组装:

  1. 阶段一build_lead_runtime_middlewares() ------ 基础设施层中间件(所有 Agent 共享)
  2. 阶段二_build_middlewares() 中追加 ------ Lead Agent 专属业务中间件

重要规则 :LangChain 的 after_model 钩子按 逆序 分发------最后 append 的中间件在 after_model 阶段 最先执行。因此 ClarificationMiddleware 必须最后 append,SafetyFinishReasonMiddleware 次之。


二、中间件详细顺序、功能与实现

阶段一:基础设施层(build_lead_runtime_middlewares

#1 ThreadDataMiddleware
项目 说明
文件 middlewares/thread_data_middleware.py
功能 为每个线程创建数据目录结构:threads/{thread_id}/user-data/{workspace,uploads,outputs}
生命周期钩子 before_agent

核心流程

复制代码
runtime.context → thread_id → Paths 计算目录路径 → 写入 state.thread_data

实现细节

  1. 路径计算 :通过 Paths 类解析基础目录,结合 thread_iduser_id 计算三个目录路径:
    • sandbox_work_dir(thread_id, user_id) → workspace 目录
    • sandbox_uploads_dir(thread_id, user_id) → uploads 目录
    • sandbox_outputs_dir(thread_id, user_id) → outputs 目录
  2. 懒加载模式lazy_init=True):仅计算路径,不调用 ensure_thread_dirs(),目录在首次使用时按需创建
  3. 消息增强 :为最后一条 HumanMessage 添加 run_idtimestampadditional_kwargs,并设置 name="user-input"
  4. 用户隔离 :通过 get_effective_user_id() 获取当前用户 ID,实现多用户场景下的路径隔离

#2 UploadsMiddleware
项目 说明
文件 middlewares/uploads_middleware.py
功能 将用户上传文件的信息(文件名、路径、文档大纲/预览)注入到 Agent 上下文中
生命周期钩子 before_agent

核心流程

复制代码
最后一条 HumanMessage → additional_kwargs.files → 解析文件元数据 → 提取大纲/预览 → 构建 <uploaded_files> 消息 → 前置到用户消息

实现细节

  1. 新文件提取 :从 HumanMessage.additional_kwargs.files 读取前端上传的文件元数据(filenamesizepathstatus),验证文件名合法性(Path(filename).name == filename 防止路径穿越)
  2. 历史文件扫描 :遍历线程 uploads 目录(uploads_dir.iterdir()),排除新文件后收集历史文件
  3. 文档大纲提取_extract_outline_for_file() 查找伴生 .md 文件,通过 extract_outline() 提取 {title, line} 结构;若大纲为空则读取前 5 行非空内容作为预览
  4. 消息构建_create_files_message() 生成 <uploaded_files> 格式化消息,包含文件列表、大纲、操作指南;新文件和历史文件分区展示
  5. 多模态兼容 :对 list 类型 content(含图像等多模态块),将文件消息作为首个 text block 插入;保留原始 additional_kwargs 供前端读取

#3 SandboxMiddleware
项目 说明
文件 sandbox/middleware.py
功能 为 Agent 创建和管理沙箱执行环境
生命周期钩子 before_agentafter_agentwrap_tool_call

核心流程

复制代码
thread_id → SandboxProvider.acquire() → sandbox_id → state.sandbox
...工具调用使用沙箱...
state.sandbox → SandboxProvider.release(sandbox_id)

实现细节

  1. 沙箱获取_acquire_sandbox(thread_id) 调用 get_sandbox_provider().acquire(thread_id) 获取沙箱 ID;异步版本使用 acquire_async()
  2. 懒加载策略lazy_init=Truebefore_agent 直接返回 super(),延迟到 wrap_tool_call 中首次工具调用时才获取沙箱
  3. 双来源释放after_agent 优先从 state.sandbox 取 sandbox_id,其次从 runtime.context.sandbox_id 取(兼容上下文直传场景)
  4. 异步释放_release_sandbox_async() 通过 asyncio.to_thread() 将同步 release() 包装为异步操作,避免阻塞事件循环
  5. 生命周期 :沙箱在同一线程内跨轮次复用,不主动释放(避免重建开销),仅在应用关闭时通过 SandboxProvider.shutdown() 统一清理

#4 DanglingToolCallMiddleware
项目 说明
文件 middlewares/dangling_tool_call_middleware.py
功能 修复消息历史中的"悬空工具调用"------AIMessage 包含 tool_calls 但缺少对应 ToolMessage 的情况
生命周期钩子 wrap_model_call

核心流程

复制代码
request.messages → 扫描 AI+Tool 消息 → 检测悬空 tool_calls → 插入合成 ToolMessage → request.override(messages)

实现细节

  1. 工具调用归一化_message_tool_calls() 从三个来源收集工具调用:
    • msg.tool_calls:结构化字段(标准路径)
    • msg.additional_kwargs.tool_calls:原始 provider payload(含 function.name / function.arguments 嵌套格式)
    • msg.invalid_tool_calls:解析失败的调用(标记 "invalid": True
  2. 修补策略_build_patched_messages() 使用两遍扫描:
    • 第一遍:建立 tool_call_id → deque[ToolMessage] 索引
    • 第二遍:遍历消息,对每条 AIMessage 的每个 tool_call 检查是否有对应 ToolMessage;缺失则插入合成错误消息
  3. 合成消息内容_synthetic_tool_message_content() 区分 invalid(参数无效)和 interrupted(被中断)两种情况
  4. 位置保持 :使用 wrap_model_call(非 before_model)确保修补消息插入到 AIMessage 之后 正确位置,而非通过 add_messages reducer 追加到末尾

#5 LLMErrorHandlingMiddleware
项目 说明
文件 middlewares/llm_error_handling_middleware.py
功能 对 LLM 调用中的瞬态错误进行重试/退避,并向用户展示友好的降级消息
生命周期钩子 wrap_model_call

核心流程

复制代码
检查熔断器 → handler(request) → 成功: record_success / 失败: classify_error → 可重试: 退避重试 / 不可重试: 返回用户消息

实现细节

  1. 熔断器(Circuit Breaker) :三态模型
    • Closed(正常):失败计数 < 阈值,正常放行
    • Open (熔断):失败计数 >= 阈值,快速失败返回 AIMessage;持续 recovery_timeout_sec 秒后转 Half-Open
    • Half-Open(探测):放行一次请求作为探针,成功则转 Closed,失败则转 Open
    • 使用 threading.Lock 保证线程安全;配置来自 app_config.circuit_breaker
  2. 错误分类_classify_error() 返回 (retriable, reason) 元组
    • quota :匹配 insufficient_quotabilling余额不足 等模式 → 不可重试
    • auth :匹配 authenticationinvalid api key无权 等 → 不可重试
    • transient :特定异常类(APITimeoutErrorAPIConnectionError 等)或 HTTP 状态码 {408,409,425,429,500,502,503,504} → 可重试
    • busy :匹配 server busyrate limit负载较高 等 → 可重试
  3. 退避策略_build_retry_delay_ms() 先检查 Retry-After / Retry-After-Ms 响应头,否则指数退避(base_delay * 2^(attempt-1),上限 cap_delay
  4. SSE 事件_emit_retry_event() 通过 get_stream_writer() 发送 llm_retry 事件给前端,包含重试次数、等待时间、原因
  5. 控制流保护GraphBubbleUp 异常(interrupt/pause/resume)直接 re-raise,不计入熔断器

#6 GuardrailMiddleware(条件启用)
项目 说明
文件 guardrails/middleware.py
启用条件 app_config.guardrails.enabled == Trueguardrails.provider 已配置
功能 在工具执行前对工具调用进行安全策略评估
生命周期钩子 wrap_tool_call

核心流程

复制代码
ToolCallRequest → 构建 GuardrailRequest → GuardrailProvider.evaluate() → allow: 执行 / deny: 返回错误 ToolMessage

实现细节

  1. 请求构建_build_request()ToolCallRequest 提取 tool_nametool_input,加上 agent_id(passport)和 ISO 时间戳,构建 GuardrailRequest
  2. 策略评估 :调用 GuardrailProvider.evaluate(gr)(同步)或 aevaluate(gr)(异步),返回 GuardrailDecision
  3. 拒绝处理_build_denied_message() 构建带 status="error" 的 ToolMessage,包含策略 code 和拒绝原因,引导 Agent 选择替代方案
  4. 故障模式 :Provider 异常时根据 fail_closed(默认 True)决定:
    • True:返回 oap.evaluator_error 拒绝决策(保守策略)
    • False:直接调用 handler(request) 放行(宽松策略)
  5. 控制流保护GraphBubbleUp 直接 re-raise,保留 LangGraph 的 interrupt/pause 信号

#7 SandboxAuditMiddleware
项目 说明
文件 middlewares/sandbox_audit_middleware.py
功能 对 bash 命令进行正则分级安全审计
生命周期钩子 wrap_tool_call

核心流程

复制代码
仅处理 bash 工具 → 输入校验 → 命令分类(两级扫描) → block: 返回错误 / warn: 执行+追加警告 / pass: 正常执行

实现细节

  1. 输入校验_validate_input):拒绝空命令、超长命令(> 10000 字符)、含 null 字节的命令
  2. 两级命令分类_classify_command):
    • 第一遍:全命令扫描高危正则(捕获跨语句的结构攻击如 fork bomb)
    • 第二遍_split_compound_command()&&||; 拆分复合命令(引号感知),逐条分类取最严结果
  3. 高危规则 (14 条正则,block):rm -rf /dd if=mkfs、管道到 sh、命令注入、/dev/tcp/、fork bomb 等
  4. 中危规则 (5 条正则,warn):chmod 777pip installsudo/su、PATH 修改
  5. 引号感知拆分_split_compound_command):逐字符扫描,跟踪单/双引号和转义状态;未闭合引号时 fail-closed 返回整条命令
  6. 审计日志 :每次 bash 调用记录结构化 JSON(timestampthread_idcommandverdict),命令超 200 字符时截断
  7. 警告追加 :中危命令执行后在 ToolMessage content 末尾追加 ⚠️ Warning 提示

#8 ToolErrorHandlingMiddleware
项目 说明
文件 middlewares/tool_error_handling_middleware.py
功能 将工具执行异常转化为结构化错误 ToolMessage,避免 Agent 运行中断
生命周期钩子 wrap_tool_call

核心流程

复制代码
try: handler(request) → 正常返回
except GraphBubbleUp: re-raise
except Exception: 构建错误 ToolMessage 返回

实现细节

  1. 异常捕获 :包裹 handler(request) 的同步/异步调用,捕获所有非控制流异常
  2. 错误消息构建_build_error_message() 生成格式化内容:包含工具名、异常类名、异常详情(截断至 500 字符);tool_call_id 缺失时使用 "missing_tool_call_id" 兜底;status="error" 标记
  3. 控制流保护GraphBubbleUp(LangGraph 的 interrupt/pause/resume 信号)直接 re-raise,不被错误处理吞掉
  4. 日志记录logger.exception() 记录完整异常栈,包含工具名和 tool_call_id

阶段二:Lead Agent 业务层

#9 DynamicContextMiddleware
项目 说明
文件 middlewares/dynamic_context_middleware.py
功能 将动态上下文(当前日期 + 用户记忆)以 <system-reminder> 形式注入消息历史
生命周期钩子 before_agent

核心流程

复制代码
扫描消息历史 → 无历史日期: 注入完整提醒 / 同日期: 跳过 / 跨午夜: 注入日期更新提醒

实现细节

  1. 完整提醒 (首次对话):
    • 构建 <system-reminder> 包含 <memory>...</memory>(可选)和 <current_date>YYYY-MM-DD, Weekday</current_date>
    • 记忆注入由 _get_memory_context() 获取,受 memory.injection_enabled 配置控制
    • 使用 ID 交换技术 :reminder 消息占用原消息 ID(原地替换),用户消息获得 {id}__user 新 ID(紧随其后插入)
    • 提醒消息标记 hide_from_ui: Truedynamic_context_reminder: True
  2. 跨午夜检测_last_injected_date() 逆序扫描消息,通过 dynamic_context_reminder 标记(非内容匹配)找到上次注入的日期;日期不同时注入轻量级日期更新提醒
  3. 注入目标过滤_is_user_injection_target() 排除已有提醒消息和 name="summary" 的摘要消息
  4. 前缀缓存优化:系统 prompt 保持静态不变,动态内容通过 HumanMessage 注入,最大化 LLM prefix-cache 命中率

#10 SummarizationMiddleware(条件启用)
项目 说明
文件 middlewares/summarization_middleware.py
启用条件 通过 _create_summarization_middleware() 检查配置是否启用
功能 当消息历史过长时,将旧消息压缩为摘要
生命周期钩子 before_model

核心流程

复制代码
计算 token 总数 → 判断是否需要压缩 → 确定截断点 → Skill 救援 → 保留动态上下文 → 触发 hooks → 创建摘要 → 替换消息列表

实现细节

  1. 继承自 SummarizationMiddleware:复用基类的 token 计数、阈值判断、消息分区逻辑
  2. Skill 救援机制_partition_with_skill_rescue):
    • _find_skill_bundles():扫描待压缩消息,识别 read_file 等读取 /mnt/skills/ 下文件的 AIMessage + ToolMessage 组合
    • _select_bundles_to_rescue():从最新到最旧,在数量预算(默认 5)和 token 预算(默认 25000)内保留最近的 Skill 内容
    • 救援的 Skill 消息从待压缩列表移到保留列表,避免重要技能上下文被摘要丢失
  3. 动态上下文保护_preserve_dynamic_context_reminders() 将隐藏的 <system-reminder> 消息从待压缩列表移到保留列表,防止 DynamicContextMiddleware 在后续轮次误判
  4. Pre-compression HookBeforeSummarizationHook Protocol,在压缩前将 SummarizationEvent(待压缩消息、保留消息、thread_id、agent_name)通知外部组件
  5. 消息替换策略 :使用 RemoveMessage(id=REMOVE_ALL_MESSAGES) 清除全部旧消息,再插入 [摘要 HumanMessage(name="summary"), ...保留消息]
  6. 摘要消息格式name="summary" 的 HumanMessage,前端可识别并隐藏显示

#11 TodoMiddleware(条件启用)
项目 说明
文件 middlewares/todo_middleware.py
启用条件 is_plan_mode == True
功能 扩展 TodoListMiddleware,增加上下文丢失检测和过早退出防护
生命周期钩子 before_modelafter_modelwrap_model_call

核心流程

复制代码
before_model: 检测 write_todos 是否还在上下文 → 不在则注入 todo_reminder
after_model: 模型产生最终响应但 todo 未完成 → queue reminder → jump_to("model")
wrap_model_call: 排空 pending reminders → 追加到 request.messages

实现细节

  1. 上下文丢失检测before_model):
    • 检查 state.todos 非空但 _todos_in_messages(messages) 为 False(write_todos 调用已被压缩出窗口)
    • 注入 HumanMessage(name="todo_reminder"),包含格式化的 todo 列表和继续追踪的指令
    • 防止重复注入:_reminder_in_messages() 检测是否已有提醒
  2. 过早退出防护after_model):
    • 检测最后一条 AIMessage 是"干净的最终回答"(无 tool_calls、无 invalid_tool_calls、无 tool-call finish_reason)
    • 检查 todos 是否全部完成;未完成则调用 _queue_completion_reminder() 排队提醒
    • 返回 {"jump_to": "model"} 强制回到模型节点继续执行
    • 最多 2 次(_MAX_COMPLETION_REMINDERS)防止无限循环
  3. 提醒注入wrap_model_call):
    • _drain_completion_reminders() 排空当前 thread/run 的待注入提醒
    • HumanMessage(name="todo_completion_reminder", hide_from_ui=True) 追加到 request.messages
    • 非持久化方式,不进入图状态,不泄露到用户可见的消息流
  4. 清理机制before_agent 清除其他 run 的残留提醒;after_agent 清除当前 run 的残留提醒;LRU 上限 4096 个 key

#12 TokenUsageMiddleware(条件启用)
项目 说明
文件 middlewares/token_usage_middleware.py
启用条件 app_config.token_usage.enabled == True
功能 记录 token 用量并标注步骤归因
生命周期钩子 after_model

核心流程

复制代码
最后一条 AIMessage → usage_metadata → 日志记录 → 构建 attribution → 写入 additional_kwargs

实现细节

  1. Token 日志 :从 AIMessage.usage_metadata 提取 input_tokensoutput_tokenstotal_tokens(含 input_token_details/output_token_details),以 logger.info 记录
  2. 步骤归因_build_attribution):
    • write_todos_build_todo_actions():对比前后 todo 列表,精确标注 todo_start/todo_complete/todo_update/todo_remove
    • tasksubagent 类型,含 description 和 subagent_type
    • web_search/image_searchsearch 类型,含 query
    • present_filespresent_files 类型
    • ask_clarificationclarification 类型
    • 其他 → tool 类型,含 tool_name 和 description
  3. 步骤类型推断_infer_step_kind):根据 actions 推断 todo_update/subagent_dispatch/tool_batch/final_answer/thinking
  4. 子 Agent Token 回写 :当 task ToolMessage 完成时,从 pop_cached_subagent_usage(tool_call_id) 取子 Agent 的 token 用量,回写到派遣它的 AIMessage 的 usage_metadata
  5. 去重additional_kwargs.token_usage_attribution 相同则跳过,避免无变化的重复更新

#13 TitleMiddleware
项目 说明
文件 middlewares/title_middleware.py
功能 在首次用户消息后自动生成线程标题
生命周期钩子 after_model

核心流程

复制代码
检查是否首次对话 → 构建 title prompt → 调用 LLM 生成标题 → 解析/截断 → state.title

实现细节

  1. 触发条件_should_generate_title):state.title 为空 + 消息数 >= 2 + 恰好 1 条用户消息 + >= 1 条助手消息
  2. Prompt 构建 :从 TitleConfig.prompt_template 构建,注入 max_wordsuser_msg[:500]assistant_msg[:500];助手消息经过 _strip_think_tags() 移除 <think>...</think> 推理块
  3. LLM 调用 (异步):create_chat_model(name=config.model_name) 创建独立模型实例,thinking_enabled=Falseattach_tracing=False 避免重复 span
  4. 本地降级 (同步路径 / 异步失败时):_fallback_title() 截取用户消息前 min(max_chars, 50) 字符作为标题,空则 "New Conversation"
  5. 标题解析_parse_title() 去除 <think> 标签、首尾引号,截断至 max_chars
  6. RunnableConfig 继承_get_runnable_config() 继承父级配置,添加 run_name="title_agent"tags=["middleware:title"]

#14 MemoryMiddleware
项目 说明
文件 middlewares/memory_middleware.py
功能 在 Agent 执行完成后将对话排入记忆更新队列
生命周期钩子 after_agent

核心流程

复制代码
过滤消息 → 检测纠正/强化信号 → 捕获 user_id → 加入 MemoryQueue

实现细节

  1. 消息过滤filter_messages_for_memory(messages) 仅保留用户输入和助手最终响应,排除工具调用和中间过程
  2. 前置检查:至少需要 1 条用户消息 + 1 条助手消息才触发记忆更新
  3. 纠正检测detect_correction(filtered_messages) 检测用户是否纠正了助手的行为
  4. 强化检测detect_reinforcement(filtered_messages) 检测用户是否强化了某个观点(仅在未检测到纠正时)
  5. User ID 捕获 :在 after_agent 中(请求上下文仍然有效)通过 get_effective_user_id() 获取 user_id;threading.Timer 在不同线程触发,ContextVar 不会传播,因此必须在此刻显式捕获
  6. 入队处理get_memory_queue().add() 将对话和元数据加入 MemoryQueue,队列内部使用防抖机制批处理多个更新,异步通过 LLM 摘要更新持久化记忆

#15 ViewImageMiddleware(条件启用)
项目 说明
文件 middlewares/view_image_middleware.py
启用条件 model_config.supports_vision == True
功能 在 LLM 调用前注入图像详情(含 base64 数据)
生命周期钩子 before_model

核心流程

复制代码
最后一条 AIMessage → 包含 view_image 工具调用? → 所有工具已完成? → 注入图像详情 HumanMessage

实现细节

  1. 检测条件链_should_inject_image_message):消息列表非空 + 最后一条 AIMessage 包含 view_image 工具调用 + 所有 tool_call_id 都有对应 ToolMessage + 尚未注入过图像详情消息
  2. 图像消息构建_create_image_details_message):从 state.viewed_images 读取图像数据,每张图像生成文本描述(路径、MIME 类型)和 image_url 内容块(data:{mime};base64,{data} 格式)
  3. 消息注入 :创建 HumanMessage(content=content_blocks) 添加到 state.messages,LLM 自动接收并分析图像
  4. 去重机制:检查 assistant 消息之后是否已有包含特定标记文本的 HumanMessage,避免重复注入

#16 DeferredToolFilterMiddleware(条件启用)
项目 说明
文件 middlewares/deferred_tool_filter_middleware.py
启用条件 app_config.tool_search.enabled == True
功能 从模型绑定中过滤掉延迟注册的工具 schema,节省上下文 token
生命周期钩子 wrap_model_callwrap_tool_call

核心流程

复制代码
wrap_model_call: request.tools → 过滤 deferred 工具 → handler(filtered_request)
wrap_tool_call: 检查工具是否在 deferred registry → 是: 返回错误提示 / 否: 正常执行

实现细节

  1. Schema 过滤_filter_tools):从 get_deferred_registry() 获取延迟工具名称集合,从 request.tools 中移除匹配的工具 schema;ToolNode 仍持有全部工具用于执行路由
  2. 直接调用拦截_blocked_tool_message):若模型直接调用延迟工具(未经 tool_search 提升),返回错误 ToolMessage 提示先调用 tool_search
  3. 设计目的 :MCP 工具数量可能很多,全部发送给 LLM 会浪费上下文 token;通过延迟注册 + tool_search 发现模式,仅暴露需要的工具

#17 SubagentLimitMiddleware(条件启用)
项目 说明
文件 middlewares/subagent_limit_middleware.py
启用条件 subagent_enabled == True
功能 限制单次模型响应中并发子 Agent(task 工具调用)的数量
生命周期钩子 after_model

核心流程

复制代码
最后一条 AIMessage.tool_calls → 统计 task 调用数量 → 超过上限: 截断多余调用 → 替换 AIMessage

实现细节

  1. 限制范围_clamp_subagent_limit() 将值限制在 2, 4 区间,默认 3
  2. 截断逻辑_truncate_task_calls):筛选 name == "task" 的工具调用索引,超过 max_concurrent 的部分标记为 drop
  3. 消息替换clone_ai_message_with_tool_calls(last_msg, truncated_tool_calls) 克隆 AIMessage 并替换 tool_calls 列表,保留相同 id 触发 add_messages reducer 的替换语义
  4. 日志警告:截断时记录被丢弃的调用数量

#18 LoopDetectionMiddleware(条件启用)
项目 说明
文件 middlewares/loop_detection_middleware.py
启用条件 app_config.loop_detection.enabled == True
功能 检测并打断重复的工具调用循环
生命周期钩子 after_modelwrap_model_callafter_agent

核心流程

复制代码
after_model: 哈希 tool_calls → 滑动窗口统计 → 达到 warn: 入队警告 / 达到 hard_limit: 清除 tool_calls
wrap_model_call: 排空 pending warnings → 追加到 request.messages

实现细节

  1. 工具调用哈希_hash_tool_calls):
    • 每个调用生成 name:stable_key,key 由 _stable_tool_key() 根据工具类型选择归一化策略
    • read_file:路径 + 行号桶(每 200 行一桶),避免微小偏移产生不同哈希
    • write_file/str_replace:全 args 哈希(内容敏感操作需区分不同 payload)
    • 其他:仅取 salient fields(path/url/query/command 等)
    • 多调用排序后 MD5 取前 12 位,保证顺序无关性
  2. 滑动窗口OrderedDict 存储每线程最近 N 次(默认 20)调用哈希;LRU 淘汰策略限制 100 个线程
  3. 警告注入 (延迟到 wrap_model_call):
    • after_model 检测到重复后入队到 _pending_warnings[(thread_id, run_id)]
    • wrap_model_call 排空队列,以 HumanMessage 追加到消息列表
    • 延迟原因:after_model 时 ToolMessage 尚未生成,直接插入消息会破坏 AIMessage→ToolMessage 配对
  4. 硬限制 :达到 hard_limit 时直接清除 AIMessage 的 tool_calls(通过 clone_ai_message_with_tool_calls),强制模型生成文本回复
  5. 工具频率检测 :独立于哈希检测,统计同一工具类型的调用频次(默认 warn=30, hard=50);支持 per-tool 覆盖(如 bash 在批处理场景可提高阈值)
  6. 线程安全 :所有共享状态通过 threading.Lock 保护

#19 Custom Middlewares
项目 说明
位置 通过 custom_middlewares 参数传入
功能 用户自定义中间件,插入在 ClarificationMiddleware 之前
说明 可使用 @Next/@Prev 装饰器指定相对于内置中间件的精确位置;create_deerflow_agent() 中的 extra_middleware 参数通过相同的定位机制工作

#20 SafetyFinishReasonMiddleware(条件启用)
项目 说明
文件 middlewares/safety_finish_reason_middleware.py
启用条件 app_config.safety_finish_reason.enabled == True(默认启用)
功能 当模型提供商因安全原因终止响应时,抑制工具执行
生命周期钩子 after_model

核心流程

复制代码
最后一条 AIMessage → 有 tool_calls? → 遍历 detectors 检测安全终止 → 命中: 清除 tool_calls + 追加说明 + 可观测性事件

实现细节

  1. 检测器链_detect):遍历 self._detectors(默认 default_detectors()),每个检测器实现 SafetyTerminationDetector Protocol(name + detect(message));检测 OpenAI content_filter、Anthropic refusal、Gemini SAFETY 等;单个检测器异常不中断链
  2. 配置加载from_config):支持通过 use 字段反射加载自定义检测器类(resolve_variable(entry.use));空列表被拒绝(要求使用 enabled=false
  3. 消息重写_build_suppressed_message):clone_ai_message_with_tool_calls(message, []) 清除结构化和原始 tool_calls;追加用户说明文本到 content(支持 list/str 两种格式);additional_kwargs.safety_termination 存储可观测性数据
  4. SSE 事件_emit_event):通过 get_stream_writer() 发送 safety_termination 事件,通知前端工具调用被抑制
  5. 审计持久化_record_audit_event):通过 RunJournal.record_middleware() 写入 RunEventStore,支持事后查询"哪些 run 被安全抑制"
  6. 放置顺序关键 :必须在 LoopDetection 之后注册------LangChain 逆序分发 after_model,Safety 先运行清除 tool_calls,Loop 再看到的是已清理的消息,避免误报

#21 ClarificationMiddleware(始终最后)
项目 说明
文件 middlewares/clarification_middleware.py
功能 拦截 ask_clarification 工具调用,中断执行并向用户展示澄清问题
生命周期钩子 wrap_tool_call

核心流程

复制代码
tool_call.name == "ask_clarification"? → 是: 格式化问题 → 返回 Command(goto=END) / 否: 正常执行

实现细节

  1. 工具拦截 :检查 request.tool_call.name == "ask_clarification",非此工具直接透传
  2. 参数提取 :从 args 读取 questionclarification_typecontextoptions
  3. 选项归一化 :处理模型将数组参数序列化为 JSON 字符串的情况(如 Qwen3-Max),json.loads() 反序列化
  4. 消息格式化_format_clarification_message):按 clarification_type 选择图标(missing_info→❓、ambiguous_requirement→🤔、approach_choice→🔀、risk_confirmation→⚠️、suggestion→💡);有 context 时先展示背景再展示问题;有 options 时编号列出
  5. 执行中断 :返回 Command(update={"messages": [ToolMessage]}, goto=END);ToolMessage 使用确定性 ID(clarification:{tool_call_id} 或 SHA256 前 16 位),确保重试时替换而非追加
  6. 必须最后注册 :LangChain 逆序分发 after_model,Clarification 需要最先拦截 ask_clarification 工具调用(在 Safety 清除 tool_calls 之前拦截到)

三、中间件执行流程图

#mermaid-svg-lhPu19gizcjFy46j{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lhPu19gizcjFy46j .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lhPu19gizcjFy46j .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lhPu19gizcjFy46j .error-icon{fill:#552222;}#mermaid-svg-lhPu19gizcjFy46j .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lhPu19gizcjFy46j .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lhPu19gizcjFy46j .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lhPu19gizcjFy46j .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lhPu19gizcjFy46j .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lhPu19gizcjFy46j .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lhPu19gizcjFy46j .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lhPu19gizcjFy46j .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lhPu19gizcjFy46j .marker.cross{stroke:#333333;}#mermaid-svg-lhPu19gizcjFy46j svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lhPu19gizcjFy46j p{margin:0;}#mermaid-svg-lhPu19gizcjFy46j .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lhPu19gizcjFy46j .cluster-label text{fill:#333;}#mermaid-svg-lhPu19gizcjFy46j .cluster-label span{color:#333;}#mermaid-svg-lhPu19gizcjFy46j .cluster-label span p{background-color:transparent;}#mermaid-svg-lhPu19gizcjFy46j .label text,#mermaid-svg-lhPu19gizcjFy46j span{fill:#333;color:#333;}#mermaid-svg-lhPu19gizcjFy46j .node rect,#mermaid-svg-lhPu19gizcjFy46j .node circle,#mermaid-svg-lhPu19gizcjFy46j .node ellipse,#mermaid-svg-lhPu19gizcjFy46j .node polygon,#mermaid-svg-lhPu19gizcjFy46j .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lhPu19gizcjFy46j .rough-node .label text,#mermaid-svg-lhPu19gizcjFy46j .node .label text,#mermaid-svg-lhPu19gizcjFy46j .image-shape .label,#mermaid-svg-lhPu19gizcjFy46j .icon-shape .label{text-anchor:middle;}#mermaid-svg-lhPu19gizcjFy46j .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lhPu19gizcjFy46j .rough-node .label,#mermaid-svg-lhPu19gizcjFy46j .node .label,#mermaid-svg-lhPu19gizcjFy46j .image-shape .label,#mermaid-svg-lhPu19gizcjFy46j .icon-shape .label{text-align:center;}#mermaid-svg-lhPu19gizcjFy46j .node.clickable{cursor:pointer;}#mermaid-svg-lhPu19gizcjFy46j .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lhPu19gizcjFy46j .arrowheadPath{fill:#333333;}#mermaid-svg-lhPu19gizcjFy46j .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lhPu19gizcjFy46j .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lhPu19gizcjFy46j .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lhPu19gizcjFy46j .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lhPu19gizcjFy46j .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lhPu19gizcjFy46j .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lhPu19gizcjFy46j .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lhPu19gizcjFy46j .cluster text{fill:#333;}#mermaid-svg-lhPu19gizcjFy46j .cluster span{color:#333;}#mermaid-svg-lhPu19gizcjFy46j div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lhPu19gizcjFy46j .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lhPu19gizcjFy46j rect.text{fill:none;stroke-width:0;}#mermaid-svg-lhPu19gizcjFy46j .icon-shape,#mermaid-svg-lhPu19gizcjFy46j .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lhPu19gizcjFy46j .icon-shape p,#mermaid-svg-lhPu19gizcjFy46j .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lhPu19gizcjFy46j .icon-shape .label rect,#mermaid-svg-lhPu19gizcjFy46j .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lhPu19gizcjFy46j .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lhPu19gizcjFy46j .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lhPu19gizcjFy46j :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Agent 执行出口
阶段二: 业务层 - before_model / after_model
阶段一: 基础设施层 - wrap_model_call / wrap_tool_call
阶段一: 基础设施层 - before_agent
Agent 生命周期入口
Agent 开始执行
#1 ThreadDataMiddleware

创建线程数据目录
#2 UploadsMiddleware

(before_agent 注入上传文件信息)
#3 SandboxMiddleware

获取/分配沙箱环境
#4 DanglingToolCallMiddleware

修补悬空工具调用
#5 LLMErrorHandlingMiddleware

LLM 重试 + 熔断器
#6 GuardrailMiddleware

工具调用安全策略评估

(条件启用)
#7 SandboxAuditMiddleware

Bash 命令分级审计
#8 ToolErrorHandlingMiddleware

工具异常 → 错误 ToolMessage
#9 DynamicContextMiddleware

注入日期 + 记忆
#10 SummarizationMiddleware

长对话压缩为摘要

(条件启用)
#11 TodoMiddleware

任务列表 + 过早退出防护

(plan_mode 启用)
#12 TokenUsageMiddleware

Token 用量追踪 + 归因

(条件启用)
#13 TitleMiddleware

自动生成线程标题
#14 MemoryMiddleware

对话 → 记忆更新队列
#15 ViewImageMiddleware

注入图像 base64 数据

(vision 模型启用)
#16 DeferredToolFilterMiddleware

过滤延迟工具 schema

(tool_search 启用)
#17 SubagentLimitMiddleware

截断超额并发子 Agent

(subagent 启用)
#18 LoopDetectionMiddleware

检测/打断重复工具调用循环

(条件启用)
#19 Custom Middlewares

用户自定义中间件
#20 SafetyFinishReasonMiddleware

安全终止 → 抑制工具执行

(条件启用)
#21 ClarificationMiddleware

拦截澄清问题 → 中断执行

(始终最后)
Agent 执行结束


四、after_model 逆序执行顺序

LangChain 的 after_model 钩子按 逆序 分发(最后注册的先执行),因此实际 after_model 执行顺序为:

复制代码
 ClarificationMiddleware          ← 最先执行 (但此中间件主要用 wrap_tool_call)
 SafetyFinishReasonMiddleware     ← 第二执行:先清除安全终止的 tool_calls
 Custom Middlewares
 LoopDetectionMiddleware          ← 在 Safety 之后:看到的是已清理的消息
 SubagentLimitMiddleware
 DeferredToolFilterMiddleware
 ViewImageMiddleware
 MemoryMiddleware
 TitleMiddleware
 TokenUsageMiddleware
 TodoMiddleware
 SummarizationMiddleware
 DynamicContextMiddleware
 ...
 ToolErrorHandlingMiddleware
 SandboxAuditMiddleware
 GuardrailMiddleware
 LLMErrorHandlingMiddleware
 DanglingToolCallMiddleware      ← 最后执行

这样设计确保 SafetyFinishReasonMiddleware 先于 LoopDetectionMiddleware 运行------

避免因安全终止产生的截断 tool_calls 触发循环检测的误报。


五、生命周期钩子对照表

# 中间件 before_agent before_model wrap_model_call after_model wrap_tool_call after_agent
1 ThreadDataMiddleware
2 UploadsMiddleware
3 SandboxMiddleware
4 DanglingToolCallMiddleware
5 LLMErrorHandlingMiddleware
6 GuardrailMiddleware
7 SandboxAuditMiddleware
8 ToolErrorHandlingMiddleware
9 DynamicContextMiddleware
10 SummarizationMiddleware
11 TodoMiddleware
12 TokenUsageMiddleware
13 TitleMiddleware
14 MemoryMiddleware
15 ViewImageMiddleware
16 DeferredToolFilterMiddleware
17 SubagentLimitMiddleware
18 LoopDetectionMiddleware
19 Custom Middlewares --- --- --- --- --- ---
20 SafetyFinishReasonMiddleware
21 ClarificationMiddleware
相关推荐
一切皆是因缘际会1 小时前
隐层表征解构:LLM感知式幻觉稀疏成因
算法·数学建模·ai·架构
iotxiaohu1 小时前
一图认识 —— 互斥锁
c语言·ai·信号量
DS随心转APP1 小时前
Claude 导出对话多方案横向测评来袭,借助 AI 导出鸭对比各类导出工具优劣,筛选最优处理办法
人工智能·ai·chatgpt·deepseek·ai导出鸭
尘埃落定wf1 小时前
LangGraph 与 Human-in-the-Loop 实战指南
ai·langragh
岳小哥AI2 小时前
一文读懂AI应用技术:自然语言处理、语音识别/合成、可解释AI
ai·ai基础
DS随心转插件2 小时前
Kimi 转 pdf 怎么压缩但清晰?AI 导出鸭一站式优化,压缩文件同时留存原版高清内容
人工智能·ai·pdf·豆包·deepseek·ai导出鸭
钱多多_qdd2 小时前
claude code(十):【企业级应用实战1】:章节介绍与前言
ai·claude
xiezhr2 小时前
Hermes官方桌面版发布了
人工智能·ai·agent·codex·hermes
CoderJia程序员甲2 小时前
GitHub 热榜项目 - 周榜(2026-06-14)
ai·大模型·llm·github