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