AI Mind 的多会话短期记忆容器:如何隔离多个聊天上下文 本文为作者原创首发于掘金现同步发布到 CSDN。内容整理自AI Mind项目的真实开发过程。GitHubhttps://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。可以用一张图表示当前浏览器会话会话注册表会话 A会话 B会话 C线程状态 A线程状态 B线程状态 C消息 / 摘要 / 固定决策消息 / 摘要 / 固定决策消息 / 摘要 / 固定决策这版真正要解决的是会话归属问题哪些会话属于当前浏览器会话 当前选中的会话是谁 页面刷新时恢复哪个会话 模型上下文来自哪个线程状态 最终回复最后写回哪个线程状态 流式输出中如何避免切换导致写错会话侧边栏只是用户看到的壳真正吃劲的是这套归属规则。3. 核心模型会话注册表 会话线程v0.4.4 的模型可以拆成两层。第一层是会话注册表它回答当前浏览器会话下有哪些持久化会话第二层是会话线程它回答某个会话里记住了什么会话注册表管理有哪些会话会话注册表是当前浏览器会话下的会话索引。它大致包含selectedConversationId当前选中会话 ID conversations会话列表 updatedAt更新时间其中会话列表最多保留 10 条按lastActiveAt最后活跃时间倒序排列。每条会话还包含hasMessages标记用于过滤未持久化消息的条目。这里有几个关键点只属于当前浏览器会话 不是全局会话表 不是账号级历史 只收纳持久化会话hasMessagestrue 空白草稿不进入注册表 最多保留 10 条所以我对它的定位是会话注册表是短期记忆容器索引不是聊天历史数据库。它负责告诉系统当前浏览器会话下有哪些可恢复的会话、当前选中哪一个、最近活跃顺序是什么。但它不直接存聊天消息也不承担完整历史系统的职责。会话线程管理会话记住了什么每个持久化会话都映射到一个独立的对话记忆线程。线程里面仍然是原来的AiMindThreadStatemessages消息 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新建会话缺了哪个都会被直接拒绝不存在模糊路径。生命周期如下否是点击新聊天空白草稿状态是否发送首条消息?不进注册表不创建持久化线程状态发送 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 选中的会话写入而应该写回请求开始时捕获的会话。也就是活跃请求归属在请求开始时捕获最终回复写入流开始时的会话。不参与本次写入会话 A 发送消息捕获流归属: A助手流式输出完成最终回复写入线程状态 A会话 B这样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 的运行态。TasklistTasklist Agent 仍然有自己的图状态 检查点 人工中断 Agent 运行状态 恢复线程标识这些不进入会话线程状态。v0.4.4 只做一件事Tasklist 跑完后把最终用户可见文本写入当前选中会话记忆。展开来说Tasklist Agent 启动时会接收conversationId参数完成后把最终回答写入该会话对应的线程状态。会话只收最终对话结果不管 Tasklist 的执行和恢复。DeliveryDelivery 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 聊天来说多会话不是简单多开几个窗口而是给每个上下文一个清晰、可验证、不会串线的归属。项目地址 GitHubhttps://github.com/HWYD/ai-mind 线上体验https://ai.hwyblog.cloud/instant-mind如果这篇文章或者 AI Mind 项目对你有所帮助也欢迎给项目点个 Star⭐。你的支持会是我持续更新这个系列、继续整理项目实现过程和设计复盘的很大动力。