
字节笔记本
2026年10月6日 · 约 7 分钟读完
DeepSeek Harness 子进程接缝设计解读
DeepSeek Harness 是 DeepSeek 开源的 agent 框架(GitHub 仓库 deepseek-ai/deepseek-harness,MIT 协议),README 里对它的定位是「一切皆插件」的架构。跑 agent 的每一步几乎都绕不开子进程:执行 shell 命令要起 bash,语言服务要起 LSP 服务器,交互式终端要分配 PTY,跨进程的 subagent 后端还要通过管道收发 ndjson 消息。不少框架让每个功能各自去调 spawn,结果环境变量清理、输出截断、进程树清理这些脏活被抄写了好几遍。DeepSeek Harness 的选择是把子进程收敛成一个独立的接缝(seam):抽象服务定义在 dsh-subprocess 包,本地实现在 dsh-subprocess-local 包,其他能力接缝与进程外后端统一从这里获取进程能力。本文基于其官方文档,拆解这套设计里值得自建 agent 借鉴的取舍。

一个接缝,四种消费方式
四类消费方的需求差异很大:bash 执行器家族要的是有界的批量输出,用收集模式拿结果;LSP 客户端要原始协议管道,自己做 JSON-RPC 分帧;PTY 终端后端要的是终端原语,由提供方分配控制终端;ACP(Agent Client Protocol)subagent 后端用管道传 ndjson,stderr 则直接继承父进程,让诊断直通。为了让 bash 消费方保持单一导入入口,dsh-shell 包还把接缝的这套词汇重导出了一遍。
接缝本身持有三样共享资产:受管的 DSH_* 环境命名空间、共享的凭据清除(scrubbedParentEnv),以及统一的 CollectedOutput 输出形状。所有被收集的流都通过这个形状报告自身的截断状态与 spill 恢复路径,消费方不必各自发明「输出被截断了怎么办」的约定。
环境变量:谁的事实谁做主
DSH_* 前缀的变量归 harness 所有,代表子进程层面的当前事实。实现会在合并调用方显式 env 之前,先丢弃环境里已存在的 DSH_* 名称,保证子进程看到的事实只来自有意提供的字符串条目,而不是父环境里残留的旧值。显式 env 中的字符串被视为调用方的有意选择,凭据形状的条目或新的 DSH_* 事实可以穿越清洗;反过来,值为 undefined 的条目是墓碑,专门用来删除普通环境里已有的变量。父环境则先经过 scrubbedParentEnv 清洗再合并,凭据不会顺手漏给子进程。
spawn spec:不设任何默认值
这个接缝的一个鲜明立场是完全显式:每项处置、限制与目录都写在 spec 上,由调用方自己的配置决定,而不是由某个隐藏的服务默认值决定。argv 数组绝不经过 shell 解释;cwd 必填;stdio 的三条流各自显式声明处置方式;graceMs 必须是正的有限毫秒数;AbortSignal 由调用方传入,超时 deadline 与原因分类都归调用方所有,接缝只对中止信号做出反应。全显式意味着每次 spawn 的行为都能从 spec 上直接读出来,排障时不需要猜测默认值的存在。

stdio 三种处置与可恢复的截断
每条流的处置方式由消费方自选。pipe 暴露原始可读流,给协议分帧用;inherit 把父进程的描述符直通给子进程,诊断输出直接落在 harness 自己的流上;collect 是有界的内存收集,内存上限溢出时保留尾部而不是头部,因为对诊断来说最新的输出往往最有价值。
collect 还可配置可选的 spill 文件:不带 spill 时只留内存尾部,这就是「诊断尾部」形状,例如语言服务器的 stderr 只在内存里缓冲,不留下任何文件;带上 spill 时,完整流在字节上限内可以整体恢复,这是 bash 工具的形状。
读取器的设计同样克制。spawn 会立即返回活动句柄,收集模式的读取器接受全流字节偏移且从不消费数据,独立的读取器不会抢走彼此的增量:一个读取器做流式跟进,另一个在进程结算后 readFrom(0) 拿全量批量结果,互不干扰。当请求的偏移已经滑出内存尾部窗口,这次读取会标记为 lossy,完整内容只能从 spill 文件找回。
终止:唯一动词,进程树范围
terminate() 是接缝唯一的终止动词,执行 SIGTERM、宽限期、SIGKILL 的三级升级:POSIX 上对脱离的进程组发信号,组不存在时退回直接子进程;Windows 用 taskkill /T 终结整棵树。waitForExit() 观察的也是整棵进程树而不只是直接子进程,所以还活着的辅助进程在收尾流程里可以被观察到。terminate() 幂等,进程树消失后再调用只是空操作。
结果对象刻意做薄:done 只承载 Node close 事件词汇,即 exitCode 与 signal,不带超时或取消的分类,也不携带输出。原因分类归调用方,例如 bash 执行器读取自己拥有的超时信号,拆分出 timedOut 与 aborted 两种失败;收集的流在结算之后仍可经 handle.collected 读取,批量与流式调用方共用同一条访问路径。仓库内的参考实现是 ACP 后端的 disposeAcpChild:先关闭 stdin 让子进程收到 EOF,再走终止升级。
终端是另一种原语
spawnTerminal(spec) 是唯一的非管道进程原语。提供方负责分配控制终端、UTF-8 文本传输、前台进程组检查与信号发送,以及一个必须等待的 TERM 到 KILL 操作,让提供方仍能观察到的每个会话成员完全停稳。提示符检测、就绪推断、scrollback、沙箱策略与持久会话所有权仍归 PTY 消费方所有。普通 spawn() 无法重建控制终端语义,这个原语因此必须单独存在。
三条值得抄的设计
对想自建 agent 运行时的团队,这个接缝有三条经验最值得借鉴。其一,能力收敛:所有进程需求走同一个接缝,环境清洗与输出形状只需实现一次,四类差异极大的消费方共享同一套经过验证的底座。其二,显式优于默认:spec 不设默认值,行为完全由调用方配置决定,审查与排障都不必猜。其三,事实归属清晰:接缝只负责进程事实,退出码、信号、流与截断状态;超时与取消的意义留给拥有 deadline 的调用方去解释。边界画对了,上层能力才能各自长好。



