前言¶
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