实现一个简单的短期记忆DEMO
短期记忆让应用能在同一段会话中接续上下文。实现的核心很直接:按会话标识保存消息,每轮请求时取出对应历史交给模型。但当会话越来越长,系统还需要决定带哪些内容、如何控制长度,以及怎样压缩较早的对话。
一、短期记忆在程序里是什么
短期记忆可以理解为一条会话线程的持续状态。应用用 threadId 标识会话,把用户消息和模型回复按先后顺序保存;下一轮再取回同一线程的历史,让模型获得这段对话的背景。
数据库记录的基本结构
messages 表保存每条消息的线程 ID、角色、正文和时间;按 thread_id 与消息顺序查询。不同线程的数据不会混在一起。
messages ├─ id 消息顺序 ├─ thread_id 会话标识 ├─ role user / assistant ├─ content 消息正文 └─ created_at 保存时间
Demo 还提供切换线程、列出线程、查看当前历史、清空当前线程等命令。模型调用成功后,应用在同一事务中保存本轮的用户问题和助手回答,避免只保存半轮对话。
本节 Demo 的代码位于 Gitee 仓库的 Page7 目录:https://gitee.com/toxplore/ai/tree/master/Page7。程序默认将数据库保存在 data/short-term-memory.db。
二、Demo 完整流程图
下面的流程图对应 Page7 中的 Java 程序:从启动配置和 SQLite 初始化,到每轮问答、命令处理,以及 thread 之间的隔离与恢复。
1. 启动:准备会话消息存储
图较宽时可横向滚动查看。
SQLite 在这个 Demo 中负责持久保存会话消息。模型不直接访问数据库;Java 应用负责按 threadId 读取和写入。
2. 普通问答:读取历史、请求模型、保存本轮
注意:Demo 在模型成功返回后才调用 saveTurn()。如果 API 报错,本轮用户问题不会写入 SQLite;此前成功的轮次仍保留。
3. Thread 命令:切换、查看或清空会话
/thread <会话ID>switchThread() 验证 ID,并读取历史条数;main 更新当前 ID。实际历史会在下一次普通提问时重新加载。
/memoryprintMemory() 调用 loadMessages(),打印当前 thread 的消息。
/threadsprintThreads() 调用 listThreads(),列出已有消息的 thread ID。
/resetclearThread(threadId) 只删除当前 thread 的消息。
/quitmain() 结束交互循环;SQLite 中已保存的会话仍保留。
4. 记忆隔离与持久化
三、会话变长后会遇到什么问题
| 问题 | 为什么会发生 | 可能的影响 | 解决方案 |
|---|---|---|---|
| 上下文窗口有限 | 每轮把全部历史重新发送,消息会不断累积。 | 请求可能超出模型限制;即使没超限,也会增加输入 token、成本和响应时间。 | 用模型对应的 tokenizer 估算 token;预算中先扣除系统提示、当前问题、工具定义、回答空间和余量。超出阈值时压缩较早消息,只发送“摘要 + 最近几轮原文 + 当前问题”。 |
| 历史噪声变多 | 已结束的话题、重复内容和临时细节仍在历史中。 | 模型可能被无关背景分散注意力。 | 围绕当前任务挑选上下文:保留任务目标、已确认决定、约束、待办和近期对话;不相关的旧话题不放进本轮请求,需要时再从完整历史查回。 |
| 简单截断会丢重点 | 只保留最近几轮,可能删掉更早但仍有效的决定、偏好或任务目标。 | 模型忘记约束,或重复询问已经确认的信息。 | 截断前先把较早消息整理成结构化摘要,保留目标、决定、偏好和未完成事项;完整原文继续存储,并记录摘要覆盖到哪条消息,便于回查。 |
| 摘要可能失真 | 模型归纳时可能遗漏细节、混淆时间顺序,或把推测写成事实。 | 后续回答依赖错误或过时的摘要。 | 采用增量摘要(旧摘要 + 新一段历史),要求区分讨论、决定和完成;只记有依据的内容,冲突标为待确认。校验摘要格式并保留原文和消息位置。 |
| 线程和隐私管理 | 线程标识使用错误会读错会话;历史也可能包含敏感信息。 | 上下文串线,或不必要地保留、发送敏感内容。 | 查询始终按已验证的 threadId 限定,并检查用户是否有权访问该线程;发送模型前按策略过滤敏感信息,支持清理或删除过期会话。 |
四、怎样从历史中挑出有用上下文
筛选要围绕当前问题和当前任务来做,而不是把历史越多越好。一个可解释、容易调试的第一版,可以把上下文拆成三部分:
任务摘要
目标、已确认决定、约束、进度、待办,以及仍有效的用户偏好。
近期原文
最近几轮完整消息,保留当前话题的细节、措辞和问答衔接。
再加上系统提示和当前问题,组成发给模型的本轮上下文。完整历史仍留在 SQLite,需要时可以按线程回查。
- 先保留系统提示、当前问题和最近几轮对话。
- 加入仍有效的目标、决定、约束、进度和待办。
- 若仍超预算,再压缩较早的消息;重复、无关或已被新决定替代的内容可省略。
- 新信息与旧信息冲突时,以用户明确更新的内容为准;无法判断时标记待确认。
五、长度控制与摘要如何配合
先计算本轮可用预算
输入可用预算 = 模型上下文上限 - 预留的回答空间 - 安全余量 历史可用预算 = 输入可用预算 - 系统提示、工具定义、当前问题及其他输入
实际 token 数应使用对应模型或服务提供的统计能力估算。字符数不能准确替代 token 数;API 返回的用量适合事后校准,发送前仍要做预算控制。
超过阈值时压缩较早消息
常见实现是直接调用大模型做摘要,或采用增量摘要:将旧摘要 + 新发生的一段较早消息交给模型,让它生成更新后的摘要。程序负责触发时机、选择消息范围、保存摘要和校验长度;模型负责理解并归纳内容。
触发条件:历史 token 估算超过阈值
↓
选取较早的一段消息
↓
旧摘要 + 这段消息 → 摘要模型
↓
保存新摘要与已覆盖的消息位置
↓
本轮上下文 = 摘要 + 最近消息 + 当前问题
摘要保留什么
| 字段 | 要记录的内容 | 示例 |
|---|---|---|
| 当前目标 | 仍在进行的任务 | 实现 Java 短期记忆 Demo |
| 已确认 | 明确决定或已经采用的方案 | SQLite 按 threadId 保存消息 |
| 进度与待办 | 已完成事项、下一步和未解决问题 | 基础会话已完成;待实现上下文裁剪与摘要 |
| 偏好与事实 | 仍有效的用户要求和必要的具体信息 | 函数需要注释;百炼额度错误码 |
摘要更新时应要求模型只记录消息明确支持的内容,区分“讨论过、已决定、已完成”,保留必要的数字、路径和错误码;冲突标为待确认。保存时可以记录摘要覆盖到哪条消息,避免重复摘要或漏处理消息。
总结
短期记忆让应用能够按会话保存并恢复交互历史。每轮请求时,应用取出当前线程的历史,与用户的新问题一起组成上下文交给模型;模型返回后,应用再保存本轮问答。随着历史增长,系统需要在 token 预算内筛选上下文:保留当前任务相关信息和近期原文,把较早对话压缩为可校验的摘要,同时保留完整历史供后续回查。