你在一次对话里告诉模型自己的名字,几轮之后再问,它通常答得出来。关掉页面,隔天新建会话,它可能又不认识你。有些产品却能记住你的写作偏好、常用工具和正在推进的项目。

模型参数没有在聊天过程中改变。应用把相关信息存了下来,并在下一次请求时重新交给模型。这个过程通常被称为 AI Memory。

AI Memory 是应用对对话信息进行提取、存储、检索、更新和遗忘,再将相关内容放回模型上下文的机制。

模型负责理解和生成,应用负责记忆管理。两者需要配合,但职责不能混为一谈。

对话历史只是短期记忆

最简单的记忆方式是把历史消息原样发回模型:

messages = [
    {"role": "user", "content": "我写 TypeScript 时使用 Bun。"},
    {"role": "assistant", "content": "记住了。"},
    {"role": "user", "content": "这个项目应该用什么命令安装依赖?"},
]

reply = client.chat.completions.create(
    model="example-model",
    messages=messages, 
)

模型能回答 bun install,因为第一轮消息仍在当前请求里。应用删掉第一轮之后,模型就失去了这条信息。

这种做法适合短会话,优点是信息完整、实现简单。会话变长后,应用会遇到三个问题:

  • 历史消息持续占用上下文窗口。
  • 旧消息会在每轮请求中重复计费。
  • 无关内容会干扰当前任务。

因此,长期运行的聊天助手和 Agent 不能只靠完整历史。

长期记忆保存什么

应用不应该把用户说过的每句话都存成长期记忆。有效的长期记忆通常属于下面几类:

类型 示例 常见存储形式
用户偏好 默认用中文、代码注释简短 结构化字段
稳定事实 用户使用 Windows 和 Bun 键值记录
项目状态 博客运行在 Astro,部署到 Vercel 项目档案
任务进度 已完成构建,尚未推送 状态记录
经历摘要 上次排查了图片路径错误 文本摘要
原始资料 文档、聊天记录、会议纪要 文档库或对象存储

这些信息的寿命不同。语言偏好可以保存几个月,任务进度可能几小时后就失效。应用需要给记忆添加时间、来源、作用域和有效期。

一条可管理的记忆可以写成:

{
  "subject": "user",
  "key": "package_manager",
  "value": "bun",
  "scope": "astro-blog-shokax",
  "source": "conversation:2026-08-28",
  "confidence": 0.96,
  "updated_at": "2026-08-28T18:00:00+08:00"
}

scope 防止信息串项目,source 方便追溯,confidence 可以帮助系统处理不确定内容。只保存一句“用户喜欢 Bun”,后续很难判断它适用于全部项目还是某个仓库。

记忆系统的四个动作

一个长期记忆系统至少要完成四件事。

提取

应用从对话中识别值得保存的信息。用户说“以后这类文章都用中文”,系统可以保存语言偏好;用户随口说“今天有点累”,通常没有长期保存的必要。

提取可以由规则完成,也可以交给模型判断。规则稳定但覆盖面窄,模型覆盖面广却可能误判。

存储

结构清晰的信息适合存数据库,例如语言、时区和常用框架。篇幅较长的经历摘要适合存文档库。两者可以同时存在。

检索

新请求到来时,应用根据用户、项目和当前问题取回相关记忆。直接按字段查询适合稳定偏好,向量检索适合语义相关的经历和文档。

更新与遗忘

用户可能换操作系统、换工作项目,也可能明确要求删除某条记录。系统要覆盖旧值、降低旧记忆权重,或者彻底删除。

只会写入、不会更新的记忆库很快就会充满冲突。

结构化记忆和向量记忆

两种方案解决的问题不同。

结构化记忆使用数据库字段保存明确事实:

SELECT value
FROM user_preferences
WHERE user_id = ?
  AND key = 'response_language';

它适合语言、时区、主题偏好和账号设置。查询精确,更新简单,也容易向用户展示和删除。

向量记忆把文本转成 embedding,根据语义相似度检索:

query_vec = embed("这个博客用什么构建?")
memories = memory_index.search(query_vec, top_k=5) 

它适合“上次怎样解决构建失败”“以前讨论过哪些选题”这类无法靠固定字段覆盖的问题。向量相似不代表事实正确,检索结果仍需经过时间、权限和作用域过滤。

实际产品通常混合使用:稳定偏好走数据库,经历和资料走语义检索。

一次记忆检索怎样进入请求

应用可以先查询相关记忆,再把它们放进 system prompt 或单独的上下文区:

memories = memory_store.find(
    user_id=user_id,
    scope=project_id,
    query=user_message,
    limit=5,
)

memory_context = "\n".join(
    f"- {memory.value} (updated: {memory.updated_at})"
    for memory in memories
)

messages = [
    {
        "role": "system",
        "content": f"""使用以下记忆辅助回答。
记忆可能过期;与用户当前表述冲突时,以当前表述为准。

{memory_context}""",
    },
    {"role": "user", "content": user_message},
]

这里有一条重要规则:当前用户输入的优先级高于历史记忆。用户说“这个新项目改用 pnpm”,系统不能因为旧记录写着 Bun 就继续执行 bun install

为什么错误记忆比幻觉更麻烦

普通幻觉通常影响一次回答。错误记忆会在后续会话中反复进入上下文,持续改变模型的判断。

常见错误包括:

  • 模型把用户的假设当成事实保存。
  • 系统把另一个项目的配置写进当前项目。
  • 用户修改偏好后,旧值没有失效。
  • 向量检索返回语义相近但主体不同的记录。
  • 多个用户共用一个记忆空间,发生数据串用。

例如,用户说“假设我的服务器在东京,延迟该怎么估算”,系统如果保存“服务器位于东京”,下次回答部署问题时就会基于一个虚构条件。

记忆写入前应区分陈述、假设、引用和临时指令。高风险信息还需要用户确认。

隐私与权限

记忆系统会保存比普通聊天记录更集中的个人信息。语言偏好风险较低,住址、健康情况、工作机密和账号信息则属于敏感数据。

产品至少应提供这些能力:

  • 用户可以查看系统记住了什么。
  • 用户可以修改或删除单条记忆。
  • 用户可以关闭长期记忆。
  • 不同账号、组织和项目使用独立作用域。
  • 敏感信息默认不进入长期记忆。
  • 删除操作同时清理数据库、向量索引和缓存。

“删除聊天记录”不一定等于“删除从聊天中提取的记忆”。产品需要把两种数据的关系说明白。

Agent 为什么更需要记忆

聊天助手可以在回答结束后停止。Agent 可能跨越几十个步骤,调用文件、终端、浏览器和外部 API。它需要记录当前目标、执行结果和失败原因。

Agent 记忆通常分成三层:

  1. 工作记忆:当前步骤需要的消息和工具结果,直接放在上下文窗口。
  2. 任务状态:待办、已完成事项、错误和下一步,保存为结构化状态。
  3. 长期经验:项目约定、用户偏好和过去的解决方案,按需检索。

把三层混在一段聊天记录里,任务越长越难维护。任务状态应由程序更新,模型只负责提出变化。文件是否存在、测试是否通过这类确定性信息,应该来自工具结果。

什么时候不需要长期记忆

长期记忆会增加复杂度,也会扩大隐私风险。下面几种场景用当前上下文就够了:

  • 一次性问答和临时写作。
  • 用户不希望系统保存信息。
  • 内容包含密码、密钥或私人文件。
  • 任务结束后信息没有复用价值。
  • 应用无法提供查看和删除记忆的入口。

如果一个功能只为省下用户重复输入两句话,却需要长期保存大量聊天内容,这个交换并不划算。

怎样设计可用的记忆系统

  • 先定义允许保存的类型。不要从“所有内容都存”开始。
  • 保存来源和时间。模型需要知道一条信息从哪里来、多久以前写入。
  • 用作用域隔离项目。个人偏好、组织规则和仓库配置不能放在同一个命名空间里。
  • 让用户纠正记忆。修改入口比更复杂的自动推理更重要。
  • 设置遗忘策略。任务状态完成后归档,临时信息到期删除,低置信度记忆逐步降权。
  • 控制取回数量。一次塞入几十条记忆会占用上下文,也会制造冲突。
  • 评估写入和检索。分别测试“该不该记”“能不能找到”“找到后有没有帮助”。

写在最后

应用应该把记忆当成可检查的数据,而不是模型内部的神秘能力。每条记忆带上来源、作用域和更新时间,用户也能查看和修改,系统才有机会长期保持可靠。