基于Ollama、Vectra和MCP搭建本地RAG知识库,实现私域数据安全高效访问。通过文本向量化、本地向量数据库构建及MCP自动化工具,使大语言模型能检索私域数据并实时更新,兼顾低成本与数据安全。
本教程将从零开始,带你一步步构建基于Ollama + Vectra + MCP的本地RAG知识库系统,让大语言模型(LLM)能够安全、高效地访问你的私域数据。无论你是AI初学者还是开发者,都能通过本文的详细步骤,掌握RAG的核心原理与实现。
我们要知道,目前所有的LLM都是推理模型,只能基于自身已经训练好的语料库进行回应。而常见的LLM都是通用模型,什么都知道一点,但是不知道私域数据,比如公司的财务报表。在一些私有领域,我们想要让LLM能接入这部分数据,我们有这么几种方法:
长期稳定更新的攒劲资源: >>>点此立即查看<<<
提示:对于大多数中小团队或个人开发者,构建本地向量数据库是性价比最高、最灵活的方式,因为无需重新训练模型,且能随时更新知识。
RAG全称 Retrieval-Augmented Generation(检索增强生成),它可以让LLM拥有访问私域数据能力,并且可以用于 context engineering (上下文工程):致力于让LLM的上下文编排更合理。
RAG工作原理:构建本地向量数据库,将用户问题和LLM的回答都存入本地向量数据库中,当用户发起新问题时,先去向量数据库做相似度查找,找出相似度最高的那几条数据,携带上传给LLM,这样就能有效的控制上下文长度。
什么是向量?向量就是用一串数字表示文本语义,相似文本的向量距离小。
向量相似度算法:余弦距离/欧式举例/点积值等。
注意:向量数据库的质量直接影响RAG的检索效果,选择合适的向量模型和分块策略至关重要。
pnpm i ollama vectra @modelcontextprotocol/sdk zod3"type":"module")小提示:如果你还没有安装Ollama,请先访问官网下载并安装,然后在终端运行 ollama pull nomic-embed-text 拉取向量模型。
我们先引入 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]}
}))
}
这样我们就封装好了一个将文本向量化的函数。
提示:你可以根据实际文本长度调整chunkSize和overlap,例如对于长文档可增大chunkSize到500,但过大会降低检索精度。
首先我们引入代码中要用到的方法:
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等。
因为手动收集文档、手动插入太低效,所以我们用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上注册两个工具:
Prompt 模板设计 ( generate.md ):指导 LLM 三步走
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扩展等)配合使用,才能发挥自动化构建知识库的能力。
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并拉取了qwen3.5:9b模型(或替换为你自己的模型),否则问答环节会失败。
提示:如果对延迟敏感,可以尝试使用更快的向量模型(如bge-small)或降低chunk大小,但可能牺牲一定精度。
可以。Ollama支持多种向量模型,如all-minilm、bge-m3等。只需在getEmbedding()中修改model参数即可。但要注意,不同模型的向量维度可能不同,需确保与vectra兼容。
这取决于你的文本类型。对于技术文档,推荐chunkSize=300-500,overlap=50-100。如果文本中有大量长句,可适当增大chunkSize;如果希望保留更多上下文,可增大overlap。建议通过实验对比检索效果来调整。
MCP Server通常需要配合支持MCP协议的客户端使用。例如,在Claude Desktop中,你可以配置MCP服务器地址,让Claude调用你的工具。对于VS Code扩展,可以安装MCP扩展并配置。具体集成方式请参考MCP官方文档。
vectra是轻量级的本地嵌入存储,适合中小规模数据(几千到几万条记录)。如果数据量达到百万级,建议使用更专业的向量数据库如Chroma、Weaviate或Pinecone,它们支持分布式存储和高效检索。
可能原因:1) 向量检索未找到相关片段,检查chunk策略是否合理;2) 系统提示词不正确,确保LLM正确使用上下文;3) 向量模型与文本语言不匹配,中文文本建议使用中文向量模型(如bge-m3)。
本文从 LLM 无法访问私域数据的痛点出发,完整实现了一个基于 Ollama + Vectra + MCP 的本地 RAG 知识库系统。通过文本向量化、本地向量数据库构建、MCP自动化工具注册以及全链路问答,你已掌握让LLM"懂私域数据"最具性价比的方案。RAG的本质是:用检索代替记忆,用外部知识库扩展LLM的能力边界。现在,你可以将这一方案应用到你的项目中,开启智能知识管理之旅。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述