一、DeepSeek Harness的核心框架
这几天圈子里都在聊 DeepSeek Harness。8 月中旬 DeepSeek 放出 v0.1 开发者预览版,MIT 协议开源,它主打的那句话是「Everything is a plugin」——一切皆插件。模型适配器是插件,工具是插件,技能是插件,会话记录是插件,沙箱是插件,文件系统是插件,连 agent loop 本身和界面都是插件。Cordis名字取自拉丁语「心」的所有格,此前已经给跨平台聊天机器人框架 Koishi(GitHub 六千多星)当了四年底座。DeepSeek 这次把 Cordis v4 整个 vendor 进了 harness 仓库,同期还发了一篇讲设计范式的论文,标题里那个词叫「时空可组合性」(Spatiotemporal Composability)。Cordis 的定位是元框架——一个用来写框架的框架。它不提供任何业务能力:没有 HTTP,没有数据库,没有调度器。它只提供三件事:插件系统、依赖注入、生命周期管理。DeepSeek 选中它做 harness 的地基,看中的正是这三件事在 agent 场景里的价值。下面用一个日常的例子把它讲清楚。二、把它想成一座商场
商场全景:Context 提供共享基础设施,各店铺是插件,cordis.yml 是铺位表商场本身不卖东西。 它提供的是公共设施:供电、供水、网络、消防、中央空调,还有一套全场通用的会员和收银系统。商场自己一件商品都不生产。每一家店是一个插件。 奶茶店、书店、生鲜超市、影院,各卖各的。它们之间不互相认识,也不需要认识。哪些店进场,由一张铺位表说了算。 表上写着 3 层 A12 是书店、B 区整排是餐饮。想换业态,改这张表就行,不用重建商场。到这里都还是普通的插件系统。VS Code 的扩展、Spring 的 Bean 容器,大体也是这个思路。Cordis 在此基础上多做了几件事,下面逐一展开。进场要登记,撤场要复原
进场登记 vs 撤场复原:每一处改动登记在册,撤场倒着来一遍一家奶茶店进场装修,动的东西不少:接了上下水、拉了专线、墙上打了孔挂招牌、在商场的导购大屏上加了一条自己的信息、还在会员系统里注册了一个商户号。商场的规矩是:每一处改动都登记在册,并且写清楚怎么撤销。等这家店撤场,物业照着登记表倒着来一遍:注销商户号、从导购屏上撤掉信息、拆招牌、补墙面、断专线、封上下水。撤完之后,这个铺位跟这家店从没来过一样。这条规矩保证了整座商场能反复折腾。一家店撤了,墙上留个洞、导购屏上还挂着已经关门的店名、会员系统里堆着一个无效商户号——这些残留一次两次不明显,翻台十几次以后,商场就不好维护了。多数插件系统不强制这件事。VS Code 扩展卸载后偶尔留个残影,重启编辑器就好了。Spring Bean 的销毁回调写不写全凭自觉,进程停了也就结束了。Cordis 把它做成强制的,原因在第四节会讲到——agent 场景对清理的要求更高。有的店,得等条件齐了才能开
生鲜超市要冷链电源才能开门。冷链没接通的时候,它就在场外候着,不开业,也不报错——因为供应商可能下周才进场。冷链一通,它自动开门。由此带来一个结果:谁先进场不重要。铺位表上生鲜写在第一行还是最后一行,最终商场长什么样完全一样。开业顺序由「谁依赖谁」推出来,跟表上的先后无关。换供应商,靠它吃饭的店集体重开
商场换了一家冷链供应商。所有靠冷链的店会先停业,等新供应商接通,再重新开门。停业这一步,走的正是前面那套撤场流程——旧的登记项全部回卷。这样就不会出现「店还在营业,但它手里握着一个已经不存在的旧冷链的钥匙」这种事。单店改造,不关整个商场
奶茶店要重新装修,让它一家撤场、装完再进场就行,隔壁书店照常营业,商场的电梯也不停。这就是热更新。消防演习:广播与响应
商场偶尔搞消防演习,中控室广播一句「全场暂停营业」。有的店直接拉闸,有的店先结完当前顾客的账再关。中控室不关心谁怎么做,甚至不知道今天有几家店在开——它只负责喊。更精细的广播可以沿途拦截:商场准备上一条新的促销推送,先经过合规审核的那家店过一遍,审核通过才真正发出去;合规那家店甚至可以改措辞再放行。三、把商场翻回代码
商场 = Context。ctx是那个共享空间,插件拿到它,往上注册自己贡献的一切。最小的插件长这样:import type { Context } from '@deepseek-ai/cordis'export const name = 'hello'export function apply(ctx: Context) {console.log('hello from my first plugin')}
铺位表 = cordis.yml。 启动器读一份 YAML 配置,逐项挂载插件。列表里的位置不代表加载顺序,顺序由依赖关系决定。disabled: true保留条目但不挂载,改回来插件就重新加载。DeepSeek Harness 那四套预设(Standard / Code / Minimal / Creator),本质就是四份不同的 cordis.yml。登记与复原 = effect 与 disposer。 Cordis 管不到的资源——定时器、连接、文件监视器——包进ctx.effect(),返回一个释放函数。花括号里是进场,return出去的函数是撤场:ctx.effect(() => {const timer = setInterval(() => console.log('tick'), 200)return () => clearInterval(timer)})
插件卸载时,Cordis 按注册顺序的逆序把所有 disposer 跑一遍。绝大多数时候你不用自己写ctx.effect(),因为内建的 API 本身就是 effect:ctx.on()挂的监听器会自动摘掉,ctx.plugin()挂的子插件跟着父插件卸载,ctx.tools.register()注册的工具自动注销。候场 = inject 与 PENDING。 提供方继承Service把自己挂成一项具名服务(比如'greeter');消费方只声明依赖,不 import 提供方。inject里列的服务没就位,插件就停在 PENDING,apply一次都不会跑;等服务出现,它自动启动。inject不是一次性的启动检查——运行中服务消失了,依赖它的插件会跟着卸载,服务回来再重新加载。Harness 里换 shell 实现就是这么干的:卸掉dsh-bash-local,挂上另一个shell提供方,所有inject: ['shell']的插件自动重启,消费方一行代码不改。单店改造 = HMR。 卸载能把 effect 全部回卷,加载又严格遵循依赖,热替换就成了「先卸再装」。装上 HMR 插件,存盘即重载,改 cordis.yml 本身也会触发更新。广播 = 事件。 Harness 用事件处理工具结果、模型请求、审批决定。一个日志插件可以用ctx.on('tools/result', ...)监听所有工具调用,完全不需要知道有哪些工具。事件有四种派发方式:emit只通知(消防演习的广播);waterfall可以改返回值和短路(促销推送过合规审核);parallel并发;serial顺序执行。落到 harness。 Harness 的子系统——会话日志、系统提示词、工具注册表、agent 循环、LLM 适配器——全是 Cordis 服务。给它加一个模型可调用的工具,就是前面这些概念拼起来:export const inject = ['tools']export function apply(ctx: Context) {ctx.tools.register(defineTool({name: 'greet',description: 'Greet the named person.',parameters: {name: { type: 'string', required: true, description: 'Who to greet' },},async execute(args) {return `Hello, ${args.name}!`},}))}
inject: ['tools']等注册表就位;ctx.tools.register()的 disposer 自动挂到插件上,卸载时工具注销;defineTool把参数规约转成 JSON Schema 给模型看。三十行不到,一个可热插拔的工具就进了 harness。四、agent harness 为什么需要这一套
普通系统 vs Agent:重启能救 vs 残留累积Agent 是长时间运行的,手里攥着多轮上下文、会话日志、未完成的工具调用、待审批的操作。中途换零件是常态:换个模型、换个沙箱、加一件工具、临时收紧权限。这些操作如果不能干净地撤销,残留就会在同一个会话里持续累积——旧模型适配器还挂在事件总线上,卸载掉的工具仍然出现在系统提示词的工具清单里,模型照着一个已经不存在的工具发起调用。这类问题不会报错,只是行为慢慢变得不对,排查起来成本很高。时间上的可组合——组件移除之后,它造成的副作用完全回卷,反复装卸不留残渣。空间上的可组合——组件之间的依赖是响应式的:B 依赖 A 提供的服务,那么 B 一定在 A 之后启动、在 A 之前停止,A 起不来 B 就不启动。两者合起来得到的性质叫路径无关:应用的最终状态只取决于「现在启用了哪些插件」,跟这些插件的装卸顺序无关。这正是安全做热替换的前提,也是那篇论文标题里「时空可组合」的意思。还有一层实际的好处:agent 可能在运行中重配自己的技术栈。每一次改动都能干净回滚,是安全做这件事的前提。Cordis 把可逆性做成了框架级的默认行为,插件作者不用靠自觉。五、最后