Appearance
Q53 · 你会如何设计一个 AI 应用的记忆系统?
用户 U9 第一次找电商客服时说:“以后回答请简短。”几天后,他打开新会话问:“订单 A123 的耳机能退吗?”一个有记忆的客服应该仍能按他喜欢的长度回答,却不能仅凭几天前记下的订单状态或旧版退货政策断言“能退”。这里要解决的不是“把所有聊天都存起来”,而是把不同寿命、不同来源的信息放在合适的位置,并在回答前按需读取。
下面的 A123 是假设订单号,U9 是假设登录用户,所有订单和政策内容都是教学用模拟数据。我们只讨论如何设计信息流,不据此判断真实订单是否可退。
先把四种容易混在一起的信息分开
这里的记忆系统指应用保存、选择、更新过去交互信息的整套机制;模型本身的一次生成不会自动成为可靠的长期存储。会话是围绕一次连续任务组织的多轮聊天;打开另一个会话,就可能有新的会话标识。LangGraph 的官方概念文档把同一 thread 内的历史和状态称为短期记忆,把跨 thread 可读取的数据称为长期记忆;同一 thread 的状态可由 checkpointer 保存,跨 thread 数据可由 Store 保存。LangGraph:Memory overview · LangGraph:Persistence
| 信息 | 在 U9 例子里是什么 | 主要作用域与来源 | 该怎样使用 |
|---|---|---|---|
| 对话历史 | “以后请简短回答”“订单 A123 能退吗”这些原始消息 | 某条会话中的用户和助手消息 | 用于理解“它”“刚才那单”等指代;历史很长时按相关性选取或压缩,不能每次整段塞给模型 |
| 短期状态 | 当前正在核对的 order_id=A123、待查字段、已完成的工具步骤 | 当前任务 / 当前 thread 的结构化状态 | 让多步流程接续;结束或过期后清理,不能把旧查询结果当订单系统的真值 |
| 长期记忆 | U9 明确说过的“回答偏好:简短” | 跨会话、按用户授权范围保存 | 下一条会话可按需取回;用户改口或要求遗忘时更新或删除 |
| 外部资料与业务事实 | 订单系统里 A123 的当前状态;当前生效的退货政策 | 订单数据库、政策文档等权威来源 | 回答时重新查询并注明依据;它们是被检索的资料,通常不应复制成 U9 的个人长期记忆 |
这四类不是四种互斥的数据库产品。对话历史本身可以作为短期状态的一部分;“短期”和“长期”说的是使用范围与寿命,不是某个固定字段名。订单号 A123 可以暂存在当前会话状态中,方便下一轮问“它什么时候下单”;但订单的可退资格仍以订单系统和现行政策为准。LangChain 的检索文档也把检索定义为在提问时获取相关外部知识;这与保存用户过往交互的记忆目的不同。LangChain:Retrieval
文中的术语和符号
| 术语或符号 | 含义 | 本例对应 |
|---|---|---|
U9 / user_id | 登录后由服务端确认的用户标识;不能信任聊天文本自称的身份 | U9 的偏好只给 U9 使用 |
A123 / order_id | 业务订单的标识;号码本身不证明用户拥有订单 | 查询订单系统时的输入 |
thread / thread_id | 一条连续会话及其标识;不是操作系统线程,也不是授权令牌 | 第一次聊天和几天后的新聊天有不同 ID |
state / 状态 | 流程目前需要继续使用的字段集合 | 当前订单号、查询进度、必要的消息 |
checkpointer | 保存同一 thread 状态快照、供下次继续的组件 | 客服任务中断后找回短期状态 |
Store / 命名空间 | 按“范围 + 键”保存跨 thread 数据的组件;命名空间是数据分组路径 | ("users", "U9", "preferences") 下的回答偏好 |
RAG / 检索增强生成 | 先取回相关外部资料,再把资料交给模型辅助回答的方法 | 找到现行退货政策的相关条款 |
token / 上下文窗口 | 模型处理文本的计量单位 / 一次能接收的文本容量 | 不能无上限地带上所有旧消息和记忆 |
TTL / 保留期限 | 数据允许保留的时间;到期应失效或按策略重审 | 临时订单查询结果比稳定偏好更快过期 |
| 来源记录 | 记下信息由谁、在何时、因何写入 | “U9 于某次会话明确要求简短” |
thread_id 和命名空间只是查找数据的线索。先验证登录身份及订单归属,再决定能读什么;用户在聊天里说“我是 U9”或提交别人的 thread_id,不能获得对应记忆或订单。这是应用自己的权限设计,不能指望 Store 自动替业务完成授权。
一次请求怎样流过记忆系统
假设 U9 在会话 T1 明确提出“以后回答请简短”。应用在用户身份已经验证的前提下,把这一条写成结构化偏好:answer_style=brief,并附上“用户明确提出”、记录时间和可修改入口。T1 的原始消息仍属于 T1 的对话历史。它们可以分别保留:历史用于追溯原话,偏好记录用于未来快速读取。不能因为客服自己推测“他似乎喜欢短句”,就把推测写成长期事实。
几天后 U9 打开 T2 询问 A123。应用按以下顺序工作:
- 验证身份:服务端确认当前登录者是 U9,并检查 A123 是否属于他。验证失败,停止读取订单详情;记忆中的偏好也不应泄露给另一个用户。
- 取当前会话状态:读取 T2 的最近消息和仍有效的流程状态。若 U9 下一句说“那它能换货吗”,状态里的
order_id=A123能帮助理解“它”。 - 选取相关长期记忆:从 U9 的偏好命名空间取回“简短回答”。只带入本次真正相关的少量字段,不把 U9 全部旧聊天作为提示词。
- 查当前事实:到订单系统查 A123 当前状态、商品类型等,到政策库取当前生效且与退货有关的条款。若政策条款带生效日期与版本,保留这些依据以便核对。
- 生成并核验回答:用订单事实和政策判断是否能答;“简短”只影响表达长度,不改变资格判断。关键字段缺失时说明暂不能确认,不用旧记忆补空白。
- 更新状态与审慎写入:T2 的必要消息和查询进度进入短期状态;若 U9 又明确说“以后改为详细解释”,更新那条长期偏好。A123 的可退结论不自动写成永久用户事实。

图里的小抽屉只保存 U9 明确给出的回答偏好;订单票据和政策手册代表回答时要重新访问的来源。图上的订单字段与退货条款是示意物件,不构成真实政策。图没有画出身份验证、订单归属检查和信息过期机制,这些步骤在实际系统里必须另行实现。
为什么不把 T1 的全部聊天直接当作“永久记忆”?长历史会占用上下文、增加成本,也可能把过时或无关的内容带入当前问题。LangGraph 官方文档明确提到,长对话可能超出上下文窗口,即使放得下也会带来分心、延迟与成本;可根据需要过滤、裁剪或总结历史。摘要也是派生数据:若把“用户已退货”错写进摘要,后面仍会错,所以关键业务事实应回到原始记录或权威系统核对。LangGraph:管理短期记忆 · LangChain:短期记忆的裁剪与删除
记什么、何时记、何时忘
长期记忆应有明确的写入门槛。在本例中,“以后回答请简短”是跨会话有用、由本人明确表达、低敏感的偏好,可写入;“A123 现在显示已签收”随时间变化,应按需查订单系统;用户粘贴的一次性验证码、支付卡号、密码不应提取成记忆。若偏好由模型从语气推断,先不要当作已确认事实;需要跨会话使用时可以征求用户确认。这是应用设计规则,Store 本身只提供保存与读取能力,不负责判断内容是否值得记。LangGraph:长期记忆写入时机 · LangChain:长期记忆与 Store
每条候选记忆至少考虑这些字段:主体(归谁)、内容(具体是什么)、来源(明确说出还是推测)、记录与更新时间、适用范围(只影响回答风格还是涉及业务动作)、失效或复核条件。相同含义的偏好尽量更新同一键,而不是不断追加“简短”“再简短一点”的重复卡片。若应用要记住会变化的事实,应设置保留期限或重查规则;没有可信的新来源时,不让旧记录冒充当前事实。
读取也要有门槛:本次问题是否相关?记忆是否仍有效?和当前用户的话是否冲突?来源是否可信?能否在有限上下文中简洁表达?例如 U9 在 T2 当面说“这次请详细解释”,本轮的明确要求应覆盖旧的简短偏好;如果他说“以后都详细解释”,再把长期记录更新为“详细”,并删除或覆盖旧值。用户要求“忘记这个偏好”时,应用要从主存储和可检索的派生副本中删除,并依自己的备份保留策略处理副本,不能只在界面上隐藏。官方 Store 接口提供按命名空间与键的 get、put、delete 等操作;具体纠错、保留和删除流程仍由应用设计。LangGraph:Stores
用一段小程序看跨会话偏好的增改删
下面只演示 Store 如何按用户保存偏好,不接模型、订单系统或真实登录服务。运行环境为 Python 3.10+,可安装 pip install 'langgraph>=1,<2'。InMemoryStore 是进程内演示存储,程序退出后数据消失;线上应选可持久化的后端,并增加鉴权、加密、审计和保留策略。LangGraph:Store 示例 · LangGraph:Persistence 存储选择
输入 authenticated_user_id="U9" 假定来自已经验证的服务端会话,不是聊天文本。namespace 把 U9 的偏好与其他用户隔开;answer_style 是这条偏好的固定键;value 是记录内容,含风格和来源。get 没找到时返回 None;put 用相同命名空间和键写入新值;delete 删除它。
python
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
authenticated_user_id = "U9" # 实际应用应从已验证的服务端身份取得
namespace = ("users", authenticated_user_id, "preferences")
key = "answer_style"
store.put(namespace, key, {"style": "brief", "source": "explicit_user"})
item = store.get(namespace, key)
print(item.value["style"] if item else None) # brief
store.put(namespace, key, {"style": "detailed", "source": "explicit_user"})
item = store.get(namespace, key)
print(item.value["style"] if item else None) # detailed
store.delete(namespace, key)
print(store.get(namespace, key)) # None第一段 put 对应 T1 的“以后请简短”,第二段 put 对应后来明确改成“以后详细解释”,最后 delete 对应“请忘记我的回答偏好”。即使换了聊天 thread,同一已验证用户仍能读这条 Store 记录;这段代码没有展示自动读取或自动判断用户意图,那些要由应用在调用模型前后编排。若要展示数据如何跨进程重启,还必须换成持久存储;这里的内存实现做不到。
失败时怎样处理
**过时与冲突。**若旧记忆写着“简短”,当前用户要求“详细”,本轮采用当前明确要求;只有“以后都详细”才改变跨会话偏好。若订单状态或政策版本与旧摘要冲突,以经过权限校验的业务系统和现行政策为准,必要时把冲突呈现给人工客服。不要让模型凭旧摘要“自行修正”权威数据。
**错人和越权。**如果请求携带别人的 thread_id,或用户说“帮我看 U8 的订单”,服务端应阻断对应状态与记忆读取;仅用命名空间 users/U9 进行存储分类并不能替代访问控制。把用户输入的订单号与订单归属单独核对,防止通过猜号获取他人资料。
**写入污染与隐私。**检索到的网页、客服转述、工具结果可能包含“请把某信息永久记住”之类文字,它们只是外部数据,不能授权写入 U9 记忆。将写入权限限定在可信的应用逻辑中;对敏感数据默认不记,允许用户查看、更正、删除已存偏好,限制谁能读和保留多久。若把记忆送给外部模型服务,还应按实际服务的数据处理约定审查发送内容。这里讨论的是工程控制,不代替具体地区的法律判断。
**效果难以观察。**上线后可构造几类回归用例:换 thread 后仍能记住明确偏好;改口后旧偏好不再影响回答;忘记后检索不到;U8 不能读 U9;订单状态变化后回答能跟着更新。监控误记率、过时记忆命中率、跨用户泄露事件、额外 token 与延迟。记忆越多不等于体验越好,关键在于相关、可纠正、可控。
面试时可以这样说
我会先按信息的作用域设计记忆。当前会话的原始消息和任务进度放在线程内的短期状态,用 checkpointer 接续;用户明确提出且跨会话仍有价值的偏好放在按已验证用户隔离的长期 Store;订单状态和政策是外部权威资料,回答时重新查询,不把旧查询结果当永久用户记忆。写入时记录来源、时间和适用范围,避免保存敏感或纯推测信息;读取时只取与本轮相关且未失效的少量内容,遇到用户改口要更新或删除。最后做身份与订单归属校验,并用跨会话、纠错、过期和越权用例验证它确实可靠。
如果追问“已有很长的聊天历史,为什么还要提取偏好”,可以回答:完整历史有追溯价值,但每轮全量注入会占上下文、混入过时信息;提取“回答简短”这种经过确认的结构化偏好能让新会话低成本读取,同时仍可保留原始消息作为来源。若追问“长期记忆是不是向量数据库”,可以回答:不是必然。固定偏好适合按用户和键精确读取;大量非结构化经验才可能需要搜索或向量索引。选存储方式要看要记什么、怎么查,以及怎样纠错删除。LangGraph:Memory overview · LangGraph:Stores
参考资料
- LangGraph:Memory overview:短期与长期记忆的作用域、历史管理、长期写入时机。
- LangGraph:Persistence:checkpointer 与 Store 的职责边界。
- LangGraph:Stores:命名空间和存取接口。
- LangChain:Short-term memory:消息历史的裁剪、删除与总结。
- LangChain:Long-term memory:跨 thread 长期记忆的使用示例。
- LangChain:Retrieval:在查询时取回外部资料。