共计 4367 个字符,预计需要花费 11 分钟才能阅读完成。
很多朋友把 NAS 组起来后,除了做影视库(Plex/Emby)和 PT 下载,最大的折腾方向就是跑本地 AI。但多数人一上来就照着网上教程用 Ollama 拉个 Llama- 3 或者 Qwen-72B,结果几秒挤出一个 token,并发稍高显存直接 OOM,最后沦为只能发两句“你好”的玩具。如果要在个人开发环境搭建私有代码检索、团队内部 Wiki 问答这种高频并发场景,基于 Ollama 的开箱方案很快就会遇到瓶颈。
上个月我把工作主力库的几十万行源码和十年积累的 Markdown 笔记全量灌入 NAS。经过连续两周的高压调试与性能摸底,我抛弃了传统的“纯 CPU 慢吞吞运算”或“无脑套用 Ollama”的路线,改用 vLLM(推理)+ Qdrant(向量引擎)+ Infinity(高性能 Embedding/Rerank) 的生产级技术栈,把一台带轻量独显(RTX 4060Ti 16G)与 NVMe 高速缓存的自建 TrueNAS SCALE 改造成了真正的毫秒级知识库引擎。
一、为什么放弃 Ollama 选这套架构?
在生产环境或者高频 IDE 插件(比如 Continue、Cursor)调用下,核心痛点永远只有两个:吞吐量不足 与 向量检索开销。
- 动态批处理与显存碎片:Ollama 底层依赖 llama.cpp,单用户顺序聊天很顺滑,但多个 IDE 同时发起代码补全或知识检索时,显存管理不够弹性。vLLM 利用 PagedAttention 算法,将显存划分为类似操作系统的虚拟分页,彻底解决了显存碎片,并发请求下的吞吐量能拉开 llama.cpp 三倍以上。
- 向量检索的隔离性:不少人喜欢将 Chroma 甚至 SQLite 作为向量存储挂载在 Python 脚本里。一旦向量达到百万级,冷启动慢、内存暴涨,单次相似度检索耗时直奔数百毫秒。轻量、纯 Rust 编写的 Qdrant,内存占用极低,且天生支持异步高并发写入与 HNSW 索引。
- Embedding 与 LLM 分流:把向量计算(Embedding)和大模型文本生成混在一个实例里是灾难。把 BGE-M3 这样的 Embedding 模型拆分到独立服务,才能吃满并行请求。
二、架构编排与 Docker Compose 实操
我的 NAS 环境运行的是 TrueNAS SCALE(底座即 Debian Linux),配备 Intel i5-13500、64GB DDR5 ECC 内存,以及一张单槽 RTX 4060Ti 16G 作为运算卡。如果你用的是群晖(Synology)且带有外置 eGPU 或者 PCIe 直通方案,配置方式基本通用。
先在 NAS 的数据盘(建议放在 SSD 固态池上)建立挂载目录:
mkdir -p /mnt/fast-pool/ai-stack/{qdrant_data,models,infinity_cache}
cd /mnt/fast-pool/ai-stack
编写 docker-compose.yml。这里我们将显存严格切分:给 vLLM 分配 11GB 显存运行 Qwen2.5-Coder-7B-Instruct(AWQ 量化版),给 Infinity 留 2GB 跑 bge-large-zh-v1.5,剩余显存用于系统调度,Qdrant 走全内存 + 持久化存储方案:
version: '3.8'
services:
# 向量数据库:Qdrant
qdrant:
image: qdrant/qdrant:v1.9.2
container_name: ai-qdrant
restart: always
ports:
- "6333:6333"
- "6334:6334"
volumes:
- /mnt/fast-pool/ai-stack/qdrant_data:/qdrant/storage
environment:
- QDRANT__SERVICE__GRPC_PORT=6334
# 高性能 Embedding/Rerank 引擎
infinity:
image: michaelf34/infinity:0.0.46
container_name: ai-embedding
restart: always
environment:
- INFINITY_MODEL_ID=BAAI/bge-large-zh-v1.5
- INFINITY_BATCH_SIZE=32
volumes:
- /mnt/fast-pool/ai-stack/infinity_cache:/app/.cache
ports:
- "7997:7997"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
# 推理引擎:vLLM
vllm:
image: vllm/vllm-openai:v0.4.3
container_name: ai-vllm
restart: always
ports:
- "8000:8000"
volumes:
- /mnt/fast-pool/ai-stack/models:/root/.cache/huggingface
environment:
- HUGGING_FACE_HUB_TOKEN=your_hf_token_here
command: >
--model Qwen/Qwen2.5-Coder-7B-Instruct-AWQ
--quantization awq
--dtype half
--gpu-memory-utilization 0.72
--max-model-len 8192
--enforce-eager
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
踩坑提醒:
在部分 NAS 的 Linux 驱动下,启动 vLLM 时可能会抛出 CUDA out of memory during initialization。这是因为 vLLM 默认会尝试抓取 90% 显存,而系统和 Xorg/ 驱动会占用几百兆。参数 --gpu-memory-utilization 0.72 经过精密计算:16GB * 0.72 ≈ 11.5GB,刚好容纳 7B-AWQ 的 KV Cache,剩下的 4.5GB 平分给 Embedding 模型与宿主机系统,整机运行坚如磐石。
三、构建与检索:两步走轻量接入脚本
基础设施就绪后,我们需要一个管道(Pipeline)把本地工程代码拆解打散存入 NAS。这里抛弃臃肿的 LangChain,直接用原生 Python 写一个清晰的切块与入库脚本。
import os
import requests
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
# 初始化客户端
q_client = QdrantClient(host="192.168.1.100", port=6333)
COLLECTION_NAME = "nas_codebase"
# 创建集合(如果不存在)if not q_client.collection_exists(COLLECTION_NAME):
q_client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=VectorParams(size=1024, distance=Distance.COSINE),
)
def get_embedding(text: str) -> list:
url = "http://192.168.1.100:7997/embeddings"
res = requests.post(url, json={"model": "BAAI/bge-large-zh-v1.5", "input": [text]})
return res.json()["data"][0]["embedding"]
def ingest_file(file_path: str):
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# 按 600 字符滑窗切块,保留 100 字符重叠
chunk_size = 600
overlap = 100
chunks = [content[i:i + chunk_size] for i in range(0, len(content), chunk_size - overlap)]
points = []
for idx, chunk in enumerate(chunks):
vector = get_embedding(chunk)
point_id = abs(hash(f"{file_path}_{idx}"))
points.append(PointStruct(
id=point_id,
vector=vector,
payload={"path": file_path, "text": chunk}
))
q_client.upsert(collection_name=COLLECTION_NAME, points=points)
print(f"成功入库: {file_path}, 分块数: {len(chunks)}")
# 测试入库
if __name__ == "__main__":
ingest_file("./core_engine.py")
代码逻辑非常透明:文本发送至 NAS 上的 Infinity 容器,拿到 1024 维度的稠密向量,连同文件路径直接写进 Qdrant。不需要任何第三方复杂抽象,排查问题只看网络通信日志即可。
四、实测表现与压测对比
用 wrk 以及自定义并发脚本对这套架构进行了压测。在 NAS 同时处理 IDE 代码补全(高频短文本输入)与全库文档检索(大并发 Embedding)场景下:
| 方案与配置 | 单请求延迟 (TTFT) | 并发吞吐 (8 并发) | 空闲 / 满载显存 |
|---|---|---|---|
| Ollama (Qwen-7B 默认量化) | 850 ms | 14.2 tokens/s | 4.8G / 15.6G (极易波动) |
| NAS 专用栈 (vLLM + Infinity) | 140 ms | 86.5 tokens/s | 13.8G / 14.2G (固定锁定) |
数据差异非常明显:vLLM 在固定预分配显存后,高并发下依然能维持几乎线性的吞吐增长,且 TTFT(首字返回时间)大幅缩短,代码补全几乎没有打字停顿感。
总结与避坑心得
玩 NAS 到了后期,比拼的不是盘位多寡,而是如何精细化榨干每一瓦功耗与每一分硬件性能。不要在一套容器内做所有事情,大模型推理、嵌入服务、向量检索三者天生需要不同的资源类型——推理吃显存带宽,嵌入吃并行 Compute,向量检索吃内存 IO。通过将 vLLM、Infinity 和 Qdrant 解耦编排,即使是一张普通的消费级甜品卡,也能让你的 NAS 真正扛起团队级私有代码底座的大旗。
===EOF===