当前位置: 首页 > news >正文

LLM 服务网关的性能优化:连接池、批量推理与请求合并的工程方案

LLM 服务网关的性能优化:连接池、批量推理与请求合并的工程方案

一、$2300 一个月的 API 账单是怎么来的

一个面向开发者的 AI 编程助手,月均调用 GPT-4 API 约 120 万次。最初的实现是每个用户的每次请求直接新建一个 HTTP 连接调用 OpenAI API。这导致了两个问题:

  1. 成本爆炸:每个请求独立发送,大量短小的 prompt(补全 50 个 token 也需要携带完整的 system prompt)造成了严重的 Token 浪费
  2. 延迟飙升:TLS 握手的频繁重复建立(每次 200-300ms),高峰期短连接耗尽端口,连接等待时间超过推理时间

排查发现:120 万次请求中,38% 的请求体 Token 消耗在重复的 system prompt 上,15% 的端到端延迟花在连接建立上。解决方向:引入 LLM 服务网关,集中管理连接、请求和缓存。

二、LLM 服务网关的三层优化

flowchart TD A[客户端请求] --> B[网关接入层] B --> C{请求分类} C -->|高频短请求| D[请求合并队列] C -->|长推理请求| E[直通连接池] C -->|缓存命中| F[直接返回] D --> G[批量推理: 合并多个短请求] G --> H[LLM Provider] E --> I[连接池: 复用 TLS 连接] I --> H F --> J[Token 缓存] H --> K[结果拆分与分发] K --> L[返回各客户端]

2.1 连接池:消除 TLS 握手开销

// LLM 连接池:复用 HTTP 连接,减少 TLS 握手 type LLMConnectionPool struct { client *http.Client semaphore chan struct{} // 并发控制信号量 maxConns int } func NewLLMConnectionPool(maxConns int) *LLMConnectionPool { return &LLMConnectionPool{ client: &http.Client{ Transport: &http.Transport{ MaxIdleConns: maxConns, MaxIdleConnsPerHost: maxConns, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, DisableKeepAlives: false, // 启用 Keep-Alive }, Timeout: 60 * time.Second, }, semaphore: make(chan struct{}, maxConns), maxConns: maxConns, } } func (p *LLMConnectionPool) Do(ctx context.Context, req *http.Request) (*http.Response, error) { // 并发控制:超过最大连接数时阻塞等待 select { case p.semaphore <- struct{}{}: defer func() { <-p.semaphore }() case <-ctx.Done(): return nil, ctx.Err() } resp, err := p.client.Do(req.WithContext(ctx)) if err != nil { return nil, fmt.Errorf("LLM API 调用失败: %w", err) } if resp.StatusCode == 429 { // 限流时读取 Retry-After 头并等待 retryAfter := resp.Header.Get("Retry-After") waitTime, _ := strconv.Atoi(retryAfter) if waitTime == 0 { waitTime = 5 // 默认等待 5 秒 } resp.Body.Close() select { case <-time.After(time.Duration(waitTime) * time.Second): return p.Do(ctx, req) // 重试 case <-ctx.Done(): return nil, ctx.Err() } } return resp, nil }

启用连接池后,TLS 握手只在连接建立时发生一次。实测 1000 QPS 场景下,P99 延迟从 850ms 降到 420ms,其中 430ms 的收益来自消除重复的 TLS 握手。

2.2 请求合并:批量推理降低 Token 浪费

// 请求合并器:将短请求批量发送给 LLM,减少 system prompt 重复 type RequestBatcher struct { batchSize int maxWaitTime time.Duration queue chan MergedRequest llmClient LLMClient } type MergedRequest struct { Prompts []string Results chan []string // 每个请求一个结果 ErrCh chan error } func (b *RequestBatcher) Submit(ctx context.Context, prompt string) (string, error) { // 如果请求很长(> 500 tokens),直接发送,不合入批量 if estimateTokens(prompt) > 500 { return b.llmClient.Complete(ctx, prompt) } resultCh := make(chan []string, 1) errCh := make(chan error, 1) select { case b.queue <- MergedRequest{Prompts: []string{prompt}, Results: resultCh, ErrCh: errCh}: case <-ctx.Done(): return "", ctx.Err() } select { case results := <-resultCh: return results[0], nil case err := <-errCh: return "", err case <-ctx.Done(): return "", ctx.Err() } } func (b *RequestBatcher) Run(ctx context.Context) { timer := time.NewTimer(b.maxWaitTime) defer timer.Stop() batch := make([]MergedRequest, 0, b.batchSize) for { timer.Reset(b.maxWaitTime) select { case req := <-b.queue: batch = append(batch, req) if len(batch) >= b.batchSize { b.processBatch(ctx, batch) batch = batch[:0] } case <-timer.C: if len(batch) > 0 { b.processBatch(ctx, batch) batch = batch[:0] } case <-ctx.Done(): if len(batch) > 0 { b.processBatch(context.Background(), batch) } return } } }

批量合并的核心收益不是"减少 API 调用次数"(API 调用次数相同),而是"一个请求携带多个子任务,system prompt 只计算一次 Token 费用"。在代码补全场景中,10 个用户的补全请求合并为一个 API 调用,system prompt 的 Token 费用降低到原来的 1/10。

2.3 响应拆分与分发

批量请求的响应需要正确拆分回各个原始请求:

func (b *RequestBatcher) processBatch(ctx context.Context, batch []MergedRequest) { allPrompts := make([]string, 0) separator := "\n---SEPARATOR---\n" for _, req := range batch { allPrompts = append(allPrompts, req.Prompts...) } combinedPrompt := strings.Join(allPrompts, separator) resp, err := b.llmClient.Complete(ctx, combinedPrompt) if err != nil { for _, req := range batch { req.ErrCh <- err } return } // 按分隔符拆分响应 parts := strings.Split(resp, separator) idx := 0 for _, req := range batch { if idx >= len(parts) { req.ErrCh <- fmt.Errorf("批量响应的分隔数量不匹配") continue } count := len(req.Prompts) req.Results <- parts[idx : idx+count] idx += count } }

三、Token 缓存的语义哈希

// 语义级 Token 缓存:相同意图的请求复用之前的结果 type SemanticCache struct { store *redis.Client embedder *EmbeddingClient threshold float64 // 相似度阈值 0.0-1.0 } func (c *SemanticCache) GetOrCompute( ctx context.Context, prompt string, compute func(context.Context) (string, error), ) (string, error) { // 1. 精确匹配:Prompot 完全相同的直接返回 exactKey := fmt.Sprintf("llm_cache:exact:%x", sha256.Sum256([]byte(prompt))) if val, err := c.store.Get(ctx, exactKey).Result(); err == nil { return val, nil } // 2. 语义匹配:通过 Embedding 向量查找相似 Prompt 的结果 embedding, err := c.embedder.Embed(ctx, prompt) if err == nil { similar, err := c.findSimilar(ctx, embedding) if err == nil && similar.Score > c.threshold { return similar.Result, nil } } // 3. 计算新结果 result, err := compute(ctx) if err != nil { return "", err } // 4. 写入缓存 pipe := c.store.Pipeline() pipe.Set(ctx, exactKey, result, 1*time.Hour) if err == nil { pipe.Set(ctx, fmt.Sprintf("llm_cache:vec:%x", sha256.Sum256(embedding)), result, 1*time.Hour, ) } pipe.Exec(ctx) return result, nil }

四、边界与权衡

请求合并的延迟代价:短请求等待批量凑齐的最长延迟是maxWaitTime(如 200ms)。对于用户等待的交互式场景,这个延迟需要控制。可以分层:实时请求 maxWait=50ms,准实时请求 maxWait=500ms,后台任务 maxWait=2000ms。

响应拆分的准确度:使用分隔符拆分合并的响应,如果 LLM 在回复中包含了分隔符文本,拆分会出错。更可靠的方案是要求 LLM 返回 JSON 格式的多个回复,而非用文本分隔符。

连接池的超时设置IdleConnTimeout太长会保持过多空闲连接,太短会频繁重建。建议设置为 90 秒(与大多数云负载均衡器的超时匹配)。监控指标是tcp_out_of_memoryTIME_WAIT连接数。

五、总结

LLM 服务网关的核心优化来自三个层面:连接复用(消除 TLS 握手开销)、请求合并(减少 system prompt 的 Token 重复计费)、语义缓存(跳过可复用的推理)。三个优化独立叠加,总体可将端到端延迟降低 50-60%,API 成本降低 30-40%。

实施路径:先搭建连接池(最小改动、最大延迟收益)→ 再实现精确匹配的 Token 缓存(对 FAQ 类场景效果显著)→ 最后做请求合并(需要评估短请求的占比和可容忍的等待延迟)。不要一开始就建一个"万能网关"——连接池 + 精确缓存已经能覆盖 80% 的优化空间。

http://www.cnnetsun.cn/news/3409519.html

相关文章:

  • 八、SALV 列属性进阶:动态调整列宽与标签国际化实战
  • wps内联mathtype后,上方工具栏仍没有mathtype
  • Spark 3.x 动态分区裁剪:为什么它能自动跳过 90% 的数据扫描
  • 2026年最新北京市轨道交通图和 北京轨道交通规划图 附图
  • LVGL学习(四)- Objects
  • 2026市面上目前靠谱的扫码点餐小程序服务商哪家专业好用?实测推荐
  • JDK 12 新特性详解
  • Redis典型应用 - 分布式锁,引入过期时间,Lua脚本原子解锁操作,看门狗机制实现过期时间动态续约,Redlock算法
  • 新材料制造工业数据中台解决方案
  • 构建高性能存储网络:NVMe-oF、SPDK与RDMA的融合实践
  • 2026免费数字人平台怎么选:基于功能、成本、体验的实测选型框架,新手、老板IP和商用团队分别看什么
  • HTTP状态码404:从“死链接”到“软404”的SEO陷阱与实战修复
  • 小白程序员转行AI必看:Agent开发vs大模型开发,谁更适合你?
  • Gemini 3.5 到底适合做什么?2026最新大模型实用边界与选型攻略
  • 《VLOOKUP/XLOOKUP 匹配不到受局限!Python写一个 多条件模糊匹配,更强的提升excel处理效率》
  • LTC4417IUF#TRPBF是一款36V 工业级防反灌电源控制器
  • springboot外卖点餐系统00198-计算机课程设计/毕业设计
  • 【Springboot毕设全套源码+文档】基于springboot图书管理系统的设计与实现(丰富项目+远程调试+讲解+定制)
  • 功率电路死区时间测量与计算:从理论到工程实践
  • 从示波器带宽反推信号上升沿:0.35系数的实战应用与选型指南
  • R Shiny生产级仪表盘实战:架构设计、缓存优化与权限部署
  • 《Java 100 天进阶之路》第60.4篇:多线程代码示例集(2026版)
  • 2026年云计算运维实战:Docker与Kubernetes全链路技能指南
  • 命名空间:面试官问“using namespace std到底好不好”,我差点说错
  • 别再用默认设置跑 Claude Code 了:12 个需要立刻开启的配置
  • Audacity音频编辑神器:5个步骤从零开始创作专业音频作品
  • 模板驱动型文档自动化:从内容到专业PDF的确定性工作流
  • 8051存储空间深度解析:从哈佛结构到SFR位寻址的实战指南
  • UI与代码平衡:提升开发效率与产品质量的关键策略
  • 多维聚合数据变形术:Slice/Dice/Rollup/Drill-down实战