数据截至 (上游 commit ac9c2a586ba7)
第 1 章:HSG —— 写入与召回主线
本章讲什么: OpenMemory 的名字叫 HSG(Hierarchical Semantic/Sector Graph,项目里称 Hierarchical Memory Decomposition,分层记忆分解)。这一章把"一条记忆怎么进库"和"一次查询怎么把它捞回来"两条主线拆开讲透。看完你就抓住了整个引擎的骨。
1. 写入主线:add → ingest_document → add_hsg_memory
1.1 入口:大文档先拆,小文本直接进
Memory.add 只是薄封装,真正干活的是 ingest_document。它要解决的小问题:一条 3 万字的文档,不能当成一条记忆整存——否则检索时它总是"半相关",还会淹没细节。
思路:估算 token,超过阈值(LG = 8000)就走 root-child 策略——建一个"目录页"根记忆 + 若干"章节"子记忆,并用 waypoint 边把根连 到每个子。
ingest_document(text)
│ 估 token = len/4
├─ ≤8000 ──▶ 单条:add_hsg_memory(text) 返回
└─ >8000 ──▶ split_text(按段落切 ~3000 字/段)
├─ mk_root:一条 reflective 摘要根记忆(salience=1.0)
└─ 逐段 mk_child ──▶ add_hsg_memory + link(root→child 边 weight=1.0)
真实实现:ops/ingest.py:85(ingest_document),阈值常量 LG/SEC 在 ops/ingest.py:13,拆分函数 split_text(:16),根记忆构造 mk_root(:31),父子连边 link(:79)。
1.2 去重:simhash + 汉明距离
要解决的小问题: 同一件事换了句式又存一遍("用户喜欢暗色" vs "用户偏好 dark mode"), 不该产生两条记忆。
思路: 给内容算一个 64-bit simhash(局部敏感哈希:相似文本哈希也相似),入库前查库里有没有 simhash 接近的老记忆;若汉明距离 ≤ 3 就判为重复——不新建,只给老记忆"加分"并刷新时间戳。
# 示意,非源码:simhash 去重的核心判断
simhash = compute_simhash(content) # 相似文本 → 相似 hash
existing = fetch_by_simhash(simhash) # 取最像的一条
if existing and hamming_dist(simhash, existing.simhash) <= 3:
boost_salience(existing, +0.15) # 重复:老记忆加重要度
return existing # 复用,不新建
# 否则:才走真正的新建流程
真实实现:memory/hsg.py:389(add_hsg_memory)开头,compute_simhash(:165)、hamming_dist(:200)。
坑:
compute_simhash里有一段把每个 token 的 hash 先算一遍又清零重算(:170-181两次h=0循环),第一段是死代码;真正生效的是第二段带符号处理的哈希。功能正确,但可读性差。
1.3 分区:正则打分把内容归到五个脑区
要解决的小问题: "我今天很沮丧"和"Python 的 GIL 是全局解释器锁"是两类记忆,不该用同一套权重去检索。
思路: 每个脑区配一组正则(SECTOR_CONFIGS),内容命中越多、权重越高的脑区得分越高;最高分者当 primary,达到阈值的其余脑区当 additional。
五个脑区(core/constants.py:10,SECTOR_CONFIGS),关键差异在 decay_lambda(衰减快慢):
| 脑区 | 装什么 | 触发词举例 | decay_lambda(越大越易忘) |
|---|---|---|---|
| episodic 情景 | 具体事件、时间 | today、went、saw、remember when | 0.015 |
| semantic 语义 | 事实、概念、定义 | is a、concept、fact、capital | 0.005 |
| procedural 程序 | 步骤、操作 | how to、install、click 、first…then | 0.008 |
| emotional 情绪 | 感受 | happy、love、frustrated、!! | 0.02(最易忘) |
| reflective 反思 | 洞察、总结 | realize、insight、lesson、pattern | 0.001(最持久) |
注意设计意图:情绪最易忘、反思最持久——模拟"当下的情绪褪色快,悟出的道理留得久"。
真实实现:classify_content(memory/hsg.py:115)。若 metadata 显式给了 sector 就直接采用、confidence=1.0(:116-118);否则逐脑区正则计分、按 30% 阈值挑 additional(:120-142)。
1.4 多脑区嵌入 + 落库 + 建链
分完区,add_hsg_memory 依次做四件事(memory/hsg.py:414-466):
- 多脑区嵌入。 对 primary + additional 每个脑区各嵌一个向量(
embed_multi_sector,memory/embed.py:48)。默认用 synthetic 适配器——无需任何 API key,靠 token 哈希 + n-gram + 位置编码在本地造向量(ai/synthetic.py:55,_gen_syn_emb)。也可切 OpenAI/Gemini/Ollama/AWS/MiniMax(emb_dispatch,embed.py:25)。 - 算 mean 向量。 把各脑区向量取均值当"整体指纹"(
calc_mean_vec,embed.py:62),额外以_mean伪脑区存进向量库,给"跨脑区找最相似邻居"用(见 Issue #141 注释,hsg.py:459)。 - 落库。 记忆行进
memories,每个 脑区向量进vectors(SQLiteVectorStore.storeVector,vector_store.py:39,向量用struct.pack存 BLOB)。初始 salience =0.4 + 0.1*len(additional)(hsg.py:430)——命中脑区越多、越重要。 - 建 waypoint 边。
create_single_waypoint(hsg.py:268):在向量库里搜 mean 向量最相似的一条老记忆,连一条权重=相似度的有向边;找不到就自连一条 weight=1.0 的边(保证图不空)。
2. 召回主线:search → hsg_query
hsg_query(memory/hsg.py:523)是全项目最重的函数。一步步拆:
2.1 五步流水
hsg_query(qt, k, filters)
① 缓存命中?(60s TTL)──▶ 直接返回
② 对 query 分区 + 逐脑区嵌入(embed_query_for_all_sectors)
③ 逐脑区向量检索,每区取 k*3 候选;算平均相似度 avg_top
④ 置信度低(avg_top<0.55)?──▶ expand_via_waypoints 沿联想边扩散拉更多候选
⑤ 对每个候选算 compute_hybrid_score → 排序取 top-k → 命中者强化
2.2 自适应扩散:不自信才去联想
巧妙点: 向量检索平均相似度高(avg_top >= 0.55,hsg.py:563)说明"直接命中够好",就不折腾;只有不自信时才 expand_via_waypoints 沿 waypoint 图 BFS 拉入联想到的记忆(:565-568)。扩散是带权重衰减的 BFS:每跳权重乘 0.8,低于 0.1 剪掉(expand_via_waypoints,hsg.py:500)。
扩张量也自适应:adapt_exp = ceil(0.3*k*(1-avg_top))(:561)——越不自信,拉越多候选。
2.3 复合打分:五个因子加权过 sigmoid
要解决的小问题: 只按余弦相似度排序,会漏掉"不那么像但很相关/很新/很重要"的记忆。
思路: 把五个信号加权求和,再过 sigmoid 压到 (0,1)。权重是硬编码常量 SCORING_WEIGHTS(hsg.py:28):
| 因子 | 权重 | 含义 | 计算 |
|---|---|---|---|
| similarity | 0.35 | 向量相似(经 boosted_sim 非线性放大) | boosted_sim(:162) |
| overlap | 0.20 | query/记忆的 canonical token 重叠比 | compute_token_overlap(:253) |
| waypoint | 0.15 | 该候选在联想扩散里的边权 | expand_via_waypoints |
| recency | 0.10 | 新近度(指数衰减) | calc_recency_score(decay.py:104) |
| tag_match | 0.20 | 标签与 query token 的匹配 | compute_tag_match_score(:80) |
真实实现:compute_hybrid_score(hsg.py:258)。
打分前相似度还做了两层调整(hsg.py:584-597):
- 跨脑区共振
calculateCrossSectorResonanceScore(ops/dynamics.py:33):用一个 5×5 脑区互依矩阵,把"记忆脑区 × query 脑区"的耦合系数乘到相似度上。 - 跨脑区惩罚:若记忆脑区 ≠ query 脑区,再乘一个
SECTOR_RELATIONSHIPS(hsg.py:56)里的折扣(默认 0.3)。
# 示意,非源码:候选的最终分怎么来的
sim = cross_sector_resonance(mem.sector, q.sector, multi_vec_sim) # 先按脑区耦合调
if mem.sector != q.sector:
sim *= sector_penalty(q.sector, mem.sector) # 跨区再打折
score = sigmoid( # 五因子加权
0.35*boosted_sim(sim) + 0.20*token_overlap + 0.15*waypoint_w
+ 0.10*recency + 0.20*tag_match + keyword_bonus)
2.4 召回即强化(闭环)
top-k 选出后,hsg_query 立刻回写(hsg.py:638-656):
- 命中记忆 salience 用
applyRetrievalTraceReinforcementToMemory回升、刷新last_seen_at。 - 若命中来自联想路径,顺着 waypoint 用
propagateAssociativeReinforcementToLinkedNodes给邻居也加分(带时间衰减gamma)。 - 调
on_query_hit(decay.py:188):进一步强化 + 必要时重嵌入被压缩过的向量。
这就是它区别于 RAG 的核心:查询不是只读的,它会改变记忆状态——用得多的记忆更容易被再召回。这条"记忆动力学"下一章细讲。
3. 关键细节 / 坑
- synthetic 嵌入是"够用"而非"高质量"