
不知道你有没有遇到过这样的场景你兴冲冲地给 AI Agent 接上了工具调用它也能查数据库、调 API 了但当你问它刚才那个结果再帮我分析一下时它一脸茫然地看着你——因为它根本不记得刚才发生了什么。大模型是无状态的。这句话听起来很学术说白了就是每次调用 API模型都像刚睡醒一样对之前的对话一无所知。想让 Agent 真正干活Memory 管理是你绕不过去的一道坎。今天我们就来聊聊怎么给 Agent 装上记忆。为什么 Memory 如此重要先看一个公式Agent LLM Harness(Tool RAG Memory ...)Tool 让模型不只是动嘴还能动手RAG 让模型能查到外部知识而Memory 是串联这一切的底座。想象一下用户问帮我查下深圳今天的天气Agent 调了工具返回结果用户接着问那适合出门跑步吗如果 Agent 不记得上一轮查到的天气数据它要么再查一次要么直接瞎编你会发现没有 Memory 的 Agent本质上就是一个高级一点的单轮问答机器人。更实际的问题是上下文窗口有限——200K 的窗口听起来很大但多轮 Tool 调用下来几个大 JSON 就能把它塞爆成本随对话膨胀——每轮都把全部历史发给模型Token 消耗线性增长服务重启即失忆——内存里存的东西进程一挂全没了这三个问题对应了 Memory 管理的三个核心维度存储、截断/压缩、持久化。从 InMemory 开始理解 Memory 的基本玩法先看最基础的实现用 LangChain 的InMemoryChatMessageHistoryimport { ChatOpenAI } from langchain/openai; import { InMemoryChatMessageHistory } from langchain/core/chat_history; import { HumanMessage, SystemMessage } from langchain/core/messages; const model new ChatOpenAI({ modelName: process.env.MODEL_NAME, apiKey: process.env.OPENAI_API_KEY, temperature: 0, configuration: { baseURL: process.env.OPENAI_BASE_URL, } }); async function inMemoryDemo() { // 数组的升华——内存记忆实例 const history new InMemoryChatMessageHistory(); const systemMessage new SystemMessage( 你是一个友好幽默的做菜助手喜欢分享美食和烹饪技巧 ); console.log([第一轮对话]); const userMessage1 new HumanMessage(你今天吃的什么); await history.addMessage(userMessage1); // 手动维护记忆 const messages1 [systemMessage, ...(await history.getMessages())]; const response1 await model.invoke(messages1); console.log(助手${response1.content}\n); await history.addMessage(response1); // 把回复也存入记忆 console.log([第二轮对话 基于历史记录]); const userMessage2 new HumanMessage(好吃吗); await history.addMessage(userMessage2); const message2 [systemMessage, ...(await history.getMessages())]; const response2 await model.invoke(message2); await history.addMessage(response2); console.log(助手${response2.content}\n); // 查看所有记忆 const allMessages await history.getMessages(); console.log(共保存了${allMessages.length}条对话); }这段代码的核心逻辑其实就三步存history.addMessage()把用户输入和模型回复都塞进记忆取history.getMessages()拿出完整历史拼上 SystemMessage 发给模型再存模型返回后再次存入形成闭环你会发现这本质上是把一个普通数组包装成了有语义的记忆对象。HumanMessage、AIMessage、ToolMessage各有各的type属性后面做精细化处理时非常有用。InMemory 内部解析与应用场景InMemoryChatMessageHistory的内部就是一个普通的 JavaScript 数组每次addMessage就是 pushgetMessages返回整个数组的引用注意这里返回的是引用如果直接修改返回的数组会污染原始记忆正确做法还是使用addMessage。它没有序列化、没有并发控制、没有 session 隔离每个实例只属于当前会话。应用场景原型验证快速跑通流程不用考虑持久化和多用户短会话测试单轮或少量轮次的 Agent 调试无状态服务的一次性任务比如一个临时生成代码的 Agent用完即弃它的优点是零配置、零依赖、速度极快缺点是进程一挂记忆全无而且如果多个用户共用一个实例数据会互相污染。持久化让记忆活过进程重启实际生产环境中服务重启是家常便饭。这时候FileSystemChatMessageHistory就派上用场了import { FileSystemChatMessageHistory } from langchain/community/stores/message/file_system; import path from node:path; async function fileHistoryDemo() { const filePath path.join(process.cwd(), chat_history.json); const sessionId user_session_001; // 多用户隔离的关键 const history new FileSystemChatMessageHistory({ filePath, sessionId }); // 对话过程中自动写入文件... await history.addMessage(userMessage); const response await model.invoke(messages); await history.addMessage(response); // 下次启动时从文件恢复 const restoredHistory new FileSystemChatMessageHistory({ filePath, sessionId }); const restoredMessages await restoredHistory.getMessages(); console.log(从文件中恢复了${restoredMessages.length}条历史信息); }这里有个细节值得注意sessionId的存在意味着这套机制天然支持多用户。不同用户的对话存在同一个 JSON 文件里但通过sessionId隔离互不干扰。FileSystem 内部解析与应用场景FileSystemChatMessageHistory会把消息序列化为 JSON 格式写入磁盘。它的内部实现大致是每次addMessage时读取对应sessionId的消息数组追加新消息然后整体写回文件有些实现会做增量写入优化。文件结构通常是一个大 JSON 对象key 是 sessionIdvalue 是消息数组。例如{ user_session_001: [ { type: human, content: 红烧肉怎么做 }, { type: ai, content: 红烧肉的做法是... } ] }应用场景单机部署的小型应用个人助手、本地调试工具原型阶段需要跨重启保留上下文比如开发一个 CLI Agent希望重启后还能继续上次对话不需要高并发、低访问量的场景文件读写有 I/O 开销不适合高并发它解决了持久化问题但仍有局限文件会随着对话增长而越来越大加载整个文件会拖慢响应并发写入同一个文件可能导致数据错乱多个服务实例共享同一份文件不现实。方案优点缺点适用场景InMemory零成本、速度快、无依赖进程重启丢失、不可共享、无 session 隔离Demo、单轮任务、无状态服务FileSystem简单持久化、可调试、支持 sessionId不适合高并发、文件会膨胀、多实例难共享单机小应用、原型验证、个人工具数据库/向量库可扩展、可检索、多实例共享、支持复杂查询需要额外基础设施生产级多用户 Agent、需要长期记忆检索选型建议原型验证用 InMemory单机小项目用 FileSystem生产级多实例部署必须上 Redis/PostgreSQL/向量数据库。Memory 管理三策略截断、总结、检索存储解决了记在哪但怎么记才是 Memory 管理的灵魂。业界主流方案就三种1. 截断最简单也最有效// 只保留最近 4 条消息 const recentMessages allMessages.slice(-4);你没看错就是这么简单粗暴。但别小看它——对于大多数短平快的对话场景记不住太久的反而是好事。用户问刚才说的那个最近 4 条足够理解了用户问三天前我说的大概率也不需要 Agent 记得。2. 总结用模型压缩历史当对话变长截断会丢失重要早期信息。这时候可以用 LLM 自己来做摘要// 伪代码定期触发总结 if (history.length 20) { const summary await model.invoke([ new SystemMessage(请总结以下对话的要点保留关键事实和用户偏好), ...history ]); // 用 summary 替换旧的历史 history [summaryMessage, ...recentMessages.slice(-6)]; }这就像你开会做笔记不需要记住每一句话但要点必须留下。总结的粒度可以按需调整可以每 N 轮总结一次也可以按 Token 阈值触发。3. 检索只拿相关的这是和 RAG 思路最接近的方式。把历史消息向量化存入向量数据库每轮对话时只检索和当前 query 最相关的历史片段// 伪代码 const relevantHistory await vectorStore.similaritySearch( currentQuery, { k: 5, filter: { sessionId } } ); const messages [systemMessage, ...relevantHistory, currentQuery];这种方式最灵活但实现成本也最高。适合长期记忆场景比如用户一个月前告诉 Agent 自己喜欢吃什么今天问今晚做什么菜Agent 能想起这个偏好。实战踩坑这些细节你必须知道坑一ToolMessage 也是 Memory 的一部分很多人只存HumanMessage和AIMessage忘了ToolMessage。结果就是Agent 查了天气返回 25 度下一轮用户问适合穿什么Agent 完全不记得自己查过天气又去调一次工具。正确做法ReAct 循环中每一步都该存入 Memory包括工具调用和工具返回。坑二getMessages()返回的是引用还是副本不同实现的getMessages()行为可能不同。如果你拿到数组后直接 push可能意外修改了 Memory 内部状态。安全起见始终用addMessage()方法写入不要直接操作返回的数组。坑三温度参数和 Memory 的微妙关系示例代码中temperature: 0不是随便设的。有 Memory 的对话场景下模型如果随机发挥记忆链会越来越歪。做 Agent 开发temperature 建议控制在 0~0.3稳定优先。坑四文件记忆的并发问题如果你使用FileSystemChatMessageHistory并且多个请求同时写入同一个 sessionId 的文件就可能出现数据覆盖。简单解决方案为每个用户session创建独立的文件或者加锁。更好的办法是使用数据库存储。总结一句话记住 Memory 的本质Memory 管理的核心不是存多少而是在该想起来的时候想起来该忘的时候忘得掉。回顾一下InMemory进程内数组适合 Demo 和短会话FileSystemJSON 文件持久化适合单机小项目注意 sessionId 隔离数据库/向量库生产环境的选择配合截断/总结/检索策略三个策略不是互斥的实际项目往往是组合拳用截断保证最近几轮精准用总结压缩中期信息用检索打捞长期偏好。最后留个问题给你如果你的 Agent 需要记住用户是素食主义者这种长期偏好但对话已经过去了 50 轮截断和总结都保不住它你会怎么设计评论区聊聊你的思路。