豆包 API 由火山方舟提供,接口形态接近 OpenAI,但错误语义有自己的特点。最典型的是 404:服务本身没有挂,问题往往出在推理接入点 ID、模型名、区域或路径。另一个高频误判是 429:它可能是速率限制,也可能是并发、配额、欠费或资源包耗尽,处理方式完全不同。
排查顺序建议固定为五步:先看 HTTP 状态码,再读响应体里的 code 和 message,然后取请求 ID,接着用最小请求复现,最后才改参数或换模型。本文按 401、404、400、429、超时和 5xx 展开,附错误处理代码和日志清单。模型与计费以站内模型价格为准。
说明:豆包、火山方舟、火山引擎是字节跳动的商标,本文仅作技术说明,与字节跳动无隶属、代理或合作关系。错误码、限流口径和接口行为可能随版本调整,最终以官方文档和实际响应为准。
一、先分清两层错误
HTTP 状态码说明协议层结果,响应体里的 code、type、message 说明具体原因。同样是 401,可能是没带 Key、Key 写错、Key 失效或项目无权限;同样是 429,可能是请求过快、并发过高、额度耗尽或账户欠费。只记录状态码,排障时只能猜。
| HTTP | 常见含义 | 豆包场景 | 是否重试 |
|---|---|---|---|
| 401 | 鉴权失败 | Key 缺失、格式错误、失效或与区域不匹配 | 否,先修凭证 |
| 403 | 无权限 | Key 无该模型或接入点权限 | 否,查授权 |
| 404 | 资源不存在 | 接入点 ID、模型名、路径或区域不对 | 否,先核对 |
| 400 | 参数错误 | 上下文超长、max_tokens 非法、图片格式不支持 |
否,改请求 |
| 429 | 限流或配额 | RPM、TPM、并发超限,欠费或资源包耗尽 | 视原因而定 |
| 500/502/503 | 服务端错误 | 上游异常、网关抖动、短暂不可用 | 是,退避重试 |
| 超时 | 连接或读取超时 | 网络、排队、输入过长、输出过长 | 有限重试 |
响应体的字段名以实际返回为准。有的接口用 error.code,有的把 code 放在顶层,代码里不要假设只有一种结构。
二、401:鉴权失败的四个原因
401 与模型能力、余额通常无关,先查凭证链路:
- 请求头是否是
Authorization: Bearer <key>,冒号后有一个空格。 - Key 是否从环境变量正确加载,前后有没有引号、空格或换行。
- Key 是否已删除、轮换或超出有效期。
- Key 所属项目、区域和 Base URL 是否一致。
从聊天窗口或文档复制的 Key 最容易带入不可见字符。先做一次最小请求,只输出一个词,能排除大部分低级错误。如果同一把 Key 在控制台可用、在代码里报 401,重点查环境变量和反向代理是否丢弃了认证头。
三、404:接入点 ID 与模型名最容易混
火山方舟支持两种常见的模型指定方式:控制台创建的推理接入点,通常以 ep- 开头;以及部分模型支持的模型 ID 直调。调用时两者都填在 model 字段,但权限、限流和适用范围可能不同。把接入点 ID 当成模型名、把模型名当接入点、或者复制了别的项目的接入点,都会得到 404 或类似的“资源不存在”。
排查顺序:
- 确认 Base URL 是
https://ark.cn-beijing.volces.com/api/v3这类区域地址,没有多写或漏写/api/v3。 - 在控制台核对接入点是否存在、是否属于当前项目、是否已删除或停用。
- 确认当前调用方式要求的是接入点 ID 还是模型 ID,不要凭记忆混填。
- 用 curl 发一条最小请求,排除 SDK 自动拼接路径和代理重写的影响。
- 看响应内容类型:如果返回的是 HTML 错误页,请求可能根本没到方舟,而是被网关或反向代理拦下了。
第 5 点很实用。404 配 HTML,通常是路径或代理问题;404 配 JSON 错误体,才更像模型或接入点问题。
四、400:上下文超长与参数错误
400 的范围很广,最常见的是上下文超长。上下文窗口通常按输入 Token 加输出 Token 计算,max_tokens 设得过大,会让原本能通过的请求越界。图片、工具定义和系统提示也占 Token,长对话还会不断累积历史。
处理方式不是一味调大上限,而是给输入留余量:
- 压缩或分段系统提示,删掉重复规则。
- 对历史对话做摘要,只保留最近若干轮和关键事实。
- 长文档先检索再拼接,不要整篇塞入。
- 图片先压缩尺寸,确认格式和大小在支持范围内。
- 检查
temperature、top_p等参数是否越界,消息角色是否合法。
Token 计数是估算值,和实际计费口径可能有差异。接近上限时留 10% 到 20% 的余量,比精确卡点更稳。
五、429:限流、并发和配额要分开处理
429 至少有四类原因,处理方式不同:
- 速率限制:每分钟请求数或 Token 数超过套餐上限。短时突发,退避后通常恢复。
- 并发限制:同时在飞的请求过多。需要降并发或排队,单纯重试会加剧拥塞。
- 配额或资源包耗尽:额度用完,重试没有意义,需要充值或调整套餐。
- 账户状态异常:欠费、未实名或权限变更,按控制台提示处理。
如果响应带 Retry-After,优先按它等待。没有就做指数退避加随机抖动。不要所有 429 都重试:配额类和欠费类 429 重试只会拉长故障时间,还会放大日志噪音。
流式请求的 429 可能发生在建立连接时,也可能以流内错误事件的形式出现。客户端要同时处理 HTTP 状态码和流内错误,不能只看第一段数据。
六、超时与流式中断:分三段定位
超时要拆成三段:
- 连接超时:DNS、TLS、出口代理或网络不可达,通常几秒内失败。
- 首 Token 超时:请求已到达上游,但在排队或处理长输入,表现为长时间没有第一个分片。
- 读取超时:两个分片之间间隔过久,可能是输出太长、推理时间长或连接被中间设备断开。
三段要用不同的超时配置。把总超时调得很大,会让故障请求长期占用连接池;调得太小,又会误杀正常的长输出。建议连接超时 5 秒左右,读取超时按业务 P95 设置,另外在应用层设总预算。
流式场景还要检查反向代理是否开启了缓冲。代理缓冲会让分片攒在一起才下发,看起来像“卡住”。同时设置心跳和空闲超时,断流后不要直接把半截内容当完整结果入库。
七、请求 ID 与日志:排障的凭证
火山方舟的响应通常带有请求标识,常见字段包括 X-Request-Id,部分接口还可能返回 X-Tt-Logid 一类的日志 ID。字段名以实际响应为准,工程上应当把响应头整体保留一份,而不是只挑一个字段。
客户端还应生成自己的 trace_id,在同一次业务请求的所有重试之间保持不变。日志至少记录:时间、trace_id、Base URL、模型或接入点、HTTP 状态码、错误 code、错误 message、耗时、重试次数、是否流式、Token 用量。
Key 只记录前几位或哈希值,用户原文和完整响应不要整段落盘。错误响应体可以保留前 500 个字符,足够定位又不至于泄露数据。
八、重试与退避:一段可用的代码
下面的代码关闭 SDK 自动重试,按状态码和错误内容判断是否重试,尊重 Retry-After,并为每次逻辑请求生成 trace_id:
import os
import random
import time
import uuid
import httpx
from openai import OpenAI, APIConnectionError, APIStatusError, APITimeoutError
client = OpenAI(
api_key=os.environ["ARK_API_KEY"],
base_url=os.environ.get(
"ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3"
),
timeout=httpx.Timeout(connect=5.0, read=180.0, write=30.0, pool=10.0),
max_retries=0, # 关闭 SDK 自动重试,统一由下面的逻辑控制
)
def parse_error(exc: APIStatusError) -> tuple[str, str]:
try:
body = exc.response.json()
except Exception:
return "", exc.response.text[:200]
err = body.get("error", body)
return str(err.get("code") or err.get("type") or ""), str(err.get("message") or "")
def is_retryable(exc: Exception) -> bool:
if isinstance(exc, (APITimeoutError, APIConnectionError)):
return True
if not isinstance(exc, APIStatusError):
return False
if exc.status_code >= 500:
return True
if exc.status_code != 429:
return False
code, message = parse_error(exc)
text = f"{code} {message}".lower()
# 配额、欠费、权限类 429 重试无意义,按实际文案补充关键词
return not any(k in text for k in ("quota", "arrears", "balance", "permission"))
def ask(prompt: str, max_attempts: int = 4) -> str:
trace_id = uuid.uuid4().hex[:12]
for attempt in range(1, max_attempts + 1):
try:
resp = client.chat.completions.create(
model=os.environ["ARK_MODEL"], # 接入点 ID 或模型 ID
messages=[{"role": "user", "content": prompt}],
max_tokens=1024,
)
return resp.choices[0].message.content
except Exception as exc:
status = getattr(exc, "status_code", "")
code, message = parse_error(exc) if isinstance(exc, APIStatusError) else ("", "")
print(
f"trace={trace_id} attempt={attempt} status={status} "
f"code={code} message={message[:200]}"
)
if attempt == max_attempts or not is_retryable(exc):
raise
retry_after = getattr(
getattr(exc, "response", None), "headers", {}
).get("retry-after")
try:
wait = float(retry_after) if retry_after else min(2 ** (attempt - 1), 20)
except ValueError:
wait = min(2 ** (attempt - 1), 20)
time.sleep(wait + random.uniform(0, 1))
raise RuntimeError("重试次数已用完")
三条补充规则:有副作用的请求先用幂等键确认状态,再决定是否重发;重试预算要全局控制,避免多个服务同时退避后一起冲击上游;监控 429 占比和 P95 延迟,突增时先降并发,而不是先调大重试次数。
九、联系支持前的清单
- 故障发生时间和时区,是持续发生还是偶发。
- Base URL、完整接口路径、模型名或接入点 ID。
- HTTP 状态码、错误 code、错误 message 和请求 ID。
- 一条可复现的最小 curl 请求,Key 做脱敏处理。
- 是否流式、是否长上下文、是否带图片或工具调用。
- 最近是否改过 Key、接入点、配额或代理配置。
- 影响范围:单 Key、单项目、单区域还是全部服务。
准备齐这些信息,支持团队才能直接对照上游日志,而不是来回询问。每次故障后补一条监控或告警,比事后写复盘更有效。
十、常见问题(FAQ)
1. 404 一定是模型下线了吗?
不一定。更常见的是接入点 ID 或模型名不对、接入点已删除、Base URL 区域不匹配,或者路径少了 /api/v3。先看响应是 JSON 错误体还是 HTML 错误页,再逐项核对。
2. 429 应该重试几次?
对速率限制和短暂拥塞,重试 2 到 3 次并做指数退避即可。配额耗尽、欠费、权限不足导致的 429 不要重试,先处理账户或套餐。尊重响应里的 Retry-After。
3. 上下文长度到底怎么算?
通常按输入 Token 加输出 Token 计算,max_tokens 是输出上限,也会占用总额度。图片、工具定义和历史消息都计入输入。接近上限时留出余量,具体口径以模型文档为准。
4. 流式请求的超时应该怎么设?
读取超时通常约束两个分片之间的间隔,而不是整个请求的总时长。连接超时单独设置,应用层再设总预算和空闲超时。代理如果开启缓冲,会让分片延迟下发,需要一并检查。
5. 接入网关后错误变多了怎么办?
先确认错误是网关返回还是上游返回:看响应体格式、状态码和请求 ID。保留网关的 trace ID,分别用直连和网关各发一条最小请求对比。多数问题出在路径拼接、认证头转发或超时设置。
下一步
先在中商起航 AI 网关创建测试 Key,把 401、404、429、超时各演练一次,再接入生产监控。可用模型和计费方式见模型价格。
