课程总纲 / 理念急救包
急救

三个实验,亲眼看见那三个理念

你读完了文字但没懂——正常。这一页不讲任何新知识,只让你动手看见。

阅读 8 分钟 + 动手 20 分钟零代码Windows PowerShell 可跑
为什么读完了还是没懂:你看到的是产品(聊天框、回复),课程讲的却是架构(插座板、账本、零件)——中间隔着"亲眼所见"。三个实验,每个 5 分钟产出一个肉眼可见的结果;做完再翻 L04–L07,那些话就有画面了。

先背下这三句,实验就是给它们配画面Three Lines First

理念课程里的话实验将让你看到
一切皆插件"模型、工具、界面全是插件,没有特权核心"一整棵插件树——界面和模型躺在同一张清单里
日志是唯一事实源"模型看到的一切都记在一本只追加的账上"你说一句话,磁盘上的文件立刻变大
能力可整体替换"拔掉一个插件,系统照常活"你亲手拔掉一个工具,dsh 没崩,模型少了个本事

实验一 · 看见插件树(5 分钟)See the Tree

目标:证明"你以为的一个产品,其实是一张插件清单"。

第 1 步 · 打印你这台机器真实启动的配置树

在仓库根目录的终端里(源码方式)跑:

pnpm dsh web --dump-config

如果你用的是免安装方式(npx),则跑:

npx @deepseek-ai/dsh web --dump-config

第 2 步 · 在输出里找这四个名字

终端会刷出一大坨带注释的 YAML。别试图读懂全部——只找这四个词出现的位置(眼睛扫,或复制到编辑器里 Ctrl+F):

找什么它是什么
llm 相关行模型调用——是插件
tool- 开头的行模型手里的工具(读文件、跑命令……)——是插件
session 相关行会话存储——是插件
web 相关行你现在用的这个网页界面——也是插件

第 3 步 · 读一下行尾注释

注意每一行上方的 # == 包名 注释——dump 输出会标明每行来自哪个包。你现在用的整个产品,就是这些包一层层叠出来的。

你刚看到了什么:没有"主程序",只有一张清单。聊天界面和读文件的工具在这张清单里地位完全平等——这就是"一切皆插件"。你原以为买了一台整机,拆开一看,是一盒积木。

实验二 · 看见账本(5 分钟)See the Ledger

目标:证明"你和模型的每一句对话,都被记进磁盘上的一本账"。

第 1 步 · 记下当前最新的几个文件

dsh 把所有数据存在主目录(默认 ~\.dsh)。在 PowerShell 里跑:

Get-ChildItem ~\.dsh -Recurse -File -ErrorAction SilentlyContinue |
  Sort-Object LastWriteTime -Descending |
  Select-Object -First 5 FullName, Length, LastWriteTime

记下最上面那个文件的名字和大小。

第 2 步 · 去 Web UI 说一句话

打开 dsh 的 Web 界面,随便问一句,比如 你好,介绍一下你自己。等它答完。

第 3 步 · 再跑一遍第 1 步的命令

对比:是不是出现了新的文件,或者某个文件的大小和修改时间变了?

你刚看到了什么:你说的那句话、模型的回答,在你按下回车的瞬间就被逐条写进了磁盘上的事件日志。注意分辨两样东西:文件里存的不是"一份对话历史",而是一条条更原始的事件(谁说了什么、调了什么工具、返回了什么)。界面上的聊天气泡、发给模型的历史消息,都是把事件从头回放一遍、现场拼出来的画面——事件被保存了,"对话历史"这个东西从未被保存,它是事件的视图。这就是"日志是唯一事实源"。类比银行:银行不保存你的余额,只保存每一笔流水,余额是算出来的。你在界面上看到的一切,都是这本流水的回放。
追问:重开界面还能看到之前的对话,这不就是"保存"吗?算保存——磁盘上确实存了东西(所以重启不丢),但存的始终是事件日志文件,不是"对话记录"。重开界面时 dsh 做四件事:读日志 → 从头回放 → 逐条翻译成气泡 → 渲染。你看到的历史,是后两步现场拼出来的成品。好比数据库视图:每次 SELECT 都看得到数据,不是视图存了数据,是基表在、查询在跑。也正因存的是原料,同一本日志能拼出屏幕气泡、发给模型的消息、会话分叉、全文检索等多种视图,且永远一致——这正是"唯一事实源"的价值;若直接存"对话记录"成品,每种用途各存一份,就会出现界面与模型所见不一致这类 bug。
打开那个文件全是乱码?正常——日志默认用 Zstandard 压缩存储,不是给人直接看的。乱码不影响本实验的结论:"文件在长"本身就证明了账本在记账。

实验三 · 拔掉一个插头(10 分钟,高潮)Unplug It

目标:证明"没有哪个零件是焊死的"——你对一个正在运行的产品做减法,它照常活着。

第 1 步 · 从实验一的输出里挑一个"锦上添花"的工具

回到 dump-config 的输出,挑一个 tool- 开头的、可有可无的行(比如网页搜索类、待办类工具)。记下它的 id

别拔这三个:名字带 fs(文件)、edit(编辑)、bash/pwsh(终端)的是模型干活的命根子,拔了实验场面会很难看。挑一个你平时用不上的。

第 2 步 · 写三行"拔插头"补丁

在仓库根目录建 unplug.yml(把 id 换成你选的):

- id: 你选的那个tool的id
  disabled: true

第 3 步 · 先验货再拔

pnpm dsh web --patch ./unplug.yml --dump-config

在输出里找到那一行:它现在带着"被 overlay 修改过"的注释。这就是 patch——在产品之上叠一层你的意愿,产品本身一个字节没动。

第 4 步 · 真的拔掉,启动它

pnpm dsh web --patch ./unplug.yml

dsh 正常启动,界面正常,对话正常。然后问模型一个必须用那个工具的问题,比如拔的是搜索就问:

帮我搜一下今天的新闻

看它怎么办:它会明确告诉你它没有这个工具(或者用别的笨办法绕)。

第 5 步 · 把插头插回去

去掉 --patch 正常启动,工具回来了。你刚才没有"重装"任何东西——那层补丁只是盖在上面,掀掉就还原。

你刚看到了什么:一个"产品功能"被你用三行 YAML 摘除,又完好无损地装回。这就是"能力可整体替换"——也是官方自己的用法(dsh 的 Web 发行包里就有 40 多处一模一样的 disabled: true 写法)。不是所有车都敢让你在行驶中换零件。这一台,零件还带快拆扣。

实验做完了,现在重读那四课Re-read With Eyes

带着刚才的画面,每课 10 分钟:

重读现在你会看到什么
L04 一切皆插件文字描述的就是实验一那张清单
L05 Cordis 五要素ctx = 每个插件手里的"延长线",一头连着全机共享的服务注册表(插座板);你拔插头发生在配置检查环节,inject 管的是幸存插件之间的加载先后——详见下方"展开阅读"
L06 能力缝三角色你拔掉的 tool-* 行是 Consumer;它背后"定义/实现"的那两层你还留着——所以只失去一个本事,系统不崩
L07 日志唯一事实源实验二里变大的那个文件

如果某一课读到这里还是卡,告诉我卡在哪一句——我直接对那一句给你做拆解,比通读快得多。

展开阅读一:ctx / inject / 拔插头,三者的精确分工ctx vs inject

三个词经常被搅在一起,用 Spring 的概念一次分清(你有后端背景,这个对照一秒就通):

准确身份Spring 世界对应
插座板(服务注册表)全机共享的一张表,每个服务认领一个名字ApplicationContext 本身
插座(服务名)tools / llm / sessions 这些 keyBean 名字
ctx每个插件手里的一根延长线:插别人的插座(消费)、自己开新插座(供电)、挂效果(注册即撤销)你这个 bean 拿到的容器视图
inject"我必需这些插座有电,没电别启动我"@Autowired

所以 ctx.toolsgetBean("tools")——按名字取,不 import 具体实现,这就是实现可整体替换的根子。

"谁等谁"的启动时间线

开机
 ├─ ① 组层:bundles + patches 叠成一张行清单    ← 实验一看到的
 ├─ ② 逐行查 disabled → 摘掉,模块根本不加载    ← 实验三拔插头发生在这里
 ├─ ③ 加载幸存的行,读出各自的 inject
 ├─ ④ 排期:依赖没就绪的插件等着               ← inject 生活在这里
 └─ ⑤ 轮到谁就调谁的 apply(ctx)

② 是"去不去"(配置说了算),④ 是"先后"(依赖推导)——两码事。一条真实的依赖链:

dsh-bash-local(Provider)  apply 跑完 → ctx.shell 插座有电
        ↓ 框架发现 tool-bash 的 inject ['shell'] 已满足
dsh-tool-bash(Consumer)   apply 才被调用 → 里面放心用 ctx.shell

加载顺序没人手工排,框架读 inject 自动推导(同 Spring 解析 bean 依赖图)。拔插头也会牵动"谁等谁":拔 Consumer(工具行)无人问津;拔 Provider(如 bash-local),所有 inject 它的插件会一起起不来——实验三让你挑"可有可无的工具",就是在避开这条连锁。

展开阅读二:插件 / patch / bundle / profile——四个名词,一场生命周期Four Nouns, One Lifecycle

别背定义。回到 dump-config 的输出——四个名词各指图上的一个东西:

# == dsh-base            ← 一层,来自叫 dsh-base 的 npm 包(= 一个 bundle)
- id: llm-deepseek       ← 一行,一个插件
- id: tool-fs            ← 一行,又一个插件
...(共几十行,全在这一层里)

# == dsh-web-app         ← 又一层(= 另一个 bundle)
- id: web-ui             ← 一行,一个插件
名词指什么磁盘上是什么
插件清单里的一行一个 .ts 文件(name/inject/apply)
bundle清单里的一整层(一个 # == 分节)node_modules 里一个包:若干 .ts + 一份 yml,package.json 带 dsh.bundle 标记
patch"往清单加行 / 按 id 改行"的指令文件一个 .yml 文件
profile整张清单的组装方案(哪几层、什么顺序)~/.dsh/profiles/<名>/ 一个目录

一句话:行叫插件,层叫 bundle。你 node_modules 里躺着的 dsh-base 文件夹,打开看是几十个插件 + 一份 yml——叫它"插件"不准(里面是几十个),叫它"普通依赖"也不准(普通依赖 dsh 不理,它却要被叠层),所以造了 bundle 这个词。困惑的根源通常是以为"装一个包 = 得到一个功能",而 dsh-base 一个包给了你几十个功能——装的单位和功能的单位不是一比一

从一段代码到别人装上,四步各出场一次

① 写 my-plugin.ts                        → 此刻叫"插件"
② 写 cordis.patch.yml(往清单插一行)      → 这份 yml 叫"patch"
③ npm publish 打包(ts+yml+标记)          → 这个包叫"bundle"
④ 同事 dsh plugin add 你的包             → 他的 profile 多一层
一句话锁死:行叫插件,层叫 bundle;patch 是改行的指令,profile 是层的组装方案。你写的是行(插件),装的是层(bundle)。
Maven/Spring 类比校准(易错处):bundle ≈ Maven 模块(可发行、含多个插件,不是微服务——bundle 全叠进同一棵树同一进程);插件 ≈ 模块里一个 @Component 类;patch ≈ 你自己写的 application.yml——不是"作者预留的可编辑配置"!作者无需知情、无需配合,覆盖机制对一切行生效,且能力不止改值:还能按 id 换实现插新行;profile ≈ 一个应用的完整依赖清单 + 配置组合。bundle 作者出厂时写的那份接线 yml,只是他的默认组装,你的 patch 叠其上、后到者胜。
📝 对答案(点击展开)
1. 实验一证明了什么?证据是什么?
"一切皆插件"。证据:dump-config 打印的整棵配置树里,模型调用、工具、会话存储、Web 界面是同一张清单上地位平等的条目,每行还标注了它来自哪个包——没有"主程序"这一说。
2. 实验二里,为什么文件内容是乱码也不影响结论?
日志默认用 Zstandard 压缩,本来就不是给人直接看的。实验要证明的是"你说的话立刻被记进磁盘"——文件新增/变大这个事实本身就是账本在记账的铁证,不需要看懂内容。
3. 实验三里,为什么拔掉一个工具 dsh 不会崩?
能力缝三角色:你拔的是 Consumer(模型面前的工具入口),它背后的 Definition 和 Provider 还在,其他插件对它也没有硬依赖(没有 inject 它)。失去的是一个可选本事,不是一根承重梁。
4. --patch 改动产品了吗?
没有。patch 是叠在产品之上的一个覆盖层,按 id 替换目标行(或插入新行);去掉 --patch 启动,一切还原。产品包一个字节没动。
本页依据:CLI reference(dump-config 注释与 patch 语义) · web-app 的官方 patch(40+ 处 disabled 用法) · persistence 文档(日志存储格式)