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