RAG 是什么?带你从0搭建本地向量数据库

一、RAG是什么?

RAG(Retrieval-Augmented Generation 检索增强生成) ,是AI领域常用到的一种技术。主要有以下两种使用场景:

  1. 在一些私有领域,想要让LLM能接入这部分数据一般有三种方法:
  • 全量微调 (效果更好,成本高)
  • 轻量微调 (效果一般,成本低)
  • 构建本地的向量数据库(效果好,成本低)

第三种方法即为RAG,通过构建本地知识库的方式,LLM可以从该知识库中检索相关信息,并增强LLM生成的内容,这样就不需要去微调大模型也能让该大模型学习到一些私有领域的知识。

  1. 上下文工程(context engineering) ,要让LLM拥有记忆功能,首先想到的就是把用户之前的对话全部存放到上下文中,但这样会导致上下文被迅速占满。通过RAG手段,我们可以构建本地向量数据库,将用户之前和LLM的对话全部转为向量 并存入本地向量数据库中;当用户提问时,检索我们的向量数据库,找出与用户提的问题最相关的几条数据,仅将与用户提问相关的这几条数据交给LLM,这样就可以在大幅减少上下文消耗的同时,又能让LLM拥有记忆功能。

二、向量数据库

  • 刚才我们提到了向量,数学中的向量通常是二维或三维的,但embedding模型输出的是高维向量(几百到上千维不等),因此这里的向量也是多维的。

要想构建本地向量数据库,首先需要一个能将文本转换成向量的embedding模型,这里我们使用ollama本地模型nomic-embed-text;其次我们需要选择使用什么向量数据库,这里我们使用nodejs的简易本地向量数据库vectra。接下来我们在nodejs中实操一下RAG。

三、将文本转向量的embedding模型

我们在终端输入ollama pull nomic-embed-text,下载向量模型,然后来到js中测试一下:

pnpm i ollama在项目中安装ollama的SDK并引入,调用该向量模型。pnpm i vectra 顺便在项目中安装vectra本地轻量级向量数据库

js 复制代码
import ollama from 'ollama'

async function main(){
    const res = await ollama.embeddings({
        model:'nomic-embed-text:latest',
        prompt:'RAG是什么?'
    }) 
    console.log(res)
}
main()

运行代码,发现它给我们返回了一个对象,对象有一个embedding属性,值为一个长数组,里面有一大串数字,这些就是prompt中'RAG是什么'这段文字对应的向量

四、 封装将文本转向量的函数

  1. 短文本处理成向量时,直接用向量模型转换
js 复制代码
import ollama from 'ollama'

// 短文本处理成向量
 export function getEmbedding(text){
    return ollama.embeddings({
        model:'nomic-embed-text:latest',
        prompt:text
    })
}
  1. 如果用户输入的是一大串几百上千字的文档,我们就需要把长文本切片成短文本,以提高向量匹配准确度
js 复制代码
//长文本处理成向量
export async function getEmbeddings(text){
    const chunks = splitText(text)
    const embeddings = await Promise.all(chunks.map(chunk => getEmbedding(chunk)))
    return embeddings.map((embedding,i) => ({
        vector:embedding.embedding,
        metadata:{ text: chunks[i] }
    }))
   }
   
   //切片函数
 function splitText(text, chunkSize = 300,overlap = 50){
    const chunks = []
    let i = 0
    while(i < text.length){
        chunks.push(text.slice(i,i + chunkSize))
        i += chunkSize - overlap
    }


    return chunks
}

五、 封装向量数据库的增添、删除、查找功能

  1. 引入之前封装的文本转向量函数和一些必要的包
js 复制代码
import path from 'node:path'
import {LocalIndex} from 'vectra'
import { getEmbeddings ,getEmbedding} from './utils/index.js'
  1. 构建向量数据库,在项目根目录下初始化vectra数据库
js 复制代码
export class SimpleRag{
    db = null
    indexPath = ''

    constructor(indexPath = '.vectra'){
        this.indexPath = path.join(import.meta.dirname,'..',indexPath)//在项目根目录下
    }

    async initialize(){
        const index = new LocalIndex(this.indexPath)//在这个路径下创建仓库
        if(!(await index.isIndexCreated())){ //查找当前位置是否已经具有数据库
            await index.createIndex()//如果没有,就初始化一个数据库
        }
        this.db = index  //db指向这个仓库
    }

    //判断数据库是否存在
    get available(){
        return this.db !== null
    }
}
  1. 封装一个可以将文本转为向量并自动往向量数据库增添数据的函数
js 复制代码
async add(text){
        if(!this.available){
            throw new Error('RAG 未初始化')
        }

        const embeddings = await getEmbeddings(text)
        const res = []
        for(const embedding of embeddings){
            const overResult = await this.db.insertItem(embedding)
            
            res.push(overResult)
        }
        //filter 过滤掉null值,返回该向量对应的id
        return res.filter(item => item).map(item => ({id: item.id}))
    }
  1. 封装一个删除向量数据库中对应id的数据的函数
js 复制代码
async del(items){
        if(!Array.isArray(items)) items = [items]
        if(!this.available) throw new Error('RAG 未初始化')

        const res = []
        for(let item of items){
            await this.db.deleteItem(item.id)
            
            res.push({id: item.id})
        }
        return res

    }
  1. 封装一个在向量数据库中查找匹配度最高的数据的函数(默认返回匹配度最高的5条数据)
js 复制代码
async query(text,topk = 5){ //topk:返回几条数据
        if(!this.available) throw new Error('RAG 未初始化')
        const vector = (await getEmbedding(text)).embedding//新问题被处理成向量
        const result = await this.db.queryItems(vector,text,topk)
        // console.log(result)
        return result.map(({item,score}) => ({
            text:item.metadata.text,
            query:text,
            similarity:score,
            id:item.id,
        }))

    }
}

六、与本地LLM通信

  1. 引入前面我们封装的函数以及Ollama,自行往本地向量数据库中加入一些数据,看看与本地模型通信时他是否真的能根据本地向量数据库匹配的内容来回答我们的问题
js 复制代码
import ollama from 'ollama'
import { SimpleRag } from '../src/index.js'

async function main() {
  const rag = new SimpleRag()
  await rag.initialize() //创建并初始化向量数据库
  const question = process.argv[process.argv.length - 1] //直接在终端监听输入的内容
  const res = await rag.query(question) //在向量数据库中查找与该内容匹配最高的几条数据

  const messages = [
    {
      role: 'system',
      content: `你是一个专业的助手,回答问题会基于当前的项目,如果上下文没有相关的信息,
      就回答"我无法回答这个问题",不要自己编造信息。\n\nContext: \n${JSON.stringify(res)}`,
    }, //将查询到的匹配度高的几条数据传给LLM
    {
      role: 'user',
      content: question
    }
  ]

  const response = await ollama.chat({ //与本地LLM通信
    model: 'qwen3.5:2b',
    messages,
    stream: true
  })
  for await (const chunk of response) {
    process.stdout.write(chunk.message.content) //直接在终端流式输出
  }
}

main()

在终端输入 node .\ask-your-lib.js {你要问的问题} ,看看大模型的回答:

我提前往向量数据库写入了我这小项目的说明文档,可以看到大模型的回答确实被改变了,由于都是本地模型,回答地比较蠢也是正常的

七、总结

  • 收集项目文档 → 通过embedding向量化得到向量 → 存入向量数据库
  • 用户提问时 → 通过embedding向量化得到向量 → 在向量数据库中做相似度查找 → 整合好的提示词(包含用户问题和相似度最高的那几条数据的上下文) → LLM 输出
相关推荐
江华森1 天前
前端构建工具 Webpack 速览
前端·webpack·node.js
无情的西瓜皮1 天前
MCP 协议实战:从零搭建一个 MCP 服务器(Node.js 版)
vue.js·人工智能·typescript·node.js
学习-学习1 天前
Node.js 现在能直接跑 TypeScript 了?Type Stripping 原理、限制与 tsx / ts-node 取舍
typescript·node.js
霍格沃兹测试开发学社测试人社区1 天前
Node.js 浏览器引擎 + Python 大脑:Playwright 混合架构爬虫系统深度解析
python·架构·node.js
小林ixn1 天前
Node.js 文件系统与路径处理:从 API 到工程化实战
node.js·编辑器·vim
明理的信封2 天前
在 Windows 服务器上部署 Node.js 项目(小白实战教程)
服务器·windows·node.js
iDao技术魔方2 天前
Node.js v26.5.0 深度解读:import Text、流式 ReadableStreamTee、以及隐藏的安全加固浪潮
安全·node.js
网络点点滴2 天前
Node.js中模块引用是如何真正运作的
node.js
BAIGAOa2 天前
还在手写 if-else 响应按键?我用 React-Ink 搭配键盘引擎解决了
node.js