本文源自学习开源的 Deepseek Harness 的一个过程,将所有 AI 的概念都剔除后,看看 Deepseek 这个开源的架构中最核心的是什么。
现在可以利用 AI 第一时间深入分析学习,然后进一步去进行实践,这可能是 AI 带给我最大的好处。
Demo 版本的代码在:https://github.com/OmniTexts/cordis-runtime-demo
8 月 13 日,Deepseek 发布了重大更新:DeepSeek-V4-Pro。
与此同时,已经预告很久的 Deepseek 自己的 Agent 产品也同期发布了,这就是 Deepseek Harness (下文简称 DSH)。
而它的理念是:一切皆插件。
但真正打开 DSH 的代码后,会发现它有些不一样。
这个差异并不只是“它也支持插件”。OpenClaw、Claude Code、Codex 和 Pi 同样存在各自的扩展机制。
更值得注意的是,DSH 把插件系统放在了更靠近地基的位置:连 Agent Loop 都是插件,模型能力则是 Service 插件,Prompt、Tools、Skills 可以按配置组合,WebUI 也只是其中一种接入方式。
支撑这一切的不是某个写死的 Agent 主循环,而是一个独立的 TypeScript 运行时:Cordis。
本文先不讨论模型效果,也不分析可能的复杂的系统提示词。我们从一个只会说“Hello”的时钟服务开始,沿着这个最小例子,拆开 DSH 最底层的运行逻辑。
先把 LLM 从 DSH 里拿掉
一个最直观的 Agent 架构通常是:
WebUI -> Agent Loop -> LLM -> Tools在这种结构中,UI、会话、模型调用和工具执行很容易逐渐耦合到同一套应用代码里。更换 WebUI、增加 TUI,或者同时运行多套 Agent 配置时,往往需要重新处理初始化顺序、依赖检查和资源清理。
Cordis 选择了另一种边界:它不负责定义 Agent 应该怎样思考,而是负责定义组件应该怎样运行。
用一句话概括它的运行模型:
所有可运行能力都通过 Plugin 进入系统;每次安装形成一个 Fiber;对外能力通过 Service 发布;临时资源由 Effect 托管;所有解析和生命周期都发生在 Context 中。
这也是本文所说的“一切皆插件”。
它指的是 Agent Loop、模型接入、工具系统、提示词系统、UI 适配和基础设施,都是作为可安装组件进入同一套运行时。
下面,我们在讨论 DSH 时,可以抛开所有 AI 概念,先讲讲它的核心:Cordis。

先建立一张概念地图
Cordis 最重要的几个概念可以先这样理解:
它们之间的关系是:

接下来逐个展开。
Context:组件运行在哪里
Context 不是聊天上下文,也不是 Prompt Context。它更接近一个带作用域的运行时视图。
插件通过 ctx 完成大部分操作:
ctx.plugin(SomePlugin) // 载入一个插件ctx.get('someService') // 获取 service 实例ctx.on('some/event', listener) // 监听命名的事件ctx.emit('some/event', payload) // 通过事件发送消息ctx.effect(register)同一个根 Context 可以派生出不同子 Context。子 Context 可以继承父级能力,也可以通过 isolate 为某些 Service 建立不同的解析空间。
所以 Context 解决的不是“代码能不能调用某个对象”这么简单,而是:当前插件应该看到哪个 Service 实例,它注册的资源属于谁,它处于哪一棵生命周期树中。
Plugin:可安装的功能定义
Plugin 是 Cordis 的基本安装单元。它可以是函数、对象或类。
constGreeterPlugin = {name: 'demo-greeter',inject: ['demoClock'],apply(ctx) {// 注册功能 },}Plugin 也不等于 npm 包。npm 包是代码的发布方式;一个 npm 包可以导出多个 Plugin,本地文件同样可以导出 Plugin。
Plugin 主要回答两个问题:
1. 我依赖哪些能力? 2. 当依赖满足时,我要安装什么,并在退出时清理什么?
Fiber:Plugin 的运行时实例
调用:
const fiber = ctx.plugin(GreeterPlugin)返回的不是 GreeterPlugin 本身,而是这一次安装对应的 Fiber。
如果把 Plugin 类比成类或程序定义,Fiber 就更像一次实例化或一次受监督的运行。它记录:
• 当前 Context 和配置 • Plugin 的依赖 • PENDING、LOADING、ACTIVE、UNLOADING等状态• 当前 Fiber 拥有的 Effect • 在它下面创建的子 Fiber • 更新和卸载过程
同一个 Plugin 可以在不同 Context、不同配置下安装多次,因此可以产生多个 Fiber。
Fiber 也不是线程或协程。它并不提供 CPU 调度,而是提供组件级的状态机、依赖管理和生命周期所有权。
Service:由 Fiber 发布的能力
普通 Plugin 可以执行逻辑、注册事件或创建连接,但不会自动出现在 Context 上。
Service 则会发布一个有名字的能力。例如:
classDemoClockServiceextendsService {constructor(ctx: Context) {super(ctx, 'demoClock') }greet(who: string) {return`Hello, ${who}` }}安装以后,其他插件可以声明:
inject: ['demoClock']然后在依赖可用时调用:
ctx.demoClock.greet('Jack')Plugin 和 Service 都使用 ctx.plugin() 安装,是因为 Service 本身也需要 Fiber 管理。区别在于,Service 的 Fiber 还会向 Context 注册一个可以被其他 Fiber 注入的能力。
普通 Plugin:拥有生命周期,但不一定对外提供 APIService Plugin:拥有生命周期,并向 Context 发布命名 APIService 不一定无状态。数据库连接池、缓存、模型客户端和 Session Store 都天然带有状态。真正需要避免的是把某个 Agent 的临时状态裸露在共享 Service 字段中。共享状态应该封装在 Service 内部,Agent 私有状态则应按 agentId、sessionId 或 Scope 分区。
Effect:把资源清理变成运行时保证
插件启动后经常会创建一些需要善后的资源:
• Event Listener • timer • WebSocket 或数据库连接 • 文件监听器 • 子进程 • 临时任务
这些资源如果只依靠开发者手工清理,很容易在插件重载或依赖失效后残留。
Cordis 用 Effect 表达“获得资源,同时登记清理方法”:
ctx.effect(() => {const timer = setInterval(tick, 60_000)return() => {clearInterval(timer) }})Effect 属于当前 Fiber。当 Fiber 卸载、失败或因为依赖消失而停止时,Cordis 会执行 disposer。
Effect 因此不是“插件产生了一个文件”这样的业务副作用,而是一个明确的资源所有权约定:谁创建,谁登记;Fiber 退出,资源释放。
Typed Event:解耦调用者和处理者
Cordis 的事件在运行时仍然是普通事件总线,但可以通过 TypeScript module augmentation 约束事件名和参数:
declaremodule'@deepseek-ai/cordis' {interfaceEvents {'demo/greet'(who: string): void'demo/complete'(): void }}这段声明只提供编译期类型,不会在运行时创建事件。
事件适合通知、广播和来源解耦。例如 Scenario 不必知道谁实现了 greeting,只需要发送:
ctx.emit('demo/greet', 'core application')但 Event 不应该替代所有 Service 调用。如果调用方需要明确的返回值、错误语义或唯一处理者,直接注入 Service 通常更清晰。
一个最小例子:Service、Plugin 和 Fiber 如何协作
针对这些基本概念,我们构建了一个不调用 LLM、也不需要 API Key 的最小示例。它只有三个主要角色。
DemoClockService 提供真正的能力:
demoClock.greet(who)GreeterPlugin 是一个组合器:
依赖 demoClock 这个 Service监听 demo/greet 事件收到事件后调用 demoClock.greet()注册 heartbeat Effect卸载时清理 listener 和 timerScenarioPlugin 则负责演示运行过程:
先安装 GreeterPlugin等待一段时间安装 DemoClockService发送 demo/greet卸载 DemoClockService再次安装新一代 DemoClockService关键生命周期如下:

这里的 delay(providerDelayMs) 没有业务意义,只是为了让使用者在终端中观察到 PENDING -> ACTIVE 的变化。
当第一代 DemoClockService 被卸载时,更能体现 Cordis 的价值:
demoClock 消失 | vGreeter 依赖失效 | vGreeter ACTIVE -> UNLOADING -> PENDING | +-- 移除 demo/greet listener `-- 清理 heartbeat timer安装第二代 DemoClockService 后,Greeter 又会重新进入 ACTIVE 并重新注册所需资源。
这让插件内部不必到处编写:
const service = ctx.get('demoClock')if (!service) return依赖可用性不再是每个调用点的临时判断,而是 Fiber 能否运行的前置条件。
cordis.yml:用配置组装插件树
同一套组件还可以通过 YAML 组装:
-id:demo-runtimename:cordis:groupgroup:trueisolate:demoClock:trueconfig:-id:lifecycle-observername:'./src/lifecycle-observer.ts'-id:scenarioname:'./src/scenario.ts'这里的 cordis:group 是 Loader 注册的 builtin 名称。group: true 表示它是一个包含子条目的配置节点。
isolate.demoClock: true 则为 demoClock 创建一个组内私有的 Service realm:组内的 Provider 和 Consumer 解析到同一个 Service 标识,组外 Context 解析到另一个标识。
需要特别注意:
isolate 不会创建 Serviceisolate 不会自动 inject Serviceisolate 也不会自动隔离同名 Event它只决定名为 demoClock 的 Service 应该在哪个解析空间中注册和查找。
从 Cordis 上升到 DSH:同一套插件如何成为 Agent preset
第二个示例复用了完全相同的 DemoClockService 和 GreeterPlugin,但不再由 Scenario 动态安装,而是在 agent.cordis.yml 中声明:
-id:demo-runtimename:cordis:groupgroup:trueisolate:demoClock:trueconfig:-id:clock-servicename:'../../src/clock-service.ts'-id:greeter-pluginname:'../../src/greeter-plugin.ts'负责加载它们的是 AgentPresets。AgentPresets 自己也是一个通过 Cordis 安装的 Service Plugin:
await ctx.plugin(AgentPresets, config).await()安装完成后,宿主通过 ctx.agentPresets 使用 preset 发现、挂载和重组能力。
这一点说明了插件复用的关键:ClockService 和 GreeterPlugin 只依赖 Cordis 标准 API,并不知道自己运行在普通应用、Agent preset、WebUI 还是 TUI 中。变化的是上层组合方式,不是底层组件实现。
Agent Scope:Agent 的运行时坐标
DSH 在 Cordis 之上又增加了 Scope。
Agent Scope 不是完整 Agent,它更接近一个 Agent 的运行时命名空间、可见性边界和生命周期边界。完整 Agent 通常还包括 Session、Driver、LLM Loop 和当前 Run 状态。
示例中创建了两个 Scope:
const firstAgent = createScope(ctx, { agent: 'alpha' })const secondAgent = createScope(ctx, { agent: 'beta' })然后将它们挂到同一个 demo preset:
await ctx.agentPresets.mount(firstAgent.ctx)await ctx.agentPresets.mount(secondAgent.ctx, 'demo')这里并不是把 Service 分别绑定到两个 Context,而是把两个 Agent Scope 的父级都关联到同一个 preset standing scope:
demo preset standing scope├── alpha Agent Scope└── beta Agent Scope同一个 preset 的插件树只安装一次。alpha 和 beta 通过 Scope 链看到 preset 注册的 Tools、Prompt Sections、Skills 和监听器,同时仍然可以拥有自己的局部注册项。
alpha 的能力视图 = Global + demo preset + alpha 私有层beta 的能力视图 = Global + demo preset + beta 私有层alpha 看不到 beta 的私有层,beta 也看不到 alpha 的私有层。
这里还要区分两种机制:
Cordis isolate:管理 Service 名称和实例的解析空间DSH Scope:管理注册项可见性和 scoped event 路由当前 demo 的 demo/greet 只是普通 Cordis typed event,并没有使用 DSH 的 scope carrier,因此不会自动按 alpha、beta 隔离。真正需要按 Agent 路由的事件,必须显式接入 DSH 的 scoped event 机制。Scope 提供了路由基础,但不会把任意事件自动改造成 Agent 私有事件。
示例中的 demoClock 位于 preset 内部的 isolate realm,因此 Agent Context 不能简单通过 agentCtx.get('demoClock') 找到它。AgentPresets.serviceFor() 会先确定 Agent 加入了哪个 standing preset,再从该 preset 的 Fiber 子树中寻找对应 Service。
同一个 preset 下的多个 Agent 得到的可能是不同的 traceable facade,但底层仍是同一个 standing Service 实例。这也再次提醒我们:Agent 私有状态不能随意放在共享 Service 的普通实例字段中。
这种机制带来了什么好处
第一,生命周期被统一管理。
Plugin、Service、事件监听器、timer 和连接都归属于 Fiber。组件退出时,不再依赖各模块自行约定清理顺序。
第二,依赖关系变成运行时状态机。
Service 不可用时,依赖插件进入 PENDING;Service 恢复后,插件自动重新激活。这比在每个调用点处理空值更系统。
第三,能力和接入方式可以分离。
一个模型 Service 可以同时被 Web API Plugin、TUI Plugin、Agent Loop Plugin 和定时任务 Plugin 使用。更换 UI 不需要重写模型、会话和工具能力。
第四,同一组件可以被不同方式组合。
它既可以由 TypeScript 代码动态安装,也可以由 cordis.yml、agent.cordis.yml 或 preset 组装。组件只关心依赖契约,不关心宿主如何发现它。
第五,适合构建多 Agent 平台。
Agent Scope、standing preset 和 scoped registry 可以让多个 Agent 共享一套能力模板,同时保留各自的覆盖层和事件边界。
第六,故障恢复和热重组更自然。
模型客户端、工具服务或配置发生替换时,依赖它们的 Fiber 可以按依赖图停用和恢复,而不是重启整个进程。
它的代价同样明显
首先是学习成本。Context、Fiber、Effect、Service、isolate 和 Scope 都涉及“边界”,但管理的又不是同一种边界。没有清晰的概念模型时,很容易把 Service 隔离和事件 Scope 混为一谈。
其次是调用链更间接。一次行为可能经过:
Event -> Plugin -> Service -> Effect排查问题时,需要同时观察事件监听器、Fiber 状态、Service realm 和 Scope 链。
第三,配置驱动会把一部分错误推迟到运行时。YAML 中的模块路径、依赖组合和 isolate 配置通常要等 Loader 真正执行后才能验证。
第四,PENDING 既是能力,也是风险。它允许 Consumer 先于 Provider 安装,但错误的依赖声明也可能让插件永久等待。如果负责创建 Service 的 Plugin 反过来静态 inject 这个 Service,还可能形成启动死锁。
第五,共享 Service 带来状态管理压力。standing preset 只安装一次,意味着同一 preset 下的多个 Agent 可能共享 Service 实例。Service 必须明确哪些状态是全局的,哪些需要按 Agent 或 Session 分区。
第六,Event 很容易被滥用。对于需要返回结果、错误处理和唯一调用目标的操作,直接注入 Service 通常比广播事件更容易理解。
最后,对于只有一个 Agent Loop、几个工具和固定 UI 的小型应用,这套运行时可能显得过重。它解决的是动态组合、长期演进和多实例隔离问题,而不是让简单函数调用变得更简单。
谁真正需要这样的 Harness
如果应用始终只有一个模型、一个 Agent Loop 和一个入口,自研一套简单 Harness 往往已经足够。
Cordis 和 DSH 更适合另一类问题:
• 同时存在 WebUI、TUI、API 和后台任务 • 不同 Agent 使用不同 Prompt、Tools 和 Skills 组合 • 模型、存储和工具服务需要动态替换 • 插件需要热更新、故障恢复和严格清理 • 子 Agent 需要继承父 Agent 的同一代能力组合 • 希望第三方组件遵循统一接入和生命周期契约
因此,它的竞争对象并不只是“另一家公司提供的 Agent Harness”。更准确地说,它想提供的是 Agent 应用内部的模块运行标准。
其他团队是否愿意采用,取决于这个标准能否带来足够丰富的插件生态、稳定的契约和可接受的调试成本。仅有抽象并不会自动形成平台价值,真正决定它能否被外部使用的,是组件复用能否超过引入框架的复杂度。
结语
回到“一切皆插件”这句话。
Cordis 的价值不在于把所有代码都包装成 Plugin,而在于让所有长期运行的能力服从同一套规则:
Plugin 描述要运行什么Fiber 记录它正在怎样运行Service 描述它向外提供什么Effect 描述退出时必须清理什么Context 决定它在哪里运行和看到什么Scope 决定它代表哪个 Agent、继承哪套能力在这个模型中,Agent Loop 不再是不可替换的应用中心,而只是组合中的一个组件。WebUI 可以换成 TUI,模型可以替换,工具和 Prompt 可以按 preset 重组,而底层 Service 不必随每一种接入方式重新实现。
这就是 DSH 架构最值得关注的地方:它尝试把 Agent 从一段固定流程,变成一套可以被组合、监督、隔离和替换的运行时组件。
夜雨聆风