
1. 项目概述当聊天窗口遇见你的知识库最近在折腾一个挺有意思的东西我把它叫做“前端RAG”。简单来说就是在一个聊天界面里让AI不仅能跟你闲聊还能实时地从你指定的文档比如公司内部Wiki、产品手册、个人笔记里找到准确信息来回答你的问题。这不再是那种需要你手动上传文件、等待后台处理的传统知识库问答而是把整个“检索-增强-生成”的流程直接搬到了前端浏览器里来跑。想想这个场景你正在用公司的内部沟通工具想快速查一下某个API的调用参数或者某个项目的排期规范。你不需要离开当前页面也不需要去另一个系统里搜索直接在聊天框里一下这个“智能助手”它就能从你们团队共享的文档库里把最相关的那几段内容找出来组织成一段清晰的回答。整个过程数据不出浏览器响应速度极快体验非常丝滑。这背后的核心就是RAGRetrieval-Augmented Generation检索增强生成。但传统的RAG方案无论是用LangChain还是LlamaIndex搭建通常都是重度依赖后端服务文档预处理、向量化、存储、检索、调用大模型API每一步都在服务器端完成。而“前端RAG”的思路是试图利用现代浏览器日益强大的计算能力通过WebAssembly、WebGPU、IndexedDB等将文档的向量化Embedding和检索Retrieval这两大核心环节尽可能地放到前端来执行。这样做有几个显而易见的好处首先是隐私与数据安全敏感文档无需上传至云端直接在用户本地处理其次是响应速度与离线能力检索过程没有网络延迟且能在断网时提供基础的知识查询最后是成本与架构简化减少了对后端向量数据库和大量Embedding API调用的依赖。当然这并非要完全取代后端RAG而是针对特定场景如轻量级、私密性要求高、实时性强的内部工具的一种有效补充和架构探索。2. 核心思路与技术选型在前端实现RAG的可行性拆解要把RAG搬到前端我们得直面几个核心挑战浏览器环境下的计算性能限制、模型体积与加载速度、以及本地存储的容量。我们的目标不是要在前端跑动一个175B参数的大模型而是找到一个性能、精度和资源消耗的平衡点。2.1 核心架构设计整个流程可以拆解为以下几个关键步骤其中绿色部分是我们希望在前端重点实现的文档加载与预处理用户在前端选择或拖入文档如PDF、TXT、MD。前端需要解析文档内容并进行文本分割Chunking。文本向量化Embedding将分割后的文本块通过一个轻量级的Embedding模型转换为向量Vector。这是前端RAG最大的技术难点和性能瓶颈所在。向量存储与索引将生成的向量和对应的原始文本存储在前端的IndexedDB中并建立高效的索引以便快速检索。查询处理当用户提问时将问题同样通过Embedding模型向量化。向量检索在IndexedDB的向量索引中进行相似度计算通常是余弦相似度找出与问题向量最相似的几个文本块。提示词构建与生成将检索到的文本块作为上下文与用户问题一起构建成提示词Prompt发送给后端的大语言模型LLMAPI如GPT-4、Claude或开源的本地API得到最终答案。前端展示将LLM返回的流式或非流式结果在聊天界面中渲染展示。可以看到步骤2、3、4、5是我们实现“前端化”的核心。步骤6和7仍然需要后端LLM的支持因为目前要在浏览器端运行一个高质量的生成式大模型仍然不现实。但即便如此将检索环节前置已经能带来巨大的体验提升。2.2 关键技术选型解析1. 轻量级Embedding模型这是前端RAG的基石。模型必须足够小通常小于100MB且能在浏览器中高效运行。目前有几个主流选择BGE (BAAI General Embedding) 的蒸馏版或量化版BGE系列如BGE-M3、BGE-large-zh在中文社区评价很高。我们可以使用其经过量化INT8或蒸馏如bge-small-zh-v1.5的版本模型体积可压缩至几十MB。通过onnxruntime-web或Transformers.js库可以在浏览器中加载和运行。Universal Sentence Encoder (USE) 的 Lite 版Google提供的模型有专门的TensorFlow.js版本体积小针对Web优化但中文能力可能稍弱。Sentence-Transformers 的微型模型如all-MiniLM-L6-v2这是一个英文为主的通用模型体积约80MB性能均衡。对于中文可能需要寻找基于它微调的中文版本。选型心得对于中文场景BGE的量化小型版本是目前的最优解。它平衡了效果、体积和对中文的理解能力。你需要从Hugging Face等平台找到转换好的ONNX或适合Transformers.js的格式。首次加载需要下载模型文件可以利用Service Worker和缓存策略来优化体验。2. 前端向量计算与检索库我们需要一个库来处理向量运算和近似最近邻搜索。Faiss.js / usearchFaiss是Meta开源的向量检索库性能极强。faiss-wasm或faiss.js项目提供了WebAssembly版本可以在浏览器中运行。对于万级甚至十万级以下的向量库其检索速度完全可以接受。HNSWlib (via wasm)HNSWHierarchical Navigable Small World图是一种高效的近似最近邻搜索算法。也有对应的WASM版本可供使用。纯JavaScript实现对于非常小规模例如少于1000条的向量库也可以直接用JavaScript计算余弦相似度进行暴力搜索。但规模稍大性能就会成为瓶颈。3. 本地存储方案IndexedDB这是浏览器的内置数据库存储容量大通常可达数百MB甚至更多适合存储向量数据、原始文本和元数据。我们可以将向量数据以Float32Array的形式存储。本地文件系统访问API (File System Access API)如果文档库很大且相对静态可以考虑让用户授权访问某个本地目录直接将向量索引文件保存在磁盘上避免每次使用都重新生成。但这需要较新的浏览器和用户授权。4. 大模型LLM接口生成部分仍需后端。我们需要一个简单的后端服务接收前端发送的提示词包含检索到的上下文和用户问题调用OpenAI、Azure OpenAI或本地部署的Ollama、vLLM等API并将结果流式或非流式返回给前端。这部分可以用任何后端语言Node.js, Python, Go快速搭建一个代理接口。3. 分步实现从零搭建一个前端RAG聊天页下面我将以一个具体的实现路径为例使用BGE-small-zhONNX模型、faiss-wasm和 IndexedDB来演示如何构建核心功能。3.1 环境准备与项目初始化假设我们使用 Vite TypeScript React 来构建前端项目这样工具链现代开发体验好。# 创建项目 npm create vitelatest frontend-rag-chat -- --template react-ts cd frontend-rag-chat # 安装核心依赖 npm install onnxruntime-web xenova/transformers faiss-wasm # 安装UI和工具库按需 npm install antd ant-design/icons lucide-react关键依赖说明onnxruntime-web: 用于在浏览器中运行ONNX格式的模型比纯JS实现快得多。xenova/transformers: 一个在浏览器中运行Transformer模型的库如果我们使用其支持的模型格式如量化后的BERT也可以用它。这里我们主要用ONNX Runtime。faiss-wasm: Faiss的WebAssembly版本用于向量检索。3.2 文档加载与预处理模块实现首先我们需要一个组件来处理用户上传的文档。// components/DocumentUploader.tsx import React, { useCallback } from react; import { InboxOutlined } from ant-design/icons; import { message, Upload } from antd; import { parsePDF, parseText, splitText } from ../utils/textProcessor; const { Dragger } Upload; interface DocumentUploaderProps { onDocumentsProcessed: (chunks: Array{ text: string; id: string }) void; } const DocumentUploader: React.FCDocumentUploaderProps ({ onDocumentsProcessed }) { const processFile useCallback(async (file: File) { try { let text ; if (file.type application/pdf) { text await parsePDF(file); // 使用 pdf.js 等库解析 } else if (file.type text/plain || file.type text/markdown) { text await parseText(file); } else { message.error(暂不支持该文件格式); return; } // 文本分割按段落、句子或固定长度分割 const chunks splitText(text, { chunkSize: 500, overlap: 50 }); const chunksWithId chunks.map((chunk, index) ({ text: chunk, id: ${file.name}_${Date.now()}_${index}, })); onDocumentsProcessed(chunksWithId); message.success(成功处理 ${file.name}生成 ${chunks.length} 个文本块); } catch (error) { message.error(处理文件 ${file.name} 时出错: ${error}); } }, [onDocumentsProcessed]); // ... Upload组件配置调用processFile return ( Dragger beforeUpload{(file) { processFile(file); return false; // 阻止自动上传 }} accept.pdf,.txt,.md multiple p classNameant-upload-drag-icon InboxOutlined / /p p点击或拖拽文档到此区域/p p支持 PDF, TXT, MD 格式/p /Dragger ); };文本分割splitText是实现效果的关键。粗糙的分割会导致检索上下文不连贯。一个实用的策略是结合“递归字符分割”和“按标记器分割”确保每个块语义相对完整且不超过模型上下文长度。3.3 Embedding模型加载与向量化这是最核心的模块。我们需要加载ONNX模型并进行推理。// utils/embeddingService.ts import * as ort from onnxruntime-web; class EmbeddingService { private session: ort.InferenceSession | null null; private tokenizer: any null; // 需要搭配对应的tokenizer private modelPath /models/bge-small-zh.onnx; // 模型需放在public目录或通过CDN加载 private tokenizerPath /tokenizers/bge-small-zh; // Tokenizer配置 async init() { if (this.session) return; try { // 1. 加载ONNX模型 this.session await ort.InferenceSession.create(this.modelPath, { executionProviders: [wasm], // 使用WASM后端兼容性最好 }); // 2. 加载Tokenizer这里需要自己实现或使用兼容的库 // 例如可以使用xenova/transformers中的AutoTokenizer但需确保与ONNX模型匹配 // this.tokenizer await AutoTokenizer.from_pretrained(this.tokenizerPath); console.log(Embedding 模型加载成功); } catch (error) { console.error(加载Embedding模型失败:, error); throw error; } } async generateEmbedding(text: string): Promisenumber[] { if (!this.session) await this.init(); if (!this.tokenizer) { // 简易的tokenize实际项目需用正确的tokenizer // 这里仅为示例 const tokens text.split().slice(0, 512); // 简单截断 const inputIds new Array(512).fill(0); tokens.forEach((_, i) inputIds[i] i1); // 伪代码 const attentionMask inputIds.map(id id 0 ? 1 : 0); } try { // 准备模型输入 const inputs { input_ids: new ort.Tensor(int64, BigInt64Array.from(inputIds.map(BigInt)), [1, 512]), attention_mask: new ort.Tensor(int64, BigInt64Array.from(attentionMask.map(BigInt)), [1, 512]), }; // 运行模型 const results await this.session!.run(inputs); // 假设模型输出名为 last_hidden_state我们取[CLS] token的表示或做mean pooling const embeddingsTensor results[last_hidden_state]; const embeddingsArray embeddingsTensor.data as Float32Array; // 简单处理取第一个token[CLS]的向量作为句子表示 const embeddingVector: number[] []; const dim embeddingsTensor.dims[2]; // 向量维度 for (let i 0; i dim; i) { embeddingVector.push(embeddingsArray[i]); } // 归一化余弦相似度通常需要归一化向量 const norm Math.sqrt(embeddingVector.reduce((sum, val) sum val * val, 0)); return embeddingVector.map(v v / norm); } catch (error) { console.error(生成向量失败:, error); throw error; } } async generateEmbeddingsForChunks(chunks: Array{text: string; id: string}): PromiseArray{id: string; vector: number[]} { const vectors []; for (const chunk of chunks) { // 可以添加简单的节流避免同时处理太多请求阻塞主线程 const vector await this.generateEmbedding(chunk.text); vectors.push({ id: chunk.id, vector }); } return vectors; } } export const embeddingService new EmbeddingService();实操心得ONNX模型和Tokenizer的获取与转换是一大难点。通常的路径是从Hugging Face下载PyTorch格式的BGE-small-zh模型使用官方工具或optimum库将其导出为ONNX格式。Tokenizer的词汇表文件也需要一并导出并放置在前端可访问的位置。这个过程可能需要一些Python脚本辅助。首次加载几十MB的模型文件会耗时务必设计加载进度提示。3.4 向量存储与检索Faiss IndexedDB接下来我们需要将向量存入IndexedDB并用Faiss建立索引。// utils/vectorStore.ts import { FaissIndex } from faiss-wasm; import { openDB, DBSchema, IDBPDatabase } from idb; interface MyVectorDB extends DBSchema { chunks: { key: string; // chunk id value: { id: string; text: string; metadata?: Recordstring, any; }; }; index: { key: string; value: ArrayBuffer; // 序列化的Faiss索引 }; } class VectorStore { private db: IDBPDatabaseMyVectorDB | null null; private faissIndex: FaissIndex | null null; private dimension: number 384; // BGE-small-zh的向量维度需根据模型确定 async init() { this.db await openDBMyVectorDB(RAG-VectorStore, 1, { upgrade(db) { if (!db.objectStoreNames.contains(chunks)) { db.createObjectStore(chunks, { keyPath: id }); } if (!db.objectStoreNames.contains(index)) { db.createObjectStore(index); } }, }); await this.loadFaissIndex(); } private async loadFaissIndex() { if (!this.db) return; // 尝试从IndexedDB加载已保存的索引 const indexData await this.db.get(index, main); if (indexData) { this.faissIndex FaissIndex.fromBuffer(new Uint8Array(indexData)); } else { // 创建新的索引 this.faissIndex new FaissIndex(this.dimension, Flat); // Flat为精确搜索数据量大可考虑HNSW await this.saveFaissIndex(); } } private async saveFaissIndex() { if (!this.db || !this.faissIndex) return; const buffer this.faissIndex.toBuffer(); await this.db.put(index, buffer, main); } async addVectors(vectors: Array{id: string; vector: number[]}, chunks: Array{id: string; text: string}) { if (!this.db || !this.faissIndex) await this.init(); // 1. 保存文本块 const tx this.db!.transaction(chunks, readwrite); for (const chunk of chunks) { await tx.store.put({ id: chunk.id, text: chunk.text }); } await tx.done; // 2. 添加向量到Faiss索引 const ids vectors.map(v Number.parseInt(v.id.split(_).pop() || 0)); // 生成数字ID const vectorArray new Float32Array(vectors.length * this.dimension); vectors.forEach((v, i) vectorArray.set(v.vector, i * this.dimension)); this.faissIndex!.add(vectorArray, ids); await this.saveFaissIndex(); // 保存更新后的索引 } async search(queryVector: number[], k: number 5): PromiseArray{id: string; text: string; score: number} { if (!this.db || !this.faissIndex) await this.init(); const queryArray new Float32Array(queryVector); const { labels, distances } this.faissIndex!.search(queryArray, k); const results []; for (let i 0; i labels.length; i) { const chunkId doc_${labels[i]}; // 需要根据你存储ID的方式反向构造 const chunkData await this.db!.get(chunks, chunkId); if (chunkData) { // 距离越小越相似这里转换为分数可选 const score 1 / (1 distances[i]); // 一种简单的转换方式 results.push({ id: chunkId, text: chunkData.text, score }); } } return results; } } export const vectorStore new VectorStore();3.5 聊天界面与流程整合最后我们将所有模块整合到一个聊天界面中。// components/ChatPage.tsx import React, { useState, useRef, useEffect } from react; import { Input, Button, List, Spin, message } from antd; import { SendOutlined } from ant-design/icons; import { embeddingService } from ../utils/embeddingService; import { vectorStore } from ../utils/vectorStore; import DocumentUploader from ./DocumentUploader; interface Message { id: string; content: string; sender: user | assistant; } const ChatPage: React.FC () { const [messages, setMessages] useStateMessage[]([{ id: 1, content: 你好我可以帮你从上传的文档中查找信息。请先上传文档。, sender: assistant }]); const [inputText, setInputText] useState(); const [loading, setLoading] useState(false); const [isIndexing, setIsIndexing] useState(false); const messagesEndRef useRefHTMLDivElement(null); // 处理上传的文档 const handleDocumentsProcessed async (chunks: Array{ text: string; id: string }) { if (chunks.length 0) return; setIsIndexing(true); try { // 1. 为每个文本块生成向量 const vectors await embeddingService.generateEmbeddingsForChunks(chunks); // 2. 存储向量和文本 await vectorStore.addVectors(vectors, chunks); message.success(知识库已更新共索引 ${chunks.length} 个文本块。); setMessages(prev [...prev, { id: Date.now().toString(), content: 已学习 ${chunks.length} 条新知识现在可以问我了。, sender: assistant }]); } catch (error) { message.error(文档索引失败: error); } finally { setIsIndexing(false); } }; // 处理用户发送消息 const handleSend async () { if (!inputText.trim() || loading) return; const userMessage: Message { id: Date.now().toString(), content: inputText, sender: user }; setMessages(prev [...prev, userMessage]); setInputText(); setLoading(true); try { // 1. 将用户问题向量化 const queryVector await embeddingService.generateEmbedding(inputText); // 2. 在本地向量库中检索 const relevantChunks await vectorStore.search(queryVector, 3); // 3. 构建Prompt const context relevantChunks.map(c c.text).join(\n\n); const prompt 请根据以下上下文信息回答问题。如果上下文不包含答案请直接说“根据提供的资料我无法回答这个问题”。\n\n上下文\n${context}\n\n问题${inputText}\n\n答案; // 4. 调用后端LLM API示例为fetch const response await fetch(/api/chat, { // 你的后端代理接口 method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ prompt }), }); if (!response.ok) throw new Error(网络请求失败); const data await response.json(); // 5. 添加助手回复 const assistantMessage: Message { id: (Date.now() 1).toString(), content: data.answer || 抱歉我暂时无法回答这个问题。, sender: assistant }; setMessages(prev [...prev, assistantMessage]); } catch (error) { console.error(处理请求失败:, error); const errorMessage: Message { id: (Date.now() 2).toString(), content: 出错了请稍后再试。, sender: assistant }; setMessages(prev [...prev, errorMessage]); } finally { setLoading(false); } }; useEffect(() { messagesEndRef.current?.scrollIntoView({ behavior: smooth }); }, [messages]); return ( div classNameflex flex-col h-screen div classNamep-4 border-b h1前端RAG智能助手/h1 div classNamemt-2 DocumentUploader onDocumentsProcessed{handleDocumentsProcessed} / {isIndexing Spin sizesmall classNameml-2 /} /div /div div classNameflex-1 overflow-y-auto p-4 List dataSource{messages} renderItem{msg ( List.Item className{msg.sender user ? justify-end : } div className{max-w-3/4 p-3 rounded-lg ${msg.sender user ? bg-blue-100 : bg-gray-100}} {msg.content} /div /List.Item )} / div ref{messagesEndRef} / /div div classNamep-4 border-t Input.TextArea value{inputText} onChange{(e) setInputText(e.target.value)} onPressEnter{(e) { if (!e.shiftKey) { e.preventDefault(); handleSend(); } }} placeholder输入您的问题... autoSize{{ minRows: 2, maxRows: 4 }} disabled{loading} / Button typeprimary icon{SendOutlined /} onClick{handleSend} loading{loading} classNamemt-2 block 发送 /Button /div /div ); }; export default ChatPage;4. 性能优化与工程化考量将RAG放到前端性能是必须跨过的坎。以下是一些关键的优化思路和工程化建议。4.1 模型加载与推理优化模型量化与压缩务必使用INT8量化的ONNX模型这能将模型体积减少近75%同时推理速度提升明显。对于BGE-small-zh量化后可以控制在20MB左右。分片加载与流式加载利用HTTP Range请求将模型文件分成多个小块加载实现边下边用减少用户等待时间。Web WorkerEmbedding模型推理是CPU密集型任务会阻塞主线程。务必将其放入Web Worker中执行保持UI流畅。// 在主线程中 const embeddingWorker new Worker(./embedding.worker.js); embeddingWorker.postMessage({ type: EMBED, text: 需要向量化的文本 }); embeddingWorker.onmessage (e) { /* 处理返回的向量 */ };模型缓存利用Service Worker和Cache API缓存模型文件第二次访问时几乎可以瞬间加载。4.2 向量检索性能优化索引选择Faiss的Flat索引是精确搜索但复杂度是O(N)。当向量数量超过1万时检索延迟会变得明显。应切换到近似最近邻索引如HNSWIndexHNSW或IVFIndexIVFFlat。faiss-wasm可能对部分高级索引支持有限需要测试。分批处理与增量索引首次索引大量文档时避免同步循环调用generateEmbedding导致页面卡死。应使用队列或setTimeout进行分批处理并更新进度条。新增文档时使用增量索引API。检索结果缓存对于相同或相似的问题可以将检索结果缓存在内存或sessionStorage中短时间内直接复用避免重复的向量计算和检索。4.3 存储与数据管理IndexedDB容量与清理不同浏览器对IndexedDB的容量限制不同通常为50%的磁盘空间。需要设计数据清理策略例如LRU最近最少使用算法或允许用户手动管理知识库。向量数据压缩存储Float32向量很占空间。可以考虑使用Float16甚至INT8量化存储向量在检索前再转换回Float32进行计算这能节省大量存储空间对精度损失在可接受范围内。结构化存储除了向量和文本还应在IndexedDB中存储文档元数据如文件名、更新时间、所属类别等以便实现按来源过滤等高级检索功能。4.4 提示词工程与回答质量前端负责检索后端负责生成中间的“提示词”是桥梁其质量直接决定最终答案的准确性。上下文优化不要简单拼接检索到的文本块。可以尝试重排序Re-ranking在浏览器端用一个更小、更快的重排序模型对检索出的Top-K结果进行二次排序将最相关的放在前面。这能有效提升上下文质量。上下文压缩如果检索到的文本块总长度超过LLM的上下文窗口需要压缩。可以尝试提取关键句或者使用LLM本身进行摘要但这需要额外调用。Prompt模板设计设计清晰、强约束的Prompt模板。你是一个专业的文档助手。请严格根据以下提供的上下文信息来回答问题。 上下文 {{context}} 用户问题{{question}} 要求 1. 答案必须完全基于上述上下文。 2. 如果上下文没有提供足够信息来回答问题请直接说“根据提供的资料我无法回答这个问题。” 3. 答案应简洁、准确并引用上下文中的关键信息。 请开始回答引用与溯源在返回的答案中标明引用了哪个文档的哪个部分。这可以通过在存储文本块时附带其位置信息如页码、段落号并在生成答案时要求LLM注明引用来实现。5. 常见问题与避坑指南在实际开发和测试中我遇到了不少坑这里总结一下希望能帮你绕过去。5.1 模型相关问题问题1ONNX模型在浏览器中加载失败报错“不支持的operator”。原因ONNX模型可能包含某些ONNX Runtime Web不支持的算子如复杂的Attention机制变体。解决在导出ONNX模型时使用opset_version14或更低版本并确保使用onnxruntime-web支持的算子集。一个更稳妥的方法是使用专门为Web优化过的模型版本例如微软的ONNX Model Zoo中的一些模型或者社区转换好的版本。问题2Embedding速度太慢处理一个句子就要好几秒。原因WASM计算虽然比纯JS快但仍远慢于原生。模型太大或序列长度太长是主因。解决确保使用量化后的小模型如BGE-small-zh量化版。将推理过程放入Web Worker避免阻塞UI。限制输入文本的最大长度如128或256个token超长部分截断。对于批量文档索引在Worker中实现一个处理队列并给用户明确的进度反馈。5.2 检索与存储问题问题3Faiss索引文件越来越大首次加载时间很长。原因Flat索引会将所有向量数据保存在内存和文件里。解决换用HNSW或IVF索引它们通过牺牲少量精度换取更小的内存占用和更快的检索速度。将索引文件进行分片存储。例如每1000个向量存一个索引文件检索时按需加载部分分片实现较复杂。设置一个向量数量上限对于个人工具型应用1万条高质量文本块通常已经足够覆盖大量知识。问题4IndexedDB操作异步容易出现“事务已结束”的错误。原因IndexedDB的事务是自动提交的在异步操作如await中如果事务内没有活动事务会自动关闭。解决将所有需要在同一事务中进行的操作集中在一个Promise链中避免在事务内使用await调用外部异步函数。使用IDBRequest的onsuccess/onerror回调风格或者使用idb这类封装良好的库我们上面就用了idb它能更好地处理Promise和事务。5.3 应用体验问题问题5首次打开页面加载模型时间太久用户体验差。解决骨架屏与加载状态在模型加载完成前显示友好的加载动画和进度提示。按需加载只有在用户首次触发需要Embedding的操作如上传文档或提问时才去加载模型。之前只加载核心UI。预加载提示在用户阅读使用说明或进行其他操作时在后台静默开始加载模型。问题6回答出现“幻觉”即模型编造了不在上下文中的信息。原因Prompt约束力不够或者检索到的上下文相关性太低。解决强化Prompt中的约束指令如“必须严格基于上下文”、“禁止编造”。在Prompt中提供少量示例Few-Shot展示什么是基于上下文的回答什么是拒绝回答。优化检索质量调整文本分割策略如按语义分割、尝试不同的Embedding模型、引入重排序。在后端LLM调用时使用较低的temperature参数如0.1减少随机性。问题7如何支持更多格式的文档解决前端文档解析能力有限。对于复杂格式如Word、Excel、PPT更务实的方案是后端预处理用户上传文件后先发送到后端进行格式解析和文本提取后端将纯文本返回给前端前端再进行后续的向量化和索引。这简化了前端复杂度但牺牲了完全的隐私性。使用Web原生API对于.docx可以使用mammoth.js对于.xlsx可以使用sheetjs。但这些库体积不小会增加前端包体积。前端RAG是一个充满挑战但也极具魅力的方向。它代表着一种将AI能力“下沉”到边缘、更注重隐私和实时性的趋势。我这个项目目前还只是一个原型但在内部工具、个人知识管理助手等场景下已经能跑起来并解决实际问题。最大的体会是平衡是关键——在模型效果、性能、用户体验和开发复杂度之间找到那个最适合你当前场景的平衡点。下一步我计划尝试集成更小的重排序模型并探索利用WebGPU来进一步加速推理的可能性。