AI Mind 的单线程短期记忆设计:如何压缩、整理并控制当前会话上下文

发布时间:2026/7/18 22:26:40
AI Mind 的单线程短期记忆设计:如何压缩、整理并控制当前会话上下文 本文为作者原创首发于掘金现同步发布到 CSDN。内容整理自AI Mind项目的真实开发过程。GitHubhttps://github.com/HWYD/ai-mind对应代码版本v0.4.2 - v0.4.3线上体验https://ai.hwyblog.cloud/instant-mindAI Mind 是一个基于 Next.js 持续迭代的 AI Chat 项目项目从本地大模型聊天起步逐步扩展流式协议、工具调用、MCP、Skill Runtime 和 Agent 等能力。如果这篇文章或 AI Mind 项目对你有所帮助也欢迎到 GitHub 给项目点个 Star⭐这会是对我继续整理后续版本复盘很大的鼓励。做到v0.4.x这一阶段时我遇上了一个看起来很基础、但很容易做偏的问题AI Chat 到底应该怎么记住当前会话第一反应很简单——把 messages 存起来不就行了页面刷新后恢复一下体验就接上了。但真放到 AI Runtime 里看事情没那么单纯。记忆不只是恢复 UI。它还会影响下一轮模型的上下文。普通聊天、工具调用、资源读取、Agent 执行、Workflow 过程每一步都在产生状态。这些状态如果全塞进 chat memory后面上下文会越来越乱——搞不好内部运行态就泄漏到前端了或者被拼进下一轮模型输入里。所以这两个版本我既没直奔长期记忆也没做 ChatGPT 左侧那种多会话历史列表而是先给当前浏览器会话补了一层底子安全、可恢复、有界的单线程短期记忆。这篇文章不侧重刷新后怎么恢复页面而是整理这套短期记忆的设计思路当前会话怎么分层、长对话怎么压缩、关键结论怎么保留工具和智能体的最终回答怎么纳入同一套记忆边界。1. 记忆不只是保存 messages很多 AI Chat Demo 的早期做法都很直接前端维护 messages每次请求把完整历史发给后端后端调模型前端流式追加回答。项目早期这么跑完全没问题。普通问答、流式输出、基本上下文连续够用。但 AI Mind 能力一多问题就出来了。前端messages本质上是 UI 状态。它可能包含半截流式消息、错误状态、工具卡片、资源卡片跟服务端状态也不一定对齐。把它当长期可信的模型上下文来源后面迟早翻车。完整历史也不能无限注入模型。对话一长上下文越来越大成本涨响应慢早期无关内容还会干扰当前问题。还有一个更隐蔽的问题真实的 AI 应用不是只有文本聊天。Tool Calling、MCP/resource、Tasklist Agent、Delivery Chain 都会产生中间执行态。它们看起来都跟上下文沾边但不都应该进 chat memory。比如下面这些就不适合进普通聊天记忆工具参数 / 工具结果 MCP 信封 / 资源原始内容 Tasklist GraphState / HITL checkpoint Delivery RuntimeArtifact / 工作流进度 子智能体原始调用 / 结果 原始提示词 / 模型响应 / 堆栈跟踪说穿了这个阶段真正要解决的不是怎么存 messages而是怎么把当前会话整理成一份可控的短期记忆。2. 什么是单线程短期记忆这里说的单线程不是操作系统线程是当前浏览器会话对应的那一条聊天运行时线程。它只服务当前浏览器里的当前聊天——不跨浏览器不跨多个会话不做左侧历史列表不做长期用户画像不做向量长期记忆不保存完整 Agent 执行轨迹。范围很克制。我想先解决的就是当前会话内的连续性用户刷新后最近对话还在继续追问时模型能看到整理后的上下文对话变长后早期内容被压缩掉工具和智能体的最终回答能被记住但中间执行过程不污染 chat memory。整体结构可以简化成这样当前浏览器会话chat:sessionHashAiMindThreadStaterecent messages最近用户可见文本summary早期对话摘要pinned decisions关键决策与边界lastCompactedAt最近压缩时间核心就是AiMindThreadState。它既不是完整聊天历史表也不是 Agent 运行时快照就是当前会话的一份短期记忆状态。3. ThreadState把当前会话整理成三层记忆AI Mind 的短期记忆没有设计成一组无限增长的 messages而是拆成三层AiMindThreadState ├── messages最多 8 条即最近 4 轮对话 │ └── 最近的用户可见文本消息 ├── summary最多 2500 字 │ └── 更早对话的有界摘要 ├── pinnedDecisions最多 20 条每条 ≤300 字 │ └── 用户明确决策、架构边界、关键结论 └── lastCompactedAt └── 最近一次成功压缩时间这三个字段各解决不同问题。recent messages最近内容保留原文最近几轮对话跟下一轮问题最相关适合保留原文。比如我刚跟 AI 讨论完这篇博客重点不放在刷新恢复而是放在短期记忆如何压缩、整理和控制上下文下一轮我继续问那标题和大纲怎么调整——这时候最近几轮原文很重要。它不只是信息还包含上下文节奏、措辞偏好和刚拍板的方向。过早压缩成摘要反而容易丢细节。所以messages的职责就是保留最近的用户 / 助手文本消息上限 8 条4 轮。但这里有一个硬边界必须保持纯文本。一个可持久化的ChatThreadMessage只有四个字段id、role、text、createdAt。不存source、turnId、displayKind也不存工具参数、资源内容、Agent 步骤、Workflow 进度。这个设计看起来保守但很重要。只要 messages 里混进运行时对象后面的 hydration、context builder、compaction 都会变复杂。更严重的是内部执行态可能被展示给前端或者被拼进下一轮模型上下文。规则很明确最近内容可以保留原文但只能保留用户可见文本。summary早期内容压缩成摘要短期记忆不能无限增长。每轮都完整保留的话当前会话也会慢慢变成无限历史。模型上下文越来越长请求成本越来越高早期无关信息干扰当前问题。所以有了summary用来保存更早对话的有界摘要最多 2500 字。它不是原始 checkpoint不是原始提示词不是模型响应就是一段安全的自然语言文本。我对它的定位很简单recent messages 保留最近原文summary 承接更早上下文。比如前面聊过很多版本规划、技术取舍和博客方向后面不需要每次都把完整过程塞给模型。更合适的做法是把早期内容压成一段背景摘要让模型知道大方向就行。pinned decisions关键决策单独保留summary 适合记录背景但有些内容不能只放在摘要里。比如用户明确拍板的设计方向、版本明确不做的范围、架构边界、后续回答必须遵守的约束、博客或面试里必须准确表达的结论——这些内容如果混在 summary 里多次压缩后可能被弱化、改写甚至丢掉。所以 AI Mind 单独设计了pinnedDecisions最多 20 条每条不超过 300 字。可以理解为当前会话里一份关键结论清单。举个例子- 这篇博客不按 v0.4.2 / v0.4.3 分段写而是融合成单线程短期记忆设计文章。 - 文章重点不放在刷新恢复而是放在短期记忆如何压缩、整理和控制上下文。 - 不把该能力描述为长期记忆、多会话历史或完整 Agent Memory。这些不是普通聊天背景是后续回答需要持续遵守的决策。pinned decisions 的价值就是把不能忘的结论从普通摘要里提出来单独保留。4. 写入策略完成后写一次不是每个 chunk 都写AI Chat 通常是流式输出的。模型一边生成一边返回 token工具调用和 Agent 执行过程里还可能夹杂工具卡片、资源卡片、进度事件、中断事件等中间状态。但 memory 不能跟着每个 chunk 写。流式过程里的内容不一定是最终结果——助手回答还没生成完、用户取消了请求、工具调用失败后模型重新解释、Agent 命中 HITL pause、Workflow 中途异常——这些半截内容如果提前写进 memory下一轮模型上下文就被污染了。所以 AI Mind 的写入策略很明确只在整个回合确认完成后写一次。否是用户输入模型 / 工具 / Agent / Workflow 执行流式输出中间内容是否得到最终用户可见回答?不写入已完成记忆确认已完成追加用户文本 助手最终文本具体约束只在completed、blocked或final状态下写入cancelled、failed、interrupted、paused都不写不按流式块写一次回合只写一次空白文本不写memory 失败不影响用户已经收到的回答上层 catch 住降级处理所以短期记忆虽然表面上像保存消息实际更像一个运行时追加边界。它要判断的不只是有没有文本还要判断这轮对话是否真的完成了、这段文本是否适合作为下一轮上下文、它是不是只是运行过程里的临时状态。5. 压缩策略recent messages summary pinned decisions当 messages 超过 8 条后就不能继续无限追加了。这时候触发 compaction把当前会话重新整理成三层结构压缩前turn1...turn12Compactionsummary早期对话摘要pinned decisions关键决策recent messages最近 4 轮原文压缩前可能是 12 轮对话共 24 条消息。压缩后变成summary: - turn1 到 turn8 的背景摘要≤2500 字 pinnedDecisions: - 用户确认不做长期记忆 - 用户确认博客重点放在短期记忆压缩和整理 - 用户确认工具和智能体只保存最终回答 recentMessages最近 4 轮共 8 条: - turn9 user - turn9 assistant - turn10 user - turn10 assistant - turn11 user - turn11 assistant - turn12 user - turn12 assistant这套设计同时做三件事。第一早期对话进入 summary——保留背景不保留完整原文。第二关键结论进入 pinned decisions——保留明确决策和架构边界避免在摘要里被稀释。第三最近 4 轮继续保留原文——保证下一轮对话仍然自然不会因为过早压缩而丢掉细节。所以模型下一轮真正看到的上下文不是全部历史 messages而是summary pinned decisions recent messages 最新用户输入这个设计解决的是一个很现实的问题长对话不是不能记而是不能用完整历史无限追加的方式记。压缩用的是模型生成deepseek/deepseek-v4-protemperature 0structured output但只信任模型输出的 summary 和 pinned decisions。recentMessages和lastCompactedAt这类结构性字段由本地派生不信任模型生成。如果压缩失败不覆盖已有 ThreadState不影响用户已经收到的本轮回答——下一轮仍然可以继续。6. 服务端 ThreadState 才是模型上下文事实源在没有服务端 memory 之前前端把完整 messages 发给后端后端直接构造模型输入——很多 AI Chat Demo 都这么干。引入 ThreadState 后这个模式就得调整了。前端 messages 更多是 UI 数据不是可信的上下文事实源。它可能过长、可能跟服务端 memory 重复、可能包含 UI 状态、可能带来历史残留。某些消息只应该展示不应该进模型。服务端已经有压缩后的 summary / pinnedDecisions / recentMessages 了。所以在 AI Mind 里模型上下文以后端 ThreadState 为准前端请求 - 当前用户最新输入 - 兼容性的 messages 数据 服务端上下文 - ThreadState.summary - ThreadState.pinnedDecisions - ThreadState.recentMessages - 最新前端用户输入也就是说前端负责展示服务端 ThreadState 负责决定模型真正看到什么。这个转变还挺关键的——AI Chat 的 memory 一旦进入服务端就不再只是恢复 UI而是参与模型输入构造。既然它会影响模型回答就必须由服务端统一整理、压缩和过滤。否则两个上下文来源前端完整历史 服务端 ThreadState一起注入轻则重复重则冲突。7. 工具和智能体的结果如何进入短期记忆普通聊天的短期记忆结构稳定后下一个问题是工具调用、MCP/resource、Tasklist Agent、Delivery Chain 的结果要不要进 memory不进体验断裂——完成了一次工具辅助回答刷新后普通直答能恢复但工具回答不见了。执行了一次 Delivery Chain最终报告生成了刷新后这轮对话丢了。这不符合用户对当前会话的直觉。但直接塞进去又破坏边界。所以 AI Mind 的策略是最终用户可见回答进入 memory中间执行过程不进入 memory。禁止用户输入Tool / MCP / Agent / Workflow 运行时中间运行时状态工具参数 / 结果MCP 信封 / 资源原始内容GraphState / checkpointRuntimeArtifact / 工作流进度最终用户可见回答最终回合适配器ThreadState 纯文本消息不持久化到聊天记忆这里有一个很重要的判断工具和智能体的执行过程属于运行时不属于聊天记忆。聊天记忆只管这一轮最终对用户说了什么。这样既能让用户刷新后看到工具 / 智能体的最终回答又不会把执行轨迹、artifact、checkpoint 混进普通聊天上下文。8. 两个容易越界的例子Tasklist Agent 和 Delivery ChainTasklist Agent 和 Delivery Chain 都不是简单文本回答各有自己的运行时结构。Tasklist Agent只保存最终文本摘要Tasklist Agent 有自己的 GraphState、checkpoint、HITL interrupt、AgentRun 状态。这些东西是 Agent 执行和恢复需要的业务运行态不是聊天历史。把它们写进 chat memory 会导致普通聊天上下文混入 Agent 内部状态、hydration 可能暴露 checkpoint 或 HITL 数据、Tasklist 恢复的线程 ID 和 chat memory 线程 ID 混淆、artifact markdown 被误当成普通助手消息。所以 Tasklist 只保存用户目标和最终回答文本摘要不保存 tasklist artifact markdown、GraphState、checkpoint、interrupt 数据、AgentRun 内部状态。关键取舍Tasklist artifact 是 Agent 产物不是聊天记忆。聊天记忆只保留这轮对话最终对用户呈现的文本摘要。Delivery Chain只保存已完成 / 已阻塞的最终报告Delivery Chain 更像一次单次本地工作流。内部产生计划阶段输出、任务阶段输出、审查阶段输出、工作流进度、RuntimeArtifact、子智能体调用/结果、管理器追踪——这些对运行过程有意义但不应该进当前聊天记忆。用户刷新后真正需要看到的是这轮 Delivery Chain 最终给了什么报告。所以 Delivery 只保存用户输入和completed/blocked状态的最终报告文本不保存工作流进度、RuntimeArtifact、管理器追踪、子智能体原始调用/结果、失败 / 异常 / 取消的输出。另外Delivery 最终报告可能很长。为了避免单个报告撑爆 memory保存前做确定性截断上限 8000 字超出部分截断并追加提示而不是再调模型生成执行摘要。这个选择比较克制——当前版本要解决的是最终文本可以进短期记忆不是要引入新的摘要系统。9. Hydration 只是出口恢复的是安全文本不是运行时快照虽然这篇文章重点不是刷新恢复但 hydration 是检验 memory 边界是否清晰的出口。如果一个 memory 系统的恢复接口返回了原始 checkpoint、模型响应、GraphState、RuntimeArtifact那说明它已经把运行时状态和公开聊天历史混在一起了。AI Mind 的 hydration 只返回安全 DTOGET /api/chat/thread ↓ 读取 ThreadState ↓ 转换为前端可展示的文本消息 ↓ 返回 threadId / messages / summaryPreview / pinnedDecisions / restored而且 hydration 出口还有一层安全校验assertNoForbiddenHydrationFields递归检查返回的 DTO 里是否包含apiKey、checkpoint、cookie、deliveryChain、graphState、mcpEnvelope、providerConfig、rawPrompt、resourceContent、runtimeArtifact、sessionId、stack、subagentInvocation、toolArgs、toolCall、toolResult、turnId、tasklist、workflowProgress等禁止字段。如果出现直接抛错不返回。也就是说Hydration 返回的是整理后的聊天记忆不是运行时快照。这个边界很重要——一旦 hydration 暴露内部状态后续前端展示、调试工具、日志甚至模型上下文都有可能被污染。10. 让短期记忆可控去重、截断和失败降级memory 是增强体验的运行时支撑能力不能反过来拖垮主流程。下面几个细节看起来小但处理不好就容易出问题。同一轮最多写一次一轮回答可能被多个运行时层观察到完成——orchestrator 捕获最终助手文本、agent coordinator 拿到最终回答、workflow runtime 也生成最终报告。如果没有去重同一轮可能重复写入。AI Mind 的处理是追加前读取当前 ThreadState有稳定消息 ID 时按 ID 去重没有稳定 ID 时按最近两条消息用户 助手的文本完全匹配去重。不需要持久化 turnId也能避免重复最终回合。长文本做确定性截断Delivery 最终报告上限 8000 字超出部分截断并追加提示标记。没有引入模型摘要因为截断最直接、可测试、没有额外模型调用、不引入新的产品语义。compaction 失败不破坏已有记忆compaction 涉及模型生成可能失败、超时、格式不合法。所以只信任模型输出里的 summary 和 pinned decisions经过 Zod schema 校验recentMessages 和 lastCompactedAt 由本地派生。如果 compaction 失败不覆盖已有 ThreadState不影响用户已经收到的本轮回答。memory 不可用时安全降级checkpoint 存储可能不可用数据库也可能临时异常。这时候普通聊天降级为无记忆聊天不暴露原始数据库错误、checkpoint 错误、模型栈。上层chat-orchestrator、delivery-chain统一 catch 住记录日志后继续——“Chat memory 是可降级能力失败不影响已经完成的用户回答。”11. 为什么没有做长期记忆和历史表这两个版本还有一个明显特点克制。没有做多会话历史、ChatGPT 左侧历史列表、ChatSession / ChatMessage 业务表、历史分页、历史搜索、编辑/删除持久化、长期记忆、向量记忆、pgvector、LangGraph Store / PostgresStore、Memory Inspector、Agent 运行摘要、工具观察摘要、执行摘要。这些东西不是没价值只是它们属于另一个阶段的事。当前阶段的重点是先把当前会话里的短期记忆边界打稳。如果一开始就把长期记忆、多会话历史、Agent 执行历史、工具记录持久化全放进来memory 系统会迅速膨胀反而看不清最核心的边界——什么是用户可见消息、什么是模型上下文、什么是运行时内部状态、什么可以恢复、什么可以压缩、什么绝对不能进 memory。AI Mind 这一阶段选择先做当前聊天 ThreadState说白了就是先打地基。只有地基清楚了后续再考虑多会话历史、长期记忆或更复杂的 Agent Memory才不会把不同层级的状态混在一起。12. 总结AI Mind 这两个版本最后形成的不是一个聊天历史表而是一套当前会话 ThreadState 设计。它把当前会话拆成三层recent messages最近 4 轮原文、summary早期上下文摘要、pinned decisions关键决策单独保留。工具、MCP/resource、Tasklist Agent、Delivery Chain 的最终用户可见回答也可以进入同一套纯文本记忆——但它们的执行过程不会进来。回过头看实际上解决的就三件事刷新后当前会话能恢复不是空白聊天长对话后模型看到的是整理过的上下文不是无限历史工具和智能体的最终回答可以成为聊天记忆但中间执行态留在运行时边界内不乱跑。AI Chat 的短期记忆重点不是保存更多而是只保存该保存的并且让这些内容以可控方式参与下一轮上下文。项目地址 GitHubhttps://github.com/HWYD/ai-mind 线上体验https://ai.hwyblog.cloud/instant-mind如果这篇文章或者 AI Mind 项目对你有所帮助也欢迎给项目点个 Star⭐。你的支持会是我持续更新这个系列、继续整理项目实现过程和设计复盘的很大动力。