6.2 KiB
local-embedding
本地嵌入(embedding)服务。完整替代 Ollama —— 零 API Key、文档不出内网,
对外提供 OpenAI 兼容的 POST /v1/embeddings。
为什么不用 Ollama
给 PandaWiki 做本地化部署时,嵌入模型 本来是走 Ollama 的,但为了这一件事装了一整套 Ollama,代价有三:
- Ollama 不归你管 —— 它由 fnOS 应用中心的
ai_installer托管, 进程和监听地址都不在自己的控制范围内; - 官方 registry 不可达 ——
registry.ollama.ai的 blob 下载会完全卡死 (实测 30 分钟零进度),hf-mirror.com也连不上,最后只能绕道 ModelScope; - 需要一层转发 —— Ollama 只监听
127.0.0.1,容器访问不到,得额外加一个 socat 容器把 docker 网关转发过去。
这个服务把这三件事都消掉了:一个容器、一个镜像、启动时自动下模型。
与 Ollama 的实测差异
同一批文本,本服务(ONNX int8)与 Ollama(bge-m3 GGUF Q8_0)的向量余弦相似度:
| 文本 | 相似度 |
|---|---|
| 如何部署 PandaWiki 知识库 | 0.9848 |
| PandaWiki 支持 Docker 部署,需要先配置向量模型 | 0.9821 |
| 今天上证指数涨跌如何 | 0.9849 |
内部相对关系也一致(0.7286 / 0.7390、0.3663 / 0.3664)。差异来自量化方式不同, 属于预期范围,不是实现偏差。维度同为 1024。
快速开始
docker compose up -d
首次启动会下载约 563MB 模型到 ./models:
[local-embedding] 模型目录: /models
[model_int8.onnx] 0.0B / 542.1MB
[model_int8.onnx] 542.1MB / 542.1MB (100%)
[model_int8.onnx] 校验通过
[local-embedding] 模型准备完成
之后重启或重建容器都是零下载:
[model_int8.onnx] 已存在且校验通过 (542.1MB)
[local-embedding] 模型已就绪,跳过下载
服务监听 63002:
curl -s localhost:63002/health
# {"status":"ok","model":"bge-m3"}
curl -s -X POST localhost:63002/v1/embeddings \
-H 'Content-Type: application/json' \
-d '{"model":"bge-m3","input":["如何部署知识库","今天天气不错"]}'
设计要点
池化:CLS + L2 归一化
ONNX 导出的第一个输出可能是 [batch, hidden](已池化)也可能是
[batch, seq, hidden](token 级),两种都兼容;后者取 [CLS](第 0 个 token)。
bge 系列官方推荐 CLS 池化 + L2 归一化,归一化后余弦相似度与内积等价, 调用方随便用哪种度量都对。实测输出 L2 范数为 1.000000。
模型不在镜像里,启动时按需下载
download_model.py 在 entrypoint 里先跑,做三件事:
- 检查 —— 每个文件比对
大小 + sha256,全通过就立刻返回,零网络请求 - 续传 —— 下到
.part,重试带Range头从断点继续 - 原子替换 —— 校验通过才
os.replace成正式文件, 避免中断留下的半个文件被当成有效模型加载
还有磁盘预检:空间不够直接报明确错误,而不是刷一片 no space。
为什么选 int8
带 AVX512-VNNI 的 CPU 上 int8 推理能直接吃到指令集加速。542MB 的 int8 vs 2.1GB 的 fp32,体积差 4 倍,精度损失很小 —— 见上面的对比数据。
注意 onnx/model.onnx 只有 0.6MB,它的权重在外置的 model.onnx_data(2.1GB)里;
model_int8.onnx 是自包含的单文件,少一个踩坑点。
接口
POST /v1/embeddings
OpenAI 标准。请求:
{
"model": "bge-m3",
"input": ["文本1", "文本2"],
"encoding_format": "float"
}
input 可以是单个字符串、字符串数组,或 token id 数组(三种可混排)。
响应:
{
"object": "list",
"data": [
{"object": "embedding", "index": 0, "embedding": [0.0123, ...]}
],
"model": "bge-m3",
"usage": {"prompt_tokens": 7, "total_tokens": 7},
"_meta": {"latency_ms": 141, "dimension": 1024}
}
_meta 是本服务额外加的非标准字段,方便排查延迟和维度,不影响兼容性。
同时接受 /embeddings、/v1/embed、/embed。
其它
GET /health—— 存活探测GET /info—— 当前模型、输入输出张量名、参数GET /v1/models—— 兼容部分客户端的启动探测
配置
全部写在 docker-compose.yml 的 environment 里,改完 docker compose up -d 生效。
| 变量 | 默认 | 说明 |
|---|---|---|
| 宿主机端口 | 63002 |
compose 的 ports,映射到容器 8000 |
MODEL_DIR |
/models |
容器内模型目录 |
MODEL_REPO |
Xenova/bge-m3 |
换模型改这里 |
MODEL_BASE |
ModelScope | 换下载源改这里 |
ONNX_FILE |
model_int8.onnx |
换量化档位 |
MAX_LEN |
512 |
截断长度,bge-m3 原生支持 8192,但 CPU 上长序列很慢 |
BATCH_SIZE |
16 |
每批条数 |
WORKERS |
1 |
调大前先看内存,每个 worker 各加载一份模型 |
同仓库可选量化档位:model_fp16.onnx(1081MB)、model_q4f16.onnx(668MB)、
model_q4.onnx(1190MB)。
实测性能
MacBook(Docker Desktop,arm64 模拟)与 i7-1165G7:
| 场景 | 耗时 |
|---|---|
| 40 条批量 | 449 ms(约 90 条/秒) |
| 5 条批量 | 141 ms |
| 镜像大小 | 401 MB |
| 模型大小 | 563 MB |
已知限制
- 单 worker:默认
--workers 1,每个 worker 会各加载一份模型(约 2GB 内存峰值)。 高并发应调WORKERS,同时注意内存翻倍。 - 不鉴权:不校验
api_key。放到非受信网络需自行加反向代理。 - 仅 CPU:未接 ONNXRuntime-GPU。
- 只支持 ONNX:换 safetensors 需改
MANIFEST并引入 torch。 - 长文本默认截断到 512:bge-m3 能力上限是 8192,但 CPU 上超过 512 明显变慢。
需要处理长文档时调
MAX_LEN并实测延迟。
文件
local-embedding/
├── app.py FastAPI 服务(OpenAI 兼容)
├── download_model.py 模型检查/下载/校验(幂等,支持续传)
├── entrypoint.sh 先下模型,再起 uvicorn
├── requirements.txt 运行时依赖(无 torch)
├── Dockerfile
├── docker-compose.yml 参数都在这,端口 63002
├── README.md
└── .gitignore
配套的 local-remarker 是同一套思路的重排服务, 接口为 SiliconFlow 格式。两个服务可以独立部署,也可以放在同一个 compose 里。