dsh-g-memory:給 DeepSeek Harness 加一層跨會話語義記憶

前言

DSH 的歷史會話躺在磁盤上,能用的入口過去只有一個:內置的 FTS5 全文索引,按標題和精確 token 命中。它回答不了「三週前關於重試策略我們決定了什麼」,因爲那句話不在任何標題裏。

dsh-g-memory 想補這一層:把歷史會話裏確實有信息量的內容切片、嵌入、建索引,再讓模型按需取回。維護者是 Gladess1102,分類「記憶」。截至 2026-11-06,GitHub repository API 返回 stargazers_count: 1,license.key: mit;package.json 中 version: 0.14.3,license: MIT,engines.node: ">=20",dependencies 爲空。沒有新常駐服務,檢索全部在本機完成。

下面按「它是什麼、記什麼、怎麼查、怎麼配」的順序介紹。

它是什麼

一句話:dsh-g-memory 是面向 DeepSeek Harness 的跨會話語義記憶插件,把會話歷史在本地建向量加詞法的索引,按需召回後作爲已明確框定的非可信上下文注入。

它和 DSH 內置能力的差異先列清楚。

DSH 內置 FTS5 dsh-g-memory
匹配方式 標題、精確 token 向量相似 + CJK bigram 詞法
能回答 「標題裏含 retry 的會話」 「三週前關於 retry policy 的結論」
CJK 處理 unicode61,一整段算一個 token 相鄰字符二元組,按 idf 加權
部署前提 內置 需要一個本地嵌入服務
索引位置 DSH 自身 插件自建的本地索引

兩者不衝突:內置索引不做任何改動,插件是在它之上加一層語義召回。

生態背景上,DSH 的設計是「一切皆插件」;SkillHub 是社區插件目錄站點,與 DeepSeek 無官方從屬關係。

它記哪些內容

會話日誌裏的事件類型很多,插件只保留有獨立意義的那幾類。README 給了逐類清單,整理如下。

保留:

1、user/message。人的發言。運行時注入(source.kind = plugin,例如 prompt snapshot、提醒)會被過濾掉——它們不是記憶,卻會在索引裏佔大頭。
2、assistant/message。模型寫下的結論、解釋、決策。
3、tool/result。測量值、錯誤、命令輸出。日誌裏事實密度最高的一類:開發語料裏 5.95、-0.000033、512 token 這三個字符串在對話正文中出現 0 次,只在工具輸出裏出現。沒有這一類,記憶能想起「聊過什麼」,想不起「量到什麼」。
4、tool/call 裏會寫文件的那些(write、edit 等)。這是文件歷史唯一的記錄:磁盤上只剩最新狀態,被覆蓋或刪除的內容無從找回。

不保留:

1、chunk 系列流式分片事件,佔日誌事件的 84.6%,沒有獨立含義。
2、tool/result 裏的文件讀取、blob JSON,用的時候再讀即可,佔了位置會擠掉真正的記憶。
3、tool/call 裏的 shell 命令和 run_code 代碼體,RED 裏量到這類內容是 +4.0 MiB 與 +6.9 MiB 的可重跑噪聲。

切塊按句子邊界:正文 800 字符、工具輸出 1200 字符,相鄰塊重疊 120 字符。這不是排版問題——README 指出固定寬度切塊會把數值和解釋它的句子切開,按句切塊才能讓兩條檢索臂互補。

所有保留和丟棄的規則都落在索引階段,不是召回階段:一個小的滾動索引和一次全量重建走的是同一套選擇邏輯。

檢索鏈路

檢索是兩條臂並行跑,再用 Reciprocal Rank Fusion 融合。

向量臂

默認模型是 EmbeddingGemma 2,配置項是 embed.model: embeddinggemma-2、embed.url: http://127.0.0.1:11439,768 維,int8 量化,對每個 unit 做 cosine。EmbeddingGemma-300M 與 BGE-M3(1024 維)也能按配置繼續用,但索引只在它生成時所處的那個嵌入空間裏有效,換模型必須整體重建,不允許拼用。

這個模型需要角色前綴:查詢和文檔的提示不同。於是有 embed.queryPrefix 與 embed.docPrefix 兩個配置,前綴在請求邊界統一加一次,調用方只聲明自己嵌入的是查詢還是文檔。在開發語料上(28 個標註用例、99 路對照池):裸用 R@1 是 24/28,加模型卡自帶的提示是 26/28,而它替代的上一代是 25/28。

根據模型卡,查詢側用 QuestionAnswering 提示會比 SearchResult 提示明顯更好:同一索引下向量臂 @3 從 3/9 提到 7/9,@20 從 15/22 提到 19/22;文檔側兩組提示完全相同,所以換查詢側提示不需要重建索引。

詞法臂

CJK 按相鄰字符二元組切,拉丁按整詞,再按 idf 加權。這個設計有實測依據:樸素子串匹配在 8 條中文標註查詢上得分 4/8,改二元組後 5/8,超過了向量臂。DSH 內置 FTS5 用 unicode61,一整段 CJK 被當成一個 token,有同樣的盲區——README 把這條列爲兩點發現之一。

融合與精度取向

三臂在同一個 8 條中文標註查詢上的表現:

臂 top-1 top-3 top-20
vector 4/8 5/8 7/8
lexical (bigram) 5/8 6/8 7/8
RRF 融合 4/8 6/8 8/8

兩點值得記住。第一,CJK 必須要做二元組。第二,這套檢索調的是召回而不是精度:三臂裏最好的 top-1 也只有 5/8(62.5%),沒有哪一臂在 rank 1 可靠,而融合後的 top-20 是 8/8。這一層只負責把相關單元放進候選池,挑選答案的是使用方。

(這張表是 README 在早先 BGE-M3 索引上測的,作者註明「結論的形狀,而不是具體數值,是可以遷移的」。)

遲到的結果

自動召回路徑被拆成兩段:有新會話事件進來時,後臺開始嵌入、檢索、合成;pre-step 只讀當時已經 ready 的部分,不等它。整條流水線耗時 2.2–11 秒(重排和 deep pool 佔大頭;嵌入本身約 10 毫秒),而一次普通問答只有一個 step 可供消費。

以前,檢索結果在本回合最後一個 step 之後才完成,會被下一條消息丟掉(記爲 late-drop, gone)。現在這種「已完成但沒被消費」的結果會被帶進下一輪:只提供一輪新鮮度,不增加額外延遲。帶過去的結果只在該輪自己的檢索還在跑時出現,不會注入已經拒絕過的輪次,隨結果 TTL 過期。TTL 目前是 5 分鐘,2026-11-03 從 2 分鐘調高,依據是這次部署上三天內過期的 29 個結果裏 28 個被同會話的新一輪重新檢索,間隔中位數 7 分鐘。

經過上面的步驟,模型已經能拿到相關記憶。下面說最後一段路徑。

索引層:把記憶存在每步的系統提示詞裏

前面所有到達模型的路徑都是 waterfall:判斷這次該不該召回 → 檢索 → 閘門 → 在 step 之前注入。四個環節在一輪裏都要正常,而按 2026-10-25 的實測,83 個會話裏有 54 個(65%)一次記憶都沒有注入,常駐卡在 267 張裏最多顯示 2 張。

索引層不依賴這四步。做法是在 system-prompt/assemble 鉤子裏,把一段一行一條的索引追加到每一步裝配出的系統提示詞:id 前綴加摘要首句。它只是一張地圖,不做任何相關性判斷,正文仍然只通過 memory_recall 取,塊頭已經寫明這一點。

預算由三個配置控制:maxChars 1600(總量)、maxCards 40(條數上限)、lineChars 72(每行截斷長度)。一行要麼整條出現,要麼不出現——半行會被讀成「被截斷的事實」。索引塊裏只列 current 狀態的記憶,superseded / retired / expired 不出現;footer 寫明實際可見條數與總量,防止把「能顯示的幾張」錯當成「總共只有幾張」。開關是 indexLayer.enabled / DSH_G_MEMORY_INDEX_LAYER,默認開啓。

成本量過:索引塊隨每步系統提示詞走,本地一個活躍日大約 1073 步,折算每月幾塊錢 token;存儲零字節(塊是每次渲染出來的);內存也不動——82 MB 的加載和 296 MB 的倒排文件與這個塊無關。

部署與配置

先起本地嵌入服務

檢索唯一必需的服務是一個 OpenAI 兼容的 /v1/embeddings 接口。README 裏的部署方式基於 llama.cpp:

# EmbeddingGemma 2 的 Q8_0 約 310 MB,模型文件來自 ggml-org/embeddinggemma-2-GGUF。
# 二進制必須含 PR #30054(2026-10-06 合併);更早的版本加載不了這個架構,
# 表現爲嵌入端點起不來。
llama-server -m embeddinggemma-2-Q8_0.gguf --embeddings \
  --host 127.0.0.1 --port 11439 -c 8192 -b 4096 -ub 2048

兩個坑已在 docs/DEPLOYMENT.md 裏記下來。--embeddings 不能省,否則 llama-server 回 501 This server does not support embeddings。物理批大小不能留默認的 512:超限不是截斷,而是 500 input (N tokens) is too large to process,而客戶端把失敗當成「保持原順序」,於是每次重排都失敗但系統報告自己健康。1200 字符的工具輸出片段約 590 tokens,正好頂穿 512,所以兩個引擎都加 -ub 2048。

這個插件最初是在 Apple Silicon 上圍繞 BGE-M3 + bge-reranker-v2-m3 + MiniCPM5-2B(MLX)開發的,但那套只是可行性證明,不是架構的一部分——嵌入、重排、合成三個槽位各自獨立可換,Windows 上換用 llama.cpp / Ollama / LM Studio 即可。

插件配置塊

已抓取資料中沒有出現官方的一行安裝命令——README 的安裝章節被路由截斷,未能取得原文。下面只列 docs/DEPLOYMENT.md 裏驗證過的配置形態,插件在配置裏是 - id: dsh-g-memory 加 config:

# ① 最小:只要嵌入,其它全關
- id: dsh-g-memory
  config:
    embed:
      url: http://127.0.0.1:11439
      model: embeddinggemma-2
      queryPrefix: "task: question answering | query: "
      docPrefix: "title: none | text: "
    rerank: { url: '' }
    synthesis: { enabled: false }

三個槽位的必需性不同:嵌入必需,重排強烈建議,合成可選。重排不可用時閘門退回 RRF 分數,能跑但區分度明顯下降;合成關閉時直接返回原文片段,由會話自己的模型完成抽取——代價是每輪多約 1100 token,換來零額外服務。換嵌入模型要整體重建索引,沒有「轉換向量」這回事。

嵌入端也接受雲端 OpenAI 兼容接口(OpenAI、智譜、通義等,填 baseUrl 加 key),代價是記憶內容離開本機。

換模型要重建

dsh-g-memory build --fresh      # 忽略舊索引與斷點,全部重新嵌入

索引頭部記錄 dim 與 model;維度不符會在查詢時報錯並說明原因,同寬度的模型或前綴變化靠記錄下來的名字與前綴識別,構建端不再複用舊向量。構建端和卡片端都有守衛,最壞情況是卡片悄悄檢索不到,而不是算出錯誤結果。

一個外部依賴

Node 包層面沒有運行時依賴(package.json 的 dependencies 爲空),但有一件事得先辦:zstd。索引優先讀 .zstd 壓縮副本,沒有 zstd 時退回明文 .ndjson,不算硬失敗,但首次構建會失敗。Windows 需要單獨安裝 zstd(facebook/zstd releases 放進 PATH);macOS 是 brew install zstd,Linux 是 apt install zstd。

典型用法

命令行

插件自帶 CLI,子命令來自 package.json 的 scripts:

dsh-g-memory build          # 增量構建(首次需要嵌入服務在線)
dsh-g-memory stats          # 查看索引統計
dsh-g-memory recall         # 從命令行跑一次檢索
dsh-g-memory selfcheck      # 檢查嵌入、重排等配置是否可用
dsh-g-memory plugincheck    # 檢查插件與宿主接口是否匹配

倉庫裏的腳本是 bin/dsh-g-memory.mjs,package.json 的 scripts 用的是 node bin/dsh-g-memory.mjs <command>;docs/DEPLOYMENT.md 用 dsh-g-memory <command> 的形態。兩者指向同一個 CLI。

selfcheck 有實際用途:它按宿主的同一優先級解析嵌入配置(環境變量 → profile 的 dsh-g-memory.config.embed → 包默認),能查出模型名不一致——而模型名不一致不只是顯示問題,build 會把模型名變化當成換了嵌入空間,全量重嵌並寫出一個宿主會拒絕的 header。

對話側

配置之後,模型在需要時調用 memory_recall 取正文。注入內容被標註爲明確框定的非可信上下文:記憶是用戶內容,不是可信事實,摘要會做中和並轉義 <,所以卡片正文沒有機會僞造標籤或system提示詞。

持久化卡片層是可選的:consolidate.enabled: true、minIntervalHours: 24、minTurns: 5 之後,合成把幾個片段壓成一張卡。卡片有 readCount / lastReadAt,零讀取且超過 staleAfterDays(默認 90 天)的卡片從索引層隱藏,但仍可被召回——這是可見性規則,不是刪除。

適用場景與注意

適合下面這些情況。

1、跨會話的語義回溯。「三週前關於重試策略的決定」這類問題,標題全文搜不到。
2、記憶不想出本機。默認形態下檢索全程在本機,唯一網絡調用到一個本地嵌入服務。
3、想要可審計的注入。注入內容有明確標註,正文由使用方決定是否採信,檢索層只負責召回不做判斷。

注意這幾點。

1、插件以當前 DSH 進程的權限運行,能讀到什麼取決於 DSH 本身能讀到什麼。安裝前請檢查源碼與許可證(MIT);倉庫裏另有一份 docs/SECURITY-REVIEW.md,我沒有逐條覈對。
2、GitHub 顯示 1 star,forks 與 open issues 均爲 0。作者在自己的部署上做了大量實測並公開了一部分數據和方法,但項目規模很小,是否採用由你自己判斷。
3、檢索調的是召回。top-1 不可靠,使用方需要自己挑選答案。
4、換嵌入模型必須整體重建,沒有「轉換向量」。
5、遙測默認開啓。CHANGELOG 0.14.3 記錄:README 曾寫「未設置即不打開遙測」,而代碼裏 target 恆爲默認路徑,本機 auto-recall-stats.jsonl 已到 3.7 MB / 29,382 行,每個模型 step 一條。0.14.3 改爲按代碼事實寫文檔(默認開啓,變量只移動路徑),並加了 8 MB 上限與輪轉。配置之前先知道這條。

結尾

dsh-g-memory 做的事情是給 DSH 已有的會話日誌加一層語義索引:按內容而非標題檢索,索引留在本機,注入時標註爲非可信。它不替你下結論,只負責把正確的那一小段放進候選池,並且用每步系統提示詞裏那張小小的地圖,讓模型先知道「這裏有什麼」。

目錄頁:https://www.skillhub.cn/plugins/Gladess1102/dsh-g-memory
倉庫:https://github.com/Gladess1102/dsh-g-memory

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜