AI 助手首字延迟优化
AI 助手场景下首字延迟(TTFT)的全链路排查与优化,区分模型首 token、HTTP 首字节和用户可见首字,并检查网络、上下文、推理与渲染路径。
#ai
#howto
#growing
[!info] related notes
- 相关概念: AI 对话前端性能优化, AI 对话与流式界面, SSE
- 相关 MOC: 前端工程化 MOC
AI 助手首字延迟优化
通用接口卡顿的 DNS → 连接 → HTTP → 应用 → DB 分层路径见 服务请求卡顿的分层排查。
目标
在不更换底层大模型的前提下,定位和优化 AI 助手每次提问后的首字延迟。先固定测量边界:
- 模型调用 TTFT:应用发出模型请求到收到首个有效输出 token;可能包含网络、排队与 prefill,取决于计时位置。
- HTTP TTFB:请求到响应首字节,可能只是响应头或状态事件,不代表模型已经输出。
- 用户可见首字延迟:用户提交到界面呈现首个回答文本。占位文字、心跳、空 delta 和工具调用事件应单独统计。
前置条件
- 有全链路可观测性(至少能在关键节点打点)
- 能区分网络延迟和服务端处理延迟
步骤
1. 全链路节点拆解
以下是候选检查点,不是可直接相加的 12 个独立耗时。连接可能复用,RAG / 工具可能不执行或并行,外部模型 TTFT 也可能已经包含排队;应沿实际请求关键路径归因,避免重叠计数:
- 前端点击到请求发出(事件处理、状态更新)
- DNS 解析
- TCP 连接 / TLS 握手
- 网关 / 鉴权 / 限流
- 后端参数校验
- 上下文组装(历史消息、system prompt)
- RAG 检索(向量搜索、文档召回)
- Tool Calling 前置判断
- 模型 API 请求排队
- 模型处理(prefill、可能的内部推理、首个输出 token;供应商不可见阶段标为未知)
- SSE flush / 代理层缓冲
- 前端渲染第一个 token
2. 每个节点打点
用同一 request / trace ID 关联前端、网关、应用与模型调用。每一进程内用单调时钟记录阶段开始、结束和耗时;不要直接相减不同主机的 Date.now() 来推断单向网络延迟。
- 浏览器:提交、请求发出、首个回答文本事件、首次可见渲染;网络阶段可参考 DevTools / Resource Timing,注意跨域可见性限制。
- 应用:上下文、检索、模型请求发出、首个有效输出收到、首次事件写入。
- 代理:检查数据是否持续转发;应用
write/ flush 完成不证明浏览器已经收到或绘制。 - 模型服务:只有自托管或供应商提供相应指标时,才进一步区分排队与 prefill。
3. 定位瓶颈
- 控制台 Network 面板查看 TTFB(Time To First Byte)
- 后端日志记录每个阶段耗时
- 如果网络延迟占比大 → 优化 DNS/TLS/CDN
- 如果服务端处理占比大 → 优化上下文组装、RAG 并行化
- 如果模型 TTFT 占比大 → prompt 压缩、减少 context 长度
4. 常见优化手段
| 节点 | 优化方式 |
|---|---|
| DNS/TCP/TLS | 连接复用、HTTP/2、预连接 |
| 网关 | 定位慢中间件与鉴权依赖;鉴权通过后才执行受保护操作 |
| 上下文组装 | 历史消息裁剪、system prompt 压缩 |
| RAG 检索 | 并行独立检索;缓存隔离用户权限与资料版本;减少 top-k 后验证召回质量 |
| 模型排队 | 自托管时调整容量和队列;外部 API 先核实限额与供应商指标 |
| 模型 TTFT | 测量输入长度、缓存命中与模型处理耗时;减少输出上限不保证缩短首 token 时间 |
| SSE flush | 关闭 nginx buffering(proxy_buffering off) |
| 代理层 | 检查代理与压缩缓冲;res.flushHeaders() 只提早发送响应头 |
| 前端渲染 | 减少首段文本的渲染工作;占位反馈改善等待体验,但不计作首字优化 |
5. 关键配置
nginx SSE 代理配置示意(将 chat_backend 换为实际已定义的 upstream):
location /api/chat {
proxy_pass http://chat_backend;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
}
不必为了 SSE 一律禁用 HTTP/1.1 chunked 编码;SSE 事件边界由空行分隔,不由传输 chunk 决定。完整链路还要检查压缩与 CDN 是否缓冲。
Node.js HTTP/1.1 handler 中的响应片段(res 为当前响应):
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
});
res.flushHeaders(); // 立即发送 headers;尚未产生回答文本
// 收到实际模型文本后,才发送一个完整 SSE 事件:
// res.write(`data: ${JSON.stringify({ text: delta })}\n\n`);
验证
- 使用相同测量边界和相近请求负载对比;分别记录 TTFB、模型 TTFT 和用户可见首字延迟
- 确认每个节点的耗时分布
- 监控线上 P50/P95/P99,区分冷连接、缓存命中、输入长度与是否检索;同时观察错误率与回答质量
常见问题
- Q: 为什么本地很快但线上很慢? A: 先比较各阶段 trace;代理缓冲、连接握手、排队、检索或模型服务差异都可能导致,不能只凭环境差异归因。
- Q: RAG 检索很慢怎么办? A: 先定位检索中的等待,再对独立检索源并行化或缓存;缓存需遵守权限与版本边界,减少召回数量需验证质量。
参考
- nginx proxy_buffering:关闭时收到上游数据后同步向客户端传递,仍需检查其余链路。