数据截至 (上游 commit 2d38dafede67)
巧妙之处、边界与代码地图
本章讲什么: 把前四章散落的「精华」收拢成一张可带走的清单,诚实标出项目会在哪崩,和兄弟项目比一比取舍,最后给一张按符号名导航的代码地图。
1. 巧妙之处(可借鉴的技术)
零 LLM 是默认,LLM 是 opt-in。 整个项目最核心的成本哲学:捕获/压缩/取代/召回/衰减都不需要 LLM——合成压缩用正则(compress-synthetic.ts:76)、取代判定用 Jaccard(remember.ts:125)。只有想要更丰富的语义摘要(AUTO_COMPRESS)和 4 层巩固(CONSOLIDATION_ENABLED)才花 token。妙在:基础记忆能力对用户「免费」,贵的能力按需付费。
RRF 的动态权重归一化。 融合时缺席流不占权重:单条记忆未命中的流记零,可达上限只统计出过结果的流,再按上限归一,多路命中另有每路 5% 的一致性加成(hybrid-search.ts:198-220),让 BM25-only 和三路全开两种部署都不用改配置就排序合理。 这是「优雅降级」落到排序公式里的写法。
对称的维度守卫。 向量维度不匹配会静默毁掉搜索(跨维余弦=0)。项目在写入侧(vectorIndexAddGuarded,search.ts:124)和加载侧(逐条 validateDimensions 否则拒绝启动,src/index.ts:429-471)都设了守卫——两头堵,而不是出了问题才查。
fire-and-forget hook 的正确退出。 纯遥测 hook 发出 fetch 不 await,再用 setTimeout(...).unref() 强制退出(AGENTS.md 详述)——否则 Node 事件循环会一直等 fetch settle,fire-and-forget 形同虚设。这是个容易写错的细节,项目把它写进了贡献规范。
删除要同步刷盘。 内存索引的删除如果只在内存里,硬退出后持久化快照会「复活」被删条目。所以 mem::forget 走 flushIndexSave() 同步刷(search.ts:93-104,remember.ts:321),而 add 走 debounce 异步刷——按操作的「能否丢失」区别对待。
Float32Array 序列化的 byteOffset 陷阱。 显式传 byteOffset+byteLength 防 Node Buffer pool 制造幽灵维度(vector-index.ts:8-21),修了四个连号 issue。值得任何做向量持久化的人抄走。
2. 边界与局限(诚实)
- 向量是暴力 KNN,没有 ANN 索引。
VectorIndex.search(vector-index.ts:49)对每条算余弦,O(N)。语料到几十万条时单次向量查询会线性变慢——没有 HNSW/IVF。索引也整个驻内存,大语料吃 RAM。 - 取代判定对语义改写不敏感。 Jaccard 是词集合交并比(
schema.ts:132);「用 yarn」和「采用 Yarn 包管理器」词重叠低,可能判不出是同一件事,留下两条并存记忆。 - 合成压缩信息密度低。 默认路径的
facts/concepts为空、narrative截断到 400 字(compress-synthetic.ts),confidence只有 0.3。不开 LLM 压缩时召回到的观察偏「流水账」。 - 召回侧有大 scope 全扫的隐患。
mem::context(context.ts:167)和取代判定都kv.list整个 scope。KV注释里多处提到 75K+ 节点的图谱已撞过 iii 调用超时(schema.ts:19-40),靠预计算快照绕过——说明全扫在大体量下是真实瓶颈。 - 强一致性不保证。 跨函数的多步写(写观察 + 更新 session + 建索引)只在单会话 keyed-mutex 内串行,没有跨 scope 事务;部分失败靠 try/catch + 重启重建兜底,不是 ACID。
3. 横向对比(同 shelf 兄弟)
memory-context 这一格关注「agent 怎么把上下文存下来、找回来」。agentmemory 的取舍位置:
| 维度 | agentmemory 的选择 | 取舍 |
|---|---|---|
| 存储 | iii-engine 内置 SQLite KV,零外部 DB | 装得轻,但受单引擎调用超时/单机内存约束 |
| 检索 | BM25+向量+图谱 RRF 融合,自实现 | 三路兜底召回好,但向量是暴力 KNN 不扩展 |
| 捕获 | hook 全 自动 + 零 LLM 默认 | 零摩擦零成本,但默认观察信息密度低 |
| 记忆模型 | 显式版本化 + 4 层巩固(LLM opt-in) | 模型完整,但巩固/语义层依赖 LLM |
| 集成面 | REST + MCP + 12 hooks,跨 10+ agent | 集成广,但一致性维护负担重(AGENTS.md 列了 8 处要同步) |
对比基线:很多「agent memory」方案直接挂外部向量库(Pinecone/Chroma)+ 纯向量召回。agentmemory 反过来——自带轻量存储 + 多路融合 + 零 LLM 默认,换来「装一个 npm 包就能跑、不烧 token」,代价是检索在超大语料下的可扩展性。
4. 代码地图(导航索引)
按符号名 grep 比按行号稳。下表是读源码的跳转表:
| 主题 | 文件 | 符号名 |
|---|---|---|
| Worker 启动 / 全部函数注册 | src/index.ts | main |
| 捕获主入口 | src/functions/observe.ts | registerObserveFunction |
| 零 LLM 合成压缩 | src/functions/compress-synthetic.ts | buildSyntheticCompression、inferType |
| 去重(SHA-256 + TTL) | src/functions/dedup.ts | DedupMap、computeHash |
| 密钥脱敏 | src/functions/privacy.ts | stripPrivateData |
| BM25 倒排索引 | src/state/search-index.ts | SearchIndex、search |
| 向量暴力 KNN | src/state/vector-index.ts | VectorIndex、cosineSimilarity |
| 三路融合检索 / RRF | src/state/hybrid-search.ts | HybridSearch、tripleStreamSearch、diversifyBySession |
| 检索函数入口 / 重建索引 | src/functions/search.ts | registerSearchFunction、rebuildIndex、vectorIndexAddGuarded |
| 上下文注入(召回拼装) | src/functions/context.ts | registerContextFunction |
| 显式记忆 / 取代 / 遗忘 | src/functions/remember.ts | registerRememberFunction、mem::forget |
| 4 层巩固 / 衰减 | src/functions/consolidation-pipeline.ts | registerConsolidationPipelineFunction、applyDecay |
| KV 状态层 | src/state/kv.ts | StateKV |
| KV scope 定义 / id 生成 | src/state/schema.ts | KV、generateId、fingerprintId、jaccardSimilarity |
| 并发锁 | src/state/keyed-mutex.ts | withKeyedLock |
| REST 端点注册 | src/triggers/api.ts | registerApiTriggers |
| 事件 / state / durable trigger | src/triggers/events.ts | registerEventTriggers |
| MCP 工具 → 函数映射 | src/mcp/server.ts | registerMcpEndpoints |
| MCP 工具定义 | src/mcp/tools-registry.ts | CORE_TOOLS、getAllTools |
| 上下文注入 hook | src/hooks/session-start.ts | main |
5. 一句话收束
agentmemory 的精华是「把 agent 记忆做成零摩擦、零默认成本的本地服务」:hook 自动捕获、正则压缩、三路融合召回、显式记忆版本化——贵的(LLM 巩固、丰富压缩)全是 opt-in。它在「装得轻、不烧 token」上做到了极致,代价是向量检索不扩展、超大语料会撞引擎超时。要它的轻便就接受它的天花板。