Strands
时间切片按时间回答「发生了什么」。但人脑还有另一种问法:「关于那件事,我们都聊过什么?」——这个问题横切时间,而不是沿着时间。Strand 就是为这种问法而生的语义层:一个关键词,贯穿每一个携带它的切片。
| 记忆类型 | 组织方式 | 单元 | 回答的问题 |
|---|---|---|---|
| 情景记忆 | 时间 | 时间切片 | 「什么时候发生了什么?」 |
| 语义记忆 | 主题 | Strand | 「关于 X 说过什么?」 |
工作原理
每个切片的 frontmatter 里带着 tags——像 wedding-speech、running 这样的关键词。当切片关闭时,这些标签被织进一个全局文件:memory/episodic/strands.json。
文件里每个键是一个 strand(一个标签),值是携带这个关键词的所有切片的相对路径——「那个东西的完整历史」,横跨时间:
{
"wedding-speech": [
"2026/06/22/1400",
"2026/07/01/0915",
"2026/07/08/1630"
],
"running": [
"2026/06/22/1400"
]
}
路径是相对的,顺着它能直接走到切片目录。一个 strand 加它指向的切片路径,就是那件事的全部历史——不多也不少。
派生,不是撰写
Strand 没有独立的撰写步骤。标签由 turn analyzer 在每轮的 housekeeping 里维护——它读对话、归类主题、写入 tags,并且优先复用已有标签,好让同一个概念跨语言、跨措辞合并到一处。切片关闭时(以及进行中切片的快照保存时),updateStrands 把每个标签织进索引,追加路径、去重、写回。
编织是合并优先的:标签在创建新键之前先被规范化(去首尾空白、ASCII 转小写、全角折叠为半角、合并内部空白)并与现有键匹配——所以 Apex 和 apex 落在同一个 strand 上,而不是把一个主题拆成两个。只有真正新的标签才创建新键。
标签织成 strand。你永远不需要直接编写 strand。
保持索引干净
一个只会追加的索引会腐坏:错别字把一个概念拆到两个键上,一年前只用过一次的标签永远留存。所以每逢切片关闭,整合流程(consolidateStrands)分两步走:
- 确定性修剪(每次都跑)。 从未成为线索的 strand 被剪掉——具体说,是只携带极少量切片、且所有切片都已陈旧的 strand。只要有足够的切片或至少一个最近的切片就能存活,所以真正的新话题绝不会在出现当天被剪掉。这一步不需要模型。
- 模型合并(索引长到 25 个以上 strand 时才启用)。 规范化抓得住机械重复,但语义重复——名字里的错别字、同一概念的两个名字——需要判断力。模型审查索引并提出一张 from→to 合并映射(每次最多 30 条,精确率优先:有疑问就不合并),随后工程层合并路径列表、移除冗余的键。
整个流程是防御性的:任何失败都原样返回索引——切片关闭永远不会因为整合而中断。
Strand 如何被读取
这个索引不是只写的,它通过几条路径喂养每一次对话:
- 记忆主题菜单。 系统提示词里带着一个 Memory topics 区块:按最近活跃排序的 strand 清单,各自标注最后一次出现的时间。当你提到其中之一时,主 agent 知道该去问回忆了。
- 回忆管道。 recall 同事预先拿到 strand 清单,用
readStrand把匹配的 strand 追踪到携带它的每一个切片。Strand 是回忆的主题轴;时间线窗口是时间轴。详见回忆。 - 时间线投影。 每次时间线对账都会解析出每个条目的 strand 归属,所以时间线索引本身也记录着哪些 strand 穿过哪些切片。
刻意保持薄
Strand 是一个薄而无损的索引:从关键词到切片路径的映射,仅此而已。这是设计,不是欠缺——丰富性由切片承载,strand 只负责指向。索引可以丢失、可以重建;真相永远在切片里。