Embedding API 的报错比聊天接口更隐蔽。聊天请求失败,用户当场就能看到;Embedding 往往跑在离线入库管道里,一次 400 或 429 可能只让某批文档缺了几行,直到几天后检索质量下降才被发现。
排查不要从“换个模型试试”开始。顺序是:先看状态码和错误类型,再看请求体大小和批量,最后检查向量本身。下面按 401、413、429、维度不匹配和空向量拆开,并给出可复用的处理代码。
说明:OpenAI、DeepSeek 等名称是其各自权利人的商标。文中接口限制以 OpenAI embeddings 接口的公开文档为例:单条输入不超过 8192 Token,单次最多 2048 条,单请求合计不超过 30 万 Token;不同厂商和网关的限制可能不同,请以实际文档为准。
一、先按错误类型分组
Embedding 报错大致可以分成七类。先归类,再决定是改代码、改批量,还是改数据。
| 现象 | 常见状态码 | 典型原因 | 先查什么 |
|---|---|---|---|
| 鉴权失败 | 401 或 403 | Key 错误、请求头缺失、Key 停用、权限或区域不对 | Authorization 头与 Key 来源 |
| 输入过长 | 400 或 413 | 单条超过模型上限、请求体超过网关限制 | 单条 Token 数与请求体字节数 |
| 批量过大 | 400 或 413 | 条数超过上限、合计 Token 超限 | 条数与合计 Token |
| 限流 | 429 | 并发突增、共享 Key、重试风暴 | 速率、并发与 Retry-After |
| 上游故障 | 5xx | 服务端过载或临时故障 | 请求 ID 与发生时间 |
| 维度不匹配 | 400 或客户端异常 | 换模型、改维度、索引固定维度 | 模型名、维度与索引配置 |
| 空向量或 NaN | 200 但数据异常 | 空文本、解析错误、归一化除零 | 向量长度、有限值与范数 |
日志至少记录时间、状态码、错误类型、模型名、批量大小、耗时和请求 ID。不要记录完整 Key,也不要把原文整段打印。
二、401 与 403:先确认请求头
401 表示身份没有通过验证,403 表示身份通过了但无权访问。两者都不要先怀疑模型能力。
常见原因:请求头没有使用 Authorization: Bearer <key>;Key 前后带空格或引号;环境变量没加载;Key 被删除或停用;Base URL 与 Key 不属于同一平台;反向代理丢弃了鉴权头。403 还要检查模型访问权限、项目权限、区域和 IP 白名单。
先用 curl 发一条只带短输入的最小请求,确认返回 200,再逐步加回批量、并发和代理。仍然 401 就换一把新 Key 对比,区分 Key 本身和网关配置。
三、413 与输入过长:先分清是谁的限制
413 通常表示请求体太大,可能是网关或反向代理的限制,不一定是模型拒绝。模型的输入上限则可能返回 400。两者要分开看。
以 OpenAI embeddings 为例:单条不超过 8192 Token,数组最多 2048 条,单请求合计不超过 30 万 Token。合计超限通常返回 400,即使每条都没超。网关还可能有请求体大小限制,在请求到达模型前就返回 413。
处理输入过长,建议按下面的顺序做:
- 用对应模型的 tokenizer 计算 Token,不要用字符数估算。
- 按标题、段落和语义边界切块,块之间保留少量重叠。
- 超长块截断兜底,记录原始长度和截断位置。
- 批量时同时限制条数和合计 Token,留出一到两成余量。
- 413 优先拆请求体,400 优先查单条内容和参数。
还要注意空字符串。OpenAI 的 embeddings 接口不接受空输入,清洗后变成空白的段落要在发送前过滤掉。
四、429:限流、并发与重试风暴
429 表示超过速率、并发或配额限制,不等于余额不足。常见触发:多个服务共用 Key、批量任务整点同时启动、重试没有退避、长请求占用较多配额。
先看响应头有没有 Retry-After,有就按它等待;没有就用指数退避加随机抖动,并设重试上限。重试只适合瞬时错误:401、403、400 和多数 413 重试多少次都不会成功。
工程上还可以:按项目拆 Key;给批量任务加队列和并发上限;大任务错峰;失败记录进死信队列。重试控制在三到五次,超过就告警,不要无限循环。
五、维度不匹配:最容易被忽略的错误
维度不匹配可能表现为数据库报错,比如“expected dimension 1536, got 1024”;也可能表现为 numpy 的形状错误、余弦相似度计算时的广播错误,甚至只是检索结果突然变得毫无相关性。
常见原因:换了模型但旧向量还在索引;调整了 dimensions 参数;同名模型在不同厂商或版本下维度不同;索引创建时固定了维度;有人手工截断或补零。维度相同也不代表能混用,不同模型的向量空间并不兼容。
修复不是补零,而是重建。每条向量记录模型名、维度、归一化方式、距离度量和切块版本;一个索引只放同一模型同一版本的向量;换模型时新建集合并全量重算,再切流验证。写入和查询两侧都要校验维度。
六、空向量、零向量与 NaN
接口返回 200 不代表数据可用。空向量、全零向量和 NaN 都会让相似度计算失效,而且往往不会抛错。
常见来源:清洗后只剩空白的文本;输入数组为空;响应解析错误导致 embedding 为空列表;本地模型对空输入返回零向量;归一化除以零产生 NaN;base64 没有正确解码。
入库前至少做三项校验:长度是否等于预期维度;每个元素是否为有限数;向量范数是否大于一个很小的阈值。校验失败的记录放进隔离表,带上原文 ID 和原因,修完再重跑,不要静默丢弃。
七、一份可复用的错误处理代码
下面的示例针对 OpenAI 兼容接口,覆盖限流退避、批量拆分和向量校验。实际使用时把模型名、维度和 Base URL 换成自己的配置。
import logging
import math
import random
import time
from openai import OpenAI, APIStatusError, APITimeoutError, RateLimitError
logger = logging.getLogger("embedding")
client = OpenAI(
api_key="sk-你的密钥",
base_url="https://api.zhongshangqihang.com/v1",
max_retries=0, # 关闭 SDK 自动重试,避免与自定义重试叠加
)
MODEL = "text-embedding-3-small"
EXPECTED_DIM = 1536
def vector_is_valid(vec: list[float]) -> bool:
if len(vec) != EXPECTED_DIM:
return False
if not all(math.isfinite(x) for x in vec):
return False
norm = math.sqrt(sum(x * x for x in vec))
return norm > 1e-6
def embed_batch(texts: list[str], attempt: int = 0) -> list[list[float] | None]:
# 空文本不发送,用 None 占位,保持下标对齐
pairs = [(i, t) for i, t in enumerate(texts) if t and t.strip()]
if not pairs:
return [None] * len(texts)
indices = [i for i, _ in pairs]
payload = [t for _, t in pairs]
try:
resp = client.embeddings.create(model=MODEL, input=payload, timeout=60)
items = sorted(resp.data, key=lambda item: item.index)
vectors = [item.embedding for item in items]
if len(vectors) != len(payload):
raise ValueError(f"返回数量不一致: {len(vectors)} != {len(payload)}")
for index, vec in enumerate(vectors):
if not vector_is_valid(vec):
raise ValueError(f"向量异常: index={index}, dim={len(vec)}")
result: list[list[float] | None] = [None] * len(texts)
for source_index, vec in zip(indices, vectors):
result[source_index] = vec
return result
except (RateLimitError, APITimeoutError) as exc:
if attempt >= 5:
raise
wait = min(2 ** attempt, 30) + random.random()
logger.warning("限流或超时,%.1f 秒后重试: %s", wait, exc)
time.sleep(wait)
return embed_batch(texts, attempt + 1)
except APIStatusError as exc:
if exc.status_code in (400, 413) and len(texts) > 1:
mid = len(texts) // 2
logger.warning("批量过大,拆分为 %d + %d", mid, len(texts) - mid)
return embed_batch(texts[:mid], attempt) + embed_batch(texts[mid:], attempt)
if exc.status_code >= 500 and attempt < 5:
wait = min(2 ** attempt, 30) + random.random()
logger.warning("上游错误,%.1f 秒后重试: %s", wait, exc)
time.sleep(wait)
return embed_batch(texts, attempt + 1)
raise
代码有两个取舍。第一,400 也参与拆批,因为有些网关把“合计 Token 超限”报成 400;拆到单条仍失败会抛出。第二,向量校验放在写入前,坏数据不会进索引。生产环境还应记录请求 ID、重试次数和失败批量。
八、排查顺序与监控清单
遇到 Embedding 报错,按下面的顺序处理,通常比反复换模型更快:
- 记录状态码、错误类型、请求 ID、模型名、批量大小和耗时。
- 401 或 403 先查请求头、Key、环境变量和平台。
- 400 先查单条 Token、合计 Token、空字符串和参数格式。
- 413 查请求体字节数和网关限制,拆小请求后重试。
- 429 看 Retry-After,退避并降低并发,检查是否共享 Key。
- 5xx 做有限次退避重试,持续出现时附请求 ID 联系服务方。
- 返回 200 但结果异常时,检查维度、范数、NaN 和归一化。
- 把上述指标接入监控,定期用固定评测集做回归。
监控按小时统计 401、400、413、429、5xx 的数量,以及空向量和维度异常条数。错误率突增时,先看是否集中在某个项目、Key、模型或文档批次,再决定降并发、拆批量、回滚还是修数据。
常见问题
413 和 400 都表示输入太长,有什么区别?
413 多数来自网关或反向代理的请求体大小限制,请求可能还没到模型就被拒绝;400 更多来自接口参数或 Token 限制。先看响应体和请求 ID,再决定是拆请求体还是切分文本。
单条没有超过 8192 Token,为什么还是 400?
可能是单请求合计 Token 超过上限,也可能是输入数组条数超限、存在空字符串、参数格式错误或模型名不对。把批量拆小、过滤空文本,再逐项检查请求体。
429 重试几次比较合适?
常见做法是三到五次,配合指数退避和随机抖动,并优先遵守 Retry-After。超过上限就进入队列或告警,不要无限重试。限流严重时,降低并发比增加重试更有效。
维度不匹配可以靠补零解决吗?
不可以。补零会改变向量方向和相似度,检索结果不可信。正确做法是统一模型和维度,新建索引后全量重算,再切流验证。
为什么接口返回 200,向量却是空的?
可能是输入为空或清洗后只剩空白,也可能是响应解析、base64 解码或本地推理出了问题。入库前校验长度、有限值和范数,把异常记录隔离出来排查。
下一步
先在中商起航 AI 网关创建测试 Key,用一小批文本把鉴权、批量、限流和向量校验各跑一遍,再接入正式入库管道。模型可用性与价格以模型价格为准。
相关阅读:
