先说说为什么要去翻这个仓库
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 系统能否打通------一个负责拆解复杂任务,一个负责定时触发,理论上是互补的。有进展会写后续。