概述
当业务系统需要同时处理海量用户请求、调用大模型(LLM)完成 AI 推理时,单请求逐一调用的模式会迅速触及 LLM API 的 Rate Limit 和成本上限。请求排队合并(Request Batch Merge)策略是解决这一矛盾的核心方案:它将同类请求在内存中暂存、相似度检测、批量打包,在保证单请求延迟可接受的前提下,显著提升系统吞吐、降低 Token 消耗和调用成本。
一、问题背景:高并发对接 LLM 的三大挑战
1.1 Rate Limit 瓶颈
主流 LLM 提供商(OpenAI GPT-4o、Claude 3.5、Kimi、通义千问等)均对 API 调用施加 QPS 或 RPM 限制。以 GPT-4o 为例,Tier 4 账户的 RPM 上限为 10000 RPM、TPM 上限为 300000 Token/min。当业务 QPS 达到数千甚至上万时,如果每个用户请求都直接转发给 LLM,将不可避免地触发 429 Too Many Requests 错误。
1.2 成本失控
大模型按 Token 计费。假设业务日活 100 万用户,平均每用户每次会话消耗 500 Token,按 OpenAI GPT-4o-mini $0.15/1M Input Token 计算,日均调用成本约 $75。如果不做请求合并,每次调用各自携带系统提示词(System Prompt)——一个 2000 Token 的系统提示词被重复发送 100 万次,等同于每天额外浪费近 20 亿无效 Token。
1.3 尾部延迟剧增
在无排队的情况下,高并发请求竞争 LLM 连接池资源,P99 延迟可能从单请求的 500ms 飙升到数秒甚至超时。排队合并通过批量调用将竞争摊平,使延迟分布更加可预测。
二、请求合并策略原理
2.1 核心思想
请求合并的本质是空间换时间:在内存中维护一个短时等待队列,将时间窗口内到达的多个请求暂存,待满足合并条件后一次性批量发送。合并后的单个 LLM 调用可以同时处理多个子请求,LLM 返回的结果再按请求 ID 分发回各个调用方。
2.2 合并条件
请求能否被合并,取决于两个维度:
- 语义相似度:通过 Embedding 模型将用户 prompt 转为向量,计算余弦相似度。相似度 > 0.85 的请求视为可合并。
- 业务兼容性:合并的请求不能有冲突的上下文(比如不同的 system prompt、不同的模型参数 temperature/top_p)。
2.3 合并窗口
合并窗口(Merge Window)是决定合并效果的核心参数:
- 时间窗口:从第一个请求入队开始计时,窗口期内不断接纳新请求。典型值 50ms~200ms。
- 数量窗口:队列达到指定请求数量即触发发送,典型值 20~100。
- 混合策略:时间窗口和数量窗口任一触发即发送,取更早到达的条件。
窗口越大,合并率越高(更多请求被纳入同一批次),但单个请求的等待延迟也越高。这是一个需要在业务 SLA 和系统吞吐之间做权衡的关键参数。
2.4 强制发送机制
为防止窗口永不触发导致请求"hung",必须设置兜底机制:
- 超时强制发送:窗口超时(即使未满)立即发送当前批次。
- 队列满强制发送:队列积压超过阈值(如 200 请求)时强制发送并告警。
- 熔断发送:LLM 错误率超过阈值时暂停合并,退化为单请求模式。
三、系统架构设计
3.1 整体分层
用户流量→ API网关(限流/鉴权)→请求合并层(核心)→ LLM连接池→ LLM提供商
↓
Sentinel/Hystrix
(熔断/限流/隔离)
3.2 请求合并层职责
请求合并层是整个架构的核心引擎,承担以下职责:
- 请求接入:接收来自网关的 AI 请求,为每个请求生成唯一 ID(correlation ID),立即返回 CompletableFuture 给调用方。
- 相似度检测:调用 Embedding 服务(或使用本地模型如 all-MiniLM-L6-v2)计算向量相似度,将相似请求加入同一批次。
- 队列管理:维护多级优先级队列(高优先级:实时交互;低优先级:离线批处理),按 FIFO 顺序出队。
- 批量调用:攒满窗口或超时后,将批次内的所有 prompt 拼接为一次 LLM 批量调用。
- 响应分发:LLM 返回后,按 correlation ID 拆分结果,异步完成各 CompletableFuture。
- 指标暴露:实时上报合并率、批次大小、队列深度、P99 延迟等核心指标。
3.3 连接池设计
与 LLM 的交互推荐使用 HTTP 连接池(如 Apache HttpClient、OkHttp):
- 连接复用:避免每次请求新建 TLS 连接,三次握手开销在高频场景下不可忽视。
- 最大连接数控制:根据 LLM 方的 Rate Limit 动态调整,防止触发限流。
- 读写超时:建议设置 readTimeout = maxTokens / tokenRate + 5s,writeTimeout = 10s。
3.4 熔断降级
当 LLM 响应超时率 > 30% 或错误率 > 10% 时,熔断器打开:
- 暂停合并,直接透传请求(退化模式)。
- 定时探测:每 10s 放行一个探测请求,若成功则逐步关闭熔断器。
- 降级响应:对用户返回"AI 服务繁忙,请稍后重试"。
四、实战实现
4.1 基于 Guava 实现请求合并
Guava 的 ListeningExecutorService 配合 AsyncFunction 是实现请求合并的最简方案:
//定义带合并能力的执行器
ListeningExecutorService executor = MoreExecutors.listeningDecorator(
Executors.newFixedThreadPool(50));
//批量请求入口
public ListenableFuture<List<ChatResponse>> batchChat(
List<ChatRequest> requests) {
// 1.等待窗口:最多等100ms或凑满20个请求
List<ChatRequest> batch = requestQueue.poll(100, TimeUnit.MILLIS);
// 2.相似度检测:按Embedding聚类
List<List<ChatRequest>> clusters = clusterByEmbedding(batch);
// 3.批量提交
List<ListenableFuture<List<ChatResponse>>> futures = clusters.stream()
.map(cluster -> batchInvoke(cluster, llmClient))
.collect(toList());
// 4.合并结果
return Futures.allAsList(futures);
}
//异步批量调用
private ListenableFuture<List<ChatResponse>> batchInvoke(
List<ChatRequest> cluster, LLMClient client) {
String systemPrompt = cluster.get(0).getSystemPrompt();
String combinedUser = cluster.stream()
.map(r -> "[请求" + r.getId() + "]:" + r.getPrompt())
.collect(Collectors.joining("\n---\n"));
ChatRequest combined = ChatRequest.builder()
.systemPrompt(systemPrompt)
.userPrompt(combinedUser)
.build();
return transform(client.chatAsync(combined), response -> {
//按请求ID拆分响应
return splitResponse(response, cluster);
});
}
4.2 基于 Sentinel 实现流量控制
Sentinel 不仅能实现熔断,还能提供多维度的流量控制:
//定义LLM调用的熔断规则
DegradeRule rule = new DegradeRule("llm-batch")
.setGrade(CircuitBreakerStrategy.SLOW_RATIO_RULE)
.setCount(0.5) // 50%慢调用(>2s)比例触发熔断
.setSlowRatioThreshold(0.8) // 80%请求超过阈值视为慢调用
.setMinRequestAmount(10) //至少10个请求才计算熔断
.setStatIntervalMs(30_000); // 30s窗口统计
DegradeRuleManager.loadRules(Collections.singletonList(rule));
//使用Sentinel API保护批量调用
try (Entry entry = SphU.entry("llm-batch", EntryType.IN, 1, batchArgs)) {
//批量调用,带流量整形
List<ChatResponse> responses = llmClient.batchChat(batch);
//正常处理
dispatch(responses);
} catch (BlockException e) {
//限流/熔断触发:降级处理
fallbackToCacheOrReject(batch);
} catch (Throwable t) {
// LLM调用异常:熔断器会计入
Tracer.traceEntry(t, entry);
}
4.3 Embedding 相似度检测实现
相似度检测是合并策略的核心环节,推荐使用轻量级 Embedding 模型:
//使用本地模型计算Embedding(推荐all-MiniLM-L6-v2)
public List<List<ChatRequest>> clusterByEmbedding(
List<ChatRequest> requests) {
//批量获取向量
float[][] embeddings = embeddingModel.encode(
requests.stream().map(ChatRequest::getPrompt).toList()
);
//聚类:简单贪心+相似度阈值
List<List<ChatRequest>> clusters = new ArrayList<>();
boolean[] used = new boolean[requests.size()];
for (int i = 0; i < requests.size(); i++) {
if (used[i]) continue;
List<ChatRequest> cluster = new ArrayList<>();
cluster.add(requests.get(i));
used[i] = true;
for (int j = i + 1; j < requests.size(); j++) {
if (used[j]) continue;
double sim = cosineSimilarity(embeddings[i], embeddings[j]);
if (sim > SIMILARITY_THRESHOLD) {
cluster.add(requests.get(j));
used[j] = true;
}
}
clusters.add(cluster);
}
return clusters;
}
private double cosineSimilarity(float[] a, float[] b) {
double dot = 0, normA = 0, normB = 0;
for (int i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
五、生产环境关键考量
5.1 缓存命中
对于完全相同的 prompt(Cache Hit),可以直接返回缓存结果,无需调用 LLM。OpenAI 的 cache-control 指令和 Anthropic 的缓存机制均支持毫秒级返回,成本为零。建议在请求合并前先做精确哈希匹配,将缓存命中率作为重要指标监控。
5.2 模型参数差异
合并的请求必须使用相同的模型参数(model、temperature、top_p、max_tokens)。如果业务允许,建议将 temperature 默认设为 0(确定性输出),不仅更容易合并,还能提升 Token 利用率。
5.3 系统提示词处理
各请求的 system prompt 可能不同。推荐策略:
- 统一系统提示词:多个请求共用相同的 system prompt 时直接合并。
- 差异标记:system prompt 不同时,在 combined prompt 中用 XML/JSON 标签区分,由 LLM 自行解析归类——这实际上是把"合并粒度"从请求级降到 prompt 级。
5.4 监控指标
上线后必须监控以下核心指标:
5.5 冷启动问题
合并层重启后,连接池需要预热。建议:
- 启动时先发送 10~20 个探测请求暖热连接池。
- 使用 Keep-Alive 保持长连接,避免超时断连。
- 监控连接池活跃连接数,预判容量。
六、总结
请求排队合并是 Java 后端在高并发场景下对接大模型的核心架构模式。它通过在网关与 LLM 之间插入合并层,利用时间窗口和相似度检测将多个请求合并为一次批量调用,在 QPS、Token 成本、尾部延迟三个维度同时获得数量级的改善。
Guava 提供了简洁的异步合并能力,Sentinel 提供了生产级的熔断限流保障,二者结合再加上 Embedding 相似度检测,能够构建一套完整可靠的请求合并系统。实际落地时,需要根据业务 SLA 选择合适的窗口参数,并通过完善的监控指标持续调优。
---
*本文为 Java 程序员大模型系列第 39 阶段内容,专注 Java 后端工程实践。*
图1:请求排队合并策略原理图
图2:高并发流量控制架构图
图3:批量请求合并流程图
图4:实战:Guava + Sentinel请求合并实现