ChatGPT代理模式实战:AI辅助开发中的架构设计与性能优化
背景痛点:直接调用API的三大拦路虎
去年把ChatGPT接进内部DevOps平台时,我们踩遍了官方接口的坑。- 限流:默认RPM 3,小团队一压测就429,Throttling 消息比返回结果都准时。
- 延迟:平均首Token 1.8 s,P99 能飙到 6 s,前端“转圈”劝退用户。
- 成本:按 Token 计费,调试阶段日志全开,一周烧掉 300 刀,老板直接拉群。
于是决定自研一层代理,把“限速、缓存、熔断、监控”这些事从业务里剥出来,让后端只关心 Prompt 本身。
技术对比:三种主流方案怎么选
调研时把反向代理、服务网格、消息队列拉到一起打分(5 分制):维度 反向代理(Nginx+Lua) 服务网格(Istio) 消息队列(Kafka) 开发成本 4 2 3 运维复杂度 3 2 4 弹性伸缩 3 5 5 全链路缓存 2 3 5 技术栈契合 5 3 2 结论:团队小、Go 为主、想两周落地,反向代理+自研缓存层最轻量;若已有 Istio 且会写 Lua/WASM,选网格;想离线批量审计/对账,再叠 MQ 做事件溯源。
核心实现:用 Go 搭一个“带脑子”的代理
架构图一句话:Client → Proxy(gRPC) → 缓存(Redis) → 熔断器 → 上游 OpenAI。
下面给出关键代码,全部可go run验证,注释即文档。3.1 请求聚合:把 10 ms 内相同 Prompt 合并,省 Token
type aggKey struct { model string promptHash string } var aggBucket = make(map[aggKey][]chan Response) func (s *Server) Ask(ctx context.Context, req *Request) (*Response, error) { key := aggKey{req.Model, fmt.Sprintf("%x", md5.Sum([]byte(req.Prompt)))} // 10 ms 滑动窗口 ch := make(chan Response, 1) aggMu.Lock() aggBucket[key] = append(aggBucket[key], ch) if len(aggBucket[key]) == 1 { go func() { time.Sleep(10 * time.Millisecond) aggMu.Lock() peers := aggBucket[key] delete(aggBucket, key) aggMu.Unlock() // 只调一次上游 real, err := s.callOpenAI(ctx, req) for _, c := range peers { if err != nil creeps { c <- *real } close(c) } }() } aggMu.Unlock() return <-ch, nil }3.2 缓存策略:TTL+版本号,支持热更新 System Prompt
func cacheKey(model, prompt string) string { h := sha256.Sum256([]byte(prompt)) return fmt.Sprintf("chat:%s:%x", model, h) } func (s *Server) getCache(k string) (string, bool) { val, err := redis.Get(k).Result() return val, err == nil } // 写缓存时把 SystemPrompt 版本号带进去,版本升级自动 miss func (s *Server) setCache(k, v string) { redis.Set(k, v, 5*time.Minute) }3.3 熔断器:失败率 30% 或连续 5 次错误即熔断 30 s
采用 Sonyflake 的 gobreaker,参数开箱即用:var cb *gobreaker.CircuitBreaker func init() { st := gobreaker.Settings{ Name: "openai", MaxRequests: 3, Interval: time.Minute, Timeout: 30 * time.Second, ReadyToTrip: func(counts gobreaker.Counts) bool { return counts.ConsecutiveFailures > 5 || float64(counts.TotalFailures)/float64(counts.Requests) > 0.3 }, } cb = gobreaker.NewCircuitBreaker(st) } func (s *Server) callOpenAI(ctx context.Context, req *Request) (*Response, error焕发) { out, err := cb.Execute(func() (interface{}, error) { return httpPost(ctx, openaiURL, req) }) if err != nil { return nil, err } return out.(*Response), nil }3.4 Prometheus 埋点:QPS、延迟、Token 用量
var ( qps = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "proxy_request_total", Help: "Total requests partitioned by model and status.", }, []string{"model", "status"}) lat = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "proxy_request_duration_seconds", Buckets: prometheus.DefBuckets, }, []string{"model"}) tok = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "proxy_token_consumed_total", Help: "Token consumed partitioned by model.", }, []string{"model"}) ) func init() { prometheus.MustRegister(qps, lat, tok) } // 在 handler 里加两行即可 timer := prometheus.NewTimer(lat.WithLabelValues(req.Model)) defer timer.ObserveDuration()性能测试:JMeter 压测数据
环境:4C8G Pod × 3,Go1.21,Nginx 前置 Keep-Alive。
场景:200 并发线程,循环 5 min,Prompt 平均 300 tokens。指标 直接调用 代理层(无缓存) 代理层(缓存 35%) QPS 42 128 186 平均延迟 1850 ms 610 ms 280 ms P99 延迟 5200 ms 1400 ms 650 ms 错误率 2.3% 0.4% 0.2% Token 花费/1000 次 300 K 300 K 195 K 吞吐量提升 30% 以上,成本直接省 35%,老板终于点头。
避坑指南:Token、会话、敏感信息
- Token 计算误区:官方只返回 usage.total_tokens,很多人忘了把 Prompt 也累加。代理层可在返回头写 X-Prompt-Tokens、X-Completion-Tokens,方便前端实时显示余额。
- 会话状态保持:ChatGPT 本身无状态,代理层可用 Redis 存 10 轮上下文,key=userID+sessionID,TTL 1 h,避免超长对话爆内存。
- 敏感信息过滤:正则脱敏只能挡 80%,建议再加企业内部 DLP 接口;代理层在 callOpenAI 前做一次、返回后再做一次,防止“回灌”。
延伸思考:动态路由算法还能怎么卷
目前按“失败率+最低成本”静态权重上游,下一步可玩:- 强化学习:把延迟、价格、RPM 当环境,用多臂老虎机选路,30 分钟收敛一次。
- 用户分级:内部用户打标“高优”,代理层优先路由到付费账号池;普通用户走免费池,实现“花自己的钱买自己的时间”。
- 边缘缓存:在 CDN 节点跑 WASM 版代理,把静态 System Prompt 缓存到边缘,首字节时间再降 40%。
如果你把算法跑通,记得回来开源,一起把代理卷成“智能调度器”。
写完这套代理,最大的感受是:把通用能力沉下去,业务层就只剩 Prompt 和创意。
如果你想亲手搭一遍,又懒得从零写熔断、缓存、监控,可以试试这个动手实验——从0打造个人豆包实时通话AI。
我跟着做了一次,官方把 ASR→LLM→TTS 整条链路都封装好了,直接改几行配置就能把自己的代理地址填进去,十分钟就能看到 QPS 曲线飙红,小白也能顺利体验。