DeepSeek Harness Hub
← 返回列表

证据记忆插件SodaMem/dsh-plugin-sodamem

DeepSeek Harnessspec-screened在 GitHub 查看 ↗
未验证

自动为每轮对话召回带出处的事实并留存新记忆

尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/8/18 · 已提供中文文档

SodaMem 的原生 DeepSeek Harness(dsh)插件——自动将基于证据的记忆注入每一轮对话,并摄取每一轮已结束的对话。

综合分
27.8
GitHub 分
27.8
用户评分
★ Stars
1
周下载量
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add SodaMem/dsh-plugin-sodamem
该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
数据截至 2026/9/16(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-agent-loop@deepseek-ai/dsh-brand@deepseek-ai/dsh-invariants@deepseek-ai/dsh-llm@deepseek-ai/dsh-scope@deepseek-ai/dsh-session@deepseek-ai/dsh-session-persistence@deepseek-ai/dsh-settings@deepseek-ai/dsh-system-prompt@deepseek-ai/dsh-tools
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

dsh-plugin-sodamem

为 DeepSeek Harness 提供长期记忆,由 SodaMem 驱动。

- 召回(Recall)——在组装某一轮对话时,插件会为该轮的问题检索出一个证据块,并将其贡献到提示词中。BM25 + 向量 + 实体融合,无需调用模型。
- 留存(Retain)——当该轮对话结束时,其消息会被重新摄取回存储中。

两者都不是工具调用,因此模型无法跳过其中任何一个,而且两者都不占用工具 schema 空间。

SodaMem 是一个独立的开源记忆引擎,作为本地守护进程运行。本仓库只是 dsh 插件;你需要让守护进程运行起来,它才能发挥作用。

相比笔记文件,你能获得什么

每一条被召回的事实都会标明它来自哪一轮对话。 这是真实证据块中的一行,逐字引用:

support=I flew United to Boston last week.
predicate=User flew United to Boston
entities=airline=United|destination=Boston
source=s4/s4_turn_0        ← the actual turn, not "some earlier chat"
date=2023-06-10

FactEvent → SourceSpan → RawTurn 是一条外键链。当模型对用户做出某种断言时,会有一行记录解释其原因。

事实会过期。 SodaMem 保留四条时间轴——事件发生的时间、事实为真的时间、被说出的时间、被存储的时间。“我去年搬到了芝加哥”和“我明年要搬家”是不同的行,而一个不再为真的事实就不会再被返回。更正是仅追加(ADD-only)的:新增一个版本外加一条 SUPERSEDES 边,绝不原地重写。

该引擎经过基准测试,且答案已公开。

| 基准测试 | 得分 | |
|---|---|---|
| LongMemEval-S | 92.8% (464/500) | 500 个答案 + 8,427 行证据,可用任意评判器重新评分 |
| LoCoMo | 86.88% (1338/1540) | 端到端问答,LLM 作为评判器 |

这些是 SodaMem 的数据——即本插件所对接的引擎,而非插件本身。

为什么不用 MCP 桥接?

SodaMem 已经在主 SodaMem 仓库中为 dsh 提供了 MCP 集成(integrations/deepseek-harness/)。它将记忆暴露为工具,这意味着模型必须选择去调用它们——而在大多数轮次中,它根本不会调用。SodaMem 提供的最强大的功能,即零 LLM 的 GET /v1/context 证据块,最终只能听凭模型自行决定。

MCP 无法解决这个问题。工具是仅拉取(pull-only)的,协议中没有任何机制能让服务器向提示词贡献内容或观察一轮对话的结束。本插件转而利用 harness 自身的接缝——用 system-prompt/assemble 实现召回,用 agent/turn-stopping 实现留存——因此两者都会无条件发生。

| | MCP 桥接 | 本插件 |
|---|---|---|
| 召回 | 模型调用工具,前提是它决定调用 | 每一轮,自动 |
| 留存 | 模型调用工具,前提是它决定调用 | 每一个结束的轮次,自动 |
| 模型能否跳过 | 能 | 不能 |
| 成本工具 schema 空间 | 是 | 否 |

不要对同一个存储同时运行两者。 它们会把相同的事实召回两次,并把每一轮对话都摄取两次。请选择其中一个。

要求

- Node >= 22(harness 要求如此;该插件使用了 AbortSignal.any)
- 一个正在运行的 SodaMem 守护进程——见下文

安装

dsh plugin --profile tui add dsh-plugin-sodamem

这就是所需的全部操作。该包附带一个 dsh.bundle 清单,指向
cordis.patch.yml,因此 dsh 会将该插件添加到
profile 的 bundle 列表中,并将其行——带有可用的默认值——组合进
profile 树中。确认它已就位:

dsh --profile tui --dump-config | grep -A6 'id: sodamem'

先启动守护进程(每台机器一次):

sodamem daemon ensure          # defaults to http://127.0.0.1:8000

事实提取需要在守护进程侧提供 LLM 凭据。将 SODAMEM_LLM_PROVIDER / SODAMEM_LLM_API_KEY / SODAMEM_LLM_MODEL 放入守护进程的环境变量或 .env 中。没有它们,召回仍然可用,但每次 retain 都会被接受,然后在提取期间失败。

配置

捆绑的 cordis.patch.yml 附带了可启动的默认值(apiUrl
http://127.0.0.1:8000,userId default)。要更改它们,请在你自己的 profile cordis.patch.yml 中覆盖该行,它会在每个 bundle
层之后应用:

$DSH_HOME/profiles//cordis.patch.yml
- id: sodamem
config:
apiUrl: 'http://127.0.0.1:8000'
apiKey: 'dev'
userId: 'your-user-id'
tokenBudget: 1200

补丁会替换目标行的整个 config,而不是合并进去,
因此请重新声明你想保留的每一个键。

如果不作为 bundle 安装,也可以临时插入同一行:

npx @deepseek-ai/dsh web --patch ./sodamem-plugin.patch.yml

配置字段

共有四个,它们全都是连接或作用域相关的事实。

| 字段 | 必填 | 默认值 | 它是什么 |
|---|---|---|---|
| apiUrl | 是 | — | SodaMem 守护进程的源 |
| apiKey | 是 | — | 随每个请求发送。当守护进程在禁用认证的情况下运行时,任何非空字符串都可以——没有魔法回退值 |
| userId | 是 | — | 每次读取和写入都限定于的 SodaMem user_id |
| tokenBudget | 否 | 1200 | 召回证据块的 token 预算 |

这里刻意没有开启或关闭召回或 retain 的开关,也没有策略选择器。自动注入正是该插件的全部意义所在;一个用于禁用它的小开关只会是一种更慢地使用 MCP 桥接的方式。

retain 时的 session_id 是 agent 的 id(在 dsh 中,一个 agent 和它的 session 共享同一个身份)。agent_id 被刻意不发送——它会是 session id,这会缩小检索范围并使跨 session 的召回碎片化。

仅远程模式

该插件通过 HTTP 与守护进程通信。它没有 data-root 选项,也不导入任何能在本地打开存储的东西,而且这是一个刻意的约束,而不是一个未完成的功能。
两个进程写入同一个 SODAMEM_DATA_ROOT 会将其损坏——在没有跨进程 WAL 的情况下,按用户隔离的 SQLite 在并发写入者下并不安全,这就是为什么守护进程被固定为单个工作进程(SodaMem mcp_server/README.md 和 ADR 0001 §2)。在任意 harness 进程中加载的插件是最不适合充当第二个写入者的候选——你根本不知道有多少个这样的进程在运行。因此,写入者只有一个,即守护进程,其他所有人都是客户端。

当 SodaMem 宕机或变慢时

SodaMem 的问题永远不是 dsh 的问题。 每次调用都被包装起来,使得任何错误、拒绝、超时或中止都不会泄漏到本轮对话中。

| | |
|---|---|
| 召回截止时间 | 1500 ms |
| 保留截止时间 | 5000 ms |
| 守护进程不可达、报错、缓慢或返回垃圾数据 | 召回不贡献任何内容;本轮对话正常进行 |
| 本轮对话被取消 | 进行中的 SodaMem 请求随之被中止 |

截止时间覆盖整个调用,包括头部和响应体,因此一个先返回 200 然后在响应体中途卡住的守护进程无法挂起本轮对话。

召回每个问题触发一次,而不是每次提示词组装触发一次——一个需要六步的工具循环仍然只发出一次 GET /v1/context。对话中途的引导是一个新问题,因此它有自己的召回。

保留只摄取人类或模型实际说过的内容。工具结果和 harness 的运行时上下文快照被排除在外——快照正是本插件自己召回的证据所在之处,摄取它会在每一轮把存储的输出再喂回给存储本身。

唯一需要知道的一点:当召回错过截止时间时,本轮对话会在没有记忆的情况下继续,且不会向用户暴露任何信息。插件在每一次降级轮次都会记录一条警告(ctx.logger.warn),而该日志是你得到的唯一信号。参见下面的性能说明。

加载时,插件还会发出一个廉价的预热请求,这样守护进程的惰性存储打开——实测约 630 ms,而稳态约 130 ms——就会在你的第一个问题之前被支付,而不是由它来支付。没有任何东西等待该预热,而且当还没有守护进程运行时它也无害。

性能

在真实的 1000 条事实存储上测量(启用认证、单工作进程守护进程、回环、单机、chromadb 1.1.1、chroma schema 在 9 个 sysdb 迁移处验证)。完整方法、注意事项和复现步骤:NOTES-latency.md(也随包发布)。
撤回声明。 本节早先版本声称冷启动会因 Chroma panic 而在 10 次运行中 10 次返回 HTTP 500,并且热稳态为 p50 17 ms。两者均被撤回。 它们是在一个存储上测得的,而该存储的 chroma schema 是由与读取它的版本不同的 chromadb 版本迁移的——这是测试机器的缺陷,而非守护进程或插件的缺陷。17 ms 这个数字是那个损坏的存储在关闭向量搜索的情况下作答,因此它将真实热延迟低估了大约 7 倍。完整说明(包括旧数字)见:NOTES-latency.md,以及 CHANGELOG.md 中的摘要。

- 冷启动有额外成本,但会成功。 守护进程会惰性打开用户的存储。在 3 次守护进程重启中,第一个请求每次都返回 HTTP 200,具备完整的向量路由且没有降级检索(0 降级 / 17 引用,3/3)。它耗时约 630 ms,而稳态约为 130 ms;第二个请求已经是热的。插件通过在加载时即发即忘的预热(src/warmup.ts)来吸收这一一次性成本,因此它会在用户第一个问题之前完成,而不是落在该问题上。
- 热稳态:p50 130 ms(最小值 101,p95 164,p99 186,最大值 296——200 个顺序请求,六个与存储相关的查询,token_budget 1200)。这就是存储打开后自动注入为 time-to-first-token 增加的时间。它是零 LLM 路径,因此不会随模型支出增长。
- 多客户端是需要注意的地方,而且比之前所述更小——但不要将其解读为宽裕。 守护进程按设计运行单个 worker,而 /v1/context 延迟随并发客户端近乎线性增长:并发 1 / 2 / 4 / 8 时中位数分别为 128 / 211 / 361 / 672 ms。在并发 8 时,40 个请求中最差的一次在原本空闲的机器上为 732 ms,在负载机器上五次运行中为 986–1268 ms(负载均值 14,此时顺序 p50 为 197 ms 而非 130 ms)。因此,相对于 1500 ms 召回截止时间的余量大约在 ~1.2 倍到 ~2 倍之间,取决于机器还在做什么——是真实的余量,而不是一个数量级,并且负载机器最差的一次运行距离截止时间仅差 16%。被撤回的数字曾使其在损坏存储上距离截止时间仅差 10%;这一更正比单凭那次撤回所暗示的要小。

请将并发数字理解为一种形态,而不是绝对毫秒数。 该探测在每个级别运行五轮,这不会像 200 个顺序请求那样预热 BM25 索引缓存,而且其右列是 n 次中最差的一次,而不是 p99。跨机器可复现的是近乎线性的排队;会变化的是毫秒数。
所以:形态没有改变——在单个单工作进程守护进程上,只要有足够多的并发客户端,召回就会开始悄悄丢失。在 8 个客户端的情况下,无论在哪台机器上,这里测量的任何内容都没有达到那个临界点。这是守护进程读取路径的特性,而不是本插件的特性;自动注入之所以能让它变得可触达,是因为它把偶尔一次的工具调用变成了每轮一次。这背后的数据以及每项数据的注意事项,都在 NOTES-latency.md 中。

开发

npm install
npm run typecheck
npm test          # 不需要运行中的守护进程;HTTP 在 fetch 边界被 mock
npm run build     # 双 ESM/CJS 输出到 dist/

npm run test:integration   # 真实 dsh 运行时 + 真实守护进程;不由 CI 运行

npm run test:integration 会把插件加载到真实的 dsh 运行时中——真实的
Cordis Context、真实的会话存储、真实的系统提示注册表、真实的 agent
循环——并针对运行中的 SodaMem 守护进程驱动真实轮次。它只对
LLM 适配器打桩。关于如何启动守护进程,请参见 test-integration/README.md。

单元测试无法证明插件在循环内部能正常工作:它们 mock 了
Cordis 注册边界,因此无法看到顺序。请把集成测试套件当作关卡。

许可证

Apache-2.0。参见 LICENSE 和 NOTICE。

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群