从零到一!前端搭建本地轻量化 RAG 问答系统

发布时间:2026/8/3 7:52:10
从零到一!前端搭建本地轻量化 RAG 问答系统 1. 引言大模型虽强但面对私有知识库、企业内部文档时往往存在「幻觉」和「知识过时」的问题。RAGRetrieval-Augmented Generation检索增强生成正是解决这一痛点的主流方案先检索出与问题相关的文档片段再交给大模型生成回答。传统 RAG 方案往往依赖后端服务、向量数据库和复杂的部署链路。而随着浏览器能力的增强前端完全可以在本地搭建一套轻量化的 RAG 问答系统——无需服务器、无需安装数据库数据不出浏览器隐私安全。本文将从零到一带你用纯前端技术栈Vite React Transformers.js localStorage搭建一套可运行的本地 RAG 问答系统。2. 技术选型与整体架构2.1 技术栈模块选型说明构建工具Vite极快的开发体验UI 框架React组件化开发向量化模型Transformers.js all-MiniLM-L6-v2纯浏览器端运行无需后端向量存储localStorage 自研余弦相似度检索轻量、零依赖生成模型可选本地模型 / 云端 API按需接入2.2 整体流程用户上传文档文本切分向量化 Embedding向量存入 localStorage用户提问问题向量化余弦相似度检索 Top-K拼接上下文 问题调用大模型生成回答3. 环境准备与项目初始化首先确保本机已安装 Node.js 18然后执行npmcreate vitelatest local-rag ----templatereact-tscdlocal-ragnpminstallnpminstallhuggingface/transformers安装完成后启动开发服务器npmrun dev4. 文本切分把长文档拆成小块RAG 检索的粒度很关键。整篇文档直接向量化检索精度差切得太碎又丢失上下文。这里实现一个简单的按段落 字符数控制的切分器。// src/utils/chunker.tsexportinterfaceChunk{id:string;text:string;source:string;}exportfunctionchunkText(text:string,source:string,maxLen500):Chunk[]{constparagraphstext.split(/\n\s*\n/);constchunks:Chunk[][];letbuffer;for(constparaofparagraphs){consttrimmedpara.trim();if(!trimmed)continue;if((buffer\ntrimmed).lengthmaxLenbuffer){chunks.push({id:crypto.randomUUID(),text:buffer.trim(),source,});buffertrimmed;}else{bufferbuffer?buffer\ntrimmed:trimmed;}}if(buffer.trim()){chunks.push({id:crypto.randomUUID(),text:buffer.trim(),source,});}returnchunks;}5. 向量化在浏览器中运行 Embedding 模型借助 Transformers.js我们可以在浏览器端直接加载并运行 MiniLM 模型把文本转成 384 维向量。// src/utils/embedder.tsimport{pipeline}fromhuggingface/transformers;letembedder:anynull;exportasyncfunctiongetEmbedder(){if(!embedder){embedderawaitpipeline(feature-extraction,Xenova/all-MiniLM-L6-v2);}returnembedder;}exportasyncfunctionembedTexts(texts:string[]):Promisenumber[][]{constmodelawaitgetEmbedder();constoutputsawaitmodel(texts,{pooling:mean,normalize:true});returnoutputs.tolist();}提示首次加载模型需要下载约 90MB 文件之后浏览器会缓存二次访问秒开。6. 向量存储与检索localStorage 余弦相似度6.1 存储层localStorage 无法直接存高维数组这里用 JSON 序列化存储并封装读写接口。// src/utils/store.tsimporttype{Chunk}from./chunker;interfaceStoredChunkextendsChunk{vector:number[];}constSTORAGE_KEYlocal-rag-chunks;exportfunctionsaveChunks(chunks:StoredChunk[]):void{localStorage.setItem(STORAGE_KEY,JSON.stringify(chunks));}exportfunctionloadChunks():StoredChunk[]{constrawlocalStorage.getItem(STORAGE_KEY);returnraw?JSON.parse(raw):[];}exportfunctionclearChunks():void{localStorage.removeItem(STORAGE_KEY);}6.2 余弦相似度检索// src/utils/search.tsimporttype{StoredChunk}from./store;functioncosineSimilarity(a:number[],b:number[]):number{letdot0,normA0,normB0;for(leti0;ia.length;i){dota[i]*b[i];normAa[i]*a[i];normBb[i]*b[i];}returndot/(Math.sqrt(normA)*Math.sqrt(normB)1e-8);}exportfunctionsearchChunks(queryVector:number[],chunks:StoredChunk[],topK3):StoredChunk[]{returnchunks.map((chunk)({chunk,score:cosineSimilarity(queryVector,chunk.vector),})).sort((a,b)b.score-a.score).slice(0,topK).map((item)item.chunk);}7. 组装问答把检索结果交给大模型检索到 Top-K 相关片段后把它们拼进 Prompt再调用大模型生成回答。这里以 OpenAI 兼容接口为例你也可以替换为本地 Ollama 或其他模型。// src/utils/qa.tsimport{searchChunks}from./search;import{loadChunks}from./store;exportasyncfunctionaskQuestion(question:string,queryVector:number[],apiKey:string,baseUrlhttps://api.openai.com/v1):Promisestring{constchunksloadChunks();consttopChunkssearchChunks(queryVector,chunks,3);constcontexttopChunks.map((c,i)【片段${i1}】\n${c.text}).join(\n\n);constprompt请基于以下资料回答问题。如果资料中没有相关信息请直接说明资料中未找到相关内容。 资料${context}问题${question}回答;constrespawaitfetch(${baseUrl}/chat/completions,{method:POST,headers:{Content-Type:application/json,Authorization:Bearer${apiKey},},body:JSON.stringify({model:gpt-4o-mini,messages:[{role:user,content:prompt}],temperature:0.3,}),});constdataawaitresp.json();returndata.choices[0].message.content;}8. 前端界面串联完整流程最后用 React 把以上模块串起来实现「上传文档 → 建立索引 → 提问 → 得到回答」的完整交互。// src/App.tsx import { useState } from react; import { chunkText } from ./utils/chunker; import { embedTexts } from ./utils/embedder; import { saveChunks, loadChunks, clearChunks } from ./utils/store; import { askQuestion } from ./utils/qa; export default function App() { const [docText, setDocText] useState(); const [question, setQuestion] useState(); const [answer, setAnswer] useState(); const [loading, setLoading] useState(false); const [indexed, setIndexed] useState(false); const handleIndex async () { setLoading(true); try { const chunks chunkText(docText, 本地文档); const vectors await embedTexts(chunks.map((c) c.text)); saveChunks(chunks.map((c, i) ({ ...c, vector: vectors[i] }))); setIndexed(true); alert(索引完成共 ${chunks.length} 个片段); } finally { setLoading(false); } }; const handleAsk async () { if (!question.trim()) return; setLoading(true); try { const [queryVector] await embedTexts([question]); const apiKey prompt(请输入 API Key本地运行仅存于内存) || ; const res await askQuestion(question, queryVector, apiKey); setAnswer(res); } finally { setLoading(false); } }; return ( div style{{ maxWidth: 720, margin: 0 auto, padding: 24 }} h1本地轻量化 RAG 问答/h1 section h21. 上传文档/h2 textarea value{docText} onChange{(e) setDocText(e.target.value)} rows{8} style{{ width: 100% }} placeholder粘贴你的文档内容... / button onClick{handleIndex} disabled{loading || !docText.trim()} {loading ? 处理中... : 建立索引} /button {indexed ( button onClick{() { clearChunks(); setIndexed(false); }} 清空索引 /button )} /section section h22. 提问/h2 input value{question} onChange{(e) setQuestion(e.target.value)} style{{ width: 100%, padding: 8 }} placeholder输入你的问题... / button onClick{handleAsk} disabled{loading || !indexed} 提问 /button /section {answer ( section h2回答/h2 div style{{ whiteSpace: pre-wrap, background: #f5f5f5, padding: 16, borderRadius: 8 }} {answer} /div /section )} /div ); }9. 效果演示与运行在项目根目录执行npm run dev浏览器打开http://localhost:5173粘贴一段产品文档或技术资料点击「建立索引」等待模型加载与向量化首次约 10~30 秒输入问题点击「提问」系统检索相关片段并调用大模型生成回答。10. 总结与优化方向本文实现了一套完全运行在浏览器端的轻量化 RAG 系统核心亮点零后端向量化、存储、检索全部在前端完成隐私安全文档数据不离开本地浏览器轻量依赖仅需 Transformers.js 一个核心库。后续可优化方向接入 Web Worker避免向量化阻塞 UI 线程使用 IndexedDB 替代 localStorage突破 5MB 存储上限引入重排序Rerank模型提升检索精度支持 PDF、Word 等格式解析接入本地 Ollama 模型实现完全离线运行。希望这篇文章能帮你快速上手前端 RAG 开发动手试试吧