Skip to content

v7.15.4:召回快了约一半(simhash 按列计数、全文库复用连接、count 缓存),结果逐字不变;修 scrub 在新版 SQLite 下的误报 - #25

Merged
sixgodgit merged 2 commits into
mainfrom
claude/intelligent-hawking-oeyjx8
Oct 26, 2026

Conversation

@sixgodgit

@sixgodgit sixgodgit commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

实测

测试数据是用 LongMemEval-S 的真实对话造的一个 1 万轮的库(970 个会话)。按 Hermes 的方式调用 prefetch(每轮对话前的那一次召回),共 100 个问题。新旧两版用同一份数据快照,在同一时间段内交替各测两次:

v7.15.3(main) 本 PR
prefetch p50 81.9 / 75.0 ms 40.6 / 40.5 ms
prefetch p95 118.7 / 110.7 ms 58.9 / 59.4 ms
每轮先写一条再召回,p50 58.4 / 58.6 ms 41.2 / 45.9 ms

结果不变:固定 PYTHONHASHSEED=0 后,新旧两版对 100 个问题的 prefetch 输出逐字相同(100/100)。

改动

剖析时把召回全部改成同步执行,cProfile 就能看到全部开销。主要发现两处:

  1. l3_search_core.simhash 占了一次召回的约六成
    • 召回时要给每条候选算一个 128 位指纹。原来的写法是「词元数 × 128」次 Python 循环,每段文本约 0.8 ms,而一次召回约有 80 条候选。
    • 现在把每个词元的 md5 存成 128 位二进制串(缓存有上限,跨文本复用),再按列统计 1 的个数,每段约 0.13 ms。
    • 指纹和原来逐位相同:用 6113 段真实文本逐条比对,0 处不同。新测试 tests/test_simhash_fast.py 保留了原实现作对照。
  2. sandglass_sqlite 每次查询都新开一个连接
    • 原来每次都要重跑一遍 WAL 检查和建表,页缓存也是冷的,一次召回要开十来个连接。count() 每次都对整张表 COUNT,一次召回要调两次。
    • 复用连接:现在一个进程只用一个连接,调用方本来就都在模块锁里。库文件被删掉重建(按 inode 判断)或换了路径时,会换一个新连接。
    • close() 不再真正关闭:调用方随手调用的 close() 现在什么都不做,要真正关闭得走 _reset_db()。原因是 CI 上 Python 3.10 在并发开库测试里出现过段错误:一个线程正在关连接,另一个线程正在用同一个连接。
    • count() 缓存:库没变时直接用上次的结果。「没变」由同一连接的 data_version、schema_version、total_changes 一起判定,所以别的进程写入也能识别出来。
    • scrub 删库重建、删除流程做 VACUUM 之前,都会先调用 _reset_db()。
    • 新测试 tests/test_sandglass_conn.py 覆盖了:连接复用、随手 close、删库(包括没先关连接就删的情况)、别的连接写入、表重建、VACUUM,以及并发 close 加使用。

顺带修:保险箱开着时,scrub --apply 误报「磁盘上还有」

  • 现象:scrub 清理完后,会按原始字节把数据目录复查一遍。复查时除了找原文,还会按规则识别「像密钥的东西」。保险箱的库里每条记录都以明文存着类别、标签和末四位(password 密码 4567),这几列连在一起,就被规则认成了「密码 4567」。
  • 与 SQLite 版本有关:在系统自带的 SQLite 3.45 上,碰巧没有连成这样。但在 SQLite 3.53 上(uv 装的 Python 自带这个版本,新版 Hermes 用的就是这种环境),20 次里有 17 次误报,test_secret_vault.py::test_scrub_to_vault 也会因此失败。
  • 修法:保险箱自己的库文件现在只查原文;如果原文真在里面,照样会报。修后 0/20。
  • 新测试:test_residual_scan_ignores_vault_metadata 不依赖 SQLite 版本,每次结果确定。

验证

  • 本地 pytest:Python 3.11(SQLite 3.45)和 Python 3.10(uv 装的,SQLite 3.53)都是 676 passed,2 skipped。
  • 3.10 上并发开库测试:修之前 5/5 段错误,修之后 5/5 通过。

顺带发现(不是本 PR 引入的)

不固定 PYTHONHASHSEED 时,同一个版本在两个进程里处理同一份数据,约一半的问题输出不同:分数相同的候选按字符串集合的遍历顺序排列。已经另外记成任务,之后改成按稳定的键排序。

🤖 Generated with Claude Code

https://claude.ai/code/session_019ZZeQFn5F6mEbmX8vPsf4g

1 万轮真实对话(LongMemEval-S),Hermes prefetch p50 从 85–91 ms 到 44–50 ms。
- simhash:词元 md5 记成 128 位二进制串、按列数 1,指纹与原实现逐位相同
- sandglass_sqlite:一个进程复用一个连接(库被删重建 / 连接被关时换新),
  count() 按 data_version / schema_version / total_changes 缓存
- scrub、erasure 删库或 VACUUM 前先关掉复用的连接

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZZeQFn5F6mEbmX8vPsf4g
- sandglass_sqlite:复用的连接用 _SharedConnection,close() 什么都不做、_reset_db() 才真关。
  CI 上 Python 3.10 在并发开库测试里段错误:一个线程 close()、另一个线程正在用同一个连接
- secret_scrub:清理后的原始字节复查,对 vault.db 只找原文。保险箱每条明文存着类别、标签、末四位,
  按规则会被认成「密码 4567」;SQLite 3.53 下 20 次里 17 次误报,修后 0/20

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZZeQFn5F6mEbmX8vPsf4g
@sixgodgit sixgodgit changed the title v7.15.4:召回快了约一半(simhash 按列计数、全文库复用连接、count 缓存),结果逐字不变 v7.15.4:召回快了约一半(simhash 按列计数、全文库复用连接、count 缓存),结果逐字不变;修 scrub 在新版 SQLite 下的误报 Oct 26, 2026
@sixgodgit
sixgodgit merged commit dfe4536 into main Oct 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants