大数跨境

DeepSeek Harness 二次开发实战:从「一切皆插件」到写出你自己的 Tool

DeepSeek Harness 二次开发实战:从「一切皆插件」到写出你自己的 Tool Ai向量云
2026-10-02
11
导读:DeepSeek Harness 真正打动二次开发者的,不是它有多少现成功能,而是它把"改框架"的权力还给了开发者——模型、工具、主循环、UI,没有一处是你不能碰的。

DeepSeek Harness 二次开发实战:从「一切皆插件」到写出你自己的 Tool

模型适配器、工具注册表、会话日志、Agent 主循环——全部是插件,没有不可替换的核心。这句话,就是 DeepSeek Harness 给二次开发者的入场券。

前面几篇我们聊了 DeepSeek Harness 的定位、桌面版体验和场景实战。但真正让它在开发者圈子里封神的,是另一件事:它是目前少有的、把"插件架构"写进论文的开源 Agent 框架——Everything is a plugin,一切皆插件。

这意味着一个普通人也能做、且价值巨大的动作:给 Harness 写插件,扩展它、改造它,甚至替换它的心脏。

这篇文章从技术架构讲起,手把手带你完成一次完整的二次开发:写 Hello World、写自定义 Tool、加可配置项、管理外部资源、打包成可安装 Bundle,最后解决几个真实的工程问题。


一、技术架构:先看懂这台"乐高机器"

1.1 架构总纲:Agent = Model + Harness

官方对 Harness 的定义很克制:

DeepSeek Harness(dsh)是连接模型与真实环境、调度工具完成任务的中介层。模型是 Agent 的灵魂,Harness 给予 Agent 理解环境、使用工具的能力。

落到架构上,就是一句话:模型、工具、技能、会话、沙箱、存储、循环、调度、UI,全部由插件组合而成。

没有"不可替换的核心"。想换模型?装个插件。想加工具?装个插件。想改 Agent 主循环?还是装个插件。

1.2 心脏:Cordis 插件系统

Harness 的底座是 Cordis——一个只负责插件"插进去、拔出来、管依赖"的微内核。它最早在机器人生态 Koishi 里跑了四年,如今被 DeepSeek 选中成为 Agent 框架的心脏,配套论文《A Programming Paradigm for Spatiotemporal Composability》(时空可组合性的编程范式)以预印本发布,署名单位为北京大学与 DeepSeek-AI。

Cordis 的职责非常聚焦,只有三件事:

  • 加载与卸载插件:按声明顺序装配,卸载时自动清理该插件注册的一切能力;
  • 依赖注入:插件通过 inject 声明依赖,框架保证依赖就绪后才调用它;
  • 生命周期管理:服务(Service)随上下文创建、随上下文销毁,热替换时自动 dispose 再重建。

记住这三个职责,后面所有代码都能看懂。

1.3 插件的本质:一个 apply 函数

在 DeepSeek Harness 里,一个插件就是一个导出 apply 函数的 TypeScript 模块:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-plugin'

export function apply(ctx: Context) {
  // 在这里注册能力
}

框架加载插件时调用 apply,传入 ctx(上下文,即服务容器)。你在 ctx 上注册的一切能力,插件卸载时会被自动清理——这是 Harness 插件和"随手写个脚本"最本质的区别:生命周期有始有终,不会泄漏。

1.4 插件的三种形态

形态
适用场景
写法
函数形式
绝大多数场景,注册能力
export function apply(ctx) {...}
对象形式
需要携带额外元信息
export default { name, inject, apply }
类形式(Service)
你要"提供服务"给别的插件依赖
class MyService extends Service

三种形态可以按需选择,类形式最强大——它能把你的能力暴露成 ctx.myService,让其他插件通过 inject: ['myService'] 来依赖和使用。

1.5 依赖注入:inject 与 ctx

插件顶部常见的 inject 字段,是 Cordis 依赖注入的入口:

export const inject = ['tools']

含义:我这个插件依赖 ctx.tools 这个服务。Cordis 会保证两件事:

  1. 服务就绪后才调用 apply;
  2. 如果 ctx.tools 的 provider 被热替换,插件会自动 dispose 并重新 apply。

这就是"一切皆插件"能真正落地的机制保证——插件之间不是靠全局变量硬耦合,而是靠声明式依赖解耦。

1.6 事件系统:emit / serial / waterfall

Harness 内置三种事件语义,插件可以监听或拦截框架的各个环节:

类型
行为
用途
emit
广播通知,监听器不影响流程
打日志、埋点
serial
按注册顺序依次执行
有序的副作用
waterfall
链式传递,必须调 next() 才能继续
拦截、改写、审批

1.7 Tool 执行管道

注册的 tool 会走一条完整管道,这是做权限控制和安全拦截的关键:

模型返回 tool_call → tools/pre-execute (waterfall)
                    → tools/execute (waterfall)
                    → 你的 execute() 函数
                    → tools/post-execute (waterfall)
                    → tool/result (session event)

看懂这条管道,你就能理解:权限策略可以在 tools/pre-execute 拦截工具,审批机制可以在执行前要求确认,结果自动记录到 session log。

1.8 配置层:按层叠加,后层胜出

Harness 的配置是分层的,加载顺序如下:

  1. profile.bundles 列表中的各 bundle(按顺序)
  2. profile 的 cordis.patch.yml
  3. $DSH_HOME/cordis.patch.yml(机器级)
  4. --patch 命令行 overlay

后应用的层按行胜出,且是整体替换 config,不是深度合并。 这个机制让"同一套代码、不同部署不同配置"变得极其干净。


二、二次开发准备:源码编译安装

要做二次开发(改内核源码、定制插件),官方推荐源码部署:

git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
corepack enable
pnpm install

前置要求:Node.js 22 或 24、pnpm。装好后用 pnpm dsh --version 验证。


三、实战一:Hello World 插件(5 分钟)

先跑通最小闭环,验证插件机制。

第一步:创建插件文件scratch-plugin/src/my-plugin.ts:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello-plugin'

export function apply(ctx: Context) {
  console.log('[hello-plugin] plugin loaded!')
}

第二步:注册到配置cordis.yml:

- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'

注意:插件路径必须是绝对路径。patch 文件只贡献配置,不改变 loader 的模块解析基目录。

第三步:启动:

pnpm dsh web --patch ./scratch-plugin/cordis.yml

终端打印 [hello-plugin] plugin loaded! 即为成功。到这里,你已经是"Harness 插件开发者"了。


四、实战二:写一个自定义 Tool

Hello World 只是证明机制通了。真正有用的是给 Agent 加一个它不会的能力——自定义 Tool。

把 my-plugin.ts 替换为:

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true, description: 'The name to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

defineTool 的关键字段,每一个都是"模型接口契约":

字段
作用
name
模型可见的工具名,64 字符内,只允许 [A-Za-z0-9_-]
description
模型用来判断是否调用该工具的描述
parameters
JSON Schema,自动推导 args 类型并做运行时校验
output.schema
声明工具输出的 schema
output.render
把规范输出转成模型可见的 ContentBlock
execute
实际执行逻辑,接收校验后的 args

ctx.tools.register() 会返回一个 disposer,插件卸载时自动把 tool 从注册表移除——模型后续请求再也看不到它。

验证:在 Web UI 里输入 Use the greet tool to greet Ada.,模型会调用 greet,返回 Hello, Ada!。


五、实战三:让插件可配置

写死的问候语不实用。用 Schemastery 给插件加配置项:

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

// 定义 Config interface
export interface Config {
  greeting: string
  emoji: boolean
}

// 导出同名 Schema(Cordis 用它校验 + 填充默认值)
export const Config: Schema<Config> = Schema.object({
  greeting: Schema.string().default('Hello'),
  emoji: Schema.boolean().default(true),
})

export function apply(ctx: Context, config: Config) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true, description: 'The name to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      const suffix = config.emoji ? ' 👋' : ''
      return `${config.greeting}, ${args.name}!${suffix}`
    },
  }))
}

配置写入 cordis.yml:

- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
      config:
        greeting: 'Hey'
        emoji: false

设计原则:凡是不同部署可能需要不同值的参数,都应定义为 Config 字段;Schema 在插件加载时执行校验,配置错了会响亮地失败(加载即报错),而不是运行时静默异常。


六、实战四:管理外部资源(数据库连接池)

很多二次开发要对接外部系统。如果 tool 需要维持一个长连接,比如数据库连接池,用 ctx.effect 管理生命周期:

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'db-query-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  let pool: ConnectionPool | undefined

  ctx.effect(() => {
    pool = createConnectionPool({ host: 'localhost', port: 5432 })
    return () => {
      pool?.close()
      pool = undefined
    }
  })

  ctx.tools.register(defineTool({
    name: 'db_query',
    description: 'Run a read-only SQL query.',
    parameters: {
      sql: { type: 'string', required: true, description: 'SQL query' },
    },
    output: {
      schema: { type: 'array', items: { type: 'object' } },
      render: (_args, rows) => [{ type: 'text', text: JSON.stringify(rows, null, 2) }],
    },
    async execute(args) {
      if (!pool) throw new Error('Database pool not available')
      return await pool.query(args.sql)
    },
  }))
}

ctx.effect 返回的清理函数会在这些场景自动执行:插件被手动 dispose、依赖服务消失(热替换)、HMR 触发配置变更、整个应用关闭。连接池绝不会泄漏。


七、实战五:打包为可安装的 Bundle

开发完要交付。把插件打包成标准 Bundle:

hello-plugin/
├── package.json       # 声明 dsh.bundle
├── cordis.patch.yml   # 该 bundle 贡献的配置层
└── index.js           # 插件入口

package.json:

{
  "name": "dsh-hello-plugin",
  "version": "0.1.0",
  "type": "module",
  "main": "index.js",
  "files": ["index.js", "cordis.patch.yml"],
  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}

cordis.patch.yml:

- insert:
    - id: hello
      name: dsh-hello-plugin

安装到 profile:

dsh plugin --profile demo add ./hello-plugin

验证:

dsh --profile demo --dump-config
dsh --profile demo

--dump-config 是二次开发的黄金命令——随时打印最终配置树,排查"配置到底生效没有"的疑难杂症。


八、实战六:提供服务给别的插件(类形式)

想让自己的插件成为基础设施,被其他插件依赖,用类形式:

import { Service, type Context } from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis' {
  interface Context {
    myService: MyService
  }
}

export default class MyService extends Service {
  static inject = ['tools']

  constructor(ctx: Context) {
    super(ctx, 'myService')
  }

  doSomething() { /* ... */ }
}

之后别的插件就能:

export const inject = ['myService']
export function apply(ctx: Context) {
  ctx.myService.doSomething()
}

这一步把"写工具"升级成了"写框架"——你提供的服务会成为整个 Harness 生态的一部分。


九、实战七:完整案例——代码仓库浏览插件

前面六个实战都是"零件级"演示。这一章把它们串成一个完整、可运行、解决真实问题的插件:repo-explorer(代码仓库浏览插件)。

要解决的问题:默认情况下,让 Harness 的 Agent 自主浏览一个本地代码仓库——列出目录结构、读取关键文件、理解项目技术栈。这在"让 AI 接手一个陌生项目"的场景里极其实用,也是社区"本地代码依赖分析"玩法的核心。

9.1 完整代码

import { readdir, readFile, stat } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'repo-explorer'
export const inject = ['tools']

// 可配置项:文件大小上限 + 忽略目录
export interface Config {
  maxFileBytes: number
  ignore: string[]
}

export const Config: Schema<Config> = Schema.object({
  maxFileBytes: Schema.number().default(128 * 1024),
  ignore: Schema.array(Schema.string()).default(['node_modules', '.git', 'dist', '.next']),
})

export function apply(ctx: Context, config: Config) {
  // 工具一:列出目录内容
  ctx.tools.register(defineTool({
    name: 'list_dir',
    description: 'List files and subdirectories in a directory.',
    parameters: {
      path: { type: 'string', required: true, description: 'Absolute path to the directory' },
    },
    output: {
      schema: { type: 'array', items: { type: 'string' } },
      render: (_args, value) => [{ type: 'text', text: value.join('\n') }],
    },
    async execute(args, exec) {
      const entries = await readdir(args.path, { signal: exec.signal })
      return entries
        .filter((entry) => !config.ignore.includes(entry))
        .sort()
    },
  }))

  // 工具二:读取文件内容(带大小上限与取消信号)
  ctx.tools.register(defineTool({
    name: 'read_file',
    description: 'Read a text file from disk. Returns its content as text.',
    parameters: {
      path: { type: 'string', required: true, description: 'Absolute path to the file' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args, exec) {
      const info = await stat(args.path)
      if (info.size > config.maxFileBytes) {
        throw new Error(`File too large (${info.size} bytes, limit ${config.maxFileBytes})`)
      }
      return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
    },
  }))
}

9.2 逐块拆解:为什么它是"完整的"

设计点
实现
价值
两个 Tool 组合
list_dir
 + read_file
Agent 先"看目录"再"读文件",形成完整浏览闭环
可配置项
maxFileBytes
、ignore
不同项目可调,防误读 node_modules 这类巨型目录
大小上限
stat
 后判断 info.size
安全边界
:避免一次性读入几百 MB 文件撑爆上下文
取消信号
exec.signal
 传给 readdir/readFile
用户打断时,底层 I/O 能真正中断,不浪费资源
输出渲染
output.render
数组类型结果转成模型可见的文本

9.3 注册配置

- insert:
    - id: repo-explorer
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/repo-explorer.ts'
      config:
        maxFileBytes: 65536
        ignore: [node_modules, .git, dist, .next, build]

9.4 启动与验证

pnpm dsh web --patch ./scratch-plugin/cordis.yml

在 Web UI 输入:

浏览 /home/user/my-project 目录,找到入口文件,
总结这个项目的技术栈和目录结构。

你会看到 Agent 自主完成一串动作:list_dir 看根目录 → 发现 package.json → read_file 读取 → 继续深入 src/ → 最终输出技术栈和结构总结。整个过程无需你手动贴代码——这正是二次开发赋予 Agent 的"动手能力"。

9.5 安全提示

read_file 能读任意路径,是高风险工具。生产环境务必叠加第九章的权限拦截:在 tools/pre-execute 里把路径白名单限定在工作区目录内,防止 Agent 越权读取敏感文件。


十、解决实际问题:拦截、审批、可观测

二次开发最常见的目标不是"加功能",而是"加管控"。结合 Tool 执行管道和事件系统,可以做三件实事:

1. 权限拦截——在 tools/pre-execute 用 waterfall 拦截危险工具:

export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.waterfall('tools/pre-execute', async (toolName, args, next) => {
    if (toolName === 'bash' && isDangerous(args)) {
      throw new Error('Blocked by safety policy')
    }
    return next()
  })
}

2. 审批确认——执行前要求人工确认。

3. 可观测性——监听事件打日志:

ctx.on('tools/post-execute', (toolName, result) => {
  console.log(`Tool ${toolName} executed, result length: ${result.length}`)
})

再配合 --dump-config 和 HMR 热更新(改插件保存后自动卸载重载,无需重启进程),二次开发的调试体验相当顺滑。


十一、二次开发的典型场景清单

总结一下,哪些实际问题值得你动手二次开发:

场景
用什么实现
接入企业内部系统(ERP、CRM、工单)
自定义 Tool + ctx.effect 管理连接
数据查询 / 报表生成
Tool 对接数据库 / API
安全管控与权限审批
tools/pre-execute
 waterfall 拦截
自定义模型接入(私有化模型)
模型适配器插件
定时任务 / 工作流编排
调度插件
审计与合规
事件监听 + session log 回放
团队内部共享能力
类形式 Service + Bundle 分发

核心思路一句话:凡是 Harness 本身不会、但你的业务需要的,写成一个插件塞进去就行。


十二、别忘了:跑起来要 Token

二次开发写得再漂亮,最终要烧 Token。Harness 本身免费开源,但每次 Agent 推理、每次工具调用都要走模型计费。省钱的关键还是那句老话:

  • 通过向量云获取火山方舟官方 API 和 Key:官方真实 Key(ark-****-****-****-****-****-88888 格式),请求直连 ark.cn-beijing.volces.com,不经中转,延迟、稳定性、计费标准与官方一致;
  • 低折扣 Token、按量计费、即用即扣,注册、充值 10 元、创建 Key 三步搞定;
  • 复杂开发调试用 DeepSeek V4 Pro(deepseek-v4-pro-ga-260813),日常跑通用 DeepSeek V4 Flash(deepseek-v4-flash-ga-260731)。

获取 API Key:打开 https://ark.tokenrize.cn/ 注册充值,创建 Key 后填入 Harness 的 设置 → 模型 即可。

Harness 走 OpenAI 兼容协议,接入地址 https://ark.cn-beijing.volces.com/api/v3。


写在最后

DeepSeek Harness 真正打动二次开发者的,不是它有多少现成功能,而是它把"改框架"的权力还给了开发者——模型、工具、主循环、UI,没有一处是你不能碰的。

从 Hello World 到自定义 Tool,从可配置插件到提供 Service,再到用事件管道做权限拦截,这条路径走下来,你收获的不只是一个插件,而是理解了"一切皆插件"背后那套优雅的工程范式。

动手试试:给 Harness 写一个解决你自己问题的插件,然后回来告诉我,你用它干了什么。


向量云(南京)科技有限公司

火山方舟官方 API · 低折扣 Token · 即用即扣 · 充值开票服务

获取 API Key:**https://ark.tokenrize.cn/**


【声明】内容源于网络
0
0
Ai向量云
向量云是国内云原生算力平台领域的企业,通过以Kubernetes为核心的云原生技术打造的新一代云原生算力调度平台,帮助企业建设新一代算力基础设施,加速构建、运行及管理人工智能应用。
内容 23
粉丝 0
Ai向量云 向量云是国内云原生算力平台领域的企业,通过以Kubernetes为核心的云原生技术打造的新一代云原生算力调度平台,帮助企业建设新一代算力基础设施,加速构建、运行及管理人工智能应用。
总阅读478
粉丝0
内容23