DeerFlow Sub-Agent 编排与沙箱隔离:多手协作的 Agent 工厂

📑 目录

这是「DeerFlow 代码全景解析」专题的第 05 篇。在本系列中,我们将逐层拆解 DeerFlow 开源项目的工程架构,从任务执行层到沙箱隔离,从多 Agent 编排到安全边界,带你看清一个工业级 AI Agent 平台如何将"复杂任务"拆解为"可控的协作单元"。


故事场景:一个 PR 的"10 手协作"

小林——刚加入某 AI 创业公司的新晋工程师——接到了人生第一个来自 AI Agent 的复杂任务:

"帮我分析这个 2000 行 Python 项目,找出所有内存泄漏点,生成修复补丁,并写出测试用例验证修复。"

他盯着 DeerFlow 的终端界面,心想:这得跑多少轮对话?上下文塞得下吗?如果中途出错,前面分析的结果岂不是要重新来过?

就在他犹豫时,Lead Agent 已经开始了行动:

  1. 它先分派了一个 general-purpose 子 Agent 去"通读代码结构";
  2. 同时启动了一个 bash 子 Agent 去"运行内存分析工具";
  3. 等两者返回后,再让一个子 Agent 去"生成修复补丁";
  4. 最后让另一个子 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-purposebash;如果未命中,再查找用户在 config.yaml 中定义的 custom_agents;最后应用 config.yaml 中针对该子 Agent 的 per-agent 覆盖(如 timeout_secondsmax_turnsmodelskills)。这种设计借鉴了 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_statethread_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 result

2.3 内置子 Agent

DeerFlow 目前内置了两个子 Agent,分别面向不同场景。general-purpose 是通用复杂任务专家,允许继承父 Agent 的几乎所有工具(disallowed_tools 中只有 task),适合需要多步推理、文件修改和工具调用的复杂工作流,最大轮数为 150。bash 则是命令执行专家,工具集被严格限制在 bashlsread_filewrite_filestr_replace 五个沙箱操作工具上,最大轮数为 60,专门用于运行构建脚本、执行测试、操作 Git 等命令密集型任务。

子 Agent用途允许工具最大轮数
general-purpose复杂多步任务,需要探索+修改继承所有工具(除 task)150
bash命令执行、构建、测试、Git 操作bash, ls, read_file, write_file, str_replace60

两个内置子 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_tokensoutput_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-purposebash

task_tool 会做以下事情:

  1. 从注册表获取 SubagentConfig
  2. 创建 SubagentExecutor,传入过滤后的工具集;
  3. 启动后台异步执行execute_async),不阻塞主线程;
  4. 通过 SSE 向客户端发送 task_startedtask_running(实时 AI 消息)、task_completed 等事件。

3.2 隔离:每个子 Agent 的独立环境

子 Agent 虽然"住在"同一个进程里,但拥有完全独立的:

  • 上下文:独立的 messages 列表,不会污染主线程对话历史;
  • 工具集:通过 allowed_toolsdisallowed_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_filewrite_file 处理文本文件;list_dirglobgrep 提供目录浏览和代码搜索能力。此外,还有 download_fileupdate_file 用于二进制内容的读写。这种抽象使得上层工具代码无需关心沙箱的具体实现,实现了真正的"一次编写,多处运行"。

4.2 三级沙箱对比

维度LocalSandboxProviderAioSandboxProvider (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-threadLocalSandbox 实例,默认上限 256 个线程。当缓存满时,最久未使用的线程沙箱被驱逐,下次访问时重建。

4.4 AioSandboxProvider:生产级容器隔离

AioSandboxProvider 是 DeerFlow 生产环境的核心。它支持两种后端模式:

LocalContainerBackend:在本地 Docker(或 macOS 的 Apple Container)中启动 sandbox 容器。核心流程:

  1. 基于 thread_iduser_id 生成确定性 sandbox ID(SHA256 哈希前 8 位);
  2. 通过文件锁(flock / msvcrt.locking)实现跨进程互斥,防止同名容器冲突;
  3. 分配空闲端口,启动容器,绑定工作目录 mount;
  4. 健康检查通过后,返回 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/workspacethreads/{thread_id}/user-data/workspace工作目录,代码读写
/mnt/user-data/uploadsthreads/{thread_id}/user-data/uploads用户上传文件
/mnt/user-data/outputsthreads/{thread_id}/user-data/outputs输出产物
/mnt/skillsskills/技能目录,只读
/mnt/acp-workspacethreads/{thread_id}/acp-workspaceACP 子进程工作区

对于 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 lock

5.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 隔离]

核心要点

  1. Sub-Agent 是"临时计算单元":不是常驻的社会化 Agent,而是按需创建、用完即走的函数式子任务。
  2. 三层配置叠加:Built-in 默认值 + Custom 自定义 + Per-agent 覆盖,兼顾开箱即用和灵活扩展。
  3. 隔离事件循环_isolated_subagent_loop 确保子 Agent 在 async 环境中稳定运行,不破坏共享 HTTP 客户端。
  4. Token 用量必须归因SubagentTokenCollector + task_tool 的回写机制,确保子 Agent 的消耗不会"漏记"。
  5. 三级沙箱渐进安全:从 Local 开发到 Docker 单节点到 K8s 集群,安全边界逐级增强,但 API 完全一致。
  6. 路径是虚拟的,安全是真实的:无论底层是本地文件系统还是容器,子 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 与部署——持久化记忆与全渠道覆盖。