← [ BACK_TO_NOTES ]

AI 助手首字延迟优化

AI 助手场景下首字延迟(TTFT)的全链路排查与优化,区分模型首 token、HTTP 首字节和用户可见首字,并检查网络、上下文、推理与渲染路径。

#ai #howto #growing

[!info] related notes

AI 助手首字延迟优化

通用接口卡顿的 DNS → 连接 → HTTP → 应用 → DB 分层路径见 服务请求卡顿的分层排查。

目标

在不更换底层大模型的前提下,定位和优化 AI 助手每次提问后的首字延迟。先固定测量边界:

  • 模型调用 TTFT:应用发出模型请求到收到首个有效输出 token;可能包含网络、排队与 prefill,取决于计时位置。
  • HTTP TTFB:请求到响应首字节,可能只是响应头或状态事件,不代表模型已经输出。
  • 用户可见首字延迟:用户提交到界面呈现首个回答文本。占位文字、心跳、空 delta 和工具调用事件应单独统计。

前置条件

  • 有全链路可观测性(至少能在关键节点打点)
  • 能区分网络延迟和服务端处理延迟

步骤

1. 全链路节点拆解

以下是候选检查点,不是可直接相加的 12 个独立耗时。连接可能复用,RAG / 工具可能不执行或并行,外部模型 TTFT 也可能已经包含排队;应沿实际请求关键路径归因,避免重叠计数:

  1. 前端点击到请求发出(事件处理、状态更新)
  2. DNS 解析
  3. TCP 连接 / TLS 握手
  4. 网关 / 鉴权 / 限流
  5. 后端参数校验
  6. 上下文组装(历史消息、system prompt)
  7. RAG 检索(向量搜索、文档召回)
  8. Tool Calling 前置判断
  9. 模型 API 请求排队
  10. 模型处理(prefill、可能的内部推理、首个输出 token;供应商不可见阶段标为未知)
  11. SSE flush / 代理层缓冲
  12. 前端渲染第一个 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:关闭时收到上游数据后同步向客户端传递,仍需检查其余链路。
创建于 2026/5/29 更新于 2026/10/6
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本