Files
metona-ollama-desktop/js/rag.js
T

203 lines
6.2 KiB
JavaScript

/**
* RAG - 检索增强生成管线
*
* 查询 → 嵌入 → 向量检索 → 构造上下文 → 注入 prompt
*/
import { state, KEYS } from './state.js';
import { VectorStore } from './vector-store.js';
import { chunkText, createChunkMetadata } from './document-processor.js';
import { showToast } from './components/toast.js';
let vectorStore = null;
/**
* 初始化向量存储
*/
export async function initVectorStore() {
if (vectorStore) return vectorStore;
vectorStore = new VectorStore();
await vectorStore.init();
return vectorStore;
}
/**
* 获取向量存储实例
*/
export function getVectorStore() {
return vectorStore;
}
/**
* 使用 Ollama 生成文本嵌入向量
* @param {string} text
* @param {string} model - 嵌入模型名称
* @returns {number[]}
*/
export async function embedText(text, model) {
const api = state.get(KEYS.API);
if (!api) throw new Error('Ollama API 未连接');
const result = await api.embed(model, text);
// /api/embed 返回 { embedding: [...] } 或 { embeddings: [[...]] }
if (result.embedding) return result.embedding;
if (result.embeddings && result.embeddings[0]) return result.embeddings[0];
throw new Error('嵌入向量返回格式异常');
}
/**
* 批量嵌入(分批处理,避免超时)
* @param {string[]} texts
* @param {string} model
* @param {function} onProgress - (done, total) => void
* @returns {number[][]}
*/
export async function embedBatch(texts, model, onProgress) {
const results = [];
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;
}
/**
* 向知识库集合添加文档
* @param {string} colId - 集合 ID
* @param {string} filename - 文件名
* @param {string} content - 文件内容
* @param {string} embedModel - 嵌入模型
* @param {function} onProgress
*/
export async function addDocumentToKB(colId, filename, content, embedModel, onProgress) {
const vs = await initVectorStore();
// 1. 分块
const textChunks = chunkText(content);
if (textChunks.length === 0) throw new Error('文档内容为空,无法处理');
// 2. 生成文档 ID
const docId = `doc_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
// 3. 生成嵌入向量
if (onProgress) onProgress(0, textChunks.length, '正在生成向量...');
const embeddings = await embedBatch(textChunks, embedModel, (done, total) => {
if (onProgress) onProgress(done, total, '正在生成向量...');
});
// 4. 构造向量数据
const vectors = textChunks.map((chunk, i) => {
const meta = createChunkMetadata(chunk, i, docId, filename);
return {
...meta,
collectionId: colId,
embedding: embeddings[i]
};
});
// 5. 存储
if (onProgress) onProgress(textChunks.length, textChunks.length, '正在保存...');
await vs.addVectors(vectors);
// 6. 更新集合统计
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, docId) {
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) {
const vs = await initVectorStore();
const vectors = await vs.getVectorsByCollection(colId);
const docMap = new Map();
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());
}
/**
* RAG 检索:查询 → 嵌入 → 相似度搜索 → 返回上下文文本
* @param {string} query - 用户查询
* @param {string} colId - 集合 ID
* @param {number} topK - 返回最相关的 K 条
* @returns {{ context: string, results: Array }}
*/
export async function retrieveContext(query, colId, topK = 5) {
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: [] };
// 构造上下文
const contextParts = results.map((r, i) =>
`[来源 ${i + 1}: ${r.filename}]\n${r.text}`
);
const context = contextParts.join('\n\n---\n\n');
return { context, results };
}
/**
* 构造 RAG 增强的系统提示词
*/
export function buildRagSystemPrompt(context, originalSystemPrompt = '') {
const ragPrompt = `你是一个知识库问答助手。以下是检索到的相关文档片段,请基于这些内容回答用户的问题。如果检索到的内容无法回答问题,请如实说明。
=== 检索到的相关内容 ===
${context}
=== 内容结束 ===
请基于以上内容回答用户的问题。引用时请注明来源编号(如"来源 1")。`;
if (originalSystemPrompt) {
return originalSystemPrompt + '\n\n' + ragPrompt;
}
return ragPrompt;
}