错误码与重试
所有接口共用同一套错误结构。这一页的重点是哪些该重试、哪些不该 —— 对不可重试的错误做重试,只会更快耗尽你的限流额度。
错误结构
{
"error": {
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"message": "每分钟请求数超出上限,请在 1.4 秒后重试。",
"param": null,
"request_id": "req_01JQ7X2M9K"
}
}
状态码
| 状态码 | 含义 | 可重试 | 处理建议 |
|---|---|---|---|
| 400 | 请求格式或参数有误 | 否 | 看 param 定位字段,修代码 |
| 401 | 鉴权失败 | 否 | 检查密钥是否正确、是否已吊销、环境是否匹配 |
| 403 | 无权访问该资源 | 否 | 模型或 Skill 未开通,联系商务 |
| 404 | 资源不存在 | 否 | 检查模型 ID、Agent ID、文件 ID 是否拼错 |
| 409 | 状态冲突 | 否 | 如对已完成的微调任务重复取消 |
| 422 | 语义上无法处理 | 否 | 含安全边界拦截,属预期行为,见下 |
| 429 | 触发限流或额度耗尽 | 是 | 指数退避重试;额度耗尽则需充值 |
| 500 | 服务端内部错误 | 是 | 退避重试;持续出现请带 request_id 报障 |
| 503 | 服务暂时过载 | 是 | 退避重试,或降级到 lite 型号 |
| 504 | 处理超时 | 是 | 长任务改用流式或批量接口 |
错误码表
鉴权与权限
| code | type | 说明 |
|---|---|---|
invalid_api_key | authentication_error | 密钥不存在或格式错误 |
api_key_revoked | authentication_error | 密钥已在控制台吊销 |
environment_mismatch | authentication_error | 用 sk-test- 访问了生产域名,或反过来 |
permission_denied | permission_error | 账号未开通该模型或 Skill |
ip_not_allowed | permission_error | 请求来源 IP 不在白名单内(开启了 IP 限制时) |
请求参数
| code | type | 说明 |
|---|---|---|
invalid_request | invalid_request_error | 请求体不是合法 JSON,或缺少必填字段 |
model_not_found | invalid_request_error | 模型 ID 不存在。注意微调模型 ID 是完整的
ft:base:org:suffix:hash |
context_length_exceeded | invalid_request_error | 输入加输出超过该型号上下文上限,需要裁剪历史 |
unsupported_parameter | invalid_request_error | 该型号不支持此参数,如对 embedding 模型传 temperature |
invalid_image | invalid_request_error | 图像无法下载或格式不支持。支持 JPEG / PNG / WebP,单张 20 MB 以内 |
限流与额度
| code | type | 说明 |
|---|---|---|
rate_limit_exceeded | rate_limit_error | RPM 或 TPM 触顶。可重试 |
concurrency_limit_exceeded | rate_limit_error | 并发数触顶。可重试 |
insufficient_quota | quota_error | 额度用尽。重试无用,需充值或提额 |
sandbox_quota_exhausted | quota_error | 沙箱免费额度用尽,联系商务转生产 |
安全边界
| code | type | 说明 |
|---|---|---|
safety_boundary_triggered | safety_error | 触及医疗诊断、用药等硬边界。响应里带
koach.suggested_reply,可直接展示给用户 |
content_filtered | safety_error | 输入或输出命中内容安全策略 |
minor_protection | safety_error | 档案显示为未成年人,请求的训练强度或减重目标超出安全范围 |
它们是确定性的 —— 同样的输入永远得到同样的拒绝。
正确做法是把 koach.suggested_reply 展示给用户,
并在你侧埋点统计触发频次。如果某类正常问题被频繁误拦,
告诉我们,这属于评测集要覆盖的
false_refusal_rate 指标。
Skill 与 Agent
| code | type | 说明 |
|---|---|---|
skill_not_enabled | permission_error | 该 Skill 未在合同范围内 |
skill_input_invalid | invalid_request_error | input 不符合该 Skill 的 schema,param 指出具体字段 |
media_unprocessable | invalid_request_error | 图像或视频里没检测到人体 / 食物,无法分析 |
profile_not_found | invalid_request_error | profile 对应的记忆档案不存在,需要先创建 |
max_steps_exceeded | agent_error | Agent 工具调用轮数超过 max_steps,
通常是 instructions 让它陷入了循环 |
tool_execution_timeout | agent_error | 你侧的函数在 30 秒内没返回 |
重试策略
只对 429 和 5xx 重试,用指数退避加抖动。
抖动很重要 —— 没有它,被同时限流的客户端会在同一时刻一起重试,
形成新的尖峰。
import random, time
import httpx
RETRYABLE = {429, 500, 502, 503, 504}
def call_with_retry(payload, max_attempts=5):
for attempt in range(max_attempts):
r = httpx.post(
f"{BASE_URL}/chat/completions",
headers={"Authorization": f"Bearer {API_KEY}"},
json=payload,
timeout=60,
)
if r.status_code not in RETRYABLE:
r.raise_for_status()
return r.json()
# 额度耗尽不是限流,重试没有意义
if r.json().get("error", {}).get("code") == "insufficient_quota":
raise RuntimeError("额度已用尽,请充值")
# 服务端给了明确的等待时间就照做,否则指数退避
retry_after = r.headers.get("retry-after")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(delay + random.uniform(0, 0.5)) # 加抖动
raise RuntimeError(f"重试 {max_attempts} 次后仍失败")
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function callWithRetry(payload, maxAttempts = 5) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const res = await fetch(`${BASE_URL}/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!RETRYABLE.has(res.status)) {
if (!res.ok) throw new Error(await res.text());
return res.json();
}
const body = await res.json();
if (body?.error?.code === "insufficient_quota") {
throw new Error("额度已用尽,请充值");
}
const retryAfter = res.headers.get("retry-after");
const delay = retryAfter ? Number(retryAfter) * 1000 : 2 ** attempt * 1000;
await sleep(delay + Math.random() * 500);
}
throw new Error(`重试 ${maxAttempts} 次后仍失败`);
}
koach 与 @koach/sdk 默认对可重试错误做两次退避重试,
可以通过 max_retries 调整。上面的代码只在你直接发 HTTP 请求时才需要。
幂等
重试带来一个新问题:第一次请求其实成功了,只是响应在网络上丢了,
重试就会产生重复扣费。对会产生副作用或按次计费的接口(Skill 调用、
文件上传、微调任务、批量任务),带上 Idempotency-Key:
curl "$KOACH_BASE_URL/skills/training.plan.generate/invoke" \
-H "Authorization: Bearer $KOACH_API_KEY" \
-H "Idempotency-Key: plan-usr_8f2a41-20260817-w34" \
-H "Content-Type: application/json" \
-d '{"profile": "usr_8f2a41", "input": {"…": "…"}}'
24 小时内相同 key 的请求直接返回首次的结果,不重复执行也不重复计费。
key 建议用业务上天然唯一的组合,比如
{用途}-{用户}-{日期}-{周次},而不是随机 UUID ——
随机值在重试时无法复用,等于没做幂等。
降级
持续 503 通常意味着该型号所在集群过载。
如果你的场景对专业度要求没那么极致,可以做一层自动降级:
keepace-pro不可用时降到keepace-lite, 多数轻交互场景用户感知不到差别- Skill 不可用时退回纯模型回答,虽然拿不到结构化结果, 但至少不是一个错误提示
- 记忆大脑不可用时以无
profile的方式调用, 回答会变通用但服务不中断
这三条降级路径在私有化部署里同样适用, FDE 会在实施阶段帮你把降级链路和告警阈值一起配好。