开发集成 · 工程实践
国内服务器调用 OpenAI/Claude API 的工程要点
线上调用失败,多数不是模型的问题,而是超时设得太短、重试没有退避、并发没有上限。这篇按工程顺序给出具体参数和处置办法。
先把失败分成四类
看到报错就改代码是最费时间的做法。先按现象归类,每一类的排查方向完全不同:
| 现象 | 常见原因 | 先查什么 |
|---|---|---|
| 连接阶段就超时 | 出口网络不通,或域名解析失败 | 在服务器上直接发起一次简单请求,看是解析失败还是连不上 |
| 连上了但一直等不到内容 | 读超时设得太短,或上游在排队 | 看首字节返回时间,再对比自己的超时设置 |
| 立刻返回鉴权类错误 | 密钥无效、没读到环境变量、被吊销 | 打印密钥的前几位和长度,确认注入生效 |
| 返回过载或网关类错误 | 上游短时过载,或自身触发了频率限制 | 看官方状态页,再看自己单位时间的请求量 |
归类的价值在于:前两类要改网络或超时配置,第三类要查密钥注入,第四类要么等待要么降速。用同一套办法去治四类问题,只会把时间耗在无关的地方。
超时参数怎么设
大部分客户端库把超时当成一个整体,实际上要拆成三段来看,分别对应不同的问题:
- 连接超时:3 到 5 秒。只负责建立连接。这一段设置过长,出口不通时会白等很久,用户看到的是界面卡死。
- 读超时:非流式 120 秒,流式 15 到 30 秒。读超时指的是两次数据到达之间的间隔上限,而不是整个请求的总时长。流式输出时数据是持续返回的,所以可以设得短一些,一旦断流就能及时发现。
- 整体超时:300 秒。作为兜底,防止某个请求永远挂在那里占着连接。超过这个时间的请求,即使最终会成功,对用户来说也已经没有意义了。
还有一个容易被忽略的点:关闭流式会显著拉长等待。长回答需要几十秒才能生成完,这段时间里服务端不返回任何数据,读超时必须设得很大才不会误判为失败。建议凡是面向用户的功能一律开启流式,只有后台批处理任务才用非流式。
重试:必须带退避和抖动
不带退避的重试等于放大故障。上游刚刚过载,你立刻再打一次,只会让它更慢。可执行的做法:
- 先判断该不该重试。频率限制、过载、网关错误值得重试;参数错误、鉴权失败、内容不合规重试多少次都一样,直接放弃并记录。
- 退避按指数增长:第一次等 1 秒,第二次 2 秒,第三次 4 秒,第四次 8 秒,上限封顶在 30 秒左右。
- 在等待时间上叠加一个随机量,范围取等待时长的三分之一以内。多个实例同时重试时,这个抖动能把请求错开,避免形成新的峰值。
- 限制总次数,建议最多 4 次。超过这个次数还失败,说明问题不在瞬时波动,继续重试只是浪费额度。
- 如果响应里带有建议等待时间的字段,优先听它的,不要自作主张用更短的时间重试。
另外要注意幂等性。查询类请求随便重试;一旦是「生成并入库」这类有副作用的操作,重试前必须用请求标识去重,否则一次网络抖动可能变成两条记录。给每个逻辑请求生成一个唯一标识,随请求一起传,落库时按它判重。
并发与速率控制
- 信号量限并发。从并发 5 开始压测,稳定后再逐步往上加。不要一上来就设几十,上游的并发上限通常比你想的低。
- 令牌桶限速率。把单位时间允许的请求数和文本量分别做成两个桶,取用前都要通过。只限请求数不限文本量,长文档任务仍然会把额度打满。
- 队列加背压。请求先入队,超过队列长度上限就直接拒绝并给用户一个明确提示。无限入队的结果是所有人都等很久,然后一起超时。
- 连接复用。保持长连接、复用连接池,能省掉每次请求的握手开销,对延迟的改善比调参明显。
- 按用途分级。面向用户的交互请求优先,后台批量任务限速跑,必要时在夜间执行。两类混在一个池子里,会互相拖慢。
密钥管理
- 密钥只从环境变量或密钥管理服务读取,绝不写进代码仓库。提交前用工具扫一遍,把密钥模式加进检查规则。
- 按环境分开:开发、测试、生产各用一个密钥。这样出问题时能单独吊销一个而不影响其它环境。
- 按用途再分一层。后台批处理用单独的密钥,方便单独设消费上限,也能在异常时快速定位来源。
- 日志里只打印密钥的前几位,用于确认加载了哪一个。完整密钥一旦进了日志系统,就等于泄露。
- 定期轮换,建议 90 天一次,并且把轮换做成可回滚的两步:先加新密钥,确认生效后再删旧的。
- 发现泄露的第一动作是吊销,第二动作是查用量。不要先改代码,顺序反了会留下继续被消耗的窗口。
错误码排查表
| 返回码 | 含义 | 处理方式 |
|---|---|---|
| 400 | 请求本身有问题,如参数不合法、模型名不存在 | 不重试。把完整错误信息记下来,对照文档逐项检查 |
| 401 | 密钥无效或未正确注入 | 不重试。确认环境变量、前后空格、是否被吊销 |
| 403 | 密钥有效但没有该模型的权限 | 不重试。换模型或申请权限 |
| 404 | 地址或模型名写错 | 不重试。核对请求路径 |
| 413 | 单次请求内容过大 | 不重试。拆分输入或先做摘要 |
| 429 | 触发频率限制,或账户额度耗尽 | 区分两种:前者退避后重试,后者充值后才有效 |
| 500 / 502 / 503 / 504 | 服务端或网关异常 | 退避重试,并准备降级模型 |
| 529 | 上游整体过载 | 拉长退避时间,或切到备用模型 |
429 需要特别说明:同一个返回码对应两种完全不同的原因,处理方式也相反。判断方法是看错误内容里是频率相关还是额度相关的描述。把这两种混为一谈,会出现「一直重试但永远失败」的情况,白白烧掉几个小时。
上线前的观测与回归
- 记录每次请求的响应标识、耗时、实际使用的模型和用量。出问题时,这个标识是唯一能拿去查的凭据。
- 把耗时按分位数看,不要只看平均值。平均值正常但长尾很糟,用户感受到的就是「有时特别慢」。
- 失败率按错误码分类打点,而不是只统计一个总的失败率。总数不变但构成变化,往往是上游策略调整的信号。
- 每天核对一次用量与账单,发现异常增长当天就查,别等到月底。
- 准备一组固定输入作为回归用例,每次改配置或换模型后跑一遍,对比输出的长度和结构。
- 准备降级路径并真的演练一次:主模型不可用时切换到备用模型,界面上的提示文案也一并准备好。
把这些做完,调用层才算稳定。剩下的模型选型和提示词优化,才是可以慢慢迭代的部分。
常见问题
服务器所在地区会影响调用稳定性吗?
会。不同地区到上游的网络路径质量差别明显,表现为延迟波动和连接失败率不同。上线前建议在目标地区实测一轮,而不是在本地开发机上判断。
本地调试正常,部署到服务器就报错,先查什么?
先查密钥注入和超时设置这两项。本地调试通常是从文件里读配置,服务器上依赖环境变量,容易读不到;超时设置在服务器上往往沿用默认值,而默认值通常偏短。
重试会不会导致重复计费?
会,而且很常见。上游已经生成完但响应在回程中断时,你重试一次就是两次计费。解决办法是记录请求标识并做去重,同时把重试次数上限压到四次以内。