换两个提供者,为什么能搬走整个 Agent
目标听众:懂一点 AI Agent 和软件架构,但第一次接触 DeepSeek Harness 的开发者 预计时长:约 11 分钟(按正常中文语速估算)
口播稿
假设你已经做出了一个能在本机写文件、跑命令、开终端的 Agent。现在产品要支持远程沙箱。
最直接的做法,是给每项能力都加一条远程分支。文件读写一条,bash 一条,终端一条,LSP 一条,子 Agent 再来一条。
这很快会遇到一个麻烦:这些分支必须永远指向同一台远程机器。只要有一处没对齐,就可能出现文件写在远程,命令却在本地执行。Agent 自己还未必知道,它看到的已经是两个不同的世界。
DeepSeek Harness,也就是 dsh,处理这个问题的方式,是把变化收进一条条“能力接缝”。英文叫 capability seam。接缝就是一个可以整体替换的能力边界。
一条完整的接缝有三个角色。
先要有服务定义,说明这个能力对外提供什么接口。然后要有服务提供者,真正实现这套接口。最后还要有消费者,也就是实际使用这项能力的工具或其他模块。
这三个角色缺一不可。只有接口没人实现,能力不能运行。只有一份实现,却没有消费者按统一接口使用,也谈不上可替换。
它有点像 USB。标准规定插头和通信方式,厂商按标准生产设备,电脑里的软件通过标准使用设备。这样你换一只鼠标,软件不用跟着重写。放到 dsh 里,换掉服务提供者,消费者代码也不用改。
这个设计里最关键的一步,是让文件系统接缝和子进程接缝共享同一个执行世界。
所谓执行世界,就是文件放在哪里,进程又在哪里启动。本地模式下,它们都在同一台宿主机。远程模式下,它们都进入同一个 Linux 沙箱运行时。
为什么子进程接缝这么重要?因为 dsh 里凡是需要启动进程的能力,都会经过它。bash 命令走它,终端后端走它,LSP 宿主走它,委派给 Codex 或 Claude Code 这类外部产品的进程外子 Agent,也走它。
于是,系统里“进程在哪里启动”这个变化点,被压缩到了一处。
当文件系统换成远程提供者,文件读写就进入远程沙箱。当子进程也换成远程提供者,bash、终端、LSP 和进程外子 Agent 就会在同一个沙箱里运行。工具层没有增加远程分支,也不需要分别搬迁这些能力。
你实际替换了两个提供者,移动的却是整个执行世界。
dsh 还用装载规则保证一致性。在同一个 context,也就是同一次运行上下文里,文件、子进程和命令执行这些接缝,各自只允许装载一个实现。再装第二个会直接报错。系统不会同时保留两套文件实现或者两套进程实现,让它们各说各话。
共享执行世界解决的是位置问题。其他接缝还分别守住了几条重要原则。
模型接缝强调模型中立。消费者面对的是一套不绑定具体 provider 的流式契约。换一家模型提供者,Agent 的驱动逻辑和压缩逻辑不需要跟着改。“使用哪个模型”这个决定,被留在消费者代码之外。
沙箱相关接缝强调边界必须显式。Agent 可以操作哪些文件,可以运行哪些命令,都由接缝和沙箱策略明确管理,不能默认把当前目录随便扫一遍。命令围栏和文件围栏还必须指向同一个根目录。一个限制在 A,另一个限制在 B,沙箱边界就会漏掉,所以两边共用同一份策略来源。
LSP 接缝采用的是封闭操作集。LSP 是给编辑器和代码工具提供跳转、查找等能力的语言服务器协议。dsh 没有把完整的原始协议直接暴露出去,而是只留下四个标准化导航操作。
这意味着,某个后端即使拥有更多功能,只要无法翻译成这四个操作,消费者就用不到。它牺牲了一部分后端能力,换来不同后端对外行为一致。
子 Agent 接缝允许实现形态有很大差别。一次子任务,可以交给当前进程里新开的 Agent,也可以委派给另一个产品完成。消费者看到的动作始终一样:发出一个子任务,等待一个结果。
审批接缝则采用 fail closed,也就是默认关闭。审批请求发出去以后,如果没人回答,结果是拒绝,不会自动放行。“无人处理”不能等同于“默认允许”。
这些接缝在产品里怎么被替换?dsh 启动时会分层组装插件树。profile 指定 bundle,bundle 插入配置行,上层 patch 再根据 id 找到某一行,把整行换掉。
因此,所谓换 provider,在具体配置里就是把一行实现包替换成另一行。重新组装以后,所有消费者都会解析到新的提供者。
沿着这套机制,可以推演出三种产品变化。
第一种是执行位置变化。文件和子进程分别换成远程提供者,文件、命令、终端、LSP 和进程外子 Agent 就一起进入远程运行时。
第二种是模型变化。模型接缝换一家提供者,底层模型整体改变,消费者继续使用原来的流式契约,Agent 的行为逻辑保持不变。
第三种是协作方式变化。子 Agent 的提供者从“当前进程里新开一个 Agent”,换成“委派给 Claude Code 完成一次执行”。工具接口不变,产品却从单个 Agent 工作,变成了多个产品之间协作。
这里需要守住一个边界。dsh 并没有把所有东西都设计成可替换。
它的服务分成三类。
模型、文件系统、命令执行、子进程、沙箱、联网、LSP、子 Agent 和审批,属于能力接缝。这些地方欢迎变化,也允许存在多个提供者。
会话日志、工具注册表、提示组装、Agent 服务和不变量断言,属于核心脊柱。它们各自只有一个所有者,整个 harness 依赖这些服务保持稳定。换模型或者换执行环境,是架构允许的替换。要换掉会话日志的数据模型,就已经是在修改核心源码。
另外,真正承载循环逻辑的组合点只有一个。其他能力都围绕这个循环驱动器接入扩展点。
这种划分让系统同时拥有两样东西:该变的位置可以替换,该稳定的位置不会因为“插件化”而到处漂移。
代价同样明确。
一次工具调用从模型发出以后,要经过接缝、提供者和事件等多个层次。抽象更多,调试链也更长。增加一项新能力时,不能只写一个函数,还要同时设计服务定义、提供者和消费者,并守住这条接缝自己的语义约束。
如果一个 Agent 永远只在一种环境里运行,也永远只使用一个模型,这套接缝很可能是过度设计。
如果目标是一套消费者代码,同时支持本地和远程执行、不同模型以及不同协作形态,那么这些抽象就有了回报。dsh 把变化集中到 provider 的替换上,再用稳定的核心脊柱托住整个系统。
所以,“换一个 provider 等于换整个产品”并不是说任意换一行配置,都能得到一个新产品。它成立的前提,是能力已经被做成完整接缝,消费者只依赖稳定接口,相关提供者共享一致的执行世界,同时核心脊柱保持不动。
在这些条件都满足时,替换提供者改变的就不只是某个功能选项。执行位置、底层模型和协作方式,都会跟着改变。
资料备注(不口播)
- 原文:
docs/ai-coding/deepseek-harness/12-capability-seams-swap-provider-swap-product.md - 保留的关键事实:接缝由服务定义、提供者、消费者三个角色组成;文件系统与子进程共享执行世界;同一 context 中相关接缝只允许一个实现;LSP 接缝只提供四个标准化导航操作;审批采用 fail closed;服务分为可替换接缝、不可替换核心脊柱和唯一循环组合点。
- 保留的关键范围:远程执行需要同时替换文件系统和子进程提供者;“换 provider 换产品”成立于稳定接口、完整接缝、共享执行世界和稳定核心脊柱这些前提下。
- 被压缩省略的内容:延伸阅读链接、上一篇和下一篇导航,以及各接缝的完整服务清单;未新增原文之外的测试数据、使用体验或案例。
GitHub 原文:12-capability-seams-swap-provider-swap-product-spoken.md
评论
EMPTY