LLM 推理延迟优化——从 Prompt 裁剪到 Streaming 首 Token 加速 LLM 推理延迟优化——从 Prompt 裁剪到 Streaming 首 Token 加速一、LLM 推理延迟的构成大语言模型推理的延迟主要由三个部分构成Prefill 阶段模型处理 Prompt 的输入 Token这一阶段的时间与 Prompt 长度基本呈线性关系。Decode 阶段逐 Token 生成响应内容每个 Token 的生成时间相对固定。网络传输与排队请求在 API Gateway 和推理服务端的排队等待时间。对于终端用户而言延迟的感知分为两个关键时刻首 Token 延迟Time to First TokenTTFT和完整响应延迟End to End Latency。首 Token 延迟决定了系统开始响应的感知速度对用户体验影响最大。二、Prompt 裁剪——减少 Prefill 阶段的开销Prompt 裁剪是降低首 Token 延迟的最有效手段之一。在实际的 Agent 系统中Prompt 往往会因为上下文累积而不断膨胀剔除冗余信息可以显著减少 Prefill 阶段的计算量。/** * Prompt 裁剪器——根据预设的 Token 预算对 Prompt 进行智能裁剪。 * * 为什么需要智能裁剪而非简单截断 * 简单截断可能丢失最关键的上下文信息如最近的对话轮次 * 智能裁剪需要根据信息的时效性和相关性进行优先级排序。 */ Component public class PromptTrimmer { private final TokenCounter tokenCounter; // 为什么预算设为 6000 而非 8192 // 预留约 2000 Token 给响应输出确保在达到 maxTokens 前 Prompt 已经处理完毕 private static final int DEFAULT_TOKEN_BUDGET 6000; public PromptTrimmer(TokenCounter tokenCounter) { this.tokenCounter tokenCounter; } /** * 按优先级裁剪 Prompt 消息列表。 * * 裁剪策略 * 1. System Prompt——永远保留角色定义不可丢失 * 2. 最近的用户消息——永远保留当前任务不可丢失 * 3. 工具调用结果——按 token 预算从新到旧保留 * 4. 历史对话——从中间开始移除保留最早和最近的消息 */ public ListMessage trim(ListMessage messages, int maxTokens) { int budget maxTokens 0 ? maxTokens : DEFAULT_TOKEN_BUDGET; int currentTokens tokenCounter.count(messages); if (currentTokens budget) { return messages; } ListMessage result new ArrayList(); // 第1步保留 System Message int systemTokens 0; for (Message msg : messages) { if (msg.getRole() MessageRole.SYSTEM) { result.add(msg); systemTokens tokenCounter.count(msg); break; } } // 第2步倒序收集最近的消息含当前用户消息 // 为什么从后向前收集最近的消息对当前任务最重要 ListMessage reversed new ArrayList(); int consumedTokens systemTokens; boolean userMessageFound false; for (int i messages.size() - 1; i 0; i--) { Message msg messages.get(i); if (msg.getRole() MessageRole.SYSTEM) { continue; // 已处理 } int msgTokens tokenCounter.count(msg); // 最近一条 User Message 无条件保留 if (!userMessageFound msg.getRole() MessageRole.USER) { reversed.add(msg); consumedTokens msgTokens; userMessageFound true; continue; } if (consumedTokens msgTokens budget) { break; // 预算耗尽 } reversed.add(msg); consumedTokens msgTokens; } // 第3步翻转回正序 Collections.reverse(reversed); result.addAll(reversed); log.info(Prompt裁剪完成, originalTokens{}, trimmedTokens{}, budget{}, currentTokens, consumedTokens, budget); return result; } }三、Streaming 首 Token 加速Streaming流式输出是降低用户感知延迟的关键手段。但仅开启 Streaming 是不够的还需要优化首 Token 到达时间。/** * LLM 流式调用客户端——通过 SSEServer-Sent Events接收流式输出 * 并在首 Token 到达后立即返回给下游减少用户等待感知。 * * 为什么使用 WebClient 而非 RestTemplate 做流式调用 * RestTemplate 是阻塞式 API会等待完整响应再返回 * WebClient 的 Flux 支持异步流式处理能够在首 Token 到达时立即消费。 */ Service public class StreamingLlmClient { private static final Logger log LoggerFactory.getLogger( StreamingLlmClient.class); private final WebClient webClient; private final MeterRegistry meterRegistry; public StreamingLlmClient( Value(${llm.api.endpoint}) String apiEndpoint, Value(${llm.api.key}) String apiKey, MeterRegistry meterRegistry) { this.webClient WebClient.builder() .baseUrl(apiEndpoint) .defaultHeader(Authorization, Bearer apiKey) .defaultHeader(Content-Type, application/json) .build(); this.meterRegistry meterRegistry; } /** * 发起流式推理请求返回首 Token 到流和完整响应的 Mono。 * * param messages 对话消息列表 * return StreamingResult 包含首 Token 到达时间和完整响应 */ public StreamingResult streamInference(ListMessage messages) { long requestStartTime System.nanoTime(); AtomicLong firstTokenTime new AtomicLong(0); AtomicBoolean firstTokenReceived new AtomicBoolean(false); StringBuilder fullResponse new StringBuilder(); MapString, Object requestBody buildRequestBody(messages); FluxString tokenStream webClient.post() .uri(/v1/chat/completions) .bodyValue(requestBody) .retrieve() .bodyToFlux(String.class) .doOnNext(chunk - { if (!firstTokenReceived.getAndSet(true)) { firstTokenTime.set(System.nanoTime()); long ttftMs TimeUnit.NANOSECONDS.toMillis( firstTokenTime.get() - requestStartTime); meterRegistry.timer(llm.ttft).record( Duration.ofMillis(ttftMs)); log.debug(首Token到达, ttftMs{}, ttftMs); } fullResponse.append(extractContent(chunk)); }) .doOnComplete(() - { long totalMs TimeUnit.NANOSECONDS.toMillis( System.nanoTime() - requestStartTime); meterRegistry.timer(llm.total.latency).record( Duration.ofMillis(totalMs)); log.info(流式推理完成, ttftMs{}, totalMs{}, tokens{}, TimeUnit.NANOSECONDS.toMillis( firstTokenTime.get() - requestStartTime), totalMs, fullResponse.length()); }) .doOnError(error - { log.error(流式推理异常, 原因{}, error.getMessage()); meterRegistry.counter(llm.stream.error).increment(); }); return new StreamingResult(tokenStream, fullResponse, firstTokenTime, requestStartTime); } private MapString, Object buildRequestBody(ListMessage messages) { MapString, Object body new LinkedHashMap(); body.put(model, qwen-7b-chat); body.put(messages, messages.stream() .map(this::toApiFormat) .collect(Collectors.toList())); body.put(stream, true); // 为什么 temperature 在流式场景下设为 0.1 而非 0.7 // 较低的 temperature 使输出更确定减少不必要的 Token 生成 // 同时也降低了闲聊式的输出概率 body.put(temperature, 0.1); body.put(max_tokens, 2048); return body; } private String extractContent(String chunk) { // 解析 SSE 格式的 chunk data if (chunk null || !chunk.startsWith(data: )) { return ; } String data chunk.substring(6).trim(); if ([DONE].equals(data)) { return ; } try { // 简化的 JSON 解析实际项目中应使用 ObjectMapper int contentStart data.indexOf(\content\:\); if (contentStart -1) { return ; } int contentEnd data.indexOf(\, contentStart 11); if (contentEnd -1) { return ; } return data.substring(contentStart 11, contentEnd); } catch (Exception e) { return ; } } private MapString, Object toApiFormat(Message msg) { MapString, Object map new LinkedHashMap(); map.put(role, msg.getRole().name().toLowerCase()); map.put(content, msg.getContent()); return map; } }四、首 Token 延迟的关键影响因素graph LR A[请求发送] -- B{Prompt Token数} B --|多| B1[Prefill时间长] B --|少| B2[Prefill时间短] B1 -- C{网络传输} B2 -- C C -- D{GPU调度排队} D --|排队| D1[等待其他请求] D --|无排队| E[Prefill计算] D1 -- E E -- F[首Token输出] style B fill:#f96,stroke:#333 style D fill:#ff9,stroke:#333 style F fill:#6f6,stroke:#333首 Token 延迟的优化策略按优先级排序Prompt 裁剪——减少 Prefill 阶段的输入 Token 数量效果最直接。Speculative Sampling——用小模型快速生成候选 Token大模型验证加速适用于自建推理服务。KV Cache 预热——对于高频 Prompt预计算并缓存 Key-Value 对。模型量化INT8/INT4——减小模型体积加速 GPU 加载。减少 API 调用链路——将 LLM 部署在低延迟网络区域减少 RTT。五、总结LLM 推理延迟优化是一个系统工程需要从 Prompt 设计、模型部署、网络链路和工程实现四个维度协同推进。Prompt 裁剪是成本最低、见效最快的优化手段应当优先实施。Streaming 输出和首 Token 加速则直接影响用户感知在交互式产品中不可或缺。在建立基础的延迟监控体系后可以进一步探索 Speculative Sampling 等更深入的优化方案。需要注意的是延迟优化往往与质量和成本之间存在权衡。例如过度的 Prompt 裁剪可能导致模型丢失上下文而产生错误回答因此需要在生产环境中建立延迟与质量的联合监控。Speculative Sampling 的工程实现Speculative Sampling推测采样是降低 Decode 阶段延迟的有效技术——用小模型Draft Model快速生成多个候选 Token再用大模型Target Model并行验证对于验证通过的 Token 一次性采纳。在我们的内部测试中使用 Qwen-1.8B 作为 Draft 模型配合 Qwen-7B 作为 Target 模型在 Code Generation 场景下 Decode 速度从 25 tokens/s 提升到 48 tokens/s1.9x 加速。工程实现的重点在(1) Draft 模型和 Target 模型需要运行在同一 GPU 上显存的额外消耗是 Draft 模型的权重约 3.6GB(2) Draft 模型每步生成 3~5 个候选 Token验证通过率约 60%~80%取决于任务类型代码类最高、创意写作类最低(3) 当验证拒绝率超过 40% 时Draft 模型的 Token 生成反而成为额外开销需要动态关闭 Speculative Sampling。实现方式是监控accepted_tokens / total_draft_tokens的比率低于 0.6 时自动回退到常规 Decode。KV Cache 的核心意义与量化压缩Prefill 阶段最耗时的部分不是 Token 计算本身而是将计算结果写入 KV Cache。KV Cache 的大小 2 × 层数 × 隐藏维度 × Token数 × 精度位数。对于 Qwen-7B32 层、4096 隐藏维度每 1000 Token 的 KV Cache 约占用 1GB 显存。KV Cache 量化如 FP16 → INT8可将显存占用减半代价是约 1% 的精度损失。对于显存已满但 GPU 利用率有余量的场景KV Cache 量化比模型量化更安全只影响 Attention 计算不影响 FFN 计算是首选优化方向。在生产部署中建议将 TTFTTime To First Token、TPOTTime Per Output Token和总生成延迟三项指标分别监控TTFT 优化重点在 Prompt 裁剪和 Prefill 加速TPOT 优化重点在 Speculative Sampling 和 KV Cache 量化总延迟则是两者的叠加效果。同时建议对不同类型的请求短问答 vs 长文生成分别设置延迟 SLO短问答 TTFT 500ms长文生成 TTFT 2s避免用一个笼统的延迟指标掩盖了不同场景的真实体验差异。