Weaviate 使用 OpenAI ada 批量导入时触发 429 限流
结论
这是 OpenAI Embeddings API 的请求频率限制,与 JSON、Pandas 或 Weaviate schema 本身无关。当前额度只有 3 requests/min,而 Weaviate 导入每个对象时都要调用 OpenAI 生成向量。批量接口只负责在客户端批量提交对象,不能绕过 OpenAI 的限流。
可以这样处理:
- 为 OpenAI API 账户配置计费,并申请更高额度。这是最直接的办法。
- 如果额度仍是 3 RPM,将导入速度控制在每 21~25 秒一个对象。
- 遇到 429 时执行指数退避,并按照
Retry-After指定的时间等待后重试。 - 使用确定性 UUID,让重试可以安全执行,避免生成重复数据。
- 记录导入失败的对象,等导入结束后单独补偿。
- 只向量化需要参与语义搜索的字段,通常是
title和description。 - 将检索和生成分开处理。ada 只负责向量化,最终回答仍需要聊天或生成模型。
5000 条数据在 3 RPM 下的理论最短耗时约为:
[ 5000 \div 3 = 1666.7\text{ 分钟}\approx27.8\text{ 小时} ]
实际导入还会受到网络延迟、失败重试和查询向量化的影响,通常需要更长时间。
调整 schema
skip 应使用布尔值,不能写成字符串 "false"。author 和 url 通常不需要参与语义向量,可以直接跳过。否则,URL 和作者名等内容可能会稀释 description 的语义。
下面继续沿用问题中的旧版 Weaviate schema 格式:
schema = {
"classes": [
{
"class": "PersonalLibrary",
"description": "Information from Library Books",
"vectorizer": "text2vec-openai",
"moduleConfig": {
"text2vec-openai": {
"model": "ada",
"type": "text",
}
},
"properties": [
{
"name": "description",
"dataType": ["text"],
"description": "The information",
"moduleConfig": {
"text2vec-openai": {
"skip": False,
"vectorizePropertyName": False,
}
},
},
{
"name": "title",
"dataType": ["text"],
"description": "The title",
"moduleConfig": {
"text2vec-openai": {
"skip": False,
"vectorizePropertyName": False,
}
},
},
{
"name": "author",
"dataType": ["text"],
"description": "The author",
"moduleConfig": {
"text2vec-openai": {
"skip": True,
"vectorizePropertyName": False,
}
},
},
{
"name": "url",
"dataType": ["text"],
"description": "The url",
"moduleConfig": {
"text2vec-openai": {
"skip": True,
"vectorizePropertyName": False,
}
},
},
{
"name": "path",
"dataType": ["text"],
"moduleConfig": {
"text2vec-openai": {
"skip": True,
"vectorizePropertyName": False,
}
},
},
],
}
]
}
ada 和 text-embedding-ada-002 都属于较早的模型配置。新建系统时,应根据当前 Weaviate 和 OpenAI API 版本选择受支持的 embedding 模型。Weaviate Python Client v3 和 v4 的 schema 写法也不同,不能直接混用。
在 3 RPM 下进行节流导入
限额只有 3 RPM 时,主动控制为一次导入一个对象,比一次提交大量对象更容易管理。下面的示例适用于问题中使用的旧版客户端调用方式。
import time
import random
import pandas as pd
import weaviate
from weaviate.util import generate_uuid5
CLASS_NAME = "PersonalLibrary"
MIN_INTERVAL_SECONDS = 21
MAX_RETRIES = 8
df = pd.read_json("dummy4.json")
last_request_at = 0.0
failed_rows = []
for index, row in df.iterrows():
properties = {
"title": str(row.get("title", "")),
"description": str(row.get("description", "")),
"author": str(row.get("author", "")),
"url": str(row.get("url", "")),
"path": "https://books.mypage.com/explore/" + str(row["id"]),
}
# 同一本书始终生成相同 UUID,重试时不会不断创建重复对象。
object_uuid = generate_uuid5({
"class": CLASS_NAME,
"source_id": str(row["id"]),
})
imported = False
for attempt in range(MAX_RETRIES):
elapsed = time.monotonic() - last_request_at
if elapsed < MIN_INTERVAL_SECONDS:
time.sleep(MIN_INTERVAL_SECONDS - elapsed)
try:
last_request_at = time.monotonic()
client.data_object.create(
data_object=properties,
class_name=CLASS_NAME,
uuid=object_uuid,
)
imported = True
print(f"Imported row={index}, id={row['id']}")
break
except Exception as exc:
message = str(exc)
is_rate_limit = (
"429" in message
or "rate limit" in message.lower()
)
if not is_rate_limit:
print(f"Non-retryable error at row={index}: {message}")
break
# 21、42、84……秒,并加入少量随机抖动。
delay = min(
MIN_INTERVAL_SECONDS * (2 ** attempt),
300,
)
delay += random.uniform(0, 3)
print(
f"Rate limited at row={index}; "
f"retry={attempt + 1}/{MAX_RETRIES}; "
f"sleep={delay:.1f}s"
)
time.sleep(delay)
if not imported:
failed_rows.append({
"index": int(index),
"id": str(row["id"]),
"properties": properties,
"uuid": str(object_uuid),
})
print(f"Finished. Failed objects: {len(failed_rows)}")
这里将间隔设为 21 秒,而不是精确的 20 秒,是为了给滑动时间窗口、网络延迟和时钟误差留出余量。如果导入期间还有其他程序使用同一个 OpenAI organization 和 API key,还需要进一步降低速度。
生产环境中不建议通过 except Exception 判断错误类型。应根据已安装的 Weaviate 客户端版本捕获具体异常,再从响应状态码或错误结构中识别 429。上面的宽泛捕获只是为了说明节流和重试流程。
使用 batch 时的处理方式
提高 OpenAI 限额后,可以继续使用 Weaviate batch,但要注意以下几点:
- batch 大小和并发数不能超过下游 OpenAI 的承受能力。
- 不能只看 Python 调用是否抛出异常,还要检查每个对象的批量响应。
- 如果响应中包含
429、rate limit或向量化失败,应将对应对象加入重试队列。 - 重试时继续使用原来的 UUID。
- 不要无限重试。超过重试上限后,应保存失败清单。
在 3 RPM 下,即使设置 batch_size=3,也不能马上连续提交下一批。每批之后至少要等待约 60 秒,滚动窗口仍可能使处于边界位置的请求收到 429。因此,一次导入一个对象,并间隔约 21 秒,通常更容易控制。
更高效的替代方案:自行生成向量
还可以由应用自行生成向量:
- 应用直接调用 OpenAI Embeddings API。
- 在应用侧统一处理限流、批处理和重试。
- 取得向量后,将对象和向量一起写入 Weaviate。
- Weaviate class 使用自带向量模式,避免导入时再次调用 OpenAI。
示意代码如下。具体参数需要根据已安装的 OpenAI SDK 和 Weaviate 客户端版本调整:
text_to_embed = "\n".join([
str(row.get("title", "")),
str(row.get("description", "")),
])
embedding_response = openai_client.embeddings.create(
model="YOUR_SUPPORTED_EMBEDDING_MODEL",
input=text_to_embed,
)
vector = embedding_response.data[0].embedding
client.data_object.create(
data_object=properties,
class_name="PersonalLibrary",
uuid=object_uuid,
vector=vector,
)
采用这种架构后,应用可以完全掌控限流和失败重试,也可以保存已经生成的向量,避免 Weaviate 写入失败后重复消耗 embedding 请求。
不过,仅凭上述错误无法判断 3 RPM 是只限制请求数,还是同时存在 token 限制,具体要以账户实际返回的限额为准。部分 Embeddings API 和 SDK 支持在一次请求中传入多个字符串,但是否适用于当前账户、模型和 Weaviate 集成,也不能仅凭该错误确定。如果由应用自行调用 API,可以在模型和限额允许的情况下,将多条文本组成一个 input 数组,以减少请求次数。
导入后的检索与回答流程
text2vec-openai 或 embedding 模型只负责生成向量,不会自动给出最终答案。完整流程如下:
用户问题
→ 生成问题向量
→ 在 Weaviate 中检索最相关的图书 description
→ 将检索结果作为上下文发送给生成模型
→ 生成有依据的自然语言回答
例如,先执行语义检索:
{
Get {
PersonalLibrary(
nearText: {
concepts: ["用户的问题"]
}
limit: 5
) {
title
author
description
url
path
_additional {
distance
}
}
}
}
然后将结果整理成提示词:
context = "\n\n".join(
f"书名:{item['title']}\n"
f"作者:{item['author']}\n"
f"内容:{item['description']}"
for item in search_results
)
prompt = f"""
请只根据下面提供的图书资料回答问题。
如果资料不足以得出答案,请明确说明资料不足,不要猜测。
资料:
{context}
问题:
{user_question}
"""
最后,将提示词发送给选定的聊天或生成模型。也可以使用与当前 Weaviate 版本兼容的生成模块完成类似流程,但检索数量、上下文长度、引用方式和无答案策略仍需由应用明确控制。
注意事项
- 批量导入不等于只发出一次 OpenAI 请求。限流发生在 Weaviate 调用向量化服务的阶段。
- API key 所属 organization 的总流量是共享的,其他服务发出的请求也会占用 3 RPM。
- 除 RPM 外,账户还可能受到 TPM、每日额度或余额限制。
- 不要对所有错误无限重试。429、临时网络错误和部分 5xx 可以重试;schema 错误和字段类型错误等问题应直接修复。
- 应提前清洗
NaN、空 description 和异常长文本,避免生成无意义的向量或触发 token 限制。 - 导入完成后,要核对 Weaviate 中的对象总数,并检查对象是否包含向量。不能仅凭脚本运行结束判断导入成功。
- 使用确定性 UUID 后,补偿导入可以安全地改为 create-or-update/upsert,具体方法名取决于客户端版本。
- 5000 条数据在 3 RPM 下可以导入,但会耗费很长时间。如果这是正式数据集,提高 API 限额或自行批量生成向量通常更实际。
备注:内容仅供参考。
