Ollama + RAG 搭建 AI

一般来说个人部署 AI 服务是及其耗费时间和精力(还有可怕的满载电量和噪音), 但对于小规模的个人来说,
利用闲置的服务器设备部署个小型 AI 服务作为个人资料库其实也可以稍微玩玩.

而个人部署就推荐采用 ollama 来搭建, 按照官方文档来说其实最简单是采用 docker,
不过我这边本身就是闲置硬件也就是总结采用二进制安装部署就行, 不需要在套一层 docker 镜像.

注意: 本文涉及的很多网络相关可能需要 ‘工具’ 来处理, 否则网速基本上很慢没办法快捷部署

这里采用 ollama-linux 安装方式处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 如果之前安装过, 需要手动先卸载清空, 执行以下命令
cd /tmp # 现在临时目录, 二进制差不多2G左右
sudo rm -rf /usr/lib/ollama

# 下载安装 ollama 应用
# 需要注意这里安装的是 amd64 的架构, 如果你是 arm64 架构需要换成 ollama-linux-arm64.tgz
# 这里的安装包差不多 2G 并且因为在国外所以网络状况不好的时候很难下载下来, 最好自备工具处理这部分下载
curl -fsSL https://ollama.com/download/ollama-linux-amd64.tgz \
| sudo tar zx -C /usr


# 或者采用 github 代理下载功能, 去下载远程的 github releases 包
# github 地址: https://github.com/ollama/ollama/releases/download/v0.13.0/ollama-linux-amd64.tgz
# ghproxy.com 就是国内代理远程 github 加速下载, 具体访问 https://gh-proxy.com 获取 CDN 地址
# 这里的 v0.13.0 需要去官网上面确定最新版本安装
curl -fsSL https://edgeone.gh-proxy.org/https://github.com/ollama/ollama/releases/download/v0.13.0/ollama-linux-amd64.tgz \
| sudo tar zx -C /usr


# 如果已经下载好 ollama-linux-amd64.tgz 压缩包, 直接执行命令
sudo tar zxf ollama-linux-amd64.tgz -C /usr


# 下载安装需要很久, 视本地网速而定
# 查看安装的 ollama 版本: client version is 0.13.0
ollama -v


# 下载完成之后, 需要构建默认的执行用户
sudo useradd -r -m -d /usr/share/ollama -s /sbin/nologin ollama
sudo -u ollama touch /usr/share/ollama/ollama.conf # 追加单元环境变量配置

# 然后就是编写对应的启动服务
sudo vim /etc/systemd/system/ollama.service

/etc/systemd/system/ollama.service 系统单元文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Unit]
Description=Ollama Service
After=network-online.target local-fs.target
# 官方虽然没写, 但建议加上 local-fs.target 来保证模型放置在扩展盘的时候, 优先等待文件系统初始化后执行

[Service]
ExecStart=/usr/bin/ollama serve
User=ollama
Group=ollama
Restart=always
RestartSec=3
Environment="PATH=$PATH"

# 我这里追加个官方没有的动态环境配置功能, 方便编写外部环境配置而不用去手动修改服务单元
EnvironmentFile=/usr/share/ollama/ollama.conf


[Install]
WantedBy=multi-user.target

最后执行命令:

1
2
3
4
5
6
sudo systemctl daemon-reload # 更新服务
sudo systemctl start ollama # 启动服务
sudo systemctl enable ollama # 开机启动
sudo systemctl status ollama # 查看服务
# 打印 msg="Listening on 127.0.0.1:11434 (version 0.13.0)" 就代表服务启动
# 默认只监听本地网络地址, 如果为要暴露外部使用建议采用 nginx 这种代理转发出来

如果想要卸载, 官方也提供了卸载方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 系统单元清除
sudo systemctl stop ollama
sudo systemctl disable ollama
sudo rm /etc/systemd/system/ollama.service

# 删除二进制
sudo rm -r /usr/local/lib/ollama
sudo rm -r /usr/lib/ollama
sudo rm -r /bin/ollama

# 清除权限和目录
sudo userdel ollama
sudo groupdel ollama
sudo rm -r /usr/share/ollama

我实验好多次代理加速 Github 都没成功, 最后不得不直接 ‘工具’ 下载(开工具下载完压缩包之后提交的服务器).

哪怕用来加速下载速度也是 100~400KB/s 左右, 速度真的一言难尽, 所以才推荐全程用 ‘工具’ 来处理

后面就是细化一些处理方式, 这里需要明确我们这边闲置的服务器一般是 没有GPU的,
所以默认采用 CPU 运行并且要启用交换内存(Swap)来用虚拟内存替代 GPU显存压力:

1
2
# 这里就需要修改之前服务单元的 /usr/share/ollama/ollama.conf 环境文件
sudo vim /usr/share/ollama/ollama.conf

这里追加几个环境配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 是否开启测试模式, 用于测试本地是否生效
# 如果后续启动之后展示 DEBUG 日志就将其注释
OLLAMA_DEBUG=1

# 禁用CUDA(NVIDIA GPU)和ROCm(AMD GPU)检测,强制纯CPU运行
OLLAMA_CUDA=0
OLLAMA_ROCM=0

# 限制Ollama使用的CPU核心数(如4核,根据你的设备调整)
# 具体通过命令获取: grep -c ^processor /proc/cpuinfo | awk '{print $1}'
# 并且最好预留一个核心给系统用
OMP_NUM_THREADS=8

# 设备总内存16GB, 限制Ollama使用12GB内存
# 具体通过命令获取: free -h --giga
OLLAMA_MAX_MEMORY=12GiB

# 默认监听地址, 一般采用 NGINX 代理不用修改
OLLAMA_HOST=127.0.0.1

# 默认监听端口, 一般采用 NGINX 代理不用修改
OLLAMA_PORT=11434

# 允许跨域请求的源(解决前端调用的 CORS 问题, 一般采用 NGINX 代理不用修改)
OLLAMA_ORIGINS=*

# 默认修改模型|缓存|配置文件的存储路径(默认在~/.ollama, 也就是以 HOME 目录为根目录)
# 但是也可以手动处理下声明:
# OLLAMA_MODELS=/data/ollama/models # 指定模型文件的存储目录(核心和模型包通常占用大量空间)
# OLLAMA_CACHE=/tmp/ollama-cache # 指定推理时的缓存目录
# OLLAMA_CONFIG=/etc/ollama # 指定配置文件存储目录
# 不过最好方法还是创建执行用户的时候指定 HOME 目录到对应本地目录

保存重启 ollama 服务就可以确认显示 [DEBUG] 日志就代表生效, 记得把 OLLAMA_DEBUG=1 注释启用正式环境:

1
2
curl 127.0.0.1:11434
# 这里会输出 Ollama is running, 代表服务已经完全启动

那么应用服务部署好了, 接下来就是关于安装配置模型相关, 这里推荐几个方便闲置服务器来使用的模型:

  • GLM 4 9B(占用: 5.5GB): 智谱出品, 中文逻辑推理强,适合技术文档|论文的解读与总结
  • Baichuan 2 7B(占用: 4.2GB): 百川大模型, 中文对话自然, 适合个人日常笔记的问答与整理
  • Qwen 1.8B(占用: 1.2GB): 超轻量中文模型, CPU 占用极低, 适合知识库的快速检索
  • DeepSeek-R1-1.5B(占用: 1.1GB): 超轻量级模型, 适合做简单问答|基础文本生成|小型个人资料库交互等轻量任务

如果是个人服务器(主要信息是中文)推荐的是 Qwen 1.8B 或者 DeepSeek-R1-1.5B 模型, 这里我采用的是 DeepSeek-R1-1.5B,
首先执行输入安装命令:

1
2
3
# 因为挂载在 ollama 执行, 所以需要依赖 ollama 用户
sudo -u ollama ollama run deepseek-r1:1.5b
# 安装完成默认会进入交互命令行, 这就代表成功部署完成

这里就需要选择 GUI 客户端来交互, 这里提供两种方式:

默认的模型都是笨笨的, 类似以下对话他都无法满足:

1
2
- 你好, 北京时间现在几点?
- 您好,建议您联网获取时效性较强的信息;如果还有其他问题需要帮助,请随时告诉我!

这里我们需要做的就是以 DeepSeek-R1-1.5B 模型为基准去自定义模型并导入相关信息语料库(新闻|时间|资料等)

资料库

目前将外部资料导入到模型的方式有以下几种:

  • 检索增强生成(RAG): 简单标记资料导入到模型当中, 作为中心资料库(比如 ‘新闻’ 只能单独检索新闻而无法 ‘总结这个月新闻’)
  • 模型微调(深度定制,需重新训练): 对AI模型再度训练开发, 让他对数据做人性化识别(会对资料进行思考学习, 然后做自己判断)

如果是个人使用, 模型微调 需要极高的成本:

  • 16GB 内存(GPU 加速需 CUDA 支持)
  • 需要利用 LoRA 微调
  • 按照数据手动调整 num_ctx(上下文)| batch_size(批次)等参数

投入的成本十分巨大, 所以对于个人来说实际上没有什么必要, 大部分情况下作为个人资料直接采用 检索增强生成(RAG) 就行了.

而检索增强生成(RAG)来将咨询语料库文本导入到模型之中, 类似将数据按照以下方式导入:

lines
1
2
3
4
5
6
7
8
{
"input": "什么是大语言模型?",
"output": "大语言模型是基于海量文本数据训练的深度学习模型,能理解和生成人类语言..."
}
{
"input": "DeepSeek-R1的特点是什么?",
"output": "DeepSeek-R1-1.5B是轻量级大模型,兼顾推理速度和效果,适合边缘设备部署..."
}

注意: 最好把 ollamarag 服务分开部署, rag 可以连接到远程的 ollama 服务, 所以只需要内网联通过去即可

我们需要做的就是把这些文本处理成类似这样的结构, 然后在运行的时候挂载进模型即可;
这里需要先安装 Python 依赖库, 因为这里利用 Python 脚本做处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 需要确认权限用户有执行 pip3 的权限
sudo -u ollama pip3 --version

# Ollama 可结合第三方工具(如 LlamaIndex, LangChain)
# 注1: 可能有的依赖安装的时候需要 '工具' 来保证顺利下载安装
# 注2: 新版本 Python 可能要求做虚拟环境隔离, 如果是打算全局安装需要将命令改写成 'pip install --break-system-packages {依赖包}'
# 注3: 要记住后续操作全部都是挂靠 ollama 系统账号, 所有操作都要基于此来运行
# 注4: --user 会将包安装到 /home/ollama/.local/lib/pythonX.X/site-packages, 需确保该目录加入ollama用户的 Python 环境变量
sudo -u ollama pip3 install --user llama-index llama-index-llms-ollama llama-index-embeddings-ollama pypdf python-dotenv # 按需安装(如处理PDF语料就需要 pypdf)

# 安装 Python 组件很大概率出现 Connection aborted, 建议配置国内镜像源
# 这里采用临时性的阿里云拉取配置, 这样可能会更快点
sudo -u ollama pip3 install --user --break-system-packages -i https://mirrors.aliyun.com/pypi/simple llama-index llama-index-llms-ollama llama-index-embeddings-ollama pypdf python-dotenv

# 这里最简单的其实只需要以下组件
# 但是需要依赖更加深层配置嵌入模型, 利用嵌入模型用于语料向量化, 可复用 Ollama 的轻量模型, 如 nomic-embed-text
sudo -u ollama pip3 install --user llama-index llama-index-llms-ollama llama-index-embeddings-ollama

# 如果要利用嵌入模型辅助解析, 就需要在 ollama 安装辅助模型, 这里推荐 nomic-embed-text 轻量级模型
sudo -u ollama ollama pull nomic-embed-text

这里说明下什么是 LlamaIndex:

1
2
3
官方文档: https://docs.llamaindex.ai/
LlamaIndex 是一个基于LLM的应用程序的数据框架,该应用程序受益于上下文增强,是典型的RAG系统
LlamaIndex 提供了必要的抽象,可以更轻松地摄取、构建和访问私有或特定领域的数据,以便将这些数据安全可靠地注入 LLM 中,以实现更准确的文本生成

注意: 使用 LlamaIndex 最好能保证 4G+ 内存来运行数据分析

这里先简单应用下 LlamaIndex 解析工具, 这里考虑到都是基于 ollama 用户加载, 所以所有操作就挂靠在其中:

1
2
3
4
5
6
# 创建主要运行目录
sudo -u ollama mkdir -p /usr/share/ollama/rag

# 首先编写下创造些基础脚本
sudo -u ollama touch /usr/share/ollama/rag/basic.py # 测试运行脚本
sudo -u ollama touch /usr/share/ollama/rag/parser.py # 正式解析脚本

/usr/share/ollama/rag/basic.py 的基础测试脚本可以先处理下看看是否运行正常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from llama_index import GPTVectorStoreIndex

# 初始化 Llama-Index
index = GPTVectorStoreIndex()

# 添加单个文档
doc_text = "这是文档内容。"
index.insert(doc_text)

# 添加多个文档
doc_texts = ["文档1内容。", "文档2内容。"]
for text in doc_texts:
index.insert(text)

# 构建索引
index.build()


# 执行查询
query = "查询内容"
response = index.query(query)
print(response)

运行下确认是否能够命中信息集合:

1
2
3
4
5
6
7
8
sudo -u ollama python3 /usr/share/ollama/rag/basic.py
# 注意: 这里会报错提示 'ImportError: cannot import name 'GPTVectorStoreIndex' from 'llama_index' (unknown location)'
# 网上很多教程都是基于 llama-index v0.8 版本讲解, 后续版本 ABI 做了大规模的更新:
# from llama_index import GPTVectorStoreIndex, SimpleDirectoryReader ---> from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
# from llama_index import Document ---> from llama_index.core import Document
# from llama_index.llms.ollama import OllamaLLM ---> from llama_index.llms.ollama import Ollama
# from llama_index.embeddings.ollama import OllamaEmbedding ---> from llama_index.embeddings.ollama import OllamaEmbedding
# GPTVectorStoreIndex ---> VectorStoreIndex

所以适配最新版本代码如下(一定要小心且锁定好版本, Python 很多组件库接口版本管理很烂, 有的时候更新就直接接口异常),
这里直接改写成连接 ollama 模型加载测试下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Ollama + DeepSeek-R1-1.5B的RAG检索脚本
加载本地语料库,通过Ollama调用DeepSeek模型生成回答
"""

# 导入核心模块
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings, Document

# 导入Ollama的LLM和嵌入模型
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding

# 配置Ollama连接(DeepSeek-R1-1.5B模型和嵌入模型)
Settings.llm = Ollama(
model="deepseek-r1:1.5b",
base_url="http://localhost:11434",
temperature=0.1
)

# 加载 nomic-embed-text 向量分析
# 记得运行 sudo -u ollama ollama pull nomic-embed-text
Settings.embed_model = OllamaEmbedding(
model_name="nomic-embed-text",
base_url="http://localhost:11434"
)

# 手动加载语料的备选方式(无需文件加载)
# 这里后续会从数据库当中加载提取内容
documents = [
Document(text="DeepSeek-R1-1.5B是轻量级大模型,适合边缘部署", metadata={"title": "DeepSeek特性"}),
Document(text="Ollama支持一键运行GGUF格式的大模型", metadata={"title": "Ollama功能"})
]

# 构建向量索引
index = VectorStoreIndex.from_documents(documents)

# 创建查询引擎并测试
query_engine = index.as_query_engine()
response = query_engine.query("DeepSeek-R1-1.5B的特点是什么?")

# 输出结果
print("查询结果:\n", response)

最后输出内容, 可以看到已经命中到传入的数据语料库:

1
2
3
4
5
6
7
8
查询结果:
DeepSeek-R1-1.5B的特点包括:

1. **轻量级大模型**:该模型设计为轻量化,适用于资源受限的环境,如边缘计算。

2. **适合边缘部署**:能够高效运行在边缘设备上,提升实时处理能力。

3. **Ollama功能支持**:通过一键运行GGUF格式的大模型,简化了部署流程。

我们需要对比下导入之前和之后功能, 之前我们说过的 当前时间 问题, 我们测试下是否我们能够正常展示语料库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 其他略

# 引入时间库
import datetime


# 获取时间并格式化输出
current_time = datetime.datetime.now()
formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S")
documents = [
Document(text=f"当前时间为{formatted_time}", metadata={"title": "当前时间"})
]

# 其他略

# 创建查询引擎并测试
query_engine = index.as_query_engine()
response = query_engine.query("当前时间")

# 输出结果
print("查询结果:\n", response)

对比之前之后的数据效果:

1
2
3
4
5
6
7
------- 导入语料库之前 ------------
- 当前时间
- 您好,建议您联网获取时效性较强的信息;如果还有其他问题需要帮助,请随时告诉我!

------- 导入语料库之后 ------------
- 当前时间
- 2025-11-26 11:38:56

但是如果采用 ollama client 之类的客户端应用时候就会发现语料库相关资料并没有加载进来,
这是因为 Ollama Client 仅能连接底层 ollama 接口, 而我们是基于 RAG(后台处理) + Ollama(模型处理).

所以我们这里就需要自己作为 代理 去模拟 ollama serve 去监听请求, 然后转发进 RAG + Ollama 处理的模型,
这里我们需要创建个 HTTP 服务:

1
2
# 按照 fastapi 暴露出 HTTP API
sudo -u ollama pip3 install --user --break-system-packages -i https://mirrors.aliyun.com/pypi/simple fastapi uvicorn

这里就需要再改进下之前的功能脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import FastAPI
import uvicorn
import datetime
from llama_index.core import VectorStoreIndex, Settings, Document
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding

# 初始化FastAPI
app = FastAPI(title="DeepSeek-R1 RAG API", version="1.0")

# 配置Ollama
Settings.llm = Ollama(model="deepseek-r1:1.5b", base_url="http://localhost:11434", temperature=0.1)
Settings.embed_model = OllamaEmbedding(model_name="nomic-embed-text", base_url="http://localhost:11434")

# 生成语料库(可替换为数据库加载)
def get_corpus():
current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return [
Document(text=f"当前时间为{current_time}", metadata={"title": "当前时间"}),
Document(text="DeepSeek-R1-1.5B是轻量级大模型,适合边缘部署", metadata={"title": "DeepSeek特性"}),
Document(text="Ollama支持一键运行GGUF格式的大模型", metadata={"title": "Ollama功能"})
]

# 构建全局索引(避免每次查询重新构建)
index = VectorStoreIndex.from_documents(get_corpus())
query_engine = index.as_query_engine()

# 定义API接口
@app.get("/rag/query")
def rag_query(question: str):
"""RAG问答接口,参数:question(查询问题)"""
response = query_engine.query(question)
return {
"question": question,
"answer": str(response),
"corpus_source": [doc.text for doc in response.source_nodes] # 返回匹配的语料
}

if __name__ == "__main__":
# 启动API服务(监听所有IP,端口8000)
uvicorn.run(app, host="0.0.0.0", port=8000)

这里启动脚本是持续运行的, 后续可以考虑直接编写成系统单元:

1
2
3
4
5
6
# 启动服务, 会默认抢占命令行运行
sudo -u ollama python3 /usr/share/ollama/rag/basic.py

# 另外打开个命令行窗口执行
# 之后访问直接采用 HTTP API 形式处理, 命令行文本需要做下 URL 编码
curl "http://127.0.0.1:8000/rag/query?question=%E5%BD%93%E5%89%8D%E6%97%B6%E9%97%B4"

对于客户端要复用接口就需要对应去实现相关 RestApi 功能, 这个后续再处理也可以, 因为基本思路和功能已经处理完成,
后续只需要处理的就是作为 Document 容器对象, 这里列举出来所有的参数:

参数名 类型 是否必选 核心作用 示例值
text str 存储原始语料文本,RAG检索和生成的核心数据来源 "DeepSeek-R1-1.5B是轻量级大模型"
metadata dict 存储语料的结构化元数据(标签、来源、时间等),用于检索过滤、溯源、分类 {"title": "DeepSeek特性", "source": "mysql", "update_time": "2025-11-26"}
doc_id str 语料的唯一标识ID,用于语料的更新、删除、索引匹配(若不指定,LlamaIndex会自动生成) "corpus_001"
embedding List[float] 语料文本的预计算嵌入向量(若已提前向量化,可直接传入,避免重复计算) [0.123, 0.456, ..., 0.789](长度与嵌入模型维度一致)
hash str 语料的哈希值,用于校验文本是否被修改(LlamaIndex会自动计算,无需手动指定) "a1b2c3d4e5f6..."
excluded_embed_metadata_keys List[str] 指定不参与向量化的元数据键(避免无关元数据影响嵌入效果) ["update_time", "author"]
excluded_llm_metadata_keys List[str] 指定不传递给LLM的元数据键(避免LLM生成时包含无关元数据) ["hash", "doc_id"]
metadata_separator str 元数据与文本拼接时的分隔符(仅在特殊场景下使用,如将元数据融入文本) "\n---\n"
text_template str 文本与元数据的拼接模板(自定义语料的最终输入格式) "Text: {text}\nMetadata: {metadata}"
metadata_template str 元数据的拼接模板(配合text_template使用) "Key: {key}, Value: {value}"
  • 采用外部数据库导入处理的时候, 可以按照相关具体需求将对应字段给导入进去, 推荐数据库唯一标识ID和 doc_id 关联起来.
  • 入库文本内容最好哈希处理传入, 有的动态数据源有时候写错编辑, 这时候就需要检测变动来确认是否更新数据

最后就是关于 metadata 对象组的参数列表:

字段名 类型 核心作用 适用场景 示例值
doc_id str/int 语料的业务唯一标识 语料的新增/更新/删除、精准匹配 "corpus_001"/1001
title str 语料的标题/名称 溯源展示、按标题检索 "DeepSeek-R1部署文档"
source str 语料的来源渠道 多源语料过滤、溯源 "mysql"/"local_file"/"api"/"pdf"
category str/list 语料的分类/标签 按分类检索、权限控制 "技术文档"/["部署", "轻量级"]
update_time str 语料的更新时间 优先检索最新语料、版本管理 "2025-11-26 19:00:00"
author str 语料的作者/维护者 权限控制、溯源 "dev_team"/"admin"
priority float/int 语料的检索权重 提升重要语料的检索优先级 2.0(高优先级)/1.0(默认)
file_path str 本地文件语料的路径 文件语料的溯源、重新加载 "/data/corpus/deepseek.pdf"
url str 网页/API语料的链接 网页语料的溯源 "https://docs.deepseek.com/r1/deploy"
status str/int 语料的生效状态 过滤失效语料 "active"/1(生效)/"inactive"/0(失效)

metadata 对象组实际上就是用于对语料库做更进一步的来源和属性标识, 作为个人资料扩展的时候很有帮助.

不过说实话 DeepSeek-R1-1.5B 这种模型本身为了轻量级处理, 所以哪怕导入数据库也是有这很强的交流 伪人感,
远远达不到大型厂商那种更加拟人的语气, 所以日常使用其实也仅仅作为新闻总结播报这样处理, 达不到 AI 助理的地步.

优化配置

因为是个人部署基本上不会投入太多资源(家用架设服务器的噪音和耗电能把小区电路干趴), 所以需要简易和极端的方式配置 ollama

首先需要明确单独简单服务器是不会采用 GPU(显卡), 无论是 nas 还是 工控机 都不具备有插显卡的价值, 所以只需要处理CPU和内存相关.

Ollama 是内存密集型应用(显存|内存), 对于小型部署来说主要处理是内存问题

最好提供扩展单挑 120GNVME 协议固态硬盘, 将其作为系统的交换分区挂载上来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Linux 的交换分区概念和 Window 的虚拟内存概念差不多
# 之所以需要用到交换分区是为了让低内存小型服务器依靠 LINUX 虚拟缓存来扩展服务器的内存上限
# 因为本身 ollama 就是内存密集型应用, 所以依靠这样技术可以让本来只能运行 DeepSeek-R1-1.5B 的服务器强制拉升到 DeepSeek-R1-8B 模型
# 而之所以需要采用 `NVME` 协议的固态硬盘, 就是尽可能让虚拟的内存速度尽可能达到物理内存那样的速度

# 1. 首先确认挂载上来的固态硬盘, 固态硬盘一般默认设备名格式为 /dev/nvme0n1
sudo lsblk
sudo swapoff -a # 关闭所有正在使用的交换分区/文件, 这是临时生效会导致重启后恢复
sudo sed -i '/swap/s/^/#/' /etc/fstab # 实际上就是在系统分区表之后将相关的 /swap 全部注释

# 2. 在NVMe硬盘上创建交换空间
sudo dd if=/dev/zero of=/dev/nvme0n1 bs=512 count=1 conv=notrunc # 内部固态硬盘不为空, 需要格式化处理下
sudo fdisk /dev/nvme0n1
# 执行以下交互命令:
# n → 创建新分区 → p → 主分区 → 1 → 回车(起始扇区)→ 回车(结束扇区,默认全容量)
# t → 更改分区类型 → 82(Linux swap类型)
# w → 保存分区表并退出

# 3. 将新分区 /dev/nvme0n1p1 格式化为交换空间
# 具体的分区名需要自行查看, 默认单个一般是 /dev/nvme0n1p1
sudo mkswap /dev/nvme0n1p1
sudo swapon /dev/nvme0n1p1

# 4. 确认目前交换空间的本地路径, 这里可以看见相关的交换分区
sudo swapon --show

# 5. 这里需要改写分区表, 让系统启动的时候就要挂载上来
sudo echo "/dev/nvme0n1p1 none swap sw 0 0" >> /etc/fstab

# 3. 最后确认下就可以信息就可以
sudo mount -a # 挂载硬盘查看
sudo swapon -a # 交换空间查看
sudo free -h # 内存信息查看

因为采用固态硬盘的虚拟内存, 所以我们也需要尽可能采用让内存调度在固态硬盘当中, 这里修改 /etc/sysctl.conf 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 让系统优先使用NVMe交换分区, 突破物理内存限制
vm.swappiness = 100

# 减少页缓存回收, 提升Ollama的内存命中率
vm.vfs_cache_pressure = 50

# 关闭页面聚类, 减少NVMe的批量写入延迟(仅内核5.0+支持)
vm.page_cluster = 0

# OOM保护, 避免Ollama占满内存导致系统崩溃
vm.overcommit_memory = 2
vm.overcommit_ratio = 80

# NVMe寿命保护, 降低写入放大(内核5.14+支持)
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
vm.dirty_writeback_centisecs = 3000

最理想效果就是利用 NVMe 交换分区的读写速度应达到 1GB/s 以上(远超机械硬盘的 100MB/s), 实现足以支撑高精度模型的虚拟内存访问.

但是虚拟的内存交换速度用于比不上物理内存和显卡显存, 并且频繁的写入会导致固态硬盘寿命锐减

之后就是 ollama 来声明环境变量文件配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 强制禁用 GPU
OLLAMA_NUM_GPU=0

# 启用低内存模式, 优先使用交换分区
OLLAMA_LOW_VRAM=1

# 只需要嵌入单个模型, 个人服务器没这么多资源加载多个模型
OLLAMA_MAX_LOADED_MODELS=1

# 全局设置模型上下文窗口大小
OLLAMA_NUM_CTX=2048

# 全局设置批处理大小
OLLAMA_NUM_BATCH=512

再次提醒: 如果作为重度使用的情况, 不要部署在和个人数据相关的服务器上, 否则很容易出现硬盘崩溃连带数据丢失

利用虚拟空间的手段, 可以让 ollama 运行高一级的模型, 其中付出的这部分代价看各人取舍;
理论上如果固态硬盘性能足够的情况下, 在个人服务器运行 DeepSeek-R1-16B 做小流量的个人助理也是没问题.

动态挂载

之前通过 fastapi 能够简单挂载起 ollama-rag 服务, 但不方便动态数据导入(需要创建定时任务来把 Document 放入模型分析)

需要把 llama-index 功能暴露出来, 让外部通过接口来实行对应数据传入功能, 并且还需要能够提供动态实时加载外部语料库的功能

那么最基本的服务就最少需要以下访问接口:

  • POST /rag/corpus: 提交语料库, 将语料库数据动态执行 VectorStoreIndex.from_documents 加载
  • POST /rag/query: 获取到最新语料库的查询句柄, 从而结合最新语料库来加载数据
  • GET /rag/status: 获取目前解析并加载语料库的状态

这样依靠 HTTP 暴露出来方便其他语言导入和查询数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
from fastapi import FastAPI, Body, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, validator
from typing import List, Optional, Dict, Any
# 仅保留低版本兼容的核心导入
from llama_index.core import VectorStoreIndex, Document, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
import os

# 初始化FastAPI应用
app = FastAPI(title="Ollama-RAG 动态语料服务", version="1.0")

# -------------------------- 全局配置与索引管理 --------------------------
# 配置Ollama LLM和嵌入模型(本地Ollama需启动并拉取对应模型)
Settings.llm = Ollama(model="deepseek-r1:1.5b", base_url="http://localhost:11434", temperature=0.1)
Settings.embed_model = OllamaEmbedding(model_name="nomic-embed-text", base_url="http://localhost:11434")

# 全局索引缓存:保存最新的语料库索引(低版本默认内存存储)
GLOBAL_RAG_INDEX = None

# -------------------------- Pydantic模型:定义Document请求结构 --------------------------
class RequestDocument(BaseModel):
"""外部传入的Document对象结构,贴合llama-index的Document原生字段"""
text: str = Field(..., description="文档的核心文本内容,不能为空")
doc_id: Optional[str] = Field(None, description="文档唯一标识,不传入则自动生成")
metadata: Optional[Dict[str, Any]] = Field({}, description="文档元数据,如作者、来源、时间等")

@validator("text")
def text_not_empty(cls, v):
"""校验text字段不能为空"""
if not v.strip():
raise ValueError("文档text字段不能为空或仅包含空白字符")
return v

# -------------------------- 核心工具函数 --------------------------
def build_index_from_documents(documents: List[Document]) -> VectorStoreIndex:
"""
从llama-index的Document列表构建向量索引(低版本默认内存存储)
:param documents: Document对象列表
:return: 构建后的VectorStoreIndex
"""
if not documents:
raise ValueError("语料库不能为空,无法构建索引")
# 低版本核心:直接构建索引,无需手动指定StorageContext和向量存储
index = VectorStoreIndex.from_documents(documents)
return index

def convert_request_docs_to_llama_docs(request_docs: List[RequestDocument]) -> List[Document]:
"""
将外部传入的RequestDocument转换为llama-index的原生Document对象
:param request_docs: 外部请求的Document列表
:return: llama-index的Document列表
"""
llama_docs = []
for req_doc in request_docs:
# 自动生成doc_id(如果外部未传入)
doc_id = req_doc.doc_id or f"doc_{os.urandom(6).hex()}" # 6位16进制随机数,更唯一
# 构建llama-index的Document对象(低版本兼容)
llama_doc = Document(
text=req_doc.text,
doc_id=doc_id,
metadata=req_doc.metadata or {}
)
llama_docs.append(llama_doc)
return llama_docs

# -------------------------- API接口定义 --------------------------
@app.post("/rag/corpus", summary="提交Document格式的语料库并构建最新索引", tags=["语料库管理"])
async def submit_corpus(
documents: List[RequestDocument] = Body(..., description="Document格式的语料库JSON数组,每个元素包含text、可选doc_id和metadata")
):
"""
接收外部传入的Document格式JSON数组,转换为llama-index原生Document后构建最新RAG索引
每次提交会覆盖原有索引,如需增量添加可修改build_index_from_documents逻辑
"""
global GLOBAL_RAG_INDEX
try:
# 1. 转换外部请求的Document为llama-index原生对象
llama_docs = convert_request_docs_to_llama_docs(documents)

# 2. 构建最新索引
GLOBAL_RAG_INDEX = build_index_from_documents(llama_docs)

# 3. 统计语料库信息
doc_ids = [doc.doc_id for doc in llama_docs]
metadata_keys = [list(doc.metadata.keys()) for doc in llama_docs]

return JSONResponse(content={
"status": "success",
"message": f"语料库提交成功,已构建最新索引(共{len(llama_docs)}个文档)",
"corpus_stats": {
"total_documents": len(llama_docs),
"document_ids": doc_ids,
"metadata_sample": metadata_keys[:3] # 仅返回前3个文档的元数据键,避免数据过大
}
})

except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"语料库提交失败:{str(e)}")

@app.post("/rag/query", summary="基于最新语料库执行RAG查询", tags=["RAG查询"])
async def rag_query(
query: str = Body(..., description="用户查询问题"),
top_k: int = Body(3, description="检索最相似的文档数量,默认3"),
response_mode: str = Body("compact", description="回答模式:compact/simple/tree等"),
with_metadata: bool = Body(True, description="是否返回源文档的元数据,默认True")
):
"""
基于最新提交的Document语料库执行RAG查询,返回检索增强后的回答及源文档信息
"""
global GLOBAL_RAG_INDEX
try:
# 校验索引是否存在
if GLOBAL_RAG_INDEX is None:
raise HTTPException(status_code=400, detail="暂无最新语料库索引,请先通过/rag/corpus提交Document格式的语料库")

# 构建查询引擎(低版本兼容)
query_engine = GLOBAL_RAG_INDEX.as_query_engine(
similarity_top_k=top_k,
response_mode=response_mode
)

# 执行查询
response = query_engine.query(query)

# 提取检索的源文档信息(包含元数据)
source_nodes = []
for node in response.source_nodes:
source_info = {
"doc_id": node.node.ref_doc_id if hasattr(node.node, 'ref_doc_id') else node.node.doc_id,
"similarity_score": round(node.score, 4) if hasattr(node, 'score') else 0.0, # 兼容极低版本无score的情况
"content_snippet": node.node.text[:200] + "..." if len(node.node.text) > 200 else node.node.text
}
# 可选返回元数据
if with_metadata and hasattr(node.node, 'metadata'):
source_info["metadata"] = node.node.metadata
source_nodes.append(source_info)

return JSONResponse(content={
"status": "success",
"query": query,
"answer": str(response),
"retrieval_result": {
"top_k": top_k,
"matched_documents": source_nodes,
"total_matched": len(source_nodes)
}
})

except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"RAG查询失败:{str(e)}")

@app.get("/rag/status", summary="查看RAG服务状态", tags=["服务监控"])
async def rag_status():
"""
查看最新语料库索引的状态及文档数量
"""
global GLOBAL_RAG_INDEX
if GLOBAL_RAG_INDEX is None:
return JSONResponse(content={
"status": "uninitialized",
"message": "尚未提交Document语料库,索引未构建"
})
else:
# 低版本兼容的文档数量统计
doc_count = len(GLOBAL_RAG_INDEX.docstore.docs) if hasattr(GLOBAL_RAG_INDEX.docstore, 'docs') else 0
sample_doc_id = list(GLOBAL_RAG_INDEX.docstore.docs.keys())[:1] if doc_count > 0 else []
return JSONResponse(content={
"status": "initialized",
"message": "Document语料库索引已构建",
"index_stats": {
"total_documents": doc_count,
"sample_document_id": sample_doc_id
}
})

# 启动服务
if __name__ == "__main__":
import uvicorn
# 监听所有网络接口,端口8000
uvicorn.run(app, host="0.0.0.0", port=8000)

启动服务之后就可以可以提供资料让 AI 模型 “认知自己”, 比如现在我要生成 自己名为 "MeteorCat" 且目前24岁 的认知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 基础版提交 - 仅核心文本
# 稍微强化下模型的基础认知
curl -X POST http://localhost:8000/rag/corpus \
-H "Content-Type: application/json" \
-d '[
{
"text": "你是一个名为MeteorCat的AI助手,你的年龄是24岁。"
}
]'


# 比较完整的提交
curl -X POST http://localhost:8000/rag/corpus \
-H "Content-Type: application/json" \
-d '[
{
"text": "你的名字是MeteorCat,你目前的年龄是24岁,这是你的核心身份信息。",
"doc_id": "meteorcat_identity",
"metadata": {
"info_type": "identity",
"source": "user_define",
"create_time": "2025-11-27"
}
}
]'


# 之后就可以开始获取 AI 认知, 这里询问 AI 目前的认知
curl -X POST http://localhost:8000/rag/query \
-H "Content-Type: application/json" \
-d '{
"query": "你叫什么名字?你今年多少岁?",
"top_k": 1,
"with_metadata": true
}'

# 这里最后返回信息如下
# {
# "status": "success",
# "query": "你叫什么名字?你今年多少岁?",
# "answer": "你叫什么名字?你今年多少岁?\n\n meteor cat 24 岁",
# "retrieval_result": {
# "top_k": 1,
# "matched_documents": [
# {
# "doc_id": "meteorcat_identity",
# "similarity_score": 0.5999,
# "content_snippet": "你的名字是MeteorCat,你目前的年龄是24岁,这是你的核心身份信息。",
# "metadata": {
# "info_type": "identity",
# "source": "user_define",
# "create_time": "2025-11-27"
# }
# }
# ],
# "total_matched": 1
# }
#}
#
# "meteor cat 24 岁" 可以看到虽然返回信息了, 但是名称大小写被拆分就是照本宣科念出来缺乏灵性

如果要让 AI 更加人性化, 就需要以下措施:

  • 标记提示词: 需要明确 MeteorCat 这个词是我们具体的名称, 让他不要做过多考虑
  • 提高 RAG 人性化: 通过初始化模型调整将 Ollama(其他略, temperature=0.1) 调整为 Temperature=0.7 提升人性化程度
  • 优化检索器配置: 新增 similarity_cutoff=0.6 参数, 过滤相似度低于 0.6 的结果,确保只使用最相关的语料

重新调整人性化处理之后提交数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 重新发送自我认知给AI
curl -X POST http://localhost:8000/rag/corpus \
-H "Content-Type: application/json" \
-d '[
{
"text": "你的名字是 MeteorCat,你目前的年龄是24岁,这是你的核心身份信息。",
"doc_id": "meteorcat_identity",
"metadata": {
"info_type": "identity",
"source": "user_define",
"create_time": "2025-11-27"
}
}
]'

# 这里就是追加 "top_k=1,similarity_cutoff=0.6" 代表我们需要过滤掉相似 0.6 结果且优先等级为 1 的数据检索

curl -X POST http://localhost:8000/rag/query \
-H "Content-Type: application/json" \
-d '{
"query": "你叫什么名字?你今年多少岁?",
"top_k": 1,
"similarity_cutoff": 0.6
}'

# 这里还有视角转换问题, 如果需要强化自我认知应该提供的资料需要设置为 "我/我的/我能/我是" 等自我认知
# 并且涉及到编写关键词和视角切换模板等代码, 涉及到比较复杂的点所以跳过这部分说明
# 所以这里最后返回 ”你叫 MeteorCat 姓名,今年 24 岁。“

后续就是比较深入和系统化的 AI 学习, 不过我一般最多都是通过 RSS 加载新闻导入进模型, 让他作为新闻数据库而已.

实际上还有静态定制生成对应的加载模板和模型(目前启动模型的时候都需要动态再次载入语料库), 个人服务器配置一般玩不动