中文资讯 掘金 AI Wed, 16 Sep 2026 正文 ok 854 words

DeepSeek Harness 系列(08):多 Agent 协作——Subagent 与 Agent Teams

英文标题:DeepSeek Harness 系列(08):多 Agent 协作——Subagent 与 Agent Teams

一个 Agent 调用另一个 Agent,结果可以是文本,也可以是结构化 JSON。这篇文章讲 dsh 的多 Agent 机制:Subagent 的启动、可继续子 Agent、工具过滤
打开原文相关 5 · 机会 1
media3.2 KB media589 B media6.3 KB

中文

一个工具解决不了的问题

假设有个任务:对一个大型代码库做全面重构——检查所有文件的命名规范、找出所有循环依赖、整理接口定义。文件数量超过一千个。

你可以把这个任务交给一个 Agent,让它一个文件一个文件地处理。但问题很快出现:

  • 上下文窗口有限 :处理到第 300 个文件时,前面的内容已经被推出窗口了
  • 工具没有 reasoning 能力 :工具只能执行固定逻辑,无法根据"这个文件属于哪个子系统"来调整策略
  • 工具没有独立的对话历史 :工具执行完就结束了,没有状态保留,下一次调用完全不记得上次看到了什么

多 Agent 的本质,就是把子任务委派给另一个有独立对话历史的 Agent。子 Agent 有自己的 Session,有自己的 reasoning 过程,可以用工具,有自己的上下文窗口。父 Agent 只需要告诉子 Agent"去处理这一批文件,告诉我结果"。

这和单纯调用工具的区别,就像是把一件事"外包给另一个人",而不是"启动一台机器"。


dsh 的 Subagent 机制

dsh 通过 Subagent 实现 Agent 委派。有两种启动方式:

  1. 模型调用 :模型决定委派某个子任务时,调用内置工具 subagent_spawn
  2. 代码调用 :在工具的 execute 函数里,通过 ctx.subagents.start() 直接启动

启动子 Agent 时,需要提供一个 SubagentStartRequest,关键字段如下:

interface SubagentStartRequest {
  
  prompt: ContentBlock[]

  
  parent: Agent

  
  signal: AbortSignal

  
  toolFilter?: ToolRestriction

  
  persona?: string

  
  outputSchema?: ObjectJsonSchema

  
  maxDepth?: number
}

重点看几个字段:

toolFilter :子 Agent 默认继承父 Agent 的所有工具,但大多数情况下你只想给它一个受限的工具集。比如做代码分析的子 Agent,只需要读文件,不需要写文件。

persona :子 Agent 可以有自己的系统提示词前缀。你可以给它一个专门的角色定义,让它在那个角色里工作,而不是继承全局 Persona。

outputSchema :这个字段让子 Agent 的输出不再是纯文本,而是结构化的 JSON 对象。父 Agent 拿到的 result.structured 是类型安全的。


一次性 vs 可继续

Subagent 有两种生命周期模式,区别在于父 Agent 是否需要多次和子 Agent"对话"。

一次性 Subagent(One-shot) :

启动 → 子 Agent 完成所有工作 → 返回结果 → 结束。整个过程就一个来回,适合独立的子任务。

可继续 Subagent(Continuable) :

子 Agent 启动后,会创建一个持久化的 Session。父 Agent 可以继续向它发消息,子 Agent 每次收到消息都会继续执行。适合需要多次往返的协作。

两种模式的对比:

一次性 Subagent:
  Parent ──start()──► Child 执行完整任务 ──result──► Parent
  (子 Agent 执行完毕后生命周期结束)

可继续 Subagent:
  Parent ──startContinuable()──► Child Session 创建,进入等待
  Parent ──sendMessage('第一批文件')──► Child 处理第一批 ──result──► Parent
  Parent ──sendMessage('第二批文件')──► Child 处理第二批 ──result──► Parent
  Parent ──sendMessage('汇总结果')───► Child 汇总 ──final result──► Parent
  (子 Agent Session 在整个过程中保持活跃,有完整的对话历史)

可继续 Subagent 的优势在于:子 Agent 的对话历史是连续的,它能记住前几轮处理了什么,在汇总阶段可以利用这些记忆。


通讯路径:谁能给谁发消息

多 Agent 场景下,一个自然的问题是:Agent 之间可以任意通讯吗?

答案是不能 。dsh 对通讯路径有明确的约束:

通讯方向 是否允许 说明
父 → 子 ✅ 允许 要求子 Agent 的 parentSession 指向父 Agent
子 → 父 ✅ 允许 子 Agent 可以向父 Agent 发消息
兄弟之间 ❌ 不允许 同父的两个子 Agent 不能互发消息
跨代(祖父 → 孙子) ❌ 不允许 隔代通讯被拒绝
       Parent
      /      \
  Child A   Child B
     |
   Grandchild

✅ Parent ──► Child A
✅ Child A ──► Parent
❌ Child A ──► Child B(兄弟不能通讯)
❌ Parent ──► Grandchild(跨代不行)

这个约束是有意为之的。允许任意通讯会让 Agent 网络变成一个难以追踪的消息图,调试时完全不知道消息从哪里来的。层级清晰,每个 Agent 的责任边界才清晰。


toolFilter 与 persona

工具过滤

给子 Agent 只分配它需要的工具,是多 Agent 系统里最重要的权限控制手段:

toolFilter: {
  allow: ['read_file', 'search_files', 'list_files'],
  
  
}

为什么要这样做?

原因不只是安全,更是任务专注度 。一个分析代码的子 Agent,如果工具列表里有 write_file,模型可能会"顺手"修改它觉得有问题的代码,这不是你期望的行为。给它只读工具,它就只能读。

Persona

persona: 'You are a specialized test writer. Focus only on writing unit tests for the given code. Do not modify existing source files, do not suggest refactoring.'

Persona 的意义在于:父 Agent 可以是一个通用助手,而它启动的子 Agent 可以是一个极度专注于单一任务的专家。每次创建子 Agent 都重新定义角色,不受全局配置的干扰。


结构化输出(outputSchema)

子 Agent 默认返回文本。但在自动化流水线里,父 Agent 需要的往往是可以直接处理的数据,而不是一段自然语言描述。

outputSchema 解决这个问题:

const result = await ctx.subagents.start({
  prompt: [{ type: 'text', text: '分析这段代码,找出所有 bug。' }],
  parent: currentAgent,
  signal,
  outputSchema: {
    type: 'object',
    properties: {
      bugs: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            file:        { type: 'string' },
            line:        { type: 'number' },
            severity:    { type: 'string', enum: ['critical', 'major', 'minor'] },
            description: { type: 'string' },
          },
        },
      },
    },
  },
})



console.log(result.structured.bugs)

这让多 Agent 系统的数据流变得干净:子 Agent 的分析结果直接成为父 Agent 下一步决策的输入,不需要再加一层文本解析。


六种 Subagent Provider

dsh 支持多种底层实现,满足不同的部署场景:

Provider 说明 适用场景
spawn-in-process 在同一进程内创建新 Agent 实例 本地开发、测试
fork-in-process Fork 当前 Session,子 Agent 继承前缀历史 需要继承上下文的子任务
dsh-sdk 通过 dsh SDK 启动独立运行时 需要完全隔离的子 Agent
acp 通过 ACP 协议与远程 Agent 通讯 远程 Agent 集群
codex 调用 Codex Agent 代码专项任务
claude-code 调用 Claude Code 代码专项任务

大多数本地开发场景用 spawn-in-process 就够了。需要隔离(比如不同子 Agent 有不同的文件系统权限)时,考虑 dsh-sdk。


实验性功能:Agent Teams

Subagent 是层级模型——父子关系明确,通讯路径严格。但有些协作场景天然是"平等的":多个专家 Agent 围绕同一个任务并行工作,谁的结果先出来谁先汇报。

Agent Teams 是 dsh 正在开发的多 Agent 协作框架,通过 ctx.agentTeams(实验性服务)访问:

  • roster(花名册) :注册团队成员,每个成员有名字和角色
  • mailbox(邮箱) :成员之间通过邮箱收发消息
  • task board(任务板) :团队共享的任务列表,成员可以认领和提交任务
  • 协调者模式 :可以有一个协调者 Agent 分发任务、汇总结果

和 Subagent 的层级模型相比,Teams 更接近"团队协作":没有严格的父子层级,成员之间可以平等通讯,任务可以动态分配。

Subagent 模型(层级):        Agent Teams 模型(平等):

     Orchestrator                   ┌──────────────────┐
    /     |      \                  │    task board     │
  子A    子B    子C                 └──────────────────┘
(严格父子关系,                  Agent A ◄──► Agent B
 单向委派)                         │              │
                                   └────► Agent C ◄┘
                                   (平等通讯,动态协作)

注意 :Agent Teams 目前是实验性功能,API 可能在后续版本中变化。生产环境中使用前请确认版本稳定性。


实战:主 Agent 调用子 Agent

下面是一个完整的模式示例:主 Agent 定义一个工具,该工具内部启动子 Agent 完成专项分析任务。

const analyzeCodebaseTool = defineTool({
  name: 'analyze_codebase',
  description: '启动一个专门的子 Agent,对指定代码库目录做全面分析。',
  parameters: {
    directory:   { type: 'string', required: true, description: '要分析的目录路径' },
    focus:       { type: 'string', required: true, description: '分析重点(例如:命名规范、依赖关系)' },
  },

  async execute(args, exec) {
    
    const result = await ctx.subagents.start({
      prompt: [{
        type: 'text',
        text: `分析目录 ${args.directory},重点关注:${args.focus}。按文件逐一检查,汇总问题清单。`,
      }],
      parent: exec.agent,   
      signal: exec.signal,  

      
      toolFilter: {
        allow: ['read_file', 'list_files', 'search_files'],
      },

      
      persona: 'You are a code review expert. Read code carefully, identify issues, and provide a structured summary. Do not modify any files.',

      
      outputSchema: {
        type: 'object',
        properties: {
          issues: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                file:        { type: 'string' },
                issue:       { type: 'string' },
                severity:    { type: 'string', enum: ['high', 'medium', 'low'] },
              },
            },
          },
          summary: { type: 'string' },
        },
      },
    })

    
    const { issues, summary } = result.structured
    return `分析完成。共发现 ${issues.length} 个问题。\n${summary}`
  },
})

这个模式的核心价值:父 Agent 不需要知道子 Agent 怎么分析代码,它只需要拿到结构化结果,继续做后续决策。


和其他框架的对比

框架 多 Agent 模型 与 dsh 的主要区别
Claude Code Agent tool 也是 spawn subagent 接口更简单,但没有 toolFilter / persona / outputSchema
LangGraph 图状工作流,固定拓扑 dsh Subagent 是动态的——由模型在运行时决定何时委派,而不是预先定义的图
AutoGen 对话式多 Agent,Agent 直接互发消息 dsh 是层级模型,有明确父子关系;兄弟 Agent 不能直接通讯

简单说:LangGraph 适合流程固定的场景,dsh Subagent 适合流程由模型动态决定的场景。


系列下一篇

下一篇讲 可观测性 :Agent 在运行时做了什么,怎么知道?dsh 提供了哪些机制来追踪工具调用、Token 消耗、子 Agent 血缘树,以及如何在生产环境里接入日志系统。


在PrimeSkills 可以找到已在真实企业场景验证过的 AI Agent 技能和工作流,不是演示级的,是用在实际项目里的。

更多内容见我的个人主页

English / 原文

一个工具解决不了的问题

假设有个任务:对一个大型代码库做全面重构——检查所有文件的命名规范、找出所有循环依赖、整理接口定义。文件数量超过一千个。

你可以把这个任务交给一个 Agent,让它一个文件一个文件地处理。但问题很快出现:

  • 上下文窗口有限 :处理到第 300 个文件时,前面的内容已经被推出窗口了
  • 工具没有 reasoning 能力 :工具只能执行固定逻辑,无法根据"这个文件属于哪个子系统"来调整策略
  • 工具没有独立的对话历史 :工具执行完就结束了,没有状态保留,下一次调用完全不记得上次看到了什么

多 Agent 的本质,就是把子任务委派给另一个有独立对话历史的 Agent。子 Agent 有自己的 Session,有自己的 reasoning 过程,可以用工具,有自己的上下文窗口。父 Agent 只需要告诉子 Agent"去处理这一批文件,告诉我结果"。

这和单纯调用工具的区别,就像是把一件事"外包给另一个人",而不是"启动一台机器"。


dsh 的 Subagent 机制

dsh 通过 Subagent 实现 Agent 委派。有两种启动方式:

  1. 模型调用 :模型决定委派某个子任务时,调用内置工具 subagent_spawn
  2. 代码调用 :在工具的 execute 函数里,通过 ctx.subagents.start() 直接启动

启动子 Agent 时,需要提供一个 SubagentStartRequest,关键字段如下:

interface SubagentStartRequest {
  
  prompt: ContentBlock[]

  
  parent: Agent

  
  signal: AbortSignal

  
  toolFilter?: ToolRestriction

  
  persona?: string

  
  outputSchema?: ObjectJsonSchema

  
  maxDepth?: number
}

重点看几个字段:

toolFilter :子 Agent 默认继承父 Agent 的所有工具,但大多数情况下你只想给它一个受限的工具集。比如做代码分析的子 Agent,只需要读文件,不需要写文件。

persona :子 Agent 可以有自己的系统提示词前缀。你可以给它一个专门的角色定义,让它在那个角色里工作,而不是继承全局 Persona。

outputSchema :这个字段让子 Agent 的输出不再是纯文本,而是结构化的 JSON 对象。父 Agent 拿到的 result.structured 是类型安全的。


一次性 vs 可继续

Subagent 有两种生命周期模式,区别在于父 Agent 是否需要多次和子 Agent"对话"。

一次性 Subagent(One-shot) :

启动 → 子 Agent 完成所有工作 → 返回结果 → 结束。整个过程就一个来回,适合独立的子任务。

可继续 Subagent(Continuable) :

子 Agent 启动后,会创建一个持久化的 Session。父 Agent 可以继续向它发消息,子 Agent 每次收到消息都会继续执行。适合需要多次往返的协作。

两种模式的对比:

一次性 Subagent:
  Parent ──start()──► Child 执行完整任务 ──result──► Parent
  (子 Agent 执行完毕后生命周期结束)

可继续 Subagent:
  Parent ──startContinuable()──► Child Session 创建,进入等待
  Parent ──sendMessage('第一批文件')──► Child 处理第一批 ──result──► Parent
  Parent ──sendMessage('第二批文件')──► Child 处理第二批 ──result──► Parent
  Parent ──sendMessage('汇总结果')───► Child 汇总 ──final result──► Parent
  (子 Agent Session 在整个过程中保持活跃,有完整的对话历史)

可继续 Subagent 的优势在于:子 Agent 的对话历史是连续的,它能记住前几轮处理了什么,在汇总阶段可以利用这些记忆。


通讯路径:谁能给谁发消息

多 Agent 场景下,一个自然的问题是:Agent 之间可以任意通讯吗?

答案是不能 。dsh 对通讯路径有明确的约束:

通讯方向 是否允许 说明
父 → 子 ✅ 允许 要求子 Agent 的 parentSession 指向父 Agent
子 → 父 ✅ 允许 子 Agent 可以向父 Agent 发消息
兄弟之间 ❌ 不允许 同父的两个子 Agent 不能互发消息
跨代(祖父 → 孙子) ❌ 不允许 隔代通讯被拒绝
       Parent
      /      \
  Child A   Child B
     |
   Grandchild

✅ Parent ──► Child A
✅ Child A ──► Parent
❌ Child A ──► Child B(兄弟不能通讯)
❌ Parent ──► Grandchild(跨代不行)

这个约束是有意为之的。允许任意通讯会让 Agent 网络变成一个难以追踪的消息图,调试时完全不知道消息从哪里来的。层级清晰,每个 Agent 的责任边界才清晰。


toolFilter 与 persona

工具过滤

给子 Agent 只分配它需要的工具,是多 Agent 系统里最重要的权限控制手段:

toolFilter: {
  allow: ['read_file', 'search_files', 'list_files'],
  
  
}

为什么要这样做?

原因不只是安全,更是任务专注度 。一个分析代码的子 Agent,如果工具列表里有 write_file,模型可能会"顺手"修改它觉得有问题的代码,这不是你期望的行为。给它只读工具,它就只能读。

Persona

persona: 'You are a specialized test writer. Focus only on writing unit tests for the given code. Do not modify existing source files, do not suggest refactoring.'

Persona 的意义在于:父 Agent 可以是一个通用助手,而它启动的子 Agent 可以是一个极度专注于单一任务的专家。每次创建子 Agent 都重新定义角色,不受全局配置的干扰。


结构化输出(outputSchema)

子 Agent 默认返回文本。但在自动化流水线里,父 Agent 需要的往往是可以直接处理的数据,而不是一段自然语言描述。

outputSchema 解决这个问题:

const result = await ctx.subagents.start({
  prompt: [{ type: 'text', text: '分析这段代码,找出所有 bug。' }],
  parent: currentAgent,
  signal,
  outputSchema: {
    type: 'object',
    properties: {
      bugs: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            file:        { type: 'string' },
            line:        { type: 'number' },
            severity:    { type: 'string', enum: ['critical', 'major', 'minor'] },
            description: { type: 'string' },
          },
        },
      },
    },
  },
})



console.log(result.structured.bugs)

这让多 Agent 系统的数据流变得干净:子 Agent 的分析结果直接成为父 Agent 下一步决策的输入,不需要再加一层文本解析。


六种 Subagent Provider

dsh 支持多种底层实现,满足不同的部署场景:

Provider 说明 适用场景
spawn-in-process 在同一进程内创建新 Agent 实例 本地开发、测试
fork-in-process Fork 当前 Session,子 Agent 继承前缀历史 需要继承上下文的子任务
dsh-sdk 通过 dsh SDK 启动独立运行时 需要完全隔离的子 Agent
acp 通过 ACP 协议与远程 Agent 通讯 远程 Agent 集群
codex 调用 Codex Agent 代码专项任务
claude-code 调用 Claude Code 代码专项任务

大多数本地开发场景用 spawn-in-process 就够了。需要隔离(比如不同子 Agent 有不同的文件系统权限)时,考虑 dsh-sdk。


实验性功能:Agent Teams

Subagent 是层级模型——父子关系明确,通讯路径严格。但有些协作场景天然是"平等的":多个专家 Agent 围绕同一个任务并行工作,谁的结果先出来谁先汇报。

Agent Teams 是 dsh 正在开发的多 Agent 协作框架,通过 ctx.agentTeams(实验性服务)访问:

  • roster(花名册) :注册团队成员,每个成员有名字和角色
  • mailbox(邮箱) :成员之间通过邮箱收发消息
  • task board(任务板) :团队共享的任务列表,成员可以认领和提交任务
  • 协调者模式 :可以有一个协调者 Agent 分发任务、汇总结果

和 Subagent 的层级模型相比,Teams 更接近"团队协作":没有严格的父子层级,成员之间可以平等通讯,任务可以动态分配。

Subagent 模型(层级):        Agent Teams 模型(平等):

     Orchestrator                   ┌──────────────────┐
    /     |      \                  │    task board     │
  子A    子B    子C                 └──────────────────┘
(严格父子关系,                  Agent A ◄──► Agent B
 单向委派)                         │              │
                                   └────► Agent C ◄┘
                                   (平等通讯,动态协作)

注意 :Agent Teams 目前是实验性功能,API 可能在后续版本中变化。生产环境中使用前请确认版本稳定性。


实战:主 Agent 调用子 Agent

下面是一个完整的模式示例:主 Agent 定义一个工具,该工具内部启动子 Agent 完成专项分析任务。

const analyzeCodebaseTool = defineTool({
  name: 'analyze_codebase',
  description: '启动一个专门的子 Agent,对指定代码库目录做全面分析。',
  parameters: {
    directory:   { type: 'string', required: true, description: '要分析的目录路径' },
    focus:       { type: 'string', required: true, description: '分析重点(例如:命名规范、依赖关系)' },
  },

  async execute(args, exec) {
    
    const result = await ctx.subagents.start({
      prompt: [{
        type: 'text',
        text: `分析目录 ${args.directory},重点关注:${args.focus}。按文件逐一检查,汇总问题清单。`,
      }],
      parent: exec.agent,   
      signal: exec.signal,  

      
      toolFilter: {
        allow: ['read_file', 'list_files', 'search_files'],
      },

      
      persona: 'You are a code review expert. Read code carefully, identify issues, and provide a structured summary. Do not modify any files.',

      
      outputSchema: {
        type: 'object',
        properties: {
          issues: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                file:        { type: 'string' },
                issue:       { type: 'string' },
                severity:    { type: 'string', enum: ['high', 'medium', 'low'] },
              },
            },
          },
          summary: { type: 'string' },
        },
      },
    })

    
    const { issues, summary } = result.structured
    return `分析完成。共发现 ${issues.length} 个问题。\n${summary}`
  },
})

这个模式的核心价值:父 Agent 不需要知道子 Agent 怎么分析代码,它只需要拿到结构化结果,继续做后续决策。


和其他框架的对比

框架 多 Agent 模型 与 dsh 的主要区别
Claude Code Agent tool 也是 spawn subagent 接口更简单,但没有 toolFilter / persona / outputSchema
LangGraph 图状工作流,固定拓扑 dsh Subagent 是动态的——由模型在运行时决定何时委派,而不是预先定义的图
AutoGen 对话式多 Agent,Agent 直接互发消息 dsh 是层级模型,有明确父子关系;兄弟 Agent 不能直接通讯

简单说:LangGraph 适合流程固定的场景,dsh Subagent 适合流程由模型动态决定的场景。


系列下一篇

下一篇讲 可观测性 :Agent 在运行时做了什么,怎么知道?dsh 提供了哪些机制来追踪工具调用、Token 消耗、子 Agent 血缘树,以及如何在生产环境里接入日志系统。


在PrimeSkills 可以找到已在真实企业场景验证过的 AI Agent 技能和工作流,不是演示级的,是用在实际项目里的。

更多内容见我的个人主页