第21讲 · 替换引擎——LLM 适配器与 FS Provider

《DeepSeek Harness 从上手到精通》系列 · 第七阶 深度定制篇

🎯 目标:动手替换框架两个核心 seam,体会"换脑不换壳" 难度 ★★★ ⏱ 约 45 分钟 前置:第 7、20 讲 · Node ≥ 22.19

dsh 最让人震撼的设计是 "换脑不换壳":同一个壳子,今天跑 DeepSeek,明天跑自家代理;今天在本机读写文件,明天搬到云端沙箱。本讲带你亲手替换 ctx.llmctx.fs / ctx.subprocess 这两条核心 seam。


一、为什么这叫"换引擎"

第 7 讲里我们讲过 seam 是"接口 / 实现 / 消费者"三件套。dsh 内部有几十个 seam,但真正"换起来"最爽的只有两个:

  • ctx.llm:模型的"心脏"。替换它,agent 的"脑"就换了;
  • ctx.fs + ctx.subprocess:agent 的"手脚"。替换它,整个执行世界搬家。
引擎替换:齿轮与插槽
图 1 替换引擎就像更换插槽里的模块——外壳不动,能力全换。

二、替换 LLM:让 dsh 接非 DeepSeek 模型

官方把 LLM 适配器放在 packages/llm/。我们要做的,是实现它的接口,挂回 ctx。

接口核心就两个方法:complete()(一次性补全)和 stream()(流式调用)。以一个"转发到 OpenAI 兼容代理"的最小适配器为例:

import { LLM, type LLMOptions } from '@deepseek-ai/dsh-llm' export class OpenAICompat extends LLM { static inject = ['config'] private baseUrl: string async start() { this.baseUrl = this.ctx.config.llm.openai_base_url } async *stream(prompt: LLMOptions) { const resp = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.ctx.config.llm.api_key}`, }, body: JSON.stringify({ model: prompt.model ?? 'gpt-4o-mini', messages: prompt.messages, stream: true, }), }) // 解析 SSE,转成 dsh 期望的事件 for await (const chunk of sseIter(resp.body!)) { yield { type: 'delta', content: chunk.choices[0].delta.content ?? '' } } yield { type: 'done' } } }

然后写一个插件把它挂上去,覆盖默认的 LLM 实现

export function apply(ctx: Context) { ctx.plugin(OpenAICompat) // 把 ctx.llm 从默认 provider 切到我们的实现 ctx.on('ready', () => { ctx.services.llm = ctx.get(OpenAICompat) }) }

启动 dsh 后,所有原本调用 LLM 的代码无感知——agent loop、工具调用、流式 UI,统统走新通道。这就是 seam 的威力。

三、替换 FS / Subprocess:执行世界搬家

第 7 讲提过:"执行世界"是个整体——ctx.fs 写文件,Bash/PTY/LSP 工具会"自动跟着"在新世界工作。官方给出了 packages/e2b/ 这个 E2B 沙箱实现做参考,我们照样画个"内存版":

import { FSProvider, SubprocessProvider } from '@deepseek-ai/dsh-fs' // 内存文件系统(演示用,真实场景换成 E2B SDK) export class MemoryFS extends FSProvider { private files = new Map<string, string>() async read(path: string) { if (!this.files.has(path)) throw new Error(`ENOENT: ${path}`) return { content: this.files.get(path)! } } async write(path: string, content: string) { this.files.set(path, content) return { bytes: content.length } } async list(dir: string) { return [...this.files.keys()] .filter(p => p.startsWith(dir)) .map(p => ({ path: p, kind: 'file' as const })) } } export class MemorySubprocess extends SubprocessProvider { // 简单的命令白名单实现 async run(cmd: string, args: string[]) { if (cmd === 'echo') return { stdout: args.join(' ') + '\n', code: 0 } return { stdout: '', code: 127, stderr: `unknown: ${cmd}` } } } export function apply(ctx: Context) { ctx.plugin(MemoryFS) ctx.plugin(MemorySubprocess) ctx.on('ready', () => { ctx.services.fs = ctx.get(MemoryFS) ctx.services.subprocess = ctx.get(MemorySubprocess) }) }

关键事实:你不需要改 任何 工具实现。Bash 工具走 ctx.subprocess、read/write 工具走 ctx.fs、LSP/PTY 也跟 subprocess 共享 PID 命名空间——切 Provider 一次,全家桶跟着切

四、cookbook 精读:两段官方示例

官方 cookbook 里有两个姊妹篇值得反复读:

章节必读内容
cookbook/llm-adapter/完整 LLM 适配器示例:流式协议、token 计数、错误重试
cookbook/chat-node/在编辑器里嵌入一个 Chat 节点,等价于本讲的"组装演练"

cookbook 的示例都是跑得起来的,而不是伪代码。建议每读一段就 pnpm dev 一次,亲手试。

五、动手练习:双替换跑通

把上面两段代码合到一个插件包里,跑通下面这个剧本:

  1. 在配置里指定 llm.provider = openai-compat,把 API key 通过 CredentialRef 引入(第 19 讲);
  2. 启动 dsh,进 Web UI,让 agent 执行:"先写一个 hello.txt,内容是 Hello, Memory!,再 cat 一下。"
  3. 打开 dsh 的内存 FS 调试页面(或在 session 日志里搜索 memory-fs),确认写入到了内存里;
  4. 再让 agent 执行 echo hello from bash,看 Bash 工具是否通过 MemorySubprocess 走到了 echo 命令。

六、常见坑

  • 流式协议不一致:OpenAI/Anthropic/DeepSeek 的 SSE 格式细节不同,转发时记得按 schema 严选字段;
  • ctx.services 直接赋值不安全:应通过 ctx.plugin() + 命名约定,或者使用 ctx.replace() 提供的辅助;
  • FS 路径是绝对的:切到 E2B/内存版后,本地路径不再有意义,工作目录一定要在 Provider 内部管理;
  • subprocess 安全白名单:内存版仅演示,生产场景必须接 Landlock + 命令白名单(第 18 讲);
  • 没重启就换 Provider:替换时要确保旧 Provider 已 dispose,否则会同时存在两份服务导致事件双发。

七、官方文档对应章节

  • docs/seams/llm.mddocs/seams/fs.mddocs/seams/subprocess.md
  • cookbook/llm-adapter/cookbook/chat-node/
  • packages/llm/packages/e2b/ 源码导读。

本讲内容基于 DeepSeek Harness 官方文档与项目源码整理,仅供学习参考,具体命令与配置请以官方最新版本为准。源码仓库:https://github.com/deepseek-ai/deepseek-harness

咨询 DeepSeek Harness 相关问题,请加微信:A0qingfengyuan_01

个人观点,仅供参考,如有不对之处,请多包涵,欢迎指出。本文由 AI 协助完成!