
NOTE这一篇是接续 MoeChat:AI 角色如何记住你、感受情绪 的讨论。
当时我们觉得 MoeChat 提出的时间检索很有意思可以内化到我们自己的记忆框架里。这里是对具体方案的分析,以及顺带补了一下 NLP 的知识。
白话总结
这是一个快速版本,如果你习惯看我平时提问求解式的方式,可以跳过这一节。我们会在那里讨论更多。
这篇留给自己输出的地方还是太少了,我来用白话简单总结一下。赶时间的话,读完这节就够了。
Hybrid Search 专指关键词匹配 + 语义相似度匹配的混合检索融合,通常使用各自的分数进行加权融合,这个权重是一个人为设定的值不是被计算出来的。
RRF 是一个混合排序方法,只关心排名,不关心分数。也可以选用 RRF 给 Hybrid Search 做融合,但是有个问题就是, RRF 不关心具体分数,年级第一超了第二一百五十分和年级第一超年级第二两分在 RRF 的视角里是没有区别的。
它类似这样:
queue_1 = ["docs2", "docs1","docs3","docs4"]queue_2 = ["docs3", "docs2", "docs1","docs4"]
rrf_index = ["docs2", "docs3", "docs1", "docs4"]它是一种仅仅只靠直觉就能理解的排序方式。算法反而不重要的。
另外关于时间检索,是否适合做成三路。
这里我们给了否定的答案。
因为,如果做成三路并列,我们不得不引入 RRF 来作为我们的混合排序融合。
这样会引入很多问题,一方面是分数被 index 化,我们丢失了每个 docs 之间实际的距离感。
另外,就是 RRF 会始终给时间这第三路引入一个系统 bias,如果 query 是时间相关的,它会过度抬高时间相关的 resource,如果 query 是时间无关的,它则会始终引入一个噪声,因为时间那一路的 RRF 排名始终在。也许只有一路影响不大感觉不出来,但终究是坏影响,你考虑一下如果我们做成十路,二十路,到时候每次只激活两三路,用 RRF 来融合,引入的噪声会掩盖住真正重要的信息。
最后我们考虑用门控 + 衰减项,至于具体什么衰减,以及如何在具体的链路里降低整体延迟,等到我们后续把 ADR 落地到 wikimem 后才能看出来。
另外,我们这边还讨论一些玄学的东西。
什么记忆结构啊,日记形态啊。那些你可以不关心。包括现在我也不关心了,它只是我理清楚框架和应用边界的一个过程。以及对陪伴场景和项目场景的再一次梳理。也许我自己都不会再看,因为它会变成我项目的结构,如果还需要再推敲,我会再读这里然后从这篇把新的疑问引出去再讨论。以上。我也更享受那种从混乱困惑到归一清晰的过程,对于我而言记录的过程比记录的结果更重要。
了解不多,问题很多
在 MoeChat:AI 角色如何记住你、感受情绪 中,我们曾预告会简单了解一下多路混合检索的具体设计方案。
我现在了解程度还仅限于几个名词, Hybrid Search,RRF 排名融合。我知道 Hybrid Search 是关键词匹配 + 语义相似度的融合。我知道融合时如果融合的分数可加权就要归一化啥但那个复杂,而 RRF 只关心排名,很简单粗暴,适用那些不可加权的方案。
但是我好奇, Hybrid Search 似乎和 RRF 有什么本质的区别?准确说 Hybrid Search 好像一般不选用 RRF 做融合?它为什么不用?如果强行给 Hybrid Search 应用上 RRF,会带来哪些性能损失或者灾难性影响?比如原本可解释的语义分数被强行 index 化?它会带来哪些影响?
然后我们就再来看看当时在 MoeChat 里想到的在语义、向量之外加入时间。但是由于时间是一个排序性的问题,所以时间没有具体分数。只能用 RRF。
另外我们还会讨论,我们直接用三路混合 RRF 做融合,和把时间作为一个门控哪个更合适?
把时间硬混进 Hybrid Search,它会带来一些问题,比如说如果取到的时间区间里包含非常多的记忆数据,而用户的 query 仅仅只是 你记不记得我上周中午一般吃什么,话题是很窄的。
这个时候如果硬混进三路,在按时间召回了具体区间后,会面临需要再次 Hybrid Search 的问题。不然所有记忆就太过于庞大了。即便不是一整周的,一个晚上的记忆也不可能全都加塞进上下文。
而如果再次 Hybrid Search 就会发现它在结构上有冗余,一个在全部时间上做 Hybrid Search,一个在所划分的时间上做 Hybird Search。但在 MoeChat 中,在时间上,它直接把日记里的内容全部召回了没有二次筛选?这点需要确认。是因为日记(LTM)里的内容不多不需要二次筛选,还是出于别的什么想法。
Korewaxnne确认了,回顾 MoeChat:AI 角色如何记住你、感受情绪 里的拆解:
MoeChat 的 LTM 召回是纯门控,没有二次筛选。具体流程:
- 用户消息先过
jioNLP提取时间表达式(“昨天”“上周五”)→ 解析为时间戳范围- 在排序好的时间戳数组上做二分查找(
bisect_left/bisect_right),O(log n) 定位范围内的所有记忆- 可选:如果
enableLongMemorySearchEnhance开启,用余弦相似度做一轮过滤(阈值 0.32),但这只是过滤,不是独立的语义检索路- 如果用户消息不包含任何时间表达式 → LTM 直接不参与,函数 return 空
所以你的猜测是对的:它确实是把时间窗口内的记忆全部召回了,不做 Hybrid Search 二次筛选。原因也如你所想——LTM 的写入是按天的事实类 JSONL,每天就几条,一整天的量直接塞进上下文是扛得住的。它不是“出于什么设计哲学”不做二次筛选,而是数据量本来就小到不需要。
所以这个时候衍生出来的一个更好的做法就是把时间作为门控,只在用户强烈激活到了某个具体时间的时候,作为划分大致时间范围然后再 Hybrid Search,但这样也会存在一个问题,就是 Hybrid Search 没法自己工作。而且从时间上来说它会比较久。因为还没确定时间的推理是否要依赖 LLM。
也许这个要从我们的目的出发,对于 MoeChat 来说,它设计 LTM 和时间检索的初衷很简单,能够让模型具备回复 “前天晚上我吃了什么” 这样具体问题的能力。而且在它的应用场景下, LTM 的写入是事实类,它恰好适合做这个召回并且,数量也不会很多。
如果是我的桌宠应用场景,我会直接应用三路混合检索,融合简单用 RRF,不为什么,就为了时间检索能够像 MoeChat 那样单独工作。而且我头脑简单,RRF 很对我胃口。
Hybrid Search 与 RRF:两路融合方法的选择
Hybrid Search 和 RRF 的关系
Hybrid Search 似乎和 RRF 有什么关系吗?是我理解的那样子, Hybrid Search 可以选用 RRF 也可以选用加权参数融合的方法的那种关系吗?
Korewaxnne对,就是这种关系。
Hybrid Search 是策略——“我同时用关键词搜索和语义搜索,然后把结果合在一起”。
RRF 是融合方法之一——“怎么把两路结果合在一起”。
所以 Hybrid Search 需要选一个融合方法,常见两种:
- 加权分数融合(Weighted Score Fusion):把两路的分数归一化后加权求和。比如
0.7 × 语义分 + 0.3 × 关键词分。你能调权重,能解释“这条结果为什么排第一”。- RRF(Reciprocal Rank Fusion):不看分数,只看排名。公式就是
1/(k + rank),然后把各路的倒数排名加起来。简单粗暴,不需要归一化。类比的话:Hybrid Search 是“我要坐车去目的地”,RRF 和加权融合是“坐公交还是打车”——都能到,取舍不同。
纳尼,这个加权的权重不是自动 cal 的吗?是人手动设置的吗?
Korewaxnne是人手动设的,不是自动算的。
加权融合里的权重(比如语义 0.7、关键词 0.3)是一个超参数,跟学习率一样——你自己定,然后靠实验调。
常见做法就是拿一批标注好的 query + 期望结果,跑不同权重组合,看哪个检索效果最好(比如 nDCG、MRR 之类的指标),然后就用那个权重。没有什么梯度下降在背后帮你优化这个值。
所以这也是 RRF 的一个优势:它不需要你调权重,扔进去就能用。而加权融合你得先有评测集才能调出一个靠谱的权重,没有评测集就只能凭感觉猜。
它为什么不用?如果强行给 Hybrid Search 应用上 RRF,会带来哪些性能损失或者灾难性影响?比如原本可解释的语义分数被强行 index 化?它会带来哪些影响?
KorewaxnneHybrid Search 可以用 RRF,但大多数场景下不划算——因为你白白丢掉了分数信息。
关键词搜索(BM25)和语义搜索(cosine similarity)都能给每条结果打一个具体的分数。这些分数本身是有意义的:cosine 0.95 和 0.60 的差距远比“第 1 名 vs 第 2 名”传达的信息多。
如果你用 RRF,就是把这些分数全扔了,只保留排名。具体会丢什么:
- 分数间距信息没了。语义上第 1 名 0.95、第 2 名 0.94,和第 1 名 0.95、第 2 名 0.30,在 RRF 眼里完全一样——都是“第 1 vs 第 2”。但实际上后者的第 2 名根本不相关。
- 加权调优的能力没了。加权融合里你可以说“语义占 70%、关键词占 30%”,按场景微调。RRF 做不到这种粒度的控制。
- 可解释性变差。加权融合下你能说“这条排第一是因为语义分 0.92 + 关键词分 0.85”。RRF 下你只能说“两路排名加起来最高”。
所以结论很简单:两路都有分数 → 加权融合更好,因为信息更丰富。RRF 的优势场景是“某一路没有可比较的分数”,比如你加了时间维度——时间只有先后顺序,没有“相关性分数”,这时候 RRF 就是自然的选择。
确实 RRF 会有个问题就是忽略分数和间距问题。但它很干净,对于那种本身就没有分数的来说。
你可以读一下:memU ADR
里的 ADR 0006,0007,0008,不过我们的 retrieve 已经实现了,你读源码也是一样的。
后续竟然又更新了 9、10、11。但这个不在你的职责内,我后续会跟进一下更新 memU 是啥?我们来拆开看看。
KorewaxnnememU 用的是加权分数融合(min-max 归一化),不是 RRF。
根据 ADR-0007 的设计:cosine embedding 分数和 BM25 关键词分数各自做 min-max 归一化,然后融合成一个排名。经典的加权分数融合路线。
不过目前源码里 retrieve 只走了 vector-only(cosine similarity),BM25 那路还没实现——hybrid search 还停留在设计阶段。
加入时间维度:三路检索的挑战与取舍
那么我们现在来考虑一个比较困难的问题。
如果我要加入 MoeChat 那样的时间作为第三路,就会破坏 ADR 0007 里原本定好的分数融合只能强行 RRF。
那么我们可以分应用场景分析,简单先分为项目场景,和长期陪伴桌宠应用场景。
分别分析以下三种做法:
a. 我们同时三路并行,全部采用 RRF 进行融合。 b. 我们同时三路并行但是同时做两件事,RRF 和原先两路加权融合都做,当 RRF 里时间排名和其他两路排名差距够大时直接应用时间,否则忽略时间这一路,正常输出原来该有的结果。 c. 我们把时间作为一个门控单元,时间负责划分召回的记忆范围,然后 Hybrid Search 在这个范围里正常执行。
这里感觉比较难,我去请 Fable 老师。Fable 老师你如果有更好的做法也可以分析出来。
Fable先给结论:c 是主干,但它还缺半块;b 可以直接排除;a 想要的“时间独立工作”,其实是 c 的一个退化情形,不需要为它保留三路投票。
先把问题看清:时间和另外两路不是同类信号
语义分和关键词分回答的是“这条记忆是不是在说这件事”——这是相关性,适合投票。时间回答的是“这条记忆在不在你问的范围里”——这是约束。“前天晚上”之外的记忆不是“不太相关”,是错的。约束不该投票,该过滤。
还有一个更隐蔽的不对称:BM25 和向量的候选列表是查询条件化的——不匹配就没分、进不了榜,query 无关时这两路会自己安静下来。时间不一样,每条记忆都有时间戳,不管 query 问什么,时间这路永远能排出一个完整榜单。一个永远有票可投的选民,在 RRF 里就是系统性偏置。
这个 bias 确实不该存在,所以简单的全 RRF 是不对的,既丢失了具体分数的精确性和间距,还永久的引入了一个噪声。
a. 三路全 RRF——不算灾难,算“没有必要地不精确”
两个问题:
- 没有时间意图的 query(“我最喜欢什么动漫”)里,时间路退化成纯 recency 投票,把“新但不相关”的记忆往上顶——就是上面说的系统性偏置。项目场景里这是纯亏;桌宠场景里勉强算个新鲜度偏好,但代价是所有查询的精度。
- 有时间意图的 query(“前天晚上吃了什么”)里,时间路要有意义,得先把“前天晚上”解析成区间,才能按“离目标多近”排序。也就是说 a 并没有省掉时间解析,解析器你横竖都要写。而解析完你手里已经有区间了,投票反而是绕路:投票不能保证赢家落在区间内——一条三周前印象深刻的晚餐,语义上可能碾压前天那顿平平无奇的,RRF 拦不住它。答错“前天吃了什么”是事实性翻车,对桌宠来说是最伤信任的那种错。
鞭策得很对。如果已经取到了区间,那么确实不应该再进投票公民组。
b. 双融合 + 分歧触发——直接排除
因为触发条件测的是错的东西。时间排名和语义排名“差距大”不是信号,是常态——新旧和相关性本来就不相关,两个榜单几乎永远分歧,这个触发器要么常开要么测不到任何东西。“该不该用时间”的证据在 query 里(“上周”“前天”这几个字),不在结果列表的分歧里。用结果分歧去反推查询意图,是用错误的传感器做正确的判断。
而且你要同时维护两套融合,外加一个比融合权重更难调的分歧阈值——当初选 RRF 就是为了不调参,b 把这个好处也吐回去了。
这个做法不仅链路看得别扭,而且还必须引入一个非常难搞的超参数:什么时候,叫做差距过大,这会让系统很 weak。
c. 时间门控——方向对了,你担心的两个问题都有解
c 在工程上就是向量库里标准的 metadata pre-filter(
WHERE ts BETWEEN ...):ADR 0007 的融合公式一行不用改,只是跑在过滤后的候选集上。你在开头提的两个顾虑:
- “解析时间要不要 LLM、会不会慢”——桌宠每轮本来就有 LLM 在跑。把检索做成 tool call,让模型顺手把 time_range 作为参数吐出来,解析成本被摊平成零。常见表达(昨天/前天/上周/几号)可以加一条正则快通道;正则做不了的(“上次我们吵架那天”)本来也只有 LLM 能做——那是事件锚定,得先检索到那件事、拿到日期、再围绕它开窗,这种两跳门控天然支持,投票路线根本表达不了。
- “门控之下时间没法独立工作”——纯时间 query(“前天都发生了什么”)在 c 里就是:门控出区间 + 语义 query 为空 → 退化成“把区间内的记忆按时间取 top-N”。这正是 MoeChat 的行为。按你开头的观察(日记整段召回、不做二次筛选),MoeChat 的时间检索本身就是门控,从来不存在一个三路融合等你模仿——它敢整段召回只是因为 LTM 少;你的记忆多,所以区间内还得跑一遍 Hybrid Search,而这恰好就是 c。
配一个兜底就齐了:解析出的窗口召回为空就自动放宽(±1 天、模糊边界),别让一次误解析变成“我不记得了”。
但 c 还缺半块:时间其实是两个角色
把“时间”拆开看,它在检索里干的是两件不同的事:
- 显式时间指称(“前天晚上”“上周中午”)→ 硬过滤,就是门控,上面说完了。
- 隐式新鲜度偏好(用户没提时间,但昨天聊过的就是应该比三个月前的容易浮上来)→ 这不需要第三路,在原有的加权融合里加一个小权重的时间衰减项(比如 ,归一化后加进去)。它有分数、可归一化、可调权,完全兼容 ADR 0007 的分数融合,不用动 RRF。
所以最终形态是:门控(角色 1)+ 衰减项(角色 2)+ 原样的两路加权融合。“第三路”这个概念整个消掉了——时间从头到尾都不是一条检索路,因为时间自己回答不了“这条记忆在说什么”,它只能约束范围、或者施加一点偏好。RRF 依然是好东西,但它的主场是“几路都是真排序器、只是分数没法比”;时间不是排序器,所以这里轮不到它出场。
分场景收尾
项目场景:连门控都可以再简化。项目记忆里的“时间”多半其实是“版本”——“最新的决定是什么”靠写入时的取代关系 + 读取时的 recency tiebreak 解决,不是靠时间检索。而且调用方本来就是 agent,偶尔真有时间查询,让它传一个可选的
time_range参数就行——不要在记忆系统内部做意图识别,理解 query 的活调用方已经干了。桌宠场景:时间是一等公民,而且有个致命细节:情景记忆里全是语义近重复——你吃了五十次午饭,五十条记忆在语义上几乎一样,唯一能区分“上周三那顿”的就是时间。近重复越多,软偏好(投票)越不保险,硬约束(过滤)越必要。“上周中午一般吃什么”在 c 里走得很顺:窗口=上周,query=午饭,窗口内 top-k,LLM 汇总规律。
方案 项目场景 桌宠场景 a. 三路 RRF ✗ recency 污染,纯亏 △ 能跑,但“前天”类问题答不稳 b. 双融合+触发 ✗ 触发器测错东西 ✗ 同左 c. 门控 ✓ 可再简化成可选过滤参数 ✓ 主干 c + 衰减项 ✓ 一个 recency tiebreak 就够 ✓✓ 推荐形态 一句话版本:显式时间做过滤,隐式时间做衰减,永远不做第三路。
设计落地:时间快通道与记忆结构
路过的读者看这里到上面那张分场景表格为止,是可以带走的通用结论。往下是我们自己项目(wikimem / XnneHangLab)的设计现场,需要 MoeChat:AI 角色如何记住你、感受情绪 的上下文——不熟悉的话,跳过后半不损失任何通用知识。
但我依然希望时间检索的快通道能够始终激活且能独立工作,而意图识别作为一个 tool call。
这个牵扯比较多,你可以看:
时间快通道:正则与意图识别双轨
我觉得时间的快检索通道应该由 wikimem 持有:
NOTE几个替代方案:
- dateparser:支持 200+ 语言含中文,纯规则,轻量,
pip install dateparser即可。dateparser.parse("昨天下午")直接返回 datetime。没有 jioNLP 的 NER 前置步骤,一步到位。- arrow + 手写正则:如果时间表达式的模式是可枚举的(“昨天”、“前天”、“上周X”、“X天前”),正则匹配 + arrow 的
shift()计算时间偏移,十几行代码就能覆盖 MoeChat 的使用场景,零依赖开销。- TimeNLP:专门做中文时间语义解析的小库,比 jioNLP 轻得多,只做时间这一件事。
最务实的方案其实是第二个 – MoeChat 实际需要识别的时间模式就那十几种(昨天/前天/上周X/X天前/X月X号),正则 + 时间偏移计算就够了,不需要一个通用 NLP 库。
这是上次我们构思的方案。
我觉得 wikimem 应该具有类似 MoeChat 那样的快通道。而意图识别,关于时间的分析,比如说 LLM call 做成一个 Tool Calling,我觉得可以集成到 XnneHangLab。
意图识别具体会带来什么?意图识别是对模糊时间,无法被我们简单正则命中方案的补充。我希望不要混淆职责。它可以让我们 focus on 正则实现简单方案,然后把复杂的事情交给意图识别的 tool call。
记忆结构:日记与 Wiki 的边界
另外就是关于记忆结构了。在 TimeLine 上, MoeChat 只记忆事实情景记忆,对于偏好、人设等等都是独立结构的。
但正如 MoeChat:AI 角色如何记住你、感受情绪 里我写得那样, LTM 和 CoreMemory 存在一定程度上的边界不清。导致让人看上去有些不适。
而 memU 早期的六种 memory type 又会带来非常重的 memorize。你有什么建议吗?
我们的记忆应该怎么存?jsonl 事实记忆碎片,还是让模型写生动的 markdown 日记?
准确说我们要怎么界定记忆?能够被时间召回的记忆又包含哪些?适合包含全部记忆吗?
像 MoeChat 那样仅记录事实的日记本仅供时间召回?但是,模型情绪的记录本就不重要了吗?要知道有情绪才有人设,如果只有事实,没有情景,就不能算有灵魂。
以及,因为我们的 wikimem 是同时支持 embedding 和 zero-embedding 的。那么我们就得为每条被 retrieve 的记忆留好 embedding 的位置,同时这个 embedding 必须带上哪些,维度,embedding 模型名称,如果名称对不上,应该警告用户并且让他们重建整个记忆数据库。
另外,什么适合存入 sqlite?什么适合存入 markdown?
我们是否应该用 sqlite 替代 MoeChat 的 jsonl 来获取更高的性能,同时,我们在自己的 XnneHangLab 里做一套可视化记忆数据的前端系统。供用户翻阅,按时间检索,按内容搜索。
但或许这个该由 wikimem 自己提供,因为 wikimem 是一个框架,至少 wikimem 要像 yutto serve rpc 改造那样,不执著于 cli,在数据接口上实现,这样别的应用层才好套自己的壳。传输协议也得规划。
同时 markdown 的日记系统最好也能被管理。
框架与应用的职责划分
Fable 老师救一下。我们可能需要更新一下 wikimem 和 XnneHangLab 的 ADR 了。明确一下,什么是作为框架该做的,什么是作为应用该做的。
Fable读完了两个仓库(wikimem 的现状、XnneHangLab 的 ADR-0001~0003 和 wikimem 插件)。先说总原则,再逐个拆。
分界线其实你们已经画过了
ADR-0001 里有一条硬约束:“retrieve 0 次 LLM 调用,memorize ≤ 1 次异步 LLM 调用、由宿主发起”。这条线推广一下就是全部答案:
框架只做确定性的机制(规则、数学、IO),任何需要模型判断的事都留在应用。 embedding 是唯一的例外,但它也是以“可插拔、可缺席、失败即降级”的方式注入的——wikimem 核心零依赖这个性质值得一直守住。
下面所有问题都用这条线切。
时间快通道:由 wikimem 持有,正确
设计成一个参数、两条来路:
index.retrieve(query, time_range=None)
- time_range 没给 → wikimem 内部跑正则快通道(昨天/前天/上周X/X天前/X月X号),命中就自己开窗。纯 stdlib(
re+datetime)就够——别引 dateparser,“200+ 语言”的通用库换来一个依赖,违反零依赖约束;arrow 也不需要,timedelta能算的事不用库。你们上面那条 note 里的方案二是对的,还可以更省。- time_range 给了 → 直接用,跳过正则。这就是应用侧意图识别 tool call 的出口:LLM 负责“上次我们吵架那天”这种正则永远够不着的表达(含事件锚定的两跳查询),但它最后吐出来的也是同一个结构化参数。
两条路汇合在同一个参数上,职责就不会混:正则是框架的地板,LLM 是应用的天花板,中间不存在第三个解析器。
但先补一块课:wikimem 现在没有时间轴
看代码会发现:wikimem 只有状态层(category/item 的 wiki),
ts存了但检索从来没用过——没有时间过滤、没有衰减。上一条回答说的“门控”,现在根本没有东西可以门——事件流这个原语是缺失的。所以“记忆结构”和“时间检索”其实是同一个问题:记忆结构:两个原语,一条边界规则
- 日记(事件流):append-only,按天一个文件,时间是主轴。这是要新加的原语。
- wiki(状态层):现有的 category/item,内容是主轴,可改写。
边界规则一句话:“发生过的事”进日记,“现在为真的事”进 wiki。 MoeChat 的 LTM 和 CoreMemory 边界不清,是因为它用“重要/永久程度”来分——重要性是连续的、会变的,切不出干净边界。“事件 vs 状态”是离散的:一条记忆要么有“它发生在何时”这个属性,要么没有。同一件事可以两边都出现:“7 月 21 日,他说他换了工作,语气很兴奋”(日记)+ wiki 里
work条目被更新(状态),互不打架。memU 的六种 memory type 不要学进框架——那是状态层的内容策略,归 extraction prompt 管(你们的 category 本来就是自由的)。六种 type 之所以 memorize 重,是因为每种 type 各跑一遍 LLM;ADR-0001 的“≤ 1 次调用“已经把这个坑填掉了。
情绪放哪:拆成两个东西
- 事件里的情绪进日记。日记条目就该是模型写的生动短段落——场景、情绪、事实在一起(
diary_writing这个 skill prompt 你们已经有了)。这就是“灵魂”的存放处。也顺便回答了“jsonl 碎片还是生动 markdown”:日记要生动的 markdown,事实碎片放 wiki,两个都要,不二选一。- 当下的情绪状态(MoeChat 的 emotion_state.json,效价/唤醒度那套)不是记忆,是应用的运行时状态。它每轮都变、不需要被检索,不进框架。
“能被时间召回的记忆包含哪些?适合包含全部吗?”——只有日记。wiki 不在时间轴上(“上周新增的偏好”这种需求以后可以用
ts做个过滤加成,但不是主轴)。全部记忆都可时间召回是错的:偏好不应该被“上周”捞出来。存储格式:文件名就是时间索引,sqlite 先不要
日记按天一个 markdown 文件(
diary/2026-07-21.md,## HH:MM一条 + ts 注释),有个容易被低估的好处:文件名本身就是时间索引。门控开窗 = 按日期范围挑文件,O(天数),零索引、零依赖——MoeChat 用按天 jsonl 已经验证过这条路。窗口内再跑现有的 BM25/融合,上一条的 c 方案就完整落地了。sqlite 的问题不是性能,是它违反你们自己的 ADR-0001:“磁盘上不能有不可读的真相”。真相必须是 markdown;sqlite 顶多做可删除的派生缓存——和 vectors.npy 同一个身位。而现在的规模(桌宠一年 ≈ 一两万条)在内存 BM25 + 现有的 signature 分层下就是毫秒级,这个缓存还不需要。结论:真源永远是 markdown;sqlite 不做真源,暂时也不做缓存,等十万条再回来谈——到时候走 FTS5 派生缓存,位置都留好了(VectorIndex 已经是 Protocol 口)。
embedding 元数据:真 gap,但修法比你想的轻
确认了现状:
vectors.keys.jsonl的 header 只有 vectors_file,模型名和维度都没记——你的担心是对的。修复:header 加{"model": ..., "dim": ...},打开时和配置比对。但 mismatch 之后不要“报错让用户重建整个记忆数据库”——wikimem 的真相是 markdown,向量只是 content-hash 键控的派生缓存,换模型 = 作废缓存 + 按需增量重嵌,根本不存在“重建数据库”这回事。行为上对齐现有哲学(embedding 失败从不 raise,静默降级 BM25-only):mismatch → 警告 + 本次会话降级 + 提示重嵌命令。zero-embedding 模式天然就是这条退化路径的终点,白送的。
顺带纠正一个提法:“为每条记忆留好 embedding 的位置”——不要在记录上留位置。真相文件里一旦出现向量,“可读真相”就毁了。现在这种派生缓存设计是对的,保持。
serve 与前端:契约是 Python API,其他都是壳
- wikimem 的契约就是 Python API(MemoryStore/MemoryIndex)。M4 与其叫“CLI”,不如定义成“薄壳层”:CLI 和
[serve](HTTP + JSON,做成 extra,学[embed]的样子)都只是同一套 API 外面的一层皮。传输协议不要自研:HTTP + JSON + 一个版本号,够了。serve 是给进程外消费者用的(别的应用、独立的浏览工具)。- XnneHangLab 不需要 wikimem 的 serve——它已经进程内 import 了(毫秒级,对比 memU 子进程那条 2 秒的路),前端可视化在它自己的 FastAPI/WebSocket 上加几条路由就行。
- 可视化 UI 百分之百归应用。框架保证数据可及(列日期、读某天、搜索、journal tail),长什么样是应用的事。另外 file-first 有个免费福利:markdown 真相意味着 Obsidian/任何编辑器已经是一个能用的“记忆浏览器”,debug 场景直接够用,别在框架里造 UI。
落到 ADR
wikimem 仓库现在没有 ADR 目录,框架的决策全记在 XnneHangLab 的 ADR 里——既然它已经独立成仓库,框架自己的决策应该跟着框架走。另外确认一下
packages/wikimem和独立仓库是不是同一份代码:我看到 workspace 里有cli.py而独立仓库还没有,如果是两份拷贝,已经开始漂移了。(发布前按语:查了,不是漂移——独立仓库只是本地 checkout 落后了 4 个 commit,上游早已合并零依赖 CLI(wikimem#12),两边是同一份代码。)
归属 该写的 ADR wikimem 日记原语(按天 markdown、append-only);retrieve 加 time_range+ 正则快通道(stdlib only);可选 recency 衰减项进融合;vectors 缓存记录 model/dim + mismatch 降级;M4 重定义为 CLI/serve 双薄壳XnneHangLab 时间意图识别 tool call(模糊时间/事件锚定 → time_range);diary_writing 升格为正式 memorize 策略(何时写、写什么、文风);情绪状态明确留在应用运行时;记忆可视化前端(走自己的路由) 一句话版本:框架管“确定性的机制”,应用管“要花钱的判断”;发生过的事进日记,现在为真的事进 wiki;真相永远是 markdown,其他一切都是可删的缓存、或可换的壳。
后记
感谢 Fable 老师,搞清 RRF 和 Hybrid Search 关系的同时让我看清 wikimem 该怎么走,XnneHangLab 应该自己负责什么。提了两个 PR:





