第1章:认识 DeepSeek Harness——一个插件化的 Agent Runtime 平台

0 阅读12分钟

第1章:认识 DeepSeek Harness——一个插件化的 Agent Runtime 平台

系列:DeepSeek Harness 源码实战 | 进度 1/16
原文仓库:github.com/deepseek-ai…

你以为 coding agent(编码智能体)是一个程序,其实它是一个可以拼装的运行时平台。c5f6fbfe164448529e5a55bbb3adb6c2.png

当你在终端里敲下一行命令,让 AI 帮你修 bug、写测试、重构代码的时候,背后跑的不是一个简单的「prompt 进、结果出」的脚本。它是一个完整的运行时:管理会话上下文、调度工具调用、执行沙箱策略、持久化历史记录、处理并发与中断。这个运行时就是 Harness(驾驭层)——介于 LLM(Large Language Model,大语言模型)和你的工程环境之间的那层软件。

我是怕浪猫,一个喜欢拆开源项目的技术博主。从这篇文章开始,我会用 8 章的篇幅,带你从源码层面完整拆解 DeepSeek 开源的 Harness 项目——dsh(DeepSeek Harness 的缩写)。这是第 1 章,先搞清楚它到底是什么、怎么跑起来、整体长什么样。

1.1 什么是 Harness:从 LLM 到可用 Agent 的最后一块拼图

先问一个问题:为什么不直接调 LLM 的 API(Application Programming Interface,应用程序编程接口)来干活?

答案很简单——裸模型只会输出文本。你让它「帮忙修一下 auth.ts 里的登录 bug」,它能给你一段修改建议,但它不会自己去读文件、跑测试、检查 git diff、写 commit message。这些「动手」的能力,需要一层软件来提供。

这层软件就是 Harness。它的职责可以用一句话概括:

把 LLM 的语言能力,变成可在真实工程环境中执行的操作能力。

具体来说,一个 Harness 要解决五件事:

职责说明
上下文管理维护对话历史,决定模型每次请求能看到什么
工具调度把模型说的「我要读文件」翻译成实际的函数调用,再把结果喂回去
执行策略权限控制、沙箱隔离、超时重试、审批流程
持久化会话记录可保存、可回放、可 fork
多形态同一个 runtime(运行时),能以 Web GUI(Graphical User Interface,图形用户界面)、CLI(Command Line Interface,命令行界面)、headless(无头模式)等多种方式被使用

如果你做过 agent 项目,大概率自己拼过这些零件。dsh 的思路是:把这些全部做成插件,用一套框架组装起来,形成一个可商用的平台。

来看一个对比:

裸 LLM 调用
┌──────────┐     ┌──────────┐
│  用户     │────▶│  LLM API │────▶ 文本输出
└──────────┘     └──────────┘
                  (没有文件、没有终端、没有持久化)

Harness 化 Agent
┌──────────┐     ┌──────────────────────────────────┐     ┌──────────┐
│  用户     │────▶│  Harness Runtime                 │────▶│  LLM API │
│          │     │  ┌─────────────────────────┐     │     └──────────┘
│  Web/CLI │     │  │ 会话管理  工具注册表      │     │
│  SDK/ACP │     │  │ 执行流水线  沙箱策略       │     │
│          │     │  │ 持久化  审批  子代理       │     │
└──────────┘     │  └─────────────────────────┘     │
                 └──────────────────────────────────┘
                  (文件、终端、LSP、shell 全部接入)

金句:LLM 是引擎,Harness 是底盘。没有底盘的引擎能转,但上不了路。

1.2 dsh 项目定位与现状

dsh 是由 DeepSeek AI 开发的开源项目,仓库地址在 GitHub(github.com/deepseek-ai…

  • 版本:0.1.0-rc.5(Release Candidate,发布候选版本)
  • 许可证:MIT(Massachusetts Institute of Technology,麻省理工学院)许可,商用友好
  • 阶段:开发者预览(Developer Preview),官方明确说「未来将出现破坏兼容性的变更」
  • 语言:TypeScript,Node.js 运行时
  • 包管理:pnpm(Performant npm,高性能 Node 包管理器),monorepo(单体仓库)结构

项目采用 monorepo 布局,主要目录结构如下:

deepseek-harness/
├── apps/               # 应用入口
│   ├── cli/            # dsh 命令行工具
│   ├── web/            # Web 前端应用(Vite 构建)
│   └── qurvis/         # 桌面应用(Electron 壳)
├── packages/           # 50+ 功能包(按家族分组)
│   ├── core/           # 核心包(session/tools/agent/agent-loop/scope)
│   ├── llm/            # LLM 适配器
│   ├── fs/             # 文件系统
│   ├── shell/          # Shell 执行
│   ├── boot/           # 启动装配
│   └── ...             # 其他能力家族
├── docs/               # 架构文档 + 子系统文档
├── examples/           # 示例项目(acp-agent/headless-agent/jsonrpc-agent 等)
└── packages/bundle/    # 默认组合包(dsh-base/dsh-web-app/dsh-headless)

packages/ 下有 50 多个包,全部以 @deepseek-ai/dsh-* 为 npm scope(命名空间)发布。这不是一个小项目——它是一个完整的平台。

金句:50 多个包,不是一个应用,是一个生态的雏形。

1.3 与 Claude Code / Codex / OpenCode 的定位差异

你可能已经在用其他 coding agent 了。Claude Code、OpenAI Codex、OpenCode 都是优秀的工具。dsh 和它们的区别在哪?

维度dshClaude CodeCodexOpenCode
架构模式全插件化,Cordis 驱动单体应用单体应用插件化
开源程度MIT 开源,可自托管闭源部分开源开源
Provider 可替换任意 LLM 适配器(ctx.llm 注册)仅 Claude仅 OpenAI多模型
程序化接入ACP + SDK + JSON-RPC不支持不支持不支持
多形态Web/CLI/Headless/SDK/ACPCLI 为主IDE 集成CLI 为主
沙箱策略可插拔(bwrap/Landlock/Seatbelt)内置内置

核心差异在于「可组合性」。dsh 的设计哲学是:每一个功能都是插件,包括模型适配器、工具注册表、会话日志、agent 循环本身。这意味着你可以替换任何一个组件,而不需要 fork 整个项目。

举个例子:你想把文件系统操作指向远程沙箱(比如 E2B 沙箱),在 dsh 里只需要替换 ctx.fs(文件系统服务)的 Provider(提供方),Bash、PTY(Pseudo Terminal,伪终端)、LSP(Language Server Protocol,语言服务器协议)等工具会自动跟着搬过去,因为它们都依赖同一个文件系统接口。在其他单体应用里,这可能需要改几十处代码。

金句:单体应用改一个功能要动全身,插件化平台换一个 Provider 就够了。

1.4 快速上手:npx 运行、源码构建、第一个会话

方式一:npx 一键运行

如果你只是想体验,最快的方式是:

npx @deepseek-ai/dsh web

前提是你装了 Node.js。这条命令会启动 Web UI,默认地址是 http://127.0.0.1:3080。打开浏览器就能看到界面,按引导设置模型 API Key(API 密钥)即可开始对话。

首次运行时,dsh 会在 $DSH_HOME(默认是 ~/.dsh)下创建 profile(装配档案)模板,包括 profiles/web/ 目录和默认配置文件。

方式二:从源码构建

如果你想深入源码,建议从源码运行:

git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web

环境要求:

  • Node.js 22.19+ 或 24+
  • pnpm 11.7.0

pnpm install 会安装所有依赖(包括 workspace 里的 50+ 包),pnpm run build 会构建全部包,pnpm dsh web 启动 Web 应用。

入口源码:bin.ts 是怎么分发的

dsh 的 CLI 入口在 apps/cli/src/bin.ts,它做的事情很简单:解析参数,按模式动态 import 对应的执行器。来看真实代码:

// apps/cli/src/bin.ts(节选)
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { loadLayeredEnv } from '@deepseek-ai/dsh-app-boot'
import { parseDshArgs } from './args.ts'

function readVersion(): string {
  const manifest = JSON.parse(
    readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8'),
  ) as { version?: unknown }
  return typeof manifest.version === 'string' ? manifest.version : '0.0.0'
}

const invocation = parseDshArgs(process.argv.slice(2), readVersion())

switch (invocation.mode) {
  case 'profile': {
    const { runProfile } = await import('./profile-boot.ts')
    await runProfile({
      environment: loadLayeredEnv('dsh'),
      profile: invocation.profile,
      patchFiles: invocation.patches,
      args: invocation.args,
    })
    break
  }
  case 'plugin': {
    const { runPlugin } = await import('./plugin.ts')
    process.exit(runPlugin(invocation.profile, invocation.args))
    break
  }
  case 'dump-config': {
    const { runDumpConfig } = await import('./dump-config.ts')
    runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches)
    break
  }
  default:
    invocation satisfies never
    throw new Error(`dsh: unhandled invocation mode ${JSON.stringify(invocation)}`)
}

这段代码的设计要点有三个:

  1. 动态 import:每个模式只加载自己需要的代码,不相关的模块不会进入进程。dsh web 不会加载 dump-config 的逻辑,反之亦然。
  2. 三种模式profile(启动一个装配档案)、plugin(管理插件依赖,转发给 pnpm)、dump-config(打印配置树并退出,不启动服务)。
  3. satisfies never 守卫default 分支用了 TypeScript(TypeScript,带静态类型的 JavaScript 超集)的 satisfies never 操作符,如果将来加了新模式但忘了加 case,编译期就会报错。这是工程级别的防御编程。

参数解析:args.ts 的设计

parseDshArgs 函数在 apps/cli/src/args.ts 中定义。它的设计有一个巧思:启动器只解析自己拥有的标志(--profile--patch--dump-config),第一个不认识的 token 之后的参数全部原样传递给被启动的应用插件。

来看帮助示例:

Examples:
  dsh --profile web                          boot the web profile (same as: dsh web)
  dsh --profile headless "run the tests"     answer one task, print the result, and exit
  dsh --profile tui --patch ./extra.yml      boot a custom profile with one extra overlay
  dsh --profile tui --resume <session>       arguments after the launcher flags reach the app
  dsh --profile web --help                   the web app's own flags and help
  dsh plugin --profile tui add <package>     install a plugin into the tui profile

注意 dsh --profile tui --resume <session> 这行。--resume 不是启动器的标志,它会被传递给 tui profile 内部的应用插件去处理。这种「启动器只管装配,应用逻辑交给插件」的分层,让 dsh 的 CLI 保持了极简。

还有一个语法糖:dsh web--profile web 的别名,定义在 args.ts 里的子命令中:

// apps/cli/src/args.ts(节选)
const web = program.command('web')
  .description('boot the web profile (alias of --profile web)')
  .helpOption(false)
  .allowUnknownOption()
  .passThroughOptions()
  .enablePositionalOptions()
  .argument('[args...]', 'arguments for the web app')
  .option('--patch <path>', 'extra patch-list overlay', collect)
  .option('--dump-config', 'print the composed web-profile tree and exit')
  .action((args: string[], options: BootOptions) => {
    rejectParentOptions('web')
    resolved = resolveBoot(web, 'web', options, args)
  })

第一个会话

启动 Web UI 后,流程是这样的:

  1. 打开 http://127.0.0.1:3080
  2. 首次使用会引导你设置模型(选 provider、填 API Key),不需要重启
  3. 选择工作区(workspace)目录
  4. 开始对话

对话过程中,任何超出权限策略的操作,dsh 会先问你。比如模型想执行一个 shell 命令,会弹审批确认。这是 dsh-base bundle 里内置的审批策略在工作,后续章节会详细拆解。

1.5 总体架构一览:从 CLI 到插件树

现在把视角拉高,看 dsh 启动后整体长什么样。

五层架构

用户入口                装配层                运行时核心              能力层
┌──────────┐      ┌──────────────┐      ┌──────────────┐      ┌──────────────┐
│ dsh web  │────▶│              │      │              │      │  fs (文件)    │
│ dsh --   │     │  Profile     │      │  Cordis      │      │  shell (终端) │
│  profile │────▶│  Bundle      │────▶│  插件树       │────▶│  llm (模型)   │
│  headless│     │  Patch       │      │              │      │  web (搜索)   │
│ SDK/ACP  │      │              │      │              │      │  subagent    │
└──────────┘      └──────────────┘      └──────────────┘      └──────────────┘
                   三层叠加装配              ctx 服务总线           可替换 Provider

每一层的职责:

职责关键概念
用户入口接收命令和参数CLI / Web / Headless / SDK / ACP
装配层把配置和代码组装成插件树Profile / Bundle / Patch
运行时核心管理插件生命周期和事件分发Cordis 框架、ctx 服务总线
能力层提供具体功能(文件读写、shell 执行等)Seam(接缝):Service / Provider / Consumer

装配层:三层叠加

启动一个 dsh 实例,实际发生的事情是:

  1. 读取 Profile:找到 $DSH_HOME/profiles/<name>/ 下的配置
  2. 叠加 Bundle:按 Profile 列出的顺序,把每个 Bundle 的 Cordis 配置项叠上去
  3. 应用 Patch:按层级顺序应用 patch(补丁),按 id 整行替换配置

叠加顺序从底到顶:

空配置列表
  │
  ▼
各 Bundle 的 patch(按 profile 列出的顺序)
  │
  ▼
Profile 自己的 cordis.patch.yml
  │
  ▼
Home 级 $DSH_HOME/cordis.patch.yml
  │
  ▼
--patch 命令行临时 overlay(覆盖层)
  │
  ▼
最终配置树 → 交给 Cordis Loader 挂载

你可以用 dsh --profile web --dump-config 查看最终合成的配置树,确认哪些条目被加载了、哪些被 patch 替换了。

运行时核心:Cordis 插件树

配置树合好后,交给 Cordis 框架的 Loader 挂载。每个配置条目对应一个插件,插件向共享上下文(Context,简称 ctx)贡献服务。最终形成一棵插件树,每个插件节点都可以注册服务、监听事件、产生副作用。

核心包及其在 ctx 上占据的键:

职责ctx 键
core/session仅追加的会话事件日志ctx.sessions
core/system-prompt提示词片段与工具 schema 组装ctx.systemPrompt
core/tools工具注册表和执行流水线ctx.tools
core/agentAgent 接口和注册表ctx.agents
core/agent-loop默认的 agent 驱动器ctx.agentLoop
core/scope按 agent 划分作用域库,无 ctx 键
llm/llm消息词汇表和适配器接口ctx.llm

这些包构成了 dsh 的核心控制主干。它们之间的关系是:agent-loop 驱动 turn(轮次)和 step(步骤),每个 step 里向 llm 发请求,拿到模型回复后调度 tools 执行,所有过程记录到 session 日志。

能力层:Seam 架构

能力层是 dsh 最有意思的设计。每个能力(文件系统、shell、搜索等)都遵循三段式:

  • Service Definition(服务定义):接口契约,声明有哪些方法
  • Service Provider(服务提供方):具体实现,可以被替换
  • Consumer(消费方):面向模型的工具,调用 Provider 的方法

比如文件系统:

  • Service Definition 在 packages/fs/fs/ 里定义了读写文件、列目录等接口
  • Provider 有 fs-local(本地文件系统)、fs-sandbox(沙箱受限)、fs-e2b(远程 E2B 沙箱)
  • Consumer 是 tool-fs,把文件操作暴露给模型作为工具调用

替换 Provider 就能改变整个产品的行为——把 fs-local 换成 fs-e2b,所有依赖文件系统的工具(Bash、终端、LSP)都会自动指向远程沙箱。不需要改工具代码。

金句:在 dsh 里,能力不是一个函数,是一根可以拨插的接缝。

六种服务方式

同一个 runtime,支持六种打开方式:

方式命令适用场景
Web GUIdsh web浏览器中交互式对话,默认 127.0.0.1:3080
Headlessdsh --profile headless "task"单次任务,打印结果退出,适合 CI(Continuous Integration,持续集成)
Web GUI(双半包)host + client浏览器端 + 宿主端分离部署
ACPpackages/acpAgent Client Protocol(代理客户端协议),程序化自动化
SDKpackages/sdkstdio JSON-RPC(JSON Lines 远程过程调用),外部进程驱动
人机协作interaction 家族命令、审批、权限、提问

这六种方式共享同一个插件树和会话日志,只是入口不同。你在 Web GUI 里的会话,可以 fork 到 Headless 模式跑 CI,也可以通过 SDK 被另一个程序调用。

启动链:从 bin.ts 到插件树

把前面讲的串起来,dsh 的完整启动链是这样的:

1. 用户执行 dsh --profile web
       │
2. bin.ts 解析参数 → parseDshArgs() → mode='profile'
       │
3. 动态 import profile-boot.ts → runProfile()
       │
4. app-boot 加载 profile:读取 bundles 列表、叠加 patch 层
       │
5. composeEntries() 合成最终配置树
       │
6. Cordis Loader 挂载配置树 → 实例化每个插件 → 注册 ctx 服务
       │
7. web-app bundle 的插件启动 Web Server(端口 3080)
       │
8. 浏览器访问 → 加载 client 端插件 → 渲染 UI

其中第 4-6 步是装配的核心。来看 profile-boot.ts 里的关键逻辑:

// apps/cli/src/profile-boot.ts(节选)
export function prepareProfile(name: string, userLayer = true): Profile {
  healProfilesModuleFallback(INSTALL_ANCHOR)
  const profile = loadProfile(NAME, name, INSTALL_ANCHOR, undefined, { userLayer })
  // ... 总是重写空根配置,防止 Loader 的回写把组合行固化进去
  return profile
}

注意注释里提到的设计细节:根配置文件 cordis.yml 每次启动都会被重写为空列表 []。这是因为 Cordis 的 Loader 有一个回写机制——插件自我卸载时会持久化当前配置树。如果不重置,上次组合的行会被固化进根文件,下次启动时每个 Bundle 的插入操作会重复一遍,导致配置膨胀。这种细节体现了工程上的谨慎。

金句:一个健壮的系统,连「回写会叠加」这种边角 case 都要兜住。

本章小结

要点说明
Harness 定义介于 LLM 和工程环境之间的运行时层,负责上下文、工具、策略、持久化、多形态
dsh 定位DeepSeek AI 开源的插件化 Agent Runtime,MIT 许可,开发者预览期
核心差异全插件化架构(Cordis 驱动),任意 Provider 可替换,六种服务方式
入口设计bin.ts 三模式分发(profile/plugin/dump-config),动态 import 按需加载
架构五层用户入口 → 装配层 → Cordis 插件树 → 核心包 → 能力 Seam
装配三概念Profile(装配档案)、Bundle(组合包)、Patch(按 id 整行替换)
能力 SeamService Definition + Provider + Consumer,换 Provider 即换产品形态
服务方式Web GUI / Headless / 双半包 / ACP / SDK / 人机协作

下章预告

这一章我们俯瞰了 dsh 的全貌。但你可能还有一个疑问:为什么 dsh 敢把所有功能都做成插件?它的插件引擎到底有多强?

下一章,我们来拆 Cordis——驱动 dsh 的插件引擎。你会看到它的四种事件分发模式、服务注入机制、可逆副作用设计,以及它在 dsh 中的具体落地。

我是怕浪猫,如果你觉得这篇有用,收藏一下整个系列。评论区聊聊你正在用的 coding agent 是单体还是插件化的?

下一章:Cordis 插件引擎