从零实现前端 RAG 组件:Vue3+Pinia 打造本地化文档问答工具

138 阅读6分钟

从零实现前端 RAG 组件:Vue3+Pinia 打造本地化文档问答工具

在前端开发中,你是否遇到过这些问题:项目文档太多找不到关键信息?用户需要频繁查阅帮助文档影响使用体验?2025 年,RAG(检索增强生成)技术已成为前端智能组件的核心能力。本文将用 Vue3+Pinia + 本地向量库,从零实现一个可嵌入项目的 RAG 文档问答组件,无需后端也能实现智能检索。

一、技术选型与核心原理

1. 为什么选择这些技术?

  • Vue3+TypeScript:保证组件类型安全,Composition API 更适合复杂逻辑拆分
  • Pinia:管理向量数据和问答状态,支持跨组件共享
  • @xenova/transformers:前端本地化运行 BERT 模型,实现文本向量转换(无需调用外部 API)
  • lance-vector:轻量级前端向量数据库,支持快速 nearest-neighbor 检索

2. RAG 前端实现核心流程

  1. 文档处理:将 Markdown 文档分割为短句,转换为向量存储
  1. 用户查询:输入问题后,前端将问题转为向量
  1. 向量检索:在本地向量库中匹配最相关的文档片段
  1. 结果生成:拼接检索结果,可对接 LLM 生成自然语言回答(本文实现基础版)

二、环境准备与依赖安装

# 创建 Vue 项目(若已有项目可跳过)
npm create vue@latest frontend-rag-component -- --typescript --pinia
cd frontend-rag-component
# 安装核心依赖
npm install @xenova/transformers@2.17.2 lance-vector@0.1.15 markdown-it@14.1.0

三、核心组件实现

1. 向量处理工具封装(utils/vectorHelper.ts)

首先实现文本转向量和向量检索的核心工具:

import { AutoModel, AutoTokenizer } from '@xenova/transformers';
import { LanceVectorDB } from 'lance-vector';
// 单例模式初始化模型(避免重复加载)
let model: AutoModel | null = null;
let tokenizer: AutoTokenizer | null = null;
let vectorDB: LanceVectorDB | null = null;
// 初始化模型和向量库
export async function initVectorTools() {
  // 加载轻量级 BERT 模型(前端约 400MB,首次加载较慢)
  tokenizer = await AutoTokenizer.from_pretrained('Xenova/bert-base-uncased');
  model = await AutoModel.from_pretrained('Xenova/bert-base-uncased');
  // 初始化向量库(维度与 BERT 输出一致:768)
  vectorDB = await LanceVectorDB.create({
    dim: 768,
    path: 'local-rag-vector-db', // 本地存储路径
  });
  return { tokenizer, model, vectorDB };
}
// 文本转向量
export async function textToVector(text: string) {
  if (!model || !tokenizer) throw new Error('请先初始化向量工具');
  
  // 处理文本并获取模型输出
  const inputs = await tokenizer(text, { padding: true, truncation: true });
  const outputs = await model(inputs);
  
  // 取 [CLS]  token 的输出作为文本向量(BERT 标准做法)
  const vector = outputs.last_hidden_state[0][0].detach().array() as number[];
  return vector;
}
// 向量检索(返回 top5 相关结果)
export async function searchSimilarVectors(queryVector: number[], topK = 5) {
  if (!vectorDB) throw new Error('向量库未初始化');
  const results = await vectorDB.search(queryVector, { topK });
  return results.map(item => ({
    score: item.score, // 相似度分数(越小越相似)
    text: item.metadata?.text as string // 存储的文档文本
  }));
}

2. Pinia 状态管理(stores/ragStore.ts)

管理文档数据、向量库状态和问答记录:

import { defineStore } from 'pinia';
import { initVectorTools, textToVector, searchSimilarVectors } from '@/utils/vectorHelper';
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt();
export const useRagStore = defineStore('rag', {
  state: () => ({
    isReady: false, // 向量工具是否初始化完成
    documentChunks: [] as string[], // 文档片段
    history: [] as { question: string; answer: string }[], // 问答历史
  }),
  actions: {
    // 初始化向量工具和加载文档
    async initRag(documentContent: string) {
      this.isReady = false;
      try {
        // 1. 初始化向量模型和数据库
        await initVectorTools();
        
        // 2. 处理 Markdown 文档(分割为短句)
        const html = md.render(documentContent);
        const text = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' '); // 提取纯文本
        this.documentChunks = text.split(/[。!?;]/).filter(chunk => chunk.length > 10); // 分割并过滤短文本
        
        // 3. 将文档片段转为向量存入数据库
        for (const chunk of this.documentChunks) {
          const vector = await textToVector(chunk);
          await vectorDB?.add(vector, { text: chunk }); // 存储向量和元数据
        }
        
        this.isReady = true;
      } catch (error) {
        console.error('RAG 初始化失败:', error);
        throw error;
      }
    },
    // 处理用户查询
    async handleQuery(question: string) {
      if (!this.isReady) throw new Error('RAG 工具未准备好');
      
      // 1. 查询转为向量
      const queryVector = await textToVector(question);
      
      // 2. 检索相似文档片段
      const similarChunks = await searchSimilarVectors(queryVector);
      
      // 3. 生成回答(基础版:拼接相关片段,进阶版可对接 LLM)
      const answer = `找到以下相关信息:\n\n${similarChunks.map((item, i) => 
        `${i+1}. ${item.text}(相似度:${(1 - item.score).toFixed(2)})`
      ).join('\n\n')}`;
      
      // 4. 记录历史
      this.history.unshift({ question, answer });
      return answer;
    }
  }
});

3. RAG 问答组件(components/RagQaComponent.vue)

实现用户交互界面,包含文档上传、问题输入和结果展示:

<template>
  <div class="rag-qa-container">
    <!-- 文档上传区域 -->
    <div class="document-upload">
      <h3>1. 上传 Markdown 文档</h3>
      <input 
        type="file" 
        accept=".md" 
        @change="handleFileUpload"
        class="file-input"
      >
      <p v-if="!ragStore.isReady" class="loading-text">模型加载中...(首次加载约30秒)</p>
    </div>
    <!-- 问答区域 -->
    <div class="qa-area" v-if="ragStore.isReady">
      <h3>2. 提问关于文档的问题</h3>
      <div class="input-group">
        <input
          v-model="question"
          type="text"
          placeholder="例如:如何实现向量检索?"
          class="question-input"
        >
        <button @click="handleSubmit" class="submit-btn">查询</button>
      </div>
      <!-- 历史记录 -->
      <div class="history-container">
        <h4>问答历史</h4>
        <div 
          class="history-item" 
          v-for="(item, index) in ragStore.history" 
          :key="index"
        >
          <div class="question">Q:{{ item.question }}</div>
          <div class="answer">A:{{ item.answer }}</div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRagStore } from '@/stores/ragStore';
import { readAsText } from '@/utils/fileHelper'; // 需自行实现:封装 FileReader
const ragStore = useRagStore();
const question = ref('');
// 处理 Markdown 文件上传
const handleFileUpload = async (e: Event) => {
  const target = e.target as HTMLInputElement;
  if (!target.files?.[0]) return;
  const fileContent = await readAsText(target.files[0]);
  await ragStore.initRag(fileContent); // 初始化 RAG 并处理文档
};
// 提交查询
const handleSubmit = async () => {
  if (!question.value.trim()) return;
  await ragStore.handleQuery(question.value.trim());
  question.value = ''; // 清空输入框
};
</script>
<style scoped>
.rag-qa-container {
  max-width: 1000px;
  margin: 20px auto;
  padding: 0 20px;
}
.document-upload, .qa-area {
  margin-bottom: 30px;
  padding: 20px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
}
.loading-text {
  color: #6b7280;
  margin-top: 10px;
}
.input-group {
  display: flex;
  gap: 10px;
  margin-top: 10px;
}
.question-input {
  flex: 1;
  padding: 10px 15px;
  border: 1px solid #e5e7eb;
  border-radius: 4px;
  font-size: 14px;
}
.submit-btn {
  padding: 10px 20px;
  background-color: #3b82f6;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.history-container {
  margin-top: 20px;
}
.history-item {
  margin-top: 15px;
  padding: 10px;
  border-left: 3px solid #3b82f6;
  background-color: #f9fafb;
}
.question {
  font-weight: 600;
  margin-bottom: 5px;
}
.answer {
  color: #4b5563;
}
</style>

4. 文件工具封装(utils/fileHelper.ts)

简单封装 FileReader 方便读取 Markdown 文件:

export function readAsText(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result as string);
    reader.onerror = () => reject(reader.error);
    reader.readAsText(file);
  });
}

四、组件使用与优化

1. 在页面中引入组件(views/RagDemo.vue)

<template>
  <div class="rag-demo-page">
    <h2>前端 RAG 文档问答演示</h2>
    <RagQaComponent />
  </div>
</template>
<script setup lang="ts">
import RagQaComponent from '@/components/RagQaComponent.vue';
</script>
<style scoped>
.rag-demo-page {
  padding: 20px;
}
h2 {
  text-align: center;
  margin-bottom: 30px;
  color: #1f2937;
}
</style>

2. 关键优化点

  • 模型加载优化:首次加载模型较慢,可添加加载动画和缓存机制(利用 IndexedDB 存储模型文件)
  • 文档分割策略:本文用简单分割,可优化为按标题层级分割(如 ### 分割为独立片段)
  • 性能优化:向量库初始化和文档处理放在 Web Worker 中,避免阻塞主线程
  • 功能扩展:对接 Claude 3 或 GPT-4o 前端 SDK,实现更智能的回答生成

五、常见问题与解决方案

  1. 模型加载失败?
  • 检查网络:首次加载需要下载模型文件,确保网络稳定
  • 浏览器兼容性:需支持 ES6 Modules 和 WebAssembly(Chrome/Firefox 最新版推荐)
  1. 检索结果不准确?
  • 优化文档分割:确保每个片段主题单一
  • 更换更大模型:可尝试 Xenova/bert-large-uncased(精度更高,但体积更大)
  1. 本地存储满了?
  • 清理向量库:调用 vectorDB?.drop() 清空数据
  • 限制文档大小:建议单文档不超过 10MB

通过这个组件,你可以轻松将 RAG 能力嵌入到任何 Vue 项目中,无论是后台管理系统的帮助中心,还是产品的文档问答功能。如果需要实现更复杂的功能,比如多文档检索或对接云端 LLM。