DeepSeek Harness 本地聊天记忆:按来源保存聊天原文,关键词 + 向量检索,可选大模型重排与检索摘要。源码公开,仅限非商用
浏览文件
main
分支
README.md
160 行 · 10.8 KB预览历史下载| 1 | # dsh-companion-memory |
| 2 | |
| 3 | 给 DeepSeek Harness 里的鱼用的本地聊天记忆(源码公开,仅限非商用,见文末[许可证](#许可证))。它把聊过的原文存进本机 SQLite,回答前按「这段对话从哪来」检索相关历史,带着时间和场合交给模型;鱼也可以自己调用工具翻记忆、记一笔。 |
| 4 | |
| 5 | **不绑定任何桥接。** 插件只用 DSH 官方接口(会话事件、`ctx.tools`、运行时上下文、web 服务)。不管消息是从 DSH 桌面端、官方 QQ 机器人、qq-bridge、AstrBot 还是别的什么进来,只要进了 DSH 会话就能记;各家的差别只体现在配置里,或者由桥接调用下面的开放接口。 |
| 6 | |
| 7 | ## 它解决什么 |
| 8 | |
| 9 | | 问题 | 做法 | |
| 10 | | --- | --- | |
| 11 | | 桥接换会话 / `/reset` / 自动轮换后就失忆 | 记忆按**来源**(主人 / 群 / 私聊)存,不跟会话 ID 走 | |
| 12 | | 鱼靠 `send_message` 一类工具说话,助手正文是空的 | 这类工具的文字参数也算「她说的话」(可配置) | |
| 13 | | 桥接每次都塞一大段固定说明,检索被它淹没 | 跨轮重复出现的长行自动识别为套话,不入库、不参与检索;检索词按稀有度挑选 | |
| 14 | | 每一步都检索、往历史里追加一条消息 | 每条新的用户消息只检索一次,结果放进 DSH 运行时上下文:内容不变就不重发,变了只追加在已缓存历史之后,不破坏 KV 缓存前缀 | |
| 15 | | 私聊里的话被群友套出来 | 别人的私聊只在和他的私聊里可见;主人和群聊内容默认共享,可分别关闭 | |
| 16 | |
| 17 | ## 记忆怎么工作(RAG) |
| 18 | |
| 19 | **存**:每一轮存一条原文,包括对方说的话、鱼通过读消息工具读到的别人的发言、鱼的回复(含通过发送工具说的话)。同一场合里重复读到的同一句话只存一次;桥接每次都会塞进来的固定说明不入库。 |
| 20 | |
| 21 | **查**:每条新消息查一次,三处共用同一条检索路径:自动回忆、`chat_memory_search` 工具、`api/search`。 |
| 22 | 1. 关键词:中文按两字切词;某个字在查询里的两字组合都没被索引过时,才单独用这个字去匹配。只用查询里最有区分度的词,太常见的词(「什么」「今天」)自动去掉。 |
| 23 | 2. 向量(可选):配了 embedding 才启用,补上换了说法、没有共同字的情况。 |
| 24 | 3. 融合:两路按各自最高分归一后相加;候选中越新的最多加 15%,用来区分分数接近的新旧记录。 |
| 25 | 4. 前后文:命中的那一轮会带上同一场合里的前一轮和后一轮(6 小时内),「那就海边吧」这种才看得懂。 |
| 26 | 5. 给模型时按时间从早到晚排列,并说明「同一件事前后说法不同时,以较晚的为准」。 |
| 27 | |
| 28 | **评测**:`npm run eval` 在一份模拟的 QQ 群聊和私聊语料上跑检索(加 `--embed bge-m3` 测向量模式),`npm test` 里有不得低于的下限。「答案在给模型的证据里」的比例: |
| 29 | |
| 30 | | 查询类型 | 只用关键词 | + nomic-embed-text | + bge-m3 | |
| 31 | | --- | --- | --- | --- | |
| 32 | | 原话 / 关键词 | 100% | 100% | 100% | |
| 33 | | 换说法 | 67% | 67% | 83% | |
| 34 | | 答案在下一轮 | 100% | 100% | 100% | |
| 35 | | 事实变过(搬家) | 100% | 100% | 100% | |
| 36 | | 夹在长提示里 | 0% | 0% | 0% | |
| 37 | | 完全没有共同字 | 0% | 0% | 50% | |
| 38 | | **合计(不含最后一类)** | **76%** | **76%** | **82%** | |
| 39 | | 排第一就命中 | 59% | 71% | 76% | |
| 40 | |
| 41 | `nomic-embed-text` 主要针对英文,对中文帮助有限;`bge-m3` 是中文首选。2 万条记录时,关键词检索中位数约 17ms。 |
| 42 | |
| 43 | **聊天模型(可选):重排 + 检索摘要**。在重排里配一个聊天模型(协议 `chat`,任何 OpenAI 兼容接口都行;DeepSeek 会自动关闭推理以提速),它会同时做两件事: |
| 44 | |
| 45 | - **重排**:每次检索时,给最多 12 条候选打 0–10 分,判为无关的直接丢弃。它能推理隐含关系,比如「有什么不能吃的」对上「花生过敏」。 |
| 46 | - **检索摘要**:在后台为每条记忆写一句摘要,把隐含的意思写明白,比如「去字节报到」写成「入职字节跳动公司,找到新工作」。攒够 10 条打包成一次请求。摘要**只用来检索**,命中后给模型的仍然是原文,所以摘要写错最多影响能不能找到,不会把编造的内容当成记忆。 |
| 47 | |
| 48 | 实测(19 条查询;DeepSeek `deepseek-flash` + 本机 bge-m3): |
| 49 | |
| 50 | | 配置 | 答案在证据里(不含语义题) | 语义题(无共同字) | 证据平均长度 | 每次检索耗时 | |
| 51 | | --- | --- | --- | --- | --- | |
| 52 | | 只用关键词 | 76% | 0% | — | 几毫秒 | |
| 53 | | + bge-m3 | 82% | 50% | 727 字 | 0.3 秒 | |
| 54 | | + 重排 + 摘要(不装 Ollama) | 82% | 50% | 371 字 | 0.9 秒 | |
| 55 | | + bge-m3 + 重排 | 100% | 50% | 442 字 | 1.0 秒 | |
| 56 | | **+ bge-m3 + 重排 + 摘要** | **100%** | **100%** | **485 字** | **1.1 秒** | |
| 57 | |
| 58 | 成本(`deepseek-flash` 高峰价,非高峰减半): |
| 59 | - 摘要:每条记忆约 40 输入 + 14 输出 token,每 1000 条约 $0.03,只在入库时付一次。 |
| 60 | - 重排:每次检索约 460 输入 + 28 输出 token,每 1000 次检索约 $0.17。 |
| 61 | |
| 62 | 模型出错或超时时,重排退回原来的检索顺序,摘要留到下次再补,记忆照常工作。配置时密钥只填**环境变量名**(如 `DEEPSEEK_API_KEY`),不要填密钥本身。 |
| 63 | |
| 64 | 评测命令:`DEEPSEEK_API_KEY=… npm run eval -- --embed bge-m3 --rerank deepseek-flash`。 |
| 65 | |
| 66 | **零配置**:没配向量模型时,插件会检查本机 Ollama 有没有已经装好的中文向量模型(bge-m3、qwen3-embedding、multilingual-e5 等)。有就自动用上,并在后台给已有记忆补齐向量;只连本机,不下载、不外发。想装的话执行 `ollama pull bge-m3`(约 1.2GB)。 |
| 67 | |
| 68 | **可以接别的模型吗**:聊天用哪个大模型由 DSH 决定,插件通过 DSH 的上下文接口提供记忆,换模型照样能用。向量和重排模型可以接本机 Ollama,或任何 OpenAI 兼容的向量接口(OpenAI、硅基流动、阿里百炼兼容模式等)、通用 `/rerank` 接口和 DashScope 重排;远程服务要求 HTTPS,并需要明确允许发送。 |
| 69 | |
| 70 | ## 来源怎么认 |
| 71 | |
| 72 | 按顺序: |
| 73 | |
| 74 | 1. 桥接通过 `api/bind` 明确告诉插件某个会话属于哪个群 / 哪个人; |
| 75 | 2. 按**会话 ID** 匹配规则(默认能认出 `group:123456`、`private:123456` 和 `xxx-g-123456-…` / `xxx-p-123456-…` 这类 ID);规则是纯数据,可在设置页改; |
| 76 | 3. 都认不出来:默认单独隔离(只能看到自己);只有你一个人用 DSH 时可以设为「当作主人」。 |
| 77 | |
| 78 | 只读会话 ID,**不读消息内容**——群友可以在消息里随便写 `private:主人QQ`,会话 ID 他改不了。`ownerIds` 里的账号的私聊归入「主人」。 |
| 79 | |
| 80 | 可见范围: |
| 81 | |
| 82 | | 当前对话 | 能想起 | |
| 83 | | --- | --- | |
| 84 | | 主人的私聊 | 主人 + 所有群 | |
| 85 | | 某个群 | 这个群 + 其他群 + 主人 | |
| 86 | | 某人的私聊 | 和他的私聊 + 所有群 + 主人 | |
| 87 | | 认不出来源的会话 | 只有它自己 | |
| 88 | |
| 89 | 「其他群」「主人」两项可以在设置页分别关掉。 |
| 90 | |
| 91 | ## 给模型的工具 |
| 92 | |
| 93 | - `chat_memory_search`:按关键词翻以前聊过的原文。 |
| 94 | - `chat_memory_note`:把约定、喜好这类值得长期记住的事写一笔。 |
| 95 | - `chat_memory_forget`:按编号删除一条记忆,**只能删当前场合的**(防止群友让她删掉主人的记忆)。 |
| 96 | |
| 97 | - `chat_memory_setup`:查看 / 修改记忆设置。用户不用懂配置,对鱼说「帮我把聊天记忆设置好,我的 QQ 是 ……」就行。修改**一律弹出 DSH 的批准**,而且只能在主人自己的对话里改,群里、别人的私聊里调用会被拒绝。 |
| 98 | |
| 99 | 工具声明固定不变,不影响缓存;前三个不需要时可以在设置页关掉。 |
| 100 | |
| 101 | ## 开放接口(给桥接或其他本机程序) |
| 102 | |
| 103 | 只接受本机请求,请求头带 `Authorization: Bearer <令牌>`(令牌在设置页「桥接接口」里,也保存在数据目录的 `api-token` 文件中)。 |
| 104 | |
| 105 | ```http |
| 106 | POST /dsh-companion-memory/api/ingest {"origin":"group:123456","speaker":"小红","text":"我下个月搬去杭州","sourceKey":"msg-889","time":1762500000000} |
| 107 | POST /dsh-companion-memory/api/search {"origin":"private:10001","query":"搬家","limit":5} |
| 108 | POST /dsh-companion-memory/api/bind {"sessionId":"<DSH 会话 ID>","origin":"group:123456"} |
| 109 | ``` |
| 110 | |
| 111 | - `ingest`:桥接把它看到的群消息直接存进来(例如鱼潜水时没进 DSH 的群聊)。同一个 `sourceKey` 重复提交只存一次。 |
| 112 | - `search`:按某个来源的可见范围检索,返回命中片段和一段可直接拼进提示词的 `evidence`。 |
| 113 | - `bind`:会话 ID 认不出来源时,告诉插件它属于哪里。 |
| 114 | |
| 115 | `origin` 取值:`owner`、`group:<号>`、`private:<号>`。 |
| 116 | |
| 117 | ## 模型接口(可选) |
| 118 | |
| 119 | 默认不联网、不下载模型,用 FTS5 关键词检索就能工作。可以接 Ollama 或 OpenAI 兼容的 embedding,以及通用 `/rerank` 或 DashScope 重排。远程地址必须 HTTPS,并且要在设置里明确允许发送;密钥只从环境变量读取。 |
| 120 | |
| 121 | ## 安装 |
| 122 | |
| 123 | 普通 npm 包。用 DSH 插件管理器安装,并把 `cordis.patch.yml` 里的 bundle patch 应用到目标 profile(命令以你的 DSH 版本文档为准): |
| 124 | |
| 125 | ```text |
| 126 | dsh plugin --profile desktop add <path-to-dsh-companion-memory> |
| 127 | ``` |
| 128 | |
| 129 | 装好后在设置页填上**主人账号**;用 qq-bridge 这类会话 ID 里带群号的桥接,默认规则就能认出来源,其他桥接按上面「来源怎么认」配置。 |
| 130 | |
| 131 | ## CLI |
| 132 | |
| 133 | 只处理你明确指定的 JSONL,不扫描 `.dsh` 历史。`export` 输出的 JSONL 可以直接 `import` 回去: |
| 134 | |
| 135 | ```text |
| 136 | companion-memory import --dir ./data --file history.jsonl --scope owner |
| 137 | companion-memory search --dir ./data --query "之前的计划" --scope group:123456 |
| 138 | companion-memory export --dir ./data --scope owner > owner.jsonl |
| 139 | companion-memory stats --dir ./data |
| 140 | ``` |
| 141 | |
| 142 | 删除记忆会删掉原文、索引并留下墓碑,防止被自动重新导入;它不会抹除 DSH 自己的会话日志、WAL 或备份。 |
| 143 | |
| 144 | ## 测试 |
| 145 | |
| 146 | ```text |
| 147 | npm test # 单元测试 + 检索质量下限 |
| 148 | npm run eval # 打印检索评测报告(只用关键词) |
| 149 | npm run eval -- --embed bge-m3 # 加本机 Ollama 向量 |
| 150 | DEEPSEEK_API_KEY=… npm run eval -- --embed bge-m3 --rerank deepseek-flash # 再加重排和检索摘要 |
| 151 | ``` |
| 152 | |
| 153 | ## 许可证 |
| 154 | |
| 155 | 源码公开,**仅限非商业用途**,许可证为 [PolyForm Noncommercial 1.0.0](LICENSE)。 |
| 156 | |
| 157 | - 可以:个人使用、学习研究、爱好项目,学校、公益组织、政府机构使用;可以修改,也可以分享修改后的版本,但要附上许可证和 `Required Notice` 那一行。 |
| 158 | - 不可以:用于商业用途,例如付费产品或服务、公司内部的商业业务、收费代部署。需要商用请先联系作者授权。 |
| 159 | |
| 160 | 注意:非商用许可不属于 OSI 定义的「开源」许可证,所以这里称作「源码公开」。许可证以 LICENSE 文件的英文原文为准。 |

