本文基于
AI Mind项目的真实实现整理。GitHub:github.com/HWYD/ai-min...
对应代码版本:
v0.4.4线上链接:ai.hwyblog.cloud/instant-min...
AI Mind 是一个持续迭代中的 Next.js AI 聊天项目。从最基础的本地聊天开始,逐步加入流式传输、工具调用、MCP、Skill 和 Agent 能力。
如果你对这个项目感兴趣,或者这篇文章对你有一点帮助,也欢迎顺手到 GitHub 帮 AI Mind 点个 Star⭐,这会是对我继续更新很大的鼓励。

到了 v0.4.4,问题从"一个会话内部怎么记住东西",变成了:
多个聊天上下文之间,怎么把短期记忆隔开?
打个比方:我一个会话在讨论博客怎么写,另一个会话在排查部署报错,还有一个在跑 Tasklist Agent 或 Delivery Chain。如果这些内容继续共用同一个对话线程,不同主题的消息、摘要、固定决策和最终回复很快就搅成一锅粥,模型下一轮看到的上下文也可能张冠李戴。
所以 v0.4.4 做的不是继续往单个线程状态里塞东西,而是把原来的单一当前对话记忆,拆成当前浏览器会话下的多个短期记忆容器。
这篇我想梳理的核心问题是:
AI 聊天的多会话不只是加一个侧边栏。真正要命的点是:多个聊天上下文怎么各自管好自己的短期记忆,新建、切换、刷新、流式输出、最终回复写入,每一步都不串线。
1. 背景:为什么单个线程状态不够用了
在 v0.4.2 / v0.4.3 里,AI Mind 的记忆模型可以简化成一条线:
text
当前浏览器会话
↓
一个对话线程
↓
一个线程状态
↓
消息 + 摘要 + 固定决策
这个设计用来解决"当前会话内部怎么组织短期记忆"是够用的。
一个会话里,messages 保留最近原文,summary 承接早期上下文,pinned decisions 保存关键决策。工具、MCP/资源、Tasklist Agent、Delivery Chain 的最终用户可见回答,也可以作为最终回复写入当前会话记忆。
但实际用起来,没人会只盯着一个主题聊。我自己的日常可能是:
text
会话 A:整理 v0.4.4 博客
会话 B:排查线上部署问题
会话 C:生成版本任务
会话 D:生成交付报告
这些上下文如果还挤在一个线程状态里,问题就很明显了:
text
博客上下文污染部署上下文
部署排错内容跑进博客摘要
Tasklist 最终回复和普通聊天搅在一起
Delivery 最终报告被压进不相关会话记忆
固定决策被不同主题反复覆盖
所以 v0.4.4 真正要解决的是:
怎么在当前浏览器会话下,让多个聊天上下文各自管好各自的短期记忆。
2. 核心目标:把一个当前会话扩展成多个短期记忆容器
v0.4.4 的核心变化不是"加一个侧边栏",而是把原来的单一当前对话记忆拆成多个会话容器。
每个持久化会话都对应一个独立的短期记忆线程:
text
当前浏览器会话
↓
会话注册表
↓
会话 A -> 线程状态 A
会话 B -> 线程状态 B
会话 C -> 线程状态 C
这样之后,会话 A 的消息、摘要、固定决策不会进入会话 B;会话 B 的最终回复也不会写回会话 A。
可以用一张图表示:
这版真正要解决的是会话归属问题:
text
哪些会话属于当前浏览器会话?
当前选中的会话是谁?
页面刷新时恢复哪个会话?
模型上下文来自哪个线程状态?
最终回复最后写回哪个线程状态?
流式输出中如何避免切换导致写错会话?
侧边栏只是用户看到的壳,真正吃劲的是这套归属规则。
3. 核心模型:会话注册表 + 会话线程
v0.4.4 的模型可以拆成两层。
第一层是 会话注册表,它回答:
当前浏览器会话下有哪些持久化会话?
第二层是 会话线程,它回答:
某个会话里记住了什么?
会话注册表:管理有哪些会话
会话注册表 是当前浏览器会话下的会话索引。
它大致包含:
text
selectedConversationId(当前选中会话 ID)
conversations(会话列表)
updatedAt(更新时间)
其中会话列表最多保留 10 条,按 lastActiveAt(最后活跃时间)倒序排列。每条会话还包含 hasMessages 标记,用于过滤未持久化消息的条目。
这里有几个关键点:
text
只属于当前浏览器会话
不是全局会话表
不是账号级历史
只收纳持久化会话(hasMessages=true)
空白草稿不进入注册表
最多保留 10 条
所以我对它的定位是:
会话注册表是短期记忆容器索引,不是聊天历史数据库。
它负责告诉系统:当前浏览器会话下有哪些可恢复的会话、当前选中哪一个、最近活跃顺序是什么。
但它不直接存聊天消息,也不承担完整历史系统的职责。
会话线程:管理会话记住了什么
每个持久化会话都映射到一个独立的对话记忆线程。
线程里面仍然是原来的 AiMindThreadState:
text
messages(消息)
summary(摘要)
pinnedDecisions(固定决策)
lastCompactedAt(上次压缩时间)
也就是说,v0.4.4 没有改变线程状态的内部结构。它没有给每条消息新增 conversationId,也没有引入来源标记、Agent 标记或运行时元数据。
这版改的是归属关系,不是状态结构。
可以概括成:
text
v0.4.2 / v0.4.3:
一个线程状态内部怎么整理短期记忆。
v0.4.4:
多个线程状态之间怎么隔离和管理。
这个设计避免了消息结构膨胀。会话归属由注册表和会话级线程 ID 解决,线程状态本身继续保持纯文本。
具体来说,v0.4.4 用 chat-conversation:{sessionHash}:{conversationHash} 格式的线程 ID,其中 sessionHash 来自浏览器会话,conversationHash 由会话和 conversationId 共同派生,确保每个会话独享一个记忆线程。v0.4.4 不复用旧版的 chat:{sessionHash} 格式,也不兼容旧版单线程 ID。
4. 新聊天为什么先是空白草稿
做多会话时,一个常见做法是:用户点击"新聊天",系统立刻创建一条持久化会话。
这个方案看起来直接,但会带来一堆后续问题:
text
用户只是点了新聊天,但没有输入内容
最近列表里出现空会话
空会话占用 10 条上限
刷新后要处理空会话恢复
多次点击新聊天会产生幽灵条目
后续还要设计空会话清理机制
为了避免这些坑,v0.4.4 选了更干净的语义:
点击"新聊天"只进入空白草稿状态。只有首条用户消息被接受后,才创建正式会话。
具体来说,前端发首条消息时必须显式标记 createConversation: true,服务端收到后才创建正式会话、分配会话级线程,并把它加入注册表。服务端随后通过 X-AI-Mind-Conversation-Id 响应头返回新创建的 conversationId,客户端拿到后把后续请求从草稿模式切到持久化会话模式。
有个细节值得提:聊天请求结构是严格的互斥约束------请求要么带 conversationId(已有会话),要么带 createConversation: true(新建会话),缺了哪个都会被直接拒绝,不存在模糊路径。
生命周期如下:
这样设计之后,语义很清楚:
text
未发送消息的草稿不污染最近列表
草稿不占用 10 条上限
不需要稳定态的空会话清理
刷新未发送草稿时,只恢复安全的空白工作面
只有真正产生用户输入后,会话才值得持久化
点"新聊天"只是进入新的工作面,不代表已经产出有价值的会话。
这个细节看着偏产品,但对记忆语义影响很大。注册表一旦允许空会话进去,就会连带引出空会话恢复、空会话裁剪、空标题、幽灵条目等一长串额外复杂度。
5. 当前选中会话必须由服务端校验
单会话的时候,系统只有一个当前线程,很多地方可以默认落到同一个线程。
多会话之后,默认值就危险了。
典型的情况有:
text
前端本地存储里的 selectedConversationId 已经过期
某个会话已被裁剪
客户端传了不存在的 conversationId
用户在多个会话之间快速切换
发请求和 UI 当前选中状态不在同一时刻
服务端要是在这时候"自动找一个默认会话",串线几乎是必然的:
text
恢复错会话
上下文用错线程状态
最终回复写回错误会话
A 会话看到 B 会话的消息 / 摘要 / 固定决策
所以在 v0.4.4 里,服务端校验过的当前选中会话是事实源。
也就是说:
text
页面恢复以服务端验证过的选中会话为准
模型可见上下文以服务端验证过的选中会话为准
完成回复写入以服务端验证过的选中会话为准
本地存储只能作为恢复提示
缺失 / 无效的 conversationId 不能回退到旧线程或另一个会话
这不是多余防御,而是多会话记忆的基本前提。
在 AI 聊天里,UI 显示错数据已经是 bug;模型要是拿错上下文继续生成,问题更隐蔽,也更难查。
6. 恢复、上下文、写入都不能串线
多会话隔离不能只看 UI 切没切对,还要看三条链路是不是都隔离了:
text
恢复(hydrate):恢复哪个会话
上下文(context):模型看到哪个会话的上下文
写入(append):最终回答写回哪个会话
恢复只能加载当前选中会话
我切到会话 A,页面就只能恢复 A 的最近消息。
不能因为 B 最近更新过,就把 B 的内容拿回来;也不能因为 A 恢复失败,就悄悄展示另一个会话的数据。
恢复接口不是"找一个可用的会话",而是:
恢复用户明确选中的持久化会话。
具体来说,恢复请求必须携带 conversationId,服务端校验该会话是否属于当前浏览器会话。校验失败时返回 404 错误或安全空态,不会偷偷替换成其他会话的数据。恢复返回的数据包含 conversationId、threadId(可选)、messages、summaryPreview(可选)、pinnedDecisions 和 restored 标记,且必须通过禁字段检查,确保不暴露原始检查点、会话 ID 或其他内部状态。
失败时应该返回安全空态或可恢复错误态,而不是跨会话兜底。
上下文只能来自当前选中会话
比 UI 显示更重要的是模型上下文。
如果会话 A 在讨论博客,会话 B 在排查部署,那 A 的下一轮模型上下文不能包含 B 的摘要或固定决策。
正确链路应该是:
text
当前选中会话 A
↓
线程状态 A
↓
摘要 A + 固定决策 A + 最近消息 A
↓
最新用户输入
而不是:
text
所有会话的消息混合
最近更新的线程状态
前端传来的完整历史
多会话真正危险的地方,不是页面显示错了,而是模型基于错误上下文继续回答。
最终回复写入流开始时的会话
v0.4.3 已经把普通聊天、工具/MCP/资源、Tasklist 最终回答、Delivery 最终报告纳入最终回复记忆。
到了 v0.4.4,这些最终回复还要解决归属:
写回哪个会话?
流式输出的时候,UI 当前选中状态可能会变。所以最终回复不能按"输出结束时 UI 选中的会话"写入,而应该写回请求开始时捕获的会话。
也就是:
活跃请求归属在请求开始时捕获,最终回复写入流开始时的会话。
这样,UI 状态变化不会影响服务端写入归属。
7. 流式输出中为什么禁止新建和切换
从 UI 上看,流式输出中禁用"新聊天"和会话切换,只是一个交互限制。
但在 AI 聊天运行时里,它是记忆写入安全的一部分。
助手正在输出的时候允许切会话,会出现这些问题:
text
当前助手输出归属哪个会话?
最终回复应该写入哪个线程状态?
线程记忆状态会不会显示到另一个会话?
流中断后是否触发错误恢复?
刚输出的助手内容会不会被切换后的恢复清掉?
当然,也可以做更复杂的方案:允许切换、后台继续输出、按流 ID 重新路由数据块、最后把结果写回原会话。
但这对 v0.4.4 来说太重了。
当前 AI Mind 页面同一时刻只支持一个活跃聊天流,MVP 阶段最稳的策略就是:
text
助手流式输出中禁用新聊天
助手流式输出中禁用会话切换
待审核状态中也禁用这些操作
流归属绑定到请求开始时的会话
这不是为了少做功能,而是先把归属关系做清楚。
多会话最怕的不是功能少一点,而是串线。
8. 最近列表为什么最多 10 个
v0.4.4 的注册表、桌面侧边栏和移动端抽屉都最多展示 10 个持久化会话。
这个 10 不是技术限制,而是产品边界。
如果一开始保留太多会话,很自然会被拖向完整历史系统:
text
是否需要搜索?
是否需要分页?
是否需要删除?
是否需要重命名?
是否需要归档?
是否需要跨设备同步?
这些都不是 v0.4.4 要解决的问题。
这版只支持当前浏览器会话下的几个短期上下文切换,所以 10 个最近活跃会话已经足够。
排序按最后活跃时间,而不是创建时间。
规则是:
text
首条用户消息提升时更新 lastActiveAt(创建时即设置)
后续用户发送消息更新 lastActiveAt(通过 touchConversation)
完成助手回复更新 lastActiveAt(通过 touchConversation)
单纯切换会话只更新 selectedConversationId,不更新 lastActiveAt
超过 10 个后,裁剪最久未活跃的持久化会话
"最近会话"更接近"最近使用",不是"最近创建"。
同时,单纯点进去看一眼不应该频繁扰动列表;真正发送消息或收到完成的助手回复,才说明这个会话被继续使用。此外,selectConversation 方法只负责切换选中状态,不会触发列表重排;只有 touchConversation 才会更新 lastActiveAt 并影响排序。
9. 侧边栏只是入口,不是完整历史管理
v0.4.4 做了桌面侧边栏和移动端会话选择器,但我不想把这篇文章写成"仿 ChatGPT 侧边栏"。
UI 只是入口,真正吃劲的是背后的会话归属关系。
桌面端只做最小能力:
text
品牌区
新聊天入口
最近会话列表
当前会话高亮
折叠 / 展开
移动端也只做最小可用版本:
text
顶部当前选中会话入口
抽屉式会话选择
新聊天入口
最近会话列表
当前会话高亮
这些能力足够表达会话注册表和当前选中会话。
但这版没有继续扩展成完整历史管理:
text
搜索
分页
删除
重命名
归档
分享
项目入口
文件库入口
悬浮操作菜单
完整 ChatGPT 侧边栏复刻
视觉上继续继承 instant-mind 聊天页的壳层和主题变量。组件实现上优先复用本地 apps/webapp/components/ui/ 里的 shadcn/ui 基础组件;缺的基础组件在本地补齐就好,不依赖 MCP 或远程注册表作为运行时组件来源,也不直接搬整套官方侧边栏模块。
跟这版的定位一致:
UI 只需要表达会话注册表和当前选中会话,不该把 v0.4.4 拖成完整应用导航系统。
10. Tasklist 和 Delivery:只接最终文本,不接管运行时
v0.4.4 需要保持 v0.4.3 的最终回复记忆能力。
换句话说,普通聊天、工具/MCP/资源、Tasklist 最终回答、Delivery 最终报告,最终都要写入当前选中会话记忆。
但这不等于会话记忆要接管 Tasklist 或 Delivery 的运行态。
Tasklist
Tasklist Agent 仍然有自己的:
text
图状态
检查点
人工中断
Agent 运行状态
恢复线程标识
这些不进入会话线程状态。
v0.4.4 只做一件事:
Tasklist 跑完后,把最终用户可见文本写入当前选中会话记忆。
展开来说,Tasklist Agent 启动时会接收 conversationId 参数,完成后把最终回答写入该会话对应的线程状态。会话只收最终对话结果,不管 Tasklist 的执行和恢复。
Delivery
Delivery Chain 仍然是本地运行的工作流。
它内部的:
text
运行时产物
工作流进度
子 Agent 原始调用/结果
管理器追踪
也不进入聊天记忆。
v0.4.4 只把已完成 / 已阻塞的最终报告文本写入当前选中会话记忆。
因此,Tasklist / Delivery 的边界一句话总结:
多会话只改变最终文本写入哪个会话,不动 Tasklist 和 Delivery 的运行时本体。
Tasklist 和 Delivery 通过接收 conversationId 参数来确定最终文本写入哪个会话,它们的内部执行和恢复机制跟会话记忆完全隔离。
不然会话注册表很容易被扩成 Agent 运行注册表,或者把 Delivery 变成持久化工作流历史,这已经跑出 v0.4.4 的范围了。
11. 范围收口:为什么暂时不做完整聊天历史系统
多会话做出来以后,其实很容易一路往完整历史系统滑。
搜索、分页、重命名、删除、归档、跨设备同步、账号级历史,都会变成很自然的下一步。
但 v0.4.4 没有往这个方向走。
原因是这版的目标不是做一个完整聊天历史产品,而是先把"当前浏览器会话下多个短期记忆容器"这件事做稳。
也就是:
text
会话可以被创建、选择和恢复
每个会话有独立线程状态
恢复、上下文、最终回复写入不串线
流式输出中不会因为切换导致写错线程
v0.4.2 / v0.4.3 的记忆内核不被破坏
所以这版没加 ChatSession / ChatMessage 业务表,也没做搜索、分页、删除、归档、跨设备同步或长期记忆。
这些能力不是没价值,而是属于后续"完整历史系统"的范围。v0.4.4 先把短期记忆容器的归属关系打稳。
12. 这版改归属关系,不改记忆内核
v0.4.4 看起来改动面挺大:注册表、草稿、路由、侧边栏、移动端抽屉、创建/切换/恢复都有变化。
但它的核心是:
在已有聊天记忆外围加一层会话归属关系。
它没有重写记忆内核。
保持不变的部分包括:
text
线程状态继续保持纯文本
聊天线程消息继续只保存 id / 角色 / 文本 / 创建时间
安全恢复继续只返回安全文本
服务端权威上下文继续有效
摘要压缩按会话隔离
固定决策按会话隔离
v0.4.3 最终回复记忆继续工作
Tasklist 检查点/恢复语义不变
Delivery 本地运行语义不变
流核心数据块合并不变
前端 reducer 公开结构不变
Prisma 业务结构不新增 ChatSession / ChatMessage
这也是 v0.4.4 最值得记录的地方。
它表面上是多会话 UI,实际是一次记忆归属扩展:不破坏原来的短期记忆结构,只把它从单线程拉成会话级线程。
验证也不能只看 UI 能不能点,还要覆盖:
text
注册表数量限制
草稿提升
会话切换
按会话恢复隔离
按会话上下文隔离
按会话最终回复写入隔离
流式输出防护
禁字段安全
v0.4.2 / v0.4.3 无回归
13. 总结:多会话的重点是上下文归属
v0.4.4 最后交出来的不是完整聊天历史系统,而是一个多会话短期记忆容器。
它解决的从来不是"怎么做一个侧边栏",而是:
text
当前浏览器会话下有哪些持久化会话
新聊天什么时候才值得转成正式会话
当前选中会话由谁确认
每个会话如何拥有独立线程状态
恢复 / 上下文 / 写入如何避免串线
流式输出中如何保护活跃请求归属
回头看 v0.4.2 / v0.4.3,它解决的是:
一个会话内部,短期记忆怎么压缩、整理和控制上下文。
那么 v0.4.4 解决的就是:
多个会话之间,短期记忆怎么隔离、选择和写回。
侧边栏只是外面能看到的壳。真正重要的是它背后的 会话注册表、空白草稿、服务端校验选中会话 和 会话级线程状态。
对 AI 聊天来说,多会话不是简单多开几个窗口,而是给每个上下文一个清晰、可验证、不会串线的归属。
项目地址
👉 GitHub:github.com/HWYD/ai-min...
👉 线上体验:ai.hwyblog.cloud/instant-min...
如果这篇文章或者 AI Mind 项目对你有所帮助,也欢迎顺手帮项目点个 Star⭐。这个支持对我来说很重要,也会让我更有动力继续整理后续版本的实现过程、设计取舍和踩坑复盘。