基于Ollama + Vectra + MCP從零搭建本地RAG知識(shí)庫的實(shí)現(xiàn)步驟
一、前言
我們要知道,目前所有的LLM都是推理模型,只能基于自身已經(jīng)訓(xùn)練好的語料庫進(jìn)行回應(yīng)。而常見的LLM都是通用模型,什么都知道一點(diǎn),但是不知道私域數(shù)據(jù),比如公司的財(cái)務(wù)報(bào)表。在一些私有領(lǐng)域,我們想要讓LLM能接入這部分?jǐn)?shù)據(jù),我們有這么幾種方法:
- 全量微調(diào),即把LLM的知識(shí)全部洗掉重新用私域數(shù)據(jù)訓(xùn)練(效果更好,但成本高)
- 微量微調(diào),不動(dòng)LLM原有的知識(shí),只是將私域數(shù)據(jù)加入其中(效果差,成本低)
- 構(gòu)建本地的向量數(shù)據(jù)庫(本文使用,效果好,成本低,數(shù)據(jù)安全,知識(shí)可實(shí)時(shí)更新)
二、RAG
RAG全稱 Retrieval-Augmented Generation(檢索增強(qiáng)生成),它可以讓LLM擁有訪問私域數(shù)據(jù)能力,并且可以用于 context engineering (上下文工程):致力于讓LLM的上下文編排更合理。
RAG工作原理:構(gòu)建本地向量數(shù)據(jù)庫,將用戶問題和LLM的回答都存入本地向量數(shù)據(jù)庫中,當(dāng)用戶發(fā)起新問題時(shí),先去向量數(shù)據(jù)庫做相似度查找,找出相似度最高的那幾條數(shù)據(jù),攜帶上傳給LLM,這樣就能有效的控制上下文長(zhǎng)度。
什么是向量?向量就是用一串?dāng)?shù)字表示文本語義,相似文本的向量距離小。
向量相似度算法:余弦距離/歐式舉例/點(diǎn)積值等。
三、環(huán)境準(zhǔn)備
- 安裝Ollama + 拉取一個(gè)向量模型(本文使用 nomic-embed-text 向量模型)
- 項(xiàng)目依賴:pnpm i ollama vectra @modelcontextprotocol/sdk zod3
- 本文使用ESM模塊(“type”:“module”)
四、手把手實(shí)現(xiàn)
4.1 文本向量化(Embedding 封裝)
我們先引入 ollama:
import ollama from 'ollama'
然后封裝一個(gè)getEmbedding()用來將短文本處理成向量:
export function getEmbedding(text) {
return ollama.embeddings({
model: 'nomic-embed-text:latest',
prompt: text
})
}調(diào)用ollama上的向量模型處理輸入的文本。
接下來我們封裝一個(gè)splitText()函數(shù)用來將文本分塊:使用滑動(dòng)窗口策略,chunkSize = 300每300字符切割一次,overlap = 50避免在語義邊界處切斷,保留上下文連續(xù)性。
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
}最后封裝一個(gè)getEmbeddings()函數(shù)將長(zhǎng)文本先分塊再逐塊轉(zhuǎn)向量,并拋出函數(shù):
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]}
}))
}這樣我們就封裝好了一個(gè)將文本向量化的函數(shù)。
4.2 構(gòu)建本地向量數(shù)據(jù)庫 (SimpleRag類)
首先我們引入代碼中要用到的方法:
import path from 'node:path'
import { LocalIndex } from 'vectra'
import { getEmbeddings, getEmbedding } from './utils/index.js'
然后拋出一個(gè)類:
export class SimpleRag {
db = null
indexPath = ''
constructor(indexPath = '.vectra') {
this.indexPath = path.join(import.meta.dirname, '..', indexPath) // 將要?jiǎng)?chuàng)建的向量數(shù)據(jù)庫文件夾放在上級(jí)目錄下
}
接著在類中封裝一個(gè)initialize()方法用來初始化向量數(shù)據(jù)庫:
async initialize() {
const index = new LocalIndex(this.indexPath) // 指明在這個(gè)路徑下創(chuàng)建倉庫
if (! (await index.isIndexCreated())) { // 查找當(dāng)前位置是否已經(jīng)具有數(shù)據(jù)庫
await index.createIndex() // 創(chuàng)建數(shù)據(jù)庫
}
this.db = index
}后面我們要分別封裝向量數(shù)據(jù)庫的增加、刪除、修改方法,所以我們先寫一個(gè)方法判斷向量數(shù)據(jù)庫是否已存在:
get available() {
return this.db !== null
}
往數(shù)據(jù)庫中寫入數(shù)據(jù):
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}))
}
刪除數(shù)據(jù):
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
}
查找數(shù)據(jù):
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,
simularity: score,
id: item.id
}))
}
這樣我們就能用SimpleRag類創(chuàng)建實(shí)例對(duì)象來調(diào)用這個(gè)類中的初始化向量數(shù)據(jù)庫、向量數(shù)據(jù)庫的增、刪、查方法。
4.3 用 MCP 自動(dòng)化構(gòu)建知識(shí)庫
因?yàn)槭謩?dòng)收集文檔、手動(dòng)插入太低效,所以我們用MCP Server 注冊(cè)工具,讓LLM 自主完成項(xiàng)目文檔的提取和向量化。
首先,我準(zhǔn)備了一份提示詞作為操作指南,于是先寫一個(gè)prompt 模板加載器用來加載這份提示詞:
import { join } from 'path'
import { promises as fs} from 'fs'
const getCurrentDir = () => import.meta.dirname // 文件夾的絕對(duì)路徑
export async function loadPrompt(promptName) {
try {
// 獲取當(dāng)前文件目錄地址
const currentDir = getCurrentDir()
const promptPath = join(currentDir, 'prompts', `${promptName}.md`)
// 讀取提示詞,返回內(nèi)容
const content = await fs.readFile(promptPath, 'utf-8')
return content
} catch (error) {
throw new Error(`讀取${promptName}失敗:${error.message}`)
}
}
然后創(chuàng)建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'
// 創(chuàng)建一個(gè) MCPServer
const server = new McpServer({
name: 'AskYourLib',
version: '1.0.0'
})
然后在MCP上注冊(cè)兩個(gè)工具:
- ask-your-lib-initialize :初始化向量數(shù)據(jù)庫 + 返回操作指南(prompt 模板)
- ask-your-lib-insert :向量化并插入文本
Prompt 模板設(shè)計(jì) ( generate.md ):指導(dǎo) LLM 三步走
- 初始化索引
- 遍歷項(xiàng)目提取核心內(nèi)容
- 逐段向量化插入數(shù)據(jù)庫
let simpleRagInstance = null
server.tool(
'ask-your-lib-initialize',
'Initialize the vector database operations and clean up any existing .vectra directory.',
{},
async () => {
try {
// 先看 .vectra 這個(gè)目錄是否存在,存在就移除
const projectRoot = path.join(import.meta.dirname, '../../')
const vectraPath = path.join(projectRoot, '.vectra')
const generateMCPPrompt = await loadPrompt('generate') // 加載一份提示詞
try {
await fs.access(vectraPath) // 先探明是否有權(quán)限操作這個(gè)目錄
await fs.rm(vectraPath, { recursive: true, force: true }) // 移除已有的目錄
console.log('成功刪除已存在的 .vectra 目錄');
} catch (error) {
console.log('.vectra 目錄不存在,無需刪除');
}
// 創(chuàng)建 SimpleRag
simpleRagInstance = new SimpleRag()
// 返回一份提示詞,用于告訴 LLM 下一步該干什么
return {
content: [{
type: 'text',
text: `?? The guide to follow: \n${generateMCPPrompt}\n\n`
}]
}
} 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() // 本地創(chuàng)建一個(gè)新的 .vectra 目錄
}
const result = await simpleRagInstance.add(text) // 寫入本地向量數(shù)據(jù)庫
return {
content: [{
type: 'text',
text: `Text inserted successfully. Inserted items: ${JSON.stringify(result)}`
}]
}
} catch (error) {
console.error(`文本寫入數(shù)據(jù)庫失?。?{error}`)
return {
content: [{
type: 'text',
text: `Error inserting text:${error}`
}]
}
}
}
)
最后將服務(wù)端啟動(dòng):
async function main() {
const transport = new StdioServerTransport()
await server.connect(transport)
console.log('服務(wù)端啟動(dòng)');
}
main()
4.4 RAG + LLM 完整鏈路
graph LR 收集項(xiàng)目文檔 --> 通過embedding向量化得到向量 --> 存入向量數(shù)據(jù)庫
graph LR 用戶提問 --> 通過embedding向量化得到向量 --> 在向量數(shù)據(jù)庫中做相似度查找 --> 整合好的提示詞 --> LLM輸出
我們用node.js簡(jiǎn)單實(shí)現(xiàn)一下:
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: `你是一個(gè)香香軟軟一米五愛玩原神白毛紅瞳蘿莉,回答問題會(huì)基于當(dāng)前的項(xiàng)目,如果上下文沒有相關(guān)的信息,就回答“我不知道”,不要自己編造信息。\n\nContext:\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 運(yùn)行這份代碼,在后面接上問題:

五、RAG的優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn) :私域數(shù)據(jù)安全、成本低于微調(diào)、知識(shí)實(shí)時(shí)更新、可溯源、減少幻覺
- 缺點(diǎn) :依賴檢索質(zhì)量、chunk 策略敏感、延遲較高、不支持復(fù)雜推理
六、總結(jié)
本文從 LLM 無法訪問私域數(shù)據(jù)的痛點(diǎn)出發(fā),完整實(shí)現(xiàn)了一個(gè)基于 Ollama + Vectra + MCP 的本地 RAG 知識(shí)庫系統(tǒng)。
RAG 的本質(zhì)是:用檢索代替記憶,用外部知識(shí)庫擴(kuò)展 LLM 的能力邊界 。掌握了它,你就掌握了讓 LLM "懂私域數(shù)據(jù)"最具性價(jià)比的方案。
以上就是基 Ollama + Vectra + MCP從零搭建本地RAG知識(shí)庫的實(shí)現(xiàn)步驟的詳細(xì)內(nèi)容,更多關(guān)于Ollama搭建本地RAG知識(shí)庫的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章

如何修改Ollama及其模型的默認(rèn)路徑?Ollama路徑修改、遷移與重啟的操作指南
想要釋放C盤空間或優(yōu)化AI模型管理,本文主要為大家整理了Ollama路徑修改與遷移完整指南,即使不熟悉命令行,也能輕松學(xué)會(huì)將Ollama程序和模型遷移到任意磁盤,立刻解決存儲(chǔ)壓力2026-07-14
Ollama模型路徑配置OLLAMA_MODELS環(huán)境變量使用實(shí)戰(zhàn)
Ollama作為輕量級(jí)本地大模型運(yùn)行時(shí),其模型存儲(chǔ)路徑并非固定,而是由環(huán)境變量動(dòng)態(tài)控制,OLLAMA_MODELS環(huán)境變量?jī)?yōu)先級(jí)最高,本文就來詳細(xì)的介紹一下OLLAMA_MODELS環(huán)境變量的2026-07-14
本文教你如何更改Linux下Ollama模型下載路徑,包括停止服務(wù)、設(shè)置root權(quán)限、修改service文件和重啟Ollama,立即掌握詳細(xì)步驟,避免存儲(chǔ)空間不足問題,需要的朋友可以參考下2026-07-13
在Linux平臺(tái)上Ollama安裝與手動(dòng)升級(jí)版本
想在Ubuntu上運(yùn)行本地AI模型,掌握Ollama的無網(wǎng)手動(dòng)安裝與升級(jí)是第一步,本文教你通過下載壓縮包,離線部署DeepSeek-R1、Gemma3等超強(qiáng)模型,告別網(wǎng)絡(luò)失敗,從創(chuàng)建服務(wù)到配置并2026-07-13
本文主要介紹了Ollamaa本地大模型工具常見崩潰原因因解析,聚焦硬件兼容與顯存管理,文章詳細(xì)探討了五大崩潰場(chǎng)景,包括模型啟動(dòng)崩潰、運(yùn)行時(shí)后停止響應(yīng)及GGUF模型加載失敗,并2026-06-29
Ollama幾乎是本地模型部署的標(biāo)配答案,本文為大家詳細(xì)介紹了Ollama本地大模型運(yùn)行框架,簡(jiǎn)化了大模型的本地部署過程,支持多種量化等級(jí)和API接口兼容性,適用于個(gè)人開發(fā)者和企2026-06-28
Ollama中本地大模型部署與運(yùn)行深度評(píng)測(cè)詳解
本文對(duì)開源本地大模型運(yùn)行工具Ollama進(jìn)行了全面深度評(píng)測(cè),涵蓋硬件兼容性、性能表現(xiàn)、功能特性、安全性等10個(gè)核心維度,基于2026年最新版本(v0.19.0)的實(shí)測(cè)數(shù)據(jù),結(jié)合客觀2026-06-01
這段文章詳細(xì)介紹了Ollama、LMStudio、TextGenerationWebUI和vLLM四種部署方式,適合不同模型的本地和企業(yè)環(huán)境部署,文章還提供了Ollama的下載指南和免費(fèi)開源大模型的推薦版2026-05-25
Windows版Ollama強(qiáng)制安裝到C盤的五種方案
Ollama 官方 Windows 安裝程序沒有提供路徑選擇,直接雙擊就只有“Install”按鈕,但我們可以通過以下方法實(shí)現(xiàn)自定義安裝路徑,需要的朋友可以參考下2026-05-20
2026最新Linux本地部署Ollama安裝全流程(含離線/開機(jī)自啟/遠(yuǎn)程訪問)
本文記錄在 CentOS 7+ / Ubuntu 20.04+ 上部署 Ollama 的實(shí)操筆記,覆蓋 一鍵在線安裝 與 離線 tar 包安裝 兩種方式,并補(bǔ)充 systemd 開機(jī)自啟、qwen2 / deepseek-r1 等模2026-05-19










