字节跳动开源 DeerFlow 2.0 源码拆解:14层Middleware、Sub-Agent并发编排和结构化记忆是怎么做的

先说说为什么要去翻这个仓库

2月底的某天,GitHub Trending 第一是一个叫 DeerFlow 的项目,字节跳动出品,star 几小时内就破了几千。我最初的反应是:又一个 Deep Research 套壳吧

然后点进去一看,README 第一行就写着:"DeerFlow 2.0 is a ground-up rewrite. It shares no code with v1."

好,有意思。v1 是那个很火的多 Agent 深度研究框架,2.0 直接不用一行 v1 的代码重写,背后肯定有一套新的架构判断。花了几天把源码翻完,这篇文章是我的完整记录。

不是教程,是拆解。

DeerFlow 2.0 想解决什么问题

v1 的定位是 Deep Research------给一个问题,多个 Agent 并发搜索、整合、生成报告。这个模式很清晰,但边界也很清晰:它只能做研究,干别的就不行了

2.0 的目标大很多:Super Agent Harness,一个能编排子 Agent、持久化记忆、隔离沙盒来完成"几乎任何任务"的平台。

对应的技术挑战就来了:

• 任务复杂度不可预测,需要弹性的 Agent 编排能力

• 长对话上下文会撑爆 context window,需要压缩和记忆管理

• 执行代码和命令存在安全风险,需要沙盒隔离

• 工具越来越多,全部塞进 system prompt 会让模型迷失

这四个问题,DeerFlow 2.0 都给出了自己的答案,架构上能明显感受到每个设计决策背后的具体考量。

14 层 Middleware:洋葱模型的严格实践

先讲我觉得 2.0 最值得关注的设计:有序 Middleware 链

很多框架都说自己有 Middleware,但大多数的 Middleware 是平铺的,谁先谁后无所谓,或者靠约定来保证顺序。DeerFlow 不一样,它的 14 个 Middleware 是严格有序的,顺序写死在代码注释里,而且顺序错了会出 bug

从外到内的完整链路:

层级 Middleware 职责
1 DanglingToolCallMiddleware 修补缺失的 ToolMessage(修复 LangChain 历史污染)
2 SandboxMiddleware 注入沙盒状态到 ThreadState
3 ThreadDataMiddleware 提取 thread_id(SandboxMiddleware 依赖它)
4 UploadsMiddleware 处理文件上传(依赖 thread_id)
5 SummarizationMiddleware 上下文超长时触发自动摘要压缩
6 TodoMiddleware plan 模式下提供 write_todos 工具
7 TokenUsageMiddleware token 计量统计
8 TitleMiddleware 首次对话后自动生成标题
9 MemoryMiddleware 将对话入队,异步更新记忆
10 ViewImageMiddleware 视觉模型:将图片内容注入上下文
11 DeferredToolFilterMiddleware 工具过多时延迟暴露,配合 tool_search
12 SubagentLimitMiddleware 截断超并发的 task() 调用
13 LoopDetectionMiddleware 滑动窗口 hash 检测重复工具调用
14 ClarificationMiddleware 拦截澄清请求(始终最后)

可以把这 14 层 Middleware 想象成机场安检通道------每道闸机负责不同的检查(证件核验、行李扫描、安全问询......),顺序颠倒就会出问题:如果行李扫描排在证件核验前面,扫完了发现证件是假的,前面的工作全白费。DeerFlow 的 Middleware 链也是如此,顺序颠倒不是"效率低了",是"真的会出 bug"。

比如 2 和 3 的顺序关系:SandboxMiddleware 需要用到 thread_id,而 ThreadDataMiddleware 负责注入它。洋葱模型"进入时由外到内、退出时由内到外"的特性,让外层的 SandboxMiddleware 能在退出阶段读到内层注入的数据。顺序不是靠文档约定,是靠执行模型保证的。

这件事看起来平凡,但工程价值很高。绝大多数框架的 Middleware 是"顺序无关"的,一旦引入依赖关系就会产生隐式约定------某个新来的开发者调整了顺序,测试可能都过了,但某个角落的功能悄悄坏掉。DeerFlow 把依赖关系写死在注释和实现里,顺序错了直接出 bug,反而更安全。

Sub-Agent 编排:不让 LLM 做它不擅长的事

DeerFlow 的 Sub-Agent 通过一个叫 task_tool 的工具触发。Lead Agent(主 Agent)调用这个工具,传入任务描述和子 Agent 类型,然后......就不管了,等结果。

听起来很普通,但魔鬼在细节里:轮询是由 task_tool 内部做的,LLM 感知不到

python 复制代码
# task_tool 内部的轮询逻辑(简化)
async def task_tool(...):
    # 启动后台任务
    task_id = executor.execute_async(prompt)
    
    while True:
        result = get_background_task_result(task_id)
        
        if result.status == COMPLETED:
            # 推送完成事件,返回结果给 LLM
            writer({"type": "task_completed", ...})
            return f"Task Succeeded. Result: {result.result}"
        
        elif result.status == FAILED:
            return f"Task failed. Error: {result.error}"
        
        # 还在跑,等 5 秒再查
        await asyncio.sleep(5)
        poll_count += 1
        
        # 超时保护:执行超时+60s buffer
        if poll_count > max_poll_count:
            return "Task polling timed out..."

传统的 multi-agent 实现(比如早期 AutoGPT 那一波)是让 LLM 自己轮询:调一次工具问"任务完成了吗",没完成再调一次......这会消耗大量 token,而且 LLM 很容易在轮询过程中"走神"------忘记原始任务、开始自言自语、甚至陷入循环。

DeerFlow 把轮询封装进工具,对 LLM 来说,task() 就是一个同步调用:发出去,结果回来。LLM 不需要知道子任务跑了多久,不需要主动检查状态,更不会因为轮询产生额外的推理成本。这是"不让 LLM 做它不擅长的事"的经典实践

同时,进度是实时推送的------通过 LangGraph 的 stream_writer,前端能实时收到 task_running 事件,渲染出子任务执行进度。用户体验和内部实现都照顾到了。

防递归和并发上限

子 Agent 在获取工具集时,会强制 subagent_enabled=False------也就是说,子 Agent 拿不到 task 这个工具,无法再派生孙 Agent。这是硬编码的防递归。

SubagentLimitMiddleware 则控制单次响应里 task() 的最大并发数(默认 3)。如果 LLM 一口气调了 10 个 task(),超出部分会被截断,不会让服务器撑爆。

LoopDetection:滑动窗口 Hash 检测

LLM 陷入工具调用循环是个真实存在的问题,特别是在复杂任务里------Agent 反复调同一个工具,参数几乎一样,就是出不来。

DeerFlow 的解决方案是 LoopDetectionMiddleware,核心是对每次工具调用集合做 hash:

ini 复制代码
def _hash_tool_calls(tool_calls: list[dict]) -> str:
    # 归一化:只保留 name + args
    normalized = [{
        "name": tc.get("name", ""),
        "args": tc.get("args", {}),
    } for tc in tool_calls]
    
    # 排序,使得顺序无关
    normalized.sort(key=lambda tc: (
        tc["name"],
        json.dumps(tc["args"], sort_keys=True, default=str),
    ))
    
    blob = json.dumps(normalized, sort_keys=True, default=str)
    return hashlib.sha256(blob.encode()).hexdigest()[:16]

你可以把这个机制想象成老师发现学生在考卷上反复写同一个答案:第一次看到还会提示"你是不是卡住了",第三次还没变化就直接收卷、强制交答案。DeerFlow 的两档响应也是这个逻辑:

顺序归一化:LLM 可能以不同顺序调用相同工具(先调 A 后调 B,下次先调 B 后调 A),这仍然是重复,hash 之前会先排序

滑动窗口:只跟踪最近 N 次(默认20次)工具调用,不是全局计数------Agent 不会被一次很早之前的重复"冤枉"

两档响应:连续相同 3 次注入警告("你在重复自己"),5 次强制剥离所有 tool_calls,逼 LLM 输出最终答案

成本几乎为零,但能有效阻断大量 Agent stuck 场景。

结构化记忆:比 Markdown 更有意思的设计

说到 Agent 的记忆,大多数实现就是:把对话历史塞进 prompt,或者存一个纯文本摘要文件。DeerFlow 用的是结构化 JSON,而且分层设计:

css 复制代码
{
  "version": "1.0",
  "user": {
    // 当前状态层:最新、最相关
    "workContext": {"summary": "...", "updatedAt": "..."},
    "personalContext": {"summary": "...", "updatedAt": "..."},
    "topOfMind": {"summary": "...", "updatedAt": "..."}
  },
  "history": {
    // 历史层:时效递减
    "recentMonths": {"summary": "..."},
    "earlierContext": {"summary": "..."},
    "longTermBackground": {"summary": "..."}
  },
  "facts": [
    // 结构化事实:带置信度
    {
      "content": "用户偏好 Kotlin 协程",
      "category": "preference",
      "confidence": 0.95
    }
  ]
}

这个设计有几点我觉得比较克制和聪明:

时效分层而不是时间戳排序。很多记忆系统按时间倒排,最新的权重最高。但"最新"不等于"最重要"------三个月前确定的技术选型,可能比昨天的闲聊更值得记住。DeerFlow 把记忆显式分成了"当前工作上下文"、"近几个月"、"更早背景"三层,让模型能按语义需要来读取,而不是机械地按时间。

facts 的置信度管理。每条 fact 有 0.0 到 1.0 的置信度,支持渐进更新------第一次观察到某个偏好,置信度 0.6;确认了几次之后升到 0.9。这个设计让记忆不是二值的"记住/忘记",而是一个连续的确信过程。

per-agent 隔离 。每个 agent_name 有独立的记忆文件,多 Agent 场景下互不污染。配合 MemoryStorage 抽象类,理论上可以换成数据库存储,接口是稳定的。

文件缓存 + mtime 检测。加载时先检查文件修改时间,没变化就用缓存,避免每次请求都读磁盘。小细节,但高频场景下有感知。

DeferredToolFilter:把 RAG 的思路用在工具上

随着 MCP 生态发展,一个 Agent 接入几十上百个工具已经是常态。全部塞进 system prompt,模型的注意力会被稀释,选工具的准确率会下降,token 成本也蹭蹭上去。

DeerFlow 的解法是 DeferredToolFilter :把它想象成图书馆的检索卡片柜 ------图书馆不会把所有书都堆到你桌上,而是先给你一个检索系统,你查到了再去取书。Agent 启动时不暴露所有工具,只暴露一个 tool_search 工具。当 LLM 需要某个能力时,先调 tool_search 搜索相关工具,找到之后再实际使用。

这本质上是把 RAG(检索增强)的思路应用到了工具层------工具也是一种"知识",按需检索比全量注入更高效。

对于工具数量少(<20个)的场景,这个机制会关掉,直接全量注入,不增加额外的 LLM 调用开销。这种"按量切换策略"的自适应做法比较务实。

Sandbox:真隔离和伪隔离的差距

很多框架说自己有"沙盒执行",但仔细一看,其实就是在本机跑 subprocess,最多加个超时控制。这不是隔离,只是延迟爆炸。

DeerFlow 的 AioSandbox 是真 Docker 容器隔离,通过 HTTP API 连接一个独立的 agent-infra/sandbox 容器:

python 复制代码
class AioSandbox(Sandbox):
    def __init__(self, id: str, base_url: str, ...):
        self._client = AioSandboxClient(
            base_url=base_url,  # e.g. http://localhost:8080
            timeout=600
        )
    
    def execute_command(self, command: str) -> str:
        result = self._client.shell.exec_command(command=command)
        return result.data.output

文件读写、命令执行都走 HTTP API 到容器里,主进程和执行环境完全分离。SandboxInfo 序列化沙盒元数据,跨进程(多 worker / K8s pod)可以共享同一个沙盒实例,不用每次都重新创建容器。

容器层面的安全还不够,SandboxAuditMiddleware 在命令到达沙盒之前还有一层 regex 审计:

python 复制代码
# 直接 block 的高危命令
_HIGH_RISK_PATTERNS = [
    # rm -rf / /* ~ /home /root
    re.compile(r"rm\s+-[^\s]*r[^\s]*\s+(/\*?|...)"),
    # curl|sh 管道执行
    re.compile(r"(curl|wget).+\|\s*(ba)?sh"),
    re.compile(r"dd\s+if="),        # dd 覆写磁盘
    re.compile(r"mkfs"),            # 格式化
    re.compile(r"cat\s+/etc/shadow"), # 读密码文件
    re.compile(r">\s*/etc/"),         # 覆写 /etc/
]

# warn 的中危命令
_MEDIUM_RISK_PATTERNS = [
    re.compile(r"chmod\s+777"),
    re.compile(r"pip\s+install"),
    re.compile(r"apt(-get)?\s+install"),
]

两层防护,容器隔离 + 命令审计,防的是不同类型的威胁:容器隔离防止攻击扩散到宿主机,命令审计在容器内部再防一道。

和 OpenClaw 的本质区别:两种 Agent 哲学

翻完源码,我觉得 DeerFlow 和另一个我熟悉的框架 OpenClaw 代表了两种截然不同的 Agent 设计哲学,值得对比一下。

维度 DeerFlow OpenClaw
核心定位 Super Agent Harness(任务编排) Personal AI Gateway(IM 接入层)
Agent 框架 LangChain + LangGraph(图结构) 自研(prompt-based)
记忆格式 结构化 JSON(分层+置信度) Markdown 文件(人工可读性强)
沙盒 Docker 容器(真隔离) 本地 exec(灵活但无隔离)
Skills Python 脚本(沙盒内确定性执行) Markdown 驱动(LLM 理解后执行)
渠道支持 飞书、Slack、Telegram + Web UI 企微、Telegram、Discord、WhatsApp 等
定时任务 ❌ 无内置 cron ✅ 内置 cron 调度系统
循环检测 ✅ 滑动窗口 hash ❌ 无
部署复杂度 高(需 Docker + Python 3.12) 低(npm 安装)

Skills 的设计差异最能体现两种哲学的分歧:

DeerFlow 的 Skills 是 Python 脚本,在沙盒里执行,行为确定、可重复、可测试。缺点是需要会写 Python,而且沙盒里有环境依赖。

OpenClaw 的 Skills 是 Markdown 文件------你告诉 LLM"遇到这种情况按这个步骤来",LLM 读完后自己理解、自己执行。灵活性极高,写一个新 Skill 甚至不需要写一行代码。但可靠性依赖 LLM 的理解能力,同一个 Skill 在不同场景下执行结果可能不一样。

这不是谁对谁错,是两种不同的确定性-灵活性权衡

值得借鉴的三个设计

1. LoopDetectionMiddleware 可以直接移植

成本极低,就是一个滑动窗口+hash,但能有效防止 Agent stuck。任何基于工具调用的 Agent 框架都可以加这一层,特别是在处理长任务的时候。实现复杂度不超过 100 行。

2. 记忆的时效分层思想

不是"记住/忘记"的二值设计,而是按时效分层管理。这对长期运行的 Agent 非常有价值------"最近一周的工作"和"三个月前确定的架构决策"应该存在不同的记忆层里,检索时按需加载,而不是全部平铺在一个文件里。

3. 工具搜索(DeferredToolFilter)

随着 MCP 生态扩展,工具数量膨胀是必然趋势。现在就设计好"工具索引+按需检索"的机制,比等到 context 被撑爆了再改要容易得多。

DeerFlow 的短板

说完优点,也说说我觉得不足的地方。

部署复杂度是真门槛。Docker + Python 3.12 + Node.js 22,可选还要配 AioSandbox 容器。对于想快速试用的个人开发者来说,光是让整个环境跑起来就要花不少时间。这和 OpenClaw 这类"一行命令启动"的工具差距明显。

没有 IM 原生集成。DeerFlow 的交互入口是 Web UI,虽然支持飞书/Slack/Telegram,但配置流程复杂。如果你想要一个"随时随地能用企微找 AI"的体验,DeerFlow 目前满足不了。

没有 cron/定时任务。这是我个人觉得最可惜的地方。一个"能做任何事"的 Agent 平台,却没有内置的定时调度能力------你没法告诉它"每天早上 9 点帮我做 XX"。这个功能不难实现,但确实缺失。

Skills 依赖沙盒,没沙盒就没 Skills 。如果不配 AioSandbox,大量内置 Skills(播客生成、PPT 生成等)都跑不了,因为它们的 Python 脚本路径写死在 /mnt/skills/public/ 下。这个耦合不太优雅。

写在最后

DeerFlow 2.0 是我近期看过的开源 Agent 框架里,工程设计最完整的一个。14 层 Middleware 的洋葱模型、Sub-Agent 内置轮询、结构化记忆分层、沙盒双重防护------这些设计不是新鲜概念的堆砌,是在解决真实的工程问题。

字节跳动能把这个开源出来,说明他们内部已经有更先进的版本了。我们能做的,是把这里面好的设计思想消化成自己的判断。

如果你在做 Agent 相关开发,建议去把 backend/packages/harness/deerflow/agents/middlewares/ 这个目录翻一遍。不一定要用这个框架,但每个 Middleware 解决的问题都是真实的,值得你思考在自己的系统里用什么方式来处理。

📌 下一步想深挖的是 DeerFlow 的 Plan 模式(TodoMiddleware)和 OpenClaw 的 cron 系统能否打通------一个负责拆解复杂任务,一个负责定时触发,理论上是互补的。有进展会写后续。

相关推荐
AI先驱体验官2 小时前
智能体变现:从技术实现到产品化的实践路径
大数据·人工智能·深度学习·重构·aigc
小程故事多_805 小时前
破解Agent“半途摆烂”困局,OpenDev凭Harness架构,撕开Code Agents的工程化真相
人工智能·架构·aigc·harness
用户754695521117 小时前
从 XML 到叙事稿:我是如何用 AI Agent 自动编辑 PPT 演讲备注的
aigc
skydaxia8 小时前
添翼思维 | 当 Openclaw 开启序幕,谁在定义这一代的金子?
ai·aigc·ai-native
树獭叔叔8 小时前
Agent 记忆系统设计全景:从短期对话到长期知识沉淀
后端·aigc·openai
攻城狮_老李9 小时前
从零开始理解 Agent Skills:进阶主题
aigc·agent·ai编程
数字游民95279 小时前
AI应用到具体的业务场景:电商物流费用计算
人工智能·ai·aigc·自媒体·数字游民9527
刘 大 望11 小时前
开发自定义MCP Server并部署
java·spring·ai·语言模型·aigc·信息与通信·ai编程
安思派Anspire12 小时前
Ghost互联网
aigc·openai