这是「DeerFlow 源码深度解析」专题的第 03 篇。在本系列中,我们将逐层拆解 DeerFlow 的 Agent 核心——从 ThreadState 的状态扩展,到 20+ 层中间件链的编排哲学,再到 Prompt 模板的工程化设计,带你一览工业级 AI Agent 的防护与编排之道。
故事场景:当 Agent 陷入无限循环
凌晨 1:23,某技术团队的内部 Agent 平台监控告警突然炸响。
小王——负责运维 AI Agent 的值班工程师——看着飙升的 Token 用量曲线,手心直冒汗。一个用户的对话线程在短短 15 分钟内消耗了 47 万 Token,而对话内容只是简单的"帮我总结一下这个目录下的代码结构"。
"又是循环,"他喃喃自语,"Agent 在重复调用 read_file,读完 src/main.py 读 src/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 <|-- ThreadState2.1 扩展字段详解
ThreadState 的每一个扩展字段都经过精心设计,且大多数都配有自定义的 Reducer——这是 LangGraph 并发安全的关键:
| 字段 | 类型 | Reducer | 职责 |
|---|---|---|---|
sandbox | SandboxState | merge_sandbox | 沙箱环境信息,带 idempotent 写入校验 |
thread_data | ThreadDataState | 无 | workspace/uploads/outputs 虚拟路径 |
title | str | 无 | 自动生成或用户指定的对话标题 |
artifacts | list[str] | merge_artifacts | 生成的文件列表,带去重和顺序保持 |
todos | list | merge_todos | 计划模式任务列表,显式更新优先 |
uploaded_files | list[dict] | 无 | 用户上传的文件元数据 |
viewed_images | dict | merge_viewed_images | Vision 模型图片数据,支持清空语义 |
promoted | PromotedTools | merge_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[最终工具列表]- Built-in:
bash、read_file、write_file、present_files等框架内建工具 - Configured:在
config.yaml中显式配置的工具(如web_search、web_fetch) - 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_name 从 config.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 --> B14.2 中间件职责表
| 序号 | 中间件 | 核心职责 | 触发条件 | 关键配置项 |
|---|---|---|---|---|
| 1 | InputSanitizationMiddleware | 输入消毒、防止 prompt injection | 每轮请求 | max_input_length |
| 2 | ToolOutputBudgetMiddleware | 限制工具输出长度 | 工具返回后 | max_tool_output_tokens |
| 3 | ThreadDataMiddleware | 初始化工作目录结构 | 首次运行 | lazy_init |
| 4 | UploadsMiddleware | 注入用户上传文件列表 | 有上传文件时 | 无 |
| 5 | SandboxMiddleware | 获取/创建沙箱环境 | 首次运行 | sandbox_provider |
| 6 | DanglingToolCallMiddleware | 修复悬空的工具调用(缺少 ToolMessage) | 每轮请求 | 无 |
| 7 | LLMErrorHandlingMiddleware | 捕获 LLM 调用异常,支持重试 | 模型调用失败 | max_retries |
| 8 | GuardrailMiddleware | 内容安全护栏(输入/输出过滤) | 每轮请求 | provider, fail_closed |
| 9 | SandboxAuditMiddleware | 审计沙箱操作日志 | 工具执行后 | 无 |
| 10 | ToolErrorHandlingMiddleware | 将工具异常转为错误 ToolMessage | 工具执行抛异常 | 无 |
| 11 | DynamicContextMiddleware | 注入当前日期、记忆到首条 HumanMessage | 每轮请求 | memory.enabled |
| 12 | SkillActivationMiddleware | 处理 /skill-name 显式技能激活 | 用户消息以 / 开头 | available_skills |
| 13 | SummarizationMiddleware | 上下文压缩、保留策略 | 触发 token/消息数阈值 | trigger, keep |
| 14 | TodoListMiddleware | 计划模式任务跟踪 | is_plan_mode=True | system_prompt |
| 15 | TokenUsageMiddleware | 记录 token 用量统计 | 每轮请求 | 无 |
| 16 | TitleMiddleware | 自动生成对话标题 | 首次对话后 | 无 |
| 17 | MemoryMiddleware | 长期记忆注入 | memory.enabled | max_injection_tokens |
| 18 | ViewImageMiddleware | 多模态图片处理(base64 注入) | 模型支持 vision | 无 |
| 19 | DeferredToolFilterMiddleware | 隐藏延迟工具 schema | tool_search.enabled | deferred_names |
| 20 | SystemMessageCoalescingMiddleware | 合并多个 SystemMessage 为单个 | 每轮请求 | 无 |
| 21 | SubagentLimitMiddleware | 限制并行子 Agent 数量 | subagent_enabled=True | max_concurrent |
| 22 | LoopDetectionMiddleware | 检测重复工具调用循环 | 每轮请求 | warn_threshold, hard_limit |
| 23 | TokenBudgetMiddleware | 强制单轮 token 上限 | 每轮请求 | max_tokens_per_run |
| 24 | SafetyFinishReasonMiddleware | 拦截安全终止的 tool_calls | finish_reason=content_filter | detectors |
| 25 | ClarificationMiddleware | 处理澄清请求、中断执行 | 调用 ask_clarification | 无 |
这个表格揭示了一个关键设计原则:每个中间件只做一件事,且按依赖关系有序排列。例如 SummarizationMiddleware 必须放在 DynamicContextMiddleware 之后,因为压缩需要保留动态注入的上下文提醒;SafetyFinishReasonMiddleware 必须放在 LoopDetectionMiddleware 之后,因为安全拦截应该优先于循环检测,避免被截断的 tool_calls 被计入循环。
特别值得解释的是 SystemMessageCoalescingMiddleware 的位置。许多严格的推理后端(如 vLLM、SGLang、通义千问、Anthropic)要求只有一个 SystemMessage,且必须位于消息列表的最开头。但 DeerFlow 的多个中间件(如 DynamicContextMiddleware、SkillActivationMiddleware、TodoListMiddleware)都可能注入 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 启动"上下文压缩"。但它的行为远比"把旧消息删掉"复杂——它是一场精细的保留手术。
三层触发机制:
- Token 触发:当消息总 token 数超过阈值(如 100K)
- 消息数触发:当消息数量超过阈值(如 100 条)
- 比例触发:当消息数占总上下文窗口的比例超过阈值(如 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_file,stable_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",
)注意两个关键细节:
- 错误长度截断到 500 字符:避免异常详情(如巨大的 stack trace)污染上下文窗口。一个典型的 Docker 沙箱错误可能包含数百行的环境变量和容器状态输出,如果不截断,模型可能在下一个回合被这些噪音淹没,做出错误的决策。
- 保留
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>这种"渐进式加载"有两个好处:
- 节省上下文:避免 20 个技能的完整内容挤占 token 预算
- 按需激活:只有匹配的技能才会进入工作内存,减少模型混淆
举个例子:假设用户询问"如何优化 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核心要点
ThreadState 是生产级 Agent 的基石:LangGraph 默认的
messages列表远远不够。sandbox、artifacts、viewed_images等扩展字段,配合 idempotent 的 Reducer 设计,让并发安全的状态更新成为可能。中间件链是有序的防护洋葱:20+ 层中间件按严格的依赖顺序排列,从输入消毒(最外层)到澄清处理(最内层),每一层只处理一个横切关注点。顺序不是随意的——
SummarizationMiddleware必须在DynamicContextMiddleware之后,SafetyFinishReasonMiddleware必须在LoopDetectionMiddleware之前。Prompt 是"引导"而非"灌输":DeerFlow 的系统提示使用渐进式技能加载、强制澄清流程、结构化引用格式等工程化手段,引导 Agent 行为而非试图通过巨量文本来控制它。配合
DynamicContextMiddleware将动态内容(日期、记忆)从系统提示中剥离到首条 HumanMessage,实现了系统提示的前缀缓存复用。异常不是终点,而是自愈的契机:
ToolErrorHandlingMiddleware将工具异常转化为结构化的ToolMessage,LoopDetectionMiddleware在检测到循环时注入警告而非直接崩溃——这些设计让 Agent 在复杂场景下具备优雅降级的能力。
小王后来在周报里写道:"DeerFlow 的 20+ 层中间件不是过度工程,而是工业级 Agent 的底线。每一个中间件背后,都是一个真实场景中踩过的坑。"
参考链接:
- DeerFlow 官方仓库:https://github.com/bytedance/deer-flow
- LangGraph 文档:https://langchain-ai.github.io/langgraph/
- MCP 协议文档:https://modelcontextprotocol.io
本章完。下一篇:DeerFlow Skills 与 Tools 系统——可扩展的 Agent 能力生态。