丁一樣改 Agent 行為)
DeepSeek Harness 配置與 Patch像打補(bǔ)丁一樣改 Agent 行為上一篇結(jié)尾說「改行為而不碰源碼」。這一篇就講這個(gè)——Harness 的配置與 Patch 體系。它是整個(gè)框架「可組裝、可替換」承諾的落地機(jī)制你想改默認(rèn)模型、換存儲(chǔ)后端、加一個(gè)工具、關(guān)掉某個(gè) provider都不需要 fork 源碼改一個(gè) YAML 文件就夠了。但要真正用好得先分清它其實(shí)有兩套配置體系很多人一開始就栽在這里。一、兩套配置體系別混為一談Harness 里叫「配置」的東西有兩套職責(zé)完全不同組合配置cordis.patch.yml決定運(yùn)行時(shí)組裝成什么樣——加載哪些插件、每個(gè) seam 選哪個(gè) provider。它管的是「結(jié)構(gòu)」。用戶設(shè)置Settings seam決定用戶可編輯的那部分行為——某個(gè) namespace 下的具體值。它管的是「數(shù)值」而且是面向「用戶在界面或配置頁里能改」的那一子集。官方對二者的邊界說得很清楚組合配置留在cordis.yml一個(gè) namespace 只攜帶用戶可編輯的子集。換句話說——「換哪個(gè)存儲(chǔ)后端」是組合配置的事「這個(gè)后端的連接串」才是 Settings 的事。二、cordis.patch.yml兩種操作一個(gè)鐵律cordis.patch.yml里能做的操作就兩種insert往樹里加新行新插件、新服務(wù)id 尋址覆蓋用穩(wěn)定的id找到某一行替換它的配置。每一行都有兩個(gè)字段穩(wěn)定的id跨層尋址用和name要加載的 npm 包。id 是「誰被覆蓋」的錨點(diǎn)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 體系里最重要的一條鐵律記不住會(huì)踩大坑一個(gè)后層 patch 用 id 命中某一行時(shí)替換的是那一行的整個(gè)config而不是深合并。也就是說如果你想覆蓋某個(gè)行必須把那一行「自己擁有的每個(gè) key」都重述一遍。少寫一個(gè) key它不會(huì)「保留之前的默認(rèn)值」而是直接沒了。官方反復(fù)強(qiáng)調(diào)patch 替換目標(biāo)行的整個(gè) config沒有 deep-merge。這條規(guī)則的代價(jià)是「覆蓋要寫全」但好處也明顯每一行最多屬于一個(gè) bundle 層 用戶覆蓋層。于是「這個(gè)值到底是誰定的」永遠(yuǎn)可查、可審計(jì)——不會(huì)出現(xiàn)「六個(gè)插件各自往同一個(gè)對象里 merge 一點(diǎn)點(diǎn)最后誰也不知道完整值從哪來」的亂象。四、bundle layer ordering誰后疊誰贏上一篇已經(jīng)埋過伏筆這里展開。層疊順序決定了「誰覆蓋誰」dsh.profile.bundles里每個(gè) bundle 的 patch按聲明順序profile 自己的cordis.patch.ymlhome 級(jí)的$DSH_HOME/cordis.patch.yml--patch命令行覆蓋層。順序的意義在于你的 bundle 的 patch 應(yīng)用在它之前所有 bundle 之后所以它能覆蓋前面任何一個(gè) bundle 配過的行。而后面的 profile / home /--patch又能覆蓋你的 bundle。這是一條清晰的「誰后誰贏」鏈。一個(gè)典型的應(yīng)用base 層把一個(gè)「按 mode 不同而不同」的行留白由各 mode bundle 去填各自完整 config——這樣「不同形態(tài)不同默認(rèn)值」的差異就落在了各自的 mode bundle 里而不是污染 base 層。五、用戶 Settingsnamespace schema 三層解析Settings 這邊更「應(yīng)用層」。每個(gè)插件可以用ctx.settings.register(ns, schema, options)注冊一個(gè)namespace得到一個(gè)SettingsScopeconstscopectx.settings.register(my-plugin,schema,{base:{timeout:3000},// 組合層默認(rèn)值applies:live,// live 即時(shí)生效 / restart 重啟生效validate:(v){/* schema 表達(dá)不了的交差校驗(yàn) */},})awaitscope.update({timeout:5000})// 合并稀疏 patch 到用戶層awaitscope.replace({timeout:5000})// 整段替換reset 路徑scope.watch((next,prev){/* 觀察變更 */})解析值的順序永遠(yuǎn)是schema 默認(rèn)值 → 組合base→ 用戶層。三個(gè)寫接口語義不同別用錯(cuò)update(patch)把稀疏 patch合并進(jìn)用戶層只改你給的 keyreplace(section)整段替換用戶層缺的 key 回落到base和 schema 默認(rèn)——這是「刪除/重置」路徑replace({})等于全重置mutate(ops)路徑尋址編輯{op:set|unset, path}給「只拿到脫敏視圖、無法重建整段」的調(diào)用方用避免誤刪沒見過的 secret 字段。每次提交還會(huì)發(fā)settings/updated (ns, next, prev, source)事件source區(qū)分是進(jìn)程內(nèi)update還是外部provider編輯。validate是 schema 表達(dá)不了的交差校驗(yàn)比如「這個(gè) provider profile 我服務(wù)不了」在寫的時(shí)候就拒絕而不是存一個(gè)會(huì)靜默禁用主人的值。六、避坑清單整行替換不是深合并。覆蓋一行要把它擁有的 key 寫全別指望少寫的 key 能「繼承默認(rèn)」。兩套配置別搞混。「換哪個(gè)后端」寫 patch「后端連哪」寫 Settings放錯(cuò)位置要么不生效要么語義錯(cuò)亂。replace會(huì)重置沒寫的 key。只想改一個(gè)字段用update或mutate別圖省事replace只傳一個(gè) key否則其余字段全回落默認(rèn)。secret 字段靠mutate保護(hù)。任何 wire 界面必須redactSecrets: true拿到脫敏視圖的調(diào)用方只能用mutate按路徑改用replace會(huì)靜默刪掉它從沒見過的 secret。層疊順序決定覆蓋力。要臨時(shí)改用--patch要持久改寫 home 級(jí) patch別去改內(nèi)置 bundle 的源文件。小結(jié)Patch 體系是 Harness「去鎖死、可替換、可組合」這一價(jià)值觀的機(jī)械實(shí)現(xiàn)。它用「id 尋址 整行替換 last-write-wins」換來一個(gè)極其可貴的性質(zhì)最終配置永遠(yuǎn)可被精確歸因。你想知道「這個(gè)模型是從哪來的」順著層疊鏈一查就知道而不是對著一個(gè)被 merge 了 N 次的運(yùn)行時(shí)對象干瞪眼。下一篇我們往「運(yùn)行起來之后」看——配置改好了、Agent 跑起來了怎么知道它到底干得怎么樣這就是可觀測性與 OpenTelemetry 遙測。參考deepseek-ai/deepseek-harness 官方倉庫docs/subsystems/settings.md、apps/cli/README.md。