ollama 部署
一般来说个人部署 AI 服务是及其耗费时间和精力(还有可怕的满载电量和噪音), 但对于小规模的个人来说,
利用闲置的服务器设备部署个小型 AI 服务作为个人资料库其实也可以稍微玩玩.
而个人部署就推荐采用 ollama 来搭建, 按照官方文档来说其实最简单是采用 docker,
不过我这边本身就是闲置硬件也就是总结采用二进制安装部署就行, 不需要在套一层 docker 镜像.
注意: 本文涉及的很多网络相关可能需要 ‘工具’ 来处理, 否则网速基本上很慢没办法快捷部署
这里采用 ollama-linux 安装方式处理:
# 如果之前安装过, 需要手动先卸载清空, 执行以下命令
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 系统单元文件内容:
[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
最后执行命令:
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 这种代理转发出来
如果想要卸载, 官方也提供了卸载方法:
# 系统单元清除
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显存压力:
# 这里就需要修改之前服务单元的 /usr/share/ollama/ollama.conf 环境文件
sudo vim /usr/share/ollama/ollama.conf
这里追加几个环境配置:
# 是否开启测试模式, 用于测试本地是否生效
# 如果后续启动之后展示 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 注释启用正式环境:
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,
首先执行输入安装命令:
# 因为挂载在 ollama 执行, 所以需要依赖 ollama 用户
sudo -u ollama ollama run deepseek-r1:1.5b
# 安装完成默认会进入交互命令行, 这就代表成功部署完成
这里就需要选择 GUI 客户端来交互, 这里提供两种方式:
默认的模型都是笨笨的, 类似以下对话他都无法满足:
- 你好, 北京时间现在几点?
- 您好,建议您联网获取时效性较强的信息;如果还有其他问题需要帮助,请随时告诉我!
这里我们需要做的就是以 DeepSeek-R1-1.5B 模型为基准去自定义模型并导入相关信息语料库(新闻|时间|资料等)
资料库
目前将外部资料导入到模型的方式有以下几种:
检索增强生成(RAG): 简单标记资料导入到模型当中, 作为中心资料库(比如 ‘新闻’ 只能单独检索新闻而无法 ‘总结这个月新闻’)模型微调(深度定制,需重新训练): 对AI模型再度训练开发, 让他对数据做人性化识别(会对资料进行思考学习, 然后做自己判断)
如果是个人使用, 模型微调 需要极高的成本:
16GB 内存(GPU 加速需 CUDA 支持)需要利用 LoRA 微调按照数据手动调整 num_ctx(上下文)| batch_size(批次)等参数
投入的成本十分巨大, 所以对于个人来说实际上没有什么必要, 大部分情况下作为个人资料直接采用 检索增强生成(RAG) 就行了.
而检索增强生成(RAG)来将咨询语料库文本导入到模型之中, 类似将数据按照以下方式导入:
{
"input": "什么是大语言模型?",
"output": "大语言模型是基于海量文本数据训练的深度学习模型,能理解和生成人类语言..."
}
{
"input": "DeepSeek-R1的特点是什么?",
"output": "DeepSeek-R1-1.5B是轻量级大模型,兼顾推理速度和效果,适合边缘设备部署..."
}
注意: 最好把
ollama和rag服务分开部署,rag可以连接到远程的ollama服务, 所以只需要内网联通过去即可
我们需要做的就是把这些文本处理成类似这样的结构, 然后在运行的时候挂载进模型即可;
这里需要先安装 Python 依赖库, 因为这里利用 Python 脚本做处理:
# 需要确认权限用户有执行 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:
官方文档: https://docs.llamaindex.ai/
LlamaIndex 是一个基于LLM的应用程序的数据框架,该应用程序受益于上下文增强,是典型的RAG系统
LlamaIndex 提供了必要的抽象,可以更轻松地摄取、构建和访问私有或特定领域的数据,以便将这些数据安全可靠地注入 LLM 中,以实现更准确的文本生成
注意: 使用
LlamaIndex最好能保证4G+内存来运行数据分析
这里先简单应用下 LlamaIndex 解析工具, 这里考虑到都是基于 ollama 用户加载, 所以所有操作就挂靠在其中:
# 创建主要运行目录
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 的基础测试脚本可以先处理下看看是否运行正常:
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)
运行下确认是否能够命中信息集合:
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 模型加载测试下:
#!/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)
最后输出内容, 可以看到已经命中到传入的数据语料库:
查询结果:
DeepSeek-R1-1.5B的特点包括:
1. **轻量级大模型**:该模型设计为轻量化,适用于资源受限的环境,如边缘计算。
2. **适合边缘部署**:能够高效运行在边缘设备上,提升实时处理能力。
3. **Ollama功能支持**:通过一键运行GGUF格式的大模型,简化了部署流程。
我们需要对比下导入之前和之后功能, 之前我们说过的 当前时间 问题, 我们测试下是否我们能够正常展示语料库:
# 其他略
# 引入时间库
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)
对比之前之后的数据效果:
------- 导入语料库之前 ------------
- 当前时间
- 您好,建议您联网获取时效性较强的信息;如果还有其他问题需要帮助,请随时告诉我!
------- 导入语料库之后 ------------
- 当前时间
- 2025-11-26 11:38:56
但是如果采用 ollama client 之类的客户端应用时候就会发现语料库相关资料并没有加载进来,
这是因为 Ollama Client 仅能连接底层 ollama 接口, 而我们是基于 RAG(后台处理) + Ollama(模型处理).
所以我们这里就需要自己作为 代理 去模拟 ollama serve 去监听请求, 然后转发进 RAG + Ollama 处理的模型,
这里我们需要创建个 HTTP 服务:
# 按照 fastapi 暴露出 HTTP API
sudo -u ollama pip3 install --user --break-system-packages -i https://mirrors.aliyun.com/pypi/simple fastapi uvicorn
这里就需要再改进下之前的功能脚本:
#!/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)
这里启动脚本是持续运行的, 后续可以考虑直接编写成系统单元:
# 启动服务, 会默认抢占命令行运行
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 是内存密集型应用(显存|内存), 对于小型部署来说主要处理是内存问题
最好提供扩展单挑 120G 的 NVME 协议固态硬盘, 将其作为系统的交换分区挂载上来:
# 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 配置:
# 让系统优先使用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 来声明环境变量文件配置:
# 强制禁用 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 暴露出来方便其他语言导入和查询数据:
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岁 的认知:
# 基础版提交 - 仅核心文本
# 稍微强化下模型的基础认知
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 的结果,确保只使用最相关的语料
重新调整人性化处理之后提交数据:
# 重新发送自我认知给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 加载新闻导入进模型, 让他作为新闻数据库而已.
实际上还有静态定制生成对应的加载模板和模型(目前启动模型的时候都需要动态再次载入语料库), 个人服务器配置一般玩不动