
实时对话场景下首字符延迟TTFB / TTFT的 4 个工程优化点连接复用HTTPS 握手 TLS 协商占 ~150ms全程长连接 keep-alive 可省 100msPrompt 预热把固定 system prompt 提前 1-2s 发起 streaming 请求让 KV cache 命中https://www.iqiyi.com/v_w51ya7apa4.htmlhttps://www.iqiyi.com/v_23n07adkhag.htmlhttps://www.iqiyi.com/v_19v9ubona80.htmlToken 预测前置客户端在用户停顿前 200-300ms 投机式发起一次草稿请求流式 UI 渲染拿到第 1 个 chunk 就立即 yield不要等完整 sentence实测从平均 TTFT 800ms → 200msOpenAI gpt-4o-mini国内中转节点。下面是踩坑过程。一、为什么实时场景对 TTFT 极度敏感非流式 batch 调用 LLM用户能容忍 2-3s 的等待loading 动画补偿。但实时音频场景不一样ASR 每 200ms 就吐一个 partial transcript用户说完最后一个词到他期待系统响应的窗口大约 300-500ms超过 600ms 用户会主观感觉卡了超过 1s 会重复说话我们/笔者在做即答侠一款面向求职者的 AI 面试 copilot时遇到这个问题早期版本 ASR 收到 finalize 信号后再调 LLMTTFT 平均 850ms用户反馈反应慢像 Siri。后面拆解发现850ms 里只有 ~200ms 是模型本身的 inference剩下都是工程链路损耗。这篇就把链路里能砍的部分挨个拆一遍。二、链路分解800ms 到底花在哪里接入 OpenTelemetry trace 之后单次请求的耗时分布大致是[Client]----DNS解析----[CDN]----TLS握手----[API网关]----排队----[模型] 30ms 80ms 120ms 50ms 20ms ~200ms ↓ 首 token 返回 ↓ [流式 chunks 一个个回来]合计 500ms 网络 200ms 模型 ~100ms 客户端 buffer 800ms 起。可砍的部分DNS / TLS / 连接建立→ 共 230ms占比 28%API 网关排队→ 50ms占比 6%客户端 buffer→ 100ms占比 12%模型 inference 那 200ms 我们改不了除非换模型但前后接近 400ms 是工程可优化的。三、四个优化点的具体实现3.1 长连接复用HTTP/2 keep-aliveOpenAI Python SDK 默认httpx.Client()每次请求理论上会复用连接但很多人在 FastAPI 里写成app.post(/chat) async def chat(req: ChatReq): client OpenAI() # ❌ 每次新建 return client.chat.completions.create(...)每次新建 client 意味着每次重新 TLS。改成模块级 singletonfrom openai import AsyncOpenAI _client AsyncOpenAI( timeouthttpx.Timeout(30.0, connect2.0), max_retries0, # 流式场景禁用重试重试会双倍延迟 http_clienthttpx.AsyncClient( limitshttpx.Limits(max_keepalive_connections20, max_connections50), http2True, ), )实测第 2 次起 TTFT 减少约 130ms。3.2 Prompt 预热与 KV cache 命中gpt-4o-mini启用 prompt caching 后重复的 system prompt few-shot 示例第一次后会进 cache命中能省约 50ms 的 prefill。要让 cache 命中system prompt 必须前缀稳定。我们把变化部分用户简历、当前轮上下文放最后messages [ {role: system, content: SYSTEM_PROMPT_FIXED}, # 前缀稳定 {role: system, content: FEWSHOT_EXAMPLES}, # 也稳定 {role: user, content: resume_summary}, # 半稳定同一面试 session 不变 {role: user, content: current_question}, # 变化 ]cache TTL 大约 5-10 分钟所以面试中每隔 4 分钟我们会发一个 keep-alive 的最小请求保活 cache。3.3 投机式预发请求Speculative Pre-fetch最反直觉但收益最大的一个。观察用户讲完一段话ASR partial 在最后 400ms 通常已经基本稳定最后只是补标点和确认词。我们不等 ASR finalize而是在 partial 文本满足下面任一条件时先发一份草稿请求句末出现明显结束词对吧、是这样、嗯静音超过 250mspartial 文本长度 30 字且含问号async def speculative_call(partial_text: str): # 提前发起但不立即返回给用户 task asyncio.create_task( _client.chat.completions.create( modelgpt-4o-mini, messagesbuild_messages(partial_text), streamTrue, ) ) return task async def on_asr_final(final_text: str, spec_task): # 比对 final 和 partial 差异 if text_similarity(final_text, spec_task.partial) 0.92: # 直接用预发的结果 async for chunk in await spec_task: yield chunk else: # 差异大丢弃重发 spec_task.cancel() async for chunk in real_call(final_text): yield chunk命中率约 70%命中时 TTFT 等于 0已经在路上了。25% 浪费的请求是成本代价对实时场景值得。3.4 流式 UI单 token 也要 flush很多 SSE / WebSocket 中转层会自带 buffernginx 默认 buffer 8KB意味着前几十个字符根本不会出去。后端async for chunk in stream: delta chunk.choices[0].delta.content or yield fdata: {json.dumps({t: delta})}\n\n网关侧务必关 bufferlocation /stream { proxy_buffering off; proxy_cache off; proxy_set_header X-Accel-Buffering no; chunked_transfer_encoding on; }不关 buffer 的话后端每个 token 都吐了用户依然要等几百毫秒看到第一字。四、踩过的坑HTTP/2 多路复用反而变慢在国内中转节点HTTP/2 单连接所有请求复用遇到一个慢请求会 head-of-line blocking。改回 HTTP/1.1 长连接池后稳定了。SDK retry 默认开流式失败 retry 会让用户等 2 倍时间。流式场景必须max_retries0失败直接报错让前端重连。timeout 不能太小connect2.0是底线给 TLS 留余地总 timeout30.0不要写成5.0长答案会被截断。https://www.iqiyi.com/v_1r26vggnm2g.htmlhttps://www.iqiyi.com/v_18gfltmr3z0.htmlhttps://www.iqiyi.com/v_1mzgwud1tbk.htmlhttps://www.iqiyi.com/v_1smrdakraw8.html投机式请求账单暴涨实测 input tokens 用量 35%。建议给 spec_call 加个开关仅在低延迟模式启用。常见问题Q1: 为什么不直接换更快的小模型A: 试过 gpt-4o-mini → claude-3-haiku → 阿里 qwen-turboTTFT 上 haiku 略快但首字符之后的吐字速度反而慢整体 perceived latency 没改善。瓶颈不在模型规模而在工程链路。Q2: 投机式请求 30% 的浪费成本能接受吗A: 我们算过gpt-4o-mini input 0.15 美元/1M token单次面试 session ~5K input token浪费 30% 即多花 ~0.0002 美元/session相对收益用户体验、续费率划算。其他高单价模型不建议这么做。Q3: prompt caching 在国内 API 中转能用吗A: OpenAI 官方 endpoint 是支持的国内中转看具体服务商是否透传prompt_cache_key字段。Azure OpenAI 默认支持。Q4: 流式响应被中间网关拦截了怎么办A: 检查 nginx / cloudflare / 阿里云 SLB 的 buffer 设置如果是 cloudflare开 Streaming 模式或用 WebSocket 替代 SSE。Q5: 投机式请求会不会导致回答内容跑偏A: 会。这是为什么需要text_similarity 0.92的相似度门槛。低于门槛直接 cancel 重发宁可多花一次请求不能让用户看到错误回答。如果做实时语音/对话类 AI 应用遇到延迟瓶颈欢迎评论区交流。链路 trace 工具我们用的是 OpenTelemetry Honeycomb下次可以单独写一篇 trace 实战。