首页 > AI教程 >Ollama+Vectra+MCP实现本地RAG知识库搭建步骤

Ollama+Vectra+MCP实现本地RAG知识库搭建步骤

来源:互联网 2026-07-16 06:09:07

基于Ollama、Vectra和MCP搭建本地RAG知识库,实现私域数据安全高效访问。通过文本向量化、本地向量数据库构建及MCP自动化工具,使大语言模型能检索私域数据并实时更新,兼顾低成本与数据安全。

本教程将从零开始,带你一步步构建基于Ollama + Vectra + MCP的本地RAG知识库系统,让大语言模型(LLM)能够安全、高效地访问你的私域数据。无论你是AI初学者还是开发者,都能通过本文的详细步骤,掌握RAG的核心原理与实现。

一、前言

我们要知道,目前所有的LLM都是推理模型,只能基于自身已经训练好的语料库进行回应。而常见的LLM都是通用模型,什么都知道一点,但是不知道私域数据,比如公司的财务报表。在一些私有领域,我们想要让LLM能接入这部分数据,我们有这么几种方法:

长期稳定更新的攒劲资源: >>>点此立即查看<<<

  • 全量微调,即把LLM的知识全部洗掉重新用私域数据训练(效果更好,但成本高)
  • 微量微调,不动LLM原有的知识,只是将私域数据加入其中(效果差,成本低)
  • 构建本地的向量数据库(本文使用,效果好,成本低,数据安全,知识可实时更新)

提示:对于大多数中小团队或个人开发者,构建本地向量数据库是性价比最高、最灵活的方式,因为无需重新训练模型,且能随时更新知识。

二、RAG

RAG全称 Retrieval-Augmented Generation(检索增强生成),它可以让LLM拥有访问私域数据能力,并且可以用于 context engineering (上下文工程):致力于让LLM的上下文编排更合理。

RAG工作原理:构建本地向量数据库,将用户问题和LLM的回答都存入本地向量数据库中,当用户发起新问题时,先去向量数据库做相似度查找,找出相似度最高的那几条数据,携带上传给LLM,这样就能有效的控制上下文长度。

什么是向量?向量就是用一串数字表示文本语义,相似文本的向量距离小。
向量相似度算法:余弦距离/欧式举例/点积值等。

注意:向量数据库的质量直接影响RAG的检索效果,选择合适的向量模型和分块策略至关重要。

三、环境准备

  1. 安装Ollama + 拉取一个向量模型(本文使用 nomic-embed-text 向量模型)
  2. 项目依赖:pnpm i ollama vectra @modelcontextprotocol/sdk zod3
  3. 本文使用ESM模块("type":"module"

小提示:如果你还没有安装Ollama,请先访问官网下载并安装,然后在终端运行 ollama pull nomic-embed-text 拉取向量模型。

四、手把手实现

4.1 文本向量化(Embedding 封装)

我们先引入 ollama:

import ollama from 'ollama'

然后封装一个getEmbedding()用来将短文本处理成向量:

export function getEmbedding(text) {
    return ollama.embeddings({
        model: 'nomic-embed-text:latest',
        prompt: text
    })
}

调用ollama上的向量模型处理输入的文本。

接下来我们封装一个splitText()函数用来将文本分块:使用滑动窗口策略,chunkSize = 300每300字符切割一次,overlap = 50避免在语义边界处切断,保留上下文连续性。

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
}

最后封装一个getEmbeddings()函数将长文本先分块再逐块转向量,并抛出函数:

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]}
    }))
}

这样我们就封装好了一个将文本向量化的函数。

提示:你可以根据实际文本长度调整chunkSizeoverlap,例如对于长文档可增大chunkSize到500,但过大会降低检索精度。

4.2 构建本地向量数据库 (SimpleRag类)

首先我们引入代码中要用到的方法:

import path from 'node:path'
import { LocalIndex } from 'vectra'
import { getEmbeddings, getEmbedding } from './utils/index.js'

然后抛出一个类:

export class SimpleRag {
    db = null
    indexPath = ''

    constructor(indexPath = '.vectra') {
        this.indexPath = path.join(import.meta.dirname, '..', indexPath)  // 将要创建的向量数据库文件夹放在上级目录下
    }

接着在类中封装一个initialize()方法用来初始化向量数据库:

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

后面我们要分别封装向量数据库的增加、删除、修改方法,所以我们先写一个方法判断向量数据库是否已存在:

get available() {
        return this.db !== null
    }

往数据库中写入数据:

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)
        }
        return res.filter(item => item).map(item => ({id: item.id}))
    }

删除数据:

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
    }

查找数据:

async query(text, topk = 1) {
        if(!this.available) throw new Error('RAG 还没初始化')
        const vector = (await getEmbedding(text)).embedding
        const result = await this.db.queryItems(vector, text, topk)
        return result.map(({item, score}) => ({
            text: item.metadata.text,
            query: text,
            similarity: score,
            id: item.id
        }))

    }

这样我们就能用SimpleRag类创建实例对象来调用这个类中的初始化向量数据库、向量数据库的增、删、查方法。

注意:vectra是一个轻量级本地向量数据库,如果要处理海量数据,建议使用专门的向量数据库如Chroma、Pinecone等。

4.3 用 MCP 自动化构建知识库

因为手动收集文档、手动插入太低效,所以我们用MCP Server 注册工具,让LLM 自主完成项目文档的提取和向量化。

首先,我准备了一份提示词作为操作指南,于是先写一个prompt 模板加载器用来加载这份提示词:

import { join } from 'path'
import { promises as fs} from 'fs'

const getCurrentDir = () => import.meta.dirname  // 文件夹的绝对路径

export async function loadPrompt(promptName) {
    try {
        // 获取当前文件目录地址
        const currentDir = getCurrentDir()

        const promptPath = join(currentDir, 'prompts', `${promptName}.md`)

        // 读取提示词,返回内容
        const content = await fs.readFile(promptPath, 'utf-8')
        return content
    } catch (error) {
        throw new Error(`读取${promptName}失败:${error.message}`)
    }
}

然后创建MCP Server:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
import { SimpleRag } from '../index.js'
import path from 'path'
import fs from 'fs/promises'
import { loadPrompt } from './utils.js'


// 创建一个 MCPServer
const server = new McpServer({
    name: 'AskYourLib',
    version: '1.0.0'
})

然后在MCP上注册两个工具:

  • ask-your-lib-initialize :初始化向量数据库 + 返回操作指南(prompt 模板)
  • ask-your-lib-insert :向量化并插入文本

Prompt 模板设计 ( generate.md ):指导 LLM 三步走

  1. 初始化索引
  2. 遍历项目提取核心内容
  3. 逐段向量化插入数据库
let simpleRagInstance = null

server.tool(
    'ask-your-lib-initialize',
    'Initialize the vector database operations and clean up any existing .vectra directory.',
    {},
    async () => {
        try {
            // 先看 .vectra 这个目录是否存在,存在就移除
            const projectRoot = path.join(import.meta.dirname, '../../')
            const vectraPath = path.join(projectRoot, '.vectra')
            const generateMCPPrompt = await loadPrompt('generate')  // 加载一份提示词


            try {
                await fs.access(vectraPath)  // 先探明是否有权限操作这个目录
                await fs.rm(vectraPath, { recursive: true, force: true })  // 移除已有的目录
                console.log('成功删除已存在的 .vectra 目录');

            } catch (error) {
                console.log('.vectra 目录不存在,无需删除');
            }

            // 创建 SimpleRag
            simpleRagInstance = new SimpleRag()

            // 返回一份提示词,用于告诉 LLM 下一步该干什么
            return {
                content: [{
                    type: 'text',
                    text: ` The guide to follow: n${generateMCPPrompt}nn`
                }]
            }
        } catch (error) {
            console.error(`初始化SimpleRag失败:${error}`)
            return {
                content: [{
                    type: 'text',
                    text: `初始化SimpleRag失败:${error}`
                }]
            }
        }
    }
)

server.tool(
    'ask-your-lib-insert',
    `Insert and vectorize text content into the vector database.`,
    {
        text: z.string()
    },
    async ({ text }) => {
        try {
            if (!simpleRagInstance) {
                return {
                    content: [
                        {
                            type: 'text',
                            text: 'Database instance is not initialized. Please call ask-your-lib-initialize first.'
                        }
                    ],
                };
            }

            if (!simpleRagInstance.available) {
                await simpleRagInstance.initialize()   // 本地创建一个新的 .vectra 目录
            }

            const result = await simpleRagInstance.add(text)  // 写入本地向量数据库

            return {
                content: [{
                    type: 'text',
                    text: `Text inserted successfully. Inserted items: ${JSON.stringify(result)}`
                }]
            }
        } catch (error) {
            console.error(`文本写入数据库失败:${error}`)
            return {
                content: [{
                    type: 'text',
                    text: `Error inserting text:${error}`
                }]
            }
        }
    }
)

最后将服务端启动:

async function main() {
    const transport = new StdioServerTransport()
    await server.connect(transport)
    console.log('服务端启动');
    
}
main()

小提示:MCP Server 需要与支持MCP协议的客户端(如Claude Desktop、VS Code扩展等)配合使用,才能发挥自动化构建知识库的能力。

4.4 RAG + LLM 完整链路

graph LR
收集项目文档 --> 通过embedding向量化得到向量 --> 存入向量数据库
graph LR
用户提问 --> 通过embedding向量化得到向量 --> 在向量数据库中做相似度查找 --> 整合好的提示词 --> LLM输出

我们用node.js简单实现一下:

import { SimpleRag } from '../src/index.js'
import ollama from 'ollama'

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: `你是一个香香软软一米五爱玩原神白毛红瞳萝莉,回答问题会基于当前的项目,如果上下文没有相关的信息,就回答"我不知道",不要自己编造信息。nnContext:n${JSON.stringify(res)}`,
    }
    ,
    {
        role: 'user',
        content: question
    }
]

const response = await ollama.chat({
    model: 'qwen3.5:9b',
    messages,
    stream: true,
})
for await (const chunk of response) {
    process.stdout.write(chunk.message.content)
}

}


main()

我这里接入的是用ollama本地部署的qwen3.5:9b模型。

然后用node 运行这份代码,在后面接上问题:

Ollama+Vectra+MCP实现本地RAG知识库搭建步骤

注意:请确保你已经安装了ollama并拉取了qwen3.5:9b模型(或替换为你自己的模型),否则问答环节会失败。

五、特色功能与优缺点

特色功能

  • MCP自动化:通过MCP Server注册工具,让LLM自主完成文档提取和向量化,无需手动干预。
  • 实时更新:知识库可以随时添加新文档,无需重新训练模型。
  • 数据安全:所有数据存储在本地,不依赖外部API,适合敏感数据场景。
  • 可溯源:每次回答都可以追溯到具体的知识片段,便于验证。

优缺点

  • 优点:私域数据安全、成本低于微调、知识实时更新、可溯源、减少幻觉
  • 缺点:依赖检索质量、chunk 策略敏感、延迟较高、不支持复杂推理

提示:如果对延迟敏感,可以尝试使用更快的向量模型(如bge-small)或降低chunk大小,但可能牺牲一定精度。

六、常见问题

Q1: 向量模型(nomic-embed-text)是否必须?我可以换成其他模型吗?

可以。Ollama支持多种向量模型,如all-minilmbge-m3等。只需在getEmbedding()中修改model参数即可。但要注意,不同模型的向量维度可能不同,需确保与vectra兼容。

Q2: chunkSize和overlap应该如何设置?

这取决于你的文本类型。对于技术文档,推荐chunkSize=300-500overlap=50-100。如果文本中有大量长句,可适当增大chunkSize;如果希望保留更多上下文,可增大overlap。建议通过实验对比检索效果来调整。

Q3: MCP Server如何与我的项目集成?

MCP Server通常需要配合支持MCP协议的客户端使用。例如,在Claude Desktop中,你可以配置MCP服务器地址,让Claude调用你的工具。对于VS Code扩展,可以安装MCP扩展并配置。具体集成方式请参考MCP官方文档。

Q4: 我的数据量很大,vectra本地数据库够用吗?

vectra是轻量级的本地嵌入存储,适合中小规模数据(几千到几万条记录)。如果数据量达到百万级,建议使用更专业的向量数据库如Chroma、Weaviate或Pinecone,它们支持分布式存储和高效检索。

Q5: 为什么我运行后LLM回答不准确?

可能原因:1) 向量检索未找到相关片段,检查chunk策略是否合理;2) 系统提示词不正确,确保LLM正确使用上下文;3) 向量模型与文本语言不匹配,中文文本建议使用中文向量模型(如bge-m3)。

总结

本文从 LLM 无法访问私域数据的痛点出发,完整实现了一个基于 Ollama + Vectra + MCP 的本地 RAG 知识库系统。通过文本向量化、本地向量数据库构建、MCP自动化工具注册以及全链路问答,你已掌握让LLM"懂私域数据"最具性价比的方案。RAG的本质是:用检索代替记忆,用外部知识库扩展LLM的能力边界。现在,你可以将这一方案应用到你的项目中,开启智能知识管理之旅。

侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述

热游推荐

更多
湘ICP备14008430号-1 湘公网安备 43070302000280号
All Rights Reserved
本站为非盈利网站,不接受任何广告。本站所有软件,都由网友
上传,如有侵犯你的版权,请发邮件给xiayx666@163.com
抵制不良色情、反动、暴力游戏。注意自我保护,谨防受骗上当。
适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。