dsh-engramory-recall 0.5.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +119 -54
- package/bm25.js +80 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,60 @@
|
|
|
3
3
|
本项目的所有重要变更都记在这里。格式参照 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
|
4
4
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
5
5
|
|
|
6
|
+
## [0.6.1] — 2026-10-15
|
|
7
|
+
|
|
8
|
+
### 新增
|
|
9
|
+
|
|
10
|
+
- **公开基准:LongMemEval-S**(`xiaowu0162/longmemeval-cleaned` 的 S 版,500 题)。
|
|
11
|
+
harness 是 `bench/longmemeval.mjs`,逐题结果随仓库发布(`bench/longmemeval-results.json`)。
|
|
12
|
+
本引擎 500 题跑 18 秒、零模型调用:RecallAny@1 86.2%、**RecallAny@5 97.0%**、
|
|
13
|
+
Recall@5(gold 覆盖率)90.7%、MRR@5 0.9056、RecallAny@10 98.2%。
|
|
14
|
+
harness 支持 `--engine unigram|bigram` 跑教科书 BM25 对照。
|
|
15
|
+
- **同格式小夹具 + 测试**(`bench/fixtures/longmemeval-mini.json`、`test/longmemeval.test.mjs`,5 例):
|
|
16
|
+
真数据集 264MB 不进 CI,夹具用四道题(英文命中 / 中文命中 / 多 gold 只找到一半 / 零字面重叠
|
|
17
|
+
必失手)守住判据与两个开关;判据被改动就红。
|
|
18
|
+
|
|
19
|
+
### 说明
|
|
20
|
+
|
|
21
|
+
- 在这份**英文**数据集上,本引擎与教科书 BM25 的五个指标完全相同:多出来的机制
|
|
22
|
+
(中文口语桥、标题字段加权、别名双计)在这里基本没被激活。README 里写明了这一点 ——
|
|
23
|
+
它只说明「英文词面检索不输常规写法」,不是「我们更准」的证据。
|
|
24
|
+
- 与 `thoth-mem` 公布的 470 题成绩不可直接相减(题集版本与 harness 都不同)。
|
|
25
|
+
|
|
26
|
+
## [0.6.0] — 2026-10-15
|
|
27
|
+
|
|
28
|
+
### 新增
|
|
29
|
+
|
|
30
|
+
- **同义表扩到 177 条 / 519 个值**(105 → 177)。新增的第五波是**通用技术口语**:
|
|
31
|
+
「变慢」「退不回去」「改表结构」「装不上」这类任何写中文技术笔记的人都会用的说法。
|
|
32
|
+
它和第一至四波的性质不同 —— 那几波是从一份具体的库里挖出来的(专有名词、某个人的叫法),
|
|
33
|
+
换一套词汇就大面积失效;第五波不依赖具体某个库。索引里多出 141 个键,实测体积变化在噪声量级。
|
|
34
|
+
|
|
35
|
+
### 修复
|
|
36
|
+
|
|
37
|
+
- **桥的碎片降权不再连键本身一起降权**:命中的同义键拆出来的碎片(单字、相邻双字)算 0.5,
|
|
38
|
+
但键本身若原样出现在查询里,照旧算满权重。原先一刀切把键本身也降到 0.5,
|
|
39
|
+
合成基准的 `verbatim` 对照组从 100% 掉到 95.1%;放开键本身后回到 98.2%,桥组没退,
|
|
40
|
+
真库的桥探针还从 9/10 变成 10/10。理由很直白:查询里原样出现的那个词是实打实的证据,
|
|
41
|
+
该降权的只是它拆出来的碎片(那些碎片在别的笔记里到处都是)。
|
|
42
|
+
- **CI 上 Node 20/22 的永久 pending**:`test/llm.test.mjs` 的超时用例依赖
|
|
43
|
+
`AbortSignal.timeout` 的定时器把事件循环撑住,而那个定时器是 unref 的,
|
|
44
|
+
只有本地 Node 25 侥幸能过。测试进程现在自己留一个 ref'd 句柄。CI 自 0.5.0 起连续四次都是红的。
|
|
45
|
+
|
|
46
|
+
### 文档
|
|
47
|
+
|
|
48
|
+
- README 的基准数字全部换成实算值,并补上两处此前没写清的取舍(别名在合成语料里是把双刃剑;
|
|
49
|
+
碎片降权为什么不能连键本身一起降)。
|
|
50
|
+
- `bench/expectations.json` 重设(查询数 683 → 704),`verbatim` 组不再要求满分(98.2%,容差 1pt)。
|
|
51
|
+
|
|
52
|
+
### 基准口径(不是引擎变好,是尺子以前量错了)
|
|
53
|
+
|
|
54
|
+
- 覆盖率判据从「整个英文短语有没有键」改成「这个值有没有键」:旧判据把 16 个域词判成只覆盖 5 个、
|
|
55
|
+
14 个事件词只覆盖 5 个,按值判其实是全覆盖。
|
|
56
|
+
- 英文体裁的笔记改成真正的术语正文。此前正文里混着中文字面,口语查询靠碎片就能蒙中,
|
|
57
|
+
桥组样本几乎构造不出来(这也是桥组只有 11 条的原因,现在 32 条)。
|
|
58
|
+
- 桥样本改为按表里**真实存在的键**抽样,而不是拿整个英文短语去查表。
|
|
59
|
+
|
|
6
60
|
## [0.5.3] — 2026-10-15
|
|
7
61
|
|
|
8
62
|
### 新增
|
package/README.md
CHANGED
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
|
|
72
72
|
### 短板
|
|
73
73
|
|
|
74
|
-
-
|
|
75
|
-
Recall@5 84.8%、NDCG@10 0.8165
|
|
76
|
-
|
|
74
|
+
- **公开基准只跑了一半**:LongMemEval-S 500 题跑出 RecallAny@5 97.0%(见下),但那只测英文词面
|
|
75
|
+
检索;`thoth-mem` 公布的 470 题版本(RecallAny@5 432/470、Recall@5 84.8%、NDCG@10 0.8165)
|
|
76
|
+
用的是别的 harness,两边数字不可相减。真正的「跨插件排名」仍然不存在。
|
|
77
77
|
- **无语义检索**:没有 embedding、没有向量库。同义改写、隐含意图要靠手工同义表覆盖,
|
|
78
78
|
没覆盖到的就是漏检。(可选的 `ctx.llm` 模块能做矛盾检测和摘要,但它**不参与检索排序**,
|
|
79
79
|
所以排序本身仍然是纯词面的。)
|
|
@@ -82,23 +82,23 @@
|
|
|
82
82
|
- **写入闸门可以被绕过**:`guard` 只拦 `write` / `edit` 工具调用,`bash` 重定向它看不见;
|
|
83
83
|
`@jipika/dsh-memory` 的 guard 挂在更底层的 `tools/pre-execute`,覆盖更广。
|
|
84
84
|
|
|
85
|
-
##
|
|
85
|
+
## 召回质量:三把可复算的尺子
|
|
86
86
|
|
|
87
87
|
### 一、合成基准(`node bench/run.mjs`)
|
|
88
88
|
|
|
89
|
-
224 篇程序生成的笔记(中文书面
|
|
90
|
-
|
|
89
|
+
224 篇程序生成的笔记(中文书面 83 / 英文术语 101 / 口语体 40,其中 37 篇写了 `aliases`)、
|
|
90
|
+
704 条查询、固定随机种子。查询按三种风格分开记分:`verbatim` 抄笔记自己的写法(对照组)、
|
|
91
91
|
`colloquial` 只用口语说法(与正文没有字面重叠)、`keyword` 口语症状加域特征词
|
|
92
92
|
(同域候选很多,必须靠事件词分辨)。语料是程序生成的,不依赖任何私人库,任何人可复算:
|
|
93
|
-
`npm run bench` 整跑 4
|
|
93
|
+
`npm run bench` 整跑 4 秒,零依赖、零网络,消融表里对我们不利的那几行也照打,
|
|
94
94
|
CI 里再跑一遍 `npm run bench:check` 跟 `bench/expectations.json` 对账。
|
|
95
95
|
|
|
96
96
|
| 引擎 | 总 hit@1 | 总 hit@4 | verbatim | colloquial | keyword |
|
|
97
97
|
|---|---|---|---|---|---|
|
|
98
|
-
| **本插件** | **49.
|
|
99
|
-
| 教科书 BM25(同分词口径) |
|
|
100
|
-
| 教科书 BM25(中文逐字切分) |
|
|
101
|
-
| 瞎猜(地板线) | 0.
|
|
98
|
+
| **本插件** | **49.9%** | 66.6% | 98.2% | 33.9% | 15.2% |
|
|
99
|
+
| 教科书 BM25(同分词口径) | 43.2% | 46.9% | 100% | 17.9% | 17.9% |
|
|
100
|
+
| 教科书 BM25(中文逐字切分) | 47.2% | 53.4% | 100% | 20.1% | 28.1% |
|
|
101
|
+
| 瞎猜(地板线) | 0.6% | 2.6% | 0.4% | 0.9% | 0.4% |
|
|
102
102
|
|
|
103
103
|
(分风格单元格是 hit@1。地板线是实测的随机命中率:`keyword` 的同域候选有 14 篇,瞎猜大约 7%。)
|
|
104
104
|
|
|
@@ -106,16 +106,17 @@ CI 里再跑一遍 `npm run bench:check` 跟 `bench/expectations.json` 对账。
|
|
|
106
106
|
|
|
107
107
|
| 改动 | 总 hit@1 | 相对本插件 |
|
|
108
108
|
|---|---|---|
|
|
109
|
-
| 本插件 | 49.
|
|
110
|
-
| 去掉同义表 |
|
|
111
|
-
| 去掉笔记别名(不再声明 `aliases`) |
|
|
112
|
-
| 去掉字段加权(`TITLE_EXTRA` 24 → 0) |
|
|
113
|
-
| 同义键碎片不降权(回到改动前) |
|
|
114
|
-
| 桥权重与同义表一致(都算 0.5) | 50.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
| 本插件 | 49.9% | — |
|
|
110
|
+
| 去掉同义表 | 46.7% | -3.1pt |
|
|
111
|
+
| 去掉笔记别名(不再声明 `aliases`) | 56.5% | **+6.7pt** |
|
|
112
|
+
| 去掉字段加权(`TITLE_EXTRA` 24 → 0) | 49.6% | -0.3pt |
|
|
113
|
+
| 同义键碎片不降权(回到改动前) | 49.1% | -0.7pt |
|
|
114
|
+
| 桥权重与同义表一致(都算 0.5) | 50.4% | +0.6pt |
|
|
115
|
+
|
|
116
|
+
「去掉笔记别名」是 **+6.7pt**,在总分上反而更好看 —— 这个语料里别名是把双刃剑:
|
|
117
|
+
它帮自己(写了别名的 37 篇 hit@1 89.2%,没写的 187 篇 23.0%),
|
|
118
|
+
也把同域别的笔记的口语查询抢过来(语料里同域的别名共用同一个口语域词,而别名是程序按比例随机发的)。
|
|
119
|
+
真库里别名是人手写的、更克制,所以别拿这一行下结论 —— 判断别名只看探针的别名组(5/5)。
|
|
119
120
|
|
|
120
121
|
最后两行是一次明确的取舍。改动前,桥组(查询里的词在目标笔记里一个字都没有)
|
|
121
122
|
只有 27.3%:查询自己的字面碎片(`崩了` 的「了」在别的笔记里到处都是)按满权重计分,
|
|
@@ -123,35 +124,45 @@ CI 里再跑一遍 `npm run bench:check` 跟 `bench/expectations.json` 对账。
|
|
|
123
124
|
实测**打红了真实存在的测试**:`test/recall.test.mjs` 的注入预算阶梯用 `代理节点与部署` 当查询,
|
|
124
125
|
而 `代理`、`部署` 既是同义键、也是笔记正文里的原词,剔掉等于把真证据扔了。
|
|
125
126
|
所以改成**降权不剔除**(`BRIDGED_FRAGMENT_WEIGHT = 0.5`,桥到的技术词算 `BRIDGE_WEIGHT = 1`)。
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
降权只针对键的**碎片**(单字、相邻双字),键本身照旧算满权重 —— 这一条是量出来的:
|
|
128
|
+
一开始连键本身一起降权,`verbatim` 对照组从 100% 掉到 95.1%,放开键本身后回到 98.2%,
|
|
129
|
+
而桥组一点没退。道理很直白:查询里原样出现的那个词是实打实的证据,降权的理由只是
|
|
130
|
+
它拆出来的碎片在别的笔记里到处都是。
|
|
131
|
+
|
|
132
|
+
代价是与「桥权重打回 0.5」相比总分少 0.6 个点(50.4% → 49.9%)。接受它的理由:
|
|
133
|
+
合成语料里绝大多数查询本来就有字面落点,总分被它们主导;桥是唯一在字面完全无交集时
|
|
134
|
+
还能工作的机制 —— 同一批桥查询,桥权重打回 0.5 只有 25.0%,碎片不降权 43.8%。
|
|
135
|
+
(历史值:修「碎片压桥」之前,那一版语料上这组查询只有 27.3%。)
|
|
136
|
+
|
|
137
|
+
「去掉字段加权」那一行要单独说:它现在是 -0.3pt(拿掉略微更差),**在一次引擎改动之前它是 +4pt**
|
|
138
|
+
(拿掉明显更好),而**真实库的中立对照一直说拿掉更差**(见下)。当时的冲突定位到机制上:
|
|
133
139
|
合成语料的标题是从正文模板派生的、与正文近乎重复,抬高标题权重只是放大域词噪声;
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
真实库的标题是人手提炼的。现在前两把尺子都偏向「留着字段加权」,但理由仍是上面这条 ——
|
|
141
|
+
不是冲突消失了,是那把合成尺子的标题变真实了一点。两者不一致时以对照 C 为准。
|
|
136
142
|
|
|
137
|
-
### 桥组:
|
|
143
|
+
### 桥组:32 条,65.6%
|
|
138
144
|
|
|
139
145
|
| 组 | 条数 | hit@1 | 说明 |
|
|
140
146
|
|---|---|---|---|
|
|
141
|
-
| 桥组(全部) |
|
|
142
|
-
| 其中公开词族 |
|
|
143
|
-
| 其中留出词族 |
|
|
147
|
+
| 桥组(全部) | 32 | 65.6% | 查询与目标笔记的字面重叠为 0,命中只能靠同义桥 |
|
|
148
|
+
| 其中公开词族 | 20 | 65.0% | 调表时看得见的词族 |
|
|
149
|
+
| 其中留出词族 | 12 | 66.7% | 没参与调表的词族,看一点泛化 |
|
|
144
150
|
|
|
145
151
|
这组查询是从引擎自己读的那张同义表里长出来的,所以它证明的是**扩展管道通不通**,
|
|
146
|
-
不是「表能不能猜中没见过的说法」,后者只有留出那
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
不是「表能不能猜中没见过的说法」,后者只有留出那 12 条能说一点。
|
|
153
|
+
真正的瓶颈在供给面:这张表有两层来源,口径必须分开说 ——
|
|
154
|
+
|
|
155
|
+
- **前四波是从真实库里挖出来的**(专有名词、某个人的叫法),换一套词汇就大面积失效;
|
|
156
|
+
- **第五波是通用技术口语**(「变慢」「退不回去」「改表结构」这类任何写中文技术笔记的人都会用的说法),
|
|
157
|
+
不依赖具体某个库,装到别人机器上也能命中。
|
|
158
|
+
|
|
159
|
+
新表扩容后,这套语料的 16 个域词、14 个事件词全部有桥;
|
|
160
|
+
但仍有一批口语键因为自带字面碎片而够不上「纯桥」资格被丢弃(本次丢弃 22 组)。
|
|
161
|
+
704 条查询里,口语风格那批字面重叠为 0 的只有 30 条 —— 绝大多数「口语查询」
|
|
162
|
+
仍能靠碎片碰到笔记,真正需要跨说法的只有那几条。
|
|
151
163
|
|
|
152
|
-
未命中的查询里,
|
|
153
|
-
|
|
154
|
-
没有灾难性的空召回。
|
|
164
|
+
未命中的查询里,101 条的 top-1 是同一领域里搞错了事件,242 条是同一事件里搞错了领域,
|
|
165
|
+
「完全不沾边」9 次、「一条都没召回」0 次。失败都是「差一点」,没有灾难性的空召回。
|
|
155
166
|
|
|
156
167
|
### 二、真库探针(`npm run probe`)
|
|
157
168
|
|
|
@@ -163,10 +174,10 @@ CI 里再跑一遍 `npm run bench:check` 跟 `bench/expectations.json` 对账。
|
|
|
163
174
|
| 口径 | 21 | 真实场景的中文口语问法 | recall@4 21/21 |
|
|
164
175
|
| 桥探针 | 10 | 全局同义表(大白话问英文/术语密集的笔记) | recall@4 9/10 |
|
|
165
176
|
| 别名探针 | 5 | 笔记自己的 `aliases:`(关键词在全局桥里一个都没有) | recall@4 5/5 |
|
|
166
|
-
| 死桥自检 |
|
|
167
|
-
| 对照 A |
|
|
168
|
-
| 对照 B |
|
|
169
|
-
| 对照 C |
|
|
177
|
+
| 死桥自检 | 519 值 | 同义桥的每个值是否真的存在于语料(不存在等于白写) | 121 个死值(均为通用词) |
|
|
178
|
+
| 对照 A | 189 | 文件名当查询的底线(对标题权重有偏袒) | top1 36/189 · recall@4 96/189 |
|
|
179
|
+
| 对照 B | 164 | 笔记中文标题当查询的底线(同样偏袒) | top1 159/164 · recall@4 163/164 |
|
|
180
|
+
| 对照 C | 176 | 正文独有词当查询(该词在任何标题里都没有) | top1 140/176 · recall@4 164/176 |
|
|
170
181
|
|
|
171
182
|
对照 A 和 B 都拿标题当查询,所以它们对「标题里的词多计几遍」这类参数是**结构性偏袒**的:
|
|
172
183
|
抬高标题权重几乎必然让这两组变好。它们既不该当目标函数,也不能用来证明「抬高标题权重是安全的」。
|
|
@@ -196,6 +207,58 @@ CI 里再跑一遍 `npm run bench:check` 跟 `bench/expectations.json` 对账。
|
|
|
196
207
|
**一个漏字段的指纹比没有指纹更坏**,因为它是权威口吻的错误。现在指纹覆盖所有喂进索引的字段,
|
|
197
208
|
并有专门的回归测试守着。
|
|
198
209
|
|
|
210
|
+
### 三、公开数据集:LongMemEval-S
|
|
211
|
+
|
|
212
|
+
前两把尺子都是自己出的卷子,这一把不是:LongMemEval 是 ICLR 2025 的记忆评测集
|
|
213
|
+
(`xiaowu0162/longmemeval-cleaned`)。我们跑的是 S 版,500 题,每题一个约 48 个 session 的
|
|
214
|
+
haystack,题目分六类。
|
|
215
|
+
|
|
216
|
+
做法:一个 session = 一篇笔记(角色前缀保留),query = 问题原文(**不读** `question_date`,
|
|
217
|
+
也**不看** `has_answer` 标注),命中 = gold session 进 top-k。500 题跑 18 秒,零模型调用。
|
|
218
|
+
|
|
219
|
+
| 指标 | 全部 500 题 |
|
|
220
|
+
| --- | --- |
|
|
221
|
+
| RecallAny@1 | 86.2% |
|
|
222
|
+
| RecallAny@5 | **97.0%** |
|
|
223
|
+
| Recall@5(gold 覆盖率) | 90.7% |
|
|
224
|
+
| MRR@5 | 0.9056 |
|
|
225
|
+
| RecallAny@10 | 98.2% |
|
|
226
|
+
|
|
227
|
+
| 题型 | 题数 | RecallAny@1 | RecallAny@5 | Recall@5 | MRR@5 |
|
|
228
|
+
| --- | --- | --- | --- | --- | --- |
|
|
229
|
+
| knowledge-update | 78 | 94.9% | 100.0% | 97.4% | 0.971 |
|
|
230
|
+
| multi-session | 133 | 88.0% | 96.2% | 82.6% | 0.915 |
|
|
231
|
+
| single-session-assistant | 56 | 98.2% | 100.0% | 100.0% | 0.991 |
|
|
232
|
+
| single-session-preference | 30 | 36.7% | 86.7% | 86.7% | 0.548 |
|
|
233
|
+
| single-session-user | 70 | 91.4% | 98.6% | 98.6% | 0.944 |
|
|
234
|
+
| temporal-reasoning | 133 | 82.7% | 96.2% | 87.6% | 0.882 |
|
|
235
|
+
|
|
236
|
+
三句必须说在前面的实话:
|
|
237
|
+
|
|
238
|
+
1. **这份数据量不出本插件的长处。** 同一份数据我们用教科书 BM25(同分词口径)也跑了一遍,
|
|
239
|
+
五个指标**一模一样**。原因很实在:LongMemEval 是英文的,session 没有标题、没有别名、
|
|
240
|
+
没有中文口语,本插件多出来的那几样在这里基本没被激活。它只说明「英文词面检索不输常规写法」,
|
|
241
|
+
机制到底值多少,回上面两把尺子看。
|
|
242
|
+
2. **别拿它跟别家数字直接相减。** thoth-mem 公布过 LongMemEval-S 470 题、RecallAny@5 91.9%。
|
|
243
|
+
我们用的是 cleaned 版 500 题 + 自己的 harness,题集版本、命中口径、读不读日期都未必一样,
|
|
244
|
+
相减没有意义。能担保的是:harness 与逐题结果都在仓库里(`bench/longmemeval.mjs`、
|
|
245
|
+
`bench/longmemeval-results.json`),可以逐题核。
|
|
246
|
+
3. **最差的是偏好题**(36.7%@1):「我喜欢什么」这类问题本身没什么词面抓手。这是零模型路线的
|
|
247
|
+
边界 —— 偏好类想做得更好,要么开可选的 `llm.js`,要么就老老实实把偏好写进标题。
|
|
248
|
+
|
|
249
|
+
复算:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
mkdir -p bench/.tmp && curl -L -o bench/.tmp/longmemeval_s_cleaned.json \
|
|
253
|
+
https://hf-mirror.com/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_s_cleaned.json
|
|
254
|
+
node bench/longmemeval.mjs --data bench/.tmp/longmemeval_s_cleaned.json # 本插件
|
|
255
|
+
node bench/longmemeval.mjs --data bench/.tmp/longmemeval_s_cleaned.json --engine unigram # 教科书 BM25
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
数据 264MB,不入库也不进 CI;CI 里跑的是同格式的小夹具
|
|
259
|
+
(`bench/fixtures/longmemeval-mini.json`,四道题分别覆盖「英文命中 / 中文命中 /
|
|
260
|
+
多 gold 只找到一半 / 零字面重叠必失手」),判据被改动就会红。
|
|
261
|
+
|
|
199
262
|
## 工作机制
|
|
200
263
|
|
|
201
264
|
### 召回:发生在每个用户轮的第一个 step
|
|
@@ -212,7 +275,7 @@ CI 里再跑一遍 `npm run bench:check` 跟 `bench/expectations.json` 对账。
|
|
|
212
275
|
|
|
213
276
|
| # | 机制 | 治什么 |
|
|
214
277
|
|---|---|---|
|
|
215
|
-
| 1 | 同义扩展(
|
|
278
|
+
| 1 | 同义扩展(177 条口语→技术词)+ 连字符再切分 + 命中键只降「碎片」权重 | 中文口语与笔记用词对不上的同义鸿沟 |
|
|
216
279
|
| 2 | 超长正文按 `##` 切桶 + **整篇**长度归一 | 单篇巨文霸榜(一篇几百 KB 的笔记曾占掉四分之一召回位) |
|
|
217
280
|
| 3 | 寒暄 / 极短消息门控 | 「继续 / 嗯」这类消息 10/10 注入噪音 |
|
|
218
281
|
| 4 | 写入闸门(fail-closed) | 不合规文件名、缺失 frontmatter、疑似真实凭据 |
|
|
@@ -258,7 +321,7 @@ CI 里再跑一遍 `npm run bench:check` 跟 `bench/expectations.json` 对账。
|
|
|
258
321
|
| 往库里写 `bad-name.md` | 拒绝:**「文件名不合规。必须是 \<user\|feedback\|project\|reference\>-\<小写slug\>.md」** |
|
|
259
322
|
| 写一篇带 `api_key: sk-…` 的笔记 | 拒绝:**「疑似含真实凭据(OpenAI 风格密钥)」** |
|
|
260
323
|
|
|
261
|
-
|
|
324
|
+
作者实测(本地库几十篇笔记、索引七十余行):四条全中,两次被拒的写入**磁盘上零字节变化** —— 被拒后索引与操作前逐字一致。
|
|
262
325
|
|
|
263
326
|
这就是它跟别家最本质的一处不同:别人的记忆预算卡在**注入**上(超了就少喂几段给模型),这里卡在**写入**上 —— 索引文件本身长不到超限,所以「超限的部分静默不再被召回」在这个库里不会发生。
|
|
264
327
|
|
|
@@ -342,7 +405,7 @@ Electron 载体走 `file://` + IPC 时可能没有 webServer,那时召回照
|
|
|
342
405
|
|
|
343
406
|
召回准不准,九成取决于「你会怎么问」到「笔记里怎么写」之间那座桥。桥有两层。
|
|
344
407
|
|
|
345
|
-
**全局同义表**(`bm25.js` 的 `SYNONYMS`,
|
|
408
|
+
**全局同义表**(`bm25.js` 的 `SYNONYMS`,177 条 / 519 个值)管的是一类说法:
|
|
346
409
|
`梯子 / 翻墙 / 科学上网` → `proxy, http-connect, socks5`。同义项默认按 0.5 权重计入,
|
|
347
410
|
查询命中了某个键时,桥到的技术词按 1 计入、该键自身的字面碎片降到 0.5(上一节的取舍)。
|
|
348
411
|
|
|
@@ -398,16 +461,17 @@ aliases: 梯子, 翻墙, 科学上网, 节点, 代理
|
|
|
398
461
|
### 二、真的短板(还做不到)
|
|
399
462
|
|
|
400
463
|
- **不做语义检索**:没有 embedding、没有向量库。中文用「单字 + 相邻双字」切分,
|
|
401
|
-
同义改写、隐含意图会漏检,目前唯一的桥接是一张
|
|
464
|
+
同义改写、隐含意图会漏检,目前唯一的桥接是一张 177 条的手工同义表。
|
|
402
465
|
需要模型的那两件事(矛盾检测、摘要)做成了可选工具(见「可选:语义增强」),
|
|
403
466
|
但它们不参与检索排序 —— 排序仍然是纯词面的,这一点不打算含糊。
|
|
404
|
-
-
|
|
467
|
+
- **没有跨插件排名**:LongMemEval-S 跑得了(见上),但那是一个英文词面基准,且各家 harness 不同;
|
|
468
|
+
合成基准与真库探针都是自建口径,只能自己跟自己比。
|
|
405
469
|
|
|
406
470
|
### 三、曾经没做、现在补上了
|
|
407
471
|
|
|
408
472
|
- **不自动归档**:已实现。它原来确实写在这一节里,理由也写了「不可逆」。
|
|
409
473
|
但确定性分层不需要 LLM,只是当时没做,所以补上了。
|
|
410
|
-
- **同义鸿沟**:同义表已扩到
|
|
474
|
+
- **同义鸿沟**:同义表已扩到 177 条并逐条经探针验证;笔记别名也真的被用起来了。
|
|
411
475
|
- **不自动合并**:降级成只出报告(体检器报疑似重复主题与失联路径),不改文件。
|
|
412
476
|
|
|
413
477
|
### 路线图
|
|
@@ -503,11 +567,12 @@ dsh plugin --profile <profile> remove dsh-engramory-recall
|
|
|
503
567
|
## 测试与自检
|
|
504
568
|
|
|
505
569
|
```bash
|
|
506
|
-
npm test # 分词 / BM25 / 切桶 / 门控 / 闸门 / 索引闸门 / 技能注册 / 体检 / 审计 / 沉淀提醒 / 归档 / 打包 / 路由
|
|
570
|
+
npm test # 分词 / BM25 / 切桶 / 门控 / 闸门 / 索引闸门 / 技能注册 / 体检 / 审计 / 沉淀提醒 / 归档 / 打包 / 路由 / 公开基准 harness
|
|
507
571
|
npm run check # 接线自检:配置键认不认、库路径对不对、装的是哪一份、包清单漏没漏文件
|
|
508
572
|
npm run probe # 召回质量探针:3 组 query + 三道对照,跑在真实记忆库上
|
|
509
573
|
npm run probe -- --save # 把这次结果存成基线,之后每次跑自动比对
|
|
510
574
|
npm run bench:check # 合成基准 + 和 bench/expectations.json 对账(CI 跑的就是它)
|
|
575
|
+
node bench/longmemeval.mjs --data bench/.tmp/longmemeval_s_cleaned.json # 公开基准(数据要自己下,264MB)
|
|
511
576
|
```
|
|
512
577
|
|
|
513
578
|
CI 跑 `npm test` 和 `npm run bench:check` 两件事。后者是防「数字悄悄漂走」的:任何一项
|
|
@@ -544,7 +609,7 @@ CI 跑 `npm test` 和 `npm run bench:check` 两件事。后者是防「数字悄
|
|
|
544
609
|
| 自动归档 | dsh-mneme `src/dream/sleep.js:411` `phaseDemotion` | 落地但换了实现:它截断 + 存 `_full_content`,本插件整段搬家、不截断 |
|
|
545
610
|
| 时间衰减 | 灵枢 `rust/src/freshness.rs` | 实测证伪。六档半衰期扫描,recall 恒定、top-1 只有一档 +1(噪声级)。它能用是因为它的 `protected_floor` 依赖人工 `importance` 字段,Engramory 笔记没这个字段 |
|
|
546
611
|
| 时间乘性加权 | hindsight `engine/search/reranking.py:35-37` | 实测证伪。五档扫描净收益为零,`w ≥ 0.1` 起还要拿对照组去换 |
|
|
547
|
-
| 同义表扩容 | 灵枢的语义归一化 | 落地。
|
|
612
|
+
| 同义表扩容 | 灵枢的语义归一化 | 落地。177 条桥 / 519 个值,每条都经探针验证,另加一道死桥自检 |
|
|
548
613
|
| 查询侧同义词要先分词再算 | 自己踩的坑 | 落地。中文同义词原样返回时永远匹配不上「英文整词 + 汉字双字」的词项形状,写了等于没写且不报错。英文同义词 tokenize 后等于自身,所以这毛病藏了很久 |
|
|
549
614
|
| 笔记级别名(`aliases:`) | dsh-mneme 的 `aliases` 钩子 | 落地。机制本来就在(计两遍 = 满权重),但真实库里 0 篇在用;给 8 篇补上后别名探针 1/5 → 5/5 |
|
|
550
615
|
|
package/bm25.js
CHANGED
|
@@ -321,6 +321,85 @@ export const SYNONYMS = new Map(
|
|
|
321
321
|
记忆库: ['engramory', 'memory', 'bm25'],
|
|
322
322
|
召回: ['recall', 'search', 'bm25'],
|
|
323
323
|
归档: ['archive', 'history'],
|
|
324
|
+
|
|
325
|
+
// ── 2026-10-15 第五波:通用技术口语 ────────────────────────────────
|
|
326
|
+
// 前面几波是**从这个库挖出来的**(专有名词、某个人的叫法),换一个库就大面积失效。
|
|
327
|
+
// 这一波换来源:只收**任何写中文技术笔记的人都会用的说法** ——「变慢」「装不上」
|
|
328
|
+
// 「退不回去」「改表结构」「打日志」…… 不依赖哪个库的词,别人装上也能命中。
|
|
329
|
+
// 判据没变:同一个概念的两种叫法,不是「相关的两件事」。宽词(请求/客户端/队列这些)
|
|
330
|
+
// 故意配上更专的伴词,靠 BRIDGE_WEIGHT 之外的其它词项兜住精度。
|
|
331
|
+
改表结构: ['migration', 'migrate', 'schema', 'ddl', 'alter'],
|
|
332
|
+
回填: ['backfill', 'migration'],
|
|
333
|
+
流水线: ['pipeline', 'ci', 'runner', 'workflow'],
|
|
334
|
+
自动检查: ['ci', 'pipeline', 'runner', 'lint'],
|
|
335
|
+
打日志: ['log', 'logging', 'logger'],
|
|
336
|
+
日志: ['log', 'logging'],
|
|
337
|
+
轮转: ['rotation', 'rotate', 'logrotate'],
|
|
338
|
+
抢锁: ['lock', 'mutex', 'locking'],
|
|
339
|
+
加锁: ['lock', 'mutex'],
|
|
340
|
+
死锁: ['deadlock', 'lock', 'mutex'],
|
|
341
|
+
并发: ['concurrency', 'concurrent', 'parallel', 'mutex'],
|
|
342
|
+
调接口: ['api', 'http', 'request', 'curl'],
|
|
343
|
+
请求: ['request', 'http', 'api', 'curl'],
|
|
344
|
+
重试: ['retry', 'backoff'],
|
|
345
|
+
退避: ['backoff', 'retry'],
|
|
346
|
+
限流: ['ratelimit', 'throttle', 'rate'],
|
|
347
|
+
发包: ['publish', 'npm', 'tarball'],
|
|
348
|
+
发版: ['release', 'publish', 'version', 'tag'],
|
|
349
|
+
版本号: ['version', 'semver', 'tag'],
|
|
350
|
+
自动点网页: ['automation', 'playwright', 'puppeteer', 'selector'],
|
|
351
|
+
自动化: ['automation', 'playwright', 'selenium'],
|
|
352
|
+
选择器: ['selector', 'xpath'],
|
|
353
|
+
找不着: ['search', 'ranking', 'recall', 'bm25'],
|
|
354
|
+
排序: ['ranking', 'rank', 'sort', 'bm25'],
|
|
355
|
+
命中率: ['recall', 'hit', 'precision'],
|
|
356
|
+
时区: ['timezone', 'utc'],
|
|
357
|
+
时间对不上: ['timezone', 'utc', 'offset'],
|
|
358
|
+
时间戳: ['timestamp', 'epoch', 'utc'],
|
|
359
|
+
升版本: ['upgrade', 'dependency', 'major', 'peer'],
|
|
360
|
+
升级依赖: ['upgrade', 'dependency', 'node_modules'],
|
|
361
|
+
版本打架: ['conflict', 'peer', 'dependency'],
|
|
362
|
+
大版本: ['major', 'semver', 'breaking'],
|
|
363
|
+
客户端: ['desktop', 'client', 'electron'],
|
|
364
|
+
打包产物: ['bundle', 'dist', 'artifact'],
|
|
365
|
+
镜像: ['image', 'docker', 'registry'],
|
|
366
|
+
容器: ['docker', 'container', 'pod'],
|
|
367
|
+
队列: ['queue', 'runner', 'wait'],
|
|
368
|
+
变慢: ['latency', 'slow', 'p95'],
|
|
369
|
+
响应慢: ['latency', 'slow', 'p95'],
|
|
370
|
+
慢得没法用: ['latency', 'slow', 'timeout'],
|
|
371
|
+
数算错了: ['wrong', 'mismatch', 'incorrect'],
|
|
372
|
+
结果不对: ['wrong', 'mismatch', 'incorrect'],
|
|
373
|
+
对不上: ['mismatch', 'wrong', 'inconsistent'],
|
|
374
|
+
安装失败: ['install', 'failed', 'npm'],
|
|
375
|
+
东西没了: ['loss', 'lost', 'missing', 'data loss'],
|
|
376
|
+
数据丢了: ['loss', 'lost', 'missing', 'data loss'],
|
|
377
|
+
内存一直涨: ['leak', 'rss', 'memory'],
|
|
378
|
+
泄漏: ['leak', 'leakage'],
|
|
379
|
+
版本冲突: ['conflict', 'peer', 'semver'],
|
|
380
|
+
悄悄不生效: ['silent', 'silently', 'quiet'],
|
|
381
|
+
静默失效: ['silent', 'silently', 'failure'],
|
|
382
|
+
没报错: ['silent', 'quiet'],
|
|
383
|
+
乱码: ['encoding', 'mojibake', 'utf8', 'gbk'],
|
|
384
|
+
编码: ['encoding', 'utf8', 'gbk', 'charset'],
|
|
385
|
+
退不回去: ['rollback', 'revert', 'undo'],
|
|
386
|
+
回滚: ['rollback', 'revert'],
|
|
387
|
+
跑两次: ['duplicate', 'idempotent', 'idempotency'],
|
|
388
|
+
重复触发: ['duplicate', 'idempotent'],
|
|
389
|
+
幂等: ['idempotent', 'idempotency', 'duplicate'],
|
|
390
|
+
卡住不动: ['hang', 'stuck'],
|
|
391
|
+
没响应: ['hang', 'timeout', 'unresponsive'],
|
|
392
|
+
心跳: ['heartbeat', 'ping', 'watchdog'],
|
|
393
|
+
配置不生效: ['config', 'override', 'precedence'],
|
|
394
|
+
改了没用: ['config', 'stale', 'cache'],
|
|
395
|
+
进程退出: ['crash', 'exit'],
|
|
396
|
+
退出码: ['exit', 'code', 'status'],
|
|
397
|
+
调用栈: ['stack', 'trace', 'traceback'],
|
|
398
|
+
权限被拒: ['forbidden', 'denied', '403'],
|
|
399
|
+
审计: ['audit', 'trail'],
|
|
400
|
+
落盘: ['disk', 'persist', 'write'],
|
|
401
|
+
常驻集: ['rss', 'memory', 'resident'],
|
|
402
|
+
排队: ['queue', 'runner', 'wait'],
|
|
324
403
|
}),
|
|
325
404
|
);
|
|
326
405
|
|
|
@@ -438,7 +517,7 @@ export function search(index, query, limit = 5) {
|
|
|
438
517
|
// 命中同义键时,这个键自身的字面碎片降权(见 BRIDGED_FRAGMENT_WEIGHT)。
|
|
439
518
|
const bridged = matchedSynonymKeys(query);
|
|
440
519
|
const fragments = new Set();
|
|
441
|
-
for (const key of bridged) for (const term of tokenize(key)) fragments.add(term);
|
|
520
|
+
for (const key of bridged) for (const term of tokenize(key)) if (term !== key) fragments.add(term);
|
|
442
521
|
const base = queryTerms(query);
|
|
443
522
|
const extras = expandQuery(query);
|
|
444
523
|
if (!base.length && !extras.length) return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-engramory-recall",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Local BM25 auto-recall, fail-closed write guard, static doctor and a memory panel for the Engramory markdown store — no network, no model, no daemon.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|