一句話版本
DeepSeek Harness(dsh)不是一個 agent,而是運行 agent 的「骨架」。 模型適配、工具系統、會話管理、沙箱、審批、Web UI、插件機制全部內建,且每一部分都可從配置替換。它的架構哲學只有一句話:Everything is a plugin(一切皆插件),沒有一個需要打補丁的特權內核——連 agent loop 本身都是插件。
這篇是對 github.com/deepseek-ai/deepseek-harness(v0.1.0-rc.5,developer preview)的源碼級拆解。不是功能介紹,是看它怎麼把「可替換性」做到骨子裡。
項目規模
| 項目 | 數值 |
|---|---|
| 組織方式 | pnpm monorepo |
| Workspace 包 | 219 個 @deepseek-ai/dsh-* |
| TypeScript 文件 | ~2,046 個 / ~45 萬行 |
| Native 代碼 | ~300 行 C11(Linux 沙箱啟動器 landlock-run) |
| 子系統文檔 | 40+ 篇雙語(en/zh 配對校驗) |
| 底座框架 | vendored Cordis 4.0.0-rc.7 |
| License | MIT |
一條命令即可跑起來:
npx @deepseek-ai/dsh web # 啟動 Web GUI,默認 http://127.0.0.1:3080
一、底座:Cordis 插件框架
DSH 沒有引入外部插件框架,而是把 Cordis 以 vendor 方式整個吃進來(重命名進 @deepseek-ai scope),完全擁有自己的框架層——可審計、可打補丁、可鎖定版本。Cordis 的五個核心概念是理解一切的鑰匙:
| 概念 | 含義 |
|---|---|
| 插件即 Service | 插件是帶 inject + apply(ctx) 的函數或 Service 子類,生命週期掛到上下文 |
| 上下文是服務容器 | 服務佔據穩定的 ctx.<key>(ctx.tools、ctx.llm、ctx.sessions),按 key 查找而非 import 實現 |
| inject 聲明依賴 | 插件等待依賴服務就緒才啟動,加載順序由依賴圖驅動,無需手動編排 |
| 類型化事件 | 用 TS declaration merging 註冊事件名;四種分發模式:emit(觀察)、waterfall(環繞中間件,next() 可委託/短路)、parallel(並行)、serial(有序) |
| 可逆副作用 | 一切註冊經 ctx.effect() / ctx.on() 安裝,插件卸載時自動撤銷 |
因為連 agent loop 本身都是插件,模型適配器、工具註冊表、會話日誌、CLI 旗標解析無一例外,所以產品的任何部分都可被替換。這是整個設計的地基。
二、Monorepo 分層
vendor/ vendored Cordis 框架層(cordis/loader/hmr/include/timer/schemastery/cosmokit)
packages/ 219 個 @deepseek-ai/dsh-* 包,按 <group>/<pkg> 分 40+ 組
core/ 產品 API 脊柱:session、system-prompt、tools、agent、agent-loop、scope
llm/ LLM 詞彙 + 適配器(deepseek、pi-ai)+ retry + token-meter
api/ typert/ 遠程 API 網關 + 類型圖生成器
fs/ shell/ subprocess/ sandbox/ terminal/ lsp/ jobs/ web/ skill/ ... 能力 seam 家族
subagent/ workflow/ goal/ plan/ compaction/ spill/ 編排與上下文工程
session/ storage/ session-query/ 持久化數據面
interaction/ guard/ preset/ hooks/ 人機協作與護欄
bundle/ 可安裝的 profile patch 層(base / web-app / headless)
host/ client/ Web GUI 的後端半與瀏覽器半
boot/ app-bin 啟動膠水
apps/ cli(dsh 命令)+ web(Vite 前端入口)
native/ landlock-run(Linux 沙箱啟動器)
python/ Python SDK 與單 exe 分發運行時
examples/ 可運行的 cordis.yml 示例葉子
依賴紀律是關鍵:擴展插件只依賴 Service Definition,絕不依賴具體 Provider。dsh-agent-loop 可換,UI/工具只依賴 dsh-agent。依賴圖由 peerDependencies 生成(docs/module-graph.md),CI 守衛新鮮度。
三、組裝機制:Profile / Bundle / Patch
運行中的 dsh 是一棵由啟動時按序疊加的各層組成的插件樹:
- Bundle(組合包):npm 分發的 Cordis 配置+掛載代碼。
dsh-base是每個 profile 的第一層(模型適配器、工具、持久化、沙箱與審批策略、設置、憑據、遙測);dsh-web-app加瀏覽器應用;dsh-headless加一次性運行器。 - Profile:Harness home 中的具名組裝,列出疊放的 bundle + 用戶自己的
cordis.patch.yml。 - Patch:按
id定位條目並整體替換其 config,或插入新條目。疊加順序:bundle(按 profile 順序)→ profile patch → home 級 patch →--patchoverlay,後寫覆蓋先寫。
幾個值得品的優雅設計:
- 旗標即服務:
--host/--port由普通的web-startup插件解析成webStartup服務,webserver 條目用!!js ctx.webStartup.port ?? 3080惰性插值。commander 只認--profile/--patch兩個啟動器旗標,其餘參數原樣交給插件樹。 - 符號鏈接場:
composeProfile在$DSH_HOME/profiles/node_modules維護符號鏈接場,保證依賴閉包共享單一 cordis 實例,插件圖不會出現重複框架實例。 - 熱重載:啟動後
watchUserPatches經 HMR 熱重載用戶 patch 層。 - 可 dump:
dsh --profile web --dump-config打印實際配置樹,其中任何條目都可被用戶 patch 替換。
dsh-base 的約 60 行 insert 涵蓋:模型(llm、llm-deepseek、llm-pi-ai、llm-retry)、持久化(session、session-persistence-jsonl、session-projection、session-query-sqlite)、沙箱/審批(sandbox-local、sandbox-policy 默認 workspace-write、bash/pwsh-sandbox 平台互斥、approval、permission-presets)、設置/憑據/遙測(otel 默認 DISABLED)、以及全部模型面工具(bash/fs/web/skill/todo/goal/subagent/workflow/ralph/jobs)。
四、核心運行時
Agent 與 Agent Loop:接口與實現分離
Agent句柄(core/agent):公開接口,send/followup/steer/inject統一投遞;ctx.agents註冊表通過setFactory()把創建委託給 agent-loop——接口與實現分離,loop 可換。ReactLoopAgent(core/agent-loop):idle | maintenance | running單相狀態機,wakeDriver → kick → while(turn())。- Inbox:雙隊列(next-turn / next-step),且是持久投影——每次變更先以
agent/inbox/spliced事件落日誌再改內存,resume 時從種子邊界重放。
Turn / Step 輪次流程
一個步驟(step) = 一次模型請求 + 它調用的工具;一個輪次(turn) = 0..n 個步驟:
turn/start
claim 下一批輸入
→ agent/pre-step (waterfall:可拒絕/改寫)
step/start → user/message 落日誌
system-prompt/assemble (waterfall)
→ agent/request → llm/stream → assistant/chunk* → assistant/message
→ tool/call* → tools/pre-execute → tools/execute → tools/post-execute → tool/result*
step/end →(還欠工作?claim 下一批 → 下一 step)
→ agent/turn-stopping (serial)
turn/end
工具調度有 exclusive 屏障 + parallel 有界滾動池,結果按模型順序提交;請求失敗走 agent/request-error(可決定 retry)。
Session 日誌:Event Sourcing 一切
append-only SessionEvent 日誌是唯一事實源。 deriveMessages() 從日誌投影出模型歷史;assistant/chunk 原始事件保證回放與 UI 保真。fork、resume、transcript、遙測、持久化全部派生自這條事件流。
核心不變量:「模型可見即已記錄」——抵達模型請求的一切都必須能從日誌重建(由運行時不變量斷言)。request/header(EpochHeader 快照)使每次請求是日誌的純函數。追加時做 lossless-JSON 校驗 + 深凍結,seq 連續。
工具系統與執行流水線
ToolDefinition= schema + 強制 output 聲明 + execute + finalizeContent + present*;defineToolDSL 做類型推導。- 註冊表按 scope 分層 + ToolRestriction(allow/deny),
schemas()白名單投影進提示詞。 - 流水線:
tools/pre-execute(allow/deny/ask)→ ask 經ctx.approval一次性把關(不可答即拒)→ 單調 ToolGuard(只有 deny/abstain,順序不可翻案)→tools/execute(超時/重試環繞)→ 工具體 →tools/post-execute→ 歸一化 →tool/result落日誌。
三個事件域
| 事件域 | 性質 | 用途 |
|---|---|---|
Session 事件(turn/*、assistant/*) |
持久事實,追加落盤 | 必須扛住 reload 的事實 |
Agent 事件(agent/*) |
實時,攜帶活體 Agent | 觀察/攔截進行中的工作 |
能力事件(fs/*、tools/*、llm/stream) |
seam 上的掛點 | 無需 import 循環即可附加策略與適配器 |
五、能力 Seam 模式:執行層的組織方式
每項可替換能力由三角色組成:
- Service Definition:
declare module把ctx.<name>合併進 Context + 抽象Service子類,包本身零實現; - Service Provider:實現並註冊(單實現 seam 載入第二個會 throw;註冊表型如 subagent/llm 按名共存);
- Consumer:只 inject ctx key,不 import 任何 provider。
切換 provider = 在 cordis.yml 換一個插件,模型面工具與 agent loop 完全不動。
最精彩的應用是執行世界(execution world):ctx.fs 與 ctx.subprocess 的契約綁定「同一個世界」。bash 執行器、PTY 後端、LSP host、外部 CLI subagent 全部只依賴 ctx.subprocess + ctx.fs。因此把這兩個 provider 換成 e2b 實現(fs-e2b + subprocess-e2b 共享一個 E2B SDK 句柄)後,bash/PTY/LSP/claude-code 等整體搬進遠程 Linux 沙箱,無一處消費者代碼改動。遷移單位是「世界」,不是單個工具——這是 seam 模式的收益極值。
沙箱與審批
ctx.sandbox:同世界進程圍欄。消費者交出即將 spawn 的精確 argv,provider 按 per-call 策略返回ConfinedArgv+enforcement: full|partial。後端:Linux bwrap / landlock(原生 C 啟動器,fail-closed)/ macOS seatbelt / Windows 私有 SID+ACL。ctx.sandboxPolicy是策略的唯一之家:bash 與 fs 兩個強制家族讀同一份解析結果與writableRoots,從結構上消滅圍欄漂移;解析結果寫進 session 日誌,重放時重建同一策略。- 升級 choreography:
escalation.ts統一更寬模式階梯、sandbox_permissions⇔justification配對校驗,approveEscalation在任何執行之前經ctx.approval走 fail-closed 審批,通過後以一次性更寬策略重試。 - 審批 seam:
approval/requestwaterfall 分發一次性決策,無回答者時 fail closed 為unavailable;approval/asked|decided落日誌審計。 - 誠實定位:fs-sandbox、workflow vm 都明確文檔化自己是 containment(遏制)而非安全邊界;真正的內核級隔離交給 sandbox runner,並如實上報 enforcement 等級。
六、多輪編排:subagent / workflow / ralph / goal
| 機制 | 本質 | 適用 |
|---|---|---|
| subagent | ctx.subagents 命名 provider 註冊表:in-process spawn/fork、外部 CLI(claude-code/codex)、ACP 協議、dsh-sdk。能力用靜態描述符聲明,不支持即 UNSUPPORTED_CAPABILITY 明確拒絕,絕不靜默降級 |
委派獨立子任務 |
| workflow | ctx.workflowEngine 在 worker-thread 的 vm context 執行模型自寫的 JS 編排腳本,腳本內 agent() 橋接 subagents 做 fan-out,phase()/log() 報進度 |
大規模扇出編排 |
| ralph | workflow 引擎上的凍結用法:每輪起全新子代理,輪間只帶不可變 objective + 有界 handoff(16KB),共享 workspace 當長期記憶 | 新鮮上下文迭代(隔離污染) |
| goal | 同 session 機制:goal 狀態從 session log fold 出(帶 revision),round driver 在同一 Agent 的 inbox 排入後續 round,不開子代理 | 同會話自治續跑(保持上下文) |
另有 ctx.jobs(通用後台作業運行時 + job_* 工具)、ctx.schedule(會話內定時跟進)、plan-mode(計劃協作狀態)。
七、上下文工程:四層防線
完整的上下文窗口防線,由四個無縫銜接的機制組成:
- token-meter:per-session replay fold 的 token 計量,判斷壓力;
- compaction-tool-result-pruner:先把超大當前 tool result 改寫成可重放的替換節點(無需模型);
- compaction:LLM 摘要。
compaction/start|summary|end三事件僅寫日誌(鎖-摘要-釋放,中途崩潰表現為可檢測的遺留鎖),摘要通過surfaceOp: {op:'replace'}的user/message做唯一的 surface 變更; - spill:
tools/post-execute變換器,結果超閾值即存全文到 spill store,只給模型有界 head/tail 預覽 + locator;失敗絕不把成功調用變 error。
八、持久化與 Projection
- Persistence:
SessionEvent事件源日誌是唯一權威;JSONL(每 session 一文件)與 SQLite(node:sqlite)兩後端共享PersistenceCoordinator(批寫、prepared-session 緩存)。 - Projection:域插件貢獻純同步
ProjectionDefinition(init/apply/view),框架對已提交事件做 fold 維護讀模型。整值事件規則(事件必須攜帶完整後態,不許裸 delta)保證 fold 廉價、單位轉移自描述。 - Cache:projection 檢查點持久化,冷讀走「cache 行 + persistence 尾部重放」的梯子——列表頁永不載入全日誌。
- 關係:persistence 是權威日誌,projection 是 fold 出的讀模型,cache 是捷徑而非權威。
九、Web GUI:前端與後端同等插件化
技術棧:React 18 + TypeScript + Vite(apps/web 僅為入口,rejectStandaloneServe 禁止裸跑——只有 host 能注入 window.__DSH_BOOT__);CSS Modules + --dsw-* 設計 token,無 Tailwind/組件庫。
通信協議:單工 RPC(POST /api/<namespace>/<method>)+ 兩條只下行 WebSocket(/api/events.mux session 事件復用流、/api/events.host)。信任邊界:/api 前統一檢查 Host/Origin/sec-fetch-site(防 DNS rebinding);特權方法(目錄選擇、設置、憑據)限 loopback。
插件化 UI 的三個關鍵設計:
window.__DSH_BOOT__:host 以 index-tap 注入的插件入口圖。shell 兩階段啟動:loading 頁 → cordis Loader 全部 ACTIVE → 一次性切換真實 UI。主 bundle 只含 shell 與模塊系統,全部功能 UI 都是運行時加載的插件 bundle。- Slots 機制:「聲明 = 渲染授權 = 運行時規約」的單表
register();chain 槽反轉選擇權(條目自薦,首個非 null 當選)——approval/question 等搶佔式 UI 無需中央 switch。 - ConversationNodeDefinition:聊天消息是開放註冊制——插件 declaration-merge
ChatNodeDataMap的 key,註冊match/start/update/buildViewNode狀態機 + keyed 渲染器;事件按 seq 可重放折疊。
Typert:構建期嚴格契約。業務服務用 @Remote 裝飾器標記;構建期從 host 的 ts.Program 生成類型圖/Zod schema/Remote 描述符,客戶端只掛載產物。複雜 host 對象(如 Agent)經 lookup 換成 wire id;冷會話自動 resume。
十、LLM 抽象層與 MCP
ctx.llm 是消息與流式詞彙(Message/ContentBlock/StreamChunk)+ 適配器註冊表。Provider:llm-deepseek(官方)、llm-pi-ai(多提供商目錄);llm-retry 以插件形式提供重試策略。添加模型供應商 = 在 ctx.llm 註冊適配器,一行配置的事。 MCP 集成:mcp-client 每實例連一個 MCP server,工具以 mcp__<server>__<name> 註冊進 ctx.tools。
十一、工程實踐與質量體系
- 文檔即代碼:40+ 篇子系統文檔雙語維護(en/zh 配對校驗),
module-graph.md、capability-seams.md、配置目錄等由腳本從源碼生成並有 CI 新鮮度守衛;type-equiv代碼塊與源碼漂移檢查。 - 測試矩陣:vitest 單測 + e2e + snapshot(錄製/回放 LLM 交互)+ web 壓力/性能測試 + Windows(Wine)門禁;
llm-mock-server/llm-replay讓測試不依賴真實 API。 - 不變量註冊表:
ctx.invariants讓包註冊自己的運行時不變量(如「模型可見即已記錄」),由框架統一斷言。 - Pre-release 姿態:明確「基礎優先於兼容」——可自由重命名/重打包,SQLite 用單調
SCHEMA_VERSION,不背兼容包袱。 - Agent 友好:倉庫自帶
AGENTS.md/CLAUDE.md、.agents/notes/(每個設計決策一篇 Agent Note)、cookbook 分步指南——這個項目本身就是為「由 agent 維護」設計的。
十二、關鍵設計決策總表
| # | 決策 | 收益 |
|---|---|---|
| 1 | 一切皆插件,無特權內核(連 agent loop 都是插件) | 任何部分可從配置替換;註冊隨卸載撤銷 |
| 2 | Event sourcing 一切(歷史/inbox/請求頭皆派生自 SessionEvent) | fork/resume/回放/審計同源;「模型可見即已記錄」可斷言 |
| 3 | 能力 seam 三角色 + 執行世界邊界 | 換 2 個 provider 即把整個執行世界遷到遠程沙箱 |
| 4 | 策略與執行分離的單一家(sandboxPolicy)+ fail-closed 審批 | 結構上消滅圍欄漂移;審批缺席 = 拒絕 |
| 5 | 三事件域 + 四種分發模式(waterfall 攔截點 + 單調 guard) | 策略可堆疊且順序安全 |
| 6 | 聲明合併擴展核心 sum type | 插件不改核心即可擴展核心類型,全為編譯期檢查 |
| 7 | Shell 自足 + 運行時 UI 插件圖 + Typert 構建期契約 | 前端與後端同等插件化;跨進程 RPC 全類型安全 |
| 8 | 上下文工程分層防線(token-meter→pruner→compaction→spill) | 長會話不爆窗口,每層可重放、可審計 |
| 9 | 框架層 vendor 自有化 | 可審計、可打補丁、版本鎖定 |
| 10 | 整值事件規則 + projection/cache 派生 | fold 廉價、冷讀走梯子;權威只有一份 |
| 11 | Patch 整體替換 config + 旗標即服務 + 符號鏈接場 | 每行至多一個 bundle 層 + 用戶層擁有,可預測、可 dump、可熱重載 |
十三、對自建 Agent 基建的移植評估
讀完這套架構,最值得偷師的不是某個具體功能,而是四個結構性原則。以我們自己的多 Agent 基建(UltraClaw / OpenClaw 生態)為參照:
① 「模型可見即已記錄」不變量 → 根治子代理數據匯報不一致
我們踩過的坑:AK-SDD 子代理把 00928 的數據混進 00653 的報告、手動匯報時丟三落四。根因是「子代理說它做了什麼」和「它實際做了什麼」之間沒有可斷言的橋。dsh 的解法是把一切抵達模型的內容都強制從 append-only 日誌重建。可移植做法:給每個子代理的產出強制寫一條結構化事件(輸入+工具調用+結果 hash),主代理驗收時比對事件流而非子代理的自述。這正好和我們已有的 external supervisor 思路合流。
② 策略單一家 → 消滅規則圍欄漂移
我們現狀:行為規則散落在 PERMANENT-RULES / RULES / AGENTS / SOUL 多處,同一約束可能有多份表述,改一處漏一處。dsh 的 sandboxPolicy 是策略的唯一之家,bash 和 fs 都讀同一份解析結果。可移植做法:把「哪些目錄可寫、哪些命令需審批、哪些來源禁用」收斂到一份機器可讀的策略文件(YAML/JSON),各 skill 與 cron 讀同一份,而非各自 hardcode。
③ fail-closed 審批 → 升級我們的 Gate 機制
dsh 的審批 seam 在無回答者時 fail closed 為拒絕,且審批決策落日誌可審計。我們的 Gate/verify.py 已有雛形,但可以補上兩點:審批缺席時默認拒絕(而非默認放行或卡住)、以及把每次 Gate 決策寫進可追溯的事件流。
④ 上下文工程四層防線 → 印證 headroom 的方向,補上 spill
我們已經部署了 headroom 做上下文壓縮。dsh 的額外啟發是 spill:工具結果超閾值時只給模型 head/tail 預覽 + locator,全文存到 store 按需取回,且「失敗絕不把成功調用變 error」。這比單純壓縮更適合我們動輒幾百 KB 的年報/PDF 提取場景——可以做成 AK-OCR pipeline 的標準後處理。
暫不建議直接搬的:Cordis 整套插件底座和 Typert 構建期契約。這些是框架級重構,收益大但遷移成本極高,更適合作為長期參照而非短期移植。我們現階段把①②③④四個原則落到現有基建,性價比最高。
結語
DSH 最打動我的一點是它的誠實:fs-sandbox 明確寫自己是「遏制而非安全邊界」,pre-release 明確寫「將有破壞性變更」,能力不支持時明確返回 UNSUPPORTED_CAPABILITY 而非靜默降級。一個框架敢把自己的邊界寫進文檔,比任何營銷話術都更有說服力。
而「一切皆插件」不是口號——當連 agent loop、CLI 旗標、前端 UI 都被做成可卸載的插件時,它是一種可以被源碼驗證的架構紀律。
分析方法:克隆源碼,由多個並行子代理分別深讀核心運行時、啟動/插件系統、Web GUI、執行層,結合官方架構文檔交叉驗證。本文為 2026-08-14《DeepSeek Harness 深度研究:首日實測》的源碼級續篇。