来源:市场资讯
(来源:筱可Datawhale)
今天我们讲点硬核的,带大家看看 DeepSeek Harness的核心架构 Cordis。
它的核心理念是一切皆插件。模型适配器、工具注册表、会话日志、agent loop 本身,每一部分都是插件,都可以在运行时替换。换一个模型提供方不用改源码,换一个沙箱后端不用重启,换 agent loop 本身也不用动框架。
这种"任意环节可替换"的设计,是 DSH 和传统 Agent 框架最大的区别。下面分三层拆开:先看整个项目怎么组装,再看插件系统怎么运转,最后看插件动态能力的三个关键机制。
一、DeepSeek Harness 是怎么组装出来的?
DSH 不是一个装好了就定型的应用。启动时按顺序叠加各层配置,最终在内存里形成一套完整的插件组合开始运行。
底层是 Cordis 微内核。 Cordis 是一个专门管插件的框架,只管一件事:插件什么时候加载、什么时候卸载、彼此怎么协作。它最初由一位叫 Shigma 的开发者为一个聊天机器人框架 Koishi 编写,2022 年独立开源。DeepSeek 把 Shigma 和 Cordis 一起雇了,把整套源码并进了自己的仓库。产品的每一部分都是 Cordis 插件,连驱动 Agent 运转的主循环本身也是。
中间是三层组装机制。 这三层解决的是"这套插件组合怎么搭起来"的问题。
Profile 是一份存在本地的组装方案,列出自己要叠放哪些组合包,也存用户自己写的配置补丁。
Bundle 是组合包,把一组插件和它们的配置打包分发。装一个组合包,就等于一次装好一组配套插件。
Patch 是补丁,按名字定位到某个具体插件,替换它的配置。
三层按顺序叠加:先装各个组合包,再打 profile 的补丁,再打本地的补丁,最后还能在启动命令里临时加一层补丁。每一层都能覆盖上一层的结果。
![]()
dsh-base 是每个 profile 必装的第一层组合包,装上它就有了模型适配器、工具、数据持久化、安全策略、遥测这些基础能力。后续的组合包和用户自己的补丁按名字覆盖它装好的插件。
顶层是四种运行模式。 Standard、Code、Minimal、Creator 是同一套插件的不同组装。Standard 功能最全,Code 专门面向编程,Minimal 只留 Shell 和文件编辑两个工具,Creator 开放给想做实验的人。切换模式就是换一套插件组合,不改任何代码。
这三层从底到顶:Cordis 管单个插件的加载和卸载,三层组装管整套组合怎么搭,四种模式管最终跑哪套组合。整个项目就是通过这套流程组装出来的一套插件组合。
二、一切皆插件:先看懂 Cordis 的核心词汇表
插件是 DSH 的基本单元。要理解插件怎么工作,先认识五个核心概念。不用记名字,理解它们各自解决什么问题就行。
![]()
插件本身有三种写法。 写一个简单的功能,用一个函数就行;写一个要被别人调用的能力,用一个类;两种都不想用,还可以用一个对象。三种写法地位平等,加载和卸载走同一条路。
// 函数写法:最简单,适合注册几个监听器export const name = 'greeter'export function apply(ctx: Context) { /* ... */ }// 类的写法:适合提供给别人调用的能力export class GreeterService extends Service {constructor(ctx: Context) { super(ctx, 'greeter') }}
Context 是存放各种能力的地方。 每个能力有一个固定的名字,比如 ctx.tools 是工具、ctx.llm 是模型、ctx.sessions 是会话。别的插件通过名字来取用能力,不关心这个能力具体是谁写的、跑在本地还是远程。某个能力可以随时换成另一个实现,取用它的插件不用改。这是整个系统能"任意替换"的基础。
Service 是一种可以被别人取用的能力。 用类写一个 Service,它会自动注册进去;插件卸载时,它自动注销。
Fiber 是每个插件的生命周期记录。 一个插件从诞生到消失,经历六个阶段:排队等待依赖、正在启动、正常运行、正在卸载、完全消失,中间还可能启动失败。这条记录就是 Fiber。它是动态加载的核心。
inject 是插件的依赖声明。 插件开工前先声明自己需要哪些能力,框架会等这些能力都就位了才让它启动。启动顺序由依赖关系决定,不由写的先后顺序决定。
五个概念之外,插件之间不直接互相调用,全靠事件总线通信。一个插件发出通知,谁在监听谁响应,发通知的插件不用知道有谁在听。事件有五种分发方式:
![]()
最后一种 waterfall 是实现拦截的模式。每个监听者拿到一个"继续往下传"的开关,调用它就把工作交给下一个监听者,不调用就直接拦下。DSH 的工具执行流水线就是四段 waterfall 串起来的。
这五个概念加事件总线,构成了插件系统的基础。下面展开三个让"动态"变得可靠的关键机制。
Fiber、inject、effect:插件动态能力的三个关键机制
Fiber:让插件的加载与卸载都有迹可循
每个插件实例都有一条 Fiber 记录它的状态,在六个阶段之间转换:
排队等待 → 正在启动 → 正常运行 → 正在卸载 → 完全消失↘ 启动失败
流程是这样的。声明一个插件后,Cordis 检查它声明的依赖。依赖都满足了,就启动它,进入"正在启动",跑完进"正常运行"。依赖没满足,它就一直在"排队等待",不报错,静默等着。
卸载反过来走:正常运行 → 正在卸载(执行清理)→ 完全消失。
"排队等待"这个状态最容易被误判成"插件坏了"。一个插件声明需要某个能力,但那个能力没装上,它会一直排队,什么都不输出,进程也可能静默退出。想知道哪个插件在排队,遍历一遍所有插件的状态就行:
for (const runtime of ctx.registry.values()) {for (const fiber of runtime.fibers) {if (fiber.state === FiberState.PENDING) {console.log(`${fiber.name} 在排队等待,缺少某个依赖`)}}}
热重载走的就是这条状态机。 开发时改了一个插件的代码保存,监视插件会发现文件变了,先把旧实例卸载到"完全消失",再加载新代码走一遍"排队 → 启动 → 运行"。改配置文件 cordis.yml 本身也会触发更新,loader 按名字比对,只重新加载变化的部分。
inject:哪个插件先启动,依赖关系说了算
inject 不是启动时检查一次就完事,是持续跟踪的依赖关系。
加载顺序无关紧要。 插件 A 声明需要插件 B 提供的能力,Cordis 会让 A 排队,直到 B 就位。配置文件里 A 写在 B 前面还是后面,结果一样。把 B 彻底移除,A 就停在排队状态,不崩溃,也不会跑一半。决定插件何时启动的是依赖关系,不是文件里的先后顺序。
运行中仍在跟踪。 应用跑着的时候,如果某个能力消失了(提供它的插件被卸载或热替换),所有依赖它的插件也会跟着卸载;等这个能力恢复了,它们再重新加载。这能防止一个插件已经拿到某个能力的引用,那个能力却突然不见了。
这条机制的实际威力在一个场景里最明显。把默认的 Shell 执行插件换掉,挂上另一个提供方,所有用到 Shell 的插件都会自动重启、用上新的实现。不用改这些插件的一行代码,整条依赖链自动重排。
effect:插件卸载了,留下的副作用怎么办?
动态加载能跑不难,难的是卸载干净。插件启动时可能开了定时器、注册了监听器、挂载了子插件,卸载时这些都得清理干净。Cordis 的办法是 effect:插件通过框架 API 做的所有注册,都属于副作用,插件卸载时框架会自动撤销。
框架没直接管的资源(比如定时器、网络连接、文件监视),包进 ctx.effect() 里,同时返回一个清理函数:
export function apply(ctx: Context) {ctx.effect(() => {const timer = setInterval(() => console.log('tick'), 200)return () => {clearInterval(timer) // 卸载时跑这个console.log('heartbeat cleaned up')}})}
上面这段里,apply 里跑的是加载逻辑(开定时器),返回的函数是卸载逻辑(关定时器)。插件进入"正在卸载"时,框架自动调用这个清理函数。
几种常见操作天生就受 effect 管理,不用自己写清理逻辑:注册事件监听器,插件卸载时监听器自动移除;挂载子插件,父插件卸载时子插件跟着清理;往工具注册表里注册一个工具,插件卸载时工具自动注销。
清理函数的执行有条纪律:按注册的反序启动,但多个异步清理会并发跑。如果清理步骤有先后顺序要求,放进同一个清理函数里依次执行。
这三个机制合起来,动态加载才从"能用"变成"可靠"。Fiber 管生命周期,inject 管依赖,effect 管清理,三条各管一段,互不重叠。
![]()
一个真实的例子:工具运行时如何串起整条链路
DSH 里有个叫工具运行时的核心组件,上面讲的五个概念和三个机制在它身上全用上了。
它本身是一个能力类,会自动注册到系统里。它往系统里注册了 6 种事件,其中 4 种是 waterfall 拦截型、2 种是广播型。它声明自己依赖"提示词组装"这个能力,等这个能力就位了才启动,启动时立刻注册自己要用的提示词片段。
它提供一个"注册工具"的方法,调用方每注册一个工具,拿回一个对应的卸载函数。想动态卸载某个工具时,直接调用这个函数即可。这就是"动态"在最细粒度上的样子。
工具的执行是一条四段流水线,每一段都是一个 waterfall 拦截点:
![]()
审批插件可以在"执行前"这一环直接拦下,工具的实际逻辑从没运行;结果屏蔽插件可以在"执行后"把成功结果改写成错误提示,模型看到的已经是改写后的内容。
同一个工具运行时还能服务不同的 Agent。每个 Agent 能看到哪几个工具,是沿着一层层继承关系算出来的:先继承全局工具,再叠加自己所在的组合包层的工具,最后应用访问限制过滤。一个 Agent 被限制了只能用某几个工具,不影响另一个 Agent。
一个真实的 harness 服务长这样:它是个能力类,注册多种事件,声明依赖,用 effect 管所有注册,用分层让不同 Agent 看到不同工具集。前面讲的所有概念和机制,在它身上都能找到对应。
DSH 的插件架构是目前开源 Agent 框架里设计最彻底的一个。不是"支持插件",是整个产品就是插件的组合,任意环节都可以在运行时替换。
这种设计给个性化实现 Agent 留了很多可能。不同场景需要不同的工具集、不同的上下文策略、不同的执行流程,在 DSH 里都是换一套插件组合的事。模型可以换,工具可以换,连 agent loop 本身都可以换。呈现方式也可以换,Web UI、CLI、API 是不同的前端插件,挂在同一棵插件树上。
后面这个方向会出现很多玩法。自定义 agent preset、第三方插件生态、不同场景的插件组合模板,都是值得关注的方向。Harness 现在还是 v0.1 开发者预览版,接口随时可能变,但架构方向已经很清楚。
单看"任意环节都能在运行时替换"这一条,我们就非常看好它的架构设计。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.