假设有个任务:对一个大型代码库做全面重构——检查所有文件的命名规范、找出所有循环依赖、整理接口定义。文件数量超过一千个。
你可以把这个任务交给一个 Agent,让它一个文件一个文件地处理。但问题很快出现:
- 上下文窗口有限:处理到第 300 个文件时,前面的内容已经被推出窗口了
- 工具没有 reasoning 能力:工具只能执行固定逻辑,无法根据"这个文件属于哪个子系统"来调整策略
- 工具没有独立的对话历史:工具执行完就结束了,没有状态保留,下一次调用完全不记得上次看到了什么
多 Agent 的本质,就是把子任务委派给另一个有独立对话历史的 Agent。子 Agent 有自己的 Session,有自己的 reasoning 过程,可以用工具,有自己的上下文窗口。父 Agent 只需要告诉子 Agent"去处理这一批文件,告诉我结果"。
这和单纯调用工具的区别,就像是把一件事"外包给另一个人",而不是"启动一台机器"。
dsh 的 Subagent 机制
dsh 通过 Subagent 实现 Agent 委派。有两种启动方式:
- 模型调用:模型决定委派某个子任务时,调用内置工具
subagent_spawn - 代码调用:在工具的
execute函数里,通过ctx.subagents.start()直接启动
启动子 Agent 时,需要提供一个 SubagentStartRequest,关键字段如下:
<span>// 精简自 packages/subagent/subagent/src/types.ts 的 SubagentStartRequest</span>
<span>// 实际字段更多,这里只列最核心的部分</span>
<span>interface</span> <span>SubagentStartRequest</span> {
<span>// 子 Agent 的初始提示词(ContentBlock 数组,支持文本、图片等)</span>
<span>prompt</span>: <span>ContentBlock</span>[]
<span>// 父 Agent 对象(提供工作目录、血缘关系信息)</span>
<span>parent</span>: <span>Agent</span>
<span>// 取消信号(允许父 Agent 中止子 Agent 的执行)</span>
<span>signal</span>: <span>AbortSignal</span>
<span>// 可选:限制子 Agent 可用的工具(只给它用得到的工具)</span>
toolFilter?: <span>ToolRestriction</span>
<span>// 可选:给子 Agent 一个专属的 Persona(覆盖全局部署 persona)</span>
persona?: <span>string</span>
<span>// 可选:结构化输出 schema(让子 Agent 返回 JSON 对象而非纯文本)</span>
outputSchema?: <span>ObjectJsonSchema</span>
<span>// 可选:最大委派深度(防止子 Agent 再启动子 Agent 形成无限递归)</span>
maxDepth?: <span>number</span>
}
重点看几个字段:
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 ──<span>start</span>()──► Child 执行完整任务 ──result──► Parent
(子 Agent 执行完毕后生命周期结束)
可继续 Subagent:
Parent ──<span>startContinuable</span>()──► Child Session 创建,进入等待
Parent ──<span>sendMessage</span>('第一批文件')──► Child 处理第一批 ──result──► Parent
Parent ──<span>sendMessage</span>('第二批文件')──► Child 处理第二批 ──result──► Parent
Parent ──<span>sendMessage</span>('汇总结果')───► Child 汇总 ──final result──► Parent
(子 Agent Session 在整个过程中保持活跃,有完整的对话历史)
可继续 Subagent 的优势在于:子 Agent 的对话历史是连续的,它能记住前几轮处理了什么,在汇总阶段可以利用这些记忆。
通讯路径:谁能给谁发消息
多 Agent 场景下,一个自然的问题是:Agent 之间可以任意通讯吗?
答案是不能。dsh 对通讯路径有明确的约束:
| 通讯方向 | 是否允许 | 说明 |
|---|---|---|
| 父 → 子 | ✅ 允许 | 要求子 Agent 的 `parentSession` 指向父 Agent |
| 子 → 父 | ✅ 允许 | 子 Agent 可以向父 Agent 发消息 |
| 兄弟之间 | ❌ 不允许 | 同父的两个子 Agent 不能互发消息 |
| 跨代(祖父 → 孙子) | ❌ 不允许 | 隔代通讯被拒绝 |
Parent
/ \
Child <span>A</span> Child <span>B</span>
|
Grandchild
✅ Parent ──► Child <span>A</span>
✅ Child <span>A</span> ──► Parent
❌ Child <span>A</span> ──► Child <span>B</span>(兄弟不能通讯)
❌ Parent ──► Grandchild(跨代不行)
这个约束是有意为之的。允许任意通讯会让 Agent 网络变成一个难以追踪的消息图,调试时完全不知道消息从哪里来的。层级清晰,每个 Agent 的责任边界才清晰。
toolFilter 与 persona
工具过滤
给子 Agent 只分配它需要的工具,是多 Agent 系统里最重要的权限控制手段:
<span>// 子 Agent 只能使用以下三个工具</span>
<span>// 父 Agent 拥有的其他工具(比如 write_file、execute_shell)对它不可见</span>
<span>toolFilter</span>: {
<span>allow</span>: [<span>'read_file'</span>, <span>'search_files'</span>, <span>'list_files'</span>],
<span>// 也可以用 deny 来排除特定工具</span>
<span>// deny: ['write_file', 'execute_shell']</span>
}
为什么要这样做?
原因不只是安全,更是任务专注度。一个分析代码的子 Agent,如果工具列表里有 write_file,模型可能会"顺手"修改它觉得有问题的代码,这不是你期望的行为。给它只读工具,它就只能读。
Persona
<span>// 子 Agent 使用专属的系统提示词前缀</span>
<span>// 这会覆盖全局部署 persona,让子 Agent 在一个专门的角色里工作</span>
<span>persona</span>: <span>'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.'</span>
Persona 的意义在于:父 Agent 可以是一个通用助手,而它启动的子 Agent 可以是一个极度专注于单一任务的专家。每次创建子 Agent 都重新定义角色,不受全局配置的干扰。
结构化输出(outputSchema)
子 Agent 默认返回文本。但在自动化流水线里,父 Agent 需要的往往是可以直接处理的数据,而不是一段自然语言描述。
outputSchema 解决这个问题:
<span>// 让子 Agent 返回结构化 JSON——父 Agent 不需要解析文本(伪代码)</span>
<span>const</span> result = <span>await</span> ctx.<span>subagents</span>.<span>start</span>({
<span>prompt</span>: [{ <span>type</span>: <span>'text'</span>, <span>text</span>: <span>'分析这段代码,找出所有 bug。'</span> }],
<span>parent</span>: currentAgent,
signal,
<span>outputSchema</span>: {
<span>type</span>: <span>'object'</span>,
<span>properties</span>: {
<span>bugs</span>: {
<span>type</span>: <span>'array'</span>,
<span>items</span>: {
<span>type</span>: <span>'object'</span>,
<span>properties</span>: {
<span>file</span>: { <span>type</span>: <span>'string'</span> },
<span>line</span>: { <span>type</span>: <span>'number'</span> },
<span>severity</span>: { <span>type</span>: <span>'string'</span>, <span>enum</span>: [<span>'critical'</span>, <span>'major'</span>, <span>'minor'</span>] },
<span>description</span>: { <span>type</span>: <span>'string'</span> },
},
},
},
},
},
})
<span>// result.text 是子 Agent 的文本输出(如果有)</span>
<span>// result.structured 是类型安全的 JSON 对象——直接用,不需要解析</span>
<span>console</span>.<span>log</span>(result.<span>structured</span>.<span>bugs</span>)
<span>// → [{ file: 'auth.ts', line: 42, severity: 'critical', description: '...' }, ...]</span>
这让多 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 │
子<span>A</span> 子<span>B</span> 子C └──────────────────┘
(严格父子关系, Agent <span>A</span> ◄──► Agent <span>B</span>
单向委派) │ │
└────► Agent C ◄┘
(平等通讯,动态协作)
注意:Agent Teams 目前是实验性功能,API 可能在后续版本中变化。生产环境中使用前请确认版本稳定性。
实战:主 Agent 调用子 Agent
下面是一个完整的模式示例:主 Agent 定义一个工具,该工具内部启动子 Agent 完成专项分析任务。
<span>// 伪代码:主 Agent 的工具定义,委派子 Agent 做代码分析</span>
<span>const</span> analyzeCodebaseTool = <span>defineTool</span>({
<span>name</span>: <span>'analyze_codebase'</span>,
<span>description</span>: <span>'启动一个专门的子 Agent,对指定代码库目录做全面分析。'</span>,
<span>parameters</span>: {
<span>directory</span>: { <span>type</span>: <span>'string'</span>, <span>required</span>: <span>true</span>, <span>description</span>: <span>'要分析的目录路径'</span> },
<span>focus</span>: { <span>type</span>: <span>'string'</span>, <span>required</span>: <span>true</span>, <span>description</span>: <span>'分析重点(例如:命名规范、依赖关系)'</span> },
},
<span>async</span> <span>execute</span>(<span>args, exec</span>) {
<span>// 启动子 Agent(伪代码,实际由 ctx.subagents.start() 实现)</span>
<span>const</span> result = <span>await</span> ctx.<span>subagents</span>.<span>start</span>({
<span>prompt</span>: [{
<span>type</span>: <span>'text'</span>,
<span>text</span>: <span>`分析目录 <span>${args.directory}</span>,重点关注:<span>${args.focus}</span>。按文件逐一检查,汇总问题清单。`</span>,
}],
<span>parent</span>: exec.<span>agent</span>, <span>// 传入父 Agent,建立血缘关系</span>
<span>signal</span>: exec.<span>signal</span>, <span>// 传入取消信号,父 Agent 取消时子 Agent 也停止</span>
<span>// 子 Agent 只能读文件,不能修改代码</span>
<span>toolFilter</span>: {
<span>allow</span>: [<span>'read_file'</span>, <span>'list_files'</span>, <span>'search_files'</span>],
},
<span>// 专属角色:代码审查专家</span>
<span>persona</span>: <span>'You are a code review expert. Read code carefully, identify issues, and provide a structured summary. Do not modify any files.'</span>,
<span>// 要求返回结构化 JSON</span>
<span>outputSchema</span>: {
<span>type</span>: <span>'object'</span>,
<span>properties</span>: {
<span>issues</span>: {
<span>type</span>: <span>'array'</span>,
<span>items</span>: {
<span>type</span>: <span>'object'</span>,
<span>properties</span>: {
<span>file</span>: { <span>type</span>: <span>'string'</span> },
<span>issue</span>: { <span>type</span>: <span>'string'</span> },
<span>severity</span>: { <span>type</span>: <span>'string'</span>, <span>enum</span>: [<span>'high'</span>, <span>'medium'</span>, <span>'low'</span>] },
},
},
},
<span>summary</span>: { <span>type</span>: <span>'string'</span> },
},
},
})
<span>// result.structured 是类型安全的 JSON,直接使用</span>
<span>const</span> { issues, summary } = result.<span>structured</span>
<span>return</span> <span>`分析完成。共发现 <span>${issues.length}</span> 个问题。\n<span>${summary}</span>`</span>
},
})
这个模式的核心价值:父 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 技能和工作流,不是演示级的,是用在实际项目里的。
更多内容见我的个人主页
多 Agent 协作的关键不在数量,而在权限边界与数据接口:toolFilter 控工具、outputSchema 控输出,让委派真正可控。适合需要动态分解大型任务的工程团队。