ChatGPT合租架构设计与实现:高可用代理服务的技术解析

作为一名开发者,我最近在项目中频繁使用ChatGPT API,虽然效果惊艳,但账单也着实让人心疼。更头疼的是,官方对单个账户的请求速率和月度配额都有严格限制,一旦项目进入测试或上线阶段,很容易就触达上限,开发进度直接卡壳。身边不少朋友开始“合租”API,但简单粗暴的账号密码共享,不仅安全性堪忧,一旦有人滥用,所有人都得跟着遭殃。于是,我决定动手设计并实现一个更优雅、更工程化的解决方案——一个高可用的反向代理服务,让多个开发者可以安全、公平地共享同一个ChatGPT API账户。

1. 背景与痛点:为什么需要合租架构?

1.1 成本压力与配额限制

对于独立开发者或小型团队,ChatGPT API的成本是一笔不小的开销。按Token计费的模式,在密集调试和迭代过程中,费用增长很快。更重要的是,OpenAI对API密钥有严格的速率限制(RPM/TPM)和月度配额,单个密钥的承载能力有限,无法满足多人同时开发的需求。

1.2 现有方案的缺陷

常见的土办法是“账号轮询”,即准备多个API Key,写个脚本轮流使用。这种方法有几个明显问题:

  • 管理混乱:Key多了容易泄露,且每个Key的消耗不透明。
  • 公平性缺失:无法控制单个用户的用量,可能出现“一人用爆,全员停工”的情况。
  • 稳定性差:一个Key触发风控或被封禁,会影响整个流程,缺乏隔离和熔断机制。
  • 功能单一:缺乏统一的鉴权、监控和日志,不便于后期维护和问题排查。

因此,我们需要一个中心化的代理服务,它应该具备:身份认证、配额管理、请求隔离、负载监控 等核心能力。

2. 技术方案设计:构建高可用代理服务

我们的目标是构建一个反向代理服务,它作为用户和OpenAI API之间的中间层。所有用户请求先发送到这个代理服务,由代理服务进行鉴权、配额校验后,再转发给真正的OpenAI API,并将响应返回给用户。

2.1 整体架构图

[用户A] --> [鉴权] --> [配额校验] --> [请求队列/隔离] --> [转发至OpenAI API]
[用户B] --> [鉴权] --> [配额校验] --> [请求队列/隔离] --> [转发至OpenAI API]
       |          |              |                     |
       |          |              |                     |
    [JWT验证]  [令牌桶]      [上下文/API Key池]   [熔断 & 重试]
       |          |              |                     |
    [用户DB]   [配额配置]     [监控 & 日志] --> [Prometheus/Grafana]

2.2 核心模块详解

模块一:基于JWT的请求鉴权

我们不能让用户直接持有OpenAI的API Key。取而代之的是,我们为每个合租用户分配一个唯一的身份标识(User ID)和密码。用户首次登录后,服务端颁发一个JWT(JSON Web Token)。后续所有请求都需要在HTTP Header中携带此Token。代理服务在收到请求后,首先验证JWT的有效性和签名,从中解析出用户身份,再进行后续处理。这确保了请求来源的可追溯性和安全性。

模块二:令牌桶算法的配额管理

为了公平地分配API调用资源,我们为每个用户实施配额管理。这里采用经典的“令牌桶算法”。

  • 每个用户对应一个令牌桶。
  • 桶以固定的速率(如每秒N个Token)生成令牌,代表可用的请求配额。
  • 桶有一个最大容量,防止令牌无限累积。
  • 用户发起请求时,需要从自己的桶中获取一个(或多个,取决于请求复杂度)令牌。如果桶中有足够的令牌,则请求被允许,并扣除相应令牌;否则,请求被拒绝(返回429 Too Many Requests)。
  • 这样既能平滑请求流量,又能精确控制每个用户的调用频率和总量。
模块三:请求上下文隔离

即使用户通过了鉴权和配额校验,他们的请求在最终转发给OpenAI时,仍然共享同一个或一组后端API Key。我们需要做好隔离,防止单个用户的错误请求(如触发内容策略)或异常流量影响到其他用户。实现上,我们可以:

  • 维护一个API Key池,采用轮询或加权随机的方式为请求分配Key。
  • 为每个API Key关联独立的熔断器(如Google SRE的熔断器模式)。当某个Key因错误率过高或超时被熔断时,仅影响分配到该Key的请求,代理服务可以自动将后续请求切换到池中其他健康的Key上。
  • 每个用户的请求上下文(如会话ID)在代理层进行标记,便于日志追踪和问题定位。

2.3 技术选型:Nginx vs 自研Go服务

  • Nginx/Lua(OpenResty):优势在于高性能、稳定,利用现成的反向代理模块和Lua脚本可以快速实现鉴权、限流。但对于复杂的配额管理、与数据库交互、动态配置更新等业务逻辑,Lua开发效率和生态不如主流后端语言。
  • 自研Go服务:Go语言以高并发、高性能和简洁的语法著称,非常适合构建此类网络代理中间件。我们可以使用成熟的Web框架(如Gin、Echo)快速搭建HTTP服务,利用丰富的Go生态库(如JWT-go、uber-go/ratelimit)实现核心功能,并且部署简单,内存占用低。最终,我选择了自研Go服务,以获得最大的灵活性和控制力。

3. 代码实现关键部分

以下是用Go语言实现的核心代码片段,采用了Gin框架。

3.1 带Prometheus监控的HTTP中间件

我们在代理转发的前后,插入监控中间件,记录请求延迟、状态码和用户用量。

package middleware

import (
    "github.com/gin-gonic/gin"
    "github.com/prometheus/client_golang/prometheus"
    "strconv"
    "time"
)

var (
    httpRequestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests.",
        },
        []string{"user_id", "path", "method", "status"},
    )
    httpRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds.",
            Buckets: prometheus.DefBuckets,
        },
        []string{"user_id", "path", "method"},
    )
)

func init() {
    prometheus.MustRegister(httpRequestsTotal, httpRequestDuration)
}

func PrometheusMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        userID := c.GetString("user_id") // 从JWT解析后设置到上下文中
        if userID == "" {
            userID = "unknown"
        }

        c.Next() // 处理请求

        duration := time.Since(start).Seconds()
        status := strconv.Itoa(c.Writer.Status())
        path := c.FullPath()
        method := c.Request.Method

        httpRequestDuration.WithLabelValues(userID, path, method).Observe(duration)
        httpRequestsTotal.WithLabelValues(userID, path, method, status).Inc()
    }
}

3.2 并发安全的令牌桶实现

我们使用 golang.org/x/time/rate 包,它提供了高效的令牌桶限流器。

package quota

import (
    "sync"
    "golang.org/x/time/rate"
)

type UserLimiter struct {
    limiter *rate.Limiter
    mu      sync.RWMutex
}

type LimiterManager struct {
    users map[string]*UserLimiter
    mu    sync.RWMutex
    // 全局默认速率 (r) 和桶容量 (b)
    defaultRate  rate.Limit
    defaultBurst int
}

func NewLimiterManager(defaultRPS float64, defaultBurst int) *LimiterManager {
    return &LimiterManager{
        users:        make(map[string]*UserLimiter),
        defaultRate:  rate.Limit(defaultRPS),
        defaultBurst: defaultBurst,
    }
}

// GetLimiter 获取或创建用户的限流器
func (lm *LimiterManager) GetLimiter(userID string) *rate.Limiter {
    lm.mu.RLock()
    ul, exists := lm.users[userID]
    lm.mu.RUnlock()

    if exists {
        return ul.limiter
    }

    // 不存在则创建(Double-checked locking 优化)
    lm.mu.Lock()
    defer lm.mu.Unlock()
    if ul, exists = lm.users[userID]; exists {
        return ul.limiter
    }
    // 这里可以从数据库或配置中心读取用户特定的 rate 和 burst
    limiter := rate.NewLimiter(lm.defaultRate, lm.defaultBurst)
    lm.users[userID] = &UserLimiter{limiter: limiter}
    return limiter
}

// Allow 检查是否允许请求,模拟消耗一个令牌
func (lm *LimiterManager) Allow(userID string) bool {
    limiter := lm.GetLimiter(userID)
    return limiter.Allow()
}

3.3 错误处理与熔断机制

使用 github.com/sony/gobreaker 为每个OpenAI API Key配置熔断器。

package proxy

import (
    "github.com/sony/gobreaker"
    "time"
)

type APIBackend struct {
    Name      string
    APIKey    string // 实际应加密存储
    Client    *http.Client
    Breaker   *gobreaker.CircuitBreaker
}

func NewAPIBackend(name, apiKey string) *APIBackend {
    cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
        Name:        name,
        MaxRequests: 5,                 // 半开状态时最多允许的请求数
        Interval:    60 * time.Second,  // 清空计数的时间窗口
        Timeout:     30 * time.Second,  // 熔断后进入半开状态的等待时间
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            // 当失败率超过50%,且请求数大于10时,触发熔断
            return counts.TotalFailures > 10 && (float64(counts.TotalFailures)/float64(counts.Requests)) > 0.5
        },
        OnStateChange: func(name string, from, to gobreaker.State) {
            // 记录状态变化,用于监控告警
        },
    })
    return &APIBackend{
        Name:    name,
        APIKey:  apiKey,
        Client:  &http.Client{Timeout: 30 * time.Second},
        Breaker: cb,
    }
}

// ForwardRequest 通过熔断器转发请求
func (b *APIBackend) ForwardRequest(req *http.Request) (*http.Response, error) {
    var resp *http.Response
    var err error

    // 使用熔断器执行可能失败的操作
    _, execErr := b.Breaker.Execute(func() (interface{}, error) {
        // 克隆请求,添加真正的OpenAI API Key
        newReq := req.Clone(req.Context())
        newReq.Header.Set("Authorization", "Bearer "+b.APIKey)
        resp, err = b.Client.Do(newReq)
        if err != nil {
            return nil, err
        }
        // 如果状态码是5xx或特定的风控错误,也视为失败
        if resp.StatusCode >= 500 || resp.StatusCode == 429 {
            _ = resp.Body.Close()
            return nil, fmt.Errorf("backend error: %s", resp.Status)
        }
        return resp, nil
    })

    if execErr != nil {
        // 处理熔断器错误(服务不可用)或执行错误
        return nil, execErr
    }
    // 类型断言,返回响应
    return resp.(*http.Response), nil
}

4. 生产环境考量

4.1 压力测试

使用 wrkvegeta 对代理服务进行压测。关键指标包括:

  • 吞吐量 (RPS):代理服务本身能处理的最大请求速率。
  • 延迟分布:P50, P95, P99延迟。在10人合租,每人限制5 RPS的场景下,代理服务的P99延迟应控制在OpenAI API延迟之上增加不超过100ms。
  • 资源消耗:CPU和内存使用率。Go服务在此类I/O密集型场景下通常表现优异。

4.2 安全性加固

  • API Key管理:绝对不要将OpenAI API Key硬编码或提交到代码库。使用Vault、AWS Secrets Manager或环境变量来管理。
  • 请求审计:记录所有请求的元数据(用户、时间、Token用量),便于异常排查和成本分摊。
  • 防重放攻击:可以为每个JWT设置较短的过期时间(如1小时),并结合nonce(一次性随机数)来防止请求被截获重放。
  • 网络隔离:将代理服务部署在私有子网,仅通过负载均衡器对外暴露HTTPS端口。

4.3 成本测算

假设一个标准ChatGPT API账户月度配额为X美元,允许的RPM为Y。

  • 单人使用:成本为X美元/月,最大并发能力为Y RPM。
  • 10人合租(通过代理):总成本仍为X美元/月,但通过代理的配额管理,可以设定每人最高Y/10 RPM(或根据付费比例动态分配)。人均成本降至X/10美元/月,且由于代理层可能实现的请求缓冲和智能调度,整体可用性可能比单人直接使用更稳定。节省比例接近90%。

5. 避坑指南

5.1 OpenAI风控

OpenAI有复杂的风控系统,异常行为可能导致API Key被限流甚至封禁。

  • 避免突发流量:即使代理服务端令牌桶有容量,也应避免在短时间内向OpenAI发起海量请求。可以在代理到OpenAI之间再加一层平滑队列。
  • 模仿正常人类行为:在请求间添加微小随机延迟,避免过于规律的机器人模式。
  • 监控错误码:密切关注返回的 429 Too Many Requests5xx 错误,一旦出现频率升高,应立即告警并可能自动切换API Key或降级。

5.2 突发流量应对

  • 多级缓存:对于常见的、非实时的问答,可以在代理层引入缓存(如Redis),直接返回缓存结果,减轻后端压力。
  • 弹性伸缩:在云上部署代理服务,并配置基于CPU使用率或请求队列长度的自动伸缩组(Auto Scaling Group)。
  • 优雅降级:当所有后端API Key都接近配额或触发熔断时,代理服务可以向用户返回友好的“服务繁忙”提示,而非直接错误。

5.3 多地域部署优化

如果合租用户遍布全球,可以考虑在多个地理区域(如美东、欧洲、新加坡)部署代理实例。

  • 用户路由:使用GeoDNS或全球负载均衡器(如AWS Global Accelerator)将用户请求导向最近的代理节点。
  • 数据同步:用户的配额消耗数据需要跨区域同步(可以使用分布式数据库如Cassandra或通过中心化的Redis集群),保证配额管理的全局一致性。
  • API Key区域化:可以考虑为不同区域的代理配置不同的OpenAI API Key(如果账户支持),进一步分散风险。

结语与思考

通过构建这样一个高可用的反向代理服务,我们不仅解决了ChatGPT API合租的成本和配额难题,更收获了一套可复用的、生产级别的API网关核心模式。这套架构稍作修改,即可应用于其他有类似需求的第三方API管理场景。

在实现过程中,最让我着迷的是配额管理模块。我们目前实现了简单的静态令牌桶。这引出了一个更深入的开放性问题:如何设计一个动态配额分配算法?

例如,在合租群里,有的用户是重度使用者,愿意支付更多费用;有的只是偶尔调用。我们能否设计一个算法,根据用户的付费比例、历史使用模式、当前系统负载等因素,动态调整其令牌桶的生成速率和桶大小?甚至引入“竞价”机制,在资源紧张时,优先保障高优先级用户的请求?这涉及到资源分配公平性、算法效率以及系统复杂度的平衡,是一个非常有挑战也很有趣的方向。

如果你也对亲手构建这样的AI应用基础设施感兴趣,想更深入地体验从模型调用到完整应用落地的全过程,我强烈推荐你试试火山引擎的 从0打造个人豆包实时通话AI 动手实验。这个实验带你完整地走一遍“语音识别(ASR)→ 大模型对话(LLM)→ 语音合成(TTS)”的实时交互链路,和你自己搭建代理服务的思路有异曲同工之妙,都是把复杂的AI能力通过工程化封装成可用的服务。我在体验时发现,它把每一步的代码和配置都讲得很清楚,对于想了解AI应用后端架构的开发者来说,是个非常不错的练手项目。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐