Context 系统
记忆管理实现
记忆管理实现
概述
Context 系统的记忆管理包含两个核心组件:
- ThreadSummary(会话摘要):压缩单个会话的历史对话
- LongTermMemory(长期记忆):跨会话的持久化记忆
两者配合,确保 AI 能够理解短期历史和长期偏好。
ThreadSummary(会话摘要)
核心功能
会话摘要负责压缩长对话历史,保留关键信息:
- 增量生成:新摘要与旧摘要合并
- 双层触发:消息数阈值 + Token 阈值
- 异步更新:不阻塞主流程
- 智能合并:保留重要决策和偏好
触发机制
参考 Goose 的两层压缩策略:
interface SummaryConfig {
/** 消息数阈值 (默认 10 条) */
messageThreshold: number;
/** Token 压缩阈值 (默认 3000) */
tokenCompactionThreshold: number;
/** 是否启用 Token 阈值触发 */
enableTokenBasedCompaction: boolean;
}
// 检查触发条件
const messageCountTrigger = newMessageCount >= this.config.messageThreshold;
const tokenTrigger = totalTokens > this.config.tokenCompactionThreshold;
// 两个条件任一满足即触发
if (messageCountTrigger || tokenTrigger) {
await this.generateIncrementalSummary(existing, newMessages);
}消息数阈值:
- 定期触发,确保历史不会过长
- 简单可靠,易于预测
Token 阈值:
- 动态触发,处理大内容场景
- 避免单个大消息导致上下文爆炸
增量摘要生成
private async generateIncrementalSummary(
previousSummary: string | undefined,
newMessages: ChatMessage[]
): Promise<string> {
// 使用 fast 模型
const model = await Model.get("fast");
const userPrompt = previousSummary
? `基于以下现有摘要和新对话,生成更新后的摘要。
## 现有摘要
${previousSummary}
## 新对话
${formattedMessages}
请生成更新后的摘要:`
: `请为以下对话生成摘要:
${formattedMessages}
请生成摘要:`;
const result = streamText({
model,
messages: [
{
role: "system",
content: `你是一个摘要助手。要求:
1. 摘要应简洁但全面
2. 保留关键信息:主要话题、重要决策、用户偏好
3. 使用第三人称描述
4. 不超过 500 字`,
},
{ role: "user", content: userPrompt },
],
temperature: 0.3,
});
await result.consumeStream();
return await result.text;
}降级策略
如果 LLM 调用失败,返回简单的统计信息:
try {
const result = streamText({ model, messages });
return await result.text;
} catch (error) {
log.error("生成摘要失败");
// 降级为简单统计
return previousSummary
? `${previousSummary}\n[+${newMessages.length} 条新消息待摘要]`
: `[${newMessages.length} 条消息待摘要]`;
}LongTermMemory(长期记忆)
核心功能
长期记忆提供跨会话的持久化记忆:
- 语义检索:基于向量相似度的 RAG 检索
- 自动提取:每轮对话后自动分析并保存
- 智能合并:相似记忆自动合并,避免重复
- 访问统计:追踪记忆的使用频率
记忆分类
export type MemoryCategory =
| "user_preference" // 用户偏好(技术栈、格式、风格)
| "project_context" // 项目上下文
| "skill" // 技能相关知识
| "fact"; // 事实性信息Upsert 逻辑
核心思想:基于向量相似度决定新增或更新
async upsert(
data: {
category: MemoryCategory;
content: string;
importance?: number;
},
options: { similarityThreshold?: number } = {}
): Promise<{ action: "created" | "updated"; record: LongTermMemoryRecord }> {
const { similarityThreshold = 0.85 } = options;
// 1. 生成 query embedding
const queryVector = await EmbeddingService.embed(data.content);
// 2. 找到最相似的候选
const candidates = await chatDb.query.longTermMemories.findMany({
where: and(
isNotNull(longTermMemories.embedding),
eq(longTermMemories.category, data.category)
),
});
let best: { rec: LongTermMemoryRecord; sim: number } | null = null;
for (const rec of candidates) {
const sim = vectorUtils.cosineSimilarity(queryVector, rec.embedding);
if (!best || sim > best.sim) best = { rec, sim };
}
// 3. 判断是更新还是新增
if (best && best.sim >= similarityThreshold) {
// 更新:合并内容
const mergedContent = this.mergeContent(
best.rec.content,
data.content
);
const record = await this.update(best.rec.id, {
content: mergedContent,
importance: Math.max(best.rec.importance, data.importance || 5),
});
return { action: "updated", record };
}
// 新增
const record = await this.add({
category: data.category,
content: data.content,
importance: data.importance || 5,
});
return { action: "created", record };
}内容合并策略
private mergeContent(existing: string, incoming: string): string {
const a = existing.trim();
const b = incoming.trim();
if (!a) return b;
if (!b) return a;
if (a === b) return a;
if (a.includes(b)) return a; // 已包含
if (b.includes(a)) return b; // 新的更完整
const merged = `${a}\n${b}`;
// 避免无限增长
return merged.length > 2000 ? b : merged;
}语义检索
async search(
query: string,
options: {
limit?: number; // 返回数量(默认 5)
minSimilarity?: number; // 最小相似度(默认 0.6)
category?: MemoryCategory; // 分类过滤
} = {}
): Promise<MemorySearchResult[]> {
const { limit = 5, minSimilarity = 0.6, category } = options;
// 1. 生成 query embedding
const queryVector = await EmbeddingService.embed(query);
// 2. 获取所有有向量的记忆
const whereConditions = [isNotNull(longTermMemories.embedding)];
if (category) {
whereConditions.push(eq(longTermMemories.category, category));
}
const allMemories = await chatDb.query.longTermMemories.findMany({
where: and(...whereConditions),
});
// 3. 计算相似度
const results: MemorySearchResult[] = [];
for (const mem of allMemories) {
const docVector = vectorUtils.deserializeVector(mem.embedding);
const similarity = vectorUtils.cosineSimilarity(queryVector, docVector);
if (similarity >= minSimilarity) {
results.push({ ...mem, similarity });
}
}
// 4. 按相似度排序
results.sort((a, b) => b.similarity - a.similarity);
// 5. 更新访问统计
const topResults = results.slice(0, limit);
this.updateAccessStats(topResults.map((r) => r.id));
return topResults;
}MemoryExtractionService(自动记忆提取)
核心功能
自动从对话中提取有价值的信息:
- 完全自动化:无需用户手动维护
- 智能过滤:只提取与用户相关的长期信息
- 后台执行:不阻塞主流程
提取流程
extractFromExchange(userMessage: string, assistantMessage: string): void {
if (!this.config.enabled) return;
// 排队执行,避免并发
this.extractionQueue = this.extractionQueue
.then(() => this.doExtract(userMessage, assistantMessage))
.catch(() => {
log.error("提取失败");
});
}
private async doExtract(
userMessage: string,
assistantMessage: string
): Promise<void> {
// 1. 跳过过短的对话
if (userMessage.length < 20 && assistantMessage.length < 50) {
return;
}
// 2. 使用 fast 模型分析
const model = await Model.get("fast");
const prompt = `分析以下对话,提取值得长期记住的【用户相关信息】。
用户: ${userMessage.slice(0, 500)}
助手: ${assistantMessage.slice(0, 1000)}
只提取与【用户本人】相关的长期信息,例如:
- 偏好:技术栈偏好、格式偏好、写作/回复风格
- 习惯:常用流程、固定约定、常见选择
以 JSON 数组格式返回,每条记忆包含:
- category: "user_preference"
- content: 内容 (简洁但完整)
- importance: 重要度 1-10
如果没有值得记忆的内容,返回空数组 []`;
const result = streamText({
model,
messages: [{ role: "user", content: prompt }],
temperature: 0.3,
});
await result.consumeStream();
const text = await result.text;
// 3. 解析 JSON
const jsonMatch = text.match(/\[[\s\S]*\]/);
if (!jsonMatch) return;
const memories: RawMemory[] = JSON.parse(jsonMatch[0]);
// 4. 处理每条记忆
for (const mem of memories) {
await this.processMemory(mem);
}
}记忆过滤
private async processMemory(mem: RawMemory): Promise<void> {
// 1. 验证格式
if (!mem.category || !mem.content || typeof mem.importance !== "number") {
return;
}
// 2. 过滤低重要度
if (mem.importance < this.config.minImportance) {
return;
}
// 3. 验证分类
const validCategories: MemoryCategory[] = ["user_preference"];
if (!validCategories.includes(mem.category as MemoryCategory)) {
return;
}
// 4. 保存到长期记忆
try {
const result = await LongTermMemory.upsert(
{
category: mem.category as MemoryCategory,
content: mem.content,
importance: mem.importance,
},
{ similarityThreshold: this.config.updateSimilarityThreshold }
);
log.info("保存记忆", {
action: result.action,
content: result.record.content.slice(0, 50),
});
} catch (err) {
log.error("保存失败");
}
}数据流
摘要更新流程
记忆提取流程
配置选项
ThreadSummary 配置
interface SummaryConfig {
/** 触发摘要的消息数阈值 */
messageThreshold: number;
/** 摘要的最大 Token 数 */
maxSummaryTokens: number;
/** 使用的模型槽位 */
modelSlot: "fast" | "primary";
/** Token 自动压缩阈值 */
tokenCompactionThreshold: number;
/** 是否启用 Token 阈值触发 */
enableTokenBasedCompaction: boolean;
}
const DEFAULT_CONFIG: SummaryConfig = {
messageThreshold: 10,
maxSummaryTokens: 1000,
modelSlot: "fast",
tokenCompactionThreshold: 3000,
enableTokenBasedCompaction: true,
};MemoryExtraction 配置
interface ExtractionConfig {
/** 是否启用自动提取 */
enabled: boolean;
/** 最小重要度阈值 */
minImportance: number;
/** 更新相似度阈值 */
updateSimilarityThreshold: number;
}
const DEFAULT_CONFIG: ExtractionConfig = {
enabled: true,
minImportance: 5,
updateSimilarityThreshold: 0.85,
};使用示例
手动更新摘要
import { ThreadSummary } from "./context";
// 检查并更新(如果需要)
await ThreadSummary.updateIfNeeded(threadId);
// 强制更新
await ThreadSummary.forceUpdate(threadId);
// 获取摘要
const summary = await ThreadSummary.get(threadId);手动管理记忆
import { LongTermMemory } from "./context";
// 添加记忆
const memory = await LongTermMemory.add({
category: "user_preference",
content: "用户喜欢使用 TypeScript",
importance: 7,
});
// 语义搜索
const results = await LongTermMemory.search("TypeScript", {
limit: 3,
minSimilarity: 0.6,
category: "user_preference",
});
// 列出所有记忆
const list = await LongTermMemory.list({
limit: 20,
category: "user_preference",
});自动提取记忆
import { MemoryExtractionService } from "./context";
// 对话结束后自动触发(通常在 ChatHandler 中)
MemoryExtractionService.extractFromExchange(
userMessage,
assistantMessage
);
// 更新配置
MemoryExtractionService.setConfig({
enabled: true,
minImportance: 6,
});性能优化
异步非阻塞
// 记忆检索超时
const memories = await Promise.race([
LongTermMemory.search(query),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("timeout")), 500)
),
]);
// 摘要更新异步
ThreadSummary.updateIfNeeded(threadId).catch(() => {});
// 记忆提取排队
this.extractionQueue = this.extractionQueue.then(() => this.doExtract());向量检索优化
当前使用全内存计算,适用于个人场景:
// 获取所有有向量的记忆
const allMemories = await chatDb.query.longTermMemories.findMany({
where: isNotNull(longTermMemories.embedding),
});
// 内存中计算相似度
for (const mem of allMemories) {
const similarity = vectorUtils.cosineSimilarity(queryVector, mem.embedding);
if (similarity >= minSimilarity) {
results.push({ ...mem, similarity });
}
}未来优化:
- 使用 SQLite 扩展(sqlite-vss)
- 分层检索(关键词 + 向量)
- 缓存热门查询
错误处理
摘要生成失败
try {
const result = streamText({ model, messages });
return await result.text;
} catch (error) {
log.error("生成摘要失败");
// 降级为简单统计
return previousSummary
? `${previousSummary}\n[+${newMessages.length} 条新消息]`
: `[${newMessages.length} 条消息]`;
}Embedding 失败
try {
await this.ensureModelReady();
const vector = await EmbeddingService.embed(content);
embeddingStr = vectorUtils.serializeVector(vector);
} catch (error) {
log.warn("生成嵌入失败");
// 允许无向量存储,但无法被检索
}