← Harness Engineering
Harness Engineering

當 Agent Harness 沒有核心:DeepSeek 的 Everything is a Plugin

2026-08-23 · — views

讀 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 HarnessClaude 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 清單:

  1. profile 列出的每個 bundle 的 patch(base 最先)
  2. profile 自己的 patch 檔
  3. 機器本地的 home patch(所有 profile 共用)
  4. 命令列 --patch 指定的 overlay(依參數順序)
dsh 開機:四層 patch 疊加成一棵 plugin tree
1. bundle patches dsh-base 打底(約 78 條 entry:loop、tools、LLM、session)
後層以 row 為單位整份覆蓋前層
2. profile patch 該 profile 自己的 cordis.patch.yml
3. home patch 機器本地層,所有 profile 共用
4. --patch overlay 命令列指定,依參數順序
最終 Plugin Tree
同一個 agent 核心,疊不同的層 → 不同的產品形態
web profile
base + dsh-web-app(424 行 patch)
web server、API gateway、約 30 個 UI plugin
Web UI @ 127.0.0.1:3080
headless profile
base + dsh-headless(35 行、3 個 plugin)
無 server、無 port
一次性任務 → stdout

疊加規則以 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 條 entryLLM adapters、session 持久化、agent 核心、全套 tools、sandbox、approval
dsh-web-app424 行 patch疊在 base 上:web server、API gateway、約 30 個 UI plugin
dsh-headless35 行、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.fsctx.shell 這種 key 的抽象契約
  • Service Provider:一或多個實作(fs-localfs-sandboxfs-e2b
  • Consumer:使用能力的一方,只透過 key 取用,從不 import 實作

repo 的 glossary 明文規定單一角色不構成 seam,新增能力要三個角色一起設計。這聽起來像教科書上的依賴反轉,差別在 dsh 把它執行到了一個少見的徹底程度,徹底到可以支撐這個場景:

想讓 agent 的所有動作跑在雲端的遠端 Linux sandbox(E2B)裡?不是包一層 Docker,不是改 Bash tool 的實作,而是掛三個 plugin,把 ctx.fsctx.subprocess 的 provider 換成遠端版本。然後 Bash、持久化終端(PTY)、LSP 一行都不用改,整組搬進同一個遠端環境。因為這三個 Consumer 的所有執行環境操作,本來就只委派給 ctx.fsctx.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 系統就能借走,成本大概是一個下午:

  1. Seam 只開在你預期會換的地方。 LLM provider 幾乎一定會換,執行環境很可能會換(本機跑著跑著就要進容器或遠端)。這兩個值得抽成抽象契約,其他直接寫死。判斷某層抽象是不是真的 seam,用前面那個測試:換實作時,使用方需不需要改。
  2. Fail loud。 設定錯誤在啟動時就炸,不要跑到第 30 輪 tool call 才發現 shell 不存在。dsh 連空的 patch 檔都直接 throw,這個態度可以原封不動搬走。
  3. 最終組態可見。 做一個 --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 永遠可重建。

延伸閱讀