自建 embedding + rerank 做 RAG 检索:VPS 上搭生产级私有知识库(2026)
2026-08-16 · DevCraft Studio
在 VPS 用 TEI 部署 BGE-M3 向量化、bge-reranker-v2-m3 重排,配合 Qdrant 或 pgvector 做混合检索,含 Docker 编排、维度匹配与成本建议。
延伸阅读
更多相关攻略推荐:【10刀以内VPS系列 01】年付不到70块钱的便宜VPS,到底能用、出海直播卡顿救星:跨境直播推流中转 VPS(SRT/RTMP 中继)、【数据库调优 02】数据库服务器性能优化:自建 vs 云数据库与 V、2026 独立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 上,比如 HostDare 的 CN2 线路机器或者 RackNerd 的年付特价款,几十美元一年就能把 Qdrant、应用和反向代理全托管了,数据完全在自己服务器,合规也省心。
但一旦你要上 GPU 跑 embedding + reranker,成本就上来了。一张 A10G / 4090 级别的 GPU 节点,2026 年的行情大概在 每月 200 到 400 美元,取决于显存、流量和是否含块存储。这笔钱换来的是:数据不出服务器、调用无限次、没有按量计费的 API 账单天花板。对文档量大、合规要求严的企业,通常几个月就能把 API 成本赚回来。
一个折中方案:用 RackNerd 或 HostDare 的便宜 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 延迟过高就告警。