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 || [],
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
/**
|
||||
* RAG - 检索增强生成管线
|
||||
*/
|
||||
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
import { VectorStore } from './vector-store.js';
|
||||
import { chunkText, createChunkMetadata } from './document-processor.js';
|
||||
import { logRAG, logDebug, logError } from './log-service.js';
|
||||
import type { OllamaAPI, SearchResult, VectorItem } from '../types.js';
|
||||
|
||||
let vectorStore: VectorStore | null = null;
|
||||
|
||||
export async function initVectorStore(): Promise<VectorStore> {
|
||||
if (vectorStore) return vectorStore;
|
||||
vectorStore = new VectorStore();
|
||||
await vectorStore.init();
|
||||
logRAG('向量存储已初始化');
|
||||
return vectorStore;
|
||||
}
|
||||
|
||||
export function getVectorStore(): VectorStore | null {
|
||||
return vectorStore;
|
||||
}
|
||||
|
||||
export async function embedText(text: string, model: string): Promise<number[]> {
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
if (!api) throw new Error('Ollama API 未连接');
|
||||
|
||||
const result = await api.embed(model, text);
|
||||
if (result.embedding) return result.embedding;
|
||||
if (result.embeddings && result.embeddings[0]) return result.embeddings[0];
|
||||
throw new Error('嵌入向量返回格式异常');
|
||||
}
|
||||
|
||||
export async function embedBatch(texts: string[], model: string, onProgress?: (done: number, total: number) => void): Promise<number[][]> {
|
||||
const results: number[][] = [];
|
||||
const batchSize = 5;
|
||||
logDebug(`嵌入批次`, `${texts.length} 个文本, 模型: ${model}`);
|
||||
for (let i = 0; i < texts.length; i += batchSize) {
|
||||
const batch = texts.slice(i, i + batchSize);
|
||||
const embeddings = await Promise.all(batch.map(t => embedText(t, model)));
|
||||
results.push(...embeddings);
|
||||
if (onProgress) onProgress(Math.min(i + batchSize, texts.length), texts.length);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function addDocumentToKB(
|
||||
colId: string, filename: string, content: string, embedModel: string,
|
||||
onProgress?: (done: number, total: number, msg: string) => void
|
||||
): Promise<{ docId: string; chunkCount: number }> {
|
||||
const vs = await initVectorStore();
|
||||
|
||||
const textChunks = chunkText(content);
|
||||
if (textChunks.length === 0) throw new Error('文档内容为空,无法处理');
|
||||
logRAG(`分块完成: ${filename}`, `${textChunks.length} 个分块`);
|
||||
|
||||
const docId = `doc_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
||||
|
||||
if (onProgress) onProgress(0, textChunks.length, '正在生成向量...');
|
||||
const embeddings = await embedBatch(textChunks, embedModel, (done, total) => {
|
||||
if (onProgress) onProgress(done, total, '正在生成向量...');
|
||||
});
|
||||
|
||||
const vectors: VectorItem[] = textChunks.map((chunk, i) => {
|
||||
const meta = createChunkMetadata(chunk, i, docId, filename);
|
||||
return {
|
||||
...meta,
|
||||
collectionId: colId,
|
||||
embedding: embeddings[i]
|
||||
};
|
||||
});
|
||||
|
||||
if (onProgress) onProgress(textChunks.length, textChunks.length, '正在保存...');
|
||||
await vs.addVectors(vectors);
|
||||
|
||||
const col = await vs.getCollection(colId);
|
||||
if (col) {
|
||||
col.docCount = (col.docCount || 0) + 1;
|
||||
col.chunkCount = (col.chunkCount || 0) + textChunks.length;
|
||||
col.embeddingModel = embedModel;
|
||||
await vs.updateCollection(col);
|
||||
}
|
||||
|
||||
return { docId, chunkCount: textChunks.length };
|
||||
}
|
||||
|
||||
export async function removeDocumentFromKB(colId: string, docId: string): Promise<void> {
|
||||
const vs = await initVectorStore();
|
||||
const allVectors = await vs.getVectorsByCollection(colId);
|
||||
const docVectors = allVectors.filter(v => v.docId === docId);
|
||||
logRAG(`删除文档`, `${docId} (${docVectors.length} 个分块)`);
|
||||
|
||||
await vs.deleteVectorsByDocument(colId, docId);
|
||||
|
||||
const col = await vs.getCollection(colId);
|
||||
if (col) {
|
||||
col.chunkCount = Math.max(0, (col.chunkCount || 0) - docVectors.length);
|
||||
col.docCount = Math.max(0, (col.docCount || 0) - 1);
|
||||
await vs.updateCollection(col);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDocumentsInCollection(colId: string): Promise<Array<{ docId: string; filename: string; chunkCount: number }>> {
|
||||
const vs = await initVectorStore();
|
||||
const vectors = await vs.getVectorsByCollection(colId);
|
||||
const docMap = new Map<string, { docId: string; filename: string; chunkCount: number }>();
|
||||
for (const v of vectors) {
|
||||
if (!docMap.has(v.docId)) {
|
||||
docMap.set(v.docId, { docId: v.docId, filename: v.filename, chunkCount: 0 });
|
||||
}
|
||||
docMap.get(v.docId)!.chunkCount++;
|
||||
}
|
||||
return Array.from(docMap.values());
|
||||
}
|
||||
|
||||
export async function retrieveContext(query: string, colId: string, topK = 5): Promise<{ context: string; results: SearchResult[] }> {
|
||||
const vs = await initVectorStore();
|
||||
const col = await vs.getCollection(colId);
|
||||
if (!col) throw new Error('知识库集合不存在');
|
||||
|
||||
const embedModel = col.embeddingModel;
|
||||
if (!embedModel) throw new Error('未配置嵌入模型');
|
||||
|
||||
logRAG('开始检索', col.name);
|
||||
const queryEmbedding = await embedText(query, embedModel);
|
||||
const results = await vs.search(colId, queryEmbedding, topK);
|
||||
if (results.length === 0) return { context: '', results: [] };
|
||||
logRAG(`检索完成`, `${results.length} 个相关片段`);
|
||||
|
||||
const contextParts = results.map((r, i) =>
|
||||
`[来源 ${i + 1}: ${r.filename}]\n${r.text}`
|
||||
);
|
||||
const context = contextParts.join('\n\n---\n\n');
|
||||
|
||||
return { context, results };
|
||||
}
|
||||
|
||||
export function buildRagSystemPrompt(context: string, originalSystemPrompt = ''): string {
|
||||
const ragPrompt = `你是一个知识库问答助手。以下是检索到的相关文档片段,请基于这些内容回答用户的问题。如果检索到的内容无法回答问题,请如实说明。
|
||||
|
||||
=== 检索到的相关内容 ===
|
||||
${context}
|
||||
=== 内容结束 ===
|
||||
|
||||
请基于以上内容回答用户的问题。引用时请注明来源编号(如"来源 1")。`;
|
||||
|
||||
if (originalSystemPrompt) {
|
||||
return originalSystemPrompt + '\n\n' + ragPrompt;
|
||||
}
|
||||
return ragPrompt;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* VectorMemory - 记忆向量存储与检索
|
||||
* 替代原 rag.ts,专为记忆系统提供向量能力
|
||||
*/
|
||||
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
import { VectorStore } from './vector-store.js';
|
||||
import { logMemory, logDebug, logError } from './log-service.js';
|
||||
import type { MemoryEntry, OllamaAPI, VectorItem, SearchResult } from '../types.js';
|
||||
|
||||
const MEMORY_COLLECTION_NAME = '记忆向量索引';
|
||||
|
||||
let vectorStore: VectorStore | null = null;
|
||||
let memoryCollectionId: string | null = null;
|
||||
|
||||
// ── 初始化 ──
|
||||
|
||||
export async function initMemoryVectorStore(): Promise<VectorStore> {
|
||||
if (vectorStore) return vectorStore;
|
||||
vectorStore = new VectorStore();
|
||||
await vectorStore.init();
|
||||
logDebug('记忆向量存储已初始化');
|
||||
return vectorStore;
|
||||
}
|
||||
|
||||
export function getMemoryVectorStore(): VectorStore | null {
|
||||
return vectorStore;
|
||||
}
|
||||
|
||||
// ── 集合管理 ──
|
||||
|
||||
export async function getOrCreateMemoryCollection(embedModel: string): Promise<{ id: string; isNew: boolean }> {
|
||||
const vs = await initMemoryVectorStore();
|
||||
|
||||
// 如果已有缓存的集合ID,验证它是否存在
|
||||
if (memoryCollectionId) {
|
||||
const existing = await vs.getCollection(memoryCollectionId);
|
||||
if (existing) {
|
||||
if (existing.embeddingModel !== embedModel) {
|
||||
existing.embeddingModel = embedModel;
|
||||
await vs.updateCollection(existing);
|
||||
}
|
||||
return { id: memoryCollectionId, isNew: false };
|
||||
}
|
||||
memoryCollectionId = null;
|
||||
}
|
||||
|
||||
// 搜索现有集合
|
||||
const collections = await vs.getCollections();
|
||||
const found = collections.find(c => c.name === MEMORY_COLLECTION_NAME);
|
||||
if (found) {
|
||||
memoryCollectionId = found.id;
|
||||
if (found.embeddingModel !== embedModel) {
|
||||
found.embeddingModel = embedModel;
|
||||
await vs.updateCollection(found);
|
||||
}
|
||||
return { id: found.id, isNew: false };
|
||||
}
|
||||
|
||||
// 创建新集合
|
||||
const col = await vs.createCollection(MEMORY_COLLECTION_NAME, embedModel);
|
||||
memoryCollectionId = col.id;
|
||||
logMemory('创建记忆向量集合', col.id);
|
||||
return { id: col.id, isNew: true };
|
||||
}
|
||||
|
||||
export function getMemoryCollectionId(): string | null {
|
||||
return memoryCollectionId;
|
||||
}
|
||||
|
||||
export function setMemoryCollectionId(id: string | null): void {
|
||||
memoryCollectionId = id;
|
||||
}
|
||||
|
||||
// ── 文本嵌入 ──
|
||||
|
||||
export async function embedText(text: string, model: string): Promise<number[]> {
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
if (!api) throw new Error('Ollama API 未连接');
|
||||
|
||||
const result = await api.embed(model, text);
|
||||
if (result.embedding) return result.embedding;
|
||||
if (result.embeddings && result.embeddings[0]) return result.embeddings[0];
|
||||
throw new Error('嵌入向量返回格式异常');
|
||||
}
|
||||
|
||||
// ── 为记忆条目生成向量 ──
|
||||
|
||||
export async function embedMemoryEntry(entry: MemoryEntry, model: string): Promise<number[]> {
|
||||
const text = `[${entry.type}] ${entry.content} ${entry.tags.join(' ')}`;
|
||||
return embedText(text, model);
|
||||
}
|
||||
|
||||
// ── 添加记忆向量 ──
|
||||
|
||||
export async function addMemoryVector(entry: MemoryEntry, embedding: number[], colId: string): Promise<void> {
|
||||
const vs = await initMemoryVectorStore();
|
||||
const item: VectorItem = {
|
||||
id: `vec_${entry.id}`,
|
||||
collectionId: colId,
|
||||
docId: entry.id,
|
||||
filename: entry.type,
|
||||
chunkIndex: 0,
|
||||
text: entry.content,
|
||||
charCount: entry.content.length,
|
||||
embedding
|
||||
};
|
||||
await vs.addVectors([item]);
|
||||
}
|
||||
|
||||
// ── 更新记忆向量 ──
|
||||
|
||||
export async function updateMemoryVector(entry: MemoryEntry, embedding: number[], colId: string): Promise<void> {
|
||||
// 先删除旧的,再添加新的
|
||||
await deleteMemoryVector(entry.id, colId);
|
||||
await addMemoryVector(entry, embedding, colId);
|
||||
}
|
||||
|
||||
// ── 删除记忆向量 ──
|
||||
|
||||
export async function deleteMemoryVector(entryId: string, colId: string): Promise<void> {
|
||||
const vs = await initMemoryVectorStore();
|
||||
await vs.deleteVectorsByDocument(colId, entryId);
|
||||
}
|
||||
|
||||
// ── 向量搜索记忆 ──
|
||||
|
||||
export async function searchMemoriesByVector(query: string, colId: string, topK = 8): Promise<SearchResult[]> {
|
||||
const vs = await initMemoryVectorStore();
|
||||
const col = await vs.getCollection(colId);
|
||||
if (!col || !col.embeddingModel) return [];
|
||||
|
||||
const queryEmbedding = await embedText(query, col.embeddingModel);
|
||||
const results = await vs.search(colId, queryEmbedding, topK);
|
||||
return results;
|
||||
}
|
||||
|
||||
// ── 重新索引所有记忆 ──
|
||||
|
||||
export async function reindexAllMemories(
|
||||
entries: MemoryEntry[],
|
||||
embedModel: string,
|
||||
colId: string,
|
||||
onProgress?: (done: number, total: number) => void
|
||||
): Promise<void> {
|
||||
const vs = await initMemoryVectorStore();
|
||||
|
||||
// 清空集合中的旧向量
|
||||
const oldVectors = await vs.getVectorsByCollection(colId);
|
||||
if (oldVectors.length > 0) {
|
||||
await vs.deleteCollection(colId);
|
||||
const col = await vs.createCollection(MEMORY_COLLECTION_NAME, embedModel);
|
||||
memoryCollectionId = col.id;
|
||||
colId = col.id;
|
||||
}
|
||||
|
||||
logMemory(`开始重新索引: ${entries.length} 条记忆`);
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
try {
|
||||
const embedding = await embedMemoryEntry(entry, embedModel);
|
||||
await addMemoryVector(entry, embedding, colId);
|
||||
} catch (err) {
|
||||
logError(`索引记忆失败: ${entry.id}`, (err as Error).message);
|
||||
}
|
||||
if (onProgress) onProgress(i + 1, entries.length);
|
||||
}
|
||||
|
||||
logMemory(`重新索引完成: ${entries.length} 条`);
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export class VectorStore {
|
||||
|
||||
async createCollection(name: string, embeddingModel = ''): Promise<VectorCollection> {
|
||||
const col: VectorCollection = {
|
||||
id: `kb_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
||||
id: `mem_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
||||
name, embeddingModel,
|
||||
docCount: 0, chunkCount: 0,
|
||||
createdAt: Date.now(), updatedAt: Date.now()
|
||||
|
||||
Reference in New Issue
Block a user