DeepSeek Harness 最狠的不是免费,是把 spawn 子 Agent 做成了协议

0 阅读18分钟

上周我让 Claude Code 干一个重构活,跑到一半发现它在偷偷开 subagent,一层套一层,等我反应过来 token 已经烧穿了三层上下文。

我盯着屏幕上那个我根本没授权的子任务,第一反应不是「模型真聪明」,而是「这玩意到底是谁派出去的,它派了几层,我怎么知道它还能不能再派」。

说实话,这是我用 Claude Code 大半年一直别扭的地方。子 Agent 很好用,但 spawn 不 spawn、嵌套多深、能不能续谈,全靠模型读 prompt 自己悟,没有硬约束。你在 subagent 的 description 里写「不要嵌套」,它心情好就听,心情不好就当没看见。

然后这周 DeepSeek 扔出了 Harness v0.1,4 天 145,505 颗星 [GitHub 2026-08-17]。我把它 subagent 子系统的类型源码从头读了一遍,读完最大的感受不是「免费真香」,而是他们把我上面那个别扭,做成了一套带类型、带能力协商、带持久化深度的显式协议。

这篇就拆这件事。不聊 npx 一键启动,不聊 star 数奇观,聊点只有翻源码才看得见的东西。

先给结论

如果你只想知道这东西跟我有没有关系,看这张表。

你是谁现在该干嘛
天天用 Claude Code subagent,被嵌套和上下文串台坑过必读,DSH 的协议设计能直接纠正你对 agent 边界的理解
在评估自建 agent runtime / 做多 agent 编排强烈建议读源码,它的 Capability Seam 抽象值得抄
想找个 Cursor 替代品写业务代码别冲动,这是 harness 不是 IDE,rc.7 还在天天 breaking
只想蹭热点发朋友圈看到这就够了,下面很硬

一句话定位,DSH 不是 Cursor 竞品,它是 Agent = Model + Harness 这个公式里的 Harness 层 [deepseek.com/harness]。模型负责思考,harness 负责给模型「理解环境、使用工具、在真实场景持续工作」的能力。Claude Code、Codex 自己也是 harness,只不过它们是封装好的产品;DSH 想做的是把 harness 本身拆开,让每一层都可替换。

DSH 在 Agent 技术栈中的分层定位,Model 与 Harness 的关系,Harness 内部 model adapter、tool registry、session log、agent loop 四大插件均挂在 Cordis 内核之上,agent loop 本身也是插件

一切皆插件,包括 agent loop 自己

要看懂子 Agent 协议,得先看懂 DSH 的骨架,因为子 Agent 在这套骨架里根本不是核心组件,而是一个「可选能力」。

DSH 基于一个叫 Cordis 的内核(cordiverse/cordis,5,366 stars),而且直接 vendored 进了仓库 [DSH vendor/]。Cordis 本身啥 agent 能力都没有,它只做一件事,插件的加载、卸载和依赖管理。在这个内核之上,DSH 把所有东西都做成了插件,model adapter 是插件,tool registry 是插件,session log 是插件,连 agent loop 本身都是插件

这句话分量很重。没有一个不可 patch 的特权核心。你想换模型,挂个 adapter 插件;你想换掉整个 agent 循环逻辑,也是挂个插件的事。扩展 DSH 不是去 fork 源码改核心,而是在别的插件旁边再挂一个,注册是 effect,插件卸载时自动回滚。

配置层叫 Profile + Bundle。一个 profile 是有序 bundle 层叠出来的插件树。dsh-base 是每个 profile 的第一层,装 model adapters、tools、persistence、sandbox、approval policy;dsh-web-app 加浏览器,dsh-headless 加一次性 runner。patch 按行 id 替换整行 config。

这里有个关键概念叫 Capability Seam,一个 seam = Service Definition(声明接口)+ Service Provider(实现)+ Consumer(通常是给模型用的工具)。三者齐备,这个能力才算在系统里「立住了」。

子 Agent,就是这样一个 seam。

子 Agent 为什么是 seam 而不是 loop 的一部分

大多数 agent 框架(包括 Claude Code)的处理方式是,把子 Agent 调用硬编码成一个工具,叫 Agent 工具,模型决定调不调、调哪个 subagent_type。这是「loop 的一部分」。

DSH 不这么干。它把 subagent 做成了一个和 bash 平级的可选能力,类型定义不在 core 里。但 subagent 和 bash 有个本质区别,bash 只允许一个 executor,你不能同时注册两个 bash 实现;subagent 却允许多个 provider 按名字共存,全部挂在 ctx.subagents 上。

这个设计的潜台词是,「怎么 spawn 一个子 agent」根本没有唯一答案。你可以在同进程里 spawn,可以 fork 一个带父上下文的,可以走远程 ACP 协议,甚至可以把整个 Claude Code 或 Codex 当成后端拉起来。框架不该替你决定,它只负责定协议。

DSH 内置了 6 个 provider [DSH 源码 packages/subagent/],我给你一个个摆出来。

Provider机制要不要父上下文
spawn-in-process同进程新建 agent不继承
fork-in-process同进程 fork,继承父已完成 turn 前缀继承 seed
acpAgent Client Protocol 远程 agent不继承
codex拉起 codex app-server --stdio不复制父对话
claude-code@anthropic-ai/claude-agent-sdk@0.3.220persistSession:false
dsh-sdkDSH 自己的 JSON-RPC SDK按 descriptor

注意后面两个,DSH 把自己的直接竞品做成了 subagent backend,这事我单独开一节讲,戏剧性很强。

ctx.subagents provider 注册表结构,按名字挂载 6 个内置 provider,分同进程组 spawn-in-process 与 fork-in-process、远程组 acp 与 dsh-sdk、外部产品进程组 codex 与 claude-code,三类共存

四个布尔值,把「能力不匹配」从运行时提前到启动时

这是我觉得整个 DSH 最值得抄的设计。每个 provider 在启动前必须声明自己支持哪些能力,就四个布尔值。

// 来源:DSH 源码 packages/subagent/src/types.ts
interface SubagentCapabilities {
  readonly outputSchema: boolean  // 能不能返回结构化输出
  readonly depthLimit: boolean    // 能不能遵守委派深度上限
  readonly toolFilter: boolean    // 能不能裁剪子 agent 的工具集
  readonly persona: boolean       // 能不能给子 agent 独立人格
}

看着简单对吧。狠的是后半句,如果父 agent 发起一个请求,要求子 agent 必须支持某个能力(比如我要你返回结构化 outputSchema),而这个 provider 声明它不支持,DSH 的做法不是「先接着,做不到拉倒」,而是启动时直接抛 SubagentError('UNSUPPORTED_CAPABILITY') 拒绝启动

官方管这叫 fail loud, no silent degradation,失败要响,不许静悄悄降级。

你可能觉得这有什么了不起。我给你看 Claude Code 的对照面。Claude Code 的 Agent 工具靠 prompt 里的 description 让模型「理解」这个 subagent 能干什么。模型读完 description,觉得它能干,就把任务派过去;结果这个 subagent 其实不支持结构化输出,或者工具集不对,它不会提前拒绝,它会「接了然后做不好」,最后给你一坨似是而非的自然语言。你排查半天才发现是能力不匹配。

DSH 把这件事从「模型靠语义猜」变成了「provider 用类型声明,运行时按契约校验」。四个布尔值就是一份合同,签字画押,做不到就别上桌。

这才是协议。

一次性 vs 可续,两种生命周期

DSH 的子 agent 有两种活法,one-shot 和 continuable。这个区分也是 Claude Code 完全没有的。

One-shot(一次性) 好理解,SubagentRun 就是一个句柄,派出去,干完,返回一个 SubagentResult,完事。没有 steering,没有 resume。结果里带 output、structured(如果请求了 outputSchema)和 stopReason,stopReason 就那几种,completed / aborted / error / max-tokens / refusal。

Continuable(可续) 才是真正有意思的东西。它是一个持久化的子 Session,可以跨多次对话甚至跨冷启动存在。一个 continuable 子 agent 最多有一个进程内 Activation(驻留期),Activation 不是 request 也不是 Task,它能在里面跑多个 FIFO turn,甚至在它自己派出去的子代还在跑的时候保持驻留。

这里有几个操作得讲清楚。

startContinuable() 先预留一个稳定的 child id,provider 返回一个 detached 的 ContinuableCreateSpec,里面只有可选的 parent-history seed,然后 continuation manager 自己造 agent、建 inbox、提交初始 prompt。注意,provider 不参与 continuable 的后续生命周期。冷恢复根本不经过 provider,manager 拿通用 descriptor 加 ctx.agents.resume() 就把它拉起来了。

followup() 是唯一的续消息操作。如果 Activation 在 running,就入队;如果在 waiting,就唤醒;如果压根没有 Activation,就冷启动一个新的。一个接口把三种状态全兜住。

interrupt() 是唯一公开的停止操作,授权检查后调 Agent.cancel(cause, {keepInbox:true}),不 await 静止,不清 inbox,也不动它的后代。意思是你打断它,它收件箱里没处理的消息还在,后代也继续跑。

还有个 reportFrom(),子到父的回报通道,凭证是子自己,调用者不能指定收件人。谁派的它,它就报给谁,不靠消息里的字段授权。

这套东西解决的问题是,长任务里子 agent 不该是「一锤子买卖」。你派一个子 agent 去做代码检索,它可能需要被追问、被打断、被要求阶段性汇报,然后过两天冷启动回来接着干。Claude Code 的 subagent 做不到,它就是一次性函数调用,没有「会话」的概念。

one-shot 与 continuable 两种子 agent 生命周期对比,左栏一次性 start 到 SubagentResult 再 dispose,右栏 continuable 含 Activation 状态机 running waiting settled 与 followup interrupt reportFrom 操作

委派深度持久化,嵌套三层是硬拒绝不是软提醒

回到我开头那个痛点,模型偷偷 spawn 了三层 subagent。DSH 怎么防的。

它把委派深度持久化在 SessionHeader.delegationDepth 字段里,运行时还有个 AgentOptions.subagentDepth,两者取较大值。子 agent 创建时,持久化父 depth 加 1。关键是冷启动不能把这个深度降下来,你 resume 一个 depth 为 2 的 session,它还是 2,没法靠重启洗白。一旦超过配置的 maxDepth,直接硬拒绝。

这跟 Claude Code 的区别是本质性的。Claude Code 的「不要嵌套」写在 prompt 里,是软约束,模型听不听是缘分;DSH 的 maxDepth 写在持久化的 session header 里,是硬约束,超了就是 error,模型再想 spawn 也 spawn 不出来。

再讲一个 fork 的细节,很见功力。fork-in-processCreateAgentOptions.seed 给子 agent 传父 log,但传的不是整个历史,而是父 log 的平衡已完成 turn 前缀,也就是截到最后一个 turn/end 为止,进行中的那个不平衡 turn 被排除掉。为什么这么抠,因为 DSH 的 session log 是 append-only 的,有个运行时不变量叫 Model-visible 等价于 logged,任何到达模型的东西必须能从 log 重建。传一个半截 turn 过去,replay 的时候 invariants 会对不上。传「平衡前缀」就是为了保证子 agent replay 出来的状态跟父 agent 在那个时间点完全一致。

这就是源码级阅读才能看到的东西,人家不是随便 fork 一下,是连「截到哪」都有协议层面的理由。

三种 Harness 哲学横评

铺垫够了,上桌。Claude Code、Codex、DSH 三家在子 Agent 这件事上的设计哲学,我整理成这张表。

维度Claude CodeCodexDeepSeek Harness
子 agent 模型Agent 工具加 subagent_type,单进程,模型决定 spawnapp-server stdio 沙箱进程,审批驱动命名 provider 注册表,多 provider 共存,能力显式协商
谁定边界模型,靠 prompt 和 description产品加审批策略provider 能力声明加 start 时 fail-loud 校验
隔离同进程独立上下文进程加 sandboxprovider 可选,同进程 spawn/fork、远程 ACP、外部产品进程
可替换性Skill/MCP 可加,loop 不可换沙箱/审批可配,核心固定一切皆插件,agent loop 本身可换
委派深度软约束,prompt 引导产品自管持久化 delegationDepth 加 maxDepth 硬拒绝
父子上下文子收 prompt,不收父历史不传父对话fork 可传平衡 turn 前缀,其他不传
可续子 agent无,ephemeralcontinuable 加 Activation 加 followup/interrupt/report
跨产品委派不能委派给 Codex不能委派给 CCclaude-code 和 codex 都是内置 provider,可互委

我挑三个最有 insight 的差异展开。

第一,谁来决定子任务的边界。 Claude Code 把这个权力交给了模型,模型读 description 自己判断要不要 spawn、spawn 谁。这很灵活,但边界是隐式的、语义的、随时可能漂移。Codex 把权力收给产品和审批策略,更可控但更死。DSH 走了第三条路,边界由 provider 的能力声明决定,启动那一刻用类型系统校验,过了就过了,过不了直接拒绝。这是把「边界」从一个 prompt 工程问题变成了一个协议契约问题。

第二,可续子 agent 这件事,只有 DSH 有。 Claude Code 和 Codex 的子 agent 都是 ephemeral 的,一次调用一次消亡。但真实的长任务里,子 agent 经常需要被追问和续谈。DSH 的 continuable 加 Activation 加 cold resume 是目前我在开源 agent 框架里看到最完整的子会话生命周期设计。代价是复杂度,这套 continuation manager 的心智负担不低,但它解决的是真问题。

第三,跨产品委派。 这个下一节细讲,它是整张表里最颠覆的一格。

三种 harness 哲学对比,Claude Code 单进程模型决定型、Codex 沙箱产品审批型、DeepSeek Harness 委派式能力协商型,三者决定子任务边界的权力分别归属模型、产品策略与 provider 能力契约

把竞品做成自己的 subagent backend

这一节是全文最有戏剧性的部分,你坐稳。

DSH 内置了 codexclaude-code 两个 provider。意思是,你在 DSH 里跑一个 agent,这个 agent 可以把子任务委派给 Codex,也可以委派给 Claude Code。DSH 把它的直接竞品,变成了自己的执行后端。

我读了官方那份 Agent Note(2026-08-04-claude-code-and-codex-subagent-backends.md),里面的设计决策非常刻意,我给你拆。

这两个 provider 都是 one-shot onlyinheritsParentContext: false,不声明任何可选能力。每次委派,拉起一个全新的产品进程和一个不可续的产品会话。只接受独立文本任务,把父 session 的 cwd 传过去,不复制父对话

具体到实现,Codex provider 启动 codex app-server --stdio,建一个 ephemeral:true 的 thread,turn/completed 是权威终态。如果任务执行中需要审批而没人在,审批策略默认 cancel 或 decline,不授予任何权限。Codex 这边 pin 的是 0.147.0,走 Responses 协议。

Claude Code provider 用 @anthropic-ai/claude-agent-sdkquery()persistSession:false,禁用 AskUserQuestion,而且故意不传 canUseTool 和 elicitation 回调,意思是一旦需要人机交互就直接失败,不弹问题。pin 的是 SDK 0.3.220 / CLI 2.1.220 [DSH package.json]。

产品进程由 dsh-subprocess 统一管,凭据清洗、进程树终止、整树退出观察都在这一层。

为什么设计得这么「绝」,每次都全新进程、不传上下文、无人交互就失败。官方原话是,每次委派付出全新产品进程加独立模型上下文的代价,产品 payload 回到父级的只有最终文本。这不是偷懒,这是安全边界

你想,如果子 agent 是一个完整的 Claude Code 进程,它默认继承了你父会话的全部上下文和权限,那它就不是「子 agent」了,它是你父 agent 的一个分身,边界瞬间崩塌。把它做成 one-shot、无上下文、无审批就死,等于在 DSH 的编排层和外部产品之间画了一道清清楚楚的线,你就是个干独立文本活的临时工,干完交结果,别想碰我的会话状态。

还有个细节,这两个 provider 默认不装在 dsh-base 里,是 production-install exclusion,需要你在 Profile 里显式安装、放到 host 平面,再由 Agent Preset 决定要不要暴露对应工具。也就是说,DSH 团队自己都觉得这是个高级能力,不打算让新手一上来就玩跨产品委派。

这一节的架构判断我留给你,一个框架敢把竞品做成自己的 backend,要么是狂妄,要么是它对自己「编排层」的定位有足够的自信。我倾向于后者。

预览版的坑,以及现在值不值得上手

吹完了,说点实在的。这东西现在是 rc,而且是 rc.7,不是 1.0。

仓库 2026-08-13 才创建,到我写稿这天 4 天 [GitHub repo.created_at]。npm 上 latest 是 rc.6,next 是 rc.7 [npm view 2026-08-17]。官方 README 明明白白写着 THERE WILL BE COMPATIBILITY-BREAKING CHANGES。SESSION_FORMAT_VERSION 是 0,没有兼容承诺,SQLite 用单调的 SCHEMA_VERSION 拒绝旧格式,意思是你今天存的 session,明天升级可能就读不出来了。

环境门槛也不低,Node 要 ^22.19.0 || >=24.0.0,包管理器 pin 了 pnpm 11.7.0 [DSH package.json engines]。头号贡献者 tianyicui 一个人提了 5262 个 commit [gh contributors],你大概能感受到这项目现在有多早期、多集中在少数人手里。

启动命令倒是简单。

# 需要 Node >= 22.19,官方一键启动 Web UI(我本机沙箱网络超时没跑成界面,命令来自 README)
npx @deepseek-ai/dsh web
# 默认监听 http://127.0.0.1:3080

但我必须诚实,我这边 npx 在沙箱里安装超时,没拿到实时 Web UI 截图。这篇是源码级拆解,我读完了架构文档、subagent 的 types.ts/index.ts/continuation.ts、Codex 和 Claude Code provider 的 Agent Note、sandbox 文档和 base bundle README,但我不编造 UI 操作体验。市面上那些「我跑了一遍 DSH 真香」的文章,你留个心眼,很多可能 npx 都没装完。

我的上手建议分三档。

  • 如果你是做 agent 基础设施的,现在就去读源码,重点读 packages/subagent/,Capability Seam 和 continuation manager 这两套抽象值回票价,哪怕最后不用 DSH。
  • 如果你是想拿它当生产工具写业务的,等 1.0。rc 阶段天天 breaking,session 格式都不兼容,别拿生产项目陪跑。
  • 如果你是来凑热闹的,star 一下放观察列表就行,生态刚起来,awesome list 都是今天才建的。

常见问题

DSH 和 Cursor 到底是不是竞品。

不是。Cursor 是 IDE,是带编辑器的产品;DSH 是 agent harness/runtime,是给模型提供工具、会话、沙箱、子 agent 编排的底层框架。它确实带个 Web UI,但定位是 harness 的一个界面,不是 IDE。更贴切的类比是,DSH 想做 agent 界的 Express/Fastify,而 Cursor 是 agent 界的 Chrome。

6 个 provider 里我日常该用哪个。

你在 DSH 内部编排,优先 spawn-in-process(要隔离就用它)和 fork-in-process(需要继承父上下文做接力时用)。跨机器或跨团队用 acpclaude-codecodex 是高级能力,需要跨产品委派时再开,而且要接受每次全新进程的代价。dsh-sdk 是给外部程序通过 JSON-RPC 接入用的。

continuable 子 agent 听着强大,是不是该一律用它。

不是。continuable 有持久化 session、有 Activation、有 inbox,复杂度和资源开销都比 one-shot 高一大截。绝大多数一次性子任务,one-shot 就够了。只有当你明确需要追问、打断、跨冷启动续谈时,才值得上 continuable。别为了用而用。

委派深度 maxDepth 设多少合适。

没有银弹,但我的经验是别超过 2 到 3 层。DSH 把它持久化硬拒绝是好事,但你要自己想清楚为什么需要深层委派。大多数「需要嵌套」的场景,其实是任务拆解没做好,而不是真的需要 5 层 subagent。把 maxDepth 当设计压力,逼自己把任务拆平。

Claude Code 用户现在能从 DSH 偷师什么。

就算不用 DSH,你也可以借鉴它的协议思维,在自己的 subagent description 里把「输入契约、输出契约、能力边界」写死,把不支持的能力显式标出来而不是让模型猜;在编排逻辑里自己做深度计数和硬截断;把一次性任务和可续会话在设计层面分开。协议不一定要框架给,你自己在 prompt 层和脚本层也能立。

写在最后

回到我开头盯着屏幕的那个下午。

我当时别扭的根本不是「模型偷偷开 subagent」这件事本身,而是没有人在协议层面回答「谁有权 spawn、能 spawn 成什么样、嵌套多深」这三个问题。Claude Code 把答案藏在 prompt 里,Codex 把答案收在产品策略里,DSH 第一次把答案摆到了台面上,用类型、用能力声明、用持久化深度,写成了一份可以校验、可以拒绝、可以跨产品复用的合同。

我不确定 DSH 最后能不能成,rc.7 的 breaking 节奏和单核心贡献者的集中度都是真实风险。但我确定一件事,agent harness 这个层未来一定会往「显式协议」方向走,而不是永远靠模型读 prompt 自己悟。隐式的东西终将被显式化,能被类型系统约束的,就不该留给心情。

你在用 Claude Code 的 subagent 时踩过什么坑,嵌套失控、上下文串台、还是子任务一去不回,评论区聊聊,我想看看有多少人跟我受过一样的罪。

做 Agent 开发的朋友,下篇我打算动手把 DSH 的 continuation manager 抄一遍,用最小可运行代码讲透 continuable 子 session 的状态机。感兴趣的话,关注「码哥跳动」,发出来你能第一时间看到。身边有人在做 agent 编排选型的,这篇可以直接甩给他,省得他再踩一遍软约束的坑。


🤖 配套实战手册

整理了一份《AI Agent 入门实战手册》,从零到可运行代码,含 5 个踩坑场景和生产级模板。

回复「agent」即可获取。

转发给正在学 AI 开发的朋友,少走弯路 🚀

beeaa00ee37c5db0e2fb2c5c5efe4f29.png