学 AI 产品 · 专业 AI 产品经理播客第 4 章 · T4 解剖 DeepSeek Harness:一切皆插件的 Agent 底座 · EP 02
第 4 章 · EP 02

Profile / Bundle / Patch:用户能把产品改到什么程度

时长 15:28音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场 · 不改源码能改到多深1:55
1:55三个名词 · 捆绑包、组装档、补丁1:31
3:26四层,每层一个归属方2:03
5:30层序写死在一个数组里1:27
6:58命中就整条替换2:24
9:22三条行为边界1:26
10:48横向对比 · 三家怎么做分层2:16
13:04一次手推1:02
14:07可带走的原则1:20
解读全文

ep34 · Profile / Bundle / Patch:用户能把产品改到什么程度

本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版
模块:T4 解剖 DeepSeek Harness:一切皆插件的 Agent 底座
来源:xueai.miyang.cn(小山学堂 · 洛小山)

一句话速览

不改源码也能换掉深层能力:配置由四层 patch 对着空数组叠出来,晚应用的赢,命中同一条就整条替换。

核心判断:可配置性的上限不是开关有多少,而是用户能否在不读源码的前提下,说清现在生效的是哪一行、它从哪一层来。DSH 把账算在了可预测性这一边 —— 代价是写 patch 时要重述想保留的字段。


自测三问 · 你的产品可配置性打几分

评估任何配置系统,都可以用这三个问题:

  1. 用户想换掉一个深层能力(如把压缩策略换成自研实现),需要读源码吗?
  2. 升级发行版时,用户能一眼看出哪些改动会被覆盖吗?
  3. 配置出问题时,用户能在不启动的前提下看到真正生效的那一份吗?

三个都答不上来,说明可配置性只是表面上存在。

本集这套系统对三问都有明确答案:不用读源码(一条 insert 即可换实现);升级不覆盖用户层(层是分开的);离线转储能看到真正生效的那一份。

能力地图

三个名词

名词归属方实体能做什么
Bundle发行版npm 包里的 patch 列表给默认(base / web-app / headless)
Profile这套组装$DSH_HOME/profiles/<name>/ 目录排 bundle 顺序 + 自带 patch
Patch改动最小单位按 id 命中条目改配置 / 禁用 / insert 新条目

树外插件也从 patch 进来:dsh plugin add 装进 profile 后,它就是普通的一层 patch。

四层叠放顺序(晚应用的赢)

#层跟着谁走说明
1Bundle发行版内置 base / web-app / headless
2Profile这套组装manifest 排 bundle 顺序
3Home这台机器对每个 profile 都生效,压过 Profile 层
4--patch这一次命令一次性实验,可重复传多份

起点真的是空数组:根配置文件内容就是 [],模板注释直接写着「别改这个文件,去改 patch 文件」。所有实际内容都由 patch insert 进来。

出处:apps/cli/src/profile-boot.ts L60–64;Home 压过 Profile 的理由见 packages/boot/app-boot/README.zh.md L43;--patch 可重复传见 apps/cli/src/args.ts L132;热重载见同包 watchUserPatches。


设计思路一 · 每一层配置都有归属方

问题

反面做法:一个大配置文件,发行版默认、组装定制、个人偏好、临时实验全写在里面。三个月后升级发行版,新默认和旧改动搅在一起 —— 说不清哪一行是谁写的、哪一行能动,升级变成手工比对。

更日常的翻车:临时试一个实验模型,顺手改了配置忘了改回来,第二天整套环境跑的都是实验配置。

根源:改动没有归属 —— 谁写的、跟着什么走、什么时候该消失,一个文件说不清这三件事。

提示1 · 层序写死在一个数组里

层序在源码里就是一个数组字面量:启动器把组合好的 profile 摊平成一个 patch 数组,数组顺序就是应用顺序。这段函数短到能当金句看 —— 四层的先后被写死在一个数组里,没有任何条件分支。出处:apps/cli/src/profile-boot.ts L121–129。

为什么这处漂亮:优先级是配置系统里最容易长歪的地方。一旦允许按条件调整顺序(如某环境下 Home 层提前),同一份配置在不同机器上结果不同,而用户完全看不出原因。写死一个数组 = 放弃灵活性,换来确定性。

为什么长期成立

分层覆盖是配置系统的通则:CSS 的层叠、systemd 的 drop-in 目录、编辑器里用户设置压过默认设置,全是同一个结构。只要一份产品同时被发行方、团队、个人、单次命令四种角色修改,层就必须分开。


设计思路二 · 命中就整条替换

直觉答案 vs 实际选择

直觉:deep-merge实际:整条替换
写法只写想改的字段必须重述要保留的字段
删除表达不了删除天然支持
排查结果和任何文件字面都对不上字面即生效
成本省事啰嗦

deep-merge 的两个代价:

  1. 合并表达不了删除 —— 下层设了字段,上层想去掉,merge 语义下没有这个动作,只能覆盖成别的值;
  2. 结果与字面脱节 —— 排查时得在脑子里把所有层的合并算法跑一遍。

机制

patch 按 id 命中目标条目后,把 patch 里除 id 之外的每个顶层键直接赋值过去。config 是一个顶层键,所以旧 config 对象被整个换掉,里面的字段一个都不保留。

官方文档明说的已知限制,原话:「profile 覆盖必须重述需要保留的组合包字段」。

出处:替换语义在 vendor/include/src/index.ts L110–124;官方说明在 packages/boot/app-boot/README.zh.md L60。

为什么还要选它

成本性质不一样:

  • 合并语义的代价落在排查阶段,且持续发生 —— 每次配置不符合预期,用户都要在脑子里跑一遍合并算法;
  • 替换语义的代价只发生在写的时候,且有形成本 —— 重述一遍字段而已。

一个是持续的无形成本,一个是一次性的有形成本。

官方文档把这条明说是已知限制,态度也值得学:不藏着、不包装成特性,直接写一句「必须重述」。这比给一个看起来聪明、实际难排查的语义要诚实。

提示2 · 最容易踩的反直觉结果

Home 层只写了 model 一个字段,下面两层设好的 provider 和 temperature 就被刷没了,而且没有任何警告,只有结果不对。

用户会以为自己只改了一个字段,实际上删掉了两个。排查时日志里什么都没有 —— 从系统角度看这是一次完全合法的替换。

提示3 · 三条行为边界

情况行为危险度
patch 指向不存在的 id只警告不报错,一行 stderr 后跳过高(静默没生效)
patch 文件为空/只有注释直接抛异常(解析结果不是列表)低(至少会告诉你)
想让某层什么都不做写 [],不要留空文件—

第一条与替换语义凑在一起最危险:你以为覆盖了,其实打空了。


回报 · 唯一算法带来的可预测性

提示4 · 转储里看到什么,改配置就能得到什么

这个算法全库只有一份:挂载用它,dsh --dump-config 的离线合成也用它 —— dump 出来的结果和真正启动的内容不可能漂移。

算法的输入永远不被改动、结果永远是深拷贝,这样配置热重载时撤掉一条 patch 才能真的还原,早先的值不会被烤进缓存。出处:vendor/include/src/index.ts L43–52。

配套:一份 3152 行的生成文档 docs/config-catalog.zh.md,把每个可加载包的 config 类型原样列出来 —— 想知道某条 patch 能写哪些键,查这份目录即可。


横向对比 · 三家怎么做配置分层

分层对象合并语义层序
DeepSeek Harness插件条目级整条替换Bundle → Profile → Home → --patch
Claude Code设置字段级字段级合并user / project / local / flag / policy 五层
Grok BuildTOML 字段级deep mergesystem_managed / managed / user

两处对比焦点

  1. 合并语义 —— 后两家字段级合并,写起来省事;DSH 条目级替换,写起来啰嗦,换来 dump 与文件字面一致的可预测性。
  2. 分层对象 —— 那两家分的是设置字段,DSH 分的是插件树本身。所以用户能改到的深度不一样:一条 insert 就能把第三方 compaction 实现接进主循环,这在前两家的配置系统里没有对应物。

别人有而这边没有的

Claude Code 有管理员策略层和只在 managed 来源生效的锁定字段,DSH 目前没有等价机制。企业场景下团队想锁死某个能力时,缺的就是这个。(基于已公开材料)

提示5 · 缺口的形状:覆盖 ≠ 锁定

四层覆盖解决的是「谁能最后说话」,没有解决「谁能禁止别人说话」。

  • 覆盖是叠加的权力 —— 沿层序往后追加;
  • 锁定是截断的权力 —— 独立于层序之外。

一个只支持覆盖的系统天然做不到强制合规:最上层的 --patch 永远能盖掉下面所有层。要做锁定,必须引入一个独立于层序的维度(如受管来源白名单 + 锁定字段表),而不是再加一层。


审查清单

写 patch 前逐条过:

  • 这条改动应该归到哪一层?(发行版 / 组装 / 本机 / 单次命令)
  • 想保留的字段有没有重述?(替换语义不保留任何旧字段)
  • 命中 id 是否真实存在?(不存在只警告,会静默跳过)
  • 想让某层空转,写的是 [] 还是留了空文件?
  • 改完有没有跑一次 --dump-config 验证字面结果?
  • Home 层与 Profile 层是否改到了同一个 id?(Home 会整条压过 Profile)
  • 是否需要团队级锁定?(当前缺等价机制,需另想办法)

排查路径

  1. 改了没生效 → 先查 patch 的 id 是不是打空了(只警告不报错)。
  2. 字段莫名其妙消失 → 上层某条 patch 命中同一 id 触发了整条替换。
  3. 两边结果不一致 → 跑 --dump-config 看字面结果,不要靠启动验证。
  4. 热重载后没还原 → 确认算法输入未被改动(应是不可变 + 深拷贝)。
  5. 想知道能写哪些键 → 查 docs/config-catalog.zh.md。

课堂练习 · 手推一次两层冲突

Profile 层写:


id: conversation-model
config: { provider: deepseek, model: v3.2, temperature: 0.2 }

Home 层写:


id: conversation-model
config: { temperature: 0 }
  1. 按整条替换语义写出最终条目的 config,指出哪些字段消失了、会不会有任何警告。
  2. 把这两条 patch 对调所在层,结果变成什么?
  3. 用 dsh --dump-config 的思路说明怎么在不启动的情况下验证答案。
答案要点:最终 config 只剩 temperature: 0,provider 与 model 消失且无警告。对调后失败方向从「丢字段」变成「改不动」—— Home 那条被 Profile 覆盖,用户想改的温度反而没生效。

约束说明

  1. 取材约束:本集全部内容取自小山学堂《学 AI 产品,从入门到精通》对应课节,未跨集取材,未虚构源码行号或产品行为。
  2. 题库隔离:题库页(quizFiles)一律不进入口播稿,仅作为集页下方的文字自测卡渲染。
  3. 源码时效:依据本地仓库 deepseek-harness-master,核对日期 2026-08-13;横向对比部分核对时间为 2026-08-22。
  4. 演示说明:原文交互演示中的条目标识与字段做了教学化简化,叠层顺序与替换语义对应真实源码;deep-merge 对照模式为教学假设,DSH 未实现该语义。
  5. 解读边界:本文为二次演绎的解读稿,用于配合音频理解;具体行为以实际运行版本为准。

Takeaway

DSH 的配置是四层 patch 对着空数组刷出来的,顺序是 Bundle、Profile、Home、--patch,晚应用的赢。patch 命中同一 id 时整条替换 config,不做字段合并,想保留的字段必须重述。同一个算法既管挂载也管 dump,所以 --dump-config 看到什么,改配置就能得到什么。


*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*