自建 embedding + rerank 做 RAG 检索:VPS 上搭生产级私有知识库(2026)

在 VPS 用 TEI 部署 BGE-M3 向量化、bge-reranker-v2-m3 重排,配合 Qdrant 或 pgvector 做混合检索,含 Docker 编排、维度匹配与成本建议。

延伸阅读

更多相关攻略推荐:【10刀以内VPS系列 01】年付不到70块钱的便宜VPS,到底能用出海直播卡顿救星:跨境直播推流中转 VPS(SRT/RTMP 中继)【数据库调优 02】数据库服务器性能优化:自建 vs 云数据库与 V2026 独立IP VPS推荐:避免共享IP被封锁连坐什么时候非得要独立 IPv4?便宜国外 VPS 独立 IP 适用场景

为什么要把 RAG 当成搜索问题来做

2026 年再做知识库问答,大家已经达成共识:RAG 的本质不是“把文档喂给大模型”,而是一个检索质量问题。向量库能帮你从几十万条切片里召回几十条候选,但召回回来的东西到底相不相关,光靠余弦相似度经常翻车。这就是为什么 reranker(重排模型)被认为是 ROI 最高的一环——它在向量召回之后,用 Cross-Encoder 把“查询 + 候选文档”拼在一起重新打分,把真正相关的顶上来。

本文要做的,是把这一整条链路全部跑在你自己的 VPS 上:embedding 用 BGE-M3,重排用 bge-reranker-v2-m3,向量库在 Qdrant 与 pgvector 之间按规模选,全程数据不出服务器。下面都是实测可落地的步骤,不是 PPT 架构图。

整体架构:快召慢排两段式

把检索想象成一场招聘。第一段是“初筛”:向量库用 BGE-M3 把问题转成向量,从海量切片里按余弦相似度快速捞出前 30 到 50 条,这一步追求速度和召回率,宁肯多捞不能漏。第二段是“精筛”:把问题和这 50 条候选逐对送进 bge-reranker-v2-m3,模型输出一个相关性分数,重排后只留最相关的 5 到 10 条给大模型。

服务拆分如下:

  • embedding 服务:HuggingFace TEI(text-embeddings-inference),模型 BAAI/bge-m3,端口 8080。
  • rerank 服务:FlagEmbedding 提供的 reranker 镜像,模型 BAAI/bge-reranker-v2-m3,端口 8899。
  • 向量库:Qdrant(端口 6333/6334)或 PostgreSQL + pgvector(端口 5432)。
  • Qdrant 或 pgvector 负责存向量与稠密检索,配合 BM25 做稀疏检索,合起来就是混合检索。

这样拆的好处是每一段都能单独扩容、单独换模型,而且全部容器化,迁移到另一台机器只要拷一份 docker-compose。

用 TEI 部署 BGE-M3 向量化

BGE-M3 是智源开源的多语言模型,输出固定 1024 维,MIT 协议,支持稠密、稀疏、多向量三种表示,对中文和中英混合语料非常友好。用 TEI 起服务是最省心的生产级方案,因为它自带动态批处理、CUDA 图编译和 /health 健康检查。

CPU 小流量下 BGE-M3(约 5.6 亿参数)也能跑,但并发一高就吃力;想稳就用一张 16GB 以上的 GPU(A10G / A100 级别)。下面是带 GPU 的启动命令:

docker run --gpus all -p 8080:80   -v $PWD/tei-data:/data   ghcr.io/huggingface/text-embeddings-inference:1.6   --model-id BAAI/bge-m3   --max-batch-tokens 16384   --max-client-batch-size 32

注意几个实测坑:

  • 别加 --truncate,否则超长文本会被静默截断到前 512 token,而你不报错,最后召回结果诡异得高。正确做法是切片时按模型最大长度切。
  • fp16 权重约 2.2GB,再加 CUDA 激活显存,8GB 卡(T4、RTX 3070)在 max-batch-tokens 调大后容易 OOM(退出码 137)。生产建议 16GB 起步。
  • 首次启动要拉镜像 + 编译 CUDA 图,等 1 到 2 分钟再打 /health,看到 {"status":"ok"} 才算就绪。

验证一下维度是不是 1024:

curl -X POST http://localhost:8080/v1/embeddings   -H "Content-Type: application/json"   -d '{"input":"测试文本"}'

返回里 data[0].embedding 的长度必须是 1024,否则下游向量库维度对不上会直接报错。TEI 1.2+ 兼容 OpenAI 的 /v1/embeddings 接口,所以你的代码只要把 base_url 指到 TEI、随便填个 api_key 就能零改动切换,model 字段被忽略。

部署 bge-reranker-v2-m3 重排服务

bge-reranker-v2-m3 是同一家出的轻量多语言排序器,模型文件约 400MB 到 1.2GB,支持 100 种语言,在 T4 上处理单条约 12 到 15 毫秒。它是 Cross-Encoder,会把查询和文档拼起来看,分数越高质量越相关,比单纯余弦相似度准得多。

官方镜像用 FlagEmbedding 的 reranker 标签,起来就是一个 HTTP 服务:

docker run -d --gpus all -p 8899:8899   --name bge-reranker   -e USE_FP16=true   -e MAX_BATCH_SIZE=32   ghcr.io/flagopen/flagembedding:reranker-v2-m3

如果没有 GPU,把 --gpus all 去掉就会自动降到 CPU 模式,能跑就是慢一些。建议加一个健康检查:

docker run -d --gpus all -p 8899:8899   --name bge-reranker   -e USE_FP16=true   --health-cmd "curl -f http://localhost:8899/health"   ghcr.io/flagopen/flagembedding:reranker-v2-m3

切片策略也值得单独说一句:别把整篇文档原样丢进去。按语义或固定长度(比如每片 300 到 500 字、带一点重叠)切,召回更准,reranker 也更容易判断单片的相关性。太长既会触发截断又稀释语义,太短则上下文丢失。

在 Python 里调用重排很简单,把向量召回的前 N 条候选送进去:

from FlagEmbedding import FlagReranker

reranker = FlagReranker("BAAI/bge-reranker-v2-m3", use_fp16=True)
pairs = [[query, doc] for doc in candidate_docs]
scores = reranker.compute_score(pairs, normalize=True)
ranked = sorted(zip(candidate_docs, scores), key=lambda x: -x[1])

实战里经典做法是向量库先召回 top 30,再交给 reranker 精排成 top 5。这一步通常能把 Top-1 准确率提升一大截,是整条链路里最划算的投入。

向量库选型:Qdrant 还是 pgvector

两者都能自托管,选哪个看规模和运维习惯。

  • pgvector:如果你本来就在跑 PostgreSQL,直接建个 vector 列就能用,SQL 过滤、备份、混合检索(配合 tsvector 做 BM25)全都免费白送。数据量在 50 万条以下、并发不高时是最省心的默认项。建表和索引示例:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documents (
  id SERIAL PRIMARY KEY,
  content TEXT,
  embedding vector(1024)
);
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);
  • Qdrant:Rust 写的专用向量引擎,自带稀疏向量,混合检索是一等公民;单节点开量化能扛上亿条,p95 延迟在百万级向量下约 12 毫秒。当你需要严格延迟、强过滤、或规模冲到几千万条时选它。起服务:
docker run -p 6333:6333 -p 6334:6334   -v $PWD/qdrant-storage:/qdrant/storage   qdrant/qdrant

无论选哪个,混合检索的套路都是一样的:稠密向量负责“语义”,BM25 稀疏负责“精确关键词”,两者分数融合后再送 reranker。英文文档关键词命中很关键,纯向量容易漏掉带专有名词的句子。

维度匹配:最容易踩的坑

整条链路最频繁的报错来自维度不一致。BGE-M3 输出是 1024 维,所以:

  • pgvector 的 vector(1024) 必须写 1024,写 1536 或 768 会直接插不进去。
  • Qdrant 建 collection 时 VectorParams 的 size 必须是 1024,distance 用 COSINE。
  • 你上层应用(比如某框架的 DIMENSIONS 配置)如果默认按 OpenAI 的 1536 维走,一定要改成 1024,否则召回为空或特征错位。

另一个坑是归一化。TEI 默认不保证向量模长为 1,若下游用余弦相似度,建议加 --normalize,并在冒烟测试里校验向量模长在 1.0 正负 1e-5 范围内。否则余弦和内经算出来的排序会偷偷不一致。

Docker 编排与成本控制

把三个服务写进同一个 docker-compose,绑定内网,前端只暴露反向代理:

services:
  tei:
    image: ghcr.io/huggingface/text-embeddings-inference:1.6
    command: ["--model-id", "BAAI/bge-m3", "--max-batch-tokens", "16384"]
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    ports:
      - "8080:80"
  reranker:
    image: ghcr.io/flagopen/flagembedding:reranker-v2-m3
    environment:
      - USE_FP16=true
    ports:
      - "8899:8899"
  qdrant:
    image: qdrant/qdrant
    ports:
      - "6333:6333"
    volumes:
      - ./qdrant-storage:/qdrant/storage

关于机器,说点实在的。如果你只是内部知识库、客服问答这类日活不高的场景,embedding 和 rerank 可以退而求其次:BGE-M3 在 CPU 小流量下能跑,reranker 不上 GPU 也能工作,只是延迟高。这种轻量组合完全可以放在一台便宜的 KVM VPS 上,比如 HostDareCN2 线路机器或者 RackNerd年付特价款,几十美元一年就能把 Qdrant、应用和反向代理全托管了,数据完全在自己服务器,合规也省心。

但一旦你要上 GPU 跑 embedding + reranker,成本就上来了。一张 A10G / 4090 级别的 GPU 节点,2026 年的行情大概在 每月 200 到 400 美元,取决于显存、流量和是否含块存储。这笔钱换来的是:数据不出服务器、调用无限次、没有按量计费的 API 账单天花板。对文档量大、合规要求严的企业,通常几个月就能把 API 成本赚回来。

一个折中方案:用 RackNerdHostDare 的便宜 VPS 跑 Qdrant + 应用 + Nginx,GPU 推理单独丢到一台 GPU 节点,两者用内网或 WireGuard 打通。这样 GPU 节点可以按需开关,平时只养便宜的存储与查询节点。

上线前检查清单

  • TEI /health 返回 ok,embedding 维度确认为 1024。
  • reranker /health 正常,单条延迟在可接受范围。
  • 向量库 collection 的 size 与 distance 与 embedding 一致。
  • 混合检索的 BM25 部分已接好,召回 top 30 再 rerank 成 top 5。
  • 所有服务走内网,外网只留带鉴权的反向代理,密钥用环境变量而非写死。
  • 给 TEI 的 /metrics 加监控,队列堆积或 p99 延迟过高就告警。