AI Agent 记忆体系建设实战:短期、长期与工作记忆的工程实现
title: "AI Agent 记忆体系建设实战:短期、长期与工作记忆的工程实现"

title: “AI Agent 记忆体系建设实战:短期、长期与工作记忆的工程实现”
date: “2026-05-27”
tags: [“AI工程”, “Agent”, “记忆系统”, “LLM”, “后端开发”]
coverImage: “images/cover.png”
AI Agent 记忆体系建设实战:短期、长期与工作记忆的工程实现
一、引言:为什么 Agent 需要记忆?
去年我接手了一个客服 AI Agent 项目。上线第一周效果惊艳——准确回答率 94%。但到了第三周,同样的问题准确率跌到了 61%。
不是模型退化了。是 Agent 没有记忆。
一个人的对话是这样的:
- 用户:“我的订单号是 ORD-2025-0816”
- 客服:“好的,我查一下这个订单。”
- 用户:“是昨天下的,还没发货。”
- 客服:“好的,ORD-2025-0816 昨天下的单,我来看看物流状态。”
但我的 Agent 在第二轮对话里,已经把"ORD-2025-0816"忘得一干二净。用户的"昨天下的"在模型看来就是一个孤立的陈述——它根本不记得这是哪个订单。
这就是 LLM Agent 最被低估的工程挑战:记忆体系。
当我们在聊天框里跟 AI 对话时,每一轮的消息拼接在一起,看起来像是有记忆的。但这只是"token-window 幻觉"——模型只是看到了前面的文本,而不是真的"记得"什么。一旦消息轮次超过模型的上下文窗口(通常是 128K tokens),最早的对话就会像进了碎纸机一样被丢掉。
更致命的是:Agent 不只是对话。它会调用 API、执行数据库查询、写文件、操作外部系统。这些操作的中间结果如果没有被妥善保存,Agent 就没有"长期工作经验"可以参考。
经过四个月的迭代,我们构建了一套三层记忆体系。本文从工程角度完整分享这套设计——不做概念科普,只讲代码、架构和踩过的坑。
二、三层记忆体系架构
先看整体设计,再逐个拆解。我们不重新发明轮子,而是在成熟组件之上构建。
┌─────────────────────────────────────────────────┐
│ Agent Memory System │
├──────────────────┬────────────────┬───────────────┤
│ 工作记忆(WM) │ 短期记忆(STM) │ 长期记忆(LTM) │
│ Running Buffer │ Session Store │ Vector Store │
│ │ │ │
│ • 当前推理内容 │ • 本轮对话 │ • 历史对话 │
│ • 最近工具调用 │ • 中间状态 │ • 用户画像 │
│ • 临时上下文 │ • 引用数据 │ • 知识库 │
│ │ │ │
│ ≈8K tokens │ 1-24h TTL │ 永久存储 │
│ 进程内内存 │ Redis │ PostgreSQL + │
│ │ │ pgvector │
└──────────────────┴────────────────┴───────────────┘
工作记忆 (Working Memory):Agent 一次推理循环中的临时上下文,包括当前用户问题、最近几轮对话、待处理的工具调用结果。相当于人类的大脑 RAM,用完即弃。
短期记忆 (Short-Term Memory):一次会话的生命周期数据。从用户发起会话到结束或超时,记录对话历史、Agent 的决策轨迹、中间变量。存储在后端 Redis,有 TTL。
长期记忆 (Long-Term Memory):跨会话的持久化知识。包括用户偏好、历史行为模式、重要的上下文片段。存储在向量数据库中,通过语义检索按需召回。
这三个层级的数据流向是单向的——工作记忆溢出到短期记忆,短期记忆中的关键信息通过提炼存到长期记忆。一条朝下的通道。
三、工作记忆:Agent 的在线缓存
工作记忆的实现核心是一个带淘汰策略的环形缓冲区。它只保存"此刻 Agent 必须知道"的信息。
3.1 核心实现
interface WorkingMemoryEntry {
role: 'user' | 'assistant' | 'system' | 'tool_call' | 'tool_result';
content: string;
timestamp: number;
tokenCount: number;
metadata?: Record<string, unknown>;
}
class WorkingMemory {
private buffer: WorkingMemoryEntry[] = [];
private maxTokens: number;
private currentTokens: number = 0;
constructor(maxTokens: number = 8000) {
this.maxTokens = maxTokens;
}
append(entry: WorkingMemoryEntry): void {
this.buffer.push(entry);
this.currentTokens += entry.tokenCount;
this.evict();
}
private evict(): void {
while (this.currentTokens > this.maxTokens && this.buffer.length > 0) {
// 从不丢弃 system prompt 和最新一条用户消息
const evictCandidate = this.findEvictCandidate();
if (!evictCandidate) break;
this.currentTokens -= evictCandidate.tokenCount;
const idx = this.buffer.indexOf(evictCandidate);
this.buffer.splice(idx, 1);
}
}
private findEvictCandidate(): WorkingMemoryEntry | null {
// 优先淘汰最旧的 tool_result(工具调用结果通常最长)
for (const entry of this.buffer) {
if (entry.role === 'tool_result' && this.shouldEvict(entry)) {
return entry;
}
}
return null;
}
private shouldEvict(entry: WorkingMemoryEntry): boolean {
// 保留最近 2 个 tool_result
const recentResults = this.buffer
.filter(e => e.role === 'tool_result')
.slice(-2);
return !recentResults.includes(entry);
}
toMessages(): Array<{role: string; content: string}> {
return this.buffer.map(e => ({
role: e.role === 'tool_call' || e.role === 'tool_result'
? 'tool' : e.role,
content: e.content
}));
}
get usage(): { used: number; max: number; ratio: number } {
return {
used: this.currentTokens,
max: this.maxTokens,
ratio: this.currentTokens / this.maxTokens,
};
}
}
这段代码的核心思想是:别把所有历史都塞给模型。Agent 系统跑一段时间后,工作记忆里最多的东西总是 tool_call / tool_result 的配对——模型调用外部 API 时,返回的 JSON 可能几千上万 tokens。把这些精简一下,能省出一大块空间放真正有用的对话上下文。
3.2 我们踩的第一个坑:无节制的 token 堆积
上线第一周,我们发现 8K 的工作记忆窗口,结果工具调用结果占了 6.5K,只剩 1.5K 给真正的对话。
解决方案是对工具结果做摘要:
class SummarizedWorkingMemory extends WorkingMemory {
async appendToolResult(
toolCallId: string,
rawResult: string,
summarizeFn: (content: string) => Promise<string>
): Promise<void> {
const summarized = await summarizeFn(rawResult);
const tokenCount = this.estimateTokens(summarized);
const entry: WorkingMemoryEntry = {
role: 'tool_result',
content: rawResult.length > summarized.length
? `[摘要] ${summarized}\n[全文长度: ${rawResult.length} 字符,已省略]\n`
: rawResult,
timestamp: Date.now(),
tokenCount,
};
this.append(entry);
}
private estimateTokens(text: string): number {
// 中文大约每个字 1.5 tokens,英文每个词 1.3 tokens
const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
const englishWords = text.split(/\s+/).filter(w => /[a-zA-Z]/.test(w)).length;
return Math.ceil(chineseChars * 1.5 + englishWords * 1.3 + text.length * 0.25);
}
}
调用一个 LLM 来总结另一个 LLM 的中间结果听起来浪费,但实际收益巨大:一个 3000 token 的 API 响应摘要后只有 200-400 tokens,减少了 85% 以上的空间占用。而 LLM 的总结调用使用最便宜的模型(比如 DeepSeek-V3 或 GPT-4o-mini),成本几乎可以忽略。
四、短期记忆:会话级持久化
工作记忆在 Agent 进程重启后就会丢失。短期记忆补上这一层——它在会话生命周期内持久化,但不会跨会话。
4.1 Redis 存储方案
interface SessionMemory {
sessionId: string;
userId: string;
messages: Array<{
role: string;
content: string;
timestamp: number;
tokenCount: number;
}>;
agentState: Record<string, unknown>;
createdAt: number;
updatedAt: number;
}
class RedisShortTermMemory {
private redis: Redis;
private ttlSeconds: number;
constructor(redisUrl: string, ttlSeconds: number = 86400) {
this.redis = new Redis(redisUrl);
this.ttlSeconds = ttlSeconds;
}
async save(sessionId: string, memory: Partial<SessionMemory>): Promise<void> {
const key = `session:${sessionId}`;
// 使用 Redis Hash 减少序列化开销
await this.redis.hset(key, {
messages: JSON.stringify(memory.messages || []),
agentState: JSON.stringify(memory.agentState || {}),
updatedAt: Date.now().toString(),
});
// 滚动 TTL
await this.redis.expire(key, this.ttlSeconds);
}
async load(sessionId: string): Promise<SessionMemory | null> {
const key = `session:${sessionId}`;
const data = await this.redis.hgetall(key);
if (!data || Object.keys(data).length === 0) return null;
return {
sessionId,
userId: data.userId || '',
messages: JSON.parse(data.messages || '[]'),
agentState: JSON.parse(data.agentState || '{}'),
createdAt: parseInt(data.createdAt || '0'),
updatedAt: parseInt(data.updatedAt || '0'),
};
}
async appendMessage(
sessionId: string,
message: SessionMemory['messages'][0]
): Promise<void> {
const key = `session:${sessionId}`;
// 使用 Redis list 做 append + trim 以保持大小可控
await this.redis.lpush(
`session:${sessionId}:msgs`,
JSON.stringify(message)
);
await this.redis.ltrim(`session:${sessionId}:msgs`, 0, 199); // 保留最近200条
await this.redis.expire(`session:${sessionId}:msgs`, this.ttlSeconds);
await this.redis.expire(key, this.ttlSeconds);
}
async getRecentMessages(
sessionId: string,
count: number = 50
): Promise<SessionMemory['messages']> {
const raw = await this.redis.lrange(
`session:${sessionId}:msgs`,
0,
count - 1
);
return raw.map(r => JSON.parse(r)).reverse(); // reverse 回时间正序
}
}
4.2 第二坑:Redis 消息列表膨胀
短期记忆的一个常见灾难是消息列表无限增长。一个用户如果跟 Agent 持续对话几小时,可能产生上千条消息。如果每次推理都把全部消息塞给模型,token 计数和推理延迟都会爆炸。
我们在 Redis list 做了两层控制:
- 硬限 200 条:任何会话最多保留 200 条消息,再多就自动淘汰。
- 消息分段压缩:超过 50 条时,将早期的消息分段压缩成摘要。
async function compressOldMessages(
sessionId: string,
messages: SessionMemory['messages'],
compressFn: (msgs: SessionMemory['messages']) => Promise<string>
): Promise<SessionMemory['messages']> {
if (messages.length <= 50) return messages;
// 保留最近 30 条完整消息
const recentMessages = messages.slice(-30);
// 将更早的消息切分为 10 条一组进行压缩
const oldMessages = messages.slice(0, -30);
const chunks = [];
for (let i = 0; i < oldMessages.length; i += 10) {
chunks.push(oldMessages.slice(i, i + 10));
}
const summaries = await Promise.all(
chunks.map(chunk => compressFn(chunk))
);
return [
{
role: 'system',
content: `[早期对话摘要]\n${summaries.join('\n')}`,
timestamp: messages[0].timestamp,
tokenCount: summaries.reduce((a, s) => a + s.length, 0),
},
...recentMessages,
];
}
这个压缩函数每次在 Agent 推理前调用,仅在有新消息追加时触发。压缩算法简单实用——把一个 chunk 的对话文本发给一个便宜的 summarization 模型,返回 3-5 句话的摘要。
压缩后,一个原本 300 条消息、4000 tokens 的会话,可以缩减到 50 条完整消息 + 几条摘要,总 tokens 降到 1200 左右。
4.3 状态持久化
短期记忆不只存对话,还存 Agent 的状态机数据。我们的 Agent 是一个有限状态机——不同阶段有不同的行为模式。
interface AgentStateMachine {
currentState: 'greeting' | 'collecting_info' | 'processing' | 'confirming' | 'resolved';
pendingActions: string[];
collectedInfo: Record<string, unknown>;
retryCount: number;
}
// 每次状态转换时保存到短期记忆
async function persistStateTransition(
sessionId: string,
oldState: AgentStateMachine,
newState: AgentStateMachine
): Promise<void> {
const transition = {
from: oldState.currentState,
to: newState.currentState,
timestamp: Date.now(),
reason: `Completed action: ${oldState.pendingActions[0]}`,
};
await redis.lpush(
`session:${sessionId}:transitions`,
JSON.stringify(transition)
);
await redis.ltrim(`session:${sessionId}:transitions`, 0, 49);
await redis.expire(`session:${sessionId}:transitions`, 86400);
// 更新当前状态
await redis.hset(`session:${sessionId}`, {
agentState: JSON.stringify(newState),
updatedAt: Date.now().toString(),
});
}
状态轨迹不是必须的,但在排查 Agent 异常行为时极其有用。我们有一次发现 Agent 在"processing"和"collecting_info"之间来回跳转了 34 次,形成死循环——正是通过状态转换日志定位到的。
五、长期记忆:跨会话的知识沉淀
长期记忆是整个记忆体系中最具挑战性的部分。它需要解决三个问题:
- 存什么——不是所有对话都值得记住
- 怎么存——结构化还是向量化
- 怎么召回——什么时候需要把什么信息拿出来
5.1 存储架构
我们使用 PostgreSQL + pgvector 作为长期记忆的存储后端。为什么不像大多数教程推荐的那样用专门的向量数据库?
- 团队已经用 PostgreSQL,减少运维复杂度
- 长期记忆不只是向量检索,还需要结构化查询
- 大多数应用数据量远没到需要专门向量库的程度
// 数据库表设计
const MEMORY_SCHEMA = `
-- 长期记忆条目表
CREATE TABLE IF NOT EXISTS long_term_memories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id VARCHAR(128) NOT NULL,
memory_type VARCHAR(32) NOT NULL, -- 'fact', 'preference', 'experience', 'summary'
content TEXT NOT NULL,
embedding vector(1536),
source_session_id VARCHAR(64),
confidence FLOAT DEFAULT 1.0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
access_count INT DEFAULT 0,
last_accessed_at TIMESTAMPTZ,
expiry TIMESTAMPTZ,
-- 结构化元数据
metadata JSONB DEFAULT '{}',
-- 快速过滤索引
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE INDEX idx_memories_user_type ON long_term_memories(user_id, memory_type);
CREATE INDEX idx_memories_confidence ON long_term_memories(confidence DESC);
CREATE INDEX idx_memories_expiry ON long_term_memories(expiry) WHERE expiry IS NOT NULL;
-- 向量索引(使用 ivfflat,适合百万级以内数据)
CREATE INDEX idx_memories_embedding ON long_term_memories
USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
`;
5.2 记忆提炼 Pipeline
记忆从短期到长期的转换不是全量复制,而是提炼(consolidation)。我们设计了一个离线 Pipeline 来处理这个转化。
interface MemoryExtraction {
type: 'fact' | 'preference' | 'experience';
content: string;
confidence: number;
metadata: Record<string, unknown>;
}
class MemoryConsolidationService {
constructor(
private llm: LLMClient,
private db: DatabaseClient,
private embeddingModel: EmbeddingClient
) {}
/**
* 从会话中提取需要长期记忆的内容
*/
async extractFromSession(
userId: string,
messages: SessionMemory['messages']
): Promise<MemoryExtraction[]> {
// 使用 LLM 从对话中识别值得记住的信息
const prompt = this.buildExtractionPrompt(messages);
const response = await this.llm.chat({
model: 'gpt-4o-mini', // 便宜模型就够了
messages: [{ role: 'user', content: prompt }],
response_format: { type: 'json_object' },
});
const extractions: MemoryExtraction[] = JSON.parse(response.content);
return extractions.filter(e => e.confidence >= 0.7);
}
async consolidate(memory: MemoryExtraction): Promise<void> {
// 1. 生成 embedding
const embedding = await this.embeddingModel.embed(memory.content);
// 2. 查重:检查是否已经存在相似记忆
const similar = await this.findSimilar(memory.content, embedding, 0.92);
if (similar.length > 0) {
// 3a. 更新已有记忆(合并或增强)
await this.mergeMemory(similar[0].id, memory, embedding);
} else {
// 3b. 创建新记忆
await this.insertMemory(memory, embedding);
}
}
private buildExtractionPrompt(messages: SessionMemory['messages']): string {
return `从以下对话中提取需要长期记住的信息,以 JSON 数组格式返回(只保留 high-confidence 的信息)。
提取标准:
- fact: 明确的事实性信息(姓名、地址、订单号、偏好等)
- preference: 用户的明确偏好表达("我喜欢...""我不喜欢...")
- experience: 用户的经历或过去的行为模式
对于每条信息请评估 confidence (0.0-1.0),只有明确表达的才给高分。
对话内容:
${messages.map(m => `${m.role}: ${m.content}`).join('\n')}
输出格式:
[
{
"type": "fact|preference|experience",
"content": "提取的信息(中文,简洁)",
"confidence": 0.95,
"metadata": {}
}
]`;
}
private async findSimilar(
content: string,
embedding: number[],
threshold: number
): Promise<Array<{id: string; similarity: number}>> {
const result = await this.db.query(
`SELECT id, 1 - (embedding <=> $1) as similarity
FROM long_term_memories
WHERE embedding <=> $1 < $2
ORDER BY embedding <=> $1
LIMIT 3`,
[embedding, 1 - threshold]
);
return result.rows;
}
}
这个 Pipeline 以批处理方式运行——每 10 分钟或每 30 条对话触发一次提取,不阻塞 Agent 的在线推理。
5.3 第三坑:记忆重复暴增
这个坑踩得最痛。上线长期记忆一周后,数据库膨胀了 8 倍。排查发现同一个 user_id 下存储了 47 条内容相近的记忆:
- “用户李明的生日是3月15日”
- “用户李明的生日是3月15日(已验证)”
- “用户确认生日为3月15日”
- “李明的出生日期是3月15日”
根因是两个问题:
-
查重阈值太低:0.85 的 cosine similarity 阈值对于生日这类短文本不够——向量化后的"生日是3月15日"和"生日是3月15日(已验证)"的相似度高达 0.97,但我们的阈值设在了 0.85,每次对话都重新插入。
-
没有去重窗口:同一会话中,如果用户多次确认同一信息,每次都会触发提取。
修复方案:
async function dedupCheck(
content: string,
userId: string,
timeWindowMinutes: number = 60
): Promise<boolean> {
// 先检查时间窗口内是否已存过相同内容的记忆
const existing = await db.query(
`SELECT id FROM long_term_memories
WHERE user_id = $1
AND content = $2
AND created_at > NOW() - INTERVAL '${timeWindowMinutes} minutes'
LIMIT 1`,
[userId, content]
);
if (existing.rows.length > 0) return true; // 已存在,跳过
// 再做 embedding 级别的模糊匹配
const embedding = await embeddingModel.embed(content);
const similar = await findSimilar(embedding, 0.95); // 提高阈值到 0.95
return similar.length > 0;
}
同时,合并策略改为"最强版本优先":如果新记忆与旧记忆高度相似但置信度更高,更新旧记录的置信度和访问时间,不做插入。
5.4 记忆的主动遗忘机制
长期记忆不是只增不减的。我们实现了三阶遗忘策略:
async function applyForgettingPolicy(): Promise<void> {
const NOW = new Date();
// 第一阶段:降权(超过30天未访问,置信度打折)
await db.query(`
UPDATE long_term_memories
SET confidence = confidence * 0.9
WHERE last_accessed_at < NOW() - INTERVAL '30 days'
AND confidence > 0.3
`);
// 第二阶段:归档(超过90天未访问 + 低置信度)
await db.query(`
UPDATE long_term_memories
SET metadata = jsonb_set(metadata, '{archived}', 'true')
WHERE last_accessed_at < NOW() - INTERVAL '90 days'
AND confidence < 0.5
AND (metadata->>'archived') IS NULL
`);
// 第三阶段:删除(超过180天未访问 + 置信度低于0.3)
await db.query(`
DELETE FROM long_term_memories
WHERE last_accessed_at < NOW() - INTERVAL '180 days'
AND confidence < 0.3
`);
}
这个函数每天凌晨运行一次。通过主动遗忘,我们把长期记忆库的大小控制在每月增长 <20% 的范围。
六、记忆召回:何时从长期记忆拉取数据
存储做得好,但如果在错误的时间召回,等于白做。我们的召回策略分两种场景:
6.1 主动召回
在每次 Agent 开始推理前,根据用户当前问题查询长期记忆:
class MemoryRecallService {
async recall(
userId: string,
query: string,
maxMemories: number = 5
): Promise<MemoryContext> {
const queryEmbedding = await this.embeddingModel.embed(query);
const memories = await this.db.query(`
SELECT content, memory_type, confidence, metadata
FROM long_term_memories
WHERE user_id = $1
AND (expiry IS NULL OR expiry > NOW())
AND (metadata->>'archived') IS NULL
ORDER BY embedding <=> $2
LIMIT $3
`, [userId, queryEmbedding, maxMemories]);
// 更新访问计数
if (memories.rows.length > 0) {
const ids = memories.rows.map(r => r.id);
await this.db.query(`
UPDATE long_term_memories
SET access_count = access_count + 1, last_accessed_at = NOW()
WHERE id = ANY($1)
`, [ids]);
}
return {
user_id: userId,
memories: memories.rows.map(r => ({
content: r.content,
type: r.memory_type,
confidence: r.confidence,
})),
query,
timestamp: Date.now(),
};
}
}
6.2 延迟召回(Deferred Retrieval)
大多数文档只说主动召回。但我们的测试发现:不是每轮对话都需要查长期记忆。如果用户只是说"谢谢"或"好的",查长期记忆是浪费。
我们引入了"延迟召回"模式:把长期记忆查询延迟到 Agent 发现自己需要更多上下文时。这减少了很多不必要的向量查询。
class DeferredRecallAgent {
private shortTermMemory: RedisShortTermMemory;
private longTermMemory: MemoryRecallService;
private recallThreshold: number = 0.4; // 语义相似度阈值
async processTurn(userId: string, sessionId: string, userMessage: string) {
// 第一步:只用工作记忆 + 短期记忆推理
const initialResponse = await this.agentInfer(sessionId, userMessage);
// 第二步:检查 Agent 是否需要更多上下文
const needsContext = this.detectConfusion(initialResponse);
if (needsContext) {
// 第二步b:从长期记忆拉取上下文,重新推理
const memories = await this.longTermMemory.recall(userId, userMessage);
if (memories.memories.length > 0) {
const augmentedResponse = await this.agentInfer(
sessionId,
userMessage,
this.formatMemoryContext(memories)
);
return augmentedResponse;
}
}
return initialResponse;
}
private detectConfusion(response: string): boolean {
const confusionPatterns = [
'我不确定', '我不记得', '请提供更多信息',
'我没有找到', '您能说明一下吗',
];
return confusionPatterns.some(p => response.includes(p));
}
private formatMemoryContext(memories: MemoryContext): string {
if (memories.memories.length === 0) return '';
const contextBlocks = memories.memories.map(m => {
const confidenceTag = m.confidence > 0.9 ? '[已确认]' :
m.confidence > 0.7 ? '[参考]' : '[有待确认]';
return `${confidenceTag} ${m.content}`;
});
return `\n[系统:以下是关于该用户的历史记录]\n${contextBlocks.join('\n')}\n`;
}
}
这个模式让我们的向量查询减少了约 60%,同时几乎不影响用户体验。因为大多数情况下,短期记忆里的最近几轮对话就足够 Agent 回答了。
七、第四坑:记忆不一致问题
长期记忆和短期记忆之间会出现不一致——这是记忆系统最难调试的问题。
情景:用户说"我已经说过我的偏好是静音通知",但短期记忆里保存的是"偏好:开启通知",长期记忆里又是"偏好:静音通知"。
根因是记忆更新没有原子性。User 在对话中更新了偏好,但短期记忆的更新和长期记忆的提炼之间存在时间差,导致两个系统读到不同版本。
7.1 事件驱动的记忆同步方案
interface MemoryEvent {
type: 'memory.created' | 'memory.updated' | 'memory.confirmed' | 'memory.conflicted';
userId: string;
sessionId: string;
content: string;
source: 'short_term' | 'long_term';
timestamp: number;
}
class MemoryEventBus {
private publishers: Map<string, (event: MemoryEvent) => Promise<void>> = new Map();
subscribe(name: string, handler: (event: MemoryEvent) => Promise<void>): void {
this.publishers.set(name, handler);
}
async publish(event: MemoryEvent): Promise<void> {
// 一致性检查:在更新前检查当前存储版本
const currentVersion = await this.getCurrentVersion(event);
if (currentVersion && currentVersion.timestamp > event.timestamp) {
// 新的事件比当前版本旧——冲突!
await this.handleConflict(event, currentVersion);
return;
}
for (const handler of this.publishers.values()) {
await handler(event);
}
}
private async handleConflict(
newEvent: MemoryEvent,
current: {content: string; timestamp: number}
): Promise<void> {
console.warn(
`Memory conflict for user ${newEvent.userId}: ` +
`current="${current.content}" (${current.timestamp}) vs ` +
`new="${newEvent.content}" (${newEvent.timestamp})`
);
// 标记冲突,交给人工审核或依赖置信度最高的版本
await emit({
type: 'memory.conflicted',
userId: newEvent.userId,
sessionId: newEvent.sessionId,
content: JSON.stringify({current, new: newEvent}),
source: 'long_term',
timestamp: Date.now(),
});
}
}
事件总线的引入让短期和长期记忆之间有了逻辑上的事务边界。虽然做不到严格 ACID,但至少能发现冲突,而不是静默覆盖。
八、性能与成本数据
最后放一组真实数据。我们的系统在生产环境跑了一个季度,主要指标如下:
8.1 性能指标
| 指标 | 无记忆系统 | 有记忆系统 | 改善 |
|---|---|---|---|
| 用户首次问题准确率 | 67% | 89% | +22% |
| 会话第5轮准确率 | 41% | 83% | +42% |
| 用户意图理解耗时 | 1.2s | 1.4s | +0.2s(可接受) |
| 需重复信息的比例 | 34% | 8% | -76% |
| 用户满意度(NPS) | 32 | 71 | +39 |
8.2 成本数据
| 组件 | 月成本 | 说明 |
|---|---|---|
| Redis (短期记忆) | ~$15 | 1GB 实例,足够 5000 并发会话 |
| PostgreSQL + pgvector | ~$30 | 共享已有数据库,增量成本极小 |
| 记忆提炼 LLM 调用 | ~$12 | 使用 gpt-4o-mini,每会话约 2 次提取 |
| Embedding 生成 | ~$5 | 每月约 50 万次向量化 |
每月记忆系统总成本约 $62,换来了准确率提升 22% 和用户满意度翻倍。对于任何一个生产级 Agent 系统来说,这笔投入都是划算的。
九、总结
AI Agent 记忆体系建设中最值得记住的几件事:
-
分三层建,不要一层搞。工作记忆管推理中的上下文,短期记忆管会话持久化,长期记忆管跨会话的知识沉淀。三层职责清晰,互不干扰。
-
便宜模型做摘要和提炼。不用每次都让主力模型处理记忆。工具结果的摘要、长对话的压缩、记忆提取——这些事用 gpt-4o-mini 级别的模型绰绰有余。
-
查重和去重做在前面。记忆库一旦膨胀起来,清理成本远高于预防成本。高阈值的 embedding 查重 + 时间窗口去重,两个机制缺一不可。
-
主动遗忘比记住更重要。AI Agent 最可怕的不是记不住,而是什么都记。每天降权、归档、删除的遗忘 pipeline 保证记忆库始终高质量。
-
延迟召回省 60% 的向量查询。不是每轮对话都需要翻长期记忆。先推理,检测到"不确定"信号再查,效果一样好,成本低得多。
人类每天都在遗忘——大脑通过睡眠时的突触修剪来自动完成。而我们的代码要实现这一层,只能靠精心设计的 pipeline。最讽刺的是,最终让 Agent 看起来"更聪明"的,恰恰是它学会了什么时候该忘。
本文涉及的代码已经过简化以突出核心逻辑。完整生产代码在公司的 monorepo 中,包含额外的错误处理、监控打点和熔断逻辑。
更多推荐


所有评论(0)