DeepSeek Harness 底层探秘:Cordis 元框架与「一切皆插件」的实现

DeepSeek Harness 底层探秘:Cordis 元框架与「一切皆插件」的实现

上一篇我们写插件时,始终绕不开一个对象------Context(ctx)。你 ctx.provide、ctx.on,最后返回 disposer,一个插件就成型了。

但为什么这套「拿到 ctx 就开始注册副作用」的写法,能让成千上万个插件安安稳稳地叠在一起、卸载时不打架?答案藏在 DSH 的地基------Cordis 元框架里。这篇我们就钻下去,把它讲透。

一、Cordis 解决的是什么问题

先回到一句话:DSH 的口号是「一切皆插件」,而 Cordis 的论文标题是《A Programming Paradigm for Spatiotemporal Composability》,直译过来是「时空可组合性的编程范式」。

拆开看这两个词:

  • 空间(spatial) :指多个插件在同一时刻、共享上下文 里如何共存------你的工具、我的事件监听、他的服务,都住在同一个 ctx 里,互不冲突。
  • 时间(temporal) :指插件装配和卸载的顺序是确定的------谁先装、谁后装、卸载时按什么顺序撤销,都有章法,不是乱序。

传统插件的两大顽疾,正是这两点没处理好:要么插件之间靠全局单例互相耦合(空间混乱),要么加载/卸载顺序不确定导致内存泄漏和幽灵副作用(时间混乱)。Cordis 用一套极简的模型,同时解决了这两个问题。

二、Context:一切服务的容器

Cordis 的世界里,Context 是唯一的主角。它同时是:

  1. 依赖注入容器 ------provide 往里塞服务,inject 从里取服务;
  2. 事件总线 ------on 订阅事件,emit 触发事件;
  3. 生命周期的所有者------它知道自己装了哪些插件,能在卸载时统一清理。
ts 复制代码
import { Context } from 'cordis'

const root = new Context()

// 塞服务
root.provide('apiBase', 'https://api.example.com')
// 取服务
const base = root.inject('apiBase')   // -> 'https://api.example.com'

// 订阅事件
root.on('ready', () => console.log('ready!'))
// 触发事件
root.emit('ready')

就是这么简单。但「简单」不等于「弱」------DSH 里 ctx.sessions、ctx.tools、ctx.agentLoop、ctx.llm 这些核心能力,本质上都是挂在 ctx 上的服务。你看到的每一个「上下文键」,都是 provide 进去的。

三、副作用必须可逆:disposer 的意义

上一篇我们反复强调「返回 disposer」。这其实是 Cordis 最硬的一条约束:

每一次注册都是一次副作用,而每一次副作用都必须登记一个撤销动作。

为什么这么严格?因为插件要能热插拔 。如果 ctx.on('ready', fn) 返回后没人管,那这个 fn 就会一直挂着------插件 A 卸载了,它注册的监听却还在悄悄执行,这就是「幽灵副作用」。

Cordis 的做法是:ctx.on、ctx.provide 这类操作都会自动登记 到当前 Context 的 disposer 列表里;当这个 Context(以及它派生出的子 Context)被 dispose() 时,所有登记的清理动作会按后进先出的顺序依次执行。

ts 复制代码
const ctx = new Context()
const off = ctx.on('tick', handler)   // 手动持有移除函数
// ...或直接交给 Context 管理,ctx.dispose() 时自动 off

理解这一点,你就理解了为什么 DSH 能「插件卸载时自动撤销一切」------不是靠插件作者自觉,而是框架层面的强制记账。

四、装配顺序与子 Context:隔离的层级

DSH 的插件树是分层的。每装一层插件,其实是在当前 Context 上派生一个子 Context(child context),这层插件的所有副作用都登记在子 Context 上。

好处是显而易见的:

  • 隔离:一个子 Context 的 dispose 只影响它自己这一层,不会波及其他层;
  • 覆盖 :子 Context 里 provide 的同名服务,会遮蔽父 Context 里的同名服务(这就是「上层 patch 覆盖下层」的实现基础);
  • 确定性卸载:整棵树按后进先出的顺序回收,不会留下悬挂引用。

前一篇架构篇提到的「运行中的 dsh 是一棵插件树」,现在你能看清它的物理结构了------就是一棵以 Context 为节点的树。

五、类型化事件:让扩展点变成「接口」

普通事件总线只有字符串事件名,写错了静默失败。Cordis 做了增强------事件也是类型化的。

在 DSH 里,agent/request、agent/pre-step、assistant/message 这些事件,每一个都带着明确的 payload 类型。TypeScript 下,你在 ctx.on('agent/request', (payload) => ...) 里能拿到精确的补全和类型检查------这从根上避免了上一篇踩坑清单里「事件名拼错、静默不触发」的那类 bug。

更妙的是,类型化事件让「扩展点」本身变成了一种可被发现的接口:官方文档能枚举出所有事件名和它们的 payload 结构,开发者照着扩展即可,不用去猜。

六、生命周期:apply 与 prepare

Cordis 的插件除了 apply(上一篇默认导出函数对应的就是 apply 阶段),还有一个 prepare 阶段:

  • prepare :在装配前执行,用于声明这个插件会用到哪些服务、会提供哪些服务------类似「体检」,让框架提前发现依赖缺失。
  • apply:正式装配,执行注册动作,返回 disposer。
ts 复制代码
import { Context } from 'cordis'

export const name = 'my-plugin'

export function prepare(ctx: Context) {
  // 声明依赖:如果缺少这些服务,框架会在装配前就报错
  ctx.require('llm')
  ctx.require('tools')
}

export default function apply(ctx: Context) {
  ctx.tools.register({ /* ... */ })
  return () => { /* 清理 */ }
}

prepare 的价值在于失败前置 :如果你写了个依赖 ctx.agentLoop 的插件,但当前 profile 没装 core/agent-loop,那么它会在 prepare 阶段就清晰报错,而不是等运行到一半才炸一个不明所以的 undefined is not a function。

七、一个最小 Cordis 应用:串起来看

把上面所有概念串成一个 30 行的完整例子,你会对 Cordis 有整体手感:

ts 复制代码
import { Context } from 'cordis'

// 一个插件:提供问候服务,并订阅消息
function greeter(ctx: Context) {
  ctx.provide('greet', (name: string) => `你好,${name}!`)
  ctx.on('user-arrived', (name: string) => {
    console.log(ctx.inject('greet')(name))
  })
  return () => console.log('greeter 卸载')
}

// 另一个插件:触发事件
function doorbell(ctx: Context) {
  ctx.emit('user-arrived', '夏文强')   // -> 你好,夏文强!
}

const root = new Context()
root.plugin(greeter)     // 装配 greeter
root.plugin(doorbell)    // 装配 doorbell,立即触发问候
root.dispose()           // -> greeter 卸载

运行输出:

复制代码
你好,夏文强!
greeter 卸载

注意最后一行------dispose() 时 greeter 登记的 disposer 被自动调用。这就是「副作用可逆」在 30 行内的完整演示。

八、小结

Cordis 之所以能撑起 DSH 的「一切皆插件」,靠的是四个朴素但严格的机制:Context 作为唯一容器、副作用强制可逆、子 Context 分层隔离、类型化事件把扩展点变成接口。它们共同保证了「时空可组合性」------插件在空间上互不干扰,在时间上有序装配、有序卸载。

一句话总结:DSH 的灵活性不是靠「设计得聪明」,而是靠「约束得死」。 正因为每个插件都被逼着变成「可逆的、有边界的、可被发现的」构件,它才敢把 Agent 的组装权完全交给你。

下一篇,我们回到应用层,聊聊 DSH 那个「自带黑匣子」的会话日志------resume、fork、replay 到底怎么用,又为什么说「模型可见即已记录」。


参考:deepseek-ai/deepseek-harness 架构文档,以及 Cordis 论文《A Programming Paradigm for Spatiotemporal Composability》。

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
回眸&啤酒鸭3 天前
【回眸】Minicart 电商购物车核心功能落地指南
人工智能
一隅论数智3 天前
给AI一张“业务概念地图“:本体如何从哲学走向企业智能
大数据·人工智能·经验分享·笔记·学习·学习方法·政务
不悔哥3 天前
开源OV-Watch:怎么做一个智能手表
单片机·开源·嵌入式
AI的探索之旅3 天前
97 个 OpenCV 实例(三十):双目立体,从标定到点云
人工智能·opencv·计算机视觉
AlbertZein3 天前
Step-5-Preview 上手实测:3D 游戏、金融分析、网页设计一次跑完
人工智能·aigc
家和803 天前
Carla仿真系列:4_在 Carla 的 Tesla 上装 4 路摄像头,并创建网格为环视拼接做准备
开源
LaughingZhu3 天前
Product Hunt 每日热榜 | 2026-09-19
人工智能·深度学习·神经网络·搜索引擎·百度
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk