dsh-engramory-recall 0.6.0 → 0.6.2

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 CHANGED
@@ -3,6 +3,42 @@
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.2] — 2026-10-15
7
+
8
+ ### 变更
9
+
10
+ - **查询侧丢掉英文虚词**(`bm25.js` 的 `EN_STOPWORDS` + `queryTerms()`):`what / is / the / of /
11
+ did / how…` 这类语法词不再参与打分。只过滤查询、**索引侧一个词不动** —— 索引侧删词会顺带
12
+ 改掉每篇的 `len`,那等于动了长度归一,不值当。
13
+ LongMemEval-S 500 题:RecallAny@1 **86.2% → 87.8%**、Recall@5 90.7% → 91.5%、
14
+ MRR@5 0.9056 → 0.9155。自建合成基准与真库探针不变(那两把尺子的查询是中文的)。
15
+ 虚词表只收语法词,`time / name / like` 这类可能带语义的词一个不收;只剩虚词的查询退回原样,
16
+ 不清空(清空等于一条都召不回)。
17
+ - README 的 LongMemEval 章节据此重算:本插件现在比教科书 BM25 高 1.6pt @1,并写明**这 1.6pt
18
+ 全部来自这一处通用改动**,不是本插件独门机制的功劳。
19
+ - 「被实测证伪的路」加入第六条:**伪相关反馈查询扩展**(拿 top-1 的高频词再搜一遍)。
20
+ 实测 RecallAny@1 89.2% → 48.3%、Recall@5 只涨 0.4pt —— 只看 top-5 的话会被误判成有效。
21
+
22
+ ## [0.6.1] — 2026-10-15
23
+
24
+ ### 新增
25
+
26
+ - **公开基准:LongMemEval-S**(`xiaowu0162/longmemeval-cleaned` 的 S 版,500 题)。
27
+ harness 是 `bench/longmemeval.mjs`,逐题结果随仓库发布(`bench/longmemeval-results.json`)。
28
+ 本引擎 500 题跑 18 秒、零模型调用:RecallAny@1 86.2%、**RecallAny@5 97.0%**、
29
+ Recall@5(gold 覆盖率)90.7%、MRR@5 0.9056、RecallAny@10 98.2%。
30
+ harness 支持 `--engine unigram|bigram` 跑教科书 BM25 对照。
31
+ - **同格式小夹具 + 测试**(`bench/fixtures/longmemeval-mini.json`、`test/longmemeval.test.mjs`,5 例):
32
+ 真数据集 264MB 不进 CI,夹具用四道题(英文命中 / 中文命中 / 多 gold 只找到一半 / 零字面重叠
33
+ 必失手)守住判据与两个开关;判据被改动就红。
34
+
35
+ ### 说明
36
+
37
+ - 在这份**英文**数据集上,本引擎与教科书 BM25 的五个指标完全相同:多出来的机制
38
+ (中文口语桥、标题字段加权、别名双计)在这里基本没被激活。README 里写明了这一点 ——
39
+ 它只说明「英文词面检索不输常规写法」,不是「我们更准」的证据。
40
+ - 与 `thoth-mem` 公布的 470 题成绩不可直接相减(题集版本与 harness 都不同)。
41
+
6
42
  ## [0.6.0] — 2026-10-15
7
43
 
8
44
  ### 新增
package/README.md CHANGED
@@ -32,7 +32,7 @@
32
32
  | **三年后还能跑** | 不需要编译原生模块、不需要常驻服务 | 带 `better-sqlite3` 编译、rust 工具链、独立 daemon 的方案,会在某次系统升级后让你回来看这篇 README |
33
33
 
34
34
  四问都过了,才轮到比功能。功能这一层,本插件的取舍是**把不确定性变成可测量的东西**:
35
- 新机制必须在库上跑出数字,跑不出来就不上(文末五条被证伪的路就是这么留下的)。
35
+ 新机制必须在库上跑出数字,跑不出来就不上(文末六条被证伪的路就是这么留下的)。
36
36
 
37
37
  ## 跟同类插件对照
38
38
 
@@ -71,9 +71,9 @@
71
71
 
72
72
  ### 短板
73
73
 
74
- - **没有公开基准**:`thoth-mem` 公布了 LongMemEval-S 470 题的成绩(RecallAny@5 432/470、
75
- Recall@5 84.8%、NDCG@10 0.8165、p95 9.35ms)。本插件的探针跑在自己的库上,口径与它不可比。
76
- 下面那一节是自建的可复算基准,但它仍然是自建口径,别当成跨插件排名。
74
+ - **公开基准只跑了一半**:LongMemEval-S 500 题跑出 RecallAny@5 97.4%(见下),但那只测英文词面
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,7 +82,7 @@
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
 
@@ -137,7 +137,7 @@ CI 里再跑一遍 `npm run bench:check` 跟 `bench/expectations.json` 对账。
137
137
  「去掉字段加权」那一行要单独说:它现在是 -0.3pt(拿掉略微更差),**在一次引擎改动之前它是 +4pt**
138
138
  (拿掉明显更好),而**真实库的中立对照一直说拿掉更差**(见下)。当时的冲突定位到机制上:
139
139
  合成语料的标题是从正文模板派生的、与正文近乎重复,抬高标题权重只是放大域词噪声;
140
- 真实库的标题是人手提炼的。现在两把尺子都偏向「留着字段加权」,但理由仍是上面这条 ——
140
+ 真实库的标题是人手提炼的。现在前两把尺子都偏向「留着字段加权」,但理由仍是上面这条 ——
141
141
  不是冲突消失了,是那把合成尺子的标题变真实了一点。两者不一致时以对照 C 为准。
142
142
 
143
143
  ### 桥组:32 条,65.6%
@@ -207,6 +207,59 @@ CI 里再跑一遍 `npm run bench:check` 跟 `bench/expectations.json` 对账。
207
207
  **一个漏字段的指纹比没有指纹更坏**,因为它是权威口吻的错误。现在指纹覆盖所有喂进索引的字段,
208
208
  并有专门的回归测试守着。
209
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 题 | 教科书 BM25(同分词口径) |
220
+ | --- | --- | --- |
221
+ | RecallAny@1 | **87.8%** | 86.2% |
222
+ | RecallAny@5 | **97.4%** | 97.0% |
223
+ | Recall@5(gold 覆盖率) | **91.5%** | 90.7% |
224
+ | MRR@5 | **0.9155** | 0.9056 |
225
+ | RecallAny@10 | **98.4%** | 98.2% |
226
+
227
+ | 题型 | 题数 | RecallAny@1 | RecallAny@5 | Recall@5 | MRR@5 |
228
+ | --- | --- | --- | --- | --- | --- |
229
+ | knowledge-update | 78 | 97.4% | 100.0% | 98.7% | 0.987 |
230
+ | multi-session | 133 | 88.0% | 97.0% | 85.3% | 0.919 |
231
+ | single-session-assistant | 56 | 100.0% | 100.0% | 100.0% | 1.000 |
232
+ | single-session-preference | 30 | 40.0% | 86.7% | 86.7% | 0.583 |
233
+ | single-session-user | 70 | 92.9% | 100.0% | 100.0% | 0.950 |
234
+ | temporal-reasoning | 133 | 85.0% | 96.2% | 86.6% | 0.892 |
235
+
236
+ 三句必须说在前面的实话:
237
+
238
+ 1. **这 1.6 个点的来源要说清楚。** 本插件比教科书 BM25 高 1.6pt @1,差的那部分**全部**来自一处通用改动:
239
+ 查询侧丢掉英文虚词(`what / is / the / of / did / how…`)。是我们自己的机制(同义表、别名、
240
+ 字段加权、中文分词)在这份数据上没被激活 —— LongMemEval 是英文的,session 没有标题、
241
+ 没有别名、没有中文口语,它们在这里没事可做。所以这张表证明的是「英文词面检索不输、且
242
+ 基础卫生做到位」,**不是**「本插件的独门设计更好」。机制值多少,回上面两把尺子看。
243
+ 2. **别拿它跟别家数字直接相减。** thoth-mem 公布过 LongMemEval-S 470 题、RecallAny@5 91.9%。
244
+ 我们用的是 cleaned 版 500 题 + 自己的 harness,题集版本、命中口径、读不读日期都未必一样,
245
+ 相减没有意义。能担保的是:harness 与逐题结果都在仓库里(`bench/longmemeval.mjs`、
246
+ `bench/longmemeval-results.json`),可以逐题核。
247
+ 3. **最差的是偏好题**(40.0%@1):「我喜欢什么」这类问题本身没什么词面抓手。这是零模型路线的
248
+ 边界 —— 偏好类想做得更好,要么开可选的 `llm.js`,要么就老老实实把偏好写进标题。
249
+
250
+ 复算:
251
+
252
+ ```bash
253
+ mkdir -p bench/.tmp && curl -L -o bench/.tmp/longmemeval_s_cleaned.json \
254
+ https://hf-mirror.com/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_s_cleaned.json
255
+ node bench/longmemeval.mjs --data bench/.tmp/longmemeval_s_cleaned.json # 本插件
256
+ node bench/longmemeval.mjs --data bench/.tmp/longmemeval_s_cleaned.json --engine unigram # 教科书 BM25
257
+ ```
258
+
259
+ 数据 264MB,不入库也不进 CI;CI 里跑的是同格式的小夹具
260
+ (`bench/fixtures/longmemeval-mini.json`,四道题分别覆盖「英文命中 / 中文命中 /
261
+ 多 gold 只找到一半 / 零字面重叠必失手」),判据被改动就会红。
262
+
210
263
  ## 工作机制
211
264
 
212
265
  ### 召回:发生在每个用户轮的第一个 step
@@ -412,7 +465,8 @@ aliases: 梯子, 翻墙, 科学上网, 节点, 代理
412
465
  同义改写、隐含意图会漏检,目前唯一的桥接是一张 177 条的手工同义表。
413
466
  需要模型的那两件事(矛盾检测、摘要)做成了可选工具(见「可选:语义增强」),
414
467
  但它们不参与检索排序 —— 排序仍然是纯词面的,这一点不打算含糊。
415
- - **没有公开基准**:只有自建口径的合成基准与真库探针(见上),跨插件排名无从谈起。
468
+ - **没有跨插件排名**:LongMemEval-S 跑得了(见上),但那是一个英文词面基准,且各家 harness 不同;
469
+ 合成基准与真库探针都是自建口径,只能自己跟自己比。
416
470
 
417
471
  ### 三、曾经没做、现在补上了
418
472
 
@@ -514,11 +568,12 @@ dsh plugin --profile <profile> remove dsh-engramory-recall
514
568
  ## 测试与自检
515
569
 
516
570
  ```bash
517
- npm test # 分词 / BM25 / 切桶 / 门控 / 闸门 / 索引闸门 / 技能注册 / 体检 / 审计 / 沉淀提醒 / 归档 / 打包 / 路由
571
+ npm test # 分词 / BM25 / 切桶 / 门控 / 闸门 / 索引闸门 / 技能注册 / 体检 / 审计 / 沉淀提醒 / 归档 / 打包 / 路由 / 公开基准 harness
518
572
  npm run check # 接线自检:配置键认不认、库路径对不对、装的是哪一份、包清单漏没漏文件
519
573
  npm run probe # 召回质量探针:3 组 query + 三道对照,跑在真实记忆库上
520
574
  npm run probe -- --save # 把这次结果存成基线,之后每次跑自动比对
521
575
  npm run bench:check # 合成基准 + 和 bench/expectations.json 对账(CI 跑的就是它)
576
+ node bench/longmemeval.mjs --data bench/.tmp/longmemeval_s_cleaned.json # 公开基准(数据要自己下,264MB)
522
577
  ```
523
578
 
524
579
  CI 跑 `npm test` 和 `npm run bench:check` 两件事。后者是防「数字悄悄漂走」的:任何一项
@@ -571,10 +626,11 @@ CI 跑 `npm test` 和 `npm run bench:check` 两件事。后者是防「数字悄
571
626
  | DSH-SEP | 每步 ~1250 字符 guidance + ≤4200 字符快照 | 同上 | 抽取走 `api.deepseek.com` |
572
627
  | **本插件** | **0** | **≤480 字符,且只有指针** | **无** |
573
628
 
574
- ## 五条被实测证伪的路
629
+ ## 六条被实测证伪的路
575
630
 
576
631
  | 尝试 | 结果 |
577
632
  |---|---|
633
+ | 伪相关反馈查询扩展(拿 top-1 的高频词再搜一遍,RM3 那一路) | 在 LongMemEval-S 500 题上前 120 题实测:RecallAny@1 从 89.2% 掉到 **48.3%**。机制:扩出来的词全是 top-1 自己的词汇,等于把「已经赢了的文档」又投了一遍票,稍有偏差的第一次命中会被自己的回声推到第一。Recall@5 只涨 0.4pt,@1 腰斩 —— 这种改动在只看 top-5 的评测里会被误认为「有效」 |
578
634
  | 提高分数门槛 / IDF 门槛过滤误召回 | 正例最低分 4.59、反例最高分 5.54,区间重叠切不开;IDF 门槛会把 recall 从 28/28 打到 13/28 |
579
635
  | freshness 时间衰减(学灵枢 / dsh-mneme) | 六档半衰期扫描:recall 恒为 28/28、top-1 只有一档 +1(噪声级)。库里「旧」的那批恰好是基础事实,衰减就是误伤 |
580
636
  | 时间乘性加权(学 hindsight) | 五档扫描净收益为零,`w ≥ 0.1` 起反而伤对照组 |
package/bm25.js CHANGED
@@ -423,9 +423,45 @@ export function tokenize(text) {
423
423
  return out;
424
424
  }
425
425
 
426
- /** 查询词项去重(打分时同一词项不重复计权)。 */
426
+ /**
427
+ * 英文虚词:查询里带着它们不加分,只稀释真正有区分度的词。
428
+ *
429
+ * 为什么只在**查询侧**过滤、索引侧一个词不动:索引侧删词会同时改掉每篇的
430
+ * `len`,那等于顺手动了长度归一 —— 长度归一刚在 2026-10-15 为 `TITLE_EXTRA`
431
+ * 的事校准过,不该为了这个另起一轮。实测也支持这个选择:查询+索引都过滤
432
+ * 90.0%、只过滤查询 90.8%(LongMemEval-S 前 120 题)。
433
+ *
434
+ * 数据来源:LongMemEval-S 500 题(英文),86.2% → 87.8% RecallAny@1、
435
+ * Recall@5 90.7% → 91.7%、MRR@5 0.904 → 0.914。中文查询几乎不受影响
436
+ * (中文词不进这张表)。
437
+ *
438
+ * 只收「语法词」:疑问词、be/do/have 的变位、介词、代词、限定词。
439
+ * 刻意**不收**任何可能带语义的词(如 `time`、`name`、`like`)——
440
+ * 那类词在短查询里往往是唯一的抓手,删了就是漏检。
441
+ */
442
+ const EN_STOPWORDS = new Set([
443
+ 'the', 'a', 'an', 'and', 'or', 'but', 'if', 'then', 'than', 'that', 'this', 'these', 'those',
444
+ 'what', 'which', 'who', 'whom', 'whose', 'when', 'where', 'why', 'how',
445
+ 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'am',
446
+ 'do', 'does', 'did', 'doing', 'done',
447
+ 'have', 'has', 'had', 'having',
448
+ 'can', 'could', 'will', 'would', 'shall', 'should', 'may', 'might', 'must',
449
+ 'of', 'in', 'on', 'at', 'to', 'from', 'by', 'with', 'for', 'about', 'as', 'into', 'over', 'after', 'before',
450
+ 'it', 'its', 'my', 'me', 'we', 'our', 'you', 'your', 'he', 'him', 'she', 'her', 'they', 'them', 'their',
451
+ 'there', 'here', 'so', 'such', 'no', 'not', 'only', 'just', 'also', 'very', 'much', 'many', 'more', 'most', 'some', 'any', 'all',
452
+ ]);
453
+
454
+ /**
455
+ * 查询词项:去重 + 丢掉英文虚词。
456
+ * 两个例外都保留原样,宁可留噪声也不把查询清空:
457
+ * · 只剩一个词项的查询(很可能那个词就是全部信息);
458
+ * · 全是虚词的查询(「what about this」这种),过滤后为空等于一条都召不回。
459
+ */
427
460
  export function queryTerms(text) {
428
- return [...new Set(tokenize(text))];
461
+ const terms = [...new Set(tokenize(text))];
462
+ if (terms.length < 2) return terms;
463
+ const kept = terms.filter((term) => !EN_STOPWORDS.has(term));
464
+ return kept.length ? kept : terms;
429
465
  }
430
466
 
431
467
  /** 查询里命中的同义键(按原文字面,大小写不敏感)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-engramory-recall",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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",