Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

chat-memory-mmu:专注于web端LLM聊天系统的微型记忆调度

给 LLM 聊天系统做的一套记忆调度模块(MMU)。核心目标只有一个:在有限的上下文里,让 LLM 用最少的 token 拿到该记住的东西。

背景

陪伴系统里,每条用户消息原本都会全量搜索 Obsidian 记忆库,100% 触发冷搜索,没有分层、没有缓存。上下文越长,每轮固定开销越大,还容易把无关记忆塞给 LLM 造成幻觉。

于是把记忆调度抽成独立模块,做了三层缓存「热层 TLB / _fileCache / 冷搜(memory-search)」。

三层缓存分层架构

用户消息 → 分类(classify, 纯正则 0 token)
  ├─ 热层 TLB 命中 → 短 context(几十字符),跳过冷搜
  ├─热层 miss →内部 _fileCache 缓存 命中 →省读盘时间
  └─ 热层 miss → 内部 _fileCache 缓存 miss →冷搜(memory-search)

测试结果 ════ 模拟 2 小时对话(120 条消息)════

三层命中分布: 热层命中 : 17 次 (14%) | 平均 198 字符 零读取 : 49 次 (41%) | 平均 183 字符 冷搜 : 54 次 (45%) | 平均 520 字符

冷搜触发率: 45% 实际总 context: 40384 字符 假设每条都冷搜: 62400 字符 token 节省: 35%

关键设计

热层 TLB

存 LLM 从对话里实时提炼的「活跃事实」,一条一个 key/value(如 job_status = 在投 AI agent 岗)。命中就跳过磁盘搜索。

测试结果真正省 token :热层命中(44 字符 vs 冷搜 335,省 87%)和零读取(ack/pushback 不搜任何层)。

消息分类(classify)

全程纯正则、0 token,按「从最省到最贵」排序决策,把「要不要搜索」从 LLM 手里抢过来:

ack(纯应答)→ pushback(抵触)→ question(疑问)→ 热层命中 → 活跃话题 → new_topic

ack 判定:重复字段压缩

判定「纯应答」时,与其枚举「好的好的」「是的是的」「哈哈哈哈」各种重复形式,不如先压缩重复再匹配基础词:

function _collapseRepeats(s) {
  s = s.replace(/(..+?)\1+/g, '$1');  // 多字符重复:是的是的 → 是的
  s = s.replace(/(.)\1+/g, '$1');     // 单字符重复:hhhh → h,!!! → !
  return s;
}

一处解决一类问题——以后加 ack 词只加基础形式,重复的自动被压缩处理。

bigram 停用词

中文 bigram 模糊匹配会误命中「什么」「干嘛」「这个」这类无意义词。加停用词表过滤,避免正则泛匹配搜出一堆无关记忆。

重构:为什么砍掉温层

最初是「热/温/冷」三层。温层(_warmCache)想当热层和冷层之间的缓存,但落地后发现它三个都不沾

  1. 不省 token——缓存的内容还得原样喂给 LLM,context 长度和冷搜一样
  2. 不省 I/O——memory-search _fileCache功能与温层冲突,冷搜第二次也不读盘
  3. 失效策略打架——温层用 30min 固定 TTL,_fileCache 用 mtime 感知数据源变化。文件更新后 30min 内,温层还会返回旧 snippet(过期数据)

根因:memory-search.js中的_fileCache与info-broker.js中的_warmCache功能定位冲突,且_fileCache表现优于_warmCache。最终决定将二者合并为_fileCache

_warmCache有一个_fileCache没有的功能:根据TTL选择 延续话题cont 还是 new_topic,暂时先不做。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages