数据截至 (上游 commit 3dcf4cad0124)
记性:线程持久化、分支、上下文压缩与 RAG
30 秒导读: 上一章讲模型怎么跑(04),这一章讲它记得住什么。 对话原文要落盘、要能长出分支、要在有限窗口里活下来;文档知识则根本不进窗口, 而是躺在本地向量库里、等模型开口要才取。
1. 这一章讲什么(零基础也能懂)
一句话定义: 这一章讲 Jan 的存储与上下文治理——聊天记录存在哪、怎么改一条回答而不丢原来那条、 消息多到装不下时丢谁、以及大文档怎么在不撑爆窗口的前提下被用上。
为什么这是个问题。 模型本身没有记性。它每一轮看到的,只是你这次塞给它的那一叠消息。 所以"记性"从来不是模型的能力,而是外面这层应用的工程活:
| 要解决的事 | 白话 | Jan 的答案 |
|---|---|---|
| 关掉 app 明天还在 | 对话得落到磁盘 | 每线程一个目录 + JSONL 追加日志(移动端换 SQLite) |
| 点"重新生成"别把旧答案冲掉 | 同一个问题要能存多份回答 | 消息 metadata 里挂父指针,兄弟节点即多版本 |
| 聊久了塞不进窗口 | 得丢掉或压缩老消息 | 先估 token,裁最旧;开关打开才调模型压成摘要 |
| 一本 300 页的 PDF | 全塞进去必炸 | 切块 → 向量化 → 存本地库 → 模型自己调工具捞 |
一句话直觉: 把模型的上下文窗口当内存,把线程目录和向量库当磁盘。 这一章讲的就是这套"虚拟内存"——什么时候换入、什么时候换出、换出的东西去哪了。
用起来什么样。 用户视角只有三个动作:正常聊天(自动落盘)、点回答下面的 < 2/3 >
切换版本(分支)、往输入框拖一个 PDF(RAG 入库)。底下这三条链路互不相干,正是这一章要拆的三条主线。
2. 顶层全景(四层记性,各管一段)
Jan 的"记性"不是一个模块,是四层各自独立的机制。它们唯一的交汇点是
CustomChatTransport 组装请求的那一刻(见 02)。
怎么读这张图:从上到下是数据离窗 口越来越远——越往下越"冷",越需要显式动作才能捞回窗口。
┌──────────────────────────────────────────┐
│ ① 窗口内:这轮真正发给模型的消息数组 │
│ 由 context-manager 裁剪/压缩后产出 │
└───────────────┬──────────────────────────┘
│ 读取「活跃路径」
┌───────────────┴──────────────────────────┐
│ ② 内存态:useMessages(zustand store) │
│ 整棵消息树都在,含所有历史版本 │
└───────────────┬──────────────────────────┘
│ 异步持久化
┌───────────────┴──────────────────────────┐
│ ③ 磁盘:线程目录 JSONL / 移动端 SQLite │
│ thread.json + messages.jsonl │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ ④ 旁路:向量库(每线程一个 .db 文件) │
│ 只在模型调 retrieve 工具时才回到 ① │
└──────────────────────────────────────────┘
部件一句话职责:
| 部件 |
|---|