AI 实时推理流式预热实战:首字符延迟从 800ms 砍到 200ms
实时对话场景下首字符延迟(TTFB / TTFT)的 4 个工程优化点: 实测从平均 TTFT 800ms → 200ms(OpenAI gpt-4o-mini,国内中转节点)。下面是踩坑过程。 非流式 batch 调用 LLM,用户能容忍 2-3s 的等待(loading 动画补偿)。但实时音频场景不一样: 我们/笔者在做即答侠(一款面向求职者的 AI 面试 copilot)时遇到这个问题:早期版本 ASR 收到 finalize 信号后再调 LLM,TTFT 平均 850ms,用户反馈"反应慢,像 Siri"。后面拆解发现,850ms 里只有 ~200ms 是模型本身的 inference,剩下都是工程链路损耗。这篇就把链路里能砍的部分挨个拆一遍。 接入 OpenTelemetry trace 之后,单次请求的耗时分布大致是: 合计 500ms 网络 + 200ms 模型 + ~100ms 客户端 buffer = 800ms 起。 可砍的部分: 模型 inference 那 200ms 我们改不了(除非换模型),但前后接近 400ms 是工程可优化的。 OpenAI Python SDK 默认 每次新建 client 意味着每次重新 TLS。改成模块级 singleton: 实测:第 2 次起 TTFT 减少约 130ms。 要让 cache 命中,system prompt 必须前缀稳定。我们把变化部分(用户简历、当前轮上下文)放最后: cache TTL 大约 5-10 分钟,所以面试中每隔 4 分钟我们会发一个 keep-alive 的最小请求保活 cache。 最反直觉但收益最大的一个。 观察:用户讲完一段话,ASR partial 在最后 400ms 通常已经基本稳定(最后只是补标点和确认词)。我们不等 ASR finalize,而是在 partial 文本满足下面任一条件时先发一份"草稿"请求: 命中率约 70%,命中时 TTFT 等于 0(已经在路上了)。25% 浪费的请求是成本代价,对实时场景值得。 很多 SSE / WebSocket 中转层会自带 buffer(nginx 默认 buffer 8KB,意味着前几十个字符根本不会出去)。 后端: 网关侧务必关 buffer: 不关 buffer 的话,后端每个 token 都吐了,用户依然要等几百毫秒看到第一字。 Q1: 为什么不直接换更快的小模型? Q2: 投机式请求 30% 的浪费成本能接受吗? Q3: prompt caching 在国内 API 中转能用吗? Q4: 流式响应被中间网关拦截了怎么办? Q5: 投机式请求会不会导致回答内容跑偏? 如果做实时语音/对话类 AI 应用遇到延迟瓶颈,欢迎评论区交流。链路 trace 工具我们用的是 OpenTelemetry + Honeycomb,下次可以单独写一篇 trace 实战。TL;DR(先看结论)
一、为什么实时场景对 TTFT 极度敏感
二、链路分解:800ms 到底花在哪里
[Client]----DNS解析----[CDN]----TLS握手----[API网关]----排队----[模型]
30ms 80ms 120ms 50ms 20ms ~200ms
↓
首 token 返回
↓
[流式 chunks 一个个回来]三、四个优化点的具体实现
3.1 长连接复用(HTTP/2 + keep-alive)
httpx.Client(),每次请求理论上会复用连接,但很多人在 FastAPI 里写成:@app.post("/chat")
async def chat(req: ChatReq):
client = OpenAI() # ❌ 每次新建
return client.chat.completions.create(...)from openai import AsyncOpenAI
_client = AsyncOpenAI(
timeout=httpx.Timeout(30.0, connect=2.0),
max_retries=0, # 流式场景禁用重试,重试会双倍延迟
http_client=httpx.AsyncClient(
limits=httpx.Limits(max_keepalive_connections=20, max_connections=50),
http2=True,
),
)3.2 Prompt 预热与 KV cache 命中
gpt-4o-mini 启用 prompt caching 后,重复的 system prompt + few-shot 示例第一次后会进 cache,命中能省约 50ms 的 prefill。messages = [
{"role": "system", "content": SYSTEM_PROMPT_FIXED}, # 前缀稳定
{"role": "system", "content": FEWSHOT_EXAMPLES}, # 也稳定
{"role": "user", "content": resume_summary}, # 半稳定(同一面试 session 不变)
{"role": "user", "content": current_question}, # 变化
]3.3 投机式预发请求(Speculative Pre-fetch)
async def speculative_call(partial_text: str):
# 提前发起,但不立即返回给用户
task = asyncio.create_task(
_client.chat.completions.create(
model="gpt-4o-mini",
messages=build_messages(partial_text),
stream=True,
)
)
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 chunk3.4 流式 UI:单 token 也要 flush
async for chunk in stream:
delta = chunk.choices[0].delta.content or ""
yield f"data: {json.dumps({'t': delta})}\n\n"location /stream {
proxy_buffering off;
proxy_cache off;
proxy_set_header X-Accel-Buffering no;
chunked_transfer_encoding on;
}四、踩过的坑
max_retries=0,失败直接报错让前端重连。connect=2.0 是底线,给 TLS 留余地;总 timeout 30.0 不要写成 5.0,长答案会被截断。常见问题
A: 试过 gpt-4o-mini → claude-3-haiku → 阿里 qwen-turbo,TTFT 上 haiku 略快但首字符之后的吐字速度反而慢,整体 perceived latency 没改善。瓶颈不在模型规模而在工程链路。
A: 我们算过:gpt-4o-mini input 0.15 美元/1M token,单次面试 session ~5K input token,浪费 30% 即多花 ~0.0002 美元/session,相对收益(用户体验、续费率)划算。其他高单价模型不建议这么做。
A: OpenAI 官方 endpoint 是支持的,国内中转看具体服务商是否透传 prompt_cache_key 字段。Azure OpenAI 默认支持。
A: 检查 nginx / cloudflare / 阿里云 SLB 的 buffer 设置;如果是 cloudflare,开 "Streaming" 模式或用 WebSocket 替代 SSE。
A: 会。这是为什么需要 text_similarity > 0.92 的相似度门槛。低于门槛直接 cancel 重发,宁可多花一次请求,不能让用户看到错误回答。