多轮上下文记忆
大模型本身是无状态的,每次调用都是独立的。所谓的“记忆”,本质上是工程侧对输入上下文的动态管理策略。核心目标是在成本、延迟、记忆长度三者之间取得平衡。
我通过长期摘要+短期滑动窗口实现记忆。前端LocalStorage只存展示数据,传给LLM的是‘增量摘要 + 最近5轮原文’。摘要异步生成,避免阻塞;关键实体单独抽取,防止信息丢失。同时支持云端持久化,实现跨会话记忆。这个方案在成本和效果之间取得了平衡,已在项目中稳定运行。
一、完整标准流程拆解(分两层,各司其职)
- 持久展示层:LocalStorage 存全部原始完整对话
作用是给用户看( 这里不做任何压缩、删减,原样存储用户提问 + 模型完整回复)
- 页面刷新、关闭重开,完整聊天记录不丢失;
- 侧边历史记录、历史对话列表,展示完整问答原文;
- 永久保存全量对话,不会丢失早期聊天细节。
LLM 推理层(传给 LLM):采用"增量摘要 + 滑动窗口"双轨制——
①最近对话原文按 Token 数量动态截断(而非硬编码轮数):每次从最新消息开始往前累加 Token,直到达到上限(如 4000 tokens),保证绝不超出模型上下文窗口,同时保留最近的交互细节,保证上下文连贯; 【原想法是:截取最近 5~8 轮完整对话---------但是如果一轮对话就有2000字,8轮直接撑爆窗口了】② 早期
历史摘要采用增量摘要:每次只把"旧摘要 + 本轮新增的 N 轮对话"压缩成"新摘要",确保摘要生成的 Token 消耗恒定为 O(1),不随对话总长度线性增长;③ 强制抽取
关键实体(时间、人名、ID、数字、核心决策)随摘要一起结构化保存,防止纯文本摘要丢失关键细节;④ 最终拼接:「历史摘要 + 关键实体清单 + 最近动态截断的原文 + 当前用户新问题」作为 Prompt 输入大模型。
【为什么把’当前用户新问题’放在最后?--------因为大多数LLM对输入末尾的注意力权重更高,把当前问题放最后能让模型更聚焦于用户当下的诉求。】
二、这么拆分的两大核心好处
- 兼顾用户体验与接口成本
- 用户侧:能随时翻阅全部完整聊天记录,不会丢失任何原文;
- 服务侧:传给大模型的文本大幅缩短,Token 消耗显著降低,不会超出模型上下文窗口,推理速度更快、幻觉更少。
- 解耦不冲突(本地存储的完整数据和发给模型的精简上下文是两套独立数据,互不干扰)
- 本地永远有完整版兜底;
- 推理只使用轻量化压缩版。
