这是「DeerFlow 代码全景解析」专题的第 05 篇。在本系列中,我们将逐层拆解 DeerFlow 开源项目的工程架构,从任务执行层到沙箱隔离,从多 Agent 编排到安全边界,带你看清一个工业级 AI Agent 平台如何将"复杂任务"拆解为"可控的协作单元"。
故事场景:一个 PR 的"10 手协作"
小林——刚加入某 AI 创业公司的新晋工程师——接到了人生第一个来自 AI Agent 的复杂任务:
"帮我分析这个 2000 行 Python 项目,找出所有内存泄漏点,生成修复补丁,并写出测试用例验证修复。"
他盯着 DeerFlow 的终端界面,心想:这得跑多少轮对话?上下文塞得下吗?如果中途出错,前面分析的结果岂不是要重新来过?
就在他犹豫时,Lead Agent 已经开始了行动:
- 它先分派了一个 general-purpose 子 Agent 去"通读代码结构";
- 同时启动了一个 bash 子 Agent 去"运行内存分析工具";
- 等两者返回后,再让一个子 Agent 去"生成修复补丁";
- 最后让另一个子 Agent 去"编写测试用例并运行验证"。
整个过程并行推进,每个子 Agent 拥有独立的上下文和工具集,互不干扰。5 分钟后,小林拿到了完整的分析报告、补丁文件和绿色通过的测试日志。
"这就是 Sub-Agent 的力量,"他在周报里写道,"它让一个人(一个 Agent)的复杂任务,变成了多个专家(多个子 Agent)的并行协作。"
一、Sub-Agent 设计哲学
1.1 为什么复杂任务需要分解
单个大模型 Agent 处理复杂任务时,面临着三个本质性困境:
上下文限制:再强大的 LLM 也有上下文窗口上限。当任务涉及千行代码、多轮工具调用、复杂推理链时,历史记录会迅速填满窗口,导致"遗忘"早期信息。
专业分工缺失:一个通用 Agent 既要理解代码结构,又要执行 Shell 命令,还要撰写测试用例——这就像让一个人同时做架构师、DevOps 和 QA,效率必然打折。
串行瓶颈:所有步骤在一个对话线程中串行执行,前面出错时后面的全部作废,且无法并行探索多个路径。
DeerFlow 的答案是 Sub-Agent 编排:将复杂任务分解为多个独立的子任务,每个子任务由一个专门的子 Agent 执行,最后由 Lead Agent 综合结果。
1.2 DeerFlow 的 Sub-Agent 模型
flowchart TD
subgraph Lead["Lead Agent 主线程"]
A[用户输入] --> B[任务分解]
B --> C[task_tool 调用]
C --> D{等待子 Agent}
D --> E[结果回传]
E --> F[综合输出]
end
subgraph Sub1["子 Agent 1"]
S1[独立上下文] --> S1a[工具调用] --> S1b[结果返回]
end
subgraph Sub2["子 Agent 2"]
S2[独立上下文] --> S2a[工具调用] --> S2b[结果返回]
end
C -->|并行分派| Sub1
C -->|并行分派| Sub2
S1b -->|SSE 流式| D
S2b -->|SSE 流式| D这个模型的核心流程是:Lead Agent 分派 → 子 Agent 执行 → 结果回传 → 综合输出。每个子 Agent 都是一个完整的 LangGraph Agent,拥有自己的 SystemMessage、工具集 和 终止条件。
1.3 与 Multi-Agent 框架的区别
| 维度 | 传统 Multi-Agent 框架 | DeerFlow Sub-Agent |
|---|---|---|
| 创建方式 | 预定义,静态注册 | 动态分派,按需创建 |
| 生命周期 | 长驻内存,持久运行 | 任务级生命周期,用完即释放 |
| 上下文隔离 | 共享或部分隔离 | 完全独立,不污染主线程 |
| 工具集 | 所有 Agent 共享 | 每个子 Agent 可配置独立的 allow/deny 列表 |
| 嵌套限制 | 通常无限制 | SubagentLimitMiddleware 防止无限嵌套 |
DeerFlow 不追求一个"多 Agent 社会",而是把子 Agent 当作 可丢弃的临时计算单元——就像函数调用中的子函数,用完即走,不留下副作用。
二、Sub-Agent 注册与执行器
2.1 注册表:registry.py
DeerFlow 的子 Agent 注册表采用三层配置叠加机制:先查找内置的 general-purpose 和 bash;如果未命中,再查找用户在 config.yaml 中定义的 custom_agents;最后应用 config.yaml 中针对该子 Agent 的 per-agent 覆盖(如 timeout_seconds、max_turns、model、skills)。这种设计借鉴了 Codex 的配置分层思想:内置提供合理的默认值,用户通过配置文件覆盖而不必修改源码。get_subagent_config 的解析顺序清晰可预测:builtin → custom → override,避免了配置冲突时的不可预期行为。以下代码片段展示了核心解析逻辑:
# deerflow/subagents/registry.py
def get_subagent_config(name: str, *, app_config=None) -> SubagentConfig | None:
# Step 1: 先查找 built-in
config = BUILTIN_SUBAGENTS.get(name)
if config is None:
config = _build_custom_subagent_config(name, app_config=app_config)
if config is None:
return None
# Step 2: 应用 config.yaml 中的 per-agent 覆盖
subagents_config = _resolve_subagents_app_config(app_config)
agent_override = subagents_config.agents.get(name)
# ... 覆盖 timeout, max_turns, model, skills 2.2 执行器:executor.py
SubagentExecutor 是子 Agent 的执行引擎,核心职责是:创建独立的 LangGraph Agent、运行任务、收集结果。它接收 SubagentConfig 和父 Agent 的完整工具列表,然后根据子 Agent 的 tools(允许列表)和 disallowed_tools(拒绝列表)精确过滤出该子 Agent 可以使用的工具集。随后,它调用 _build_initial_state 组装初始状态——包括系统提示词、技能注入(以 SystemMessage 形式注入,而非直接拼接 prompt 文本)、以及从父线程继承的 sandbox_state 和 thread_data。
执行器的一个关键设计是 隔离事件循环:当父线程已经运行在一个 asyncio 事件循环中时,子 Agent 的执行会转交给一个持久化的独立事件循环(_isolated_subagent_loop),避免事件循环嵌套冲突,尤其是共享的 HTTP 客户端(如 httpx)不会因临时 loop 关闭而失效。以下代码展示了核心执行流程:
# deerflow/subagents/executor.py
async def _aexecute(self, task, result_holder=None) -> SubagentResult:
state, final_tools, deferred = await self._build_initial_state(task)
agent = self._create_agent(final_tools, deferred_setup=deferred)
# Token 收集器挂载到 run_config
collector = SubagentTokenCollector(caller=f"subagent:{self.config.name}")
run_config = {
"recursion_limit": self.config.max_turns,
"callbacks": [collector],
}
# 流式执行 + 协作取消
async for chunk in agent.astream(state, config=run_config):
if result.cancel_event.is_set():
result.try_set_terminal(SubagentStatus.CANCELLED)
return result
# ... 提取并去重 AI 消息
result.try_set_terminal(SubagentStatus.COMPLETED)
return result2.3 内置子 Agent
DeerFlow 目前内置了两个子 Agent,分别面向不同场景。general-purpose 是通用复杂任务专家,允许继承父 Agent 的几乎所有工具(disallowed_tools 中只有 task),适合需要多步推理、文件修改和工具调用的复杂工作流,最大轮数为 150。bash 则是命令执行专家,工具集被严格限制在 bash、ls、read_file、write_file 和 str_replace 五个沙箱操作工具上,最大轮数为 60,专门用于运行构建脚本、执行测试、操作 Git 等命令密集型任务。
| 子 Agent | 用途 | 允许工具 | 最大轮数 |
|---|---|---|---|
general-purpose | 复杂多步任务,需要探索+修改 | 继承所有工具(除 task) | 150 |
bash | 命令执行、构建、测试、Git 操作 | bash, ls, read_file, write_file, str_replace | 60 |
两个内置子 Agent 的 disallowed_tools 都默认包含 task,这是防止子 Agent 继续分派子 Agent,避免无限嵌套。
2.4 Token 用量归因:token_collector.py
子 Agent 完成后,它的 Token 消耗必须被准确归因到主线程的计费系统中。DeerFlow 为此设计了 SubagentTokenCollector,它是一个轻量级的 LangChain callback handler,挂载在子 Agent 的 run_config["callbacks"] 上。每当子 Agent 中的 LLM 调用完成时,on_llm_end 会被触发, collector 从 response.generations 中提取 usage_metadata,记录 input_tokens、output_tokens 和实际使用的 model_name。同时,通过 _counted_run_ids 集合对 run_id 去重,避免同一调用被重复统计。
任务完成后,这些记录通过 task_tool 中的 _report_subagent_usage 回写到父 Agent 的 RunJournal,实现跨层级的 Token 用量聚合。
三、Sub-Agent 执行流程
3.1 分派:Lead Agent 通过 task_tool 调用子 Agent
当 Lead Agent 决定使用子 Agent 时,它会调用 task_tool,参数包括:
description:任务的简短描述(用于日志和 SSE 展示)prompt:传给子 Agent 的具体任务指令subagent_type:子 Agent 类型(如general-purpose或bash)
task_tool 会做以下事情:
- 从注册表获取
SubagentConfig; - 创建
SubagentExecutor,传入过滤后的工具集; - 启动后台异步执行(
execute_async),不阻塞主线程; - 通过 SSE 向客户端发送
task_started、task_running(实时 AI 消息)、task_completed等事件。
3.2 隔离:每个子 Agent 的独立环境
子 Agent 虽然"住在"同一个进程里,但拥有完全独立的:
- 上下文:独立的
messages列表,不会污染主线程对话历史; - 工具集:通过
allowed_tools和disallowed_tools精确控制; - 终止条件:
max_turns(最大轮数)和timeout_seconds(超时时间)独立配置; - 追踪 ID:继承父线程的
trace_id,但生成自己的task_id。
3.3 并行与收敛
当任务之间无依赖时,Lead Agent 可以并行分派多个子 Agent。每个子 Agent 在独立的线程池(subagent-scheduler-)中运行,通过全局的 _background_tasks 字典管理结果。
sequenceDiagram
participant Lead as Lead Agent
participant TT as task_tool
participant BE as Background Executor
participant Sub as Subagent
participant Client as 客户端
Lead->>TT: 调用 task_tool(prompt, subagent_type)
TT->>BE: execute_async(prompt)
BE->>Sub: 在独立线程中运行
TT->>Client: SSE: task_started
loop 每 5 秒轮询
TT->>BE: get_background_task_result()
BE-->>TT: 状态 + 新消息
TT->>Client: SSE: task_running (AI 消息)
end
Sub-->>BE: 完成/失败/超时
TT->>Client: SSE: task_completed
TT-->>Lead: 结构化结果 3.4 限制:SubagentLimitMiddleware 防止无限嵌套
flowchart LR
A[Lead Agent] -->|task_tool| B[子 Agent 1]
B -->|禁止 task| C[子 Agent 无法继续嵌套]
A -->|task_tool| D[子 Agent 2]
D -->|禁止 task| E[子 Agent 无法继续嵌套]在 factory.py 中,SubagentLimitMiddleware 被配置为中间件链的第 11 环。它监控子 Agent 的嵌套深度,确保不会出现"子 Agent 又分派子 Agent"的无限递归。同时,每个子 Agent 的 disallowed_tools 默认都包含 task,这是软件层面的最后一道防线。
四、Sandbox 架构:三级隔离
如果说 Sub-Agent 是"任务层面的隔离",那么 Sandbox 就是"资源层面的隔离"。DeerFlow 提供了三种沙箱隔离级别,从开发到生产形成渐进的安全阶梯。
4.1 Sandbox 抽象基类
所有沙箱实现都基于统一的抽象接口 Sandbox(定义在 sandbox.py),无论底层是本地进程还是 Docker 容器,工具层看到的 API 完全一致。它定义了六个核心操作:execute_command 用于在沙箱内执行 shell 命令;read_file 和 write_file 处理文本文件;list_dir、glob 和 grep 提供目录浏览和代码搜索能力。此外,还有 download_file 和 update_file 用于二进制内容的读写。这种抽象使得上层工具代码无需关心沙箱的具体实现,实现了真正的"一次编写,多处运行"。
4.2 三级沙箱对比
| 维度 | LocalSandboxProvider | AioSandboxProvider (Docker) | AioSandboxProvider (K8s) |
|---|---|---|---|
| 隔离级别 | 进程内直接执行 | Docker 容器隔离 | K8s Pod 隔离 |
| 适用环境 | 本地开发(make dev) | 单节点生产 | 多节点集群生产 |
| 启动速度 | 即时(无冷启动) | 数秒(容器启动) | 数十秒(Pod 调度) |
| 资源开销 | 无额外开销 | 一个容器 / 线程 | 一个 Pod / 线程 |
| 安全边界 | 弱(共享宿主机) | 中(容器隔离) | 强(Pod + 网络隔离) |
| bash 可用性 | 默认禁用(allow_host_bash) | 容器内 bash 安全可用 | 容器内 bash 安全可用 |
| 路径映射 | 本地路径直接映射 | bind-mount 到容器 | 通过 provisioner 挂载 PVC |
4.3 LocalSandboxProvider:开发环境的单例
本地沙箱直接使用 subprocess.run 在宿主机上执行命令,文件操作通过 pathlib 直接读写。为了支撑 /mnt/user-data/... 的虚拟路径约定,它实现了精巧的正则替换系统:
本地沙箱直接使用 subprocess.run 在宿主机上执行命令,文件操作通过 pathlib 直接读写。为了支撑 /mnt/user-data/... 的虚拟路径约定,它实现了一个精巧的路径解析系统:LocalSandbox._resolve_path_with_mapping 方法接收虚拟路径,通过最长前缀匹配找到对应的 PathMapping,将其转换为本地绝对路径。同时,它使用 resolved_path.relative_to(local_root) 检查路径是否逃逸出了挂载目录的根范围——如果 .. 遍历导致路径越界,会立即抛出 PermissionError。这种"正向解析 + 越界检测"的双保险,确保了即使在 Local 模式下,恶意或错误的路径也无法访问不该访问的宿主机目录。LocalSandboxProvider 使用 LRU 缓存管理 per-thread 的 LocalSandbox 实例,默认上限 256 个线程。当缓存满时,最久未使用的线程沙箱被驱逐,下次访问时重建。
4.4 AioSandboxProvider:生产级容器隔离
AioSandboxProvider 是 DeerFlow 生产环境的核心。它支持两种后端模式:
LocalContainerBackend:在本地 Docker(或 macOS 的 Apple Container)中启动 sandbox 容器。核心流程:
- 基于
thread_id和user_id生成确定性 sandbox ID(SHA256 哈希前 8 位); - 通过文件锁(
flock/msvcrt.locking)实现跨进程互斥,防止同名容器冲突; - 分配空闲端口,启动容器,绑定工作目录 mount;
- 健康检查通过后,返回 sandbox URL。
RemoteSandboxBackend:将容器生命周期委托给 K8s Provisioner。Provisioner 在 k3s 中动态创建 Pod + NodePort Service,Backend 通过 HTTP API 与 Provisioner 通信。这种模式下,DeerFlow Gateway 本身不需要访问 Docker Daemon,只需一个 provisioner URL。
4.5 虚拟路径映射
无论哪种沙箱,都遵循统一的虚拟路径约定:
| 虚拟路径 | 物理路径(示例) | 用途 |
|---|---|---|
/mnt/user-data/workspace | threads/{thread_id}/user-data/workspace | 工作目录,代码读写 |
/mnt/user-data/uploads | threads/{thread_id}/user-data/uploads | 用户上传文件 |
/mnt/user-data/outputs | threads/{thread_id}/user-data/outputs | 输出产物 |
/mnt/skills | skills/ | 技能目录,只读 |
/mnt/acp-workspace | threads/{thread_id}/acp-workspace | ACP 子进程工作区 |
对于 LocalSandboxProvider,这些路径通过 PathMapping 在 Python 层做正则替换;对于 AioSandboxProvider,它们通过 Docker 的 --mount 参数 bind-mount 到容器内。
五、安全模型
5.1 Local 模式的安全边界
LocalSandbox 最大的风险是它与 DeerFlow Gateway 共享宿主机。一个恶意的 bash 命令可能直接删除宿主机文件、访问环境变量中的密钥、或者发起网络攻击。
因此,security.py 中有明确的策略:
LocalSandbox 最大的风险是它与 DeerFlow Gateway 共享宿主机。一个恶意的 bash 命令可能直接删除宿主机文件、访问环境变量中的密钥、或者发起网络攻击。因此,security.py 中实现了严格的策略判断:is_host_bash_allowed 函数会检查当前是否使用了 LocalSandboxProvider,如果是,则默认返回 False,除非用户在 config.yaml 中显式设置了 sandbox.allow_host_bash: true。当使用 LocalSandboxProvider 时,bash 子 Agent 默认被禁用,且 bash 工具也会提示安全警告。只有在完全可信的本地开发环境中,通过显式配置才能启用。这种"默认关闭、显式开启"的安全哲学,与 Linux 的 sudo 设计理念异曲同工。
5.2 Docker 模式的安全增强
在 Docker 模式下,安全模型大幅增强:
- 容器隔离:子 Agent 的命令运行在独立容器内,即使执行
rm -rf /,也只会影响容器本身; - 路径遍历防护:
download_file和文件操作都会检查..和路径前缀,拒绝越界访问; - 只读挂载:
/mnt/skills默认以read_only=True挂载,防止技能文件被篡改; - 命令锁:
AioSandbox使用threading.Lock串行化命令执行,防止并发请求损坏容器的单一会话。
5.3 文件操作锁:file_operation_lock.py
当多个子 Agent 并行运行时,它们可能同时读写同一个文件。DeerFlow 使用 WeakValueDictionary 实现了一个按 (sandbox_id, path) 粒度的文件锁:
当多个子 Agent 并行运行时,它们可能同时读写同一个文件。DeerFlow 使用 WeakValueDictionary 实现了一个按 (sandbox_id, path) 粒度的文件锁。get_file_operation_lock 根据沙箱 ID 和文件路径生成唯一的锁键,如果不存在则创建新的 threading.Lock。使用弱引用的好处是:当没有任何线程持有某个锁时,它会自动从字典中移除,防止长期运行的 Gateway 进程积累无限多的锁对象。这个设计在并发安全性和内存效率之间取得了很好的平衡。以下是实现核心:
# deerflow/sandbox/file_operation_lock.py
_FILE_OPERATION_LOCKS: weakref.WeakValueDictionary = weakref.WeakValueDictionary()
def get_file_operation_lock(sandbox: Sandbox, path: str) -> threading.Lock:
lock_key = (sandbox.id, path)
with _FILE_OPERATION_LOCKS_GUARD:
lock = _FILE_OPERATION_LOCKS.get(lock_key)
if lock is None:
lock = threading.Lock()
_FILE_OPERATION_LOCKS[lock_key] = lock
return lock5.4 沙箱生命周期与资源回收
AioSandboxProvider 实现了完善的资源管理:
- Warm Pool:释放的 sandbox 容器不会被立即销毁,而是进入温池,供同一线程的下次对话快速复用(无冷启动);
- Idle Checker:后台线程每 60 秒检查一次,销毁空闲超过
idle_timeout(默认 600 秒)的容器; - Replica 限制:
replicas(默认 3)是软上限,只有温池中的容器会被驱逐,活跃容器不会强制停止; - Orphan Reconciliation:Gateway 启动时,会自动"收养"上次运行遗留的容器,防止进程崩溃后容器永远挂在那里。
六、文件系统与工具执行
6.1 核心工具集
所有沙箱实现都支持以下工具,构成子 Agent 的"基础设施层":
| 工具 | 功能 | 沙箱层调用 |
|---|---|---|
bash | 执行 Shell 命令 | sandbox.execute_command() |
read_file | 读取文件内容 | sandbox.read_file() |
write_file | 写入/覆盖文件 | sandbox.write_file() |
str_replace | 精确替换文件片段 | 内部读取 + 写入 |
ls | 列出目录内容 | sandbox.list_dir() |
glob | 按模式匹配文件 | sandbox.glob() |
grep | 在文件中搜索文本 | sandbox.grep() |
6.2 文件转换:markitdown
当用户上传 PDF、DOCX 等文件时,DeerFlow 会在 UploadsMiddleware 中自动调用 markitdown 将其转换为 Markdown。这使得子 Agent 可以直接用 read_file 读取结构化文本内容,而不必处理复杂的二进制格式。
6.3 路径的双向解析
本地沙箱的一个精巧设计是路径的双向解析:
- 正向解析:子 Agent 在 prompt 中写的
/mnt/user-data/workspace/foo.py,在执行前会被替换为真实的本地路径C:\Users\...\workspace\foo.py; - 反向解析:命令的输出(如
ls的结果)中的本地路径,会被替换回虚拟路径,让子 Agent 看到的始终是一致的虚拟文件系统。
这种设计让子 Agent 的 prompt 和输出可以透明地在不同沙箱实现之间迁移——从 Local 切到 Docker,子 Agent 的"世界"没有任何变化。
总结
graph TD
A[DeerFlow Sub-Agent 架构] --> B[设计哲学: 任务分解 + 并行 + 隔离]
A --> C[执行层: Registry + Executor + TokenCollector]
A --> D[编排层: task_tool + 后台执行 + SSE 流式]
A --> E[安全层: Local/Docker/K8s 三级沙箱]
A --> F[资源层: Warm Pool + Idle Checker + File Lock]
B --> B1[独立上下文]
B --> B2[独立工具集]
B --> B3[防止无限嵌套]
C --> C1[内置 + 自定义注册]
C --> C2[LangGraph Agent 实例化]
C --> C3[Token 归因回传]
D --> D1[execute_async 后台运行]
D --> D2[5s 轮询 + 实时 SSE]
D --> D3[协作取消机制]
E --> E1[Local: 开发环境]
E --> E2[Docker: 容器隔离]
E --> E3[K8s: Pod 隔离]核心要点
- Sub-Agent 是"临时计算单元":不是常驻的社会化 Agent,而是按需创建、用完即走的函数式子任务。
- 三层配置叠加:Built-in 默认值 + Custom 自定义 + Per-agent 覆盖,兼顾开箱即用和灵活扩展。
- 隔离事件循环:
_isolated_subagent_loop确保子 Agent 在 async 环境中稳定运行,不破坏共享 HTTP 客户端。 - Token 用量必须归因:
SubagentTokenCollector+task_tool的回写机制,确保子 Agent 的消耗不会"漏记"。 - 三级沙箱渐进安全:从 Local 开发到 Docker 单节点到 K8s 集群,安全边界逐级增强,但 API 完全一致。
- 路径是虚拟的,安全是真实的:无论底层是本地文件系统还是容器,子 Agent 看到的始终是
/mnt/user-data/...,而越界访问会被拒绝。
小林后来在公司内部分享会上说:"DeerFlow 的 Sub-Agent 和沙箱设计,让我第一次理解了什么叫’把复杂任务交给专家,把专家关在安全的房间里’。"
参考链接:
- DeerFlow 官方仓库:https://github.com/bytedance/deer-flow
deerflow/subagents/— 子 Agent 注册与执行器源码deerflow/sandbox/— 沙箱抽象与 Provider 实现deerflow/community/aio_sandbox/— Docker/K8s 隔离沙箱实现docs/ARCHITECTURE.md— 架构文档(Sandbox & Sub-Agent 章节)
本章完。下一篇:DeerFlow Memory、Channels 与部署——持久化记忆与全渠道覆盖。