前言¶
用 DeepSeek Harness(DSH)做跨會話的項目開發,會碰到兩類遺忘。
一類在會話之間:每次新開對話,agent 不記得你上週定過的方案,文檔要重讀一遍,同一個坑會再踩一次。另一類更隱蔽,發生在一次會話內部:上下文變長後宿主會壓實(compaction),把原始消息替換成摘要。上下文便宜了,但摘要是有損的,實際發生過的數值、取捨背後的推理,被丟掉就回不來了。README 裏給過一個實測:588 k token 的歷史,壓實後的摘要只有約 2 k token。
iv3rins/dsh-memory 想解決的是這兩件事,辦法是把記憶放進倉庫,而不是放進上下文窗口。插件名在 GitHub 倉庫上標爲 dsh-project-memory,分類是「記憶」。
這是什麼¶
一個面向 DSH 的項目級持久化知識與開發狀態記憶插件,維護者是 iv3rins,包名 @dsh-external/dsh-project-memory,MIT 協議,要求 Node 20+。
它是一個宿主側的 cordis 插件:註冊一批 agent 工具、一段 system prompt 小節、幾條斜槓命令,外加一個瀏覽器端側邊欄面板,面板通過兩條迴環地址路由讀取宿主數據,不給宿主增加額外工作。不修改 DSH 本身。
三套存儲系統的分工¶
README 把存儲拆成三套,各自的寫入方和存活範圍都不同,這一步是理解整個插件的前提。
| 系統 | 存什麼 | 誰寫 | 能熬過什麼 |
|---|---|---|---|
| A — 文檔 | 倉庫裏已有的 Markdown | 你自己在 git 裏寫 | 一切 |
| B — 狀態 | 任務、進度、阻塞項、決策、下一步、git 事實、交接筆記 | state_update / state_handoff 工具調用 |
重啓、壓實 |
| C — 原子(memory atoms) | 簡短的持久陳述 | 壓實觀察器 + memory_remember |
重啓、壓實 |
設計上刻意做了切分。三種內容生命週期不同,塞進一個 blob 會同時損害三者。
系統 A 裏 Markdown 是真相源,插件索引、搜索、按 ## 小節讀取,但從不改寫權威文檔;需要提改動時寫到 docs/proposals/。系統 B 只由工具調用持久化,插件明確不抓取對話去推斷進度,從不調用 state_update 的 agent 拿不到任何狀態記憶——這是一次可見的失敗,而不是某個安靜出錯的推斷。系統 C 是上面「壓實即丟失」問題的答案。
稀疏召回:原子與門控¶
系統 C 的寫與讀是兩件事。
每次壓實發生時,插件把摘要裏值得留下的內容提取成短原子,追加到 .dsh-memory/memory/atoms.jsonl。下一次請求到來時,把請求與原子庫做匹配,只選出少數幾條能放進預算的,而不是把整個庫倒進上下文。原子 id 是「類型 + 歸一化文本」的哈希,同一句話遇到兩次是強化同一條記錄,不是追加副本。
README 給了這條鏈路的四個性質,值得逐條看,因爲它們決定了這套東西的失效方式:
- 稀疏而非傾倒。幾百條裏選幾條,並且有硬 token 上限,成本由配置決定,而不是由項目規模決定。
- 記錄而非抓取。系統 B 靠工具調用寫入,不靠解析對話。
- 規則優先於相關性。帶硬約束的原子在任何打分之前就被選中,路由器無法移除它。只在你碰巧提到時才生效的規則不是規則。
- 選擇過程不寫庫。路由器子代理只能返回交給它的那些 id,自己編出來的 id 會被丟棄。記憶的變更始終由確定性代碼完成,幻覺不會寫壞存儲。
原子還有兩個互相獨立的屬性:重要性(weight,1–5)隻影響排序,常駐資格(standing)決定能否入選。常駐資格由類型 constraint 決定,與權重無關。README 坦白過一箇舊判據 STANDING_MIN_WEIGHT 是真實缺陷:它會靜默丟掉所有以權重 2 存下的約束,在真實庫上意味着 66 條硬規則裏有 32 條不合格。常駐原子仍然受條數與 token 上限約束,是預算分成,不是豁免。
打分是確定性的:詞面重疊、類型優先級、強化次數、新近度。詞面匹配不夠時可以讓子代理充當路由器,三檔:
routerTier |
行爲 | 成本 |
|---|---|---|
off |
只用確定性門控 | 免費 |
smart(默認) |
先走確定性門控,只有沒選到相關內容或匹配很弱時才升級給路由器 | 近乎免費,routerProvider 與 routerModel 都設置之前不花調用 |
always |
每一步都問路由子代理 | 每步一次調用,建議配便宜模型 |
路由器還有個額外的策略維度,三選一:task 偏向這一步即將要做的事,compliance 保留管轄當前所涉文件的規則,orient 在「想弄懂」而非「想動手」的請求裏偏向子系統如何運作。
上下文側還有一條注入路徑。會話第一步注入一段基線(項目身份、git 事實、當前任務、不變量、下一步、阻塞項、決策、文檔地圖),適配預算項 l0Budget / l1Budget,只在身份變化時重發,且發送前會掃一遍會話表面避免重複。每一步可能額外收到一個 recall 塊。基線以插件自有、包在 nonce 圍欄裏的用戶消息形式到達,項目文本被當作數據:角色前綴被中和,”ignore previous instructions” 這類措辭被剝除,不可見字符與雙向控制字符被移除,代碼圍欄被閉合。
一個被清楚標註的實測¶
README 有一組對比數據,來源是 CodeMemo 基準(每個項目 40 題),模型與裁判都是 deepseek-v4.1-flash,溫度 0。三種上下文配置看同一道題:A 只給壓實摘要(約 1.3k / 2.0k token),B 只給最後 6k token 原始轉錄,C 給摘要加召回的原子(約 0.4k token)。80 題彙總,正確率分別是 16.3%、15.0%、25.0%。
README 自己對這組數據的態度很明確,照錄三點:
- 歷史幾乎能裝下時(17k token)摘要與原始尾部一樣好,兩者都是 30.0%,此時壓實不是瓶頸。歷史裝不下時(588k token)三種方案都崩塌,記憶崩塌得最慢,但三者的絕對值都很低。
- 置信區間包含零。C − A 是 +8.8 pp,區間 −0.7 … +18.2,與「沒有效果」和「效果約 18 個點」都兼容。兩個項目與 partial credit 三個方向一致,這是擴大樣本的理由,不是引用這個數字作爲定論的理由。
- 召回改善的是部分正確的答案(32.5% → 41.9%),上下文更接近充足,但不等於充足。
這組數據測的是「預算內的可檢索性」,不是路由質量(routerTier: 'off'),也不是壓實永久銷燬了多少文本。完整方法、逐題數據和構建過程中發現的缺陷在倉庫內的 docs/design/benchmark-compaction-loss.md。
安裝與啓用¶
README 給出的安裝方式是把倉庫放到本地後,以 link: 協議註冊進 profile:宿主自己會加上依賴和 bundle 條目,因爲清單裏聲明瞭 dsh.bundle,reconcileProfilePlugins() 會把任何新裝且帶該聲明的依賴提升進去。
$dsh = "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd"
& $dsh plugin --profile desktop add -w "link:D:\path\to\dsh-memory"
D:\path\to\dsh-memory 換成你自己的本地路徑。隨後重啓宿主。用 project_inspect 驗證:能報出項目根目錄與已索引文檔數,說明插件已經生效。
README 也提示了另外一條手改 profile 的路(dependencies 加一條 + dsh.profile.bundles 加一條):更慢,而且容易只做對一半,只加依賴不加 bundle 條目的結果是裝上了但永遠不會加載。pnpm 會警告 link: 依賴不從目標的 node_modules 解析 peer,這裏無害:插件沒有運行時依賴,唯一的 peer @deepseek-ai/dsh-llm 由宿主提供。
bundle 補丁 cordis.patch.yml 只插入一行,默認即開箱可用;要改默認值時在 config 上寫:
- insert:
- id: project-memory
name: '@dsh-external/dsh-project-memory'
config:
l0Budget: 300
l1Budget: 1200
registerCommands: true
routerTier: smart
宿主側有完整的驗證腳本,npm run setup 從已安裝的桌面版負載裏提取宿主包並鏈接,是 test:host 的一次性前置:
npm run setup # 從已安裝的 app.asar 裏提取宿主包並鏈接
npm test # 14 個單元測試,針對純 core/ 層(不需要 DSH)
npm run test:host # 14 項宿主級檢查,對着真實 harness
tests/compat.mjs 把真實清單餵給宿主自己的 evaluatePluginCompatibility,engines.dsh 被寫成 >=0.2.0-rc.0 <0.3.0,真正被測過的版本是 0.2.1-rc.2。README 解釋這個窄範圍是有意的:宿主小版本升級就可能改動工具 schema 子集或者注入消息的形狀,而那樣一來插件會在掛載時靜默失敗,而不是在安裝時大聲報錯。對着更新的宿主重跑測試套件之後再放寬這個範圍。
常用工具與命令¶
一共 15 個工具。任何陌生會話裏,第一個該調的是 memory_status——記憶初始化了沒、當前修訂號、相對 git 是否陳舊、有多少原子,也可以重建索引。
| 工具 | 回答什麼問題 |
|---|---|
memory_bootstrap |
一次調用給出新會話所需的全部必要內容,且在 token 預算內 |
memory_status |
記憶狀態與修訂號,可重建索引 |
memory_search |
哪些文檔談到了這件事,帶狀態/類型/路徑過濾,有排序 |
memory_read |
讀一篇文檔或其中某個 ## 小節,有長度上限,被框爲數據 |
memory_recall |
已存原子裏哪些與這個問題相關,排序、有預算、帶路由與置信度 |
memory_remember |
存入持久事實、約束與決策 |
memory_forget |
按 id 退役原子,寫的是墓碑,再次提取無法復活 |
state_get / state_update / state_handoff |
讀狀態 / 記錄變更(compare-and-set)/ 寫會話末交接筆記 |
project_init / project_inspect |
創建 .dsh-memory/ / 只讀診斷根目錄、git 事實、索引狀態 |
docs_check / docs_sync / architecture_check |
文檔 front-matter 檢查 / 更新文檔或提改動建議(拒絕受保護路徑)/ 按 arch.json 做確定性 import 規則檢查 |
一條典型的長任務收尾流程:state_update 記錄進度與決策,memory_remember 存下繞過坑的規則,state_handoff 寫交接筆記,下一個會話開屏先跑 memory_status。
斜槓命令:/memory-status、/memory-context、/memory-init、/memory-handoff、/memory-recall、/memory-route、/memory-forget、/memory-sync、/architecture-check。其中 /memory-route 是試運行,顯示當前檔位會走哪條路由以及爲什麼,不花任何模型調用。
存儲佈局¶
全部位於項目根下的 .dsh-memory/。提交它或忽略它都自洽:提交給團隊共享狀態,忽略則給每個開發者一份私有的。
.dsh-memory/
project.json 穩定的 projectId + 首次見到的 git 事實
state/
current.json 狀態文檔(schema 2,帶修訂計數)
journal.jsonl 已接受寫入的追加日誌
compactions.jsonl 每次觀測到的壓實一行,含無產出的
.lock 短時寫鎖
memory/
atoms.jsonl 持久原子,一行一條 JSON 記錄
.lock 短時寫鎖
index.json 派生緩存,可安全刪除
arch.json 可選的架構規則
settings/
<項目鍵>.json 按項目覆蓋
compactions.jsonl 值得單獨說。插件每觀測到一次壓實就記一行,包含沒有產出原子的那些,每行帶一個封閉集合裏的 skipReason(empty-summary、no-extractable-statements、store-write-failed 等)。原子庫只能體現成功,沒有這個文件時,「提取器壞了」和「項目很閒」看起來完全一樣。
側邊欄面板¶
插件在左側邊欄增加一個「項目記憶」入口,顯示生效與已遺忘的原子數、其中有多少是常駐硬約束、按類型和重要性的分佈、壓實記錄、當前召回預算與上限,以及路由控制(routerTier、策略、提供方與模型)。
保存走宿主的 configEditor,改動生效方式與其他配置改動一致;某個 profile 沒有暴露 configEditor 時面板會直說不支持,而不是報告一次並未發生的保存。面板沒有任何改動記憶的控件——刪除仍然走 memory_forget,因爲一個能刪原子的瀏覽器按鈕會同時繞過壓實觀察器的來源信息與墓碑機制。
它的兩個端點只接受迴環地址。端點會暴露項目記憶並改變路由行爲,所以來自其他主機的請求會被拒絕而不是被回應。
它明確不做的事¶
這些邊界寫在 README 與 LIMITATIONS.md 裏,不做過度解釋:
- 不修改
AGENTS.md、docs/decisions/、docs/adr/、.git/,docs_sync拒絕這些路徑並在結果裏說明原因。 - 不 commit、branch、push,只記錄 git 事實。
- 不悄悄編造狀態,每次變更經過
state_update的 compare-and-set 修訂號,併發寫入會衝突而不是互相覆蓋。 - 搜索與原子打分都是詞面的,沒有 embeddings、沒有同義詞、除了小寫化不做詞幹化。換一種措辭問,文檔會漏。
- 召回的原子不是權威文檔,是帶出處的一句話,可能過時,可能把摘要裏的內容簡化過。真正要緊的東西應該寫進文檔,讓
memory_read能返回整個小節。 memory_forget是墓碑不是刪除,記錄留在atoms.jsonl裏當證據。- 路由器輸出不被信任:只採納提交給它的 id,自己編的 id 計入 rejected 並丟棄,路由器從不寫庫。
- 提取器「保守」得有限:確定性解析器會把多數散文當成可能的事實,把裸標題或代碼圍欄也存成原子。
另外 LIMITATIONS.md 列出了未端到端驗證的部分:GUI 會話跑得不深(沒有真實壓實、恢復、切分支、長程多會話),路由子代理從未對真實模型跑過(smart 檔在路由器失敗時退化爲確定性選擇,失效模式是漏一次改進,不是壞掉一步),面板從未被人看過渲染效果。
適用場景與注意¶
適合的場景比較明確:單個項目跨越幾十次會話、上下文經常被壓實、需要在不同分支之間共享一份決策記錄,或者團隊想用 .dsh-memory/ 共享狀態。搜索是詞面匹配這一點意味着,文檔寫得分散、同義詞多的項目收益有限,這類項目的痛點更適合先把文檔結構本身理清楚。
使用上注意四點。
- 插件以當前 dsh 進程的權限運行。安裝前請閱讀源碼與
LICENSE(MIT),不需要的功能可以關掉對應配置項,比如registerCommands: false。 engines.dsh範圍是>=0.2.0-rc.0 <0.3.0,宿主升級到 0.3.0 系列之前先去倉庫確認兼容性,或者自己重跑一次npm run test:host。always檔每步一次路由器調用,配便宜模型,並且確認routerProvider與routerModel都就位。- 側邊欄的兩條迴環端點不要轉發到其他主機,它與項目記憶和路由行爲直接相關。
結尾¶
這個插件的價值不在於讓模型變得更聰明,而是把「讀過的文檔、做過的事、定過的規則」從一次性上下文裏剝出來,落成倉庫裏可檢索的普通文件,每次按需召回幾條,成本由預算項釘住。三套系統、稀疏門控、規則優先於相關性這三處設計是它的主要取捨,LIMITATIONS.md 裏對未驗證部分的說明也足夠坦率。想上手前按前一節檢查一遍源碼與宿主版本是必要的。
倉庫地址:
- 目錄頁:https://www.skillhub.cn/plugins/iv3rins/dsh-memory
- GitHub:https://github.com/iv3rins/dsh-memory