尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

DeepSeek Harness 为什么敢说“一切皆插件“?拆透 Cordis 引擎的五大核心机制

DeepSeek Harness 为什么敢说“一切皆插件“?拆透 Cordis 引擎的五大核心机制 大多数 Agent 框架的核心循环锁死在代码里想改调度逻辑只能 fork。DeepSeek HarnessDSH选了一条更激进的路连 agent loop 本身都是插件。支撑这套设计的底层引擎叫 Cordis——本文拆透它的五个核心机制插件、生命周期与副作用、服务、事件、可配置插件。1. 问题Agent 框架的黑盒困境大多数 Agent 框架——LangChain、AutoGen、CrewAI——都采用同一套模式一个固定的核心引擎外挂一些可扩展的工具和模型适配器。你能在边缘加东西但核心循环、上下文管理、调度策略都锁死在框架内部。想换一个 agent loop 的调度逻辑要么 fork 整个框架要么提交一个 PR 等人 review。想替换 session 存储方式对不起核心代码里写死了。DeepSeek Harness下称 DSH选了一条不同的路一切皆插件。模型适配器是插件、工具注册表是插件、会话日志是插件、agent loop 本身也是插件。没有特权核心没有不能替换的组件。支撑这套设计的底层引擎叫Cordis——一个由 Koishi 框架作者开发、经过 4000 社区插件验证的元框架。它只做三件事管插件加载卸载、管服务依赖、管事件分发。所有 Agent 业务逻辑都在它之上以插件形式存在。Cordis 分层架构——底层是元框架中层是 DSH 核心插件顶层是用户扩展。三层之间没有硬编码依赖全部通过服务键和事件协作。注意架构图里一个关键特征三层之间没有箭头指向核心——因为不存在特权核心。agent-loop、session、tools这些看起来像框架骨架的组件和顶层的自定义工具是同一种东西普通插件。你可以在顶层写一个插件替换掉中层的任何一个组件不需要 fork不需要 PR。接下来五个章节逐一拆解这套架构的五大核心机制。2. 插件一块自带说明书的积木在 Cordis 里插件是什么不是一段被注入的脚本不是一个接口的实现类——它就是一个函数接收一个ctx上下文在里面干自己的活。就这么简单。三种写法同一个东西Cordis 支持三种等价的插件定义方式// 写法一纯函数最常见exportfunctionapply(ctx){ctx.on(tool/call,(event){console.log(工具被调用了,event.name)})}// 写法二带依赖声明和名字的对象exportconstnamemy-pluginexportconstinject[tools,session]exportfunctionapply(ctx){// 此时 ctx.tools 和 ctx.session 一定已就绪}// 写法三类classMyPlugin{staticinject[tools]constructor(ctx){// 等价于 apply(ctx)}}三种写法在运行时完全等价。核心约定只有一个插件需要一个apply(ctx)入口函数本身就是 apply类的 constructor 等价对象需要 apply 方法。在 Harness 中一切皆插件长什么样DSH 默认部署有 159 个插件。这不是夸张——连 agent loop负责驱动每一轮对话的核心调度器都是一个普通插件挂在ctx.agentLoop这个服务键上。你可以直接禁用它换上自己的调度逻辑。来看一个真实的例子。假设你想给 agent 加一个每次调用工具前记录审计日志的功能exportconstnameaudit-logexportconstinject[tools]// 依赖工具服务exportfunctionapply(ctx){// 监听 tools/pre-execute 事件waterfall 类型ctx.on(tools/pre-execute,(event,next){console.log([审计] 工具${event.name}参数${JSON.stringify(event.args)})next()// 继续执行链不阻塞})}这就完了。不需要继承某个基类不需要实现某个接口不需要注册到某个全局注册表。apply(ctx)里你拿到了上下文你就可以监听事件、注册工具、提供服务。Cordis 负责把你的插件挂到插件树上在合适的时机调用apply。关键插件的本质不是实现某个接口而是拿到 ctx 后在里面注册副作用。ctx 是一切的中介——你不需要 import 任何具体实现所有协作都通过 ctx 上的服务和事件完成。3. 生命周期与副作用来得干净走得也干净插件系统的头号难题不是怎么加载而是怎么卸载。一个插件启动时可能注册了 5 个事件监听器、开了 2 个定时器、连了一个数据库、注册了 3 个工具。卸载它的时候你怎么保证这些全都被正确清理传统做法是让插件作者自己写cleanup()方法——但人总会忘一旦忘了就是内存泄漏。Cordis 的答案是把所有副作用收口到一个原语ctx.effect()让框架自动追踪和回滚。Fiber 状态机插件的一生每个插件在 Cordis 中被包装成一个Fiber实例有自己的状态机Fiber 状态机——从等待依赖到完成卸载的完整生命周期。DISPOSED 后如果依赖重新出现会自动回到 PENDING 重新加载。这里要先澄清一个容易混淆的点Fiber 是插件的生命周期容器不是副作用的生命周期容器。副作用是挂在 Fiber 上的子项由 Fiber 统一管理回收但两者的地位不同Fiber插件的生命周期容器 ├── 状态机PENDING → LOADING → ACTIVE → DISPOSING → DISPOSED ├── 依赖声明inject [tools, session] ← Fiber 负责解析 │ ├── 副作用 1: ctx.on(agent/pre-step, handler) ├── 副作用 2: ctx.provide(notify, {...}) ├── 副作用 3: ctx.effect(() clearInterval(timer)) └── 子 Fiber如果 ctx.plugin(child) 被调用 └── 又有自己的状态机和副作用...简单说Fiber 是壳副作用是壳里装的东西。你 dispose 的是 Fiber壳Fiber 负责把里面的副作用逐个清掉。副作用本身没有状态机——只有已注册和已清理两个状态。几个关键细节PENDING → LOADING插件声明了inject: [tools, session]Cordis 会等这两个服务都就绪后才执行apply()。不需要手写轮询逻辑。ACTIVE → DISPOSING当插件被卸载或者它依赖的服务被卸载时Fiber 进入 DISPOSING 状态开始逆序执行所有 disposer。DISPOSED → PENDING如果依赖的服务重新出现比如热重载插件会自动重新加载。这就是热插拔的基础。ctx.effect()副作用的可逆注册核心机制是ctx.effect()。它接收一个函数函数里做副作用操作并返回一个撤销函数exportfunctionapply(ctx){ctx.effect((){// 做副作用 consttimersetInterval((){console.log(心跳)},5000)constoffctx.on(tool/call,handler)// 返回撤销函数 return(){clearInterval(timer)off()}})}当这个插件被卸载时Cordis 自动调用那个返回的撤销函数。定时器被清除事件监听被移除——全自动化。如果一个插件注册了多个 effect它们按LIFO后进先出顺序执行撤销类似退栈可逆副作用是 LIFO 回滚——后注册的先撤销保证依赖关系不被破坏。在 Harness 中的真实作用DSH 的热重载HMR插件就是这个机制的受益者。当你修改了一个工具插件的代码Cordis 会卸载旧插件→ 自动回滚它注册的所有工具、监听器、定时器加载新插件→ 重新执行apply(ctx)注册新的副作用整个过程对其他插件透明——它们只看到工具列表变了不需要知道是谁在热重载这就是Cordis 的‘时间可组合性’一个组件的副作用在移除时可以完全回退。不是靠人写 cleanup 代码而是靠框架自动追踪。需要注意的是ctx.effect()只能回滚通过 Context 做的修改——注册事件、提供服务、挂子插件。对于外部世界的不可逆操作已发送的 HTTP 请求、已写入数据库的数据、已发出的邮件框架无法自动回滚。插件作者需要自己处理这类边界。什么时候需要手动调用 ctx.effect()一条规则记住Cordis 内置 API 注册的东西自动回收外部资源的手动清理才需要ctx.effect()。exportfunctionapply(ctx){// ✅ 这些不用包 effectFiber 卸载时自动撤销ctx.on(agent/pre-step,handler)// listener 自动移除ctx.provide(notify,{...})// service 自动注销ctx.middleware((next,send){...})// 中间件自动摘除ctx.plugin(childPlugin)// 子 Fiber 自动 dispose// ❌ 这些是外部资源Cordis 管不到必须手动注册 effectconsttimersetInterval(()heartbeat(),5000)ctx.effect(()clearInterval(timer))// 否则定时器泄漏constdbawaitconnectDatabase(url)ctx.effect(()db.close())// 否则连接泄漏constwatcherfs.watch(./config.json,reload)ctx.effect(()watcher.close())// 否则 watcher 泄漏constserverapp.listen(3000)ctx.effect(()server.close())// 否则端口泄漏}判断口诀这个资源是 Cordis 的 API 创建的吗是 → 不用 effect否 → 用 effect。常见需要 effect 的setInterval、setTimeout、EventEmitter.on、fs.watch、数据库连接、HTTP server、WebSocket、child_process、第三方库的订阅。4. 服务插件之间的接头暗号插件之间怎么协作如果插件 A 需要调用插件 B 的功能直接importB 的代码吗不行——那样就硬耦合了B 被替换掉 A 就坏了。Seam 不是可选的设计模式是插件体系的根本协作方式先回答一个关键问题Seam 到底是什么是自定义服务时用的一种设计模式还是插件体系本身的东西答案是后者。Seam 是 Cordis 插件体系唯一的跨插件协作模型。不存在用 Seam和不用 Seam两种选择——只要你通过ctx.xxx访问另一个插件的能力你就在消费一个 Seam只要你通过ctx.provide(xxx, ...)注册能力你就在提供一个 Seam。DSH 里所有核心服务——ctx.tools、ctx.llm、ctx.sessions、ctx.agentLoop——全部是 Seam。它们不是碰巧用了这个模式而是 Cordis 框架内置的服务注册表机制本身。框架只认服务键不认具体实现。这意味着核心服务也是 Seamcore/tools插件通过ctx.provide(tools, ...)注册工具服务和你的自定义插件注册ctx.provide(myService, ...)走的是同一条路径没有特权。替换核心服务 提供新 provider想换掉默认的工具执行管道写一个插件ctx.provide(tools, yourImpl)原消费者自动切到新实现。没有旁路你不能绕过 Seam 直接 import 另一个插件的代码。Cordis 的模块隔离机制保证了插件之间只能通过 ctx 上的服务键通信。一个 Seam 的完整生命周期定义 → 提供 → 消费来看一个完整的例子。假设 DSH 里没有通知服务你想自己建一个——让其他插件可以发送桌面通知。第一步定义服务接口契约// 通知服务的接口契约——约定了消费者能调用什么方法interfaceNotificationService{notify(title:string,body:string):voidsetEnabled(enabled:boolean):void}在 DSH 中服务接口通常以 TypeScript 类型声明存在作为插件之间的合同。消费者看接口就知道能调什么方法不需要看提供者的实现代码。第二步提供者——注册服务实现// desktop-notify.js — 通知服务的提供者exportconstnamedesktop-notifyexportfunctionapply(ctx){letenabledtrue// 注册服务把实现挂到 notify 这个键上ctx.provide(notify,{notify(title,body){if(!enabled)return// 调用系统通知 APIprocess.stdout.write(\x1b]9;${title}^${body}\x07)},setEnabled(val){enabledval}})// 提供者卸载时框架自动注销 notify 服务键// 消费者会感知到服务消失自动进入 PENDING 等待}第三步消费者——通过 ctx 使用服务// task-reminder.js — 通知服务的消费者exportconstnametask-reminderexportconstinject[notify]// 声明依赖exportfunctionapply(ctx){// 到这里ctx.notify 一定已就绪// 因为 inject 声明了依赖框架保证了加载顺序ctx.on(task/completed,(event){ctx.notify.notify(任务完成,「${event.taskName}」已完成)})}三个角色各司其职定义者管能调什么提供者管怎么实现消费者管什么时候调。提供者可以被随时替换消费者代码一行不用改。Seam 模型——这不是某个自定义服务碰巧用了的模式而是 Cordis 插件体系的根本协作方式。所有ctx.xxx访问都是 Seam。DSH 中的核心服务全部遵循这个模型服务键提供者能力ctx.toolscore/tools 插件工具注册表和受保护的执行管道ctx.llmllm/llm 插件消息词汇表和模型适配器接缝ctx.sessionscore/session 插件追加式事件日志和内存存储ctx.agentLoopcore/agent-loop 插件默认的 Turn/Step 驱动实现ctx.systemPromptcore/system-prompt 插件Prompt 段落和工具 schema 组装注意这些核心服务和上面例子里的ctx.notify走的是完全相同的注册路径。core/tools插件里写的也是ctx.provide(tools, {...})没有特权 API。替换一个 provider 换了半个产品Seam 最强大的地方在于换一个 provider消费方代码一行都不用改。来看 DSH 里的一个真实场景。默认情况下文件系统 provider 指向本地磁盘——Bash 工具在本地执行文件编辑器改本地文件。现在你想把所有执行都搬到远程沙箱// remote-sandbox.js — 替换 fs 服务的提供者exportconstnameremote-sandboxexportfunctionapply(ctx){// 提供新的 fs 服务实现覆盖默认的本地文件系统ctx.provide(fs,{readFile:(path)rpc.call(remote_read,path),writeFile:(path,data)rpc.call(remote_write,path,data),exec:(cmd)rpc.call(remote_exec,cmd),})}挂上这个插件后Bash、PTY、LSP 三个工具自动迁移到远程沙箱——因为它们消费的是ctx.fs这个服务键而不是 import 某个具体的本地文件系统模块。provider 换了消费方无感知。替换 provider 的效果——从本地文件系统到远程沙箱零代码修改。这就是核心服务也是 Seam的直接好处连文件系统这种基础设施都能被一个普通插件替换。inject声明的依赖自动的加载顺序插件通过inject声明它需要哪些服务。Cordis 根据这个声明自动推导加载顺序// 这个插件需要 tools 和 session 两个服务exportconstinject[tools,session]exportfunctionapply(ctx){// 到这里ctx.tools 和 ctx.session 一定已就绪// 不需要 if (ctx.tools) 之类的判断ctx.tools.register({name:search,execute:(args){...}})}如果tools服务还没就绪提供者还没加载这个插件的 Fiber 会停在PENDING状态直到tools可用才进入LOADING。反过来如果tools服务的提供者被卸载了这个插件会先被自动卸载因为依赖没了等tools重新出现时再自动加载。这就是 Cordis 的‘空间可组合性’组件之间通过服务声明依赖框架自动管理加载和卸载的因果关系。你不需要写一行等对方准备好的代码。5. 事件插件的神经系统服务解决了插件怎么调用彼此的能力但还有一类问题服务解决不了插件怎么在关键节点插一脚比如每次模型请求前检查一下消息是否包含敏感信息。每次工具执行后记录一下耗时。每次 turn 结束前决定是否要追加一个 step。这些不是调用某个服务——它们是在某个时机拦截或观察。Cordis 用类型化事件解决这个问题有四种派发模式四种事件模式模式行为类比emit发射即忘所有监听器同步执行忽略返回值广播通知——“我发生了一件事听到的自己处理”waterfall链式传递每个监听器收到上一个的结果必须调next()才继续中间件管道——“数据经过我手我可以改它也可以直接拦下来”serial串行执行无next()不能委托逐一询问——“每个人说一句没有反驳权”parallel并行扇出所有监听器同时执行群发任务——“大家一起干等最慢的那个”在 DSH 中怎么选需要拦截/改写数据 → waterfall如 agent/pre-step 可拒绝或改写消息需要观察/记录 → emit如 session/created 不影响流程需要逐一决策 → serial如 agent/turn-stopping 每个监听器投票实战Agent Loop 里的事件流DSH 的 agent loop 是事件系统最好的教学案例。一轮对话Turn被切成多个步骤Step每个关键节点都有对应的事件上图是Agent Loop 的完整事件流——标记了扩展点的是可拦截事件waterfall/serial其余是 durable 持久化事件。注意图中两种节点的区别durable 节点持久化事件写入 session log。用于记录发生了什么——fork、resume、replay 都从这条事件流派生。扩展点节点waterfall 或 serial 事件是插件可以拦截的接缝。举个实际的拦截例子。假设你想做一个敏感词过滤插件——每次模型请求前检查消息发现敏感词就拦截exportconstnamesensitive-filterexportconstinject[agent]exportfunctionapply(ctx){// agent/pre-step 是 waterfall 事件ctx.on(agent/pre-step,(event,next){constmessagesevent.messagesconsthasSensitivemessages.some(mm.content.includes(密码)||m.content.includes(token))if(hasSensitive){// 不调 next()直接 reject——短路整条链return{kind:reject,reason:检测到敏感信息}}// 没问题放行next()})}关键在于next()。waterfall 事件中每个监听器收到(event, next)两个参数。调用next()就把控制权交给下一个监听器不调用就直接短路——后面的监听器和默认行为都不会执行。这和 Koa 的中间件、Express 的 middleware 是同一个思路。事件 vs 服务什么时候用哪个有一条简单的判断原则拦截和策略用事件直接调用稳定能力用服务方法。比如每次工具调用前检查权限是策略用tools/pre-execute事件注册一个新工具是直接能力用ctx.tools.register()服务方法。6. 可配置插件用配置文件拼乐高到目前为止我们说的都是用代码写插件。但 DSH 还有一层更高级的能力用配置文件组合插件不需要写一行代码就能定制你的 Agent。这套系统由三个概念组成Bundle、Profile、Patch。四层配置从粗到细四层配置的层叠模型——从 Bundle 到 CLI overlay逐层覆盖。每一层的作用Bundle一组 Cordis 配置行 对应代码的分发格式。dsh-base是所有 Profile 的第一层提供核心能力。上面再叠dsh-web-app加浏览器 UI或dsh-headless加无头运行器。Profile Patch针对特定 Profile 的覆盖文件。比如你的 web Profile 想换一个不同的模型适配器就在这里 patch。Home Patch全局覆盖对所有 Profile 生效。比如你想全局禁用某个工具。CLI Overlay命令行--patch参数临时最高优先级覆盖。适合调试和一次性实验。Patch 长什么样Patch 文件就是一个 YAML通过行 ID 定位要替换或新增的配置# cordis.patch.yml# 替换默认的 LLM 适配器改用自定义 provider-id:llm-deepseekreplace:plugin:my-custom-llmconfig:apiKey:${env.MY_API_KEY}model:deepseek-v4-pro# 新增一个审计日志插件-id:audit-loginsert:plugin:my-org/dsh-auditconfig:logPath:/var/log/dsh-audit.jsonl想看你的机器实际启动了什么一行命令dsh--profileweb --dump-config这会打印出合并后的完整插件树——每一行都能被你自己的 patch 覆盖。在 Harness 中的作用四种模式DSH 内置了四种 Profile 模式每种加载不同的插件集合模式加载的插件适用场景标准模式完整工具组合 Web UI日常开发使用PTC 模式程序化工具调用——模型生成代码来组合多轮工具复杂工作流自动化极简模式仅 Shell 文件编辑工具最小环境下的模型基准测试创造模式可检查运行时、在内存中试验 Cordis 插件组合和创作新的模式这四种模式的区别仅仅是加载的插件集合不同——没有任何 if-else 分支写在代码里。切换模式就是切换 Profile就是换一棵插件树。这就是一切皆插件在实践中意味着什么连产品形态本身都是配置。7. 这套架构的优势在哪里五个章节拆完回到最开始的问题DSH 为什么要用 Cordis这套架构到底好在哪优势一零 fork 扩展传统框架想改核心行为路径是 fork → 改源码 → 维护差异。Cordis 的路径是写一个插件 →ctx.provide(xxx, newImpl)→ 完了。前面看到的远程沙箱替换就是典型案例把本地文件系统换成远程 RPCBash/PTY/LSP 三个工具零代码修改自动迁移。在传统框架里这是大工程——你需要改框架源码里所有fs.readFile的调用点。在 Cordis 里你只是提供了一個新的 Seam provider。优势二安全的热插拔ctx.effect() LIFO 回滚保证了插件来得干净走得也干净。这意味着你可以热重载修改插件代码后自动卸载旧的、加载新的其他插件无感知动态启停运行时按需加载/卸载插件不需要重启进程A/B 实验同时加载两个实现不同策略的插件通过配置切换哪个生效这些能力的根基是框架自动追踪副作用——不是靠插件作者自觉写 cleanup而是靠ctx.effect()的可逆注册机制。优势三依赖自组织inject声明 Fiber 状态机 依赖关系自动推导。你不需要手动排插件加载顺序写if (ctx.tools)判断服务是否就绪担心循环依赖——框架在加载阶段就能检测到插件之间通过服务键声明依赖框架负责拓扑排序和生命周期联动。依赖消失时自动卸载消费者依赖恢复时自动重新加载。这一切都是声明式的。优势四配置即产品形态四种 Profile 模式标准/PTC/极简/创造的差别仅仅是加载了不同的插件集合。没有任何if (mode ptc)写在代码里。切换产品形态 切换配置文件 换一棵插件树。这意味着你可以用同一套代码库通过不同的 Bundle Patch 组合派生出完全不同的产品形态——开发工具、CI 机器人、基准测试平台——而不需要维护多个 fork。一句话总结当 Agent 领域还在快速演化——新的模型能力、新的工具类型、新的调度策略层出不穷——你需要的不是一个固定的框架而是一个能让所有部件自由替换、自由组合、自由热插拔的底座。Cordis 就是这个底座没有特权核心一切皆插件注册即可逆依赖自组织。参考deepseek-ai/deepseek-harness — GitHub 仓库cordiverse/cordis — Cordis 元框架A Programming Paradigm for Spatiotemporal Composability— DeepSeek AI 北京大学, 2026-08-13cordis.moe — Cordis 官方文档Koishi — 四年开发4000 社区插件Cordis 的首个大规模验证案例
返回列表