第2298篇:AI系统的网络优化——减少LLM服务的端到端延迟
2026/4/30大约 5 分钟
第2298篇:AI系统的网络优化——减少LLM服务的端到端延迟
适读人群:对AI系统性能有要求的工程师 | 阅读时长:约14分钟 | 核心价值:系统性地识别AI请求链路上的延迟瓶颈,掌握每个环节的优化方法
我给一个客户做AI客服系统的性能调优时,他们反映用户体验不好:感觉很慢。
我开始测量各个环节的延迟:
- 前端到API网关:25ms
- API网关认证:15ms
- 从Redis加载对话上下文:12ms
- Prompt构建和RAG检索:180ms
- AI API调用(到claude-sonnet):4800ms
- 响应序列化和返回:8ms
总计:约5040ms
其中AI API调用占了95%。我能把这4800ms优化掉一大半吗?
通过流式输出、减少prompt token数、优化RAG流程,最终用户体验的"第一个字符延迟"从5040ms降到了480ms,整体响应质量没有下降。
端到端延迟的构成
AI请求的端到端延迟分几个部分:
LLM调用的延迟细分:
- TTFT(Time To First Token):从发送请求到收到第一个token的时间。对用户感知影响最大。
- TBT(Time Between Tokens):token之间的间隔,影响流式显示的流畅度
- 总延迟:TTFT + 所有token的生成时间
流式输出:最重要的用户体验优化
不要等AI全部生成完再返回,要用流式输出。用户看到第一个字符的时间从"总时间"变成了"TTFT",体验有质的提升:
@RestController
public class StreamingConversationController {
@PostMapping(
value = "/api/chat/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE // SSE流式响应
)
public SseEmitter streamChat(@RequestBody ChatRequest request) {
SseEmitter emitter = new SseEmitter(60000L); // 60秒超时
CompletableFuture.runAsync(() -> {
try {
// 加载上下文(快)
List<ChatMessage> history = contextService.loadHistory(
request.getConversationId()
);
// 构建prompt(快)
String prompt = promptBuilder.build(history, request.getMessage());
// 流式调用Claude
anthropicClient.messages()
.stream(MessageStreamParams.builder()
.model("claude-sonnet-4-5")
.maxTokens(2048)
.messages(/* ... */)
.build())
.on(MessageStreamEvent.class, event -> {
if (event instanceof ContentBlockDeltaEvent delta) {
if (delta.getDelta() instanceof TextDelta textDelta) {
try {
// 每收到一个token立刻发送给前端
emitter.send(SseEmitter.event()
.data(Map.of("token", textDelta.getText()))
);
} catch (IOException e) {
emitter.completeWithError(e);
}
}
}
})
.on(MessageStopEvent.class, stopEvent -> {
try {
emitter.send(SseEmitter.event().data(Map.of("done", true)));
emitter.complete();
} catch (IOException e) {
emitter.completeWithError(e);
}
})
.execute();
} catch (Exception e) {
emitter.completeWithError(e);
}
}, asyncExecutor);
return emitter;
}
}前端配合:
// 前端用EventSource接收流式响应
const eventSource = new EventSource('/api/chat/stream?...');
let fullResponse = '';
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.token) {
fullResponse += data.token;
// 实时更新UI,用户立刻看到文字出现
updateDisplay(fullResponse);
}
if (data.done) {
eventSource.close();
}
};Prompt Token数优化:减少TTFT
TTFT和prompt token数量正相关——prompt越长,模型"理解"prompt的时间越长,第一个输出token越慢。
1. 精简系统提示词
系统提示词要长度适中,不要把所有可能的说明都塞进去:
// 不好:系统提示词过于冗长
String badSystemPrompt = """
你是一个专业的客服助手,你需要用友好、专业的态度回答用户的问题。
你应该保持简洁,避免过于冗长的回答。同时你需要注意以下几点:
1. 如果用户询问退款问题,你需要...(后面还有500字的各种说明)
""";
// 好:简洁的系统提示词,把详细规则用RAG按需注入
String goodSystemPrompt = """
你是公司客服助手。友好、简洁地回答用户问题。
回答前查阅提供的相关文档,优先根据文档内容回答。
""";
// 相关的业务规则通过RAG动态插入,不需要预置到system prompt里2. 对话历史压缩
不要把所有历史消息都放进上下文,而是做智能截断:
@Service
public class ContextCompressor {
public List<ChatMessage> compress(List<ChatMessage> history, int maxTokens) {
// 策略1:最近N轮完整保留,更早的压缩为摘要
if (countTokens(history) <= maxTokens) {
return history;
}
// 最近5轮完整保留(用户记忆里最重要的部分)
int keepRecent = 10; // 5轮 = 10条消息
List<ChatMessage> recent = history.size() > keepRecent ?
history.subList(history.size() - keepRecent, history.size()) :
history;
// 早期历史压缩为一条摘要消息
List<ChatMessage> older = history.size() > keepRecent ?
history.subList(0, history.size() - keepRecent) :
List.of();
if (older.isEmpty()) {
return recent;
}
String summary = generateSummary(older);
ChatMessage summaryMessage = new ChatMessage(
"system",
"【历史对话摘要】" + summary
);
List<ChatMessage> compressed = new ArrayList<>();
compressed.add(summaryMessage);
compressed.addAll(recent);
return compressed;
}
private String generateSummary(List<ChatMessage> messages) {
// 用轻量模型生成摘要(异步,不阻塞当前请求)
String content = messages.stream()
.map(m -> m.getRole() + ": " + m.getContent())
.collect(Collectors.joining("\n"));
return lightweightLlm.complete("用50字摘要以下对话要点:\n" + content);
}
}3. RAG结果去重和精简
RAG检索到的文档片段常有重复或相关性不强的内容:
@Service
public class RagResultOptimizer {
public String optimizeRagContext(String query, List<RagResult> rawResults) {
// 去重:余弦相似度超过0.9的片段视为重复
List<RagResult> deduped = deduplicateResults(rawResults, 0.9);
// 只保留相关性最高的片段
List<RagResult> topResults = deduped.stream()
.filter(r -> r.getScore() > 0.7) // 过滤低相关性结果
.limit(5) // 最多5个片段
.collect(Collectors.toList());
// 把片段内容精简(截断过长的段落)
StringBuilder context = new StringBuilder();
for (RagResult result : topResults) {
String content = result.getContent();
// 每个片段最多400字
if (content.length() > 400) {
content = content.substring(0, 400) + "...";
}
context.append(content).append("\n---\n");
}
return context.toString();
}
}并行化RAG:把串行变并行
很多RAG实现是串行的:先检索,再调用AI。如果能并行做,可以减少延迟:
@Service
public class ParallelRagService {
public String chatWithParallelRag(String question, String conversationId)
throws ExecutionException, InterruptedException {
// 并行执行:RAG检索 + 上下文加载(两者互不依赖,可以并行)
CompletableFuture<String> ragFuture = CompletableFuture.supplyAsync(
() -> retrieveRelevantContext(question)
);
CompletableFuture<List<ChatMessage>> historyFuture = CompletableFuture.supplyAsync(
() -> contextService.loadHistory(conversationId)
);
// 等两个都完成(通常200ms内完成)
CompletableFuture.allOf(ragFuture, historyFuture).get();
String ragContext = ragFuture.get();
List<ChatMessage> history = historyFuture.get();
// 然后才调用AI(最慢的部分)
return llmClient.chat(history, question, ragContext);
}
}选择更快的模型
不是所有问题都需要最强的模型。对于简单查询,用Claude Haiku而不是Claude Sonnet,TTFT从2000ms降到500ms:
@Service
public class AdaptiveModelSelector {
public String chat(String question, ChatContext context) {
// 简单分类问题(规则匹配)
if (isSimpleQuery(question)) {
// 用快速小模型
return haiku.complete(buildSimplePrompt(question, context));
}
// 需要深度分析
if (requiresDeepAnalysis(question)) {
return opus.complete(buildDetailedPrompt(question, context));
}
// 默认用中等模型
return sonnet.complete(buildStandardPrompt(question, context));
}
private boolean isSimpleQuery(String question) {
// 简单问题的特征:短、包含FAQ关键词、无需推理
return question.length() < 50 &&
faqMatcher.matchesFaq(question);
}
}端到端延迟优化的优先级:流式输出 > 减少prompt token > 并行化 > 选更快的模型。流式输出改变的是用户感知延迟,收益最大,应该最先做。
