當 Agent Harness 沒有核心:DeepSeek 的 Everything is a Plugin
讀 DeepSeek Harness(dsh)的原始碼時,我一直在找它的「核心」在哪裡。找 agent loop 的主程式、找 tool registry 的初始化順序、找 web server 的進入點。最後發現這個問題本身問錯了:dsh 沒有核心。model adapter 是 plugin,tool registry 是 plugin,session log 是 plugin,連 agent loop 本身都是一個可以從設定檔換掉的 plugin。
這跟我們熟悉的 Claude Code 路線完全相反。Claude Code 是一個固定的核心,外圍留了 Hooks、Skills、MCP 這些擴展點讓你掛東西(我在之前分析 claw-code 的文章拆過這三層的職責分工)。dsh 則是把「擴展點」這個概念取消了,因為當一切都是 plugin,你不需要擴展點,你只需要組合。
讀完精華版(2 分鐘),你會理解:
- 「沒有特權核心」在工程上是什麼意思,以及
dsh用 35 行設定檔證明了什麼 - Patch 層疊加的組合模型:一個運行中的 harness 是怎麼從四層設定檔疊出來的
- Capability Seam 三角色,以及為什麼換三個 plugin 就能把整個執行環境搬到遠端
- 這條路線和 Claude Code「固定核心 + 擴展點」路線各自的代價
- 一人團隊要自建任務型 agent 時,該從兩條路線各借鑒什麼
這篇不是 dsh 的使用教學,是借它的架構決策,看 agent harness 的另一條設計路線。
精華版
| 維度 | DeepSeek Harness | Claude Code |
|---|---|---|
| 擴展模型 | 一切皆 plugin,沒有核心與外掛的分界 | 固定核心 + Hooks / Skills / MCP 擴展點 |
| 組合方式 | 四層 patch 設定檔疊加出 plugin tree | 核心行為不可換,擴展點掛在預留位置 |
| 換掉一個部件 | 改 patch,換 provider,核心邏輯不動 | 只能換擴展點允許的東西 |
| 換執行環境 | 換 3 個 plugin,Bash / PTY / LSP 整組搬走 | 不在擴展點的範圍內 |
| 設定錯誤 | 載入時就炸(fail loud) | 部分擴展點靜默略過 |
- DeepSeek Harness:整個產品是一棵由設定檔疊加出來的 plugin tree,改行為等於掛一個 plugin 到別人旁邊,沒有任何部件享有「不可替換」的特權。
- Claude Code:一個封閉的核心負責 loop、permission、context 管理,開發者透過 Hooks、Skills、MCP 三種預留的擴展點注入行為,核心本身不開放組合。
幾個關鍵設計問題的簡答:
沒有核心,那開機時跑起來的是什麼? 一份 profile 指定的 bundle 清單。dsh-base 這個 bundle 用約 78 條設定描述 LLM adapter、tools、session、sandbox 等所有 plugin;web 版是在上面多疊一層 424 行的 patch。開機就是把這幾層疊加成一棵 plugin tree 然後全部 mount。
怎麼證明 web 只是其中一層,不是特例? dsh-headless bundle 只有 35 行、插 3 個 plugin,拿掉 web 那層之後,同一個 agent 核心變成無 server、無 port 的一次性任務執行器。同一棵樹,少疊一層而已。
plugin 之間怎麼解耦? Capability Seam:每個能力(fs、shell、subprocess、sandbox)都拆成 Definition(抽象契約)、Provider(實作)、Consumer(使用者)三個角色,Consumer 只認 ctx.fs 這種 key,從不 import 實作。
這樣做的代價是什麼? 你得先學會這套組合語言才能讀懂系統。Claude Code 打開就能用,dsh 要先理解 profile、bundle、patch、seam 這一整套詞彙。
一人團隊要自建任務型 agent,該選哪條路線? 別照搬全組合式。組合式的回報跟產品變體數量成正比,一種形態、一個部署的 agent 付不回這個稅。正確的借法是固定 loop 加上少數刻意保留的 seam,等第二個產品形態出現再升級。
以下是完整版,按需取用。
「沒有特權核心」在工程上是什麼意思?
「沒有特權核心」的意思是:沒有任何程式碼可以繞過 plugin 機制存在,連 agent loop 都是一條可以在設定檔裡替換的 entry。「Everything is a plugin」這種口號很多專案都喊過,多數的實際意思只是「我們有一個 plugin 系統」,dsh 的版本極端得多。判斷標準很具體,看兩件事。
第一,agent loop 能不能換。dsh 的 agent loop(turn / step 驅動器)是一個叫 agents 的 plugin,它在設定檔裡有一條 entry,跟 tool registry、LLM adapter 平起平坐。你可以在 patch 裡把它換成自己的實作,不需要 fork 程式碼。多數系統的 plugin 機制到 loop 這一層就停了,loop 是神聖不可侵犯的主程式。
第二,改行為的方式是什麼。在 dsh 裡,攔截 tool 執行、改寫 LLM 請求、否決一個 step,全部都是「掛一個 plugin 到別人旁邊」:plugin 向共享的 context 註冊 listener,所有註冊都是可逆的 effect,plugin 卸載時自動 unwind。沒有 monkey patch,沒有子類覆寫,也沒有「這段邏輯只有核心能做」。
底層支撐這件事的是 vendored 進 repo 的 Cordis plugin 引擎。plugin 宣告自己依賴哪些 service(inject),缺 service 時停在 PENDING 等待;provider 卸載時,所有依賴它的 plugin 自動跟著卸載,provider 回來再自動重載。載入順序不是 boot 腳本排的,是依賴關係解出來的。
這裡有個容易忽略的細節:dsh 把 Cordis 這 9 個引擎套件的原始碼直接 vendor 進 repo 並改名到自己的 scope 下,至今累積了 18 個記錄在案的本地修改。理由寫得很直白:harness 必須完全擁有自己的引擎層,可稽核、可 patch、可鎖版。對一個「一切都建立在 plugin 引擎上」的產品,引擎本身不能是一個會被上游更新影響的外部依賴。
工程含義:當「改行為 = 掛 plugin」成立,每個行為修改都是一個可以獨立測試、獨立卸載的單元。你要驗證某個攔截邏輯,掛上去測完拆掉就好,不會在核心留下疤痕。
一個運行中的 harness 是怎麼疊出來的?
dsh 開機時做的事,是把四層設定檔依序疊加成一份最終的 plugin 清單:
- profile 列出的每個 bundle 的 patch(base 最先)
- profile 自己的 patch 檔
- 機器本地的 home patch(所有 profile 共用)
- 命令列
--patch指定的 overlay(依參數順序)
web server、API gateway、約 30 個 UI plugin
無 server、無 port
疊加規則以 row 為單位:後層的 patch row 用 id 鎖定前層的既有 row,然後整份 config 替換,沒有 deep merge。想保留前層的某個欄位,你得重寫它。這個決定犧牲了便利性,換來的是每一層 patch 都可以獨立讀懂,你不需要在腦中模擬 merge 演算法才能知道最終值是什麼。
錯誤處理的取向也一致:patch 指到不存在的 id 只是警告,但空的 patch 檔直接 throw(要停用一層得明確寫 [])。同一個能力有兩組 provider 時(例如 Linux 的 bash 家族和 Windows 的 pwsh 家族都註冊同名的 bash service),平台條件寫錯導致兩組同時載入或同時缺席,載入時就炸,不會跑到一半才發現 shell 不存在。設定錯誤的正確爆炸時機是開機,不是第 30 輪 tool call。
三個內建 bundle 的大小對比說明了分層的實際效果:
| Bundle | 規模 | 內容 |
|---|---|---|
dsh-base | 約 78 條 entry | LLM adapters、session 持久化、agent 核心、全套 tools、sandbox、approval |
dsh-web-app | 424 行 patch | 疊在 base 上:web server、API gateway、約 30 個 UI plugin |
dsh-headless | 35 行、3 個 plugin | 無 server、無 port 的一次性任務模式 |
dsh-headless 是整個架構最有說服力的證據。dsh --profile headless "run the tests" 會建立一個 agent、把任務當成普通 user message 送入、等它跑完、把最後的 assistant 回覆寫到 stdout,成功 exit 0。全程不開 port、不起 server。這證明 Web UI 真的只是疊在同一個 agent 核心上的另一層 patch,不是一個「web 版」和「CLI 版」共用部分程式碼的分支結構。
想知道自己開機的到底是哪棵樹,dsh --dump-config 會離線輸出疊加後的完整結果。組合式架構的可除錯性靠的就是這種「讓最終狀態可見」的工具,不然四層疊加對使用者是黑箱。
工程含義:分層組合把「產品變體」從程式碼分支問題變成設定檔問題。headless、web、你自己的客製版本,差異全部收斂在 patch 層,agent 核心只有一份。
Capability Seam:為什麼換三個 plugin 就能搬走整個執行環境?
Capability Seam 是 dsh 管理 plugin 解耦的模式:每個能力(fs、shell、subprocess、sandbox)必須拆成 Definition、Provider、Consumer 三個角色,Consumer 只認抽象契約的 key,從不 import 實作。少了這條規則,plugin 之間直接 import 彼此,「一切皆 plugin」就只是把耦合換了個目錄結構。三個角色具體是:
- Service Definition:擁有
ctx.fs、ctx.shell這種 key 的抽象契約 - Service Provider:一或多個實作(
fs-local、fs-sandbox、fs-e2b) - Consumer:使用能力的一方,只透過 key 取用,從不 import 實作
repo 的 glossary 明文規定單一角色不構成 seam,新增能力要三個角色一起設計。這聽起來像教科書上的依賴反轉,差別在 dsh 把它執行到了一個少見的徹底程度,徹底到可以支撐這個場景:
想讓 agent 的所有動作跑在雲端的遠端 Linux sandbox(E2B)裡?不是包一層 Docker,不是改 Bash tool 的實作,而是掛三個 plugin,把 ctx.fs 和 ctx.subprocess 的 provider 換成遠端版本。然後 Bash、持久化終端(PTY)、LSP 一行都不用改,整組搬進同一個遠端環境。因為這三個 Consumer 的所有執行環境操作,本來就只委派給 ctx.fs 和 ctx.subprocess 這兩個抽象契約。Harness 行程、model 呼叫、session 狀態則留在本地。
對照組是常見的做法:在 sandbox 抽象層裡支援「本機後端」和「容器後端」。dsh 明確拒絕這條路,它的 sandbox seam 只管同一台機器上的檔案存取限制(bwrap、Landlock、Seatbelt),容器和遠端執行器不是 sandbox 的後端,而是成組替換整個 provider 家族。兩件事的抽象層級不同:sandbox 限制「這台機器上能碰什麼」,換 provider 家族改變「動作發生在哪個世界」。把兩者混進同一個抽象,就會得到一個什麼都能設定但語義說不清的 sandbox 介面。
工程含義:seam 的價值不在第一天,在第 N 天你需要換掉某個實作的時候。判斷自己系統裡的抽象是不是真的 seam,就看這個測試:換 provider 時,Consumer 需不需要知道?需要,就不是 seam,只是一層命名好聽的間接呼叫。
組合式與固定核心加擴展點,兩條路線差在哪?
dsh 和 Claude Code 的差異是路線級的:Claude Code 用固定核心加預留擴展點,你能改的範圍由產品團隊劃定;dsh 用組合式,沒有東西不能換,代價是要先學會組合的詞彙。這不是誰做得比較好的問題,是兩組不同的取捨。
Claude Code 的模型是固定核心 + 預留擴展點。核心負責 agent loop、permission、context 管理,這些你動不了;你能動的是核心預留的位置:Hooks 在生命週期節點攔截、Skills 注入領域知識、MCP 接外部工具(這五層生態我在另一篇整理過)。擴展點的邊界劃在哪裡,是產品團隊替你決定的。
dsh 的模型是組合式。沒有預留擴展點這回事,因為沒有東西不能換。有趣的是 dsh 同時提供了 Claude Code hooks 協議的相容橋接,而橋接套件的 README 直說:原生 plugin 能做到橋接的一切且更強,橋只是讓使用者既有設定能沿用的相容路徑。這句話本身就是兩條路線的差異總結,hooks 能做的事是 plugin 能力的子集。
兩條路線的代價分布不同:
| 固定核心 + 擴展點 | 組合式 | |
|---|---|---|
| 上手成本 | 低,裝了就能用 | 高,要先學組合的詞彙 |
| 可改範圍 | 擴展點圈定的範圍 | 全部 |
| 出錯面積 | 小,核心行為有保證 | 大,疊錯 patch 整棵樹都不對 |
| 產品變體 | 官方出什麼用什麼 | 一層 patch 就是一個變體 |
| 適合對象 | 用 harness 的人 | 建 harness 的人 |
我自己的判斷是:用一個 coding agent,固定核心是對的,你要的是穩定和開箱即用;建一個給自己團隊或產品用的 harness,組合式值得認真考慮,因為你遲早會撞到「這個行為我必須換掉,但它不在擴展點裡」的牆。如果你還沒想清楚自己在哪一層,Harness Engineering 的定義文有整理這個判斷。
值得補一句:dsh 目前是 developer preview(v0.1.0-rc.5),session 格式明寫不保證相容、無遷移。它此刻是一個架構樣本,不是一個可以押生產環境的產品。這篇取的是它的設計決策,不是推薦你明天就換過去。
一人團隊要建任務型 Agent,該借鑒哪條路線?
我的答案是:別照搬全組合式,但去偷它的紀律。 場景收窄到最常見的情況:企業想針對某個任務做一個 agent(不是 coding agent),起始團隊很小,可能只有一個人。這時候固定 loop 加少數刻意保留的 seam 是正確起點,理由如下。
組合式的回報跟變體數量成正比。dsh 付得起 patch 疊加和 plugin 引擎的稅,是因為它同時要出 web、headless、SDK 三種形態,還要讓使用者客製任何部件。你的任務型 agent 第一天只有一種形態、一個部署,先建 plugin 引擎換到的選擇權是零,換到的維護成本倒是真的。一個寫死的 loop 配上清楚的函式邊界,對一人團隊就是正確起點。
固定 loop 不等於什麼都寫死。dsh 有三個紀律不需要任何 plugin 系統就能借走,成本大概是一個下午:
- Seam 只開在你預期會換的地方。 LLM provider 幾乎一定會換,執行環境很可能會換(本機跑著跑著就要進容器或遠端)。這兩個值得抽成抽象契約,其他直接寫死。判斷某層抽象是不是真的 seam,用前面那個測試:換實作時,使用方需不需要改。
- Fail loud。 設定錯誤在啟動時就炸,不要跑到第 30 輪 tool call 才發現 shell 不存在。
dsh連空的 patch 檔都直接 throw,這個態度可以原封不動搬走。 - 最終組態可見。 做一個
--dump-config的等價物,把 agent 實際生效的 model、tools、prompt 組合印出來。只有一個人時你會覺得多餘,出第一次「本機好好的、部署上去不對」的問題時它就回本了。
除錯這件事要誠實地雙面說。組合式讓你可以二分搜尋:懷疑哪個行為有問題,拆掉那個 plugin 重跑就知道。但它同時多了一類固定核心不會有的 bug:行為是 N 個攔截層疊加的結果,其中一層寫錯就靜默改變全局。dsh 自己的 repo 鐵律是最好的證據:waterfall listener 就算只是觀察、不改任何東西,也必須呼叫 next(),忘了會無聲吞掉下游所有行為。這種 bug 需要第二雙眼睛 review 攔截鏈才容易抓,而一人團隊沒有第二雙眼睛。這是先走固定 loop 的另一個理由。
那什麼時候該升級成組合式?我認為信號很明確:第二個產品形態出現的時候。同一個 agent 要同時有 API 版和排程版、不同客戶要掛不同的工具組,這類需求一出現,變體就開始跟核心搶同一份程式碼,這時再把 loop 和外殼的分界抽成 seam。階段性驗證也是同一件事的副產品:headless 用 35 行就能跑,證明的不是 plugin 很棒,是核心跟外殼分乾淨之後,最小可驗證版本自然存在。你的 agent 如果拿掉 API 層就跑不起來,分界就還沒分乾淨。
結語:擴展點是產品替你劃的邊界,組合是你自己劃邊界的能力。讀懂
dsh的價值不在學會用它,在看清「當 harness 沒有核心」時,換 loop、換執行環境、換產品形態各自變成多小的一件事。
本文是 DeepSeek Harness 系列第一篇。下一篇會拆它的狀態核心:「模型可見 ⟺ 已記錄」這條 runtime invariant,怎麼讓 agent 的 context 永遠可重建。
延伸閱讀
- Harness Engineering — AI 工程師的第三個維度:還不確定自己是在「用 harness」還是「建 harness」,先讀這篇的定義與分層
- Claude Code:從 claw-code 的分析看一個 Coding Agent 的設計關心:固定核心路線的具體長相,Hook、Permission、擴展層的職責分工
- 你在比的是模型,但決定 Claude Code 效果的是 Harness:Claude Code 五層生態的整理,本文對比表的背景