跳到主要内容

DeerFlow — 架构与原理

30 秒导读: DeerFlow 2.0 是字节开源的「超级 agent 底座」(super agent harness)。你给它一句话——「查一下腾讯股价为什么跌,写份报告」——它会自己拆成几路并行的子任务,每路子 agent 在隔离的上下文 + 自己的沙箱里搜网页、读文件、跑命令,最后把结果汇总成一篇报告(或一个网页、一套幻灯片)。它没有复杂的「规划图」:骨架就是一张扁平的 LangGraph ReAct 图,所有工程含量都藏在一条有序的 middleware 链和三个子系统里。本文只讲原理与精华,不讲怎么部署。


1. 这是什么(零基础也能懂)

一句话定义: DeerFlow 是一个基于 LangGraph 的 deep-research / super-agent 运行时——把「深度研究」需要的一切(文件系统、记忆、沙箱、技能、子 agent)打包成一个开箱即用、又可拆改的底座。

解决什么问题 / 给谁用:

假设你想让 AI 干这种活:

「调研一下 5 家云厂商的定价和优劣,给我一份带表格的对比报告。」

这件事一次问答做不完:要发很多次搜索、读很多网页、边读边记、最后组织成文。单个大模型上下文塞不下这么多材料,塞进去也会「越读越糊」。DeerFlow 的答案是分而治之 + 上下文工程:把大任务拆成几路,每路开一个上下文互相看不见的子 agent 去啃,主 agent 只收「干净的结论」再汇总。它面向想搭「能干活几分钟到几小时的 agent」的开发者和研究者。

它能做什么:

  • 深度研究并产出报告 / 网页 / 幻灯片 / 图像(靠可插拔的 Skills)。
  • 主 agent 按需派生子 agent(sub-agents),并行探索、各自隔离上下文。
  • 每个任务有自己的沙箱和文件系统:能读写文件、看图片、(配置安全时)执行 shell。
  • 跨会话的长期记忆:记住你的偏好、技术栈、常用工作流。
  • 模型无关:任何 OpenAI 兼容 API 都能跑。

用起来什么样:

最能说明本质的是它的内嵌 Python 客户端——不启 HTTP 服务,直接在进程里跑同一套 agent(摘自 backend/README.mdDeerFlowClient 用法):

# 示意,非源码:内嵌模式下,一句 chat 背后就是一整张 LangGraph 图在转
from deerflow.client import DeerFlowClient

client = DeerFlowClient()
response = client.chat("帮我分析这篇论文", thread_id="my-thread") # 同步:累积流式增量,返回最终文本

for event in client.stream("hello"): # 流式:LangGraph SSE 协议
if event.type == "messages-tuple" and event.data.get("type") == "ai":
print(event.data["content"]) # AI 文本是「增量」,按 id 拼接

一句话直觉 / 类比: 把它想成一个项目经理(lead agent)带一个可随时扩编的临时团队。经理不亲自钻进每个细节——他把活拆成几张工单(task),发给几个互不通气、各自带笔记本和工位的外包(subagent);外包干完只交回一段结论,经理再把几段结论缝成最终交付物。经理的「记忆」是一张始终不变的岗位说明书(静态系统提示)+ 一张每天更新的便利贴(<system-reminder>)。


2. 顶层全景(它大概怎么转)

先认清一件反直觉的事:DeerFlow 的图是「扁平」的。没有 manager/planner/executor 这样的多节点规划图。整个 lead agent 就是 LangChain 的一个 create_agent(...) ——即经典的「模型 ↔ 工具」ReAct 循环。真正的复杂度不在图的拓扑,而在套在这个循环外面的一长串 middleware(build_middlewares,packages/harness/deerflow/agents/lead_agent/agent.py:270)和它能调用的工具/子系统

图注:下面是一次请求的控制流。从上到下是一轮 ReAct;middleware 链像洋葱一样包在「模型调用」外层,每层在调模型前后各插一手。

主要部件一句话职责:

部件干什么在哪个文件(符号)
make_lead_agentLangGraph 图工厂,langgraph.json 里注册的唯一入口packages/harness/deerflow/agents/lead_agent/agent.py:416
create_deerflow_agentSDK 级纯参数工厂,用 RuntimeFeatures 声明式拼 middlewarepackages/harness/deerflow/agents/factory.py:61
build_middlewares组装 lead agent 那条 20+ 层有序 middleware 链.../lead_agent/agent.py:270
get_available_tools汇总 config 工具 + MCP + 内置 + 子 agent 工具packages/harness/deerflow/tools/tools.py:44
apply_prompt_template生成完全静态的系统提示(技能只放元数据).../lead_agent/prompt.py:779
task_tool把一个子任务委派给子 agent,并在后端轮询到底packages/harness/deerflow/tools/builtins/task_tool.py:187
SubagentExecutor子 agent 的后台执行引擎(线程池 + 独立事件循环)packages/harness/deerflow/subagents/executor.py:278
DeerFlowClient内嵌客户端,和 Gateway 走同一套图packages/harness/deerflow/client.py

主线走一遍(高层,不进代码):

  1. Gateway 或内嵌客户端拿到消息 → 调 make_lead_agent(config) 造出这一轮的图(agent.py:416)。
  2. 工厂按 config.configurable 里的 model_name / thinking_enabled / subagent_enabled 等,选模型、装工具、拼 middleware(agent.py:429-447)。
  3. create_agent(...) 编出一张扁平 ReAct 图(agent.py:534),开始「模型 ↔ 工具」循环。
  4. 模型每一步要么直接回话,要么调工具:调 bash/read_file 落到沙箱,调 task 派生子 agent,调 ask_clarification 就中断回问用户。
  5. 子 agent 在独立线程 + 独立上下文里跑完,只把一段结论字符串交回主循环(task_tool.py:397)。
  6. 模型不再调工具时给出最终答案,沿 LangGraph SSE 协议(values / messages-tuple / custom)流回前端。

一个关键认知:复杂度在「链」不在「图」。 想读懂 DeerFlow,不要去找规划算法——去读那条 middleware 链的顺序,以及三个子系统(Skills / Subagent / Sandbox)怎么被这条链和工具挂进那唯一的 ReAct 循环。下面三节各钻透一个。


3. 核心机制

3.1 中间件链:扁平 ReAct 图的「隐形骨架」

它要解决的小问题: 一个裸的 ReAct 循环啥都没有——不会截断超长工具输出、不会在跨越 token 上限时压缩历史、不会把 provider 的安全终止拦下来、不会把技能/日期注入上下文。DeerFlow 不去改图,而是把这些能力做成一层层 middleware,按固定顺序套在「模型调用」外面

思路 / 直觉: 顺序即语义。LangChain 的 middleware 有 wrap_model_call(调模型前后)和 after_model(拿到回复后)两类钩子;链里越靠前的,wrap_model_call 就越「外层」;而 after_model 是反序派发的。DeerFlow 精心排这个顺序,让每层看到的消息状态正好是它需要的。

链分两段拼出来。共享基座build_lead_runtime_middlewares 建(.../middlewares/tool_error_handling_middleware.py:200,内部转调 _build_runtime_middlewares:129),子 agent 复用其中大部分;lead 专属的再由 build_middlewares 追加(.../lead_agent/agent.py:270)。举几个能说明「为什么这个顺序」的例子:

  • InputSanitizationMiddleware 放最前,所以它是最外层的 wrap_model_call 包装——里面所有 middleware(包括 LLM 重试)看到的都是已清洗的消息。
  • DynamicContextMiddleware(agent.py:306)把当前日期(和可选记忆)作为 <system-reminder> 注入第一条 HumanMessage,而改系统提示——这是 prefix-cache 的关键(见 §4)。
  • SystemMessageCoalescingMiddleware(agent.py:356)在请求发给 provider 前,把所有 SystemMessage 合并成唯一一条打头的——因为 vLLM / SGLang / Qwen / Anthropic 这些严格后端会拒绝「非打头的系统消息」。
  • ClarificationMiddleware 永远放最后(agent.py:390):它专门拦截 ask_clarification 工具调用,用 Command(goto=END) 中断整张图去回问用户,所以必须是链尾。
  • SafetyFinishReasonMiddleware 注册在自定义 middleware 之后(agent.py:387):因为 after_model反序派发的,注册在后 = 最先跑,它才能在 Loop/Subagent 计数之前把「被安全终止」的回复的 tool_calls 清干净。

真实实现里这条链是条件拼接的——很多层只在对应 config/runtime 打开时才 append,所以实际链长随配置变。看这段就懂它的「按需组装」风格:

# 摘自 lead_agent/agent.py:359-374,真实源码(节选)
subagent_enabled = cfg.get("subagent_enabled", False)
if subagent_enabled:
max_concurrent_subagents = cfg.get("max_concurrent_subagents", 3)
middlewares.append(SubagentLimitMiddleware(max_concurrent=max_concurrent_subagents))
loop_detection_config = resolved_app_config.loop_detection
if loop_detection_config.enabled:
middlewares.append(LoopDetectionMiddleware.from_config(loop_detection_config))

关键细节 / 坑: factory.py 里还有另一套声明式装配路径 create_deerflow_agent(features=...)(factory.py:61),用 @Next/@Prev 锚点把额外 middleware 插到指定位置,并在 _insert_extra(factory.py:316)里检测循环依赖、并保证 ClarificationMiddleware 永远被挪回队尾(factory.py:304-306)。两条路径(应用级 make_lead_agent vs SDK 级 create_deerflow_agent)刻意保持同样的顺序语义。

3.2 子 agent 委派:decompose → delegate → synthesize

它要解决的小问题: 深度研究的材料太多,全塞进主上下文会「污染」——主 agent 越读越乱、token 也爆。要让主 agent 只看结论、不看过程

思路 / 直觉: 主 agent 当编排器:把大任务拆成并行子任务,每个子任务丢给一个上下文完全隔离的子 agent;子 agent 在自己的世界里折腾,最后只回一段结论。系统提示里明确把这套写成三步——DECOMPOSE / DELEGATE / SYNTHESIZE,还硬编了一条并发上限(_build_subagent_section,.../lead_agent/prompt.py:216):

⛔ HARD CONCURRENCY LIMIT: MAXIMUM {n} task CALLS PER RESPONSE. ——每轮最多 n(默认 3)个 task 调用,超出的会被系统静默丢弃

这个限额不是嘴上说说:SubagentLimitMiddlewareafter_model 里真的会把超出的 task 调用截掉(agent.py:362)。

图示(一次 task 委派的生命周期):

真实实现: task_tool(task_tool.py:187)从 runtime 里取出父 agent 的沙箱、thread_data、模型名、trace_id、user_id,构造 SubagentExecutor,调 execute_async 把执行丢进后台线程池(executor.py:827),然后自己在后端循环轮询——不是让大模型反复问「好了没」:

# 摘自 task_tool.py:340-397,真实源码(节选):后端轮询 + 实时 SSE
task_id = executor.execute_async(prompt, task_id=tool_call_id)
writer = get_stream_writer()
writer({"type": "task_started", "task_id": task_id, "description": description})
while True:
result = get_background_task_result(task_id)
# ... 每有新 AI 消息就 writer({"type": "task_running", ...}) 流给前端 ...
if result.status == SubagentStatus.COMPLETED:
return f"Task Succeeded. Result: {result.result}" # 只回结论
await asyncio.sleep(5) # 每 5s 轮一次

子 agent 的图在 _create_agent 里编译时刻意带 checkpointer=False(executor.py:375)——因为子 agent 是一次性、永不恢复的,不该继承父 run 的 checkpointer。它跑的上限是 recursion_limit = config.max_turns(executor.py:548),内置 general-purpose 默认 max_turns=150bash60(subagents/builtins/general_purpose.py:60bash_agent.py:49),而全局兜底超时是 subagents.timeout_seconds(默认 1800s / 30 分钟,见 subagents/config.py:27 的注释)。

关键细节 / 坑:

  • 上下文隔离靠「另起一张图 + 另一份 ThreadState」:子 agent 的初始 state 只从父那里透传 sandboxthread_data(executor.py:497-500),带父的对话历史——这就是「互相看不见」的物理实现。
  • 超时是双保险:后台线程池按 timeout_seconds 硬砍(executor.py:871),task_tool 里又设了 max_poll_count = (timeout+60)//5 的轮询兜底(task_tool.py:347),防止后台线程卡死时前台永远转。
  • token 归因:子 agent 用掉的 token 会通过 _report_subagent_usage 回写到「触发它的那次 AIMessage」(task_tool.py:146),这样账单能算到发起委派的那一步。

3.3 Skills 渐进式披露:系统提示只放「菜单」,用到才读「菜谱」

它要解决的小问题: 一个 skill(如 researchslide-creation)的 SKILL.md 可能几千 token。若把所有启用技能的全文都塞进系统提示,上下文瞬间被撑爆,对 token 敏感的模型直接崩。

思路 / 直觉: 渐进式披露(progressive disclosure)——系统提示里只放每个技能的元数据(名字 + 一句话描述 + 容器内路径),像一张菜单;真正要用某个技能时,agent 自己用 read_file 去读那份 SKILL.md 全文(菜谱)。

真实实现: SKILL.md 是「YAML frontmatter + 正文」的 Markdown。parse_skill_file(skills/parser.py:66)用正则抠出打头的 --- ... --- 块,yaml.safe_loadname / description / license / allowed-tools。注入系统提示时,get_skills_prompt_section(.../lead_agent/prompt.py:666)只把元数据拼成 XML,每个技能一行、带上路径可变性标签:

# 摘自 lead_agent/prompt.py:638-643,真实源码:只放元数据,不放全文
f" <skill>\n"
f" <name>{name}</name>\n"
f" <description>{description} {_skill_mutability_label(category)}</description>\n"
f" <location>{location}</location>\n"
f" </skill>"

<location> 是技能在容器里的路径(默认 /mnt/skills/...),_skill_mutability_label(prompt.py:157)给自定义技能标 [custom, editable]、内置的标 [built-in]——模型据此知道「这份能不能改」。全文只在需要时才进上下文,有两条路径:

  1. 模型自主:看到菜单后,主动 read_file(location) 读全文。
  2. 用户显式激活:用户以 /skill-name task 开头,SkillActivationMiddleware(agent.py:313;实现见 skill_activation_middleware.py:66)只对这一轮把该技能全文作为隐藏上下文注入,并做严格校验(拒绝前导空格、未安装/已禁用、不在当前 agent 白名单、以及 /new /help 这类保留命令)。

子 agent 走的是「Codex 风格」的另一条注入路:每个子 agent 按自己的 config.skills 白名单独立加载技能,并把全文作为 SystemMessage(而非系统提示文本)塞进初始 state(executor.py:409-438_build_initial_state:440)。

关键细节 / 坑:

  • skill 的 allowed-tools 是一道工具围栏:filter_tools_by_skill_allowed_tools 会按启用技能声明的 allowed-tools 过滤 agent 实际能用的工具(agent.py:505532),避免技能声明用不到的工具还挂在模型面前。
  • 缓存换性能:启用技能列表是异步预热 + 按 config 缓存的(prompt.py:20-93 的一整套 _enabled_skills_* 机制),prime_enabled_skills_cache() 甚至在 deerflow.agents 被 import 时就先热身(agents/__init__.py:10),让请求路径通常读到暖缓存,不必在提示模块 import 时同步做文件 IO。

4. 巧妙之处与局限

值得偷师的设计:

  1. 静态系统提示 + 动态 <system-reminder> = 榨干 prefix-cache。 这是最值得学的一招。系统提示 apply_prompt_template(prompt.py:779)刻意做成跨用户、跨会话完全一致——连当前日期、当前用户的记忆都放进去。日期和记忆改由 DynamicContextMiddleware 作为 <system-reminder> 注入第一条 HumanMessage(dynamic_context_middleware.py:125 附近)。这样每个请求的前缀完全相同,provider 端的 KV prefix cache 每次都命中,省钱又省延迟。跨越午夜时它只补一条「日期更新」的 reminder,不动前缀。

  2. 后端轮询 + 实时 SSE,而不是让大模型自己 poll。 子任务的进度靠 task_tool 在后端 while True: sleep(5) 轮询后台结果(task_tool.py:356),每有新消息就 writer({"type":"task_running",...}) 流给前端。大模型只在子任务终态才拿到一段结论字符串——既省掉了「大模型反复问好了没」的 token 浪费,又能给用户实时进度。

  3. 上下文工程是「物理隔离 + 主动压缩」的组合拳。 隔离靠子 agent 另起一张图 + 独立 ThreadState(§3.2);压缩靠 DeerFlowSummarizationMiddleware(summarization_middleware.py),在逼近 token 上限时把旧消息总结掉,还带 BeforeSummarizationHook(如 memory_flush_hook,agent.py:126-127)在删消息前把该记的先落盘。

  4. ThreadState 用自定义 reducer 表达并发语义。 例如 merge_sandbox(thread_state.py:21)对同一步里多个沙箱工具的写入只接受幂等——不同 sandbox_id 直接 raise,把「隔离 bug」变成显式崩溃而非静默择一;merge_promoted(thread_state.py:90)按 catalog_hash 作用域合并「被提升的延迟工具」,catalog 变了就整体丢弃旧名字,防止一个残留的裸名字指到别的工具。

  5. tool_search 延迟披露 MCP 工具。 MCP 服务器可能挂几十个工具,全绑给模型既贵又乱。assemble_deferred_tools(在 agent.py:506533 处调用)把 MCP 工具藏起来,只在系统提示里列名字;模型用 tool_search 提升后,DeferredToolFilterMiddleware 才放行其完整 schema(agent.py:349)。子 agent 也复用这套(executor.py:440-468)。

边界与坑(诚实):

  • 不是多节点规划图。 若你期待 v1 那种 planner→researcher→reporter 的显式 DAG,会失望——2.0 是扁平 ReAct + middleware,「规划」体现在系统提示的 orchestrator 指令和 write_todos(plan 模式)里,不是图结构。README 明确说 2.0 是从零重写、与 v1 不共享代码。
  • 并发上限是「提示 + 截断」双管,但硬上限只有 3。 每轮 >3 个 task 会被静默丢弃(agent.py:362),模型若不遵守「分批」指令,就会丢工作;这把复杂编排的节奏压在了模型的指令遵循能力上。
  • Local 沙箱不是安全边界。 README 明说 LocalSandboxProvider 下 host bash 默认禁用(tools.py:70 附近据 is_host_bash_allowed 过滤),因为本地执行不是隔离边界;真隔离要上 AioSandboxProvider(Docker)。
  • 子 agent 无 checkpointer,不可恢复。 checkpointer=False(executor.py:375)意味着子任务一旦失败/超时就整段丢弃、不能断点续跑;深研究里这偶尔意味着重跑整路。
  • tiktoken 首用可能阻塞。 记忆注入的 token 计数默认用 tiktoken,首用可能从公网下载 BPE 数据、在受限网络里长时间卡住(README 提到的 issues #3402/#3429),故有 memory.token_counting: char 的网络无关兜底。

代码地图(导航索引)

关注点文件说明
图入口(LangGraph 工厂)backend/packages/harness/deerflow/agents/lead_agent/agent.pymake_lead_agent / _make_lead_agent:选模型、装工具、拼 middleware,编出扁平 ReAct 图
中间件链装配(lead)backend/packages/harness/deerflow/agents/lead_agent/agent.pybuild_middlewares:20+ 层有序链,条件 append,Clarification 永远收尾
中间件链(SDK 声明式)backend/packages/harness/deerflow/agents/factory.pycreate_deerflow_agent / _assemble_from_features / _insert_extra:@Next/@Prev 锚点插入
共享 runtime 基座backend/packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.pybuild_lead_runtime_middlewares / build_subagent_runtime_middlewares:lead 与 subagent 复用同一基座
系统提示(静态)backend/packages/harness/deerflow/agents/lead_agent/prompt.pyapply_prompt_template / get_skills_prompt_section / _build_subagent_section:技能只放元数据,orchestrator 指令
动态上下文(prefix-cache)backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.pyDynamicContextMiddleware:日期/记忆作 <system-reminder> 注入首条 HumanMessage
子 agent 工具backend/packages/harness/deerflow/tools/builtins/task_tool.pytask_tool:委派 + 后端轮询 + task_running 实时 SSE + token 归因
子 agent 执行引擎backend/packages/harness/deerflow/subagents/executor.pySubagentExecutor / execute_async / _build_initial_state:线程池 + 独立事件循环 + 上下文隔离
子 agent 内置类型backend/packages/harness/deerflow/subagents/builtins/general_purpose.pyGENERAL_PURPOSE_CONFIG(max_turns=150);bash_agent.pybash(max_turns=60)
Skills 解析backend/packages/harness/deerflow/skills/parser.pyparse_skill_file / parse_allowed_tools:抠 YAML frontmatter,得到 name/description/allowed-tools
Skills 显式激活backend/packages/harness/deerflow/agents/middlewares/skill_activation_middleware.pySkillActivationMiddleware:/skill-name 只对当轮注入全文,严格校验
工具汇总backend/packages/harness/deerflow/tools/tools.pyget_available_tools:config 工具 + MCP + 内置 + task,按 group / vision / 沙箱条件裁剪
ThreadState 与 reducerbackend/packages/harness/deerflow/agents/thread_state.pyThreadState / merge_sandbox / merge_promoted / merge_artifacts:用 reducer 表达并发语义
上下文压缩backend/packages/harness/deerflow/agents/middlewares/summarization_middleware.pyDeerFlowSummarizationMiddleware / BeforeSummarizationHook:逼近上限时总结旧消息、删前先落盘
内嵌客户端backend/packages/harness/deerflow/client.pyDeerFlowClient:不启 HTTP,进程内跑同一套图,返回 Gateway 对齐的响应