Agent记忆模块学习笔记
学习日期:2026年3月16日
学习目标:深入理解Agent记忆模块的核心原理、底层架构、实现方式及工程化解决方案,掌握RAG与记忆模块的协同机制、对话记忆的管理策略,能够结合LangChain框架实现基础的记忆功能开发,解决实际应用中记忆模块面临的Token消耗、上下文窗口限制等问题,为后续Agent系统的搭建与优化奠定基础。
核心结论:Agent的记忆模块是实现多轮对话连贯性、提升大模型响应精准度的核心基石,其本质是通过对对话历史、上下文信息的存储、管理与复用,弥补大模型“无状态”的先天缺陷。结合RAG技术、记忆截断策略、持久化存储方案,可在控制成本的前提下,实现记忆模块的高效、稳定运行,让Agent具备“持续学习”和“精准响应”的能力。
一、前言:为什么Agent离不开记忆模块?
在接触Agent开发的过程中,我们首先会发现一个核心问题:大语言模型(LLM)本身是“无状态”(Stateless)的。这意味着,大模型每次接收请求时,都无法记住上一轮的对话内容,每一次交互都是一个独立的请求-响应过程,就像一个“健忘的专家”——虽然具备强大的推理、生成能力,但无法积累对话中的信息,导致多轮对话缺乏连贯性,无法完成复杂的任务。
例如,当我们向大模型询问“红烧肉怎么做”,得到回复后,再追问“需要哪些食材”,如果没有记忆模块,大模型会无法关联上一轮的“红烧肉”话题,可能会给出通用的食材建议,而非针对红烧肉的精准回复。而Agent的核心价值之一,就是具备“上下文感知能力”,能够记住用户的需求、对话的历史,甚至用户的偏好,从而提供更具个性化、连贯性的服务——这一切,都依赖于记忆模块(Memory)的支撑。
同时,在Agent的整体架构中,记忆模块并非孤立存在,而是与大模型(LLM)、工具(Tool)、检索增强生成(RAG)共同构成了Agent的核心能力闭环:LLM负责核心的推理与生成,Tool负责执行具体的操作(如调用API、查询数据),RAG负责提供精准的知识库支撑,而Memory则负责串联所有环节,存储对话历史、工具调用记录、知识库检索结果,为LLM的推理提供持续的上下文支撑。
此外,从成本角度来看,记忆模块也是降低Agent开发与运行成本的关键。大模型的微调(Fine-tune)虽然也能提升模型的特定能力,但存在花费巨大、操作复杂、迭代周期长等问题,对于大多数开发者而言,门槛极高;而通过记忆模块结合RAG技术,仅需通过嵌入(Embedding)技术将上下文信息转化为向量,再通过余弦相似度(Cosine Similarity)进行精准匹配,就能以极低的成本丰富大模型的上下文,提升响应的精准度,无需对大模型本身进行修改。
因此,学习Agent的记忆模块,不仅是理解Agent工作原理的关键,更是实现高效、低成本Agent开发的核心前提。本次学习将围绕记忆模块的核心概念、底层实现、常见问题及解决方案、代码实践等方面,进行全面、深入的梳理与总结。
二、核心概念解析:Agent记忆模块的基础认知
2.1 记忆模块的核心定义
Agent的记忆模块,本质上是一个“上下文信息管理系统”,其核心功能是:存储对话过程中的用户输入、大模型响应、工具调用记录、知识库检索结果等信息,在后续的对话或任务执行中,能够快速提取、复用这些信息,为大模型的推理提供上下文支撑,让无状态的大模型具备“有状态”的对话能力。
从本质上来说,记忆模块可以类比为人类的“短期记忆”与“长期记忆”的结合:短期记忆用于存储当前对话的近期信息,确保多轮对话的连贯性;长期记忆用于存储跨越多个会话的关键信息,实现Agent对用户偏好、历史任务的长期记忆,让Agent能够“越来越懂用户”。
结合LangChain框架的实践来看,记忆模块的核心载体是“消息数组(messages数组)”——这是最基础、最核心的记忆形式,所有的对话信息、工具调用信息、系统提示信息,都会以消息对象的形式存储在messages数组中,大模型每次推理时,都会读取该数组中的信息,从而实现上下文感知。
2.2 记忆模块与LLM、Tool、RAG的协同关系
在Agent的整体架构中,记忆模块是连接LLM、Tool、RAG的核心枢纽,四者的协同关系可以总结为:LLM + Tool(干活) + RAG(知识库Context) + Memory(记忆) ,具体协同逻辑如下:
- LLM:核心推理与生成单元,接收来自记忆模块的上下文信息(messages数组)、RAG提供的知识库信息,结合Tool的调用结果,生成最终的响应或工具调用指令;
- Tool:执行具体的外部操作(如查询文件、调用API、执行代码),其调用逻辑依赖于记忆模块中的对话历史(用户需求),调用结果也会被存储到记忆模块中,供LLM后续推理使用;
- RAG:检索增强生成技术,通过嵌入(Embedding)将知识库信息转化为向量,结合余弦相似度(Cosine Similarity)检索与当前对话相关的信息,补充到记忆模块的上下文的中,提升LLM响应的精准度,其核心作用是“丰富上下文”,与记忆模块的“存储上下文”形成互补;
- Memory:存储所有相关信息,包括用户输入(HumanMessage)、大模型响应(AIMessage)、工具调用记录(ToolMessage)、系统提示(SystemMessage)、RAG检索结果等,为LLM、Tool、RAG提供统一的信息支撑,确保整个Agent系统的连贯性与一致性。
需要特别注意的是,RAG与记忆模块虽然都能为LLM提供上下文支撑,但二者的定位存在本质区别:RAG主要负责“外部知识库的检索与补充”,解决的是大模型“知识有限”的问题;而记忆模块主要负责“对话历史的存储与复用”,解决的是大模型“无状态”的问题。二者协同作用,才能让Agent既具备丰富的知识储备,又具备连贯的对话能力。
2.3 大模型的“无状态”特性与记忆模块的必要性
要深刻理解记忆模块的价值,首先需要明确大模型的“无状态”(Stateless)特性——这是记忆模块存在的核心前提。
大模型的无状态,具体表现为:每次接收请求时,仅能基于当前输入的Prompt进行推理与生成,无法获取上一轮或之前的对话信息,就像HTTP协议一样——HTTP协议本身也是无状态的,虽然通过Cookie、Authorization等头部信息可以实现状态保持,但协议本身并不具备记忆能力。大模型的无状态特性,主要是为了降低推理成本、提升并发处理能力:无状态的大模型可以快速处理每个独立请求,无需维护复杂的会话状态,消耗的算力、电力以及高并发基础设施成本更低。
但这种无状态特性,也带来了明显的缺陷:无法实现多轮对话的连贯性,无法完成需要依赖历史信息的复杂任务(如多步骤的代码开发、个性化咨询、长期任务跟踪等)。而记忆模块的核心作用,就是“为无状态的大模型添加状态”——通过将对话历史存储在messages数组中,每次请求时,将messages数组作为Prompt的一部分传入大模型,让大模型能够“记住”之前的对话内容,从而实现多轮对话的连贯性。
举个简单的例子:如果没有记忆模块,当用户连续询问“什么是Agent”“Agent的核心组件有哪些”时,大模型会将两个问题视为独立的请求,在回答第二个问题时,不会关联第一个问题的上下文;而有了记忆模块后,第二个问题的Prompt会包含第一个问题的对话历史,大模型会明确用户是在了解Agent的基础上,进一步询问核心组件,从而给出更连贯、更精准的回复。
2.4 记忆模块的核心分类(基于LangChain实践)
结合LangChain框架的实践,以及上传文件中的核心内容,Agent的记忆模块主要可以分为两类,二者相辅相成,共同实现Agent的完整记忆功能:
2.4.1 会话记忆(Session Memory)
会话记忆主要用于存储单个会话内的对话历史,核心载体是messages数组,适用于单轮或多轮连续对话场景。其特点是:会话结束后,记忆可以被清空或保留,主要用于支撑当前会话的连贯性,是记忆模块的基础形式。
上传文件中的messages数组,就是会话记忆的核心实现——通过将HumanMessage、AIMessage、SystemMessage、ToolMessage等消息对象存入数组,大模型每次推理时,读取数组中的所有消息,就能获取当前会话的完整上下文。例如,在modelWithTools的实现中,就是通过将SystemMessage(角色定义)、HumanMessage(用户需求)、ToolMessage(工具调用结果)存入messages数组,让无状态的大模型能够完成多轮对话的复杂任务。
2.4.2 持久化记忆(Persistent Memory)
持久化记忆主要用于存储跨越多个会话的对话历史,核心是将会话记忆持久化到文件、数据库等存储介质中,适用于需要长期记忆用户偏好、历史任务的场景(如个性化助手、长期项目跟踪等)。其特点是:会话结束后,记忆不会丢失,后续会话可以通过检索、恢复的方式,复用之前的对话历史,让Agent能够“记住”跨越多个会话的信息。
上传文件中的FileSystemChatMessageHistory,就是持久化记忆的典型实现——通过将对话历史存储到本地文件(如chat_history.json)中,实现会话记忆的持久化,支持会话的恢复与继续聊天。例如,用户在一个会话中询问了“红烧肉怎么做”,后续开启新的会话时,通过恢复之前的会话ID,就能获取之前的对话历史,继续追问“需要哪些食材”,无需重新输入上下文。
三、记忆模块的底层实现:基于LangChain的实践解析
LangChain是目前Agent开发中最常用的框架之一,其提供了丰富的记忆模块相关API,能够快速实现会话记忆、持久化记忆、记忆截断等功能。结合上传文件中的代码实例,我们从“会话记忆的实现”“持久化记忆的实现”“带工具的记忆协同”三个方面,深入解析记忆模块的底层实现逻辑。
3.1 会话记忆的底层实现:messages数组与基础消息类型
会话记忆的核心是messages数组,而数组中的每一个元素,都是LangChain框架中定义的消息对象(Message)。LangChain提供了多种消息类型,分别对应不同的场景,上传文件中主要涉及以下4种核心消息类型,其作用与使用场景如下:
3.1.1 核心消息类型解析
- SystemMessage:系统提示消息,用于定义大模型的角色、功能、行为准则,是大模型推理的“指导方针”。例如,上传文件中,将SystemMessage设置为“你是一个友好的做菜助手,喜欢分享美食和烹饪技巧”,就是为了定义大模型的角色,让其在对话中始终围绕“做菜助手”的身份进行响应。SystemMessage通常会作为messages数组的第一个元素,确保大模型在整个会话中都能遵循设定的角色。
- HumanMessage:用户消息,用于存储用户的输入内容,是大模型推理的“输入源”。例如,用户询问“红烧肉怎么做”“需要哪些食材”,都会被封装为HumanMessage对象,存入messages数组中,供大模型读取。
- AIMessage:大模型响应消息,用于存储大模型的生成结果,是对话历史的重要组成部分。例如,大模型针对“红烧肉怎么做”给出的具体步骤,会被封装为AIMessage对象,存入messages数组中,后续对话中,大模型可以通过读取该消息,关联之前的响应内容。
- ToolMessage:工具调用消息,用于存储工具调用的结果,是Agent结合工具完成复杂任务的关键。在modelWithTools的实现中,大模型会根据用户需求,判断是否需要调用工具(tool_calls),工具执行完成后,其结果会被封装为ToolMessage对象,存入messages数组中,大模型再基于该结果,生成最终的响应。
这四种消息类型的协同,构成了会话记忆的完整逻辑:SystemMessage定义角色,HumanMessage传入需求,ToolMessage补充工具执行结果,AIMessage存储响应内容,所有消息共同存入messages数组,形成完整的对话上下文。
3.1.2 会话记忆的基础操作(基于代码实例)
结合上传文件中的代码,会话记忆的基础操作主要包括“创建消息对象”“添加消息到记忆中”“读取记忆中的消息”“调用大模型进行推理”四个步骤,具体解析如下:
第一步:导入依赖包,创建大模型实例。首先需要导入LangChain相关的依赖包,包括ChatOpenAI(大模型实例)、消息类型(HumanMessage、AIMessage、SystemMessage)、记忆存储相关的类(如InMemoryChatMessageHistory)等,然后创建大模型实例,配置模型名称、API密钥、基础URL等参数。
示例代码(简化版):
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage, AIMessage, SystemMessage } from "@langchain/core/messages";
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
// 创建大模型实例
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0, // 温度为0,确保响应的确定性
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
第二步:创建记忆实例,初始化会话记忆。使用InMemoryChatMessageHistory类,创建会话记忆实例,该类用于存储会话内的对话历史(内存存储,会话结束后丢失),无需额外配置存储路径,适合短期会话场景。
示例代码:
// 创建会话记忆实例(内存存储)
const history = new InMemoryChatMessageHistory();
// 定义系统提示消息
const systemMessage = new SystemMessage("你是一个友好的做菜助手,喜欢分享美食和烹饪技巧。");
第三步:添加消息到记忆中,构建对话上下文。将用户输入(HumanMessage)、大模型响应(AIMessage)等消息,通过addMessage方法添加到记忆实例中,逐步构建完整的对话上下文。
示例代码:
// 添加用户消息到记忆中
const userMessage1 = new HumanMessage("红烧肉怎么做");
await history.addMessage(userMessage1);
// 读取记忆中的所有消息,结合系统提示,调用大模型
const messages1 = [systemMessage, ...(await history.getMessages())];
const response1 = await model.invoke(messages1);
// 将大模型响应添加到记忆中
await history.addMessage(response1);
第四步:读取记忆中的消息,实现多轮对话。在后续的对话中,通过getMessages方法读取记忆中的所有消息,结合新的用户输入,再次调用大模型,实现多轮对话的连贯性。
示例代码:
// 第二轮对话:用户追问“好吃吗?”
const userMessage2 = new HumanMessage("好吃吗?");
await history.addMessage(userMessage2);
// 读取记忆中的所有消息(包含第一轮的对话历史)
const messages2 = [systemMessage, ...(await history.getMessages())];
const response2 = await model.invoke(messages2);
await history.addMessage(response2);
通过以上四个步骤,就实现了最基础的会话记忆功能——大模型能够读取之前的对话历史,实现多轮对话的连贯性。需要注意的是,InMemoryChatMessageHistory是内存存储,当程序重启或会话结束后,记忆中的消息会丢失,适合短期会话场景;如果需要长期保存对话历史,就需要使用持久化记忆方案。
3.2 持久化记忆的底层实现:FileSystemChatMessageHistory
上传文件中重点介绍了FileSystemChatMessageHistory类的使用,该类是LangChain框架中用于实现“文件持久化记忆”的核心类,其核心作用是将对话历史存储到本地文件中,实现会话记忆的持久化,支持会话的恢复与继续聊天。结合代码实例,我们从“核心原理”“使用步骤”“关键特性”三个方面,解析其底层实现。
3.2.1 核心原理
FileSystemChatMessageHistory的核心原理是:将对话历史(messages数组中的消息对象)序列化为JSON格式,存储到指定的本地文件(如chat_history.json)中,每个会话通过唯一的会话ID(sessionId)进行区分。当需要恢复会话时,通过会话ID从文件中读取对应的对话历史,重新构建messages数组,实现会话的继续聊天;当添加新的对话消息时,会更新文件中的对应会话记录,实现记忆的持久化更新。
与InMemoryChatMessageHistory相比,FileSystemChatMessageHistory的优势在于:会话记忆不会因程序重启而丢失,支持跨会话的记忆复用;缺点在于:存储效率低于内存存储,适合对会话记忆有长期保存需求的场景(如个性化助手、长期项目跟踪等)。
3.2.2 使用步骤(基于上传文件代码解析)
结合上传文件中的代码实例,FileSystemChatMessageHistory的使用主要分为“创建持久化记忆实例”“添加对话消息”“恢复会话”三个核心步骤,具体解析如下:
第一步:导入依赖包,配置存储路径与会话ID。首先需要导入FileSystemChatMessageHistory类,以及path模块(用于配置文件存储路径),然后指定存储文件的路径(如当前工作目录下的chat_history.json),并定义唯一的会话ID(sessionId)——每个会话对应一个sessionId,用于区分不同的会话记忆。
示例代码:
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { FileSystemChatMessageHistory } from "@langchain/community/stores/message/file_system";
import { HumanMessage, AIMessage, SystemMessage } from "@langchain/core/messages";
import path from "node:path";
// 创建大模型实例(与会话记忆一致)
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
// 配置文件存储路径(当前工作目录下的chat_history.json)
const filePath = path.join(process.cwd(), "chat_history.json");
// 定义会话ID(唯一标识一个会话)
const sessionId = "user_session_001";
第二步:创建持久化记忆实例,添加对话消息。通过FileSystemChatMessageHistory类,传入filePath(存储路径)和sessionId(会话ID),创建持久化记忆实例,然后通过addMessage方法添加用户消息、大模型响应消息,消息会自动被持久化到指定文件中。
示例代码(第一轮对话):
async function fileHistoryDemo() {
// 定义系统提示消息
const systemMessage = new SystemMessage("你是一个友好的做菜助手,喜欢分享美食和烹饪技巧。");
console.log("[第一轮对话]");
// 创建持久化记忆实例
const history = new FileSystemChatMessageHistory({
filePath: filePath,
sessionId: sessionId,
});
// 添加用户消息到记忆中(自动持久化)
const userMessage1 = new HumanMessage("红烧肉怎么做");
await history.addMessage(userMessage1);
// 读取记忆中的消息,调用大模型
const messages1 = [systemMessage, ...(await history.getMessages())];
const response1 = await model.invoke(messages1);
// 将大模型响应添加到记忆中(自动更新文件)
await history.addMessage(response1);
console.log(`用户: ${userMessage1.content}`);
console.log(`助手: ${response1.content}`);
console.log(`✓ 对话已保存到文件: ${filePath}\n`);
}
第三步:恢复会话,继续聊天。当需要恢复之前的会话时,只需使用相同的filePath和sessionId,创建FileSystemChatMessageHistory实例,通过getMessages方法读取之前的对话历史,然后添加新的用户消息,继续调用大模型,实现会话的无缝衔接。
示例代码(恢复会话,第三轮对话):
async function fileHistoryDemo(){
const systemMessage = new SystemMessage("你是一个友好的做菜助手,喜欢分享美食与烹饪技巧。");
// 创建持久化记忆实例(使用相同的filePath和sessionId,恢复之前的会话)
const restoreHistory = new FileSystemChatMessageHistory({
filePath,
sessionId,
});
console.log("[第三轮对话]");
// 添加新的用户消息(追问食材)
const userMessage3 = new HumanMessage("需要哪些食材?");
await restoreHistory.addMessage(userMessage3);
// 读取记忆中的所有消息(包含前两轮的对话历史)
const messages3 = [systemMessage, ...(await restoreHistory.getMessages())];
const response3 = await model.invoke(messages3);
await restoreHistory.addMessage(response3);
console.log(`用户: ${userMessage3.content}`);
console.log(`助手: ${response3.content}`);
console.log(`对话已保存到文件: ${filePath}\n`);
}
通过以上步骤,就实现了会话记忆的持久化——无论程序是否重启,只要filePath和sessionId不变,就能恢复之前的对话历史,继续聊天。需要注意的是,不同的会话需要使用不同的sessionId,避免会话记忆混淆;同时,存储文件的路径需要确保程序有读写权限,否则会导致记忆无法持久化。
3.2.3 关键特性(结合上传文件总结)
结合上传文件中的内容,FileSystemChatMessageHistory的关键特性主要包括以下4点,这些特性也是持久化记忆的核心优势:
- 会话隔离:通过sessionId区分不同的会话,每个会话的记忆独立存储,互不干扰。例如,用户可以创建“js八股”“算法”“手写代码”“AI”等不同主题的会话,每个会话对应一个sessionId,切换会话时,只需更换sessionId即可恢复对应的记忆。
- 持久化存储:对话历史被存储到本地文件中,程序重启后,记忆不会丢失,支持长期保存。例如,用户在一周前的会话中询问了“红烧肉怎么做”,一周后,通过相同的sessionId,仍然可以恢复该会话,继续追问相关问题。
- 无缝恢复:支持会话的随时恢复,只需创建相同filePath和sessionId的记忆实例,就能读取之前的对话历史,实现无缝衔接,提升用户体验。
- 简单易用:无需复杂的数据库配置,只需指定存储路径和会话ID,就能实现记忆的持久化,适合中小规模的Agent开发场景。
3.3 带工具的记忆协同:modelWithTools的实现逻辑
在Agent的实际应用中,记忆模块不仅需要存储对话历史,还需要与工具(Tool)协同,存储工具调用记录,支撑大模型完成复杂的多步骤任务。上传文件中提到的modelWithTools,就是记忆模块与工具协同的核心实现,其核心逻辑是:通过messages数组存储系统提示、用户需求、工具调用结果,让无状态的大模型能够基于记忆中的工具调用记录,完成多轮工具调用与推理。
3.3.1 核心协同逻辑
modelWithTools的核心协同逻辑可以总结为以下5个步骤,形成完整的闭环:
- 初始化messages数组,添加SystemMessage:定义大模型的角色、功能,以及工具调用的规则(如“如何判断是否需要调用工具”“如何处理工具调用结果”),让大模型明确自身的职责与工具调用逻辑。
- 添加HumanMessage:将用户的需求(如“查询今天的天气”“生成一份报表”)封装为HumanMessage,存入messages数组中,作为大模型推理的输入。
- 大模型推理,判断是否需要调用工具:大模型读取messages数组中的上下文信息(系统提示、用户需求),通过智能循环判断,确定是否需要调用工具(tool_calls)。如果需要调用工具,生成工具调用指令;如果不需要,直接生成响应。
- 执行工具调用,添加ToolMessage:工具执行完成后,将执行结果封装为ToolMessage,存入messages数组中,补充上下文信息。
- 大模型再次推理,生成最终响应:大模型读取messages数组中的所有信息(系统提示、用户需求、工具调用结果),结合自身的推理能力,生成最终的响应,完成整个任务。
需要特别注意的是,整个过程中,messages数组是核心载体——所有的信息(系统提示、用户需求、工具调用结果、大模型响应)都被存储在数组中,大模型每次推理时,都能读取完整的上下文,从而实现工具调用与对话的连贯性。这种协同方式,让无状态的大模型能够完成需要多轮工具调用的复杂任务,体现了记忆模块的核心价值。
3.3.2 关键注意点
结合上传文件中的内容,modelWithTools的实现过程中,有两个关键注意点,直接影响记忆模块与工具协同的效果:
- SystemMessage的配置:SystemMessage需要明确大模型的角色、工具调用的规则,以及如何处理ToolMessage中的工具调用结果。如果SystemMessage配置不清晰,大模型可能无法正确判断是否需要调用工具,或者无法正确处理工具调用结果,导致协同失败。
- ToolMessage的及时添加:工具调用完成后,必须将结果及时封装为ToolMessage,存入messages数组中,否则大模型无法获取工具调用结果,无法进行后续的推理。同时,ToolMessage的格式需要规范,确保大模型能够正确解析其中的信息。
例如,当用户需求为“生成一份2024年的销售报表”时,大模型会通过messages数组中的上下文,判断需要调用“读取销售数据”的工具,工具执行完成后,将销售数据封装为ToolMessage存入messages数组,大模型再基于该数据,生成销售报表,完成整个任务——这一过程,完全依赖于记忆模块对上下文信息的存储与复用。
四、记忆模块的核心问题与解决方案
在记忆模块的实际应用中,最核心的问题是:随着对话的不断进行,messages数组中的消息会越来越多,导致上下文(Context)越来越长,进而引发两个关键问题:一是Token消耗越来越多,增加运行成本;二是上下文长度超出大模型的上下文窗口大小,导致大模型无法处理,出现报错或响应异常。
上传文件中重点介绍了针对这一问题的多种解决方案,结合LangChain的实践,我们将这些方案分为“基础解决方案”“进阶解决方案”“手动/自动优化方案”三类,详细解析其原理、实现方式及适用场景。
4.1 基础解决方案:滑动窗口截断(LRU策略)
4.1.1 核心原理
滑动窗口截断是最基础、最常用的记忆优化方案,其核心原理是:基于LRU(最近最少使用)策略,只保留最近的N轮对话消息,删除早期的对话消息,从而控制messages数组的长度,避免上下文过长。这种方案的核心思想是:用户在当前对话中,最关心的是最近的对话内容,早期的对话内容对当前推理的影响较小,因此可以被截断,以节省Token消耗,避免超出上下文窗口。
例如,上传文件中提到的“slice(-3)”,就是滑动窗口截断的简单实现——只保留最近的3轮对话消息,删除之前的所有消息,确保messages数组的长度始终控制在3轮以内,从而控制Token消耗和上下文长度。
4.1.2 实现方式(基于上传文件代码解析)
结合上传文件中的messageCountTruncation函数,滑动窗口截断的实现主要分为“存储所有对话消息”“截断消息数组”“使用截断后的消息进行推理”三个步骤,具体代码解析如下:
import{
InMemoryChatMessageHistory
} from '@langchain/core/chat_history';
import{
HumanMessage,
AIMessage,
SystemMessage
} from "@langchain/core/messages";
async function messageCountTruncation(){
// 创建会话记忆实例
const history = new InMemoryChatMessageHistory();
// 定义最大保留消息数量(滑动窗口大小)
const maxMessages = 4;
// 模拟多轮对话消息(10条消息,5轮对话)
const messages = [
{ type: 'human', content: '我叫张三' },
{ type: 'ai', content: '你好张三,很高兴认识你!' },
{ type: 'human', content: '我今年25岁' },
{ type: 'ai', content: '25岁正是青春年华,有什么我可以帮助你的吗?' },
{ type: 'human', content: '我喜欢编程' },
{ type: 'ai', content: '编程很有趣!你主要用什么语言?' },
{ type: 'human', content: '我住在北京' },
{ type: 'ai', content: '北京是个很棒的城市!' },
{ type: 'human', content: '我的职业是软件工程师' },
{ type: 'ai', content: '软件工程师是个很有前景的职业!' },
];
// 将所有消息添加到记忆中
for (const msg of messages){
if(msg.type === 'human'){
await history.addMessage(new HumanMessage(msg.content))
}else{
await history.addMessage(new AIMessage(msg.content))
}
}
// 读取所有消息,进行截断(保留最近的4条消息,即最近2轮对话)
let allMessages = await history.getMessages();
console.log('所有消息:',allMessages);
const trimmedMessages = allMessages.slice(-maxMessages); // 滑动窗口截断,保留最后4条
// 输出截断后的结果
console.log(trimmedMessages.length,'保留消息数量');
console.log("保留消息:",trimmedMessages.map(m =>`
${m.constructor.name}: ${m.content}
`).join('\n\n'));
}
// 执行截断函数
async function runAll(){
await messageCountTruncation();
}
runAll().catch(console.error);
代码解析:
- 首先,创建InMemoryChatMessageHistory实例,用于存储所有对话消息;
- 定义maxMessages(最大保留消息数量)为4,即滑动窗口大小为4条消息(2轮对话,每轮包含1条用户消息和1条大模型响应消息);
- 模拟10条对话消息(5轮对话),将其添加到记忆实例中;
- 通过slice(-maxMessages)方法,截断消息数组,只保留最近的4条消息;
- 输出截断后的消息,用于后续的大模型推理。
这种实现方式简单、高效,无需复杂的逻辑,适合大多数简单的会话场景。需要注意的是,滑动窗口的大小(maxMessages)需要根据大模型的上下文窗口大小、Token消耗预算进行调整,一般建议保留最近3-5轮对话,既能保证对话的连贯性,又能有效控制Token消耗。
4.1.3 适用场景与优缺点
适用场景:适用于对话内容关联性不强、用户只关心最近对话的场景,如简单的咨询、问答、临时任务等。
优点:实现简单、高效,无需额外的存储或计算成本,能够快速控制上下文长度和Token消耗。
缺点:会丢失早期的对话消息,如果早期对话中包含关键信息(如用户偏好、任务背景),会导致大模型“遗忘”这些信息,影响响应的精准度;同时,无法适用于需要长期记忆的场景。
4.2 进阶解决方案:消息总结(Summarize)
4.2.3 核心原理
消息总结是对滑动窗口截断的优化与补充,其核心原理是:当对话消息过多,需要截断时,不直接删除早期的对话消息,而是对早期的对话消息进行总结,将总结后的内容保留在messages数组中,同时保留最近的N轮对话消息。这样既能控制上下文长度和Token消耗,又能保留早期对话的核心信息,避免大模型“遗忘”关键内容。
例如,当对话进行了10轮,需要控制上下文长度时,可以对前7轮对话进行总结,生成一段简洁的总结文本(如“用户张三,25岁,软件工程师,住在北京,喜欢编程,询问了编程相关的基础问题”),然后将总结文本与最近的3轮对话消息一起存入messages数组中,既减少了Token消耗,又保留了早期对话的核心信息。
消息总结的核心优势在于:在控制Token消耗的同时,最大限度地保留对话的核心信息,确保大模型能够基于完整的对话背景进行推理,避免因截断导致的信息丢失。
4.2.2 实现方式(结合LangChain实践)
结合LangChain框架的ConversationSummaryMemory类(上传文件中未直接给出代码,但属于核心延伸内容),消息总结的实现主要分为“创建总结记忆实例”“添加对话消息”“自动生成总结”三个步骤,具体实现代码如下(基于上传文件的代码风格扩展):
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { ConversationSummaryMemory } from "langchain/memory";
import { HumanMessage, AIMessage, 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,
},
});
// 创建总结记忆实例,自动对早期消息进行总结
const summaryMemory = new ConversationSummaryMemory({
llm: model, // 用于生成总结的大模型
memoryKey: "chat_summary", // 总结内容的存储键名
returnMessages: true, // 返回消息对象,便于加入messages数组
});
// 模拟多轮对话,添加消息并自动生成总结
async function summaryDemo() {
const systemMessage = new SystemMessage("你是一个友好的助手,负责与用户进行对话并记住关键信息。");
// 第一轮对话
const userMessage1 = new HumanMessage("我叫张三");
const aiMessage1 = new AIMessage("你好张三,很高兴认识你!");
await summaryMemory.saveContext({ input: userMessage1.content }, { output: aiMessage1.content });
// 第二轮对话
const userMessage2 = new HumanMessage("我今年25岁,是一名软件工程师");
const aiMessage2 = new AIMessage("25岁正是奋斗的年纪,软件工程师是个很有前景的职业!");
await summaryMemory.saveContext({ input: userMessage2.content }, { output: aiMessage2.content });
// 第三轮对话
const userMessage3 = new HumanMessage("我住在北京,喜欢编程");
const aiMessage3 = new AIMessage("北京是个很棒的城市,编程也是一项很有趣的技能!");
await summaryMemory.saveContext({ input: userMessage3.content }, { output: aiMessage3.content });
// 第四轮对话
const userMessage4 = new HumanMessage("我想学习Python编程,有什么建议吗?");
// 读取总结内容和最近的对话消息
const summary = await summaryMemory.loadMemoryVariables({});
const messages = [
systemMessage,
...summary.chat_summary, // 总结内容
userMessage4 // 当前用户消息
];
// 调用大模型,生成响应
const response4 = await model.invoke(messages);
console.log(`用户: ${userMessage4.content}`);
console.log(`助手: ${response4.content}`);
}
summaryDemo().catch(console.error);
代码解析:
- 创建ConversationSummaryMemory实例,传入大模型(用于生成总结)、记忆键名(chat_summary)、returnMessages(返回消息对象)等参数;
- 每轮对话结束后,通过saveContext方法,将用户消息和大模型响应存入记忆中,记忆模块会自动对早期的消息进行总结;
- 当需要调用大模型时,通过loadMemoryVariables方法读取总结内容,将总结内容与当前用户消息一起传入大模型,确保大模型能够获取完整的对话背景;
- 大模型基于总结内容和当前用户消息,生成精准的响应,既控制了Token消耗,又保留了早期对话的核心信息。
需要注意的是,消息总结的质量取决于大模型的推理能力,因此建议使用性能较好的大模型(如GPT-4、Claude 3)进行总结,确保总结内容能够准确反映早期对话的核心信息;同时,总结的频率可以根据对话长度进行调整,避免过于频繁的总结导致Token消耗增加。
4.2.3 适用场景与优缺点
适用场景:适用于对话内容关联性较强、需要保留早期对话关键信息的场景,如个性化咨询、长期任务跟踪、复杂问题解答等。
优点:能够在控制Token消耗的同时,保留早期对话的核心信息,避免大模型“遗忘”关键内容,提升响应的精准度;适用于中长对话场景。
缺点:需要额外调用大模型生成总结,增加了少量的Token消耗;总结的质量可能受到大模型性能的影响,存在总结不精准的风险。
4.3 高级解决方案:检索式记忆(结合RAG)
4.3.1 核心原理
检索式记忆是结合RAG技术的高级记忆优化方案,其核心原理是:将早期的对话消息、关键信息存储到数据库或文件中(如向量数据库、本地文件),当需要使用这些信息时,通过RAG技术(嵌入+余弦相似度检索),从存储中检索与当前对话相关的信息,补充到messages数组中,实现“按需检索”,既避免了上下文过长,又能保留所有对话信息。
这种方案的核心思想是:不将所有对话消息都存入messages数组中,而是只保留最近的N轮对话消息,早期的对话消息被存储到外部存储中,当当前对话需要用到早期信息时,通过检索的方式获取,实现“记忆的按需加载”。上传文件中提到的“cursor等超越当前对话,将之前对话存储,RAG利用的场景”,就是检索式记忆的典型应用。
检索式记忆的核心优势在于:理论上可以存储无限的对话信息,无需担心上下文窗口限制和Token消耗;同时,通过检索技术,能够快速获取与当前对话相关的信息,提升大模型响应的精准度,让Agent能够“越来越懂用户”。
4.3.2 实现方式(结合上传文件与LangChain实践)
检索式记忆的实现主要分为“存储早期对话信息”“检索相关信息”“补充到上下文”三个步骤,结合上传文件中的FileSystemChatMessageHistory和RAG技术,具体实现代码如下(扩展版):
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { FileSystemChatMessageHistory } from "@langchain/community/stores/message/file_system";
import { HumanMessage, AIMessage, SystemMessage } from "@langchain/core/messages";
import { OpenAIEmbeddings } from "@langchain/openai/embeddings";
import { Chroma } from "@langchain/community/vectorstores/chroma";
import path from "node:path";
// 创建大模型实例
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
// 配置持久化存储路径和会话ID
const filePath = path.join(process.cwd(), "chat_history.json");
const sessionId = "user_session_001";
// 创建向量存储实例(用于存储早期对话信息,支持RAG检索)
const embeddings = new OpenAIEmbeddings({ apiKey: process.env.OPENAI_API_KEY });
const vectorStore = await Chroma.fromTexts([], [], embeddings, { persistDirectory: "./chroma_db" });
await vectorStore.persist();
// 存储早期对话信息到向量数据库
async function storeEarlyMessages(messages) {
// 提取早期对话的文本内容
const earlyMessagesText = messages.map(msg => `${msg.constructor.name}: ${msg.content}`).join("\n");
// 嵌入并存储到向量数据库
await vectorStore.addDocuments([{ pageContent: earlyMessagesText }]);
}
// 检索与当前对话相关的早期信息
async function retrieveRelevantMessages(query) {
// 通过RAG检索相关信息
const relevantDocs = await vectorStore.similaritySearch(query, 1); // 检索最相关的1条信息
return relevantDocs.length > 0 ? relevantDocs[0].pageContent : "";
}
// 检索式记忆核心逻辑
async function retrievalMemoryDemo() {
const systemMessage = new SystemMessage("你是一个友好的做菜助手,喜欢分享美食和烹饪技巧,会结合历史对话信息进行响应。");
// 1. 创建持久化记忆实例,添加多轮对话消息
const history = new FileSystemChatMessageHistory({ filePath, sessionId });
// 模拟早期对话(前3轮)
const earlyMessages = [
new HumanMessage("红烧肉怎么做?"),
new AIMessage("红烧肉的做法:1. 准备五花肉、冰糖、姜片等食材;2. 五花肉焯水;3. 炒糖色;4. 炖煮入味。"),
new HumanMessage("焯水需要多久?"),
new AIMessage("焯水时间建议3-5分钟,去除血沫和多余油脂。"),
new HumanMessage("炒糖色需要注意什么?"),
new AIMessage("炒糖色时,火候要小,不停搅拌,避免炒糊,直到冰糖融化呈深褐色。")
];
// 将早期对话消息添加到记忆中,并存储到向量数据库
for (const msg of earlyMessages) {
await history.addMessage(msg);
}
await storeEarlyMessages(earlyMessages);
// 2. 截断早期消息,只保留最近的1轮对话(模拟上下文窗口限制)
let allMessages = await history.getMessages();
const recentMessages = allMessages.slice(-2); // 保留最近1轮对话(2条消息)
await history.clear(); // 清空记忆
for (const msg of recentMessages) {
await history.addMessage(msg);
}
// 3. 当前用户消息(需要用到早期对话信息)
const userMessage = new HumanMessage("红烧肉的食材有哪些?");
// 4. 检索与当前消息相关的早期信息
const relevantInfo = await retrieveRelevantMessages(userMessage.content);
// 5. 构建上下文,调用大模型
const messages = [
systemMessage,
...(await history.getMessages()), // 最近的对话消息
new HumanMessage(`参考早期对话信息:${relevantInfo}\n${userMessage.content}`) // 补充检索到的早期信息
];
const response = await model.invoke(messages);
await history.addMessage(response);
console.log(`用户: ${userMessage.content}`);
console.log(`助手: ${response.content}`);
console.log(`✓ 检索到的早期信息: ${relevantInfo}`);
}
retrievalMemoryDemo().catch(console.error);
代码解析:
- 创建向量存储实例(Chroma),用于存储早期对话信息,通过OpenAIEmbeddings将对话文本转化为向量,支持余弦相似度检索;
- 模拟早期对话消息,将其添加到持久化记忆中,并存储到向量数据库中,实现早期对话信息的持久化存储;
- 截断早期消息,只保留最近的1轮对话,控制上下文长度和Token消耗;
- 当前用户消息需要用到早期对话中的“食材信息”,通过retrieveRelevantMessages方法,检索向量数据库中与当前消息相关的早期信息;
- 将最近的对话消息、检索到的早期信息、当前用户消息一起传入大模型,生成精准的响应,实现“按需检索”的记忆功能。
需要注意的是,检索式记忆的实现需要依赖向量数据库(如Chroma、Milvus)和嵌入技术(如OpenAIEmbeddings),适合中大型Agent开发场景;同时,检索的精准度取决于嵌入模型的性能和检索策略(如相似度阈值、检索数量),需要根据实际场景进行调整。
4.3.3 适用场景与优缺点
适用场景:适用于需要长期存储对话信息、对话内容复杂、需要频繁复用早期信息的场景,如个性化助手、长期项目跟踪、知识问答系统等。
优点:可以存储无限的对话信息,无需担心上下文窗口限制和Token消耗;通过检索技术,能够快速获取相关信息,提升响应精准度;支持跨会话的信息复用,让Agent能够“越来越懂用户”。
缺点:实现复杂度较高,需要依赖向量数据库和嵌入技术;检索过程会增加一定的响应延迟(相较于滑动窗口截断、消息总结),需要优化检索策略以提升速度;同时,嵌入模型和向量数据库的使用会增加一定的开发与运维成本,对开发者的技术要求较高,不适合小型、快速迭代的Agent开发场景。
4.4 三种解决方案对比与选型建议
结合前文解析的三种记忆优化方案,我们从实现复杂度、Token消耗、信息保留度、适用场景四个核心维度,进行对比总结,为实际开发中的方案选型提供参考:
| 解决方案 | 实现复杂度 | Token消耗 | 信息保留度 | 核心适用场景 |
|---|---|---|---|---|
| 滑动窗口截断 | 低(仅需数组截断操作) | 低(仅保留最近N轮消息) | 低(丢失早期非关键信息) | 简单咨询、临时任务、短对话场景 |
| 消息总结 | 中(依赖LangChain的总结类,需配置大模型) | 中(需额外消耗Token生成总结) | 中(保留早期核心信息,丢失细节) | 个性化咨询、中长对话、需保留关键背景的场景 |
| 检索式记忆(结合RAG) | 高(需部署向量数据库、嵌入模型,设计检索策略) | 低-中(仅加载检索到的相关信息) | 高(保留所有对话信息,按需检索) | 长期项目跟踪、个性化助手、知识问答系统等复杂场景 |
选型建议:实际开发中,无需盲目追求“最先进”的方案,应结合Agent的应用场景、开发成本、性能要求综合判断。小型Agent、快速验证场景可优先选择滑动窗口截断;需要保留关键上下文、中长对话场景可选择消息总结;大型、复杂Agent、需要长期记忆用户偏好的场景,可采用检索式记忆结合RAG的方案,也可根据实际需求,将三种方案结合使用(如近期消息用滑动窗口保留,早期消息用总结或检索补充)。
五、学习总结与实践展望
本次学习围绕Agent记忆模块展开,从核心概念、底层实现、协同机制到实际问题解决方案,结合LangChain框架的代码实践,全面梳理了记忆模块的核心知识,明确了记忆模块在Agent架构中的核心价值——弥补大模型“无状态”缺陷,实现多轮对话连贯性,降低开发与运行成本。
通过学习,我们掌握了记忆模块的核心分类(会话记忆、持久化记忆),理解了messages数组作为记忆载体的核心作用,以及记忆模块与LLM、Tool、RAG的协同逻辑;同时,针对记忆模块面临的Token消耗、上下文窗口限制等核心问题,掌握了滑动窗口截断、消息总结、检索式记忆三种解决方案的原理与实现方式,能够根据实际场景选择合适的优化策略。
结合LangChain的实践来看,记忆模块的开发具有较强的实用性和可操作性:会话记忆可通过InMemoryChatMessageHistory快速实现,持久化记忆可借助FileSystemChatMessageHistory完成文件存储,记忆优化可结合ConversationSummaryMemory、向量数据库等工具实现,无需复杂的底层开发,即可快速搭建基础的记忆功能。
未来实践中,可重点关注以下几个方向:一是记忆模块与RAG、Tool的深度协同,优化检索策略和工具调用逻辑,提升Agent的响应精准度和效率;二是持久化记忆的优化,探索数据库存储(如MySQL、MongoDB)与文件存储的结合方案,提升记忆存储的效率和安全性;三是记忆优化方案的动态调整,根据对话长度、用户需求,自动切换滑动窗口、总结、检索等策略,实现成本与体验的平衡;四是复杂场景下的记忆管理,如多用户会话隔离、记忆过期清理、关键信息优先级排序等,提升Agent的实用性和稳定性。
总之,记忆模块是Agent系统的“灵魂”,其设计与实现直接决定了Agent的用户体验和核心能力。只有深入理解记忆模块的原理与实践,灵活运用各类优化方案,才能开发出高效、稳定、个性化的Agent系统,为后续的Agent开发与优化奠定坚实的基础。