DeerFlow Lead Agent 与中间件链:20+ 层防护的 Agent 核心

📑 目录

这是「DeerFlow 源码深度解析」专题的第 03 篇。在本系列中,我们将逐层拆解 DeerFlow 的 Agent 核心——从 ThreadState 的状态扩展,到 20+ 层中间件链的编排哲学,再到 Prompt 模板的工程化设计,带你一览工业级 AI Agent 的防护与编排之道。


故事场景:当 Agent 陷入无限循环

凌晨 1:23,某技术团队的内部 Agent 平台监控告警突然炸响。

小王——负责运维 AI Agent 的值班工程师——看着飙升的 Token 用量曲线,手心直冒汗。一个用户的对话线程在短短 15 分钟内消耗了 47 万 Token,而对话内容只是简单的"帮我总结一下这个目录下的代码结构"。

"又是循环,"他喃喃自语,"Agent 在重复调用 read_file,读完 src/main.pysrc/utils.py,再读 src/main.py……"

打开日志,场景更加触目惊心:模型输出了 read_file 工具调用,执行完拿到结果,下一个回合又输出了完全相同的 read_file 调用。没有自省、没有收敛,像一个在迷宫里打转的机器人。

在 DeerFlow 的源码中,这个问题由 LoopDetectionMiddleware 解决——它在第 3 次检测到完全相同的工具调用时注入警告,第 5 次直接强制终止。但这只是 20+ 层中间件链中的其中一环。真正让 DeerFlow 在工业场景下稳定运行的,是这张由 ThreadState、Lead Agent 工厂和中间件链共同编织的防护网


一、Agent 核心设计理念

1.1 为什么选择 LangGraph 作为编排引擎

DeerFlow 的 Agent 核心不是从零构建的。它选择站在 LangGraph 的肩膀上——后者提供了图结构的状态机、消息流和节点编排能力。但 DeerFlow 没有止步于 LangGraph 的默认行为,而是做了一件关键的事:扩展状态、扩展消息流、在中间件层注入工业级防护

LangGraph 的 AgentState 只提供了最基础的 messages 列表。对于一个生产级 Agent 框架来说,这远远不够。你需要知道:

  • 当前对话运行在哪个沙箱里?
  • 用户上传了哪些文件?
  • 对话的标题是什么?
  • 上下文是不是该压缩了?
  • 模型是不是因为安全原因终止了响应?

DeerFlow 的答案是:保留 LangGraph 的图结构,但将一切"非消息"的状态和横切关注点抽入 ThreadState 和中间件链

这个选择背后有一个清晰的权衡:自研状态机和图引擎虽然能带来完全的掌控力,但代价是重新发明轮子——并发调度、消息校验、流式输出、断点恢复,每一项都需要巨大的工程投入。LangGraph 在这些问题上已经做了大量工作,而 DeerFlow 所做的,是在其提供的扩展点上填补工业级场景中的空白。这些空白包括:消息历史的并发安全更新(Reducer)、多轮对话中的工具权限隔离(技能过滤)、长上下文下的自动压缩(Summarization),以及生产环境中不可避免的异常恢复(ToolErrorHandling)。可以说,LangGraph 提供了舞台,而 DeerFlow 编排了整场演出。

1.2 Lead Agent 模式:单入口、多工具、多中间件

DeerFlow 的架构图可以简化为一个漏斗:

flowchart TD
    A[用户请求] --> B[Gateway API]
    B --> C[make_lead_agent]
    C --> D[Middleware Chain
20+ 层] D --> E[LangGraph Agent
模型 + 工具 + Prompt] E --> F[工具执行] F --> G[结果回传] G --> D D --> H[SSE 流式输出]

make_lead_agent 是唯一的入口。它不干具体的业务逻辑——而是编排一切:解析模型、构建工具列表、加载中间件链、注入系统提示。这种"工厂 + 中间件"的模式,让 Agent 的行为可以像搭积木一样组合:需要计划模式?加 TodoListMiddleware。需要多模态?加 ViewImageMiddleware。需要 token 限制?加 TokenBudgetMiddleware

这个设计的妙处在于"组合优于继承"。如果 DeerFlow 选择通过继承 LangGraph 的基类来扩展功能,那么每增加一个新特性都需要修改核心类,风险高、耦合紧。而中间件模式让每一个横切关注点都成为一个独立的插件,可以独立开发、独立测试、独立开关。更重要的是,中间件之间通过 LangGraph 的 AgentState 共享状态,而不是通过函数参数传递,这让整个链条的接口保持简洁——每个中间件只需要关心"读取什么状态"和"更新什么状态"。

1.3 与 ReAct 范式的关系

DeerFlow 并没有重新发明 ReAct。它的底层仍然是 LangGraph 的 create_agent,遵循标准的 ReAct 循环:模型思考 → 决定工具调用 → 执行工具 → 观察结果 → 继续思考。但 DeerFlow 在这个循环的外层包裹了 20+ 层中间件,在每一轮前后做干预:

  • before_model:压缩上下文(Summarization)、注入记忆(Memory)、检查预算(TokenBudget)
  • after_model:检测循环(LoopDetection)、处理安全终止(SafetyFinishReason)、修复悬空调用(DanglingToolCall)
  • wrap_tool_call:捕获工具异常(ToolErrorHandling)、审计沙箱(SandboxAudit)

这就是"在 LangGraph 之上封装"的精髓——不改变范式,但让范式更健壮。


二、ThreadState:超越 LangGraph 的 AgentState

LangGraph 的默认状态只包含 messages。DeerFlow 的 ThreadState 将其扩展为一个富状态机,承载了对话生命周期中所有需要持久化或跨轮传递的数据。

classDiagram
    class AgentState {
        +list messages
    }
    class ThreadState {
        +Annotated sandbox
        +NotRequired thread_data
        +NotRequired title
        +Annotated artifacts
        +Annotated todos
        +NotRequired uploaded_files
        +Annotated viewed_images
        +Annotated promoted
    }
    AgentState <|-- ThreadState

2.1 扩展字段详解

ThreadState 的每一个扩展字段都经过精心设计,且大多数都配有自定义的 Reducer——这是 LangGraph 并发安全的关键:

字段类型Reducer职责
sandboxSandboxStatemerge_sandbox沙箱环境信息,带 idempotent 写入校验
thread_dataThreadDataStateworkspace/uploads/outputs 虚拟路径
titlestr自动生成或用户指定的对话标题
artifactslist[str]merge_artifacts生成的文件列表,带去重和顺序保持
todoslistmerge_todos计划模式任务列表,显式更新优先
uploaded_fileslist[dict]用户上传的文件元数据
viewed_imagesdictmerge_viewed_imagesVision 模型图片数据,支持清空语义
promotedPromotedToolsmerge_promoted延迟工具升级(MCP tool_search)

2.2 Reducer 设计:为何不是简单的赋值

LangGraph 的图节点可能在同一轮中并行执行多个更新。如果没有 Reducer,后写的状态会覆盖先写的,导致数据丢失。DeerFlow 的 Reducer 设计体现了三种典型策略:

策略一:Idempotent 写入 + 冲突检测(sandbox)

def merge_sandbox(existing: SandboxState | None, new: SandboxState | None) -> SandboxState | None:
    if new is None:
        return existing
    if existing is None:
        return new
    existing_id = existing.get("sandbox_id")
    new_id = new.get("sandbox_id")
    if existing_id == new_id:
        return existing
    raise ValueError(f"Conflicting sandbox state updates: {existing_id!r} != {new_id!r}")

sandbox_id 的生命周期应该贯穿整个对话。如果两个节点在同一轮中写入了不同的 sandbox_id,说明存在严重的生命周期隔离 bug——DeerFlow 选择 fail closed,直接抛出异常,而不是静默覆盖。

策略二:合并 + 去重(artifacts)

def merge_artifacts(existing: list[str] | None, new: list[str] | None) -> list[str]:
    if existing is None:
        return new or []
    if new is None:
        return existing
    return list(dict.fromkeys(existing + new))

使用 dict.fromkeys 同时实现去重顺序保持——这是 Python 3.7+ 字典有序性的巧妙运用。多个节点可能同时生成文件,artifacts 需要安全聚合而不丢失任何一个。

策略三:作用域隔离 + 版本感知(promoted)

def merge_promoted(existing: PromotedTools | None, new: PromotedTools | None) -> PromotedTools | None:
    if not new:
        return existing
    if existing is None or existing.get("catalog_hash") != new["catalog_hash"]:
        return {"catalog_hash": new["catalog_hash"], "names": list(dict.fromkeys(new["names"]))}
    return {
        "catalog_hash": existing["catalog_hash"],
        "names": list(dict.fromkeys(existing["names"] + new["names"])),
    }

promoted 用于 MCP 工具的延迟加载(tool_search)。如果 catalog_hash 变了(说明 MCP 工具目录发生了漂移),旧的 promoted names 会被全部丢弃,避免"同名不同工具"的安全隐患。


三、Lead Agent 工厂 _make_lead_agent

如果说 ThreadState 是"状态骨架",那么 _make_lead_agent 就是"组装流水线"。它把模型、工具、中间件和 Prompt 拼接成一个可运行的 LangGraph Agent。

3.1 模型解析:create_chat_model + Tracing 回调

def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
    # 1. 从运行时配置中提取参数
    cfg = _get_runtime_config(config)
    requested_model_name = cfg.get("model_name") or cfg.get("model")
    is_plan_mode = cfg.get("is_plan_mode", False)
    subagent_enabled = cfg.get("subagent_enabled", False)
    
    # 2. 解析模型(支持 fallback 到默认模型)
    model_name = _resolve_model_name(requested_model_name, app_config=resolved_app_config)
    
    # 3. 在 graph 根节点注入 tracing 回调(Langfuse/LangSmith)
    tracing_callbacks = build_tracing_callbacks()
    if tracing_callbacks:
        config["callbacks"] = [*existing, *tracing_callbacks]
    
    # 4. 创建 Agent
    return create_agent(
        model=create_chat_model(name=model_name, attach_tracing=False),
        tools=final_tools,
        middleware=build_middlewares(config, model_name, ...),
        system_prompt=apply_prompt_template(...),
        state_schema=ThreadState,
    )

注意一个关键的不变量(invariant):tracing 回调只在 graph 根节点注入一次,而所有 create_chat_model 内部调用都必须传 attach_tracing=False。如果忘记这个标志,会产生重复的 span,并且 session_id / user_id 无法正确传播到 trace 上。

3.2 工具注册:三层来源 + 技能过滤

DeerFlow 的工具来源有三个层级:

flowchart LR
    A[Built-in Tools] --> D[技能过滤 + 延迟加载]
    B[Configured Tools] --> D
    C[MCP Tools] --> D
    D --> E[最终工具列表]
  1. Built-inbashread_filewrite_filepresent_files 等框架内建工具
  2. Configured:在 config.yaml 中显式配置的工具(如 web_searchweb_fetch
  3. MCP:通过 extensions_config.json 动态接入的外部工具服务器

组装后,工具列表会经过 filter_tools_by_skill_allowed_tools 过滤——只有当前技能白名单允许的工具才会暴露给模型。这一步是安全隔离的关键:不同 Agent 可以拥有不同的工具权限。

3.3 Bootstrap Agent 与 Custom Agent 的区别

_make_lead_agent 支持两种模式:Bootstrap Agent 和 Custom Agent。前者是一个极简的入口 Agent,只拥有 setup_agent 工具和最基本的 bootstrap 技能,用于用户创建自己的自定义 Agent。它的工具列表被故意限制得很窄,以确保 Agent 创建流程在自定义配置存在之前就具有确定性。

Custom Agent 则是正常的生产模式。它根据 agent_nameconfig.yaml 加载对应的配置(SOUL.md 个性设定、技能白名单、工具分组、默认模型),并通过 update_agent 工具支持自更新。这种"先有一个极简的创建者,再有一个丰富的执行者"的双层设计,让 DeerFlow 的 Agent 生态可以自我扩展——用户不需要改代码就能创建新的 Agent 角色。

3.3 运行时配置合并:_get_runtime_config

def _get_runtime_config(config: RunnableConfig) -> dict:
    cfg = dict(config.get("configurable", {}) or {})
    context = config.get("context", {}) or {}
    if isinstance(context, dict):
        cfg.update(context)
    return cfg

这个简单的函数体现了 DeerFlow 对配置来源的兼容策略:configurable(LangGraph 标准)+ context(遗留/扩展),后者优先级更高。这样用户既可以通过标准 LangGraph 客户端传参,也可以通过自定义 Gateway 接口传参。


四、中间件链全景(20+ 层)

这是 DeerFlow Agent 最核心的工程创新之一。20+ 层中间件按严格的顺序组装,每一层只负责一个横切关注点,像洋葱一样包裹在核心 ReAct 循环之外。

4.1 中间件执行顺序图

flowchart LR
    subgraph RuntimeBase [运行时基础层]
        A1[InputSanitizationMiddleware] --> A2[ToolOutputBudgetMiddleware]
        A2 --> A3[ThreadDataMiddleware]
        A3 --> A4[UploadsMiddleware]
        A4 --> A5[SandboxMiddleware]
        A5 --> A6[DanglingToolCallMiddleware]
        A6 --> A7[LLMErrorHandlingMiddleware]
        A7 --> A8[GuardrailMiddleware]
        A8 --> A9[SandboxAuditMiddleware]
        A9 --> A10[ToolErrorHandlingMiddleware]
    end
    
    subgraph LeadOnly [Lead Agent 专属层]
        B1[DynamicContextMiddleware] --> B2[SkillActivationMiddleware]
        B2 --> B3[SummarizationMiddleware]
        B3 --> B4[TodoListMiddleware]
        B4 --> B5[TokenUsageMiddleware]
        B5 --> B6[TitleMiddleware]
        B6 --> B7[MemoryMiddleware]
        B7 --> B8[ViewImageMiddleware]
        B8 --> B9[DeferredToolFilterMiddleware]
        B9 --> B10[SystemMessageCoalescingMiddleware]
        B10 --> B11[SubagentLimitMiddleware]
        B11 --> B12[LoopDetectionMiddleware]
        B12 --> B13[TokenBudgetMiddleware]
        B13 --> B14[SafetyFinishReasonMiddleware]
        B14 --> B15[ClarificationMiddleware]
    end
    
    A1 --> B1

4.2 中间件职责表

序号中间件核心职责触发条件关键配置项
1InputSanitizationMiddleware输入消毒、防止 prompt injection每轮请求max_input_length
2ToolOutputBudgetMiddleware限制工具输出长度工具返回后max_tool_output_tokens
3ThreadDataMiddleware初始化工作目录结构首次运行lazy_init
4UploadsMiddleware注入用户上传文件列表有上传文件时
5SandboxMiddleware获取/创建沙箱环境首次运行sandbox_provider
6DanglingToolCallMiddleware修复悬空的工具调用(缺少 ToolMessage)每轮请求
7LLMErrorHandlingMiddleware捕获 LLM 调用异常,支持重试模型调用失败max_retries
8GuardrailMiddleware内容安全护栏(输入/输出过滤)每轮请求provider, fail_closed
9SandboxAuditMiddleware审计沙箱操作日志工具执行后
10ToolErrorHandlingMiddleware将工具异常转为错误 ToolMessage工具执行抛异常
11DynamicContextMiddleware注入当前日期、记忆到首条 HumanMessage每轮请求memory.enabled
12SkillActivationMiddleware处理 /skill-name 显式技能激活用户消息以 / 开头available_skills
13SummarizationMiddleware上下文压缩、保留策略触发 token/消息数阈值trigger, keep
14TodoListMiddleware计划模式任务跟踪is_plan_mode=Truesystem_prompt
15TokenUsageMiddleware记录 token 用量统计每轮请求
16TitleMiddleware自动生成对话标题首次对话后
17MemoryMiddleware长期记忆注入memory.enabledmax_injection_tokens
18ViewImageMiddleware多模态图片处理(base64 注入)模型支持 vision
19DeferredToolFilterMiddleware隐藏延迟工具 schematool_search.enableddeferred_names
20SystemMessageCoalescingMiddleware合并多个 SystemMessage 为单个每轮请求
21SubagentLimitMiddleware限制并行子 Agent 数量subagent_enabled=Truemax_concurrent
22LoopDetectionMiddleware检测重复工具调用循环每轮请求warn_threshold, hard_limit
23TokenBudgetMiddleware强制单轮 token 上限每轮请求max_tokens_per_run
24SafetyFinishReasonMiddleware拦截安全终止的 tool_callsfinish_reason=content_filterdetectors
25ClarificationMiddleware处理澄清请求、中断执行调用 ask_clarification

这个表格揭示了一个关键设计原则:每个中间件只做一件事,且按依赖关系有序排列。例如 SummarizationMiddleware 必须放在 DynamicContextMiddleware 之后,因为压缩需要保留动态注入的上下文提醒;SafetyFinishReasonMiddleware 必须放在 LoopDetectionMiddleware 之后,因为安全拦截应该优先于循环检测,避免被截断的 tool_calls 被计入循环。

特别值得解释的是 SystemMessageCoalescingMiddleware 的位置。许多严格的推理后端(如 vLLM、SGLang、通义千问、Anthropic)要求只有一个 SystemMessage,且必须位于消息列表的最开头。但 DeerFlow 的多个中间件(如 DynamicContextMiddlewareSkillActivationMiddlewareTodoListMiddleware)都可能注入 SystemMessage,导致消息列表中出现多个。SystemMessageCoalescingMiddleware 作为倒数第二层,在请求到达模型之前将所有 SystemMessage 合并为单个,确保了对各种推理后端的兼容性。这种"先分散注入、后统一合并"的策略,既保持了中间件的独立性,又满足了后端的约束。

4.3 中间件链中的数据流

从数据流的角度看,中间件链的工作方式可以用一个更细致的序列图来描述:

sequenceDiagram
    participant User as 用户
    participant Gateway as Gateway API
    participant Middleware as 中间件链
    participant LangGraph as LangGraph Agent
    participant Tools as 工具节点
    
    User->>Gateway: 发送消息
    Gateway->>Middleware: 构建初始状态
    Middleware->>Middleware: before_agent(清理循环状态)
    Middleware->>Middleware: before_model(压缩上下文、注入记忆)
    Middleware->>LangGraph: wrap_model_call(发送请求)
    LangGraph->>Middleware: after_model(模型输出)
    Middleware->>Middleware: SafetyFinishReason(拦截安全终止)
    Middleware->>Middleware: LoopDetection(检测循环)
    Middleware->>Tools: wrap_tool_call(执行工具)
    Tools->>Middleware: 返回结果
    Middleware->>Middleware: after_agent(清理状态)
    Middleware->>Gateway: 返回更新后的状态
    Gateway->>User: SSE 流式输出

这个序列图展示了中间件在 Agent 执行周期中的精确介入时机。每个钩子都有明确的语义边界:before_agent 在图节点运行前执行(适合全局清理),before_model 在模型调用前执行(适合上下文处理),after_model 在模型输出后执行(适合响应校验),wrap_model_call 包裹整个模型调用(适合重试和注入),wrap_tool_call 包裹单个工具调用(适合错误处理和审计)。


五、关键中间件深度解析

5.1 SummarizationMiddleware:上下文压缩的精细手术

当对话历史越来越长,token 预算逼近红线时,SummarizationMiddleware 启动"上下文压缩"。但它的行为远比"把旧消息删掉"复杂——它是一场精细的保留手术

三层触发机制

  1. Token 触发:当消息总 token 数超过阈值(如 100K)
  2. 消息数触发:当消息数量超过阈值(如 100 条)
  3. 比例触发:当消息数占总上下文窗口的比例超过阈值(如 0.8)
def _maybe_summarize(self, state: AgentState, runtime: Runtime) -> dict | None:
    messages = state["messages"]
    total_tokens = self.token_counter(messages)
    if not self._should_summarize(messages, total_tokens):
        return None
    
    cutoff_index = self._determine_cutoff_index(messages)
    messages_to_summarize, preserved = self._partition_with_skill_rescue(messages, cutoff_index)
    summary = self._create_summary(messages_to_summarize)
    new_messages = self._build_new_messages(summary)
    
    return {
        "messages": [
            RemoveMessage(id=REMOVE_ALL_MESSAGES),  # 清空旧消息
            *new_messages,                           # 注入摘要
            *preserved,                              # 保留最近消息
        ]
    }

Skill Rescue 机制:这是 DeerFlow 的独创设计。压缩时,如果检测到最近的消息包含技能加载(read_file 读取了 /mnt/skills/ 下的文件),这些技能相关的 AIMessage + ToolMessage 对会被优先保留。因为技能是 Agent 执行任务的"说明书",丢失它们会导致 Agent 在后续回合中忘记自己已经加载了哪些工作流。

def _preserve_dynamic_context_reminders(self, messages_to_summarize, preserved):
    """Keep hidden dynamic-context reminders out of summary compression."""
    reminders = [msg for msg in messages_to_summarize if is_dynamic_context_reminder(msg)]
    remaining = [msg for msg in messages_to_summarize if not is_dynamic_context_reminder(msg)]
    return remaining, reminders + preserved

另外,动态上下文提醒(当前日期、记忆)也会被单独保留,避免 DynamicContextMiddleware 误判摘要消息为"第一条用户消息"而错误注入。

5.2 LoopDetectionMiddleware:如何检测和打破循环

这是 DeerFlow 的"防卡死"保险。Agent 陷入循环的典型场景包括:

  • 对同一文件反复 read_file(参数完全相同)
  • 对同一目录反复 ls(参数相同)
  • 跨文件循环:read_file 了 40 个不同的文件,每个都是独立的参数,但行为本质相同

DeerFlow 使用了双层检测

Layer 1:Hash 检测(精确匹配)

def _hash_tool_calls(tool_calls: list[dict]) -> str:
    normalized = []
    for tc in tool_calls:
        name = tc.get("name", "")
        args, fallback_key = _normalize_tool_call_args(tc.get("args", {}))
        key = _stable_tool_key(name, args, fallback_key)
        normalized.append(f"{name}:{key}")
    normalized.sort()
    blob = json.dumps(normalized, sort_keys=True, default=str)
    return hashlib.md5(blob.encode()).hexdigest()[:12]

对于 read_filestable_tool_key 做了特殊处理:按 200 行分桶,避免同一文件不同行号范围被当成不同调用。对于 write_file / str_replace,则使用完整参数哈希,因为内容变化确实代表不同意图。

Layer 2:频率检测(模糊匹配)

即使参数完全不同,如果 read_file 被调用了 30 次以上,也会触发频率警告。这个阈值对特定工具可覆盖(如 bash 在批处理管道中可能需要更高的容忍度)。

警告注入的时机选择:这是一个精妙的工程决策。after_model 检测到循环后,不能立即插入警告消息——因为此时工具节点还没执行,ToolMessage 尚未产生。如果在这里插入消息,会打破 OpenAI/Moonshot 的 "tool_calls 必须紧跟 ToolMessage" 的校验。DeerFlow 的解决方案是:将警告加入队列,在下一个 wrap_model_call 中注入,此时所有 ToolMessage 已经就位,警告可以安全地追加到消息列表末尾。

def _augment_request(self, request: ModelRequest) -> ModelRequest:
    warnings = self._drain_pending_warnings(request.runtime)
    if not warnings:
        return request
    new_messages = [
        *request.messages,
        HumanMessage(content=self._format_warning_message(warnings), name="loop_warning"),
    ]
    return request.override(messages=new_messages)

5.3 ToolErrorHandlingMiddleware:strict tool-call recovery

当工具执行抛出异常时,最糟糕的行为是让整个 Agent 运行崩溃。ToolErrorHandlingMiddleware 的策略是:捕获所有异常,转化为结构化的错误 ToolMessage,让模型有机会自我修复

def _build_error_message(self, request: ToolCallRequest, exc: Exception) -> ToolMessage:
    tool_name = str(request.tool_call.get("name") or "unknown_tool")
    detail = str(exc).strip() or exc.__class__.__name__
    if len(detail) > 500:
        detail = detail[:497] + "..."
    
    content = f"Error: Tool '{tool_name}' failed with {exc.__class__.__name__}: {detail}. Continue with available context, or choose an alternative tool."
    return ToolMessage(
        content=content,
        tool_call_id=tool_call_id,
        name=tool_name,
        status="error",
    )

注意两个关键细节:

  1. 错误长度截断到 500 字符:避免异常详情(如巨大的 stack trace)污染上下文窗口。一个典型的 Docker 沙箱错误可能包含数百行的环境变量和容器状态输出,如果不截断,模型可能在下一个回合被这些噪音淹没,做出错误的决策。
  2. 保留 GraphBubbleUp:LangGraph 的控制流信号(interrupt/pause/resume)不会被误拦截,这是框架级异常和业务异常的严格区分。工具执行失败是"业务异常",应该被捕获并转化为 ToolMessage;但 LangGraph 的图控制流(如用户暂停执行)是"框架异常",需要原样向上传递。

对于 task 工具(子 Agent),还有额外的 subagent_status 打戳逻辑——前端依赖这个结构化字段来渲染子 Agent 状态卡片,而不是从文本前缀中猜测。这体现了 DeerFlow 在 API 设计上的"前后端分离"思维:后端提供结构化数据,前端负责渲染呈现,而不是让后端输出带格式的文本供前端解析。


六、Prompt 工程化

6.1 系统提示的构建:基础 Prompt + 技能元数据 + 当前激活技能

DeerFlow 的系统提示不是静态字符串,而是通过 apply_prompt_template 函数动态组装的。它的模板结构如下:

<role>
    You are {agent_name}, an open-source super agent.
</role>

{soul}                          ← 从 SOUL.md 加载的个性化设定
{self_update_section}           ← 自定义 Agent 的自更新说明
<thinking_style>                ← 思考风格引导(澄清优先、分解任务)
<clarification_system>          ← 强制澄清流程(5 种场景)
{skills_section}                ← 可用技能列表(渐进式加载说明)
{deferred_tools_section}        ← 延迟工具(MCP tool_search)
{subagent_section}              ← 子 Agent 并发限制与使用策略
<working_directory>             ← 工作目录说明(sandbox 路径)
<response_style>                ← 输出风格(自然段落、避免过度格式化)
<citations>                     ← 引用格式强制要求
<critical_reminders>            ← 关键提醒(澄清优先、技能优先、文件编辑规范)

6.2 渐进式技能加载:为何只加载需要的技能

DeerFlow 的一个核心设计是技能不一次性全部注入系统提示。它只在系统提示中列出技能名称、描述和文件路径,然后要求 Agent 在需要时显式调用 read_file 加载 SKILL.md

<skill_system>
**Progressive Loading Pattern:**
1. When a user query matches a skill's use case, immediately call `read_file` on the skill's main file
2. Read and understand the skill's workflow and instructions
3. Load referenced resources only when needed during execution
4. Follow the skill's instructions precisely
</skill_system>

这种"渐进式加载"有两个好处:

  1. 节省上下文:避免 20 个技能的完整内容挤占 token 预算
  2. 按需激活:只有匹配的技能才会进入工作内存,减少模型混淆

举个例子:假设用户询问"如何优化 Python 代码性能"。系统提示中可能列出了 15 个技能,包括 PDF 处理、前端设计、数据库查询优化等。如果一次性注入所有技能的完整内容,不仅浪费 token,还可能让模型在无关技能之间"分心"。渐进式加载确保只有"python-performance"技能被加载,其他技能保持静默。

同时,用户也可以通过 /<skill-name> 前缀显式激活某个技能。SkillActivationMiddleware 会检测到这种激活模式,将对应的 SKILL.md 内容直接注入到 HumanMessage 中, bypass 渐进式加载。这种显式激活的优先级高于模型端的自动匹配,避免了"模型以为不需要加载,但用户明确想要"的错位。

6.3 动态上下文与静态系统提示的分离

DeerFlow 的 Prompt 工程化中有一个容易被忽视但极其重要的设计:系统提示(system prompt)保持完全静态,而动态内容(日期、记忆)通过 DynamicContextMiddleware 注入到首条 HumanMessage 中

为什么这样做?因为大多数 LLM 提供商(如 OpenAI、Anthropic)都支持前缀缓存(prefix caching)——如果系统提示在不同请求之间保持不变,推理框架可以复用已计算的 KV Cache,显著降低延迟和成本。如果每轮都把"今天是 2026-06-24"放进系统提示,这个缓存就会失效。DeerFlow 的系统提示(apply_prompt_template 的返回值)在 Agent 生命周期中是静态的,只有 DynamicContextMiddleware 在每轮开始时将日期和记忆注入到第一个 HumanMessage 中。

# DynamicContextMiddleware 将动态内容注入 HumanMessage
# 而不是修改 system prompt
<system-reminder>
Current date: 2026-06-24
<memory>用户偏好...</memory>
</system-reminder>
--- BEGIN USER INPUT ---
用户实际输入
--- END USER INPUT ---

这种"静态系统提示 + 动态 HumanMessage 注入"的分离策略,在对话轮次很多时尤其有价值——它让系统提示始终处于前缀缓存的命中路径上,即使 SummarizationMiddleware 压缩了消息历史,静态的 system prompt 仍然可以复用。

6.4 多模态支持:图片注入与文件引用

对于支持 vision 的模型,ViewImageMiddleware 会检查用户消息中引用的图片,将其转换为 base64 编码注入到 viewed_images 状态中。模型可以通过工具调用查看这些图片,而不需要直接处理文件路径。

UploadsMiddleware 则负责在每一轮开始时,将用户上传的文件列表以结构化的 XML 格式注入到消息中,确保 Agent 知道有哪些文件可用、它们的路径是什么。文件引用格式如下:

<uploaded_files>
    <file>
        <filename>report.pdf</filename>
        <path>/mnt/user-data/uploads/report.pdf</path>
        <converted_md>/mnt/user-data/uploads/report.md</converted_md>
    </file>
</uploaded_files>

注意 PDF、PPT、Excel 等文档会自动生成 Markdown 转换版本(通过 markitdown),Agent 可以直接读取 .md 版本获取文本内容,而不需要解析二进制格式。这是 DeerFlow 文件处理管道的一个重要细节。


总结

graph TD
    A[DeerFlow Agent 核心] --> B[ThreadState
状态扩展 + Reducer 安全] A --> C[Lead Agent 工厂
模型 + 工具 + 中间件 + Prompt] A --> D[20+ 层中间件链
分层防护 + 有序编排] B --> E[LangGraph AgentState 之上
sandbox/artifacts/todos/viewed_images] C --> F[create_agent
单入口工厂模式] D --> G[Summarization
上下文压缩 + Skill Rescue] D --> H[LoopDetection
双层检测 + 强制终止] D --> I[ToolErrorHandling
异常转 ToolMessage + 自愈] D --> J[SafetyFinishReason
安全终止拦截 + 审计] style A fill:#ff6b6b,stroke:#333,stroke-width:2px,color:#fff style D fill:#4ecdc4,stroke:#333,stroke-width:2px,color:#fff

核心要点

  1. ThreadState 是生产级 Agent 的基石:LangGraph 默认的 messages 列表远远不够。sandboxartifactsviewed_images 等扩展字段,配合 idempotent 的 Reducer 设计,让并发安全的状态更新成为可能。

  2. 中间件链是有序的防护洋葱:20+ 层中间件按严格的依赖顺序排列,从输入消毒(最外层)到澄清处理(最内层),每一层只处理一个横切关注点。顺序不是随意的——SummarizationMiddleware 必须在 DynamicContextMiddleware 之后,SafetyFinishReasonMiddleware 必须在 LoopDetectionMiddleware 之前。

  3. Prompt 是"引导"而非"灌输":DeerFlow 的系统提示使用渐进式技能加载、强制澄清流程、结构化引用格式等工程化手段,引导 Agent 行为而非试图通过巨量文本来控制它。配合 DynamicContextMiddleware 将动态内容(日期、记忆)从系统提示中剥离到首条 HumanMessage,实现了系统提示的前缀缓存复用

  4. 异常不是终点,而是自愈的契机ToolErrorHandlingMiddleware 将工具异常转化为结构化的 ToolMessageLoopDetectionMiddleware 在检测到循环时注入警告而非直接崩溃——这些设计让 Agent 在复杂场景下具备优雅降级的能力。

小王后来在周报里写道:"DeerFlow 的 20+ 层中间件不是过度工程,而是工业级 Agent 的底线。每一个中间件背后,都是一个真实场景中踩过的坑。"


参考链接:


本章完。下一篇:DeerFlow Skills 与 Tools 系统——可扩展的 Agent 能力生态。