DeepSeek把Agent内核拆成了插件

DeepSeek Harness 整体架构图
DeepSeek Harness 整体架构:Profile/Bundle 分层组装,Cordis 插件树没有特权内核,能力接缝与运行形态全部可替换

2026 年的 Agent 框架赛道挤满了「内核 + 插件」的设计:核心循环写死在框架里,插件只能在边缘挂一点工具和提示词。DeepSeek 开源的 DeepSeek Harness(命令行叫 dsh)走了一条极端路线——没有内核。模型适配器是插件,工具注册表是插件,会话日志是插件,连 Agent 循环本身都是插件。你可以从配置文件里把官方实现整行替换成自己的版本,框架对此没有任何怨言。

这种「一切皆插件」(everything is a plugin)的架构不是营销话术,而是一套可验证的工程主张:仓库文档里有一张「产品特性 → 插件机制」的映射表,每一行产品功能都对应某个已文档化扩展点上的监听器,没有一行直接修改循环。支撑这一切的底座是 Cordis——一个把「时空可组合性」当作第一性原理的插件框架,其设计思想写在论文《A Programming Paradigm for Spatiotemporal Composability》里。

本文按四层展开:先看整体架构怎么分层组装,再拆 Cordis 插件机制的五个核心概念与四种事件分发模式,然后走一遍 Agent 生命周期的全链路事件流,最后落到工具插件与 Hook 插件的完整技术契约,以及从零扩展一个 dsh 的实战路径。

一句话主线:dsh 的可扩展性来自三个设计决策——注册即效应(所有注册都是可回滚的副作用)、事件即扩展点(插件通过类型化事件互相协作)、模型可见即已记录(会话日志是模型上下文的唯一来源)。理解这三句,就理解了 dsh 的全部插件设计。

整体架构:一切皆插件

一个正在运行的 dsh 进程,本质上是一棵启动时装配出来的 Cordis 插件树。装配不是写代码,而是分层叠加配置

Profile 与 Bundle:配置层的乐高

dsh 用两个概念管理组装:

  • Profile(画像):一个命名组合模板,存放在 Harness home 目录。它声明自己堆叠哪些 bundle、持有哪些目录外的第三方插件,还保留用户自己的 cordis.patch.yml 补丁。官方内置 webheadless 两个模板。
  • Bundle(捆):Cordis 配置行及其所挂载代码的分发格式。bundle 插入的任何配置行,仍然可以被它上层的 patch 覆盖——分发与定制不冲突。

两者都在各自 package.jsondsh 字段里自我声明:dsh.profile 列出 profile 堆叠的 bundle 列表,dsh.bundle 指向 bundle 的 patch 文件。

// 一个 bundle 的 package.json 自我声明 { "name": "my-bundle", "dsh": { "bundle": "./cordis.patch.yml" // 本 bundle 插入的配置行 } }

三层 bundle:base、web、headless

Bundle职责搭配 Profile
dsh-base所有 profile 的第一层:模型适配器、工具、持久化、沙箱与审批策略、设置、凭据、遥测web / headless
dsh-web-app浏览器应用与 Web UI、对话节点、编辑器联动web
dsh-headless一次性执行器,无服务器、纯 CLIheadless

装配顺序严格确定:profile 列出的 bundle 按声明顺序依次应用到空列表上,然后叠加 profile 自己的 cordis.patch.yml,再叠加 home 级补丁,最后是命令行 --patch 覆盖。补丁按 id 定位目标行,整行替换其配置,或插入全新行。想看你的机器实际启动了哪棵树:

# 打印当前实际装配的插件树,任何一行都可以被你自己的补丁替换 $ dsh --profile web --dump-config

核心包与稳定的服务键

插件树上的核心包各自贡献一个稳定的服务键,插件之间不 import 实现,只按键查找:

核心包拥有什么服务键
core/session追加式 SessionEvent 日志与内存存储ctx.sessions
core/system-prompt提示词段落与工具 schema 的装配ctx.systemPrompt
core/tools作用域工具注册表与受守卫的执行管线ctx.tools
core/agentAgent 接口、活动注册表与 agent/* 事件ctx.agents
core/agent-loop实现该接口的默认驱动器ctx.agentLoop
llm/llm消息与流式词汇表 + 模型适配器接缝ctx.llm
关键点:连 core/agent-loop(Agent 循环的默认实现)都是插件。理论上你可以写一个自己的循环驱动器,用配置补丁把官方实现整行换掉——这就是「没有特权内核」的字面意思。

插件系统底座:Cordis 五大核心

Cordis 是 dsh 脚下的框架。官方文档的表述很精确:插件向共享上下文贡献服务、类型化事件、可逆效应。拆开看是五个环环相扣的核心概念。

Cordis 插件机制五大核心概念图
Cordis 插件机制:五大核心概念 + waterfall 短路语义 + 四种事件分发模式

一个插件的最小形状

函数插件只有三个字段,这是 Cordis 世界的 Hello World:

import type { Context } from '@deepseek-ai/cordis' export const name = 'my-plugin' // 插件名:树上的唯一标识 export const inject = ['tools'] // 声明依赖的服务键 export function apply(ctx: Context) { // 挂载时执行,ctx 是本插件的上下文 ctx.tools.register(/* 注册你的能力 */) }

也可以写成 Service 子类,由 Cordis 挂载完整生命周期。两种形态等价,函数形态覆盖绝大多数场景。

核心一:插件即 Service

插件不是一个「挂件」,而是一个服务单元。它的生命周期由 Cordis 管理:挂载时执行 apply(ctx),卸载时自动撤销 apply 里做过的所有事。插件可以随时被加载、被替换、被卸载——这就是「时空可组合性」里的时间维度。

核心二:上下文即仓库

每个插件拿到自己的 ctx,而 ctx 树是一个服务仓库:服务用稳定键声明(ctx.toolsctx.llmctx.sessionsctx.agents……),消费方按键查找,从不 import 具体实现。这带来一个直接后果:任何服务的实现都可以被另一个插件替换,消费方代码一行不改。

核心三:inject 声明依赖

inject: ['tools'] 不是装饰性注释,而是加载顺序的编排依据。Cordis 看到这个声明,会等 tools 服务就绪后再挂载你的插件。整棵插件树的加载顺序由所有插件的 inject 声明自动推导,不需要任何手工编排的启动序列——这是依赖注入在插件系统里的完全体。

核心四:类型化事件

插件之间通过类型化事件协作。事件名和载荷用 TypeScript 声明合并(declaration merging)定义在 Events 接口上,监听器签名由编译器检查。事件不是字符串魔法,而是编译期契约——拼错事件名、载荷类型不匹配,都会在构建时报错,而不是运行时静默失败。

核心五:注册即效应

这是整个架构的灵魂。插件在 apply 里做的一切——注册工具、安装提示词段落、监听事件——都通过 ctx.effect() / ctx.on() 登记为可逆副作用。插件卸载(dispose)时,这些副作用按注册的逆序全部回滚:

  • 注册过的工具自动注销,模型不再看到它
  • 安装过的提示词段落自动拆除
  • 监听中的事件自动解绑
为什么「注册即效应」重要:它让插件热重载成为免费功能。dsh vendor 了 HMR(热模块替换),因为「每次注册都是一个 ctx.effect」,热更新一个插件文件 = 卸载旧效应 + 挂载新效应,框架不需要为此写任何专门代码。写作插件的人也不需要记住「退出时清理」——没有清理函数这个概念,只有效应栈。

四种事件分发模式与短路语义

类型化事件怎么分发,决定了插件间协作的表达力。Cordis 提供四种分发模式,每种语义精确对应一类协作场景:

emit
广播
通知所有监听器,不关心返回值。适合纯通知场景:遥测上报、日志落盘、UI 刷新。
waterfall
瀑布
监听器收到 (...args, next),上一个的产出作为下一个的输入,可改写、可短路。适合拦截决策:权限、改写、拒绝。
parallel
并行
所有监听器并行执行,等全部完成。适合互不依赖的旁路处理:多个观测者各自独立消费同一事实。
serial
串行
监听器严格按顺序执行,前一个完成才轮到下一个,无 next()。适合强序场景:回合收尾时的收束逻辑。

waterfall:around 中间件,可改写、可短路

waterfall 是表达力最强的一种,语义类似 Koa 的中间件:监听器可以选择调用 next() 把调用委托给下游,也可以不调用 next() 直接返回——这就是短路,意味着「这次调用到此为止,我的返回值就是最终结果」。

以工具执行前的 tools/pre-execute 瀑布为例,三个插件排成一条链:

// 链上的三个监听器,注册顺序即执行顺序 // 1. 权限门:不决策,只观察,原样放行 ctx.on('tools/pre-execute', async (exec, next) => { audit(exec) // 记录审计日志 return next() // 委托下游 }) // 2. 策略插件:拥有决策权,直接短路 ctx.on('tools/pre-execute', async (exec, next) => { if (!isAllowed(exec)) { return { kind: 'deny', reason: 'Denied by policy.' } } // 不调用 next() = 短路 return next() }) // 3. 最终执行:只有上游全部放行才会到达

短路语义的精妙之处在于决策权的显式化:一个插件返回什么类型,就宣称自己拥有什么决策权。PreToolDecisionallow / deny / ask 是类型系统的产物,不是靠约定字符串。而 agent/turn-stopping 用的是 serial——它没有 next(),监听器按序执行完,回合才真正收束,因为收尾逻辑天然需要强序。

Agent 生命周期:一次 Turn 的全链路事件流

dsh 的 Agent 循环用两个时间单位组织:Step 是一次模型请求加上它引发的所有工具调用;Turn 是零到多个 Step,在第一份输入被认领前开启,在「不再欠任何一方任何东西」时闭合。把生命周期摊开,是 15 个事件的接力:

Agent 生命周期事件流图
一次 Turn 的全链路事件流:左侧 15 步接力,右侧会话日志以「模型可见 = 已记录」为不变量投影模型上下文

事件的三种身份

这 15 个事件不是同质的,按身份分三类,选错类别是扩展 dsh 时最常见的错误:

类别事件语义
持久会话事件turn/* · step/* · user/message · assistant/* · tool/*追加到会话日志的持久事实,重载后仍存在,经 session/event 广播
Agent 事件agent/pre-step · agent/request · agent/turn-stopping携带活动 Agent 的实时事件:收件箱、步骤、状态、请求
能力事件llm/stream · tools/* · fs/* · telemetry/*往接缝上挂策略与适配器,不 import 循环本身

其中 agent/pre-stepagent/requestllm/stream 和三个 tools/* 事件是 waterfall(必须调 next() 委托),agent/turn-stopping 是 serial(无 next())。

收件箱与 pre-step:模型看到什么,插件说了算

所有输入通过单一收件箱到达驱动器:有的消息立刻唤醒 Agent,注入的上下文则在收件箱里安静排队,直到下一条消息到来。agent/pre-step 决定模型看到什么——监听器可以改写认领到的消息,也可以直接拒绝。一个耐人寻味的细节:被拒绝的、或首条消息被改写为空的首个回合,仍然会以一个「没花任何 step 的 durable turn」闭合——日志必须记录这次尝试,哪怕模型根本没被调用。

会话日志:模型上下文的唯一来源

右侧的会话日志(Session Log)是整个架构的支点。deriveMessages() 从日志投影出模型历史;原始的 assistant/chunk 事件逐块保存流式增量,保证重放与 UI 保真;fork、resume、transcript、遥测、持久化,全部从这一条事件流派生。

不变量:模型可见 = 已记录。任何到达模型请求的内容,都必须能从日志重建——运行时用断言强制这一点。所以「新增一种模型可见的输入」在 dsh 里只有一条正确路径:扩展 SessionEventMap,新增一种会话事件,从日志渲染。直接往请求里塞字段的路被架构性封死了。

技术实现:工具插件的完整契约

工具是插件系统里最高频的扩展形态。dsh 用一个 defineTool 帮手把「模型可见的契约」收进类型系统:

import { readFile } from 'node:fs/promises' import type { Context } from '@deepseek-ai/cordis' import { defineTool } from '@deepseek-ai/dsh-tools' export const name = 'my-tool' export const inject = ['tools'] export function apply(ctx: Context) { ctx.tools.register(defineTool({ name: 'read_file', description: 'Read a file from disk.', // 模型看到的说明 parameters: { path: { type: 'string', required: true, description: 'Absolute path' }, limit: { type: 'number' }, // 缺省即可选 }, output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }], }, async execute(args, exec) { // args 已按 schema 推导出类型: { path: string; limit?: number } // exec 携带不可变身份 + token;signal 是唯一的操作字段 return readFile(args.path, { encoding: 'utf8', signal: exec.signal }) }, })) }

注册即效应:销毁插件纤程即注销工具。schema 自动流入系统提示词装配,模型由此得知工具的存在。这份契约有五条铁律,每条都在解决一类真实的工程事故:

01参数已为你校验
defineToolexecute 运行前,就用统一的 ParameterSchemaSpec 校验模型生成的 arguments:类型、必填键、字面量约束、exact-one 联合、嵌套值,全部过关才进入执行。DSL 表达不了的约束(非空字符串、正数、跨字段规则)仍需手工检查。直接注册原始 JSON-Schema 的工具(MCP 来源的工具就是这样进来的)则自己负责输入校验。
02注册借用只读定义
同进程内的类型化贡献不是序列化边界,注册后不得变更 schema 或替换回调。想热替换一个工具?销毁它的持有效应,注册新版本——而不是原地改。schemas() 只物化显式的模型侧投影。
03执行身份受保护
注册表把 arguments 物化为脱离原引用的无损 JSON(一次递归遍历完成),在策略启动前冻结,并分配不透明的 exec.tokencallIdnameargumentsagenttokensignal 在整个分发过程中不可变。唯一例外:around 分发包装器拿到的是可变视图,可以替换并恢复 exec.signal 来施加截止时间,但不能删除它。
04声明并返回单一规范值
output.schemaValueSchemaSpec,根可以是对象、数组、标量或 null。execute 只返回推导类型的值;注册表将其快照为无损 JSON、校验、冻结,再交给 output.render(args, value)。不要从执行体里返回内容块,也不要让调用方解析散文来找 id——结构化数据走规范值,呈现走 render
05抛错或非法值即 isError
注册表捕获执行体的抛错,并兜住 schema、渲染器、元数据投影、无损 JSON 的所有失败,不让异常逃逸到观测者之前。基础设施故障用 throw 表达;业务上的「不理想结果」放进规范值里,由 Native 渲染器解释给用户。

工具执行管线:五层各司其职

一次工具调用从模型发出到结果落日志,经过一条五层管线。dsh 文档给出了精确的选型规则——用错层是最常见的集成错误:

tools/pre-execute
策略门,waterfall。返回 allow / deny / ask 类型化决策;审批、沙箱策略、计划模式都在这层。
tools/execute
包装执行生命周期。截止时间、重试、指标采集;唯一可替换 exec.signal 的位置。
tools/post-execute
显式变换结果:改写呈现内容、附加面向模型的上下文。
tool/result*
不可变最终结果的持久落日志(durable 会话事件),被观测者消费。
tools/result
对不可变权威结果的只读观测:最终审计、指标、捕获。

此外还有两个不走管线的闸门:ctx.tools.guard() 提供单调终局拒绝(一旦 guard 拒绝,后续任何插件都翻不了案);ctx.tools.restrict()对齐的可见集过滤——工具搜索、渐进披露这类「可见工具集动态变化」的场景用它,注册表会同时保持呈现、查找、执行三处对齐。

Hook 插件:可重排的策略层

权限门禁是 Hook 插件的教科书案例。完整代码不到 20 行:

import type { Context } from '@deepseek-ai/cordis' import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools' declare function isAllowed(exec: ToolExecution): Promise<boolean> export const name = 'permission-gate' export function apply(ctx: Context) { ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => { if (!(await isAllowed(exec))) { return { kind: 'deny', reason: 'Denied by policy.' } } return next() }) }

这条 waterfall 是一个可重排的策略层:多个门禁插件各自监听同一事件,注册顺序决定优先级,每个插件都可以改写决策对象后放行,也可以直接返回类型化决策短路。所谓「原生 Hook」就是拦截点上的普通 Cordis 插件,不需要任何外部协议。

审批闭环是这套设计的点睛之笔:门禁返回 { kind: 'ask' },问题经 ctx.approval 送达用户,用户的回答回流为最终决策——AskUserQuestion 交互、权限系统全部建立在这一个机制上。而需要「单调终局拒绝」的不变量(比如沙箱硬边界)则绕过 waterfall,直接用 ctx.tools.guard(),保证任何插件都无法翻转。

与传统 middleware 对比:Express/Koa 中间件靠 app.use 顺序堆叠,决策是隐式的、无类型的;dsh 的 Hook 靠事件系统组织,决策是类型化的显式返回值,且生命周期跟随插件——插件卸载,Hook 自动摘除。你可以给同一个项目同时装三个来源的权限策略,它们天然可组合、可重排、可整体替换。

实战:从零扩展一个 dsh

下面五个实战由浅入深,覆盖从跑起来到深度定制的完整路径。dsh 目前处于 developer preview 阶段(官方明确会有破坏性变更),以下 API 以仓库 master 分支为准。

实战 1
跑起来,看一眼插件树
$ npx @deepseek-ai/dsh web # Web UI 默认 http://127.0.0.1:3080 $ dsh --profile web --dump-config # 打印实际装配的整棵插件树

dump-config 打印的每一行都能被你的补丁按 id 替换。把输出存下来,是理解「我这台机器上 dsh 到底由什么组成」的最快方式——也是后续所有定制操作的地图。

实战 2
写第一个工具插件

直接使用上文 defineTool 的完整模板。三个工程要点:

  • 长任务用 run_in_background 模式:经 ctx.jobs.start() 发布为后台任务,模型用 job_* 工具收集或停止它,而不是阻塞一个 step
  • exec.signal 传给所有异步操作,让取消能够传导到 I/O 层
  • 参考实现看 packages/shell/tool-bash——官方称它是「生产级三包示例」,参数 schema、输出规范、后台执行一应俱全
实战 3
组合一个带审批的权限门禁
ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => { const risk = classify(exec.name, exec.arguments) if (risk === 'safe') return next() if (risk === 'dangerous') { return { kind: 'deny', reason: 'Blocked by org policy.' } } return { kind: 'ask', reason: 'This command writes outside the repo.' } // ask → ctx.approval → 用户回答回流为决策 })

组织级不变量(如「生产目录只读」)再加一道 ctx.tools.guard() 单调终局拒绝,任何插件都无法翻转。能力级拒绝放 pre-execute,进程级隔离交给 ctx.sandbox 后端(官方 dsh-bash-sandbox 用 landlock / sandbox-exec 实现)。

实战 4
用补丁替换任意一行配置

补丁机制是「没有特权内核」的兑现方式:按 id 定位 dump-config 输出中的任意一行,整行替换其配置,或插入全新行。典型用法:

  • 换掉默认的持久化插件,指向自己的存储
  • ctx.llm 换一个自建模型网关的适配器
  • dsh-base 之上插入公司内部的审批策略插件行

覆盖顺序是:bundle(按 profile 声明序)→ profile 级 cordis.patch.yml → home 级 → 命令行 --patch。所有配置字段的权威说明在仓库的 config catalog 文档里。

实战 5
接入 MCP 与自定义模型

MCP 在 dsh 里的实现方式出乎意料地朴素:每个 MCP server 一个插件——发现工具,然后逐个 ctx.tools.register()。MCP 工具以原始 JSON-Schema ToolDefinition 注册(这就是它们需要自管输入校验的原因),与第一方 defineTool 工具在同一注册表中平起平坐。

接自定义模型则走 ctx.llm 接缝:写一个 LlmAdapter 子类,经 registerAdapter 注册(官方 dsh-llm-deepseekdsh-llm-pi-ai 就是这么做的)。循环、工具、UI 全部不用动。

调试与可观测

遥测走同一条会话事件流:session/event 落 JSONL;回放 = sessions.create(id, { seed })。因为原始 chunk 全量在日志里,回放是逐比特保真的——UI 长什么样、模型看到了什么,事后都能完整复现。想分叉一个会话做对照实验,ctx.sessions.fork(source, boundary?) 按边界复制。

能力接缝:一次换动,全局生效

「一切皆插件」的深层价值,体现在 dsh 文档所谓的能力接缝(capability seam)上。一条接缝由三种角色构成,缺一不可:

Service Definition
声明接口的服务定义——接缝的契约面
Service Provider
实现接口的服务提供者——可替换的实现面
Consumer
使用能力的消费者,通常是模型可见的工具——消费面

一个包可以同时扮演多种角色,但只有一种角色不构成接缝;新增一个能力,意味着三种角色都要设计。接缝的威力在于一次换动,全局生效:文件系统与子进程提供者共享同一个执行世界,把二者指向一个远程沙箱后端,Bash、PTY、LSP 就整体迁移过去了——没有任何一个工具需要 fork 出「远程版」。子 Agent 的提供者谱系更宽:从全新子进程 Agent 到委托给另一个产品的一个回合,藏在同一个接口后面。

回看那张「产品特性 → 插件机制」映射表,每个特性都是某个扩展点上的监听器,没有一行直接改循环:Hook 系统桥接 Claude Code / Codex 的配置文件(dsh-hooks-claude-code / dsh-hooks-codex),上下文压缩走 ctx.compaction 接缝(自动压缩在 serial 的 agent/pre-step 上由压力触发),系统提示词用 ctx.systemPrompt.section() 分段落装配,定时任务在计时器触发时以 {source: {kind: 'cron'}} 的身份 followup()——微内核的主张由此变成可审计的清单,而不是架构图上的口号。

总结:什么时候该认真看 dsh

把 dsh 的插件设计压缩成三个可迁移的洞见:

  • 可逆性先于功能性。「注册即效应」让加载与卸载对称,热重载、A/B 换实现、故障回滚全部免费。任何插件系统在设计第一天的第一问应该是:这个操作怎么撤销?
  • 事件分发模式就是协作语法。emit / waterfall / parallel / serial 四种模式各对应一类协作,类型化决策让「谁拥有决定权」显式化——这比任何权限文档都可靠。
  • 持久化的事实只有一个来源。「模型可见 = 已记录」把上下文、遥测、重放、分叉统一到一条追加式事件流上,消灭了一整类「内存态与日志不一致」的 bug。
dsh 适合你的判断清单
  • 需要把 Agent 嵌进自己的产品,且要控制模型接入、工具集、权限策略的每一层
  • 团队要给同一个 Agent 叠加多个来源的策略(安全、审计、合规),且策略必须可组合可回滚
  • 对会话的可回放性、可分叉性有硬需求(评测、对照实验、事后审计)
  • 能接受 developer preview 阶段的破坏性变更,愿意跟 master 演进

如果只是想要一个开箱即用的编码助手,dsh 的极致可拆卸反而是负担;但如果你在构建自己的 Agent 平台,dsh 是目前把「插件化」推进到哲学高度、又用类型系统和不变量把它约束成工程实践的最佳样本——毕竟,能被配置文件整体替换的 Agent 循环,市面上独此一家。

参考资料

  1. deepseek-ai · DeepSeek Harness 仓库(README、开发者预览声明、运行方式) github.com/deepseek-ai/deepseek-harness
  2. deepseek-harness · docs/architecture.md(Cordis、Profiles 与 Bundles、核心包、事件、Turn Flow、接缝) docs/architecture.md
  3. deepseek-harness · docs/cookbook/extension-cookbook.md(工具/Hook/UI/协议驱动插件模式与特性映射表) extension-cookbook.md
  4. deepseek-harness · docs/cookbook/adding-a-tool.md(工具契约五条铁律与执行身份保护) adding-a-tool.md
  5. cordiverse · Cordis 与《A Programming Paradigm for Spatiotemporal Composability》论文 github.com/cordiverse/paper
  6. deepseek-harness · docs/cordis-primer.md(Cordis 入门指南) docs/cordis-primer.md