学 AI 产品 · 专业 AI 产品经理播客第 4 章 · T4 解剖 DeepSeek Harness:一切皆插件的 Agent 底座 · EP 13
第 4 章 · EP 13

Subagent 是一个 seam:从进程内到委派 Claude Code

时长 15:10音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场 · 子 Agent 不是功能,是接缝1:40
1:40六个实现,一个接口1:50
3:30输入和输出各是什么1:48
5:19fork 为什么是独立实现1:56
7:15能力门闩 · 启动前就报错1:14
8:30为什么不接受后降级1:26
9:56两种生命周期1:48
11:45三家三种抽象1:56
13:41可带走的原则1:28
解读全文

dsh13 · Subagent 是一个 seam:从进程内到委派 Claude Code

  • 模块:T4 解剖 DeepSeek Harness:一切皆插件的 Agent 底座
  • 集页:https://xueai-podcast.pages.dev/t/dsh13/
  • 取材课节:Subagent 是一个 seam:从进程内到委派 Claude Code(1 节,素材 4387 字)
  • 来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本页文字稿为音频的配套解读,内容取自小山学堂课程素材,属二次演绎版本。
行号与源码核对日期为 2026-08-13。

一、本集要解决什么

不是"子 Agent 怎么用",而是一个更前置的问题:子 Agent 这个概念,应该在哪一层被定义。

先说结论:这里没有做一个单独的"子 Agent 功能",而是做了一张注册表——任何实现了 SubagentProvider 约定的传输层,都可以按名字注册进来。

做成功能 → 每一种新形态都要求改功能本身,越做越臃肿;
做成接缝 → 差异全部沉在接缝之下,接缝之上只有一个词汇表。

贯穿全篇的线索:本集真正的主角不是子 Agent,而是"接缝"这个词。后面讲的每一处设计——能力门闩、能力表、统一的输入输出——都在为同一件事服务。


二、能力地图 · 六个 provider,一个接口

出处: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 是一个独立 provider。

原因:它俩差的不只是一个开关。fork 要从父日志里切出"已完成轮次的平衡前缀"当种子,切到最后一个 turn/end 为止。进行中的轮次不平衡、回放不了,必须排除。

种子函数(七行)

出处:packages/subagent/subagent-fork-in-process/src/index.ts 第 48–54 行。

  1. 在 parent.session.events 中 findLast 类型为 turn/end 的事件;
  2. 找不到 → 返回空数组;
  3. 找到 → events.slice(0, lastEnd.seq + 1)(seq 等于数组下标,这是仅追加契约的直接推论)。
spawnfork
种子从零开始父日志切到最后一个 turn/end
差别位置—在种子怎么切,不在开关打不打

易误读点:inheritsParentContext 只是描述性字段,供工具层生成不骗人的措辞,真正的差别在种子。别把描述当实现。

两条教训

  1. 当一个布尔参数开始需要长篇注释来解释时,它多半该被拆成两个东西。
  2. 平衡前缀为什么关键:会话日志仅追加,一轮对话从开始到结束构成一个平衡单元。半路切一刀 = 种子是进行中的对话,回放会卡在没有出口的状态。切在轮次边界上不是洁癖,是回放能不能成立的前提。
切种子涉及对日志结构的理解,属于会话层,不属于"要不要"的开关层。把跨层语义塞进参数,短期省事,长期一定说不清。

五、能力门闩 · 启动前 fail loud

出处:packages/subagent/subagent/src/index.ts 第 481–495 行(报错文案逐字复刻自第 490–493 行)。

assertCapabilities 把四个可选项排成需求清单,逐项对照能力表:

请求带了要求能力表
outputSchemaoutputSchema 为真
maxDepthdepthLimit
toolFiltertoolFilter
personapersona

第一个对不上的当场抛 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,就能只对复制版的会话开放这个产品后端。


九、审查清单

  1. 子 Agent 是做成了功能还是接缝?接缝定义在哪一层?
  2. 接口两端是否都统一(入口收敛 + 出口收敛)?
  3. 可选项是否恰好覆盖"不同实现间真正会有差别"的维度?字段是否少而每项都可校验?
  4. 能力表是否静态声明且在启动前校验?
  5. 能力不匹配时是 fail loud 还是静默降级?子进程有没有被启动过?
  6. "带不带父上下文"是布尔参数还是独立实现?种子是否切在轮次边界(平衡前缀)?
  7. 描述性字段是否被误当成实现?
  8. 两种生命周期是否分开设计?可继续路径是否区分了结算通知与主动汇报的消息来源?
  9. 委派别家产品时,是否明确接受了对方的能力边界、权限模型与输出格式?

十、约束说明

  • 行号口径:基于 2026-08-13 对 deepseek-harness-master 本地仓库核对。
  • 演示口径:课程交互演示中的会话日志与 provider 切换为教学化模拟;能力表数据来自各 provider 源码,报错文案逐字复刻自源码。
  • 证据边界:关于 Claude Code 无"委派到另一产品"的结构位置、Grok 执行仍在自家进程内,均基于已公开还原源码/书稿证据。
  • 本页用途:文字稿仅供阅读,音频以集页播放器为准;题目页内容不进入音频。

十一、实践提示

提示一 · 先问接缝定义在哪一层

凡要支持多种实现,先把接口两端钉死,再让实现自由发挥。定义错层,后面全是补丁。

提示二 · 入口统一,出口统一

判断抽象好不好,看两端是否都收敛。只有一端收敛,只是把复杂度挪了个位置。

提示三 · 能力不支持就当场报错,绝不接受后降级

越早暴露越便宜;且只有当不支持就真的跑不了,你的能力表才是一份可信的合同。

提示四 · 布尔参数需要长篇注释时,就该拆

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 与静默降级的关键分界。

第四问(为什么静默降级更难排查):

  • 父 Agent 预期一个可解析结构,实际拿到一段 prose;
  • 失败点后移到"解析结果"这一很后的环节,此时子 Agent 已跑完、时间与 token 已消耗;
  • 要从一段文本反推为什么结构不对,而模糊结果看起来像是成功了——这是最难排查的一类 bug。

十三、Takeaway

  • 子 Agent 在 DSH 里是一张 provider 注册表:进程内 fork 与委派 Claude Code 走同一个接口,差异全部沉在接缝之下;
  • 接口两端统一:入口是 SubagentStartRequest + 四个可选项,出口是一条普通的工具结果;
  • fork 是独立 provider 而非布尔参数——差别在"种子怎么切"(切到最后一个 turn/end 的平衡前缀),且 inheritsParentContext 只是描述性字段;
  • 能力不匹配在启动前 fail loud,绝不接受后静默降级;
  • 想给自己的 Agent 系统加"委派给别家"的能力,先问接缝定义在哪一层,再问能力表怎么校验。

来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)

本内容改编自小山学堂课程素材,为二次演绎版本。