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

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,依次完成以下步骤:

  1. 点击「选择文件」,上传本地知识库文档(如产品手册、个人笔记等);
  2. 点击「构建知识库」,等待文本切分与向量化完成;
  3. 在输入框中输入问题,点击「发送」,系统将基于本地知识库生成回答。

6.2 优化方向

优化项 方案 收益
检索精度 引入 BM25 混合检索 兼顾语义与关键词匹配
推理速度 使用 WebGPU 加速 显著提升生成速度
知识库规模 接入 IndexedDB 持久化 支持更大规模文档
回答质量 使用更大参数模型 提升生成质量

7. 总结

本文从零到一搭建了一套完全运行在浏览器本地的轻量化 RAG 问答系统。通过 Transformers.js 完成文本向量化、自实现余弦相似度检索、WebLLM 在浏览器端运行大模型,实现了「上传文档 → 构建知识库 → 本地问答」的完整闭环。

这套方案的优势在于:

  • 零部署成本:无需服务器,打开浏览器即可使用;
  • 数据隐私安全:所有数据均在本地处理,不上传云端;
  • 技术栈统一:纯前端实现,降低维护成本。

当然,受限于浏览器端的计算资源,这套方案更适合知识库规模较小、对实时性要求不高的场景。对于生产级应用,仍建议结合后端服务与专业向量数据库。希望本文能为你搭建自己的本地 RAG 系统提供参考与启发。

相关推荐
夏炳辉.35 分钟前
Flex布局中 flex: 1 的完整解析与实战指南
前端·css·css3
CIO_Alliance1 小时前
AI提示系列(2)| Few-shot与ReAct有何不同? 大模型工具调用的底层逻辑详解
前端·人工智能·深度学习·神经网络·react.js·前端框架·ai+ipaas
cindershade1 小时前
别只收三个数字:前端 RUM 如何建立可解释的体验数据链
前端
前端 贾公子1 小时前
第09章:上下文与记忆 (4)
java·服务器·前端
Coodor2 小时前
使用web也可以写NFC微信小程序拉取
前端·微信小程序·小程序·nfc拉起小程序
Wang's Blog3 小时前
Vibe Coding一人即团队系列28: 前端、后端与数据库的概念解析
前端·数据库
Dovis(誓平步青云)3 小时前
模拟器横评:电脑上看小说、追短剧用什么模拟器?MuMu、雷电、腾讯手游助手实测
android·java·服务器·前端·javascript·电脑
踩蚂蚁3 小时前
把自定义唤醒词部署到 ESP32-S3:ONNX 转 INT8 TFLite 的完整链路
前端