第 1 篇:核心原理与设计理念
本篇结论:
dsh的一切都建立在「没有特权核心」这一条上 —— 连模型适配器、工具注册表、会话日志、Agent 主循环本身都是插件,随时可以拆下来换掉。读完你能得到:看懂「一切皆插件」是怎么落地的,掌握 dsh 的五条核心原理和三条设计理念,并且知道自己以后该往哪儿加东西。
开篇:乐高,还是一体成型玩具?
市面上的 AI Agent 框架,大多像一体成型的玩具车:车身、轮子、发动机焊死在一起。你想换个更好的发动机?要么等官方出新款,要么自己拿锯子锯开——锯完还容易装不回去。
dsh更像乐高。而且不是普通的乐高,是那种连发动机都是标准积木块、拔下来换个涡轮就能上赛道的乐高。
官方文档对这一点说得非常直白(docs/architecture.md:11):
每一个产品部件都是插件,包括模型适配器、工具注册表、会话日志,以及Agent 循环本身。
注意最后那半句。「Agent 循环」是什么?是「收到用户消息 → 组装提示词 → 调模型 → 执行工具 → 再调模型……」这条主心骨。在任何别的框架里,这就是框架本身,是绝对不会让你碰的东西。
在 dsh 里,它只是清单上的一行,可以换。
1.1 那这个「乐高标准」是谁定的?—— Cordis
Cordis是 dsh 底下的插件框架(源码被整包放进vendor/cordis,改了作用域名重新发布)。它提供了三样东西:插件机制、共享上下文、类型化事件。
dsh 官方给 Cordis 总结了五条核心思想(docs/cordis-primer.md:7-13),这五条就是理解 dsh 的全部钥匙。下面逐条讲。
术语小卡片:Cordisdsh 底下的插件框架,被 dsh 以源码形式内置(vendored)并重新命名作用域为
@deepseek-ai/cordis。你写 dsh 插件时用到的ctx、inject、effect全都来自它。
1.2 原理一:一切皆插件
一个「插件」在 Cordis 里就是一个实现了Service的对象 —— 可以是一个带inject和apply(ctx)的函数,也可以是一个Service子类。
关键不在于它是什么,而在于「什么都能是它」。
| 你以为它是 | 在 dsh 里它其实是 |
|---|---|
| 框架的核心 | 一个插件 |
| 模型调用能力 | 一个插件 |
| 文件读写能力 | 一个插件 + 可替换的实现 |
| bash 执行 | 一个插件 + 可替换的实现 |
| Agent 主循环 | 一个插件 |
这意味着什么?意味着你想给 dsh 加功能,不需要改 dsh 的源码。你只需要写一个新插件,挂到它旁边去。
官方文档的原话(docs/architecture.md:13):
There is no privileged core to patch: you extend dsh by mounting a plugin beside the others.(没有需要打补丁的特权核心:你通过在其他插件旁边挂载一个插件来扩展 dsh。)
用乐高的话说:没有哪块积木是「底座」。整辆车都是标准件拼的,所以你永远能在任意位置插一块新的。
1.3 原理二:Context 是「服务的仓库」
所有插件共享一个Context(代码里都写成ctx)。谁想提供能力,就把能力注册到ctx上一个固定的 key;谁想用能力,就按 key 去取。
ctx.sessions → 会话存储 ctx.tools → 工具注册表 ctx.llm → 模型调用 ctx.systemPrompt → 提示词装配 ctx.agents → Agent 注册表 ctx.fs → 文件系统 ctx.shell → Shell 执行为什么这很重要?看一段对比:
// ❌ 传统做法:直接依赖具体实现import{DeepSeekClient}from'./deepseek-client'constanswer=awaitnewDeepSeekClient().chat(messages)// ✅ dsh 做法:依赖「能力」,不依赖「谁实现的」constanswer=awaitctx.llm.stream(messages)第二种写法里,我根本不知道背后是 DeepSeek 还是别的模型。我只要「能流式聊天的东西」,ctx.llm给我哪个我就用哪个。
这就是「换 Provider 就换掉整条产品线」的技术基础。你把ctx.llm换成另一个实现,所有调用它的地方——不管有多少个——全部自动跟着换。
术语小卡片:Context(
ctx)所有插件共享的「服务仓库」。插件通过ctx.<key>存取服务,而不是互相 import。你可以把它想成一面工具墙:谁需要锤子就去墙上拿,不用认识锤子的厂家。
1.4 原理三:依赖靠inject声明,加载顺序自动推导
现实问题:插件 A 需要用到ctx.skills,但提供ctx.skills的插件 B 还没加载,怎么办?
传统做法是手动排启动顺序 —— 然后这个顺序表会随着插件数量增长变成一个维护噩梦。
dsh 的做法:插件声明我需要什么,框架自己算顺序。
// 我这个插件需要 ctx.skills 存在才能工作exportconstinject=['skills']exportfunctionapply(ctx){// 能进到这里,说明 ctx.skills 一定已经就绪了}类比:装修时你不需要自己排「刷墙必须在铺地板前」的工期表。你只要跟工长说「我需要地板先铺好」,工长自己排期。而且如果哪天地板供应商跑了,工长会告诉你「这活干不了」而不是让你在没铺地板的房间里刷墙。
一个额外的好处:如果ctx.skills被撤走了(比如那个插件被禁用),依赖它的插件会整个卸载,而不是留下一个半死不活的状态。这是靠原理五实现的。
1.5 原理四:类型化事件是扩展点
插件之间不直接互相调用,而是通过事件通信。Cordis 的事件有五种派发模式:
| 模式 | 是否等待 | 顺序 | 有返回值 | 类比 |
|---|---|---|---|---|
emit | 否 | 按注册顺序 | 无 | 小区广播,听到了就听着 |
waterfall | 否 | 按注册顺序 | 有 | 流水线,每个人加工完传给下一个 |
parallel | 是 | 并行 | 无 | 群里发通知,大家一起干 |
serial | 是 | 按注册顺序 | 有 | 排队签字,一个人签完下一个 |
bail | 否 | 遇到第一个「拍板」的停 | 有 | 谁先举手谁说了算 |
这里面waterfall(瀑布)最重要,因为它是 dsh 里「拦截和改写」的主要手段。
瀑布是这样工作的:监听器收到(...参数, next),你加工完必须调用next()把结果传给下一棒;你不调用next()就是短路——后面的监听器全部看不见这次事件。
ctx.on('fs/write-intent',(intent,next)=>{if(intent.path.startsWith('/etc')){return// 不调 next() → 拦截!这次写入不发生}returnnext()// 放行})用乐高的话说:事件就是在积木之间插的那根电线。你不需要拆掉原来的积木,只要在电线上接一个自己的模块,就能改变信号的走向。
术语小卡片:事件域dsh 的事件分三大域:
- Session 事件(
turn/start、user/message…)—— 持久化到日志里的事实,重启不丢- Agent 事件(
agent/pre-step、agent/request…)—— 观察或拦截正在进行的工作- Capability 事件(
fs/*、tools/*…)—— 给某个能力接缝挂策略或适配器
1.6 原理五:注册即效应,卸载即回滚
这是五条里最容易被忽略、但技术上最关键的一条。
你在插件里做的每一个贡献—— 注册一个提示词段落、挂一个工具、监听一个事件 —— 都必须通过ctx.effect()或ctx.on()完成。这两个调用会记录一个「撤销函数」(disposer)。
exportfunctionapply(ctx){ctx.effect(()=>{consttool=ctx.tools.register(myTool)return()=>tool.dispose()// 返回撤销函数})}当这个插件被卸载时,所有效应会按相反顺序回滚,干干净净。
类比:魔术贴 vs 502 胶水。
用 502 粘的东西,你想换就得撬,撬完留一坨胶。用魔术贴粘的,一撕就掉,墙面完好。
dsh 里所有东西都是魔术贴。这带来两个直接好处:
- 热重载是安全的—— 改一行配置,重排插件树,旧的撕干净新的贴上去,不会留下僵尸监听器。
- 依赖消失时整个卸载是安全的—— 回到原理三那个例子:
ctx.skills撤走时,依赖它的插件连同它注册的所有监听器、工具、文件监视器,一次性全部撤干净。
1.7 三条设计理念
原理是「怎么做的」,理念是「为什么这么选」。
理念一:没有特权核心
已经讲过了,但值得再说一遍,因为它是 dsh 所有其他设计的源头。
你要加功能,就在旁边挂一个新插件。不要改核心代码 —— 因为根本没有核心代码可以改。
理念二:模型可见即已落日志
官方文档原话(docs/architecture.md:121):
Model-visible means logged.Anything that reaches a model request must be reconstructable from the log.
任何送到模型面前的内容,都必须能从会话日志里重建出来。dsh 甚至有一个运行时断言在检查这件事。
为什么?因为如果一个东西能影响模型的判断,但你无法从日志里复现它,那这个 Agent 的行为就是不可解释、不可调试、不可复盘的。你永远说不清「它当时到底看到了什么」。
代价是什么?想给模型加一种新的可见输入,就必须新增一种会话事件类型,然后从日志里渲染出来。不能直接偷偷塞一段文本进去。
用乐高的话说:每一块积木都必须有说明书上的编号。没有编号的积木不许上车。
理念三:一切从配置可替换
你已经见过--dump-config打印的那张插件清单了。那份清单里的任何一行,你都可以用自己的 Patch 替换掉。
不需要改源码、不需要 fork、不需要等官方合并 PR。你写一份cordis.patch.yml,点名要换哪一行,就换了。
这是理念一和原理二的必然结果:既然都是插件、都挂在ctx上按 key 取用,那么「换掉某个 key 的实现」就只是配置问题。
1.8 能力接缝:三件套
这是理解 dsh 模块组织的核心心智模型,第 2 篇会展开,这里先立个概念。
dsh 里一个「可替换的能力」叫Seam(接缝),它由三个角色组成(docs/architecture.md:127):
| 角色 | 职责 | 类比 |
|---|---|---|
| Service Definition | 声明接口标准:「文件系统应该有 read / write / list」 | 插座的国家标准 |
| Provider | 具体实现:「本地文件系统」「沙箱文件系统」「远程沙箱」 | 某个品牌的插头 |
| Consumer | 使用方,通常是暴露给模型的工具 | 插在插座上的电器 |
一句话记住它:三缺一就不成接缝。加了定义没实现,或者实现了没人用,都不算一个完整的能力。
它的威力:因为 Consumer 只依赖 Definition,从不 import 具体 Provider,所以你换一个 Provider,整条产品线跟着变。
官方举的例子(docs/architecture.md:129)特别能说明问题:文件系统和子进程共享同一个执行世界,所以你把ctx.fs和ctx.subprocess指向一个远程沙箱 ——Bash 工具、PTY 终端、LSP 语言服务器会一起跟着搬过去,你不需要 fork 任何一个 Provider。
用乐高的话说:你换的不是「某块积木」,而是「某一种积木接口的实现」,所有卡在这个接口上的零件一起升级。
1.9 两个常见误解
误解一:dsh 是一个现成的聊天机器人
不是。dsh 是造机器人的工作台。你拿到的是一个底座 + 一大箱零件 + 一张装配图。
当然它也自带了几个装好的成品(web/headless/sdk等 Profile),开箱能跑。但它的价值不在「能跑」,在「能改」。
误解二:dsh 是 LangChain 那种编排框架
不是。LangChain 类框架的核心是「你写代码把步骤串起来」——Chain、Graph、Router 都是你显式定义的流程。
dsh 的核心不是流程编排,而是能力装配:你把「有哪些能力、每个能力用哪个实现」配好,Agent 主循环(那个插件)自己去跑。流程不是你画的,是那块叫agent-loop的积木自带的。
所以:
- 想改能力(换个模型、加个工具、换掉文件系统的实现)→ 改配置 / 挂插件
- 想改流程(turn 怎么走、什么时候停)→ 那才需要动
agent-loop,或者换掉它
动手试试
练习 1打开docs/cordis-primer.md,读「Cordis In Five Ideas」那一节(只有十几行)。对照本篇的讲解,看看官方的英文表述和你理解的是否一致。这一步会帮你建立「中文理解 ↔ 英文术语」的映射,以后查文档会快很多。
练习 2回到你上一节跑过的--dump-config输出,随便找三行,想一想:
- 这一行如果换成别的实现,会影响谁?
- 如果把这一行删掉,哪些插件会因为
inject不满足而卸载?
想不清楚很正常,第 2 篇讲完分层你就明白了。
本篇小结
- dsh 是乐高,不是一体成型玩具。连 Agent 主循环都是一块可换的积木。
- 五条核心原理:① 一切皆插件 ②
ctx是服务仓库 ③ 依赖靠inject声明、顺序自动推导 ④ 类型化事件是扩展点(waterfall用于拦截改写) ⑤ 注册即效应、卸载即回滚(魔术贴,不是 502)。 - 三条设计理念:没有特权核心 / 模型可见即已落日志 / 一切从配置可替换。
- 能力接缝三件套:Service Definition(插座标准)+ Provider(插头厂商)+ Consumer(电器)。换 Provider 就换掉整条产品线。
- dsh不是现成聊天机器人,也不是流程编排框架 —— 它是能力装配工作台。
下篇预告
你现在知道了 dsh 的「游戏规则」:一切皆插件、挂在ctx上、可换可撕。
但打开仓库一看 —— 250 个包,56 个分组。这些包都是什么?谁是地基,谁是脊梁,谁是可以随手换的零件?
👉[第 2 篇:整体组成与模块职责]—— 我们拿一张餐厅的岗位表,把这 250 个包理清楚。