Files
metona-ollama-desktop/src/renderer/services/rag.ts
T
Metona Fix af70054875 feat: 全量日志接入 + 日志服务重构(非阻塞)
log-service 重构:
- DOM 渲染改用 requestAnimationFrame 批量队列
- 全程 try-catch,任何异常都不阻塞主流程
- 新增 logConnection/logModel/logSession/logMemory/logRAG/logSetting/logIPC/logInit 便捷方法

全量接入(12 个文件):
- header.ts: 连接状态变化(connected/disconnected/error)
- model-bar.ts: 模型列表加载、模型切换、能力检测结果
- settings-modal.ts: 所有设置变更(系统提示词/上下文/温度/工具调用/命令执行/记忆)、显存释放、导出/导入
- tool-confirm-modal.ts: 工具确认弹窗、确认/取消操作
- memory-manager.ts: 记忆初始化、新增/删除、自动提取
- tool-registry.ts: 工具执行异常
- rag.ts: 文档分块、向量检索
- kb-modal.ts: 知识库创建、文档上传、批量上传
- history-modal.ts: 会话加载、会话删除
- main.ts: 应用初始化各阶段、桌面集成、菜单/托盘操作、新建会话
- input-area.ts: 消息发送、流式完成/错误、文件/图片添加、Agent Loop 错误
2026-04-06 19:23:03 +08:00

149 lines
5.4 KiB
TypeScript

/**
* 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();
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;
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);
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('未配置嵌入模型');
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;
}