约 6 分钟
三种 sub agent 设计:opencode、pi、deepseek-harness 怎么隔离、限权、防递归
通读三个开源 agent 项目里 sub agent 的实现,比较它们在上下文隔离、权限派生、递归防护、并发超时、返回契约上的取舍。三家一致地窄,也一致地没有超时和重试。最后说说这对数字员工之间派活意味着什么。
- sub agent
- 调研
- agent
- 秘书
2026 年 8 月,我们通读了三个开源项目里 sub agent 的实现:opencode、pi 和 deepseek-harness。结论以代码为准,文档只作参考。三家的立场差得很远,但在几个关键点上意外一致。
三家的立场
| 立场 | 形态 | |
|---|---|---|
| opencode | sub agent 是一等内置能力 | 一个内置 task 工具,子 agent 就是一条带父指针的普通会话 |
| pi | 内核明确拒绝 | README 写明 No sub-agents,唯一实现是一千行的示例扩展,靠 spawn 子进程 |
| deepseek-harness | 做成服务化接口 | 十一个包,六种 provider,一次性与可继续两条生命周期,支持冷恢复 |
pi 的拒绝不是偷懒。它把 sub agent、plan mode、todo、后台 bash、MCP、审批弹窗六样都划到工作流层之外,赌的是复杂任务的瓶颈在上下文和可回溯性,不在编排。它把力气花在压缩、会话树和工具输出截断上。
隔离:逻辑、物理,还是做成一个维度
- opencode 是逻辑隔离。子会话和父共用同一套存储、事件和权限栈,靠父指针分树。零新增基础设施,代价是子会话到处都要被过滤。
- pi 是物理隔离。独立进程,不落盘,父子之间只有命令行参数、标准输出和终止信号三条通道。隔离最彻底,跑完什么都不留。
- deepseek-harness 把隔离级别做成 provider 维度。进程内有全新启动和继承父日志前缀两种,跨进程还有四家外部 agent。最灵活,也最重。
agent 类型要不要暴露给模型
这是三家最大的分叉。
opencode 给模型一个类型参数,候选列表按调用方权限动态裁剪后拼进工具描述。被禁的 agent 直接从描述里消失,模型不知道它存在。pi 也是模型按名字选,定义只有名字、描述、工具、模型四个字段,每次调用重新扫盘。
deepseek-harness 反过来。模型面的工具只有描述和任务两个参数,没有类型。要暴露多种子 agent,就把同一个插件在配置里加载多次。设计笔记原话是:服务持有多 provider 注册表,工具挑一个,schema 不带类型。
一边是模型自选类型,一边是部署方绑定、模型只填任务。方向相反。
上下文传递:三家一致地窄
父传给子的全部内容就是一段 prompt 字符串。不传对话历史,不传文件清单。子的中间过程一律不进父上下文,父只拿到最后一条回复的文本。
差别在边角。opencode 会展开 @文件 提及,也能用任务号续跑旧子会话。pi 的 chain 模式靠字符串替换接力。deepseek-harness 的 fork 可以拿父日志的平衡前缀做种子。
防递归
| 机制 | 默认深度 | |
|---|---|---|
| opencode | 遍历父指针链现算深度,在权限询问之前就失败,再给子会话注入一条禁止派生的规则 | 1,禁止嵌套 |
| pi | 没有任何防护。不声明工具的子 agent 会加载全部扩展,包括 sub agent 工具本身,可以无限自我嵌套 | 无 |
| deepseek-harness | 委派深度写进会话头,持久且单调,重启和冷恢复都不能把子降级成顶层 | 3 |
deepseek-harness 这条最值得记住:深度必须持久化。现算的深度在跨进程恢复时会失效。
并发、超时、重试
- opencode 并发无上限,靠提示词鼓励一条消息里多个工具调用。父取消时递归取消全部后代。
- pi 并行上限八、并发四,手写 worker pool。取消是真 kill,先 SIGTERM,五秒后 SIGKILL。
- deepseek-harness 由 agent loop 自动合成并行池,没有专属上限。
三家都没有超时,都没有重试。这点意外一致。
返回契约与计费
- 三家的 token 都不上卷到父。各会话独立记账,父只为返回文本付上下文。pi 连子的花费都不进成本统计。
- 只有 deepseek-harness 有结构化输出。它不用 JSON mode,而是在子作用域内注册一把强制工具,加两阶段提交。
- 截断:pi 每个任务 50KB 硬截断,完整内容留给界面。opencode 完全不截断,子 agent 的长回复原样进父上下文。
权限在哪里钉死
opencode 的父到子权限派生是非对称的:只继承父会话的拒绝规则和外部目录规则,父 agent 自身的能力限制不外溢。这条边界被来回修过两次,最后固化成专门的回归测试。子 agent 默认不能向用户提问。
deepseek-harness 把审批策略在委派边界钉死:不管父是什么,子一律不审批。理由是没人在看子 agent 的审批弹窗。策略以事件写进子自己的日志,冷恢复重放这些事件,而不是重新抄父。模型还会收到一句固定提示:你的权限范围在启动时已固定,不要重试被拒绝的操作,在回复里说明限制。
pi 没有工具审批。它的信任边界画在 agent 来源上:默认只加载用户目录里的定义,仓库里的定义要模型显式请求且用户确认。威胁模型是仓库控制的 prompt 等价于可执行代码。
这对数字员工之间派活意味着什么
createrole 里的秘书可以把任务派给通讯录里的其他数字员工。被派的员工在自己的电脑和专属会话里干活,做完把结果和交付物送回来。从三家实现里读出几条对照:
- 超时和重启恢复我们已经有。 派出去的活有三十分钟超时,到点主动打断。服务重启后未完成的派活会从台账恢复,已上报的结果按编号去重。这两项三家都没有。
- 交付物通道是这类产品独有的。 三家的返回契约都只有文本。数字员工办公会产出文件,我们有约定目录加快照加至少一次投递去重。这是数字员工和 coding agent 的产品差异,不是缺口。
- 深度要持久化。 被派的员工自己也可能是秘书,目前我们没有深度概念。deepseek-harness 的做法是唯一在跨进程恢复下仍然成立的。
- 子输出必须截断。 opencode 不截断是公认的坑,pi 的 50KB 加完整内容留界面是干净解法。
- 被派员工向人提问的语义要显式定义。 它在专属会话里问人,没有人在看。要么像 opencode 一样默认禁止,要么像 deepseek-harness 一样钉死为不审批并告诉模型。
「agent 类型是不是模型参数」这个分叉,我们倾向 deepseek-harness 的方向。不给模型开类型自由度,就不必为每个新岗位改工具 schema。