DeepSeek Harness 系列(07):能力 Seam——换一行配置,能力全换

文章来源声明: 原文作者:冬奇Lab; 来源站点:掘金; 原文链接:https://juejin.cn/post/7685651702582607914; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

对做 Agent 基础设施的团队很有参考价值:Seam 把测试、部署、权限三类痛点一次解决,尤其适合需多环境运行、多模型切换或强审计要求的企业级 Agent 项目。

先从一个家用比喻开始 ----------

你家的电灯开关,它不关心电从哪里来——市电、太阳能还是发电机都行。开关只定义一件事:"开/关"。只要背后接的东西遵守这个接口,你按下开关,灯就亮。

这就是 Seam(缝隙)的本质:一条接口边界,把"用这个能力"和"这个能力怎么实现"隔开来。

在 dsh 里:

  • 文件系统操作是一个 Seam(ctx.fs
  • Shell 执行是一个 Seam(ctx.shell
  • LLM 调用是一个 Seam(ctx.llm
  • 进程沙箱是一个 Seam(ctx.sandbox

你可以把默认的本地文件系统换成 E2B 沙箱文件系统,工具代码一行不改。这就是 Capability Seam 要解决的问题。


Seam 的三个角色

每个 Seam 都由三类参与者构成:

┌────────────────────────────────────────────────────────┐
│  Consumer: tool-fs                                     │
│  import ctx.fs → calls ctx.fs.<span>readFile</span>(path)           │
│                  ctx.fs.<span>writeFile</span>(path, content)       │
└─────────────────────┬──────────────────────────────────┘
                      │ 依赖服务 <span>"ctx.fs"</span>
┌─────────────────────▼──────────────────────────────────┐
│  Service Definition: dsh-fs                             │
│  interface FileSystem { readFile, writeFile, ... }     │
└─────────────────────┬──────────────────────────────────┘
                      │ 实现
      ┌───────────────┴───────────────┐
      ▼                               ▼
  fs-local                         fs-e2b
  (本地文件系统)                 (E2B 沙箱)

三个角色的分工:

角色职责举例
**Service Definition**定义接口 + 服务名`dsh-fs`:定义 `ctx.fs` 的方法签名
**Service Provider**实现接口,注册到 `ctx``fs-local`、`fs-e2b`、`fs-sandbox`
**Service Consumer**使用接口,不关心实现`tool-fs`:读写文件的工具插件

Consumer 只知道服务名(如 ctx.fs),不知道背后是哪个 Provider。切换 Provider 只影响 Bundle 的配置,Consumer 完全无感。


核心 Seam 一览

dsh 内置了以下核心 Seam:

Seam服务名默认实现可替换为
文件系统`ctx.fs``fs-local`(本机)`fs-e2b`(E2B 沙箱)、`fs-sandbox`(受限本机)
Shell 执行`ctx.shell``bash-local``bash-sandbox`(受限执行)、远程 Shell
进程沙箱`ctx.sandbox``sandbox-local`(bwrap/Seatbelt/ACL)容器、microVM
LLM 适配器`ctx.llm``llm-deepseek``llm-pi-ai` 等第三方
认证凭据`ctx.credentials``credentials-local`远程 vault
用户设置`ctx.settings``settings-file`远程设置服务
持久化存储`ctx.sessionPersistence``session-persistence-jsonl`SQLite、远端

重点:ctx.fs(文件系统 Seam)

为什么不直接 import fs from 'node:fs'

直接 import Node.js 内置的 fs 模块,会带来三个具体问题:

  1. 测试时:要测试"读文件"逻辑,必须在磁盘上真的创建文件。或者用 mock,但 mock Node.js 内置模块需要额外工具(jest.mock 等),很麻烦。
  2. 在沙箱里跑时:E2B 有自己的文件系统接口,不是本地 Node.js fs。如果工具代码直接 import 了 node:fs,到 E2B 里就完全跑不起来。
  3. 审计:你无法统一拦截所有文件读写操作,比如做日志、权限检查,因为调用点散落在每个工具里。

ctx.fs Seam,这三个问题都消失了:

  • 测试时注入内存文件系统 Provider
  • E2B 里注入 fs-e2b Provider
  • 审计时在 Provider 层统一记录所有调用

Consumer 代码示例(概念示例)

<span>// 工具插件:读文件——只依赖 ctx.fs,不关心实现</span>
<span>// 声明依赖 'fs' 服务</span>
<span>export</span> <span>const</span> inject = [<span>'fs'</span>]

<span>export</span> <span>function</span> <span>apply</span>(<span>ctx: Context</span>): <span>void</span> {
  ctx.<span>tools</span>.<span>register</span>(<span>defineTool</span>({
    <span>name</span>: <span>'read_file'</span>,
    <span>description</span>: <span>'读取本地文件内容'</span>,
    <span>execute</span>: <span>async</span> (args, exec) => {
      <span>// ctx.fs 是文件系统 Seam——本地、沙箱、E2B,取决于加载了哪个 Provider</span>
      <span>// 这里完全不关心背后用的是什么文件系统</span>
      <span>const</span> content = <span>await</span> ctx.<span>fs</span>.<span>readFile</span>(args.<span>path</span>)
      <span>return</span> content
    },
  }))
}

<span>// 想切换到沙箱文件系统?</span>
<span>// 只需在 Bundle 里换掉 Provider 插件:</span>

<span>// bundle.ts(概念示例)</span>
<span>export</span> <span>default</span> [
  <span>// 替换这一行就够了,工具代码不用改:</span>
  <span>// '@deepseek-ai/dsh-fs-local'     → 本地文件系统(默认)</span>
  <span>// '@deepseek-ai/dsh-fs-sandbox'   → 受限本地文件系统</span>
  <span>// '@deepseek-ai/dsh-fs-e2b'       → E2B 沙箱文件系统</span>
  <span>'@deepseek-ai/dsh-fs-sandbox'</span>,   <span>// ← 只改这一行</span>

  <span>// Consumer 代码一行未动</span>
  <span>'@deepseek-ai/dsh-tool-fs'</span>,
  <span>// ... 其他插件</span>
]


重点:ctx.sandbox(进程沙箱 Seam)

Shell 执行背后是进程沙箱。dsh 定义了三种沙箱模式:

<span>// 进程沙箱的三种模式(概念示例)</span>
<span>type</span> <span>SandboxMode</span> =
  | <span>'read-only'</span>          <span>// 只读模式:子进程不能写入任何文件</span>
  | <span>'workspace-write'</span>    <span>// 工作区写模式:只能在工作目录下写文件</span>
  | <span>'danger-full-access'</span> <span>// 无限制模式:不经过沙箱,直接 spawn 子进程</span>

平台实现是自动适配的:

平台底层机制
Linuxbwrap + Landlock
macOSSeatbelt
WindowsACL 受限令牌

一行配置换后端(概念示例):

<span># 本地开发:sandbox-local(平台原生沙箱)</span>
<span># CI/CD:sandbox-e2b(E2B 容器,完全隔离,沙箱崩了也不影响宿主)</span>

同一份 tool-shell 代码,在本地跑用 sandbox-local,在 CI 里跑用 sandbox-e2b,切换只发生在 Bundle 配置层。


重点:ctx.llm(LLM 适配器 Seam)

LLM 也是 Seam。这个设计乍一看有点出人意料,但仔细想想很合理。

为什么 LLM 调用也要抽象?

  1. 切换供应商:同一个 Agent,本地测试用轻量模型,生产用 DeepSeek,只换 Provider 插件。
  2. 回放测试llm-replay Provider——不发真实 API 请求,直接按顺序回放历史 session 的 assistant 响应。测试 Agent 行为不需要 API key,也不会有随机性。
  3. 多模型路由:可以实现一个 Provider,根据任务类型路由到不同模型。
<span>// llm-replay 的用途(概念示例)</span>
<span>// 在测试里:</span>
<span>//   1. 先跑一次真实对话,保存 session 日志</span>
<span>//   2. 之后跑测试时,把 Provider 换成 llm-replay,</span>
<span>//      replay adapter 按顺序回放历史 assistant/message 事件</span>
<span>//   3. 测试完全确定性,不消耗 API quota,速度快 10x</span>

<span>// 切换方式:Bundle 里一行</span>
<span>// '@deepseek-ai/dsh-llm-deepseek'  → 真实 API</span>
<span>// '@deepseek-ai/dsh-llm-replay'    → 回放模式(测试用)</span>


实战:自己实现一个 Provider

实现一个只读文件系统 Provider——所有写操作直接报错,适合给 Agent 开只读访问权限。

<span>// 只读文件系统 Provider(概念示例)</span>
<span>// 适用场景:只想让 Agent 读代码库,不允许修改任何文件</span>

<span>export</span> <span>const</span> name = <span>'my-readonly-fs'</span>

<span>export</span> <span>function</span> <span>apply</span>(<span>ctx: Context</span>): <span>void</span> {
  <span>// 向框架注册为 ctx.fs 的 Provider</span>
  ctx.<span>provide</span>(<span>'fs'</span>, {
    <span>// 读操作:正常执行</span>
    <span>async</span> <span>readFile</span>(<span>path</span>: <span>string</span>): <span>Promise</span><<span>string</span>> {
      <span>return</span> <span>await</span> <span>localReadFile</span>(path)
    },

    <span>// 写操作:直接拦截,抛出明确错误</span>
    <span>async</span> <span>writeFile</span>(<span>path</span>: <span>string</span>, <span>content</span>: <span>string</span>): <span>Promise</span><<span>void</span>> {
      <span>throw</span> <span>new</span> <span>Error</span>(<span>`Read-only filesystem: cannot write to <span>${path}</span>`</span>)
    },

    <span>// 目录列表:正常执行</span>
    <span>async</span> <span>listFiles</span>(<span>dir</span>: <span>string</span>): <span>Promise</span><<span>string</span>[]> {
      <span>return</span> <span>await</span> <span>localListFiles</span>(dir)
    },

    <span>// ... 其他方法(mkdir、rm 等都抛只读错误)</span>
  })
}

<span>// 在 Bundle 里使用自定义 Provider(概念示例)</span>
<span>export</span> <span>default</span> [
  <span>// 用自己的只读 Provider 替换默认的 fs-local</span>
  <span>'./my-readonly-fs'</span>,

  <span>// tool-fs 照常使用,它不知道背后是只读的</span>
  <span>'@deepseek-ai/dsh-tool-fs'</span>,
]

这个模式的强大之处:你不需要改 tool-fs 的任何代码,就限制了 Agent 的文件系统权限。


Seam 设计的核心价值

用一张表对比:

场景不用 Seam用 Seam
切换 LLM 供应商改遍所有 API 调用代码换一个 Provider 插件
单元测试文件操作需要 mock `node:fs`,或在磁盘上建真实文件直接注入内存文件系统 Provider
部署到沙箱环境需要改大量工具代码适配沙箱 API换 `fs-sandbox` 或 `fs-e2b`
新增平台/后端支持改核心代码,可能引入 bug实现新 Provider,Consumer 不受影响
权限控制在每个工具里加 if 判断在 Provider 层统一拦截

Seam 本质上是依赖注入(DI)的一种特化形式,只是在插件系统里,注入的粒度是"服务名",而不是构造函数参数。


设计总结

dsh Capability Seam 的核心思路:

Consumer 只知道接口名,Provider 只知道接口,切换发生在配置层。

设计决策原因
所有外部能力走 Seam测试、部署、多后端支持全都受益
LLM 也是 Seam支持 replay 测试、供应商切换、多模型路由
沙箱也是 Seam本地/CI/远程环境用不同沙箱后端,Agent 代码不变
Consumer 只声明服务名解耦彻底,不依赖任何具体实现
Bundle 是配置层组合不同 Provider 只改一处,不改业务代码

系列下一篇

下一篇讲 多 Agent 协作:当一个 Agent 解决不了复杂任务时,dsh 是怎么支持多个 Agent 分工配合的——委托(delegation)、子 Agent、并行执行,以及这些机制在 Session 层面是怎么表示的。


PrimeSkills 可以找到已在真实企业场景验证过的 AI Agent 技能和工作流,不是演示级的,是用在实际项目里的。

更多内容见我的个人主页