news 2026/7/15 21:32:42

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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% 的优化空间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/15 21:29:41

八、SALV 列属性进阶:动态调整列宽与标签国际化实战

1. SALV列属性动态调整的核心价值在企业级ABAP开发中&#xff0c;SALV&#xff08;Simple ALV&#xff09;作为轻量级报表工具&#xff0c;其列属性动态调整能力直接影响用户体验。我曾参与过一个全球销售数据分析项目&#xff0c;当德国用户查看"客户编号"列时&…

作者头像 李华
网站建设 2026/7/15 21:28:04

wps内联mathtype后,上方工具栏仍没有mathtype

1.wps内联mathtype后&#xff0c;正常情况下工具栏会有mathtype&#xff0c;如下图所示&#xff1a;2.若是没有mathtype&#xff0c;可根据以下步骤进行解决&#xff1a;找到工具栏的工具&#xff0c;点击工具&#xff0c;再点击加载项启动mathtype相关的模块&#xff0c;如图所…

作者头像 李华
网站建设 2026/7/15 21:24:49

Spark 3.x 动态分区裁剪:为什么它能自动跳过 90% 的数据扫描

Spark 3.x 动态分区裁剪&#xff1a;为什么它能自动跳过 90% 的数据扫描 一、从一个真实场景说起 大家好&#xff0c;我是朱大喜。先问大家一个问题&#xff1a;你们在 Spark 里做两张大表 JOIN 的时候&#xff0c;有没有遇到过一个尴尬的场景——明明事实表按日期分了 365 个分…

作者头像 李华
网站建设 2026/7/15 21:24:30

2026年最新北京市轨道交通图和 北京轨道交通规划图 附图

北京市地铁运营有限公司&#xff08;简称“地铁公司”&#xff09;成立于1970年4月15日&#xff0c;是北京市市属大型国有独资公司&#xff0c;是国内最早成立的城市轨道交通运营企业&#xff0c;开通运营了新中国第一条地铁。地铁公司定位于“保障城市运行安全&#xff0c;提升…

作者头像 李华
网站建设 2026/7/15 21:23:52

LVGL学习(四)- Objects

一、简介前面介绍的东西都比较简单&#xff0c;这篇文章介绍点有深度的东西。二、什么是Objects在LVGL中&#xff0c;用户界面的 基本组成部分 是对象&#xff08;控件&#xff09;&#xff0c;也称为 Widgets。例如&#xff0c;一个 按钮、标签、图像、列表、图表 或者 文本区…

作者头像 李华
网站建设 2026/7/15 21:18:48

2026市面上目前靠谱的扫码点餐小程序服务商哪家专业好用?实测推荐

在餐饮行业竞争日益激烈的当下&#xff0c;扫码点餐小程序成为了众多商家提升服务效率、降低运营成本的利器。然而&#xff0c;市场上扫码点餐小程序服务商众多&#xff0c;让人眼花缭乱&#xff0c;究竟哪家才专业好用呢&#xff1f;今天&#xff0c;咱们就来实测推荐一番。用…

作者头像 李华