news 2026/7/13 12:09:12

LLM 推理延迟优化——从 Prompt 裁剪到 Streaming 首 Token 加速

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LLM 推理延迟优化——从 Prompt 裁剪到 Streaming 首 Token 加速

LLM 推理延迟优化——从 Prompt 裁剪到 Streaming 首 Token 加速

一、LLM 推理延迟的构成

大语言模型推理的延迟主要由三个部分构成:

  • Prefill 阶段:模型处理 Prompt 的输入 Token,这一阶段的时间与 Prompt 长度基本呈线性关系。
  • Decode 阶段:逐 Token 生成响应内容,每个 Token 的生成时间相对固定。
  • 网络传输与排队:请求在 API Gateway 和推理服务端的排队等待时间。

对于终端用户而言,延迟的感知分为两个关键时刻:首 Token 延迟(Time to First Token,TTFT)和完整响应延迟(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 List<Message> trim(List<Message> messages, int maxTokens) { int budget = maxTokens > 0 ? maxTokens : DEFAULT_TOKEN_BUDGET; int currentTokens = tokenCounter.count(messages); if (currentTokens <= budget) { return messages; } List<Message> 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步:倒序收集最近的消息(含当前用户消息) // 为什么从后向前收集:最近的消息对当前任务最重要 List<Message> 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 流式调用客户端——通过 SSE(Server-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(List<Message> messages) { long requestStartTime = System.nanoTime(); AtomicLong firstTokenTime = new AtomicLong(0); AtomicBoolean firstTokenReceived = new AtomicBoolean(false); StringBuilder fullResponse = new StringBuilder(); Map<String, Object> requestBody = buildRequestBody(messages); Flux<String> 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 Map<String, Object> buildRequestBody(List<Message> messages) { Map<String, 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 Map<String, Object> toApiFormat(Message msg) { Map<String, 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 延迟的优化策略按优先级排序:

  1. Prompt 裁剪——减少 Prefill 阶段的输入 Token 数量(效果最直接)。
  2. Speculative Sampling——用小模型快速生成候选 Token,大模型验证加速(适用于自建推理服务)。
  3. KV Cache 预热——对于高频 Prompt,预计算并缓存 Key-Value 对。
  4. 模型量化(INT8/INT4)——减小模型体积,加速 GPU 加载。
  5. 减少 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/s(1.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-7B(32 层、4096 隐藏维度),每 1000 Token 的 KV Cache 约占用 1GB 显存。KV Cache 量化(如 FP16 → INT8)可将显存占用减半,代价是约 1% 的精度损失。对于显存已满但 GPU 利用率有余量的场景,KV Cache 量化比模型量化更安全(只影响 Attention 计算,不影响 FFN 计算),是首选优化方向。在生产部署中,建议将 TTFT(Time To First Token)、TPOT(Time Per Output Token)和总生成延迟三项指标分别监控,TTFT 优化重点在 Prompt 裁剪和 Prefill 加速,TPOT 优化重点在 Speculative Sampling 和 KV Cache 量化,总延迟则是两者的叠加效果。同时建议对不同类型的请求(短问答 vs 长文生成)分别设置延迟 SLO,短问答 TTFT < 500ms,长文生成 TTFT < 2s,避免用一个笼统的延迟指标掩盖了不同场景的真实体验差异。

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

Java课程设计实战:从零构建Swing通讯录管理系统(附完整源码)

1. 项目概述与核心功能通讯录管理系统是Java课程设计中非常经典的实战项目&#xff0c;它涵盖了GUI开发、数据库操作、模块化设计等核心知识点。这个项目特别适合刚学完Java基础语法的同学练手&#xff0c;能帮你把零散的知识点串联起来。我当年做第一个Java课设时也选择了类似…

作者头像 李华
网站建设 2026/7/13 12:07:28

STM32F030RC与MCP3551高精度ADC系统设计与优化

1. MCP3551与STM32F030RC硬件搭档解析 MCP3551作为Microchip推出的22位Δ-Σ ADC芯片&#xff0c;在工业测量领域有着广泛应用。其核心优势在于极低的噪声性能&#xff08;典型值2.5μVrms&#xff09;和高达2LSB的积分非线性度。与STM32F030RC这款Cortex-M0内核MCU搭配&#x…

作者头像 李华
网站建设 2026/7/13 12:06:57

Camera Tuning 实战:从实验室到场景的成像优化指南

1. Camera Tuning的核心逻辑&#xff1a;从实验室到真实场景的闭环Camera Tuning的本质是建立客观数据与主观体验的桥梁。实验室里用imatest跑分的参数再漂亮&#xff0c;如果用户拍出的照片发朋友圈不好看&#xff0c;这个调校就是失败的。我见过太多工程师沉迷于DXO评分&…

作者头像 李华
网站建设 2026/7/13 12:05:59

ComfyUI+Krea2人设图生成:从工作流搭建到提示词工程实战

在AI绘画领域&#xff0c;人设图生成一直是创作者们关注的重点。最近在测试Krea2模型时&#xff0c;发现其在人物角色设计方面表现出色&#xff0c;但网上关于具体工作流配置和提示词规则的详细教程相对零散。本文将分享一套完整的ComfyUIKrea2人设图生成方案&#xff0c;包含工…

作者头像 李华
网站建设 2026/7/13 12:05:44

多分类混淆矩阵实战:从TP/TN/FP/FN计算到性能指标解析

1. 三分类混淆矩阵实战入门第一次接触多分类混淆矩阵时&#xff0c;我也被那些TP、FP、TN、FN绕得头晕。直到在图像识别项目中踩了几个坑&#xff0c;才真正搞明白它们的计算逻辑。让我们从一个真实的三分类案例开始&#xff0c;假设我们训练了一个识别猫、狗、兔子的模型&…

作者头像 李华