
DeepSeek Harness 配置与 Patch像打补丁一样改 Agent 行为上一篇结尾说「改行为而不碰源码」。这一篇就讲这个——Harness 的配置与 Patch 体系。它是整个框架「可组装、可替换」承诺的落地机制你想改默认模型、换存储后端、加一个工具、关掉某个 provider都不需要 fork 源码改一个 YAML 文件就够了。但要真正用好得先分清它其实有两套配置体系很多人一开始就栽在这里。一、两套配置体系别混为一谈Harness 里叫「配置」的东西有两套职责完全不同组合配置cordis.patch.yml决定运行时组装成什么样——加载哪些插件、每个 seam 选哪个 provider。它管的是「结构」。用户设置Settings seam决定用户可编辑的那部分行为——某个 namespace 下的具体值。它管的是「数值」而且是面向「用户在界面或配置页里能改」的那一子集。官方对二者的边界说得很清楚组合配置留在cordis.yml一个 namespace 只携带用户可编辑的子集。换句话说——「换哪个存储后端」是组合配置的事「这个后端的连接串」才是 Settings 的事。二、cordis.patch.yml两种操作一个铁律cordis.patch.yml里能做的操作就两种insert往树里加新行新插件、新服务id 寻址覆盖用稳定的id找到某一行替换它的配置。每一行都有两个字段稳定的id跨层寻址用和name要加载的 npm 包。id 是「谁被覆盖」的锚点name 是「加载什么」。# 概念示意patch 的两种操作rows:-id:system-prompt# insert 或 override 都靠 id 定位name:deepseek-ai/dsh-system-promptconfig:persona:You are a coding agent…三、last-write-wins整行替换不深合并这是 Patch 体系里最重要的一条铁律记不住会踩大坑一个后层 patch 用 id 命中某一行时替换的是那一行的整个config而不是深合并。也就是说如果你想覆盖某个行必须把那一行「自己拥有的每个 key」都重述一遍。少写一个 key它不会「保留之前的默认值」而是直接没了。官方反复强调patch 替换目标行的整个 config没有 deep-merge。这条规则的代价是「覆盖要写全」但好处也明显每一行最多属于一个 bundle 层 用户覆盖层。于是「这个值到底是谁定的」永远可查、可审计——不会出现「六个插件各自往同一个对象里 merge 一点点最后谁也不知道完整值从哪来」的乱象。四、bundle layer ordering谁后叠谁赢上一篇已经埋过伏笔这里展开。层叠顺序决定了「谁覆盖谁」dsh.profile.bundles里每个 bundle 的 patch按声明顺序profile 自己的cordis.patch.ymlhome 级的$DSH_HOME/cordis.patch.yml--patch命令行覆盖层。顺序的意义在于你的 bundle 的 patch 应用在它之前所有 bundle 之后所以它能覆盖前面任何一个 bundle 配过的行。而后面的 profile / home /--patch又能覆盖你的 bundle。这是一条清晰的「谁后谁赢」链。一个典型的应用base 层把一个「按 mode 不同而不同」的行留白由各 mode bundle 去填各自完整 config——这样「不同形态不同默认值」的差异就落在了各自的 mode bundle 里而不是污染 base 层。五、用户 Settingsnamespace schema 三层解析Settings 这边更「应用层」。每个插件可以用ctx.settings.register(ns, schema, options)注册一个namespace得到一个SettingsScopeconstscopectx.settings.register(my-plugin,schema,{base:{timeout:3000},// 组合层默认值applies:live,// live 即时生效 / restart 重启生效validate:(v){/* schema 表达不了的交差校验 */},})awaitscope.update({timeout:5000})// 合并稀疏 patch 到用户层awaitscope.replace({timeout:5000})// 整段替换reset 路径scope.watch((next,prev){/* 观察变更 */})解析值的顺序永远是schema 默认值 → 组合base→ 用户层。三个写接口语义不同别用错update(patch)把稀疏 patch合并进用户层只改你给的 keyreplace(section)整段替换用户层缺的 key 回落到base和 schema 默认——这是「删除/重置」路径replace({})等于全重置mutate(ops)路径寻址编辑{op:set|unset, path}给「只拿到脱敏视图、无法重建整段」的调用方用避免误删没见过的 secret 字段。每次提交还会发settings/updated (ns, next, prev, source)事件source区分是进程内update还是外部provider编辑。validate是 schema 表达不了的交差校验比如「这个 provider profile 我服务不了」在写的时候就拒绝而不是存一个会静默禁用主人的值。六、避坑清单整行替换不是深合并。覆盖一行要把它拥有的 key 写全别指望少写的 key 能「继承默认」。两套配置别搞混。「换哪个后端」写 patch「后端连哪」写 Settings放错位置要么不生效要么语义错乱。replace会重置没写的 key。只想改一个字段用update或mutate别图省事replace只传一个 key否则其余字段全回落默认。secret 字段靠mutate保护。任何 wire 界面必须redactSecrets: true拿到脱敏视图的调用方只能用mutate按路径改用replace会静默删掉它从没见过的 secret。层叠顺序决定覆盖力。要临时改用--patch要持久改写 home 级 patch别去改内置 bundle 的源文件。小结Patch 体系是 Harness「去锁死、可替换、可组合」这一价值观的机械实现。它用「id 寻址 整行替换 last-write-wins」换来一个极其可贵的性质最终配置永远可被精确归因。你想知道「这个模型是从哪来的」顺着层叠链一查就知道而不是对着一个被 merge 了 N 次的运行时对象干瞪眼。下一篇我们往「运行起来之后」看——配置改好了、Agent 跑起来了怎么知道它到底干得怎么样这就是可观测性与 OpenTelemetry 遥测。参考deepseek-ai/deepseek-harness 官方仓库docs/subsystems/settings.md、apps/cli/README.md。