DeepSeek-Harness 的 Cordis 架构:一切皆插件
做 AI Agent 产品时,模型适配、工具调用、会话日志、主循环往往先堆进一块「核心」:换厂商要改核心,关某个工具要改核心,日志换存储还是改核心。特权中心一旦形成,周边模块都围着它转,谁都不敢动。DeepSeek-Harness(简称 dsh)的取舍是不要这块特权核心:产品的每一部分都是挂在 Cordis 上下文上的插件,模型适配器、工具注册表、会话日志,以及 agent loop 本身都包含在内。
Cordis 上下文当公共插线板
Cordis 是轻量插件框架。可以把它看成一块共享插线板:每个插件把自己的能力插上去,别的插件再从板上取用。它们地位相同,都挂在同一个 Context 上;没有哪一块是「必须围着转」的内核。
扩展方式是把新插件挂到已有插件旁边。卸载时,该插件留下的注册会一起撤掉。文档里的说法是:不存在需要打补丁的特权内核,扩展 dsh 靠挂载,注册都是副作用,会在插件卸载时撤销。
插件向上下文贡献三样东西
挂载之后,插件通常贡献服务、类型化事件,以及可逆的副作用。三者经常一起出现,但职责分开。
服务:往 ctx 上挂能力供别人用。例如模型适配器注册到 ctx.llm:
1 | ctx.llm.register({ |
别的插件通过 ctx.llm 找到实现。调用方认接口键,不认具体包名;这是后文 Capability Seam 的雏形。工具侧同理,通过 ctx.tools 注册与查找。
类型化事件:监听或触发约定好的事件名。主循环会发 agent/request 一类事件,旁边的插件可以观察或介入。横切能力(日志、限流、审计)优先找有没有合适事件可挂,往往比改主循环省事,也少碰驱动器内部状态。
可逆副作用:注册工具、挂服务、订事件,在 Cordis 里都记成该插件的副作用。插件卸载时框架按记录撤销。例如 ctx.tools.register('read_file', readFileTool) 在卸载后从注册表消失,调用方不必手写对称的 cleanup。拔掉一块积木,它连过的接口自动松开。对长时间跑的进程来说,这比「记得在某处调用 unregister」可靠得多:生命周期跟 fiber / 插件绑定,泄漏面更小。
最小扩展:挂一个工具插件
给模型加「查询当前时间」这类能力时,不必改主干。写一个插件,只做注册:
1 | export default function myTimePlugin(ctx) { |
ctx.plugin(...) 执行插件函数,并把这次注册记到该插件名下。之后卸载,get_time 会自动消失,模型侧工具列表随之变短。扩展等于挂载,回退等于卸载,已有代码不用动。
时序上可以简化成:调用方 → Cordis 上下文执行插件 → 插件调用 ctx.tools.register → 框架记录副作用归属;卸载时框架对注册表做反向撤销。清理责任在框架,业务插件只声明「我贡献了什么」。
源码里哪些包是可替换主干
docs/architecture.md 把理念写得很直白:每一部分都是插件,都可以从配置替换。packages/core/ 里几块主干组件对应关系大致是:
| 包 | 职责 | ctx 键 |
|---|---|---|
core/session |
仅追加的会话事件日志 | ctx.sessions |
core/tools |
工具注册表与执行流水线 | ctx.tools |
core/agent |
Agent 接口与事件 | ctx.agents |
core/agent-loop |
默认驱动器(主循环) | ctx.agentLoop |
llm/llm |
消息与流式词汇表、适配器 seam | ctx.llm |
注意 core/agent-loop 只是默认驱动器,接口定义在 core/agent。主循环本身也可换成别的插件,只要仍对接同一套 Agent 事件与服务约定。换模型、关工具、换日志存储,对应操作是换插件或卸载插件;共享核心里不必再堆「按厂商分支」的硬编码。
和「大一统内核 + 外围补丁」比,这套结构把可替换边界提前钉死在 Context 的服务键和事件名上。读源码时也可以按这个边界扫:先看某个 ctx.* 谁在 register,再看谁在消费;中间很少有第三份「偷偷直连」的路径。实际启动时挂哪些插件、按什么顺序叠,由 Profile 与 Bundles 决定;Cordis 这一层先定规矩:共享 Context、服务与事件作扩展点、副作用可逆。配置档案负责组合,Capability Seam 负责接口与实现分离,二者都建立在「没有特权核心」这条线上。
DeepSeek-Harness 的 Cordis 架构:一切皆插件