字节跳动开源 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 系统能否打通------一个负责拆解复杂任务,一个负责定时触发,理论上是互补的。有进展会写后续。

相关推荐
长沙三为智能科技13 分钟前
GEO与传统SEO的5个关键差异:排名与被引用、关键词与答案型的全维度对比
python·aigc·seo·ai搜索·geo优化
threerocks2 小时前
一个人指挥一支 AI 剧组:Gemini 4 + Omni 1.2 剧情短片制作保姆级教程
aigc·openai·ai编程
西安小哥3 小时前
大厂把大模型做成一层网关之后,我先把三件事重新想了
aigc
小和尚同志4 小时前
阿里大佬做的一款轻量化 Token 监控工具—知度 Tokei
人工智能·aigc·监控
小虎AI生活14 小时前
WorkBuddy 积分不够烧?我找到了每天白送 5000 的平台
aigc·ai编程
Csvn14 小时前
模型挂了全站 502?降级链让它先保住“有响应”(R04)
人工智能·aigc·agent
狼爷16 小时前
AI 时代学习真相:答案唾手可得,思考才是稀缺品
aigc
用户157415681653417 小时前
让「大肥鱼」在群里开口说话:NapCat + AstrBot + GPT-SoVITS 语音机器人搭建实录
机器人·aigc
Rocky Ding*1 天前
REPA-E技术深度解析:解冻 VAE 之后,扩散模型为什么需要选择梯度
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·repa-e
DQQzero1 天前
0.16 元/秒的短剧流水线:即梦短剧工作室把 AI 视频生产改成了什么样
人工智能·ai·aigc·视频生成