计算机访问系统

智能体记忆系统:为你的 AI 提供持久化上下文


每个 AI 智能体都存在记忆问题。每次调用模型都是无状态的。模型不记得上一次的请求,不记得用户的名字、偏好或之前的决策。没有外部记忆,你的智能体在每次对话中都从零开始。

本指南涵盖四种记忆策略——从最简单到最强大——以及何时使用每种策略。所有示例均使用 TypeScript 和 Anthropic SDK。

为什么智能体需要记忆

考虑一个代码审查智能体。在第一次运行时,用户解释了团队的编码规范:不使用 any 类型,始终显式处理错误,优先使用函数式风格。智能体给出了一次很好的审查。

在第二次运行时,用户提交了另一个文件。智能体已经忘记了一切。它遗漏了昨天标记过的相同规范违规。

这是核心问题:智能体是无状态的,但有用的智能体需要连续性

记忆为智能体提供了三种能力:

  • 回忆 — 从对话早期或之前的会话中检索事实
  • 个性化 — 随着时间的推移适应用户的偏好
  • 协调 — 在多智能体系统中,在智能体之间共享状态

四种记忆类型

在编写代码之前,清晰地命名各种策略很有帮助。智能体记忆分为四种模式:

类型存储位置作用范围最适用于
缓冲区上下文中(消息数组)当前会话短对话
摘要上下文中(压缩)当前会话长对话
语义记忆外部(向量数据库)跨会话知识检索
情景记忆外部(键值存储)跨会话用户事实、偏好

每种策略在简单性、成本和能力之间都有不同的权衡。选择能解决你问题的最简方案。

策略一:对话缓冲区

最简单的策略是将完整的对话历史传递给每次模型调用。模型看到所有之前的消息,并在完整上下文中进行响应。

import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface Message {
role: "user" | "assistant";
content: string;
}
// 缓冲区保存完整的对话历史
const buffer: Message[] = [];
async function chat(userMessage: string): Promise<string> {
// 将新的用户消息追加到缓冲区
buffer.push({ role: "user", content: userMessage });
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
// 每次调用时传递完整的缓冲区
messages: buffer,
});
const assistantMessage =
response.content[0].type === "text" ? response.content[0].text : "";
// 将助手回复追加到缓冲区
buffer.push({ role: "assistant", content: assistantMessage });
return assistantMessage;
}
// 使用示例
await chat("我叫 Alex,我更喜欢 Python 而不是 TypeScript。");
await chat("我下一个项目应该使用什么语言?");
// 模型记住了第一条消息中声明的偏好

何时使用: 少于 20 轮的对话。简单的聊天机器人。原型。

局限性: 上下文窗口是有限的。长会话最终会超过 token 限制,最旧的消息必须被丢弃。模型悄悄地丢失早期上下文,导致行为不一致。

策略二:滚动摘要

当对话变长时,对旧轮次进行摘要而不是丢弃。维护一个压缩摘要,记录最近消息滑动窗口之前发生的事情。

import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface ConversationState {
summary: string; // 旧轮次的压缩历史
recentMessages: Array<{ role: "user" | "assistant"; content: string }>;
maxRecentTurns: number; // 保留最近 N 轮的原文
}
const state: ConversationState = {
summary: "",
recentMessages: [],
maxRecentTurns: 6, // 3 轮用户 + 3 轮助手
};
// 将旧轮次压缩到滚动摘要中
async function compressSummary(
currentSummary: string,
messagesToCompress: Array<{ role: string; content: string }>
): Promise<string> {
const formatted = messagesToCompress
.map((m) => `${m.role.toUpperCase()}: ${m.content}`)
.join("\n");
const response = await client.messages.create({
model: "claude-haiku-4-5-20251001",
max_tokens: 512,
messages: [
{
role: "user",
content: `通过添加以下新的对话内容来更新对话摘要。
只返回更新后的摘要。保持在 200 字以内。
当前摘要:
${currentSummary || "(无)"}
新的对话内容:
${formatted}`,
},
],
});
return response.content[0].type === "text" ? response.content[0].text : currentSummary;
}
async function chat(userMessage: string): Promise<string> {
// 当缓冲区超过限制时,压缩最旧的一半
if (state.recentMessages.length >= state.maxRecentTurns * 2) {
const toCompress = state.recentMessages.splice(0, state.maxRecentTurns);
state.summary = await compressSummary(state.summary, toCompress);
}
state.recentMessages.push({ role: "user", content: userMessage });
// 构建消息数组:系统摘要 + 最近的原文轮次
const messages = [
...(state.summary
? [
{
role: "user" as const,
content: `本次对话早期的上下文:\n${state.summary}`,
},
{
role: "assistant" as const,
content: "明白。我会使用那些上下文。",
},
]
: []),
...state.recentMessages,
];
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages,
});
const assistantMessage =
response.content[0].type === "text" ? response.content[0].text : "";
state.recentMessages.push({ role: "assistant", content: assistantMessage });
return assistantMessage;
}

摘要调用使用快速、低成本的模型(Haiku)。主对话使用更强大的模型(Sonnet)。这种分工在保持质量的同时降低了成本。

何时使用: 超过 20 轮的会话。客服智能体。长时间运行的任务智能体。

局限性: 摘要会丢失细节。对话早期声明的具体事实可能被压缩成模糊的陈述。对于精确召回特定事实,请使用情景记忆。

策略三:情景记忆

情景记忆将关于用户或会话的特定事实存储在键值存储中。智能体在对话期间提取事实,并在未来的会话中检索它们。

import Anthropic from "@anthropic-ai/sdk";
import fs from "fs/promises";
const client = new Anthropic();
// 在生产环境中,用 Redis 或数据库替换此文件存储
const MEMORY_PATH = "./memory.json";
interface EpisodicStore {
[userId: string]: Record<string, string>;
}
async function loadMemory(): Promise<EpisodicStore> {
try {
const data = await fs.readFile(MEMORY_PATH, "utf-8");
return JSON.parse(data);
} catch {
return {};
}
}
async function saveMemory(store: EpisodicStore): Promise<void> {
await fs.writeFile(MEMORY_PATH, JSON.stringify(store, null, 2));
}
// 从最新的对话中提取结构化事实
async function extractFacts(
userMessage: string,
assistantReply: string
): Promise<Record<string, string>> {
const response = await client.messages.create({
model: "claude-haiku-4-5-20251001",
max_tokens: 256,
messages: [
{
role: "user",
content: `从这次对话中提取任何个人事实、偏好或重要决定。
返回一个键值对的 JSON 对象。如果没有值得记住的内容,返回 {}。
用户:${userMessage}
助手:${assistantReply}`,
},
],
});
try {
const text = response.content[0].type === "text" ? response.content[0].text : "{}";
const match = text.match(/\{[\s\S]*\}/);
return match ? JSON.parse(match[0]) : {};
} catch {
return {};
}
}
// 将存储的事实格式化为系统提示注入
function formatMemory(facts: Record<string, string>): string {
const entries = Object.entries(facts);
if (entries.length === 0) return "";
return (
"你对该用户了解的内容:\n" +
entries.map(([k, v]) => `- ${k}:${v}`).join("\n")
);
}
async function chat(userId: string, userMessage: string): Promise<string> {
const store = await loadMemory();
const userFacts = store[userId] ?? {};
const systemPrompt = formatMemory(userFacts);
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: systemPrompt || undefined,
messages: [{ role: "user", content: userMessage }],
});
const assistantMessage =
response.content[0].type === "text" ? response.content[0].text : "";
// 提取事实并将其持久化以供未来会话使用
const newFacts = await extractFacts(userMessage, assistantMessage);
if (Object.keys(newFacts).length > 0) {
store[userId] = { ...userFacts, ...newFacts };
await saveMemory(store);
}
return assistantMessage;
}
// 会话一
await chat("user-123", "我喜欢简洁的回答。除非必要,否则不要用项目符号。");
// 会话二(几天后的不同进程)
await chat("user-123", "解释 TCP 握手是如何工作的。");
// 智能体记住了会话一中的格式偏好

何时使用: 跨会话运行的面向用户的智能体。个性化。偏好追踪。

局限性: 事实随时间积累。过时的事实可能导致错误的行为(用户更改了首选语言;旧偏好仍在存储中)。添加更新或过期事实的机制。

策略四:语义记忆(向量搜索)

情景记忆存储离散事实。语义记忆存储按含义索引的文档、代码或对话片段。当智能体需要信息时,它使用查询搜索索引。

这是检索增强生成(RAG)的基础。

import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface Document {
id: string;
content: string;
embedding?: number[];
}
// 计算两个向量之间的余弦相似度
function cosineSimilarity(a: number[], b: number[]): number {
const dot = a.reduce((sum, val, i) => sum + val * b[i], 0);
const magA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
const magB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
return dot / (magA * magB);
}
class SemanticMemory {
private documents: Document[] = [];
// 索引文档以便后续检索
async add(id: string, content: string): Promise<void> {
const embedding = await embed(content); // 使用专用的嵌入模型
this.documents.push({ id, content, embedding });
}
// 检索与查询最相关的 k 个文档
async search(query: string, topK = 3): Promise<Document[]> {
const queryEmbedding = await embed(query);
return this.documents
.filter((doc) => doc.embedding)
.map((doc) => ({
doc,
score: cosineSimilarity(queryEmbedding, doc.embedding!),
}))
.sort((a, b) => b.score - a.score)
.slice(0, topK)
.map(({ doc }) => doc);
}
}
const memory = new SemanticMemory();
async function answerWithContext(question: string): Promise<string> {
// 检索相关文档
const relevant = await memory.search(question);
const context = relevant.map((d) => d.content).join("\n\n---\n\n");
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: context
? `使用以下上下文回答问题:\n\n${context}`
: undefined,
messages: [{ role: "user", content: question }],
});
return response.content[0].type === "text" ? response.content[0].text : "";
}
// 示例:索引知识库并查询
await memory.add("doc-1", "团队使用 React 18 和 app router 模式。");
await memory.add("doc-2", "所有 API 路由必须使用 Zod 模式验证输入。");
await memory.add("doc-3", "数据库查询使用 Drizzle ORM。避免原始 SQL。");
const answer = await answerWithContext("我应该如何编写一个新的 API 端点?");
// 智能体检索 doc-2 和 doc-3,并生成有依据的回答

对于生产使用,用专用向量数据库替换内存存储。如果你已经运行 PostgreSQL,Pgvector 是最简单的选择。ChromaQdrant 是不错的独立选项。

何时使用: 文档助手。代码搜索智能体。知识库问答。

将记忆连接到 MCP

如果你遵循了 MCP 服务器指南,可以将记忆作为 MCP 资源暴露。这使你的记忆层可供任何兼容 MCP 的客户端访问,包括 Claude Code。

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{ name: "memory-server", version: "1.0.0" },
{ capabilities: { tools: {}, resources: {} } }
);
// 工具:存储一个事实
server.setRequestHandler("tools/call", async (request) => {
if (request.params.name === "remember") {
const { key, value, userId } = request.params.arguments as {
key: string;
value: string;
userId: string;
};
await storeEpisodicFact(userId, key, value);
return { content: [{ type: "text", text: `已存储:${key} = ${value}` }] };
}
if (request.params.name === "recall") {
const { userId } = request.params.arguments as { userId: string };
const facts = await loadEpisodicFacts(userId);
return {
content: [{ type: "text", text: JSON.stringify(facts, null, 2) }],
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);

任何使用 MCP 的智能体现在都可以将 rememberrecall 作为标准工具调用。记忆成为共享服务,而不是每个智能体各自实现。

选择正确的策略

以此决策表为起点:

情况策略
单次会话,少于 20 轮缓冲区
单次会话,长时间运行摘要
多会话,用户偏好情景记忆
大型知识库查询语义记忆
以上所有情况组合:会话内摘要 + 跨会话情景/语义记忆

从缓冲区开始。当对话变长时添加摘要层。当用户跨会话返回时添加情景记忆。当你有文档语料库需要检索时添加语义记忆。

不要在需要之前添加复杂性。对话缓冲区是大多数原型的正确答案。

结论

记忆不是单一的东西——它是一组在不同范围内解决不同问题的策略。缓冲区处理当前会话。摘要延伸该会话。情景记忆连接各个会话。语义记忆将回答建立在知识库的基础上。

选择能解决你今天问题的层次。将其封装在干净的接口后面,这样以后可以切换实现。如果你的智能体通过 MCP 通信,将记忆作为共享工具暴露——这使每个智能体保持简单,同时给整个系统一个持久的大脑。


相关文章