NAS打造私有化代码检索与知识库:vLLM + Qdrant + Embedding轻量落地指南

6次阅读
没有评论

共计 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===

正文完
 0
评论(没有评论)