模型调用的可靠性、延迟与成本

# 模型调用的可靠性、延迟与成本

本篇目标

把模型当作一个高延迟、受限流并按量计费的外部依赖,掌握超时、重试、并发、缓存、取消和可观测性的工程边界。

分类调用失败设计安全重试控制并发成本建立调用指标

# 先记住一句话

可靠的模型调用不是失败后一直重试,而是设置总超时、限制并发、只重试暂时性错误,并用幂等、取消和监控控制重复费用与资源泄漏。

# 模型调用为什么比普通函数复杂

一次远程模型调用可能经历排队、网络传输、内容检查、推理和流式返回。任何阶段都可能失败,而且客户端超时不代表服务端一定没有执行。

业务请求
  ↓ 排队与并发控制
模型请求
  ├─ 成功并返回
  ├─ 被限流
  ├─ 网络超时但服务端仍可能执行
  ├─ 模型拒绝
  └─ 输出被截断或解析失败
1
2
3
4
5
6
7
8

因此错误处理必须区分 “请求根本不合法” 、“暂时无法执行” 和 “可能已经执行但结果未知”。

# 超时要分层设置

超时 控制什么
连接超时 与模型服务建立连接的最长等待
首 Token 超时 流式请求多久必须出现首个有效事件
读取超时 两次数据到达之间允许等待多久
总超时 整个业务请求最多占用多久

只设置底层读取超时可能让重试后的总耗时无限增长。业务入口需要一个总 Deadline,并把同一个取消信号传到模型客户端。

用户关闭页面或上游请求取消时,应停止读取流、关闭连接并阻止结果写回。不要中途把请求 Context 换成一个永不取消的后台 Context。

# 哪些错误可以重试

适合有限重试的通常是限流、短暂网络故障和部分服务端错误。以下情况通常不应原样重试:

  • API Key 或权限错误;
  • 输入超出模型限制;
  • 请求 Schema 非法;
  • 内容被明确拒绝;
  • 输出业务校验失败,但输入和 Prompt 没有任何变化。

重试使用指数退避并加入随机抖动,避免大量实例同时再次请求:

# 文件位置:examples/backoff.py
import random


def retry_delay(attempt: int, base_seconds: float = 0.5, cap_seconds: float = 8.0) -> float:
    """返回第 attempt 次重试前的等待时间;attempt 从 0 开始。"""
    if attempt < 0:
        raise ValueError("attempt 不能小于 0")
    maximum = min(cap_seconds, base_seconds * (2**attempt))
    # Full Jitter 让并发请求分散在 0 到 maximum 之间。
    return random.uniform(0, maximum)


if __name__ == "__main__":
    for current_attempt in range(4):
        print(round(retry_delay(current_attempt), 2))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

等待算法只是重试的一部分。实际调用还要限制最大次数、尊重服务端重试提示,并确保总超时尚未耗尽。

# 幂等为什么重要

幂等表示同一个业务请求重复执行,最终效果仍与执行一次一致。模型生成本身可能每次不同,但业务可以给任务分配唯一键,并记录状态:

request_key = 用户 + 资源版本 + 任务类型

不存在 → 创建 running 任务并调用模型
running  → 返回处理中,不启动第二次调用
succeeded → 复用已保存结果
failed_retriable → 按策略重新执行
1
2
3
4
5
6

如果进程在模型成功后、结果保存前崩溃,应用可能无法知道模型是否执行完成。只有提供方支持幂等请求或结果查询时,才能更彻底地避免重复计费;否则只能通过任务记录和缩小不确定窗口降低风险。

# 怎样限制并发

每个 HTTP 请求直接启动模型调用,会同时消耗提供方配额、连接、内存和预算。常见控制方式包括:

  • 进程内 Semaphore 限制并发调用数;
  • 按用户或租户设置速率和 Token 配额;
  • 使用有界队列吸收短时突发;
  • 队列满时快速拒绝,而不是无限堆积;
  • 长任务使用持久化任务队列和 Worker;
  • 为高优先级请求保留容量。

并发上限应根据提供方限额、平均 Token、P95 延迟和实例数预算,并通过排队时间与拒绝率调整。

# 缓存能省什么,又有什么风险

缓存方式 适合场景 主要风险
精确结果缓存 相同模型、Prompt 和输入重复请求 忘记模型或 Prompt 版本
Prompt Prefix Cache 大段稳定前缀反复使用 动态内容放在前面导致无法命中
语义缓存 问法不同但答案可复用 相似问题实际条件不同

缓存键至少考虑模型版本、Prompt 版本、输入、权限范围和业务数据版本。含个人数据的结果不能跨用户复用。实时价格、库存和权限等数据也不适合长时间缓存。

# 成本怎样估算

可以先使用下面的通用关系:

单次成本
= 输入 Token × 输入单价
+ 缓存输入 Token × 缓存单价
+ 输出 Token × 输出单价
+ 工具、图片或其他计费项
1
2
3
4
5

月度预算还要乘以流量,并考虑重试率、失败调用、评测流量和峰值。价格与计量方式会变化,应读取当前模型官方价格,而不是把数值永久写死在业务代码中。

# 最少要记录哪些指标

  • 模型与 Prompt 版本;
  • 请求 ID、业务任务 ID 和租户;
  • 输入、缓存输入、输出和推理 Token;
  • 排队时间、首 Token 延迟和总延迟;
  • 完成、取消、拒绝、限流、超时和解析失败;
  • 重试次数、缓存命中和估算费用。

日志不要记录 API Key,也不要默认记录完整敏感 Prompt。需要排查内容时,应脱敏、采样并设置访问和保留策略。

# 高频面试题与回答

1. 模型请求超时后可以直接重试吗?参考答案

不能一概而论。客户端超时不代表服务端没有执行,直接重试可能重复计费。应先区分错误类型,使用任务幂等键和请求记录,只对暂时性错误有限重试,并把所有尝试限制在同一个总超时内。

2. 为什么重试要使用指数退避和随机抖动?参考答案

指数退避逐步降低请求频率,给服务恢复时间;随机抖动让多个实例不要在同一时刻再次发起请求,避免形成重试风暴。还必须设置最大次数和总时间上限。

3. 模型结果缓存的 Key 应包含什么?参考答案

至少包含模型版本、Prompt 版本、规范化输入、权限范围和相关业务数据版本。否则模型升级、规则变化或不同用户之间可能错误复用旧结果,造成质量或数据泄漏问题。

4. 模型调用需要观察哪些延迟?参考答案

至少区分排队时间、首 Token 延迟和总完成时间,并看 P50、P95 等分位数。这样才能判断瓶颈来自本地排队、模型开始生成慢,还是输出过长。

# 接下来学什么

下一篇学习 Prompt、RAG 与微调怎样选择,根据问题根因选择方案,而不是看到效果不好就直接训练模型。

# 参考资料