跳到主内容

LLM API 服务端架构实战:流式转发、超时重试与成本限流的网关设计 ​

问题背景:业务直连模型厂商的四大失控 ​

业务团队接入大模型的第一版往往简单粗暴:每个服务里塞一个厂商 SDK 和一个 API Key。三个月后问题集中爆发——某服务把 Key 硬编码进了开源仓库,被迫紧急轮换全量密钥;月底账单是预估的三倍,没人说得清哪个业务烧掉的钱最多;模型厂商限流(429)时各业务各自重试,形成重试风暴;流式对话在网关/Nginx 层被缓冲,用户看到的是 40 秒后一次性吐出全文而非打字机效果。这些问题的共同解法是引入一层统一 LLM 网关:密钥只在网关存在、业务侧持内部凭证;流式转发、超时重试、限流计费在同一处实现与演进;切换模型厂商时业务方不感知。这层网关的定位与 API 网关不同——它必须深刻理解 LLM 的流式语义与 token 计费模型,而不是简单地转发 HTTP。

方案对比:自研、开源网关与多云聚合 ​

开源方案(LiteLLM、OneAPI 类项目):开箱支持多厂商路由与 Key 管理,适合快速起步;但深度定制(如自有计费体系、内部审批流)时改造成本上升。自研薄网关:只做鉴权、限流、流式转发、用量记录四件事,一个几百行的服务就能覆盖 80% 需求,运维负担小,适合有平台团队的公司。多云聚合平台:接多家厂商做故障切换与成本路由,适合多模型策略成熟的大型团队。绝大多数团队从「自研薄网关」起步是最稳的路径——LLM 网关的复杂度不在代码量,而在流式语义与计费正确性这些细节上,自己写一遍才能真正掌控。

实操步骤:四个核心能力逐一落地 ​

第一步:SSE 流式转发,逐块冲刷是关键。 反向代理默认会缓冲响应体,流式输出必须在网关逐行写出并立即 flush:

go
func proxyStream(w http.ResponseWriter, r *http.Request) {
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "当前连接不支持流式响应", http.StatusInternalServerError)
        return
    }
    upReq := buildUpstream(r)              // 换上真正的厂商 Key,剥离内部凭证
    upReq.Header.Set("Accept", "text/event-stream")
    resp, err := httpClient.Do(upReq)
    if err != nil {
        http.Error(w, "上游模型请求失败", http.StatusBadGateway)
        return
    }
    defer resp.Body.Close()
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")

    scanner := bufio.NewScanner(resp.Body)
    for scanner.Scan() {
        line := scanner.Text()
        fmt.Fprintf(w, "%s\n", line)   // SSE 按行转发
        flusher.Flush()                 // 立即冲刷,保证打字机体验
        if strings.HasPrefix(line, "[DONE]") {
            break
        }
    }
    // 结束后解析 usage 帧,异步落用量表用于计费
}

第二步:超时分层设计。 LLM 请求的超时不能只设一个总超时:连接超时 2 秒;首 token 超时(发出请求到收到第一个 SSE 事件)通常 10~30 秒,超时说明排队异常应尽早失败;整体超时按生成上限估算(如 120 秒)。首 token 超时是 LLM 场景特有的关键指标——它直接决定用户感知的响应速度,监控里要单列。

第三步:重试只覆盖「未发生输出」的失败。 非 2xx 响应(429、5xx、网络错误)可按指数退避重试,429 尊重 Retry-After 头;流式一旦开始输出,任何失败都不能重试——重发会与已推送的内容拼接重复,只能标记本次对话异常让客户端提示用户重问。多厂商接入时,把「失败切换到备用模型」做在网关路由层,业务无感。

第四步:成本核算与双维度限流。 网关从响应 usage 字段(或自算 token 数)记录每次调用的模型、输入输出 token、业务方、场景,落库聚合出各团队账单;限流按「API Key 维度 + 用户维度」双层令牌桶,防止单用户刷爆团队配额:

sql
CREATE TABLE llm_usage (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    api_key_id BIGINT NOT NULL COMMENT '内部凭证 id,厂商 Key 不落业务库',
    biz_scene VARCHAR(64) NOT NULL COMMENT '业务场景,用于账单归集',
    model VARCHAR(64) NOT NULL,
    input_tokens INT NOT NULL,
    output_tokens INT NOT NULL,
    cost_usd DECIMAL(10, 6) NOT NULL COMMENT '按模型单价折算的成本',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_key_time (api_key_id, created_at),
    INDEX idx_scene_time (biz_scene, created_at)
) COMMENT='LLM 调用用量与成本台账';

配额控制读取台账聚合值:团队月配额耗尽返回 402 而不是默默超支,把成本决策还给业务负责人。

多模型路由把「切换」从事故变成配置。 网关在路由层维护模型注册表:主模型、备用模型与各自的优先级、健康状态。上游持续 5xx 或首 token 超时率超阈值时自动摘除,流量切往备用模型,恢复后自动回归。路由粒度还可以更细:按场景路由(摘要用小模型、复杂推理用旗舰模型)、按成本路由(非高峰时段切低价模型)、按能力路由(超长上下文请求只发给支持的模型)。路由规则作为配置热更新、业务侧零改动,这是网关相对 SDK 直连最大的架构红利。

避坑总结 ​

  1. 厂商密钥只存在于网关。 业务侧一律使用内部短期凭证,泄漏面从 N 个服务收敛到 1 个服务;日志必须脱敏,请求体、Key 都不能进访问日志。
  2. 流式请求的重试与缓冲语义要显式处理。 网关和 Nginx 都要关闭缓冲(X-Accel-Buffering: no),输出开始后的失败绝不能重发,这是最容易出线上事故的边界。
  3. 成本核算只认网关台账。 客户端上报的 usage 不可信,账单以网关从上游响应解析的 usage 为准,定期用厂商账单对账校准。
  4. 限流要区分错误预算与成本预算。 QPS 限流防击穿,token/金额配额防超支,两者独立配置;429 响应要返回剩余配额信息,方便业务做优雅降级。
  5. 模型切换要靠协议适配层而非改业务。 网关对外提供统一请求格式(对齐 OpenAI 兼容协议已成事实标准),对内适配各家差异,业务永远只写一份调用代码。

收尾 ​

LLM 网关是 AI 应用工程化的分水岭:没有它,每个业务都在重复处理鉴权、流式、重试、计费这些同样的问题,且各有各的坑;有了它,模型切换、成本管控、故障降级变成平台能力。先花一周写一个只做四件事的薄网关,比在业务代码里散养十几个 Key 三个月后被迫重构划算得多。AI 应用的下一步竞争在工程化,而网关正是工程化的第一块基石——它是模型成本、稳定性与合规三条曲线交汇的唯一收口点,值得像数据库一样被认真对待。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。