> ## Content Index
> Fetch the complete content index at: https://lucent.blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# DeepSeek Harness Cordis：为动态 Agent 而生的组件模型
- URL: https://lucent.blog/deepseek-harness-cordis/
- Published: 2026-08-23T16:40:24.000Z
- Updated: 2026-08-23T16:58:47.000Z
- Author: Lucent
- Tags: AI

最近在看 DeepSeek Harness 的时候，我顺着它的底层框架 Cordis 找到了一篇论文：《A Programming Paradigm for Spatiotemporal Composability》。

这个标题第一次看有点劝退。"时空可组合性"听起来很像编程语言理论里的东西，论文也确实花了不少篇幅做形式化定义和演算。

但如果先把这些公式放一边，它研究的问题其实很工程：

**一个正在运行的软件系统，怎么安全地安装、卸载和替换组件？**

这件事在传统插件系统里已经做了很多年。但到了 Agent 系统，问题突然严重起来。

一个 Agent 运行时里可能同时存在 LLM Provider、Tool、MCP、Memory、Sandbox、RAG、Skills、事件监听器、Prompt 扩展。以后甚至可能出现 Agent 自己安装 Skill、替换工具、调整运行组件的情况。

这时候，"能加载插件"远远不够。

真正麻烦的是：**插件能不能卸干净，以及插件之间的依赖发生变化后，系统还能不能保持正确状态。**

这正是 Cordis 这篇论文研究的东西。

论文把问题拆成了两个方向：Temporal Composability 和 Spatial Composability，也就是时间可组合性和空间可组合性。作者进一步把传统的 Effect 和 Coeffect 概念变成运行时机制，再用一个统一的 Context 把它们组织起来。

先从最容易踩坑的那个说起。

## 插件真正难的地方不是加载，而是卸载

假设我们给一个 Agent 动态安装了 PDF 处理插件。

插件启动的时候做了这些事：

```text
PDF Plugin
│
├── 注册 pdf_parse Tool
├── 注册 pdf_summary Tool
├── 监听 message/received
├── 创建临时目录
├── 启动文件 watcher
└── 注册 PDF Service

```

加载当然没什么难度。

麻烦出现在卸载的时候。

如果只是：

```ts
plugins.delete("pdf")

```

那基本什么都没解决。

之前注册到 Tool Registry 里的工具还在，事件监听器还挂着，watcher 继续工作，临时资源没有释放，其他组件手里甚至可能还拿着旧的 PDF Service 引用。

插件没了，但它留下的痕迹还在。

这类问题在大型系统里非常烦，因为错误通常不会立即出现。可能过几个小时，你才发现某个 listener 被重复注册了三遍；或者 HMR 十几次以后，一个请求会被处理十几次。

Cordis 给这个问题起了一个名字：

**Temporal Composability，时间可组合性。**

它要求组件被移除以后，它对运行环境造成的副作用能够被完整撤销。论文为此提出了 Revertible Effects，也就是"可逆 Effect"。运行时不只记录一个组件做了什么，同时还跟踪对应的逆操作。

在 Cordis 里，一个典型写法是：

```ts
ctx.effect(() => {
  const timer = setInterval(() => {
    // do something
  }, 1000)

  return () => {
    clearInterval(timer)
  }
})

```

前面的函数建立副作用，返回的 disposer 负责撤销。

插件被卸载时，不需要插件作者重新手动遍历自己注册过的东西，Cordis 会沿着生命周期把这些 effect 清掉。

实际开发时甚至不需要所有东西都手写 `ctx.effect()`。

像：

```ts
ctx.on(...)
ctx.plugin(...)
Service 注册
Tool 注册

```

Cordis 和 DeepSeek Harness 已经把这些操作本身设计成 effect。插件卸载时，listener 会删除，子插件会递归卸载，Service 会解绑，Tool 也会从注册表里消失。对于 Cordis 不认识的 timer、连接、watcher 等资源，开发者才需要自己提供 disposer。

这有点像给插件生命周期加了一层事务。

只是这里的"事务"不是数据库事务，而是运行时资源的事务。

组件：

```text
安装
 ↓
产生 effect A
产生 effect B
产生 effect C

```

卸载时：

```text
撤销 C
撤销 B
撤销 A
 ↓
回到组件安装前的状态

```

我觉得这是整篇论文里最容易理解、也最实用的一部分。

## 只解决卸载还不够

再来看另一个问题。

假设 PDF 插件依赖 OCR：

```text
PDF Plugin
    │
    └── OCR Service

```

传统的依赖注入系统通常在启动阶段把 OCR 传进去：

```ts
const pdf = new PdfPlugin(ocr)

```

程序正常启动以后，大家基本默认：

```text
ocr 永远在那里

```

可动态插件系统不能这么假设。

运行过程中，OCR 插件可能被禁用。

也可能：

```text
OCR V1
  ↓
OCR V2

```

被另一个实现替换。

如果 PDF 插件还握着原来的 OCR V1 对象，就进入了一种很尴尬的状态：

```text
系统认为 OCR V1 已经不存在

但 PDF Plugin 还在调用 OCR V1

```

这种 bug 很难查。

Cordis 把这一类问题归到 Spatial Composability，也就是空间可组合性。

解决办法来自另一个概念：

**Reactive Coeffects。**

如果说 Effect 表达的是：

```text
这个组件会对环境产生什么影响

```

那么 Coeffect 更接近：

```text
这个组件运行需要什么环境

```

于是 PDF Plugin 不需要自己寻找 OCR，而是声明：

```ts
export const inject = ['ocr']

```

意思很简单：

> 没有 OCR，我就不应该运行。

Cordis 会让这个插件保持 `PENDING`，直到 OCR Service 出现。

```text
OCR 不存在

PDF Plugin
    ↓
 PENDING

```

OCR 加载完成：

```text
OCR Service
    ↓
PDF Plugin
    ↓
 ACTIVE

```

比较有意思的是，`inject` 不是启动时检查一次就结束。

如果运行过程中 OCR 消失，依赖 OCR 的插件也会被卸载；OCR 恢复以后，它们再重新启动。DeepSeek Harness 的 Cordis 文档明确说明，服务依赖会在运行过程中持续跟踪，而不是一次性的启动顺序检查。

于是替换实现也变得自然：

```text
            OCR V1
              │
              ▼
          PDF Plugin

```

卸载 V1：

```text
OCR V1 removed
      ↓
PDF Plugin unload
      ↓
撤销自己的 effects

```

然后加载 V2：

```text
OCR V2 available
      ↓
PDF Plugin reload
      ↓
重新绑定当前 OCR Service

```

消费方不需要知道究竟是谁提供 OCR。

这就比我们常见的 DI 多了一层东西：

**依赖关系本身也是动态的。**

## Effect 和 Coeffect 最终都落到了 Context

到这里，论文真正想构建的模型已经比较清楚了。

一个组件身上同时存在两个方向：

```text
                 Component
                    │
          ┌─────────┴─────────┐
          │                   │
       Coeffect             Effect

       我需要什么           我改变什么
          │                   │
          ▼                   ▼
       Context             Context

```

组件从 Context 获取它需要的能力，也通过 Context 对系统产生影响。

论文做了一步更理论化的处理：把 Effect Context 和 Coeffect Context 统一成一个 Context Type，并在这个基础上定义 Component 和动态组合演算。

这部分是论文真正偏编程语言理论的地方。

作者关心的已经不是：

```text
某一个 plugin 能不能正确 unload

```

而是：

```text
Plugin A
Plugin B
Plugin C
Plugin D
...

```

不断交错加载、卸载、替换以后，整个系统是不是仍然保持那些性质。

论文通过动态组合演算和对应的元理论，把单个组件上的时空可组合性推到一组互相交错的组件系统上。

工程开发不一定需要读懂里面每一个证明，但它解释了 Cordis 为什么要把很多看起来"框架帮你做一下就行"的事情设计得这么严格。

因为 HMR、依赖替换、插件树、资源释放，其实都建立在同一套生命周期语义上。

## Fiber：Cordis 怎么管理一个活着的插件

到了 Cordis 的工程实现里，每次插件挂载都会对应一个 Fiber。

Fiber 可以理解成：

```text
某个插件实例在运行时的生命周期句柄

```

它大概会经历：

```text
PENDING
   ↓
LOADING
   ↓
ACTIVE
   ↓
UNLOADING
   ↓
DISPOSED

```

如果依赖不满足，就留在 `PENDING`。

如果 `apply` 执行失败，则进入 `FAILED`。

这种状态机有一个很实用的好处：**挂载插件和启动插件不再是同一个动作。**

例如配置里写：

```yaml
- name: './consumer.ts'
- name: './ocr.ts'

```

即便 consumer 写在 OCR 前面也没关系。

Consumer 声明：

```ts
export const inject = ['ocr']

```

它就会等。

什么时候启动不是 YAML 顺序决定，而是依赖关系决定。

这其实解决了插件系统里一个很古老的问题：启动顺序。

很多项目最后都会出现这种东西：

```text
先初始化 database
再初始化 redis
然后 event bus
然后 tool registry
然后 llm
最后 agent

```

系统稍微复杂一点，就开始出现几十行初始化顺序。

Cordis 换了一种思路：

```text
别告诉 runtime 谁先谁后。

告诉它：
我依赖谁。

```

剩下的让依赖图决定。

## 这样一来，HMR 也变得顺理成章

普通 HMR 最麻烦的地方就是旧模块留下的状态。

新代码加载进来了，但：

```text
旧事件还在
旧 timer 还在
旧对象还在

```

于是 HMR 次数越多，程序状态越奇怪。

Cordis 的处理方式很直接：

```text
发现 hello.ts 修改

        ↓

卸载 Plugin V1

        ↓

回卷 V1 所有 effects

        ↓

加载 Plugin V2

        ↓

重新执行 apply()

```

DeepSeek Harness 的 Cordis 教程就是这么描述 HMR 的：旧实例先卸载，effect 被回卷，再加载新的插件代码；配置文件变化时，Loader 还可以根据稳定 ID 只重新挂载发生变化的部分。

换句话说，HMR 并不是一套孤立功能。

它只是：

```text
可逆生命周期 + 重新组合

```

产生的自然结果。

这一点我挺喜欢。

## 为什么这套东西放到 Agent 上突然变得很合适

如果只是普通 Web 后端，大部分组件其实不需要频繁动态变化。

数据库连接启动以后一般不会突然换实现，Spring Bean 运行到一半也很少自己重新组合。

Agent 不一样。

今天一个完整 Agent Runtime 里已经可能出现：

```text
Agent
│
├── LLM Provider
├── Tool Registry
├── MCP
├── Skills
├── Memory
├── RAG
├── Sandbox
├── System Prompt
├── Session
└── Agent Loop

```

这些东西中的很多都天然适合动态安装和替换。

用户启用一个 Skill：

```text
增加新的 Tool

```

连接一个 MCP Server：

```text
增加一批能力

```

切换 Sandbox：

```text
替换 shell Service

```

换模型：

```text
替换 LLM Provider

```

甚至 Agent 自己以后都可能修改自身组件。

这时候 Agent Runtime 已经不像传统应用：

```text
启动
  ↓
运行
  ↓
退出

```

而更像一个长期存在、不断改变内部结构的系统。

所以我觉得 Cordis 和 Agent 的结合并不是巧合。

DeepSeek Harness 本身就采用了 "everything is a plugin" 的设计。官方 README 直接写明，它由 Cordis 驱动，而 Cordis 的设计正来自这篇时空可组合性的论文。

甚至一个 Tool 也是普通插件。

DeepSeek 官方教程里的例子就是：

```ts
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(
    defineTool({
      name: 'greet',
      // ...
    })
  )
}

```

这个插件依赖 `tools` Service。

加载以后 Tool 出现在 Agent 的工具注册表里。

插件卸载，Tool 自动注销。

这种模型扩展起来很舒服，因为 Tool、LLM、Shell、Memory 不需要被框架写死成几个特殊模块。

它们都只是 Component。

## 可逆也不是魔法

这里有个边界需要说清楚。

"Revertible Effect" 很容易让人产生一种错觉：

```text
插件干了什么
框架都能自动 rollback

```

实际上没这么神。

比如：

```text
发送了一封邮件
执行 DROP TABLE
调用支付接口
向远程服务器写入文件

```

这些事情发生以后，运行时不可能凭空把现实世界倒放。

Cordis 真正能保证的是它管理范围内的 Context 和生命周期副作用能够撤销。

对于框架不知道的资源，开发者仍然要提供 disposer：

```ts
ctx.effect(() => {
  const resource = createResource()

  return async () => {
    await resource.close()
  }
})

```

Cordis 文档也明确要求，框架没有管理的资源应该包装进 `ctx.effect()`，并提供释放逻辑。

所以这套范式没有消灭资源管理。

它做的是把：

```text
"记得 destroy"

```

从一种开发约定，提升成运行时生命周期的一部分。

差别挺大。

## 我觉得这篇论文真正值得看的地方

读完以后，我对它印象最深的其实不是那些形式化定义。

而是它重新定义了"插件"。

我们以前理解插件，通常是：

```text
一段可以动态加载的代码

```

Cordis 的 Component 更接近：

```text
一个声明自己需要什么，
同时把自己产生的变化交给 runtime 管理的运行单元。

```

一旦这么定义，很多过去需要分别解决的问题就串起来了。

插件卸载依靠 Effect 回卷。

依赖管理依靠 Coeffect。

依赖消失会触发生命周期变化。

生命周期可靠，HMR 才能可靠。

HMR 可靠以后，运行时动态重新组合才真正可用。

最后才轮到 Agent 自己动态调整系统组成这种更激进的场景。

所以如果让我用一张图概括 Cordis，我可能会画成这样：

```text
                  Context
                     │
        ┌────────────┴────────────┐
        │                         │
     Components                Services
        │                         │
        │ inject                  │ provide
        └──────────┐     ┌────────┘
                   ▼     ▼
                Dependency
                   Graph
                     │
              Context 发生变化
                     │
                     ▼
              重新计算生命周期
                     │
         ┌───────────┴───────────┐
         ▼                       ▼
      unload                   reload
         │                       │
     revert effects          apply effects

```

这套设计未必适合所有软件。

如果你的应用启动以后组件基本固定，那传统 DI、模块系统和明确的初始化/销毁流程已经够用了，引入 Cordis 反而会增加理解成本。

但如果系统本身就是动态的，尤其是 Agent Harness、插件平台、IDE、机器人框架这类长期运行又需要不断增删能力的软件，我觉得这篇论文提出的问题很准确。

现在很多 Agent 框架都在讨论怎么让 Agent 获得更多 Tool、更多 Skill、更多 MCP。

Cordis 往前多问了一句：

**这些能力加进来以后，将来还能不能干干净净地拿出去？**

等 Agent 真开始修改自己的运行时，这个问题恐怕会比"怎么加载一个 Tool"难得多。