Agent = LLM + Harness (Tool + RAG + Memory + ...)
前言
在 AI Agent 开发中,我们经常遇到一个问题:大模型是无状态的。每次调用都是一次独立的请求,模型不会记住之前的对话内容。
那么问题来了:
- 如何让 Agent 记住用户之前说过什么?
- 如何在多轮对话中保持上下文连贯?
- 如何控制记忆的存储和清理?
这就是 Memory 管理 要解决的问题。
为什么需要 Memory?
大模型的无状态特性
javascript
// 第一次调用
const response1 = await model.invoke([new HumanMessage("我叫张三")]);
// 模型回答:你好张三!
// 第二次调用
const response2 = await model.invoke([new HumanMessage("我叫什么?")]);
// 模型回答:我不知道你叫什么名字...
模型不会自动记住之前的对话,每次调用都是"失忆"状态。
Memory 的作用
Memory 就是给 Agent 配一个"记忆系统":
- 临时记忆:当前会话的对话历史
- 长期记忆:持久化存储,跨会话保留
Memory 存储方案
1. 内存存储(InMemoryChatMessageHistory)
最简单的方案,数据存在内存中,程序退出就丢失。
javascript
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { HumanMessage, AIMessage, SystemMessage } from '@langchain/core/messages';
const history = new InMemoryChatMessageHistory();
const systemMessage = new SystemMessage("你是一个友好的助手");
// 第一轮对话
const userMessage1 = new HumanMessage("我叫张三");
await history.addMessage(userMessage1);
// 组装上下文:系统消息 + 历史消息
const messages1 = [systemMessage, ...(await history.getMessages())];
const response1 = await model.invoke(messages1);
await history.addMessage(response1); // 记录AI回复
// 第二轮对话(基于历史)
const userMessage2 = new HumanMessage("我叫什么?");
await history.addMessage(userMessage2);
const messages2 = [systemMessage, ...(await history.getMessages())];
const response2 = await model.invoke(messages2);
// 模型回答:你叫张三!
优点 :简单、快速 缺点:程序退出数据丢失
2. 文件存储(FileSystemChatMessageHistory)
数据持久化到 JSON 文件,程序重启后可恢复。
javascript
import { FileSystemChatMessageHistory } from '@langchain/community/stores/message/file_system';
import path from 'node:path';
const filePath = path.join(process.cwd(), "chat_history.json");
const sessionId = "user_session_001"; // 支持多用户
const history = new FileSystemChatMessageHistory({
filePath,
sessionId
});
// 添加消息
await history.addMessage(new HumanMessage("红烧肉怎么做"));
await history.addMessage(new AIMessage("红烧肉做法:..."));
// 保存到文件(自动)
// 下次启动时可直接读取
const messages = await history.getMessages(); // 恢复历史
JSON 文件格式:
json
{
"user_session_001": {
"messages": [
{ "type": "human", "data": { "content": "红烧肉怎么做" } },
{ "type": "ai", "data": { "content": "红烧肉做法:..." } }
]
}
}
优点 :持久化、支持多用户 缺点:文件大时读写慢
3. 向量数据库(Milvus/Pinecone)
适合大规模、需要语义检索的场景。
javascript
// 伪代码示例
const vectorStore = new Milvus(collectionName);
await vectorStore.addDocuments([message]);
// 语义检索
const relevantMessages = await vectorStore.similaritySearch("红烧肉", 5);
优点 :支持语义检索、可扩展 缺点:复杂、需要额外基础设施
Memory 管理策略
随着对话增长,Memory 会越来越大,需要管理策略。
1. 消息数量截断(slice)
最简单的截断方式,保留最近 N 条消息。
javascript
const maxMessages = 4;
const allMessages = await history.getMessages();
const trimmedMessages = allMessages.slice(-maxMessages); // 只保留最后4条
示例:
- 原始:8条消息(4轮对话)
- 截断后:4条消息(最近2轮)
- 丢弃:前4条(最早2轮)
优点 :简单、快速 缺点:不考虑消息长度,可能超 token 限制
2. Token 数量截断(trimMessages)
精确控制 token 开销,适合生产环境。
javascript
import { trimMessages } from '@langchain/core/messages';
import { getEncoding } from 'js-tiktoken';
const enc = getEncoding("cl100k_base"); // OpenAI tokenizer
// 计算 token 数量
function countTokens(messages, encoding) {
let total = 0;
for (const msg of messages) {
const content = typeof msg === 'string' ? msg : JSON.stringify(msg.content);
total += encoding.encode(content).length;
}
return total;
}
// 截断消息
const trimmedMessages = await trimMessages(allMessages, {
maxTokens: 100, // token 上限
tokenCounter: async (msgs) => countTokens(msgs, enc),
strategy: 'last', // 保留最新消息
});
trimMessages 配置:
| 参数 | 说明 |
|---|---|
maxTokens |
token 上限 |
tokenCounter |
自定义 token 计算函数 |
strategy |
保留策略:'last'(最新)或 'first'(最早) |
内部原理:二分查找,找到最大满足条件的消息数量。
优点 :精确控制 API 开销 缺点:计算开销较大
3. 总结压缩
将旧消息总结成摘要,保留关键信息。
javascript
// 伪代码
const oldMessages = allMessages.slice(0, -4); // 旧消息
const summary = await model.invoke([
new HumanMessage(`请总结以下对话:${JSON.stringify(oldMessages)}`)
]);
// 用总结替换旧消息
const newMessages = [
new SystemMessage(`历史总结:${summary.content}`),
...allMessages.slice(-4) // 保留最近4条
];
优点 :保留关键信息、减少 token 缺点:需要额外 API 调用
截断策略对比
| 策略 | 复杂度 | 精确度 | 适用场景 |
|---|---|---|---|
| 消息数量截断 | ⭐ | ❌ | 开发/测试 |
| Token 数量截断 | ⭐⭐⭐ | ✅ | 生产环境 |
| 总结压缩 | ⭐⭐⭐⭐ | ✅ | 长对话场景 |
完整示例:带截断的 Memory 管理
javascript
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { HumanMessage, AIMessage, SystemMessage, trimMessages } from '@langchain/core/messages';
import { getEncoding } from 'js-tiktoken';
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_BASE_URL }
});
const enc = getEncoding("cl100k_base");
function countTokens(messages) {
let total = 0;
for (const msg of messages) {
total += enc.encode(msg.content).length;
}
return total;
}
async function chatWithMemory() {
const history = new InMemoryChatMessageHistory();
const systemMessage = new SystemMessage("你是一个友好的助手");
const maxTokens = 100;
// 模拟多轮对话
const conversations = [
"我叫张三",
"我是一名设计师",
"我喜欢艺术和音乐",
"我擅长 UI/UX 设计",
"我最近在做什么项目?"
];
for (const userMsg of conversations) {
// 添加用户消息
await history.addMessage(new HumanMessage(userMsg));
// 获取所有消息并截断
let allMessages = await history.getMessages();
const trimmedMessages = await trimMessages(allMessages, {
maxTokens,
tokenCounter: async (msgs) => countTokens(msgs),
strategy: 'last',
});
// 组装上下文
const messages = [systemMessage, ...trimmedMessages];
// 调用模型
const response = await model.invoke(messages);
await history.addMessage(response);
console.log(`用户:${userMsg}`);
console.log(`助手:${response.content}\n`);
}
}
chatWithMemory().catch(console.error);
最佳实践
1. 根据场景选择存储方案
| 场景 | 推荐方案 |
|---|---|
| 开发/测试 | InMemory |
| 小型应用 | FileSystem |
| 生产环境 | 向量数据库 |
2. 截断策略选择
- 开发阶段:消息数量截断,简单快速
- 上线前:Token 数量截断,精确控制
- 长对话:总结压缩,保留关键信息
3. 多用户支持
javascript
// 使用 sessionId 区分用户
const history = new FileSystemChatMessageHistory({
filePath: "chat_history.json",
sessionId: `user_${userId}` // 动态用户ID
});
总结
Memory 管理是 Agent 开发的核心环节:
- 存储:内存 → 文件 → 向量数据库,按需选择
- 截断:消息数量 → Token 数量 → 总结压缩,逐步精确
- 持久化:文件存储支持跨会话,向量数据库支持语义检索
掌握 Memory 管理,让你的 Agent 从"金鱼记忆"升级为"长期记忆"。
💡 思考:你的 Agent 项目用的是哪种 Memory 方案?遇到了什么问题?欢迎评论区交流!
相关资源: