feat: 将记忆系统和知识库合并改造为长效向量记忆系统
- 移除知识库(KB/RAG)功能,删除 kb-modal.ts、rag.ts、memory-panel.ts - 新增 vector-memory.ts:记忆向量存储与检索引擎 - 新增 memory-modal.ts:大模态框布局的记忆管理面板 - 简化记忆分类为3类:fact(事实)、preference(偏好)、rule(规则) - 嵌入模型配置移至设置面板 - 无嵌入模型时自动降级为关键词搜索模式 - 向量搜索支持语义相似度检索 - 嵌入模型变化时自动重新索引所有记忆
This commit is contained in:
@@ -1,35 +1,41 @@
|
||||
/**
|
||||
* MemoryManager - Agent 记忆系统
|
||||
* MemoryManager - Agent 记忆系统(长效向量记忆)
|
||||
* 自动提取 → 存储 → 检索 → 注入上下文
|
||||
*
|
||||
* 记忆类型:
|
||||
* - fact: 用户告诉我的事实(项目信息、个人背景等)
|
||||
* - preference: 用户偏好(语言、风格、格式习惯)
|
||||
* - rule: 应遵守的规则(命名规范、输出格式要求)
|
||||
* - episode: 重要事件(完成了什么、得到了什么结论)
|
||||
*/
|
||||
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
import { generateId } from '../utils/utils.js';
|
||||
import {
|
||||
initMemoryVectorStore, getOrCreateMemoryCollection,
|
||||
embedMemoryEntry, addMemoryVector, updateMemoryVector,
|
||||
deleteMemoryVector, searchMemoriesByVector, reindexAllMemories,
|
||||
getMemoryCollectionId
|
||||
} from './vector-memory.js';
|
||||
import { logMemory, logDebug, logWarn } from './log-service.js';
|
||||
import type { MemoryEntry, MemorySearchResult, MemoryExtractionResult, ChatDB, OllamaAPI } from '../types.js';
|
||||
|
||||
const TYPE_ICONS: Record<string, string> = {
|
||||
fact: '📌',
|
||||
preference: '⚙️',
|
||||
rule: '📏',
|
||||
episode: '📝'
|
||||
rule: '📏'
|
||||
};
|
||||
|
||||
const TYPE_NAMES: Record<string, string> = {
|
||||
fact: '事实',
|
||||
preference: '偏好',
|
||||
rule: '规则',
|
||||
episode: '事件'
|
||||
rule: '规则'
|
||||
};
|
||||
|
||||
let memoryCache: MemoryEntry[] = [];
|
||||
let memoryEnabled = true;
|
||||
let embeddingModel = '';
|
||||
|
||||
// ── 初始化 ──
|
||||
|
||||
export async function initMemoryManager(): Promise<void> {
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
@@ -38,12 +44,70 @@ export async function initMemoryManager(): Promise<void> {
|
||||
memoryEnabled = await db.getSetting('memoryEnabled', true);
|
||||
state.set('memoryEnabled', memoryEnabled);
|
||||
|
||||
// 加载嵌入模型设置
|
||||
embeddingModel = await db.getSetting('embeddingModel', '');
|
||||
state.set('embeddingModel', embeddingModel);
|
||||
|
||||
memoryCache = await db.getAllMemories();
|
||||
state.set('memoryEntries', memoryCache);
|
||||
|
||||
logMemory(`初始化完成, 加载 ${memoryCache.length} 条`);
|
||||
// 如果有嵌入模型,初始化向量存储
|
||||
if (embeddingModel) {
|
||||
try {
|
||||
await initMemoryVectorStore();
|
||||
logMemory('向量存储已初始化');
|
||||
} catch (err) {
|
||||
logWarn('向量存储初始化失败', (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
logMemory(`初始化完成, 加载 ${memoryCache.length} 条${embeddingModel ? ', 向量记忆已启用' : ', 仅关键词模式'}`);
|
||||
}
|
||||
|
||||
// ── 嵌入模型管理 ──
|
||||
|
||||
export function getEmbeddingModel(): string {
|
||||
return embeddingModel;
|
||||
}
|
||||
|
||||
export async function setEmbeddingModel(model: string): Promise<void> {
|
||||
const oldModel = embeddingModel;
|
||||
embeddingModel = model;
|
||||
state.set('embeddingModel', model);
|
||||
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await db.saveSetting('embeddingModel', model);
|
||||
|
||||
if (model && model !== oldModel) {
|
||||
// 嵌入模型变化,重新索引所有记忆
|
||||
logMemory(`嵌入模型变更: ${oldModel || '(无)'} → ${model}`);
|
||||
await reindexMemories();
|
||||
}
|
||||
}
|
||||
|
||||
export function isVectorMemoryEnabled(): boolean {
|
||||
return !!embeddingModel;
|
||||
}
|
||||
|
||||
// ── 重新索引所有记忆 ──
|
||||
|
||||
export async function reindexMemories(): Promise<void> {
|
||||
if (!embeddingModel || memoryCache.length === 0) return;
|
||||
|
||||
try {
|
||||
await initMemoryVectorStore();
|
||||
const { id: colId } = await getOrCreateMemoryCollection(embeddingModel);
|
||||
await reindexAllMemories(memoryCache, embeddingModel, colId, (done, total) => {
|
||||
logMemory(`重新索引进度: ${done}/${total}`);
|
||||
});
|
||||
logMemory('向量索引重建完成');
|
||||
} catch (err) {
|
||||
logWarn('向量索引重建失败', (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 基础管理 ──
|
||||
|
||||
export function isMemoryEnabled(): boolean {
|
||||
return memoryEnabled;
|
||||
}
|
||||
@@ -67,11 +131,52 @@ export function getTypeName(type: string): string {
|
||||
return TYPE_NAMES[type] || type;
|
||||
}
|
||||
|
||||
// ── 记忆检索(关键词 + 标签 + 重要性加权)──
|
||||
// ── 记忆检索(向量优先,关键词补充)──
|
||||
|
||||
export function searchMemories(query: string, limit = 8): MemorySearchResult[] {
|
||||
if (!memoryEnabled || memoryCache.length === 0) return [];
|
||||
|
||||
// 始终执行关键词搜索(即时结果)
|
||||
const keywordResults = searchMemoriesByKeyword(query, limit);
|
||||
|
||||
// 如果启用了向量记忆,异步触发向量搜索(结果在下次调用时更新)
|
||||
if (embeddingModel) {
|
||||
triggerVectorSearch(query, limit);
|
||||
}
|
||||
|
||||
return keywordResults;
|
||||
}
|
||||
|
||||
// 向量搜索异步缓存
|
||||
const vectorSearchCache = new Map<string, MemorySearchResult[]>();
|
||||
let vectorSearchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function triggerVectorSearch(query: string, limit: number): void {
|
||||
if (vectorSearchTimer) clearTimeout(vectorSearchTimer);
|
||||
vectorSearchTimer = setTimeout(async () => {
|
||||
try {
|
||||
const colId = getMemoryCollectionId();
|
||||
if (!colId) return;
|
||||
const results = await searchMemoriesByVector(query, colId, limit);
|
||||
const memoryResults: MemorySearchResult[] = results.map(r => {
|
||||
const entry = memoryCache.find(e => e.id === r.docId);
|
||||
if (!entry) return null;
|
||||
return { ...entry, score: r.score };
|
||||
}).filter(Boolean) as MemorySearchResult[];
|
||||
vectorSearchCache.set(query, memoryResults);
|
||||
} catch (err) {
|
||||
logWarn('向量搜索失败', (err as Error).message);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 导出:获取向量搜索结果(供 UI 使用)
|
||||
export function getVectorSearchResults(query: string): MemorySearchResult[] {
|
||||
return vectorSearchCache.get(query) || [];
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
function searchMemoriesByKeyword(query: string, limit: number): MemorySearchResult[] {
|
||||
const queryLower = query.toLowerCase();
|
||||
const queryWords = queryLower.split(/[\s,,。!?、;:""''()\[\]{}<>`~@#$%^&*+=|\\/.]+/).filter(w => w.length > 1);
|
||||
|
||||
@@ -155,6 +260,20 @@ export async function addMemory(data: {
|
||||
if (db) await db.saveMemory(entry);
|
||||
logMemory(`新增: ${data.type}`, entry.content.slice(0, 60));
|
||||
|
||||
// 向量存储
|
||||
if (embeddingModel) {
|
||||
try {
|
||||
await initMemoryVectorStore();
|
||||
const colId = getMemoryCollectionId() || (await getOrCreateMemoryCollection(embeddingModel)).id;
|
||||
const embedding = await embedMemoryEntry(entry, embeddingModel);
|
||||
entry.embedding = embedding;
|
||||
await addMemoryVector(entry, embedding, colId);
|
||||
if (db) await db.saveMemory(entry); // 保存包含 embedding 的版本
|
||||
} catch (err) {
|
||||
logWarn('向量存储失败', (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -169,6 +288,21 @@ export async function updateMemory(id: string, updates: Partial<MemoryEntry>): P
|
||||
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await db.saveMemory(memoryCache[idx]);
|
||||
|
||||
// 更新向量
|
||||
if (embeddingModel && (updates.content || updates.type || updates.tags)) {
|
||||
try {
|
||||
const colId = getMemoryCollectionId();
|
||||
if (colId) {
|
||||
const embedding = await embedMemoryEntry(memoryCache[idx], embeddingModel);
|
||||
memoryCache[idx].embedding = embedding;
|
||||
await updateMemoryVector(memoryCache[idx], embedding, colId);
|
||||
if (db) await db.saveMemory(memoryCache[idx]);
|
||||
}
|
||||
} catch (err) {
|
||||
logWarn('向量更新失败', (err as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 记忆删除 ──
|
||||
@@ -179,6 +313,17 @@ export async function deleteMemory(id: string): Promise<void> {
|
||||
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await db.deleteMemory(id);
|
||||
|
||||
// 删除向量
|
||||
if (embeddingModel) {
|
||||
try {
|
||||
const colId = getMemoryCollectionId();
|
||||
if (colId) await deleteMemoryVector(id, colId);
|
||||
} catch (err) {
|
||||
logWarn('向量删除失败', (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
logMemory(`删除`, id);
|
||||
}
|
||||
|
||||
@@ -190,6 +335,25 @@ export async function clearAllMemories(): Promise<void> {
|
||||
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await db.clearAllMemories();
|
||||
|
||||
// 清空向量集合
|
||||
if (embeddingModel) {
|
||||
try {
|
||||
await initMemoryVectorStore();
|
||||
const { id: colId } = await getOrCreateMemoryCollection(embeddingModel);
|
||||
const { getMemoryVectorStore } = await import('./vector-memory.js');
|
||||
const vs = getMemoryVectorStore();
|
||||
if (vs) {
|
||||
await vs.deleteCollection(colId);
|
||||
// 重新创建空集合
|
||||
const { setMemoryCollectionId } = await import('./vector-memory.js');
|
||||
setMemoryCollectionId(null);
|
||||
await getOrCreateMemoryCollection(embeddingModel);
|
||||
}
|
||||
} catch (err) {
|
||||
logWarn('向量集合清空失败', (err as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 标记记忆被使用 ──
|
||||
@@ -240,13 +404,12 @@ export async function extractMemoriesFromConversation(
|
||||
sessionTitle?: string
|
||||
): Promise<number> {
|
||||
if (!memoryEnabled) return 0;
|
||||
if (messages.length < 3) return 0; // 至少一轮完整对话
|
||||
if (messages.length < 3) return 0;
|
||||
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
const model = state.get<string>('_defaultModel', '');
|
||||
if (!api || !model) return 0;
|
||||
|
||||
// 取最近 20 条消息作为提取素材
|
||||
const recentMessages = messages.slice(-20);
|
||||
const conversationText = recentMessages
|
||||
.filter(m => m.role === 'user' || m.role === 'assistant')
|
||||
@@ -277,7 +440,6 @@ ${conversationText.slice(0, 4000)}
|
||||
- fact: 关于用户的事实(项目、身份、背景、习惯)
|
||||
- preference: 用户偏好(语言风格、输出格式、技术栈偏好)
|
||||
- rule: 用户要求遵守的规则(编码规范、输出要求)
|
||||
- episode: 重要事件(完成的任务、达成的结论)
|
||||
|
||||
提取规则:
|
||||
1. 只提取真正有价值、值得跨会话记住的信息
|
||||
@@ -308,8 +470,9 @@ ${conversationText.slice(0, 4000)}
|
||||
const currentSession = state.get(KEYS.CURRENT_SESSION);
|
||||
for (const entry of parsed.entries) {
|
||||
if (!entry.content || entry.content.length < 5) continue;
|
||||
const validType = (['fact', 'preference', 'rule'] as const).includes(entry.type as any) ? entry.type : 'fact';
|
||||
await addMemory({
|
||||
type: entry.type || 'fact',
|
||||
type: validType as any,
|
||||
content: entry.content,
|
||||
importance: Math.min(10, Math.max(1, entry.importance || 5)),
|
||||
tags: entry.tags || [],
|
||||
|
||||
Reference in New Issue
Block a user