DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

TASHKENT-SN /

TASHKENT-SN/dsh-memory-retrieval

Verified

让你的Agent想起来已经忘掉的事

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@17a133ea

dsh-memory-retrieval

死去的记忆突然开始攻击我。

让 agent 看见一个东西,想起一段本来已经忘了的事。

人在对话里被一句话勾起的,往往不是"资料",而是自己早先的经历:三天前查过的结论、上周犯过的错、某个角色自己写下的私事。这个插件把这些都变成可以被动撞上来的候选片段。

三条设计原则(改代码前先接受这三条)

  1. 召回优先于精确 假阳性很便宜——模型看一眼就忽略了;假阴性很贵——那段记忆永远想不起来。 所以匹配故意做得宽松,注入块里也明确写着「不相关就忽略」。

  2. 全量索引 ≠ 全量注入 集合内所有正文都进索引(本机实测 199 万字),但一次只注入千余字, 不会把几百万字塞进上下文。

  3. 默认零依赖 不装词库、不调 API、不上 embedding 也能完整工作。 内置的 55 组近义词是锦上添花,不是运行前提。

五种召回来源

来源 检索什么 默认
笔记 模型自己判断值得记的结论(带出处,可调原文);集合检索时成员会话的笔记也并进池子 开
会话集合 你拖在一起的几个会话,彼此可以互相检索 开(拖了就有)
自己的历史 本会话更早说过的内容(只取 15 分钟前的,避免和当前上下文重复) 开
全量记忆卡 角色自己的 memory-card.json(不进会话日志的那部分) 配了路径才用
memory.jsonl 你手写的记忆条目 有文件才用

所有来源共用同一条注入通道:按当前消息做关键词匹配 → 取出 top-k 片段 → 附在 system prompt 后 → 由模型判断要不要用。

【集合记忆 ·「项目资料」跨会话】
- 《会话甲》 2026-11-03:…(原文片段)
【自己的历史 · 旧片段】
- 2026-11-02 05:40:…(原文片段)

模型拿到的不是"事实",是线索——相关就自然带出,拿不准就问「我之前提过……是同一个吗?」,不相关就忽略。

主动检索(可选)

除了被动撞上来,还有一个工具 memory_recall:模型想查旧账时可以主动调用, 支持 scope=collections | self | card | notes | all。这是补充手段,不是主路径—— 主路径永远是"它自己想起来",而不是"你让它去查"。

纯本地:不调 API、不做 embedding、不联网;只有注入的候选块占输入 token(默认上限约 1400 字)。

笔记:模型自己记的结论(0.9.54)

原文片段要靠模型再看一遍才判断得出价值;笔记是已经判断过的结论。所以它排在所有来源之前注入。

这一层就是「自动写入记忆」:插件不会在后台偷偷抽记忆,但模型在理解上下文之后, 可以调用 memory_note 把结论写进 notes.jsonl。写入的是结论(不是原文摘抄), 每条都带出处,写完之后下一轮就能被注入或主动查到。

memory_note(text="这个项目用 pnpm 不用 npm", kind="decision")
memory_note(text="用户偏好:先给结论,再给理由", kind="preference")
memory_note(list=true)           # 看看记了什么
memory_note(origin="a3f9")       # 顺着出处调出原文核对
memory_note(retire="a3f9")       # 标为作废(不物理删除)

# 顺手把同义词一起记了(不用再单独调 memory_links)
memory_note(text="这个项目用 pnpm 不用 npm", synonyms="网速慢,卡顿,延迟")

# 主动查(和被动注入是两条路)
memory_recall(query="pnpm 相关", scope="notes")
memory_recall(query="...", scope="all")     # 笔记 + 自历史 + 集合 + 记忆卡

每条都带出处:会话标题、时间、当时的原话片段。注入块长这样:

【笔记 · 我自己记下的】
下面不是原文,是我以前理解过之后记下来的结论——可以当成事实线索直接参考。
每条都带出处;不确定或与原文冲突时,用 memory_note(origin="<id>") 调出原文核对。
- [a3f9c1] 这个项目用 pnpm 不用 npm
  出处:《会话甲》 2026-11-07 · decision

规则:

项 行为
写入 模型显式调用;不做每轮后台抽取(贵、噪声大)
重复 内容哈希去重,同样的结论只存一条
作废 追加一条 retired 记录,原始行保留可追溯
注入优先级 order: 0,排在其他四种来源之前
可见范围 跟着来源会话走:本会话 + 与它同属一个集合的会话;集合外的笔记看不见。注入和 memory_recall 用同一套规则
无相关笔记时 仍注入最近 1–2 条 preference/decision(长期有效,不需要被勾起)
存储 ~/.dsh/dsh-memory-retrieval/notes.jsonl,append-only

配置:notesEnabled(默认 true)、notesTopK(3)、notesMaxChars(600)、notesAlwaysInject(true)。

插件市场

插件市场在 DSH 里是一个 JSON 索引,不内置任何源——所以下面这个索引由本仓库自己发布 (market/index.json,可自行核对内容):

https://tashkent-sn.github.io/dsh-memory-retrieval/market/index.json

用法:

  1. 插件 → 实验性插件 → 打开「自定义插件市场」
  2. 切到「发现」→ 添加索引源 → 粘贴上面的 URL
  3. 卡片上会显示本插件,点安装即从锁定的 commit 拉取

索引里的每个版本都锁定了 40 位 commit,engines.dsh 与 package.json 一致; 不经过 npm,也不依赖第三方市场。

安装

# 1) 把本目录加入 profile 依赖(示例:web profile)
dsh plugin --profile web add <你的本地路径>\dsh-memory-retrieval

# 2) 在 profile 的 package.json 里把包加入 bundles
#    "dsh": { "profile": { "bundles": [ ..., "dsh-memory-retrieval" ] } }

# 3) 重启 DSH

也可以直接把 cordis.patch.yml 的内容合并进 profile 的 cordis.patch.yml。

配置

- insert:
    - id: dsh-memory-retrieval
      name: 'dsh-memory-retrieval'
      config:
        enabled: true
        label: 'Memory'
        # memoryPath: 'C:/Users/you/.dsh/dsh-memory-retrieval/memory.jsonl'
        topK: 5
        maxChars: 800
        minScore: 1
        perRecordChars: 180
        askWhenUnsure: true
        # 只在这些 session 启用;空 = 全部
        includeSessionPatterns: []
        # 这些 session 永不启用
        excludeSessionPatterns: []
        # 会话级开关(GUI 写入)
        sessionOverrides: {}
        # 工作区级全局检索开关(GUI 写入;true = 跨工作区检索,默认只查本工作区)
        workspaceGlobalRetrieval: {}
        # 会话集合(GUI 拖拽写入;集合内跨会话检索)
        collectionRetrieval: true
        collectionTopK: 5
        collectionMaxChars: 1400
        collectionPerSnippetChars: 300
        collectionChunkChars: 600
        collectionSessionCapChars: 0   # 0 = 每个会话全文进索引
        collections: []
        #   - { id: col-1, name: 合同与法务, enabled: true, sessionIds: [session-aaa, session-bbb] }
        # 联想节奏:false = 只在开口时联想一次;true = 任务中途也继续想起新片段(更拟人,也更容易跑题)
        recallMidTask: false

默认记忆路径:

~/.dsh/dsh-memory-retrieval/memory.jsonl

文件不存在会自动创建空文件;把 memory.example.jsonl 复制过去即可试用。

会话集合(拖拽)

比 memory.jsonl 更省事的一层:把会话拖成一个集合,集合内的会话互相检索对方的历史对话。

  • 设置页左侧是全部会话(按工作区分组),直接拖到右侧集合卡片即可加入;拖到「+ 新建集合」会顺手创建一个。不方便拖时,每条会话右边有「加入…」下拉。
  • 集合内每个会话的完整正文都会进索引(user/message、assistant/message 的文本块 + compaction/summary,跳过运行时快照、reasoning、tool-call 参数)。
  • 检索时只把最相关的几段注入,不是把全文塞进上下文——199 万字的集合不可能整块进 prompt。
  • 注入块带来源标注 - 《会话标题》 2026-11-02 03:57:…,模型据此判断出处。
  • 每个集合有独立开关;关掉就不参与检索。

实测(本机 13 个会话、正文 199 万字):全量索引 311ms;3 会话集合 133ms、缓存命中 1ms;单次检索 1–10ms;最坏的 161 万字集合 250ms、39MB 堆。索引按 (会话文件 mtime/size + 分块参数) 缓存,会话更新后自动重建。

「试检索」按钮用的是和真实注入同一套逻辑,可以直接在设置页看某句话会捞出哪些片段。

原来的逐会话开关和工作区「全局检索」开关没有删,收进了设置页的「高级设置」折叠区,展开后和 0.6.x 一样用。

会话日志是多帧 zstd(一个 565KB 的文件有 242 帧),Node 24 的 zstdDecompressSync 只解第一帧, 所以 lib/sessions.mjs 里是逐帧解码。改这块之后务必用宿主运行时跑测试,见文末「测试」。

全量记忆卡(角色记忆)

角色把自己的记忆存在独立的 memory-card.json 里(不是会话日志)。插件可以直接读它来检索, 好处是不会混进唤醒模板——那些"【生物钟】""【统一人设卡】"根本不在这个文件里。

{
  "memoryCardPath": "C:\\Users\\you\\.dsh\\<角色插件>\\characters\\<角色>\\memory\\memory-card-full.jsonl",
  "memoryCardSessionIds": ["<角色所在的会话 id>"],
  "memoryCardTopK": 3,
  "memoryCardMaxChars": 900,
  "memoryCardAutoDisableSelf": true
}

归属规则(重要):memoryCardSessionIds 是这张卡的访问白名单。

  • 只有名单里的会话会注入它,也只有它们能用 memory_recall(scope=card) 查它;
  • 没有"全局可读"开关,这是有意的:角色的私人记忆不该漏进别的工作会话;
  • 想让别的会话也能用这张卡 → 把那个会话 id 加进 memoryCardSessionIds。

装完先验一下:是全量,还是裁剪版

角色插件通常同时留着几个文件:

文件 是什么
memory-card.json 活跃卡,会被维护脚本裁剪、降权、清理,条数少
memory-card-full.jsonl 全量归档,一行一条,只增不减
memory-card-full.md 给人看的可读版,插件不读

memoryCardPath 指错的话,插件读到的就是被裁剪过的那份——记忆看着"总是差一点",很难察觉。

最简单的检查方式:直接问你的角色。

你现在读到的记忆卡有多少条?是全量归档还是裁剪版?

它应该会调用 memory_recall(scope=card) 或去看配置,然后报一个条数。然后你自己核对:

# 全量归档应有的条数
wc -l ~/.dsh/<角色插件>/characters/<角色>/memory/memory-card-full.jsonl
  • 对得上 → 读的是全量;
  • 明显少 → memoryCardPath 还指着 memory-card.json,改成 .jsonl 那份。

0.9.66 起两种格式都支持:.json(数组或 { records })和 .jsonl(一行一条,坏行自动跳过)。

记忆卡注入的每一条都会带上记录的 createdAt(和集合、自检索一致):没有时间的旧片段, 模型分不清是三个月前还是刚刚,容易把旧决定当新消息。

从 0.9.67 起,记忆卡注入块的第二行会写明这次读进来多少:

(全量卡 N 条 · 本次命中 M 段)

N 应该等于你自己 memory-card-full.jsonl 的行数。这个数字直接暴露"插件解析出多少条"——只读进一部分时会明显偏小,不用靠感觉判断注入厚不厚。

记忆卡在注入前会先跳过最近 24 条消息里已经出现过的原话:卡里也可能有刚发生的事, 把同一段话再贴一遍只会占字数。所以每次注入的都是"当前上下文里没有的"那部分。

{
  "memoryCardPath": "C:\\Users\\you\\.dsh\\<角色插件>\\characters\\<角色>\\memory\\memory-card-full.jsonl"
}

记忆卡 ≠ 集合(别混)

数据来源 怎么共享
会话集合 会话日志 session.v4.jsonl.zstd 把会话拖进同一个集合
全量记忆卡 独立的 memory-card.json 把会话加进 memoryCardSessionIds

加集合并不会让别的会话读到记忆卡——集合索引的是"对话里说过的话", 卡里那些从没在对话中出现过的条目,集合永远拿不到。

匹配方式:关键词 + 近义词,不做语义

检索是字面匹配(中文按二元组切),不是向量检索。没改的话,「网速慢」永远捞不到「网络卡顿」。 0.8.0 起加了两层来缓解:

  • 严格度 minHits(默认 2):一条查询里至少命中 2 个词才算相关,压掉「原因」「什么」这类泛词造成的假命中; 查询特别短(≤2 个词)时自动放宽到 1,避免把正常问题挡掉。
  • 近义词表 synonyms.example.json:查询词按同组词扩展(权重 synonymWeight 默认 0.7,原词优先)。 词表可放 ~/.dsh/dsh-memory-retrieval/synonyms.json,或由 synonymPath 指定;都没有就用内置的 55 组 / 167 个词条。 条目按包含关系匹配,所以「网速慢」整词在表里时,「网速」这个二元组也能扩出来。

实测(3 个真实会话 / 206 段):

查询 关闭近义词 开启近义词
网速慢是什么原因 2 段,9.46 3 段,9.46
上网很卡怎么办 0 段 3 段,9.02(命中《会话丙》)
上网很卡怎么办 0 段 3 段,6.73
量子力学的基本假设 0 段 0 段(不相关就该不命中)

同义词表只影响召回,不改写记忆内容;表里没有的领域术语可以让模型自己记(memory_links,见后文),也可以导入外部词库。

排序:通用词和虚词不能撑起一次"想起"(0.9.38)

集合一大,会出现两种典型的假命中:

现象 例子
通用词到处都有 问「插件怎么没注入」,捞出一段讲 sandbox 权限的英文片段,只因两者都含「插件」「DSH」
虚词二元组偶然撞上 「提的」「我很」「是不」这种 bigram 在某些段里恰好出现,df 很小,看起来像"少见词"

三条约束把这两类压掉(语料 ≥20 段才生效,测试夹具这种小语料不做降权):

  1. 通用词降权:出现率 >35% 的词权重 ×0.2,>60% 基本当停用词(×0.05)
  2. 虚词降权:由「的了是我你他…」组成的二元组权重 ×0.25
  3. 分层而不是淘汰(0.9.40):只命中通用词的片段照样保留,标成 weak、排在强命中之后, 只在 topK 还有空位时补进来,并在注入块里单独标一句「以下只是词面重合、可信度较低」。 这是为了守住设计原则第一条:检索层只负责召回,不替模型判断"这段有没有用"

效果(集合 295 段,查询「为什么我提到词典但是插件没有返回注入,这个词我提的很少」):

最高分片段
改前 13.25 —— 一段讲 sandbox ACL 的英文,完全无关
改后 6.92 —— 讲的是 B站 登录态/插件功能,至少沾题

代价:语料很小(新建集合、只有两三个会话)时统计量不足,降权会自动关闭,此时仍然更宽松。

运行原理(先读这个)

这个插件不训练模型、不调 API、不依赖任何外部词库/向量库。它只在 prompt 组装阶段做一件事: 从你自己的历史里捞出可能相关的原文片段,贴进上下文,然后由模型自己判断要不要用。

两条链路:

① 记忆文件(memory.jsonl,手写 JSONL)
   每轮用户消息 → 本地关键词检索 → top-k 片段 → 注入【旧记忆候选】

② 会话集合(设置页拖拽组成)
   当前会话属于某集合 → 取集合内其它会话的正文
     → 读 ~/.dsh/sessions/<ws>/<sid>/session.v4.jsonl.zstd(逐帧 zstd 解压)
     → 按 ~600 字切块 → 关键词/近义词打分 → top-k 片段
     → 注入【集合记忆 · 「集合名」】,每段带《会话标题》+ 日期

0.9.12 起:memory_recall(query) 改为在 apply() 时全局注册一次, execute(args, exec) 里用 exec.agent.session.id 反查当前会话属于哪些集合。 之前按 agent 注册时,register() 虽然返回了 disposer,但工具不会进入该 agent 的可用工具快照,模型看不到它;现在它和两条自动注入链路并存。

设计约束:同义词只在查询侧展开,不进索引

这条比"能不能做增量"更根本,改动前先读。

会话日志 同义词 / 联想词
追加会改变旧内容吗 不会(不可变前缀) 会——加一条「网速慢 = 卡顿」,回头改变了所有旧片段的可达性
失效范围 只影响新片段 全局
适合增量索引吗 ✅ 帧偏移 + 只重分块尾部 ❌ 语义上不成立

所以:

语料索引:只存原文词项(卡顿、延迟)
查询时:  cue 词 → 查同义词表 → 扩展成多个查询词

同义词是查询侧展开,不是索引侧预写。带来两个结果:

  1. 加一条同义词立刻全局生效,不需要重建任何语料索引("追溯改变含义"是免费得来的);
  2. 词典这一侧不做哈希桶增量——几千条的整表重建 <1ms,比维护桶 + 碰撞校验 + 改组同步便宜得多。

唯一的红线:不要把同义词预先写进语料索引(例如为了给同义命中加分)。 一旦写进去,加一条词就得回头重算所有片段的分数,增量假设当场作废, 只能全量重建——这正是本插件花了很多力气避开的事。

主动检索的 scope(0.9.13)

memory_recall 增加 scope 参数:

scope 检索范围
collections(默认) 当前会话所属集合里的其它会话
self 当前会话自己的完整历史(会话日志全量建索引)
all 自己的历史 + 集合里的其它会话

self 只返回 15 分钟以前的片段,避免把刚刚还在 prompt 里的消息又捞回来。它和集合检索一样是“全量索引、只取片段”,不会把整份会话塞进上下文。

memory_recall({ query: "上次那个 EPERM 是怎么修的", scope: "self" })

三条设计原则见开头的「三条设计原则(改代码前先接受这三条)」。这里补一条实现层的推论:

注入块里写「不相关就忽略」,是因为判断权属于模型:检索层只负责把候选放到它眼前,不替它筛。

联想节奏(recallMidTask)

设计目标是「看见一个东西,想起一段本来已经忘了的事」——所以注入是被动的:由线索撞上来,而不是模型主动去查。

模式 行为 适合
false(默认) 你开口时联想一次;本轮不再重复检索 日常。成本约每轮几百 token
true 每个步骤都按当前正在做的事重新检索,能陆续想起不同片段 想要「边干活边被勾起回忆」的体感

代价:true 更像人,也更容易跑题(话题跳脱、ADHD 感),而且每步都要额外注入,一个十步任务会多花几千 token。建议先用默认值,觉得“它怎么什么都想不起来”时再打开。

线索从哪来(这是两种模式真正的区别):

  • false:最近 3 轮用户消息拼接(截断 800 字)。话题延续时记忆跟着延续,但一轮之内线索不变;
  • true:额外读取 agent/pre-step 事件里的 messages(当前步骤的 tool 结果、助手输出), 用它覆盖线索。所以你在任务中途查了别的东西,联想的方向也会跟着拐。

只放大候选池不算"中途联想"——线索不变的话,多给的还是同一批片段,只是分几次贴完而已。

被动自检索(selfRetrieval)

除了查集合里的其它会话,也可以让被动注入查当前会话自己的完整历史:

  • 默认开(selfRetrieval: true);

  • 只取 selfMinAgeMinutes(默认 15 分钟)以前的片段,最近的消息还在 prompt 里;

  • 本地拿最近 24 条消息 + 当前线索做前缀去重,当前上下文里已经有的内容不再注入;

  • 本地建索引、本地筛选,不额外调 API;只有真正捞到「新」旧片段时才多花注入 token;

  • 先取候选、再按时间过滤:自检索的片段全属于同一个会话,而集合检索默认「每会话最多 2 段」。 若直接复用那个上限,得分最高的 2 段往往就是刚说的话,再被 15 分钟规则滤掉 → 一条不剩(0.9.39 修);

  • 默认 selfTopK: 2、selfMaxChars: 600,比集合检索更克制;

  • 插件不内置任何模板规则;哪些 user 消息不该进记忆,由 sessionSkipPatterns 按你的 agent 情况配置(见下)。

设置页「自己的历史(被动)」里可以关掉或调这三个值。

跳过模板消息(sessionSkipPatterns)

会话日志里有很多「不是她说过的话」的 user/message:生物钟唤醒、任务模板、人设注入等。插件默认不猜这些前缀,sessionSkipPatterns 默认是空数组;需要时自己加正则:

{
  "sessionSkipPatterns": [
    "^【生物钟】",
    "^【当前时间】",
    "^[统一人设卡",
    "^【管理端手动任务"
  ]
}
  • 数组里每项是一个正则字符串,匹配到的 user/message 不进入自检索索引,也不进入集合索引;
  • 只过滤 user 消息,assistant 的正常发言不受影响;
  • 不同 agent 的唤醒模板不一样,按自己的日志调,不要照抄;
  • 坏正则会被忽略,不影响其它规则;
  • 修改后由设置页保存或直接改 ~/.dsh/dsh-memory-retrieval/config.json,重启 DSH 生效。

本机当前配置里已经放了一组 SurfCore 唤醒模板的规则;那只是本机配置,不是插件默认值。

单独的全量记忆卡(memoryCardPath)

除了会话日志,可以直接挂一份角色自己的 全量记忆卡 JSON,作为独立检索源:

{
  "memoryCardPath": "C:/Users/you/.dsh/<角色插件>/characters/<角色>/memory/memory-card.json",
  "memoryCardRetrieval": true,
  "memoryCardTopK": 3,
  "memoryCardMaxChars": 900
}
  • 支持顶层数组,或 { records: [...] } / { memories: [...] } / { items: [...] };
  • 每条记录至少要有 text / content / detail / summary 之一,title、tags、importance 可选;
  • 这份文件不经过会话日志,所以不会被唤醒模板污染;sessionSkipPatterns 只需要处理会话来源;
  • 记忆卡、自检索、集合检索共用一份「纯文本前缀」去重表:同一段内容不会因为同时命中三个来源就注入三遍;不需要为了记忆卡去手动关掉该会话的插件开关;
  • memoryCardSessionIds 可以标出这张卡属于哪些会话;memoryCardAutoDisableSelf(默认开)会在本轮记忆卡真的命中并注入后,暂停这些会话的被动自检索,避免卡和会话重复;
  • 这只暂停被动自检索,主动 memory_recall({ scope: "self" }) 仍然可用;用户取消勾选「注入记忆卡时暂停关联会话的自检索」即可恢复被动自检索;
  • 如果这一轮记忆卡没有命中,自检索照常运行;
  • 设置页「全量记忆卡」里可以填路径、关联会话、开关、topK 和字数上限;
  • 不知道路径时,可以让 DeepSeek 在磁盘上找角色的 memory-card.json,把绝对路径填进来;插件不猜角色目录,也不联网。

图形设置页

装进 DSH web profile 后,打开:

设置 → 记忆检索

里面有开关和所有参数:总开关、标签、记忆文件、topK、maxChars、minScore、单条截断、拿不准是否询问、按工作区分组的会话开关(含历史会话)。会话列表直接读 DSH 的 SessionController,显示 DSH 自己的会话大标题(不是 session-id);未加载的历史会话也会列出来,勾选后等它恢复时生效。

每个工作区标题旁还有「全局检索」开关:

  • 开:这个工作区的会话可以跨工作区检索全局记忆;
  • 关(默认):只检索 workspace 等于该工作区的记忆;没标 workspace 的记录不会被用。

下面的正则过滤仍然保留,作为高级用法(批量包含/排除)。

CLI 菜单仍然保留,适合没有 Web UI 的场景。

菜单

不想手改 YAML 时,用交互菜单:

node menu.mjs

可调:

  1. 总开关
  2. 标签(注入块里的记忆名字)
  3. 记忆文件路径
  4. topK
  5. maxChars
  6. minScore
  7. 单条截断字数
  8. 拿不准时是否询问用户
  9. 只在这些会话启用(正则)
  10. 排除这些会话(正则)
  11. 保存

非交互:

node menu.mjs --show
node menu.mjs --set topK=8 --set askWhenUnsure=false

配置写入 ~/.dsh/dsh-memory-retrieval/config.json,插件在下次加载/重启 DSH 时读取;它覆盖 cordis.patch.yml 里的同名配置。

记忆格式

一行一条 JSON:

{"id":"m1","title":"称呼","text":"用户希望被叫作示例名。","tags":["称呼","示例名"],"workspace":"/path/to/your-workspace","importance":4,"at":"2026-01-01T00:00:00+08:00"}
  • text 必填;
  • title/tags 会参与检索;
  • workspace 可选;工作区关闭“全局检索”时只匹配这个字段;
  • importance 用于同分排序;
  • at 用于同分时新记录优先。

工具调不到怎么办(重要)

memory_recall、memory_note、memory_links 都是插件注册的工具。如果模型说"我没有这个工具"或者明明调了却没反应,按顺序排查:

0. 先调 memory_status(0.9.70 起)

它是专门为这件事准备的自检工具,会直接告诉你答案:

插件版本:0.9.70
本 agent 可见工具:62 个
memory_recall=✅  memory_note=✅  memory_links=✅
  • 三个都是 ✅ → 工具在,是模型没找对名字,或它看到的工具清单有缓存滞后(清单滞后 ≠ 调不到,直接按名字调用通常就能通);
  • 有 ❌ → 才继续往下走。

1. 换一个新会话再试

工具表在会话创建/恢复时装配。0.9.69 把注册路径上的 await import 去掉了(工具在 agent/created 当场注册完),重启后旧会话一般也能直接拿到;仍不行就开新会话对比,能调 → 只是会话装配问题,不是插件坏了。

2. 还是不行?看名字有没有被"钉死"

会话会按名字缓存第一次见到的工具定义。如果某个名字曾经用不合法的定义注册过(比如早期版本),之后同名注册会被当成"已存在"跳过——表现是:新会话能用、老会话永远调不到,且换定义/重装/重启都无效。

判断方法:在工具表里看名字对不对得上版本(memory_links 是 0.9.46 起的新名字)。如果名字还是旧的,说明插件没升级到对应版本。

解法只有一个:换名字。 这不是 DSH 的 bug,是插件早期留下的脏状态;memory_search → memory_recall → memory_links 两次改名都是这个原因。换名字前先用 memory_status 确认到底是"看不见"还是"调不到"——两者表现很像,但只有前者需要改名。

3. 最后才怀疑版本/安装

node -e "console.log(require('./package.json').version)"

对照 README 里写的最新版本号;profile 里是 link: 安装的话改完即生效,但需要重启 DSH。

限制

项 说明
learned.json 会增长 每次 memory_links 落盘一条,没有上限也没有清理界面。模型只能增和改(重复的会合并),不提供删除——想清理就 memory_links(list=true) 看内容、直接删文件里的条目。不提供删除是有意留的接口形状:将来词表大了要按内容哈希做增量(只处理新增和变更),物理删除会重排文件、让哈希与位置失效,只能整份重传;现在统一"只增 + 作废",以后不用改契约。
只在 Windows + DSH 0.2.2-rc.1 验证过 macOS / Linux 未测;sessions.mjs 依赖 DSH 的会话日志格式,升级 DSH 后需复测。
联想默认开 relatedHops: true。觉得话题容易跑偏就关掉,或把 relatedTopK 降到 1(默认已是 1)。
  • 自动写入记忆 = 笔记层:会话日志、记忆卡、集合正文全部只读;模型可以调用 memory_note 往 notes.jsonl 追加结论(不是原文)。写入由模型显式发起,插件不做后台自动抽取;

  • 会写文件的只有两处:notes.jsonl(笔记,结论,可作废)和 learned.json(同义词与关联词词典)。会话日志、记忆卡、集合正文全部只读;

  • 全量索引 ≠ 全量注入:集合内所有正文都进索引,但一次只注入几段(默认 ≤1400 字),不是把整个集合的对话塞进 prompt;

  • 关键词/BM25,不是向量语义检索(有意不做,理由见「被否决的方案」);同义词/改写可能召回不到(专有名词、编号、项目名效果最好);

  • 注入是软约束,模型仍可能误用候选;

  • 索引是增量的:会话日志只追加,插件记住"解到第几帧、分到第几段",下一轮只处理新增内容。 实测 17.9MB / 452 万字的会话:冷启动约 590ms,之后每轮 6–10ms;文件被重写或分块参数变化时自动全量重建;

  • 集合检索依赖本地会话日志 session.v4.jsonl.zstd 的格式,DSH 升级若改格式需跟着改 lib/sessions.mjs;

  • 默认所有 agent 生效,可用 includeSessionPatterns / excludeSessionPatterns 限制;

  • 未在多个 DSH 版本上验证;agent/inbox/claimed、agent.ctx.systemPrompt 属于 DSH 插件接口,升级后需复测。

  • 工具名不要复用旧定义:会话会按名字缓存第一次见到的工具定义。如果早期版本用过同名工具、且定义不合法, 之后同名的重新注册会被当成"已存在"跳过(表现为:新会话正常、老会话永远调不到)。 排查手段:换一个全新的工具名注册,若立刻生效即为此因。当前工具名为 memory_recall。

扩充词表:先量,再决定要不要学(0.9.41)

词表相关有两个诊断工具。先说结论:用统计方法自动学词,在个人语料上不成立,两个工具都验证过:

# 1) 候选挖掘:从语料里找「X(也叫 Y)」这种明确定义
node tools/synonym-candidates.mjs

# 2) 覆盖率:现有词表覆盖了语料里多少词
node tools/synonym-coverage.mjs --top 30

实测(22 份会话 / 168 万词项):

方法 结果 为什么不行
覆盖率 2.5%,top 全是「自己/可以/因为/如果」 中文里出现最多的永远是泛词,它们不需要同义词
术语挖掘(不加引号) 抽到「保存 ≈ 清空」「出头 ≈ 两百万」 中文没有词边界,正则会把动宾短语当词
术语挖掘(引号锚定) 0 组 你的语料里没人写「X(也叫 Y)」

所以「统计自动学词」这条路是死的。但现在有第四条路,而且已经跑通:让模型自己记。

路 谁来做 覆盖什么 现状
内置 55 组 我 通用相近说法(网速慢/卡顿) 够用
导入外部词库 你 某个领域的专业词 注意许可
手工补几组 你 零散但重要的 最划算
模型自己记 它自己 它真正见过的说法与概念关系 已实测生效

实测(不是模拟):另一个会话在分析音乐时,自己写入了一组同义词和一个联想——

{ "groups": [["调性", "key", "调式"]],
  "links":  [["网络", "路由器"], ["和弦", "调性"]] }

这三条都不是我写的,是它判断「这三个词说的是同一件事」「这两个概念本来就相关」之后自己记下的。 下次检索就能接上——判断语义本来就是模型的活,不该交给统计。

两个工具留着当诊断:换了一个领域的新语料时,先跑一次看看该不该补词,比凭感觉加词靠谱。

让模型自己往词表里加(memory_links)

统计方法学不出同义词(见上一节),但判断"这两个词是不是一回事"本来就是模型的活。所以给了一个工具:

memory_links(words="网速慢,卡顿,延迟", note="用户习惯说网速慢")
memory_links(list=true)      # 看看已经学到了什么
  • 写到 ~/.dsh/dsh-memory-retrieval/learned.json,不动你的 synonyms.json(那个会被导入工具整份覆盖);
  • 与已有组有交集时并进那一组,不会分裂成两张表;
  • 保存后立即生效,下一次检索就会用新词扩展;
  • 只接受两个以上、长度 ≥2 的词;单组最多 8 个,防止一次写进来十几个把扩展面撑爆。

联想词:kind="related"(0.9.43)

同义词是"替换",联想是"下一跳"。后者不能直接拿来扩词——问「网络卡顿」时扩成「路由器」,会把一堆路由器配置拉进来。

所以联想只在语境已经提到其中一方时才启用:

memory_links(words="网络,路由器", kind="related")

生效方式:

你问「网络卡怎么回事」
  → 第一轮用原话检索,命中「网络最近特别卡,测速只有 90 兆」
  → 语境里出现了「网络」→ 顺藤摸一次「路由器」
  → 注入块里多一段:
     【顺带想起 · 由「网络」联想到】
     - 《会话乙》:路由器的 WAN 口如果协商成百兆,速度就上不去…

为什么不再加"次数上限"

试过一版「每轮最多一跳」,后来撤了。原因是:联想天然延后一步——它发生在后续步骤, 而那时的线索里已经包含当前在想什么,模型读到时就在判断"这个词跟现在聊的事有没有关系"。

所以在注入时砍次数,等于用减少信息去替代判断:既砍掉了本来合理的第二跳, 也没让判断变准(一跳就足以带偏)。该守的只有两件事:

  1. 播块时把判断交还给模型("没关系就自己咽回去");
  2. 每轮注入有字数上限(relatedMaxChars),别让候选挤占正文。

连锁本身不靠代码切——人思绪飘远也是自己拽回来的。

四条约束:

  • 单向触发:语境里出现 A 且没有 B,才去摸 B(两者都出现就不重复摸);
  • 只摸一跳:不会 A→B→C 连锁,避免话题越跑越远;
  • 自己拽回来:注入块里明确要求——先判断这段跟当前任务有没有关系, 没关系就自己咽回去,不要说"我想起…",也不要为了用上它而改话题。

开关:relatedHops(默认 true)、relatedTopK(默认 1)、relatedMaxChars(默认 300)。

主动记,不用你开口

这一步的关键不是工具,是行为:模型在对话里注意到「网络和路由器被一起谈起」时,应当自己调 memory_links(kind="related") 记下来——而不是等用户说"你记一下"。

所以注入块尾部会带一句提示(autoLearnWords,默认开):

如果你注意到「同一个东西的不同说法」(网速慢/卡顿),或「两个本来就有关系的概念」(网络/路由器、磁盘/固态硬盘),可以用 memory_links 记下来——同类用默认,关联用 kind="related"。下次检索就能接上。

只在真的注入了内容时才提一句,不占额外的常驻 token。

什么时候该调

同义词(kind 省略,默认)——同一个东西的不同说法:

  • 用户说「网速慢」,你的笔记里写的是「卡顿」;
  • 用户纠正你的用词(「我们叫它 X,不是 Y」);
  • 两个词在同一个语境里被混用。

联想(kind="related")——两个不同的概念,本来就有关系:

  • 网络 ↔ 路由器、磁盘 ↔ 固态硬盘、登录态 ↔ Cookie;
  • 它和"用户怎么叫"无关——这是世界的事实,不是用词差异;
  • 只在对话里已经提到其中一边时才会顺藤摸一下另一边。

两个都别记:只是同一次对话里偶然同现的词(咖啡 ↔ 报错),那不是关系,是巧合。

同义词词典与许可

内置表:lib/synonyms.mjs 里的 DEFAULT_GROUPS(55 组 / 167 个词条)是本项目自己写的, 随包以 MIT 发布,可自由使用、修改、再分发。

外部词库请自备:本仓库不包含任何第三方词库,也不提供下载地址、不内置抓取脚本。 原因很实际:第三方词库通常是学术共享协议或自定义许可,不是 MIT/CC 这类可自由再分发的授权, 把它打进包里会让所有使用者一起承担许可风险。

不需要外部词库也能用。 内置 55 组覆盖了常见技术/运维说法(卡顿、报错、崩溃、插件、依赖、仓库、提交……)。 下面这段只在你想扩充词表时才需要看;不扩充的话,跳过即可,不会影响任何功能。

真要扩词表时,按对方许可自行取得数据,再用转换器变成插件能读的 JSON:

node tools/import-synonyms.mjs --in "你下载的词表.txt" --out "$env:USERPROFILE\.dsh\dsh-memory-retrieval\synonyms.json"

本项目格式上兼容「编码前缀 + 一行一组」的词表(Aa01A01= 词 词 这种), 以及一行一组、空格/逗号分隔的纯文本:

  • 学术用途与商用适用不同的许可条件,请以对方协议为准;
  • 本仓库不附带、不镜像、不自动下载任何第三方词表。

支持三种输入:

格式 例子
带编码前缀 Aa01A01= 安装 部署 装好
纯文本(一行一组) 安装 部署 装好(空格 / 逗号 / 顿号分隔皆可)
本插件 JSON {"groups": [["安装","部署"]]}

转换时会丢掉单字和超长词、按 --max-group(默认 6)切分大组、去重, 避免一个词扩出几十个变体把检索搅浑(buildQueryUnits 另有 8 个变体的硬上限)。

外部词库的许可只在用户与权利人之间成立,商用请自行确认;本插件只读转换后的本地 JSON,不联网。

测试

node test-lexical.mjs  # 分词、严格度、近义词扩展、记忆检索一致性
node test-import.mjs   # 外部词表转换:三种格式、切块、脏数据过滤
node test-mock.mjs     # section 注册、memory 注入、集合注入、排除规则
node test-self-card.mjs # 自检索(15 分钟/24 条去重)+ 记忆卡归属闸门
node test-incremental.mjs # 增量索引:追加后不重解全量、文件重写退回全量
node test-api.mjs      # 配置读写、会话/工作区开关、集合增删改查、试检索
node test-ui.mjs       # 用 React 桩把设置页真渲染一遍,抓白屏类问题

这些都不需要 DSH 进程。集合相关测试用 test-fixtures.mjs 造 zstd 压缩的假会话日志, 通过 DSH_SESSIONS_DIR 指向临时目录,不会读到你的真实会话。

必须用宿主运行时跑一遍,不能用系统 node —— 插件实际跑在 Electron 的 node 里,两者行为会不一致 (例如 Node 24 的 zstdDecompressSync 只解第一个 zstd 帧,Node 26 解全部):

$env:ELECTRON_RUN_AS_NODE='1'
$dsh = '<DSH 安装目录>\DeepSeek Harness.exe'   # 换成你自己的安装路径

& $dsh test-lexical.mjs
& $dsh test-import.mjs
& $dsh test-mock.mjs
& $dsh test-api.mjs
& $dsh test-ui.mjs
& $dsh test-incremental.mjs
& $dsh test-self-card.mjs
& $dsh test-synonyms.mjs
& $dsh test-words.mjs
& $dsh test-hop.mjs
& $dsh test-notes.mjs

提交前自动跑测试

仓库带一个 pre-commit 钩子:提交前跑全部 12 套测试,任何一套失败就中止这次提交。

npm run hooks:install   # 一次性安装(.git/hooks 不进版本库,克隆后要跑一次)
npm test                # 手动跑全部

只想改文档、或本机没装 DSH 时可跳过:

git commit --no-verify

钩子优先用宿主的 DeepSeek Harness.exe 跑(插件实际跑在 Electron 的 node 里,行为可能和系统 node 不同), 找不到才退回系统 node;可用 DSH_BIN 指定路径。

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 17a133eabba6

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout