本页文字稿为音频的配套解读,内容取自小山学堂课程素材,属二次演绎版本。
行号与源码核对日期为 2026-08-13。
不是"子 Agent 怎么用",而是一个更前置的问题:子 Agent 这个概念,应该在哪一层被定义。
先说结论:这里没有做一个单独的"子 Agent 功能",而是做了一张注册表——任何实现了 SubagentProvider 约定的传输层,都可以按名字注册进来。
做成功能 → 每一种新形态都要求改功能本身,越做越臃肿;
做成接缝 → 差异全部沉在接缝之下,接缝之上只有一个词汇表。
贯穿全篇的线索:本集真正的主角不是子 Agent,而是"接缝"这个词。后面讲的每一处设计——能力门闩、能力表、统一的输入输出——都在为同一件事服务。
出处:docs/subsystems/subagent.zh.md 第 5–7 行。官方发行版注册了六个:
| 名字 | 形态 | 说明 |
|---|---|---|
spawn | 进程内新开 | 从零开始 |
fork | 进程内带上下文 | 从父日志切种子 |
acp | 协议桥 | — |
codex | 起真实产品 CLI 进程 | 委派给 Codex |
claude-code | 起真实产品 CLI 进程 | 委派给 Claude Code |
sdk | 远程 DSH 实例 | — |
进程内新开、带上下文复制、委派给别家产品、连远程实例——四种看起来完全不同的东西,在这张表上是平级的几行。父智能体不需要知道它面对的是哪一种。
subagent-claude-code/src/index.ts 第 62–91 行:解析 claude 可执行文件 → 在父会话的 cwd 里通过官方 Agent SDK 起一个真实 CLI 进程 → 挂到共享的 subprocess owner 之下。实现只有几十行。
但成本低 ≠ 没有代价:把任务交给别家产品,意味着接受对方的能力边界、权限模型、输出格式。这些差异不会消失,只是被推到接缝的另一侧。
接缝能做的是让你不必为每个差异改一次主干,而不是让差异本身归零。
成本从来不在接线,而在你愿不愿意接受对面的那套语义。
| 内容 | |
|---|---|
| 输入 | 工具层把委派请求组装成 SubagentStartRequest:prompt、父 Agent、取消信号 + 四个可选项 |
| 发生什么 | 服务先查所选 provider 的静态能力表,四个可选项每项都要有对应能力 flag,缺一项就在启动前抛错 |
| 输出 | 一个 SubagentRun 句柄 → 父 Agent 等 result → 落成一条普通的工具结果 |
四个可选项:outputSchema(结构化输出)、maxDepth(深度上限)、toolFilter(限工具)、persona(换人设)。
对父 Agent 来说,几种实现回来的都是同一种东西——一条工具结果。正因为出口统一,上游才完全不需要关心下游是谁。
入口统一 + 出口统一 → 中间实现随便换。 只有一端收敛的抽象,只是把复杂度挪了个位置,没有真正消除它。
这四个恰好是"不同实现之间真正会有差别"的维度——结构化输出不是谁都给得了,深度限制不是谁都做得到,工具过滤和换人设同理。
若请求对象有二十个字段、十八个在某些实现上被静默忽略,能力表就失去意义。字段少而每一项都可校验,是能力表能成立的前提。
很多框架把"带不带父上下文"做成布尔参数。这里没有——fork 是一个独立 provider。
原因:它俩差的不只是一个开关。fork 要从父日志里切出"已完成轮次的平衡前缀"当种子,切到最后一个 turn/end 为止。进行中的轮次不平衡、回放不了,必须排除。
出处:packages/subagent/subagent-fork-in-process/src/index.ts 第 48–54 行。
parent.session.events 中 findLast 类型为 turn/end 的事件;events.slice(0, lastEnd.seq + 1)(seq 等于数组下标,这是仅追加契约的直接推论)。| spawn | fork | |
|---|---|---|
| 种子 | 从零开始 | 父日志切到最后一个 turn/end |
| 差别位置 | — | 在种子怎么切,不在开关打不打 |
易误读点:inheritsParentContext 只是描述性字段,供工具层生成不骗人的措辞,真正的差别在种子。别把描述当实现。
切种子涉及对日志结构的理解,属于会话层,不属于"要不要"的开关层。把跨层语义塞进参数,短期省事,长期一定说不清。
出处:packages/subagent/subagent/src/index.ts 第 481–495 行(报错文案逐字复刻自第 490–493 行)。
assertCapabilities 把四个可选项排成需求清单,逐项对照能力表:
| 请求带了 | 要求能力表 |
|---|---|
outputSchema | outputSchema 为真 |
maxDepth | depthLimit |
toolFilter | toolFilter |
persona | persona |
第一个对不上的当场抛 SubagentError,错误文案直说哪个 provider 不支持哪个能力,错误码 UNSUPPORTED_CAPABILITY。
启动之前。 没有降级、没有警告后继续,子进程在这之前一个都不会启动。
具体例子:部署启用了 subagent_claude_code,模型发起委派时带上 outputSchema。因为 Claude Code 档的能力表是 NO_START_CAPABILITIES(subagent-claude-code/src/index.ts 第 54 行),四项能力一项都不支持 → 请求当场被拒,其 CLI 进程压根没被启动过。
取向的名字叫 fail loud(响亮地失败),对立面是"接受请求然后静默忽略"。
各 provider 能力表出处:spawn(subagent-spawn-in-process/src/index.ts 第 42 行)与 fork(同系列第 62 行)四项全支持;Codex 为第 49 行。
设想另一种做法:请求带 outputSchema,实现不支持 → 忽略 schema,照常跑,返回一段普通文本。
父智能体会拿到什么:它以为会拿到一个可解析的结构,结果拿到一段 prose。
| 层 | 后果 |
|---|---|
| 第一层(直接) | 解析失败,或解析出错误的结果 |
| 第二层(更麻烦) | 失败发生在很后面——子智能体已经跑完,时间与 token 都花掉了,要从一段文本反推为什么结构不对 |
当场报错的排查成本:一行错误信息,写着哪个 provider 不支持哪个能力,完事。
能力不支持这件事,越早暴露越便宜。 把它推迟到运行之后,你就把一个明确的错误换成了一个模糊的结果——而模糊的结果是最难排查的一类 bug,因为它看起来像是成功了。
隐藏好处:不接受后降级让能力表变成了一份可信的合同。若请求会被静默降级,能力表写了什么就不重要了;只有当不支持就真的跑不了,这张表才值得被调用方信任。
一次性 SubagentRun | 可继续 Activation | |
|---|---|---|
| 形态 | 等结果 → dispose → 结束,一锤子买卖 | 没有 run:一份持久会话 + 至多一个驻留 Activation |
| 交互 | 一次委派 → 一条结果 | send_message 追加轮次、interrupt_agent 打断、收 report |
| 适用 | 子任务能在一轮里说完 | 需要多轮推进、中途插话或纠偏 |
| 代价 | 无状态要管 | 持久会话 + 驻留生命周期 + 消息来源区分 |
可继续子智能体结算时,管理器无条件给父级投一条 subagent-settled 通知,带最终输出。它与子智能体主动的 report 用不同的消息来源 kind——transcript 不会把运行时的记账算成子智能体说的话。
一旦混在一起,回放和展示会把系统记账当成对话内容,模型看到的历史就不再干净。又一次看到那个老原则:账本各记各的。
选择标准(只有一条):这个子任务能不能在一轮里说完。能用一次性解决的,别上可继续——不要因为听起来更灵活就默认选它。
| 产品 | 抽象层次 | 特点 |
|---|---|---|
| Claude Code | 产品层 | Task 工具(AgentTool)一个入口,参数塞进各种形态:subagent_type 挑角色、run_in_background、isolation: "worktree"、model 换模型;往上还有 Coordinator Mode 与 Agent Teams。表达力很强,但每种形态都是该产品内的功能分支——委派对象永远是另一个 Claude Code 实例,"把任务交给另一个产品"这件事没有位置 |
| Grok Build | 配置解析层 | 抽成纯逻辑库 xai-grok-subagent-resolution,按 explicit override > role > persona > parent 解析生效配置(src/lib.rs 第 7–8 行),执行仍在自家 shell 进程内 |
| DeepSeek Harness | 传输层 / 位置 | 产品差异下沉到 provider 一层,接缝之上只有一个词汇表 |
Grok 抽象的是"子 Agent 长什么样";DSH 抽象的是"子 Agent 跑在哪"。
抽象"长什么样" → 能换角色、人设、深度;抽象"跑在哪" → 能换进程、产品、机器。
两者不冲突,但抽象层次不同,能替换的东西就不同。
发行版四个官方 preset 里,codex 与 claude-code 的委派工具行都带着 disabled: true 出厂(apps/cli/config/agent-presets/standard/agent.cordis.yml 第 200–219 行),注释写明:复制 preset、删掉 disabled,就能只对复制版的会话开放这个产品后端。
deepseek-harness-master 本地仓库核对。凡要支持多种实现,先把接口两端钉死,再让实现自由发挥。定义错层,后面全是补丁。
判断抽象好不好,看两端是否都收敛。只有一端收敛,只是把复杂度挪了个位置。
越早暴露越便宜;且只有当不支持就真的跑不了,你的能力表才是一份可信的合同。
fork 与 spawn 的差别在种子怎么切,不在开关打不打。把跨层语义塞进参数,短期省事、长期说不清。
判断依据只有一条:子任务能否在一轮里说完。可继续要额外处理结算通知与消息来源,让账本和对话各走各的通道。
部署启用了subagent_claude_code工具,模型发起委派时带上了outputSchema。请推演:错误在哪一层抛出、错误码是什么、Claude Code 的 CLI 进程有没有被启动过?若选择接受请求但忽略 schema,父 Agent 拿到的工具结果会出什么问题,为什么这比当场报错更难排查?
第一问:错误在服务层 assertCapabilities(packages/subagent/subagent/src/index.ts 第 481–495 行)抛出,早于任何 provider 的启动逻辑。
第二问:错误码 UNSUPPORTED_CAPABILITY,文案形如 subagent provider "claude-code" does not support the "persona" capability(按实际请求的能力项替换)。
第三问:没有被启动过。能力门闩在任何子进程启动之前就抛错——这是 fail loud 与静默降级的关键分界。
第四问(为什么静默降级更难排查):
SubagentStartRequest + 四个可选项,出口是一条普通的工具结果;turn/end 的平衡前缀),且 inheritsParentContext 只是描述性字段;来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂课程素材,为二次演绎版本。