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

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

上一篇我们写插件时,始终绕不开一个对象------Contextctx)。你 ctx.providectx.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.sessionsctx.toolsctx.agentLoopctx.llm 这些核心能力,本质上都是挂在 ctx 上的服务。你看到的每一个「上下文键」,都是 provide 进去的。

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

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

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

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

Cordis 的做法是:ctx.onctx.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/requestagent/pre-stepassistant/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 那个「自带黑匣子」的会话日志------resumeforkreplay 到底怎么用,又为什么说「模型可见即已记录」。


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

相关推荐
龙亘川37 分钟前
智赋能文旅领域治理|一网统管能做什么?文化服务管理系统四大场景拆解
大数据·人工智能
指针常量38 分钟前
【RL相关笔记】RL 效率综述
人工智能·笔记·大语言模型·后训练
番茄不是西红柿kk40 分钟前
OpenCode Go 与 Command Code 性价比深度分析(2026.09.04 更新)
人工智能·opencode·commondcode
Carl_奕然1 小时前
【智能体】Agent的四种设计模式之:React(2026最新版)
javascript·人工智能·python·react.js·设计模式·语言模型
Thomas.Sir1 小时前
第24课:TensorFlow|图像分类实战训练【手写数字、日常图像分类完整项目】
人工智能·分类·tensorflow
岁月宁静1 小时前
四、《从零手撸 Agent》 — 流式输出:接住 AI “一个字一个字” 想出来的过程
python·agent
Herlie1 小时前
2026,当AI不再是“玩具”:01Agent如何终结内容创作的“碎片化时代”?
人工智能
2601_967659881 小时前
AI重构流量入口,本地GEO服务正在成为一门新生意
人工智能