203 lines
6.2 KiB
JavaScript
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;
|
|
}
|