1. 引言
大语言模型(LLM)虽然能力强大,但存在知识截止日期、无法访问私有数据、回答可能产生幻觉等问题。检索增强生成(Retrieval-Augmented Generation,RAG)通过「先检索、后生成」的方式,让模型基于外部知识库回答问题,从而显著提升回答的准确性和时效性。
传统 RAG 方案往往依赖 Python 后端、向量数据库服务等重型组件,部署成本高、链路复杂。而随着浏览器能力的不断增强,前端完全有能力在本地搭建一套轻量化的 RAG 问答系统------无需服务器、无需安装依赖,打开浏览器即可使用。
本文将带你从零到一,使用纯前端技术栈(HTML + JavaScript + WebLLM + Transformers.js)搭建一套完全运行在浏览器本地的 RAG 问答系统。
2. 技术选型与整体架构
2.1 技术栈
| 组件 | 选型 | 说明 |
|---|---|---|
| 前端框架 | 原生 HTML + JavaScript | 零依赖,开箱即用 |
| 大模型推理 | WebLLM | 浏览器端运行 Llama 等模型 |
| 文本向量化 | Transformers.js + all-MiniLM-L6-v2 | 轻量级嵌入模型 |
| 向量检索 | 自实现余弦相似度 | 数据量小时无需数据库 |
| 文本切分 | 自实现滑动窗口切分 | 简单可控 |
2.2 整体架构
#mermaid-svg-dvGBvQUTxKv7P17M{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dvGBvQUTxKv7P17M .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dvGBvQUTxKv7P17M .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dvGBvQUTxKv7P17M .error-icon{fill:#552222;}#mermaid-svg-dvGBvQUTxKv7P17M .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dvGBvQUTxKv7P17M .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dvGBvQUTxKv7P17M .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dvGBvQUTxKv7P17M .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dvGBvQUTxKv7P17M .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dvGBvQUTxKv7P17M .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dvGBvQUTxKv7P17M .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dvGBvQUTxKv7P17M .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dvGBvQUTxKv7P17M .marker.cross{stroke:#333333;}#mermaid-svg-dvGBvQUTxKv7P17M svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dvGBvQUTxKv7P17M p{margin:0;}#mermaid-svg-dvGBvQUTxKv7P17M .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dvGBvQUTxKv7P17M .cluster-label text{fill:#333;}#mermaid-svg-dvGBvQUTxKv7P17M .cluster-label span{color:#333;}#mermaid-svg-dvGBvQUTxKv7P17M .cluster-label span p{background-color:transparent;}#mermaid-svg-dvGBvQUTxKv7P17M .label text,#mermaid-svg-dvGBvQUTxKv7P17M span{fill:#333;color:#333;}#mermaid-svg-dvGBvQUTxKv7P17M .node rect,#mermaid-svg-dvGBvQUTxKv7P17M .node circle,#mermaid-svg-dvGBvQUTxKv7P17M .node ellipse,#mermaid-svg-dvGBvQUTxKv7P17M .node polygon,#mermaid-svg-dvGBvQUTxKv7P17M .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dvGBvQUTxKv7P17M .rough-node .label text,#mermaid-svg-dvGBvQUTxKv7P17M .node .label text,#mermaid-svg-dvGBvQUTxKv7P17M .image-shape .label,#mermaid-svg-dvGBvQUTxKv7P17M .icon-shape .label{text-anchor:middle;}#mermaid-svg-dvGBvQUTxKv7P17M .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dvGBvQUTxKv7P17M .rough-node .label,#mermaid-svg-dvGBvQUTxKv7P17M .node .label,#mermaid-svg-dvGBvQUTxKv7P17M .image-shape .label,#mermaid-svg-dvGBvQUTxKv7P17M .icon-shape .label{text-align:center;}#mermaid-svg-dvGBvQUTxKv7P17M .node.clickable{cursor:pointer;}#mermaid-svg-dvGBvQUTxKv7P17M .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dvGBvQUTxKv7P17M .arrowheadPath{fill:#333333;}#mermaid-svg-dvGBvQUTxKv7P17M .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dvGBvQUTxKv7P17M .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dvGBvQUTxKv7P17M .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dvGBvQUTxKv7P17M .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dvGBvQUTxKv7P17M .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dvGBvQUTxKv7P17M .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dvGBvQUTxKv7P17M .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dvGBvQUTxKv7P17M .cluster text{fill:#333;}#mermaid-svg-dvGBvQUTxKv7P17M .cluster span{color:#333;}#mermaid-svg-dvGBvQUTxKv7P17M div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dvGBvQUTxKv7P17M .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dvGBvQUTxKv7P17M rect.text{fill:none;stroke-width:0;}#mermaid-svg-dvGBvQUTxKv7P17M .icon-shape,#mermaid-svg-dvGBvQUTxKv7P17M .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dvGBvQUTxKv7P17M .icon-shape p,#mermaid-svg-dvGBvQUTxKv7P17M .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dvGBvQUTxKv7P17M .icon-shape .label rect,#mermaid-svg-dvGBvQUTxKv7P17M .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dvGBvQUTxKv7P17M .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dvGBvQUTxKv7P17M .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dvGBvQUTxKv7P17M :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户输入问题
问题向量化
向量相似度检索
召回 Top-K 文本块
拼接 Prompt
WebLLM 本地推理
生成回答
整个流程完全在浏览器本地完成,不经过任何服务器,数据隐私有保障。
3. 环境准备
3.1 项目结构
rag-local/
├── index.html # 主页面
├── app.js # 核心逻辑
├── style.css # 样式
└── docs/ # 本地知识库文档
└── sample.txt
3.2 引入依赖
在 index.html 中通过 CDN 引入所需库:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>本地轻量化 RAG 问答系统</title>
<script type="module" src="https://cdn.jsdelivr.net/npm/@webllm/webllm@0.2.46/dist/web-llm.mjs"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0/dist/transformers.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- 页面结构 -->
<script type="module" src="app.js"></script>
</body>
</html>
4. 核心实现
4.1 文本切分与向量化
首先实现文本切分功能,将长文档切分为固定大小的文本块:
javascript
// text-splitter.js
export function splitText(text, chunkSize = 500, overlap = 50) {
const chunks = [];
let start = 0;
while (start < text.length) {
const end = Math.min(start + chunkSize, text.length);
chunks.push(text.slice(start, end));
start = end - overlap;
}
return chunks;
}
接着使用 Transformers.js 加载嵌入模型,将文本块转换为向量:
javascript
// embed.js
import { pipeline } from '@huggingface/transformers';
let embedder = null;
export async function getEmbedder() {
if (!embedder) {
embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
}
return embedder;
}
export async function embedTexts(texts) {
const model = await getEmbedder();
const outputs = await model(texts, { pooling: 'mean', normalize: true });
return outputs.tolist();
}
4.2 向量检索
实现余弦相似度计算与 Top-K 检索:
javascript
// retriever.js
function cosineSimilarity(a, b) {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
export function retrieve(queryVector, documentVectors, k = 3) {
const scores = documentVectors.map((vec, idx) => ({
index: idx,
score: cosineSimilarity(queryVector, vec)
}));
scores.sort((a, b) => b.score - a.score);
return scores.slice(0, k);
}
4.3 本地大模型推理
使用 WebLLM 在浏览器中运行大模型:
javascript
// llm.js
import { CreateWebWorkerMLCEngine } from '@webllm/webllm';
let engine = null;
export async function initLLM() {
const model = 'Llama-3.2-1B-Instruct-q4f32_1-MLC';
engine = await CreateWebWorkerMLCEngine(
new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }),
{ model }
);
}
export async function generateAnswer(prompt) {
const reply = await engine.chat.completions.create({
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 512
});
return reply.choices[0].message.content;
}
4.4 组装 RAG 主流程
将以上模块串联起来,形成完整的 RAG 问答流程:
javascript
// app.js
import { splitText } from './text-splitter.js';
import { embedTexts } from './embed.js';
import { retrieve } from './retriever.js';
import { initLLM, generateAnswer } from './llm.js';
let documentChunks = [];
let documentVectors = [];
export async function buildKnowledgeBase(rawText) {
documentChunks = splitText(rawText);
documentVectors = await embedTexts(documentChunks);
}
export async function ask(question) {
const [queryVector] = await embedTexts([question]);
const results = retrieve(queryVector, documentVectors, 3);
const context = results
.map(r => documentChunks[r.index])
.join('\n\n---\n\n');
const prompt = `基于以下资料回答问题:\n\n${context}\n\n问题:${question}`;
return await generateAnswer(prompt);
}
5. 页面交互设计
5.1 界面布局
html
<div class="container">
<h1>📚 本地 RAG 问答系统</h1>
<section class="upload-section">
<h2>1. 上传知识库文档</h2>
<input type="file" id="fileInput" accept=".txt,.md">
<button id="buildBtn">构建知识库</button>
<div id="buildStatus"></div>
</section>
<section class="chat-section">
<h2>2. 开始提问</h2>
<div id="chatHistory"></div>
<div class="input-row">
<input type="text" id="questionInput" placeholder="输入你的问题...">
<button id="askBtn">发送</button>
</div>
</section>
</div>
5.2 交互逻辑
javascript
// 构建知识库
document.getElementById('buildBtn').addEventListener('click', async () => {
const file = document.getElementById('fileInput').files[0];
if (!file) return alert('请先选择文件');
const text = await file.text();
await buildKnowledgeBase(text);
document.getElementById('buildStatus').textContent = `✅ 知识库构建完成,共 ${documentChunks.length} 个文本块`;
});
// 提问
document.getElementById('askBtn').addEventListener('click', async () => {
const question = document.getElementById('questionInput').value;
if (!question) return;
const answer = await ask(question);
// 渲染到聊天区域
renderMessage('user', question);
renderMessage('assistant', answer);
});
6. 效果演示与优化
6.1 运行效果
在浏览器中打开 index.html,依次完成以下步骤:
- 点击「选择文件」,上传本地知识库文档(如产品手册、个人笔记等);
- 点击「构建知识库」,等待文本切分与向量化完成;
- 在输入框中输入问题,点击「发送」,系统将基于本地知识库生成回答。
6.2 优化方向
| 优化项 | 方案 | 收益 |
|---|---|---|
| 检索精度 | 引入 BM25 混合检索 | 兼顾语义与关键词匹配 |
| 推理速度 | 使用 WebGPU 加速 | 显著提升生成速度 |
| 知识库规模 | 接入 IndexedDB 持久化 | 支持更大规模文档 |
| 回答质量 | 使用更大参数模型 | 提升生成质量 |
7. 总结
本文从零到一搭建了一套完全运行在浏览器本地的轻量化 RAG 问答系统。通过 Transformers.js 完成文本向量化、自实现余弦相似度检索、WebLLM 在浏览器端运行大模型,实现了「上传文档 → 构建知识库 → 本地问答」的完整闭环。
这套方案的优势在于:
- 零部署成本:无需服务器,打开浏览器即可使用;
- 数据隐私安全:所有数据均在本地处理,不上传云端;
- 技术栈统一:纯前端实现,降低维护成本。
当然,受限于浏览器端的计算资源,这套方案更适合知识库规模较小、对实时性要求不高的场景。对于生产级应用,仍建议结合后端服务与专业向量数据库。希望本文能为你搭建自己的本地 RAG 系统提供参考与启发。