上周接了个私活,甲方要做企业知识库问答系统。核心技术栈是 RAG,第一步就是把文档切片后做 Embedding 向量化。听起来不复杂,但我在调 Embedding API 这一步折腾了快两天------OpenAI 的 text-embedding-3-large 延迟飘忽不定,某些开源模型效果又拉胯,中间还踩了个维度对齐的大坑。
直接说结论:调用 Embedding API 的核心就是选对模型、配好接口、处理好分批请求。2026 年主流方案有三种------OpenAI 官方接口、开源模型本地部署、通过聚合 API 平台一个 Key 切换多家 Embedding 模型。对中小项目来说,第三种性价比最高,改一行 base_url 就能跑。
先说结论
我实测了三种方案,按「部署成本 × 效果 × 延迟」综合排序:
| 方案 | 模型 | 维度 | 中文效果 | 平均延迟 | 月成本(10万次/月) | 推荐场景 |
|---|---|---|---|---|---|---|
| 聚合 API | text-embedding-3-large | 3072 | ★★★★★ | ~350ms | ≈¥95 | 生产环境首选 |
| OpenAI 官方 | text-embedding-3-large | 3072 | ★★★★★ | 200ms~2s+ | $13(≈¥95) | 网络稳定时 |
| 本地部署 | bge-large-zh-v1.5 | 1024 | ★★★★ | ~80ms | 显卡电费 | 数据敏感/离线场景 |
最终我生产环境选了聚合 API,开发调试用本地 bge 模型,下面展开讲。
环境准备
bash
# Python 3.10+
pip install openai numpy tiktoken
# 如果要本地部署 bge 模型
pip install sentence-transformers torch
测试环境:MacBook Pro M3 + Python 3.12,服务器是 2C4G 的轻量云主机。
方案一:OpenAI text-embedding-3-large
效果最好的通用 Embedding 模型,2026 年在 RAG 场景依然是标杆。
python
from openai import OpenAI
import numpy as np
client = OpenAI(
api_key="your-api-key",
base_url="https://api.ofox.ai/v1" # 聚合接口,低延迟直连
)
def get_embeddings(texts: list[str], model="text-embedding-3-large") -> list[list[float]]:
"""批量获取文本向量,单次最多 2048 条"""
response = client.embeddings.create(
input=texts,
model=model
)
return [item.embedding for item in response.data]
def cosine_similarity(a, b):
"""计算余弦相似度"""
a, b = np.array(a), np.array(b)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# 测试中文语义相似度
texts = [
"如何部署 Kubernetes 集群",
"K8s 集群搭建教程",
"今天天气真不错",
]
embeddings = get_embeddings(texts)
print(f"语义相近: {cosine_similarity(embeddings[0], embeddings[1]):.4f}") # 预期 > 0.85
print(f"语义无关: {cosine_similarity(embeddings[0], embeddings[2]):.4f}") # 预期 < 0.3
实测输出:
makefile
语义相近: 0.8917
语义无关: 0.1243
效果没话说。这里用的是 ofox.ai 的聚合接口,ofox.ai 是一个 AI 模型聚合平台,一个 API Key 可以调用 GPT-5、Claude Opus 4.6、Gemini 3 等 50+ 模型(包括各家的 Embedding 模型),低延迟直连无需代理,支持支付宝付款。对我来说最大的好处是不用分别管理 OpenAI、Google、百度各家的 Key。
方案二:本地部署 bge-large-zh-v1.5
数据敏感不能外传,或者调用量大想省钱,本地部署是正解。BAAI 的 bge 系列在中文场景下效果很能打。
python
from sentence_transformers import SentenceTransformer
import numpy as np
# 首次运行会自动下载模型,约 1.3GB
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')
def get_local_embeddings(texts: list[str]) -> np.ndarray:
"""本地模型生成向量"""
# bge 模型推荐加 instruction prefix
prefixed = [f"为这个句子生成表示以用于检索相关文章:{t}" for t in texts]
return model.encode(prefixed, normalize_embeddings=True)
texts = [
"如何部署 Kubernetes 集群",
"K8s 集群搭建教程",
"今天天气真不错",
]
embeddings = get_local_embeddings(texts)
sim_related = np.dot(embeddings[0], embeddings[1])
sim_unrelated = np.dot(embeddings[0], embeddings[2])
print(f"语义相近: {sim_related:.4f}") # 实测 0.8641
print(f"语义无关: {sim_unrelated:.4f}") # 实测 0.1087
比 OpenAI 的差一点点,但胜在免费且零延迟。有个坑要注意------bge 输出 1024 维,OpenAI 是 3072 维,两个模型的向量不能混进同一个向量数据库,维度不对齐检索直接报错。
方案三:完整 RAG Pipeline
前两个方案只是调 API,实际项目里需要一个完整的流程。我把私活的核心代码精简了一下:
完整代码:
python
from openai import OpenAI
import numpy as np
import json
client = OpenAI(
api_key="your-key",
base_url="https://api.ofox.ai/v1"
)
# ========== Step 1: 文本切片 ==========
def chunk_text(text: str, chunk_size=500, overlap=50) -> list[str]:
"""滑动窗口切片,overlap 防止语义断裂"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start = end - overlap
return chunks
# ========== Step 2: 批量 Embedding ==========
def batch_embed(texts: list[str], batch_size=100) -> list[list[float]]:
"""分批请求,防止单次请求过大被限流"""
all_embeddings = []
for i in range(0, len(texts), batch_size):
batch = texts[i:i + batch_size]
resp = client.embeddings.create(
input=batch,
model="text-embedding-3-large"
)
all_embeddings.extend([item.embedding for item in resp.data])
print(f"已处理 {min(i + batch_size, len(texts))}/{len(texts)}")
return all_embeddings
# ========== Step 3: 简易向量检索(生产用 Milvus/Qdrant) ==========
class SimpleVectorStore:
def __init__(self):
self.texts = []
self.vectors = []
def add(self, texts: list[str], vectors: list[list[float]]):
self.texts.extend(texts)
self.vectors.extend(vectors)
def search(self, query_vector: list[float], top_k=3) -> list[str]:
query = np.array(query_vector)
scores = []
for i, vec in enumerate(self.vectors):
sim = np.dot(query, np.array(vec)) / (
np.linalg.norm(query) * np.linalg.norm(np.array(vec))
)
scores.append((sim, i))
scores.sort(reverse=True)
return [self.texts[idx] for _, idx in scores[:top_k]]
# ========== Step 4: RAG 问答 ==========
def rag_answer(question: str, store: SimpleVectorStore) -> str:
# 问题向量化
q_resp = client.embeddings.create(
input=[question],
model="text-embedding-3-large"
)
q_vec = q_resp.data[0].embedding
# 检索相关片段
relevant_chunks = store.search(q_vec, top_k=3)
context = "\n---\n".join(relevant_chunks)
# 让 LLM 基于上下文回答
completion = client.chat.completions.create(
model="gpt-5",
messages=[
{"role": "system", "content": "基于以下参考资料回答用户问题。如果资料中没有相关信息,请说明。"},
{"role": "user", "content": f"参考资料:\n{context}\n\n问题: {question}"}
]
)
return completion.choices[0].message.content
# ========== 使用示例 ==========
if __name__ == "__main__":
# 模拟文档
doc = """
Kubernetes(简称 K8s)是一个开源的容器编排平台。它可以自动化部署、扩展和管理容器化应用程序。
K8s 的核心组件包括 API Server、etcd、Scheduler、Controller Manager。
Pod 是 K8s 最小的部署单元,一个 Pod 可以包含一个或多个容器。
Service 用于暴露 Pod 的网络服务,支持 ClusterIP、NodePort、LoadBalancer 三种类型。
Deployment 用于管理 Pod 的副本数量,支持滚动更新和回滚。
"""
# 切片 + 向量化
chunks = chunk_text(doc, chunk_size=200, overlap=30)
vectors = batch_embed(chunks)
# 存入向量库
store = SimpleVectorStore()
store.add(chunks, vectors)
# 提问
answer = rag_answer("K8s 的 Service 有哪几种类型?", store)
print(answer)
这段代码可以直接跑。生产环境里 SimpleVectorStore 要换成 Milvus、Qdrant 或 Weaviate,我那个私活最后用的 Qdrant,Docker 一行命令就起了。
踩坑记录
坑 1:Embedding 维度不一致导致检索炸了
开发时用 bge 模型(1024维),上线换 OpenAI(3072维),忘了重新跑向量入库,检索接口直接报维度不匹配。换模型就得重跑全量数据,这个别忘了。
坑 2:单次请求塞太多文本被 429
OpenAI Embedding API 单次最多传 2048 条文本,但如果每条文本都很长,总 token 数超了也会报错。解决方案就是上面的 batch_embed,每次 100 条,很稳。
坑 3:中文切片不能按固定字符数硬切
一开始 chunk_size=500 按字符切,把「容器编排」切成了「容器编」和「排平台」,语义断裂,检索召回率直接暴跌。后来改成按句号分割再合并到目标长度,好多了:
python
import re
def smart_chunk(text: str, max_size=500) -> list[str]:
"""按句子边界切片"""
sentences = re.split(r'(?<=[。!?\n])', text)
chunks, current = [], ""
for s in sentences:
if len(current) + len(s) > max_size and current:
chunks.append(current.strip())
current = s
else:
current += s
if current.strip():
chunks.append(current.strip())
return chunks
坑 4:没做 normalize 导致相似度计算异常
bge 模型的 encode 方法默认不归一化,余弦相似度算出来会偏大。加上 normalize_embeddings=True 就正常了。OpenAI 的 API 返回值默认已经归一化,不用管。
小结
Embedding 就三件事:选模型、切文本、管向量。2026 年的选法:
- 效果优先选
text-embedding-3-large,中英文通吃 - 数据敏感选
bge-large-zh-v1.5本地部署 - 怕折腾直接用聚合 API,一个 Key 搞定 Embedding + Chat
代码都贴完整了,复制就能跑。做 RAG 项目的话,切片策略对最终效果的影响比模型选择大得多,这个坑我替你踩过了。有问题评论区聊。