拆解 AI 接口延迟,定位排队与流式瓶颈
文章将 AI 请求耗时拆成首个有效文本等待、流式输出速度与整体完成时间,并提醒纳入应用排队和工具等待。建议先明确测量边界、用单调时钟记录时长,文中示例不代表供应商性能基准。
AI 深度解读
AI 请求花了 12 秒,到底是模型生成慢、应用排队久,还是答案被代理攒着没发?只记一个总耗时,分不清这些原因,也解释不了为什么同样等待 12 秒,有的聊天界面很快开始出字,有的始终一片空白。
这篇文章给出的办法很务实:先把请求拆成能定位责任的时间段,再决定优化哪里。它更像一份排障指南,没有提供厂商性能实测;其中最有用的,是把「用户什么时候看见答案」和「接口什么时候收到响应」分开计算。
一次请求至少要记下五个时刻
先确定用户觉得哪里慢:长时间不出字、开始后出字慢,还是迟迟不能完成。这三种症状需要分别记录。
交互式聊天关心首次可展示文本;文档处理任务可能更关心排队和完成时间;会调用工具的 Agent,还要把搜索、数据库查询等等待算进去。如果计时从发出 HTTP 请求才开始,之前等 worker 的 5 秒就会消失。
作者建议记录五个时刻:应用接受逻辑操作、发起出站 HTTP 尝试、收到响应头、解析出首次可展示内容,以及协议确认成功完成。失败和取消也要单独记录,流中断前吐过文字,不代表请求成功。
下面这段假设时间线,用来说明应该怎样拆分耗时,单位是秒:
accepted 0.0
dispatched 0.8
headers received 1.0
first displayable text 2.4
completed 4.0
要看清 accepted 和 dispatched 两行:请求发出前已经等了 0.8 秒。从用户操作被接受算,首次有用文本耗时 2.4 秒;从 HTTP 尝试发出算,则是 1.6 秒。
首次文本到完成又经过 1.6 秒,但不能直接叫「模型解码时间」。这里可能混着缓冲、传输、解析,以及最后一段文字之后的协议事件。没有更细的观测,就叫「首次内容之后的耗时」。
进程内计算时长应使用单调时钟(monotonic clock),避免系统时间调整影响结果。跨服务关联则保留墙上时钟时间戳和 request/trace ID,不能拿不同机器的单调时钟直接相减。
收到响应头,为什么还不能算开始回答
首字节时间 TTFB 是 HTTP 层面的里程碑;首 token 时间 TTFT 则涉及推理或应用层。厂商面板、HTTP 客户端和聊天 UI,可能各算各的。
流式接口可以先发送响应头、心跳、角色事件或其他元数据,答案正文稍后才来。后端认为「已经收到数据」,用户看到的仍然是空白。
对聊天应用,更贴近体验的指标是首次可展示内容时间。
如果把它叫作 TTFT,就要说明「第一个 token」具体指什么。结构化输出和工具调用,也需要另行定义首次有用事件,不能套用首段文本的标准。
同样,网络读取块(chunk)不等于 token。一次读取可以包含多个事件,一个事件可以包含多个 token,一个事件也可能被拆到几次读取里。统计每秒收到几个 chunk,测不到模型的 token 生成速度。
即使要公布 tokens/s,也得交代 token 数量来自哪里、分母用了哪段时间。可见文本、隐藏推理和供应商上报的 usage,覆盖的工作可能不同。定义没对齐,一个漂亮的比值说明不了多少问题。
定位时可以沿时间线逐段查:接受到发出慢,检查 worker、本地准入控制,以及是否被计入这一段的连接池等待;发出到响应头慢,检查建连、网络距离、代理和上游响应;响应头快但正文慢,再查上游等待、输入处理、推理行为、缓冲,以及早期事件是否只有元数据。
这些是调查方向,不是单凭一段时长就能确定的结论。
流式输出一卡一卡,要沿整条链路找
作者给出的典型路径是:
model service -> gateway -> application -> edge/proxy -> browser parser -> UI
要关注相邻节点之间的交接,尤其是 browser parser -> UI:浏览器收到事件和用户看见文字,中间仍有解析与渲染工作。
排障时不必记录回答正文。时间戳、字节数、事件类型和关联 ID,通常就能帮助判断内容在哪一段开始积压。
如果应用持续收到答案事件,浏览器却很晚才收到一大批,优先检查中间代理的缓冲和刷新。如果浏览器及时收到了事件,界面却迟迟不动,就检查客户端解析、批量更新和主线程工作。
解析器还必须跨网络读取维护事件边界。不能把每次读取当成完整 JSON 或完整的服务器发送事件(SSE),否则拆包就可能造成输出损坏或看似无故的停顿。
UI 更新也有取舍:每个微小片段都触发组件更新,会制造过多工作;积攒太久再显示,又会把正常的流做成卡顿。原文建议选择适合界面的短批处理间隔并实测,没有给出通用毫秒数。
Streaming 能改善开始等待的体验,但不保证生成更快,也不保证代理立即转发每个片段。
有网关时,应关联它的入站请求、出站尝试和首次转发内容。直接拿两个面板的耗时相减并不可靠,它们可能使用不同边界,甚至聚合了不同请求。Server-Timing 可以暴露选定的服务端时长,但只能展示已经埋点的部分,不能自动看穿上游隐藏队列,公开时也应避开内部拓扑和敏感细节。
我的判断
这篇文章说得最对的,是先统一计时口径,再讨论模型、网关或部署方案。对前端和全栈团队,这比只看接口总耗时更容易形成可执行的排查路径。
不过,它是一套测量方法,不能据此认定某种网关或模型更快。作者说明自己参与 YW Code API(ywcode)网关,也披露文章由 AI agent 撰写;所有时间例子都是示意,没有供应商对照测试。文中的建议应作为排障方法评估,不能当作产品性能证据。
真正比较方案时,还要固定确切模型标识和版本、prompt 大小、输出长度、streaming 模式、启用功能及客户端区域。十个词的问候和带工具的长对话,即使用同一个模型标签,也不是同一负载。
冷连接可能包含 DNS、连接和 TLS 建立成本;热连接可以避开其中一部分。缓存命中也不能证明模型推理更快。冷、热情况需要分别报告。
作者建议先跑小规模顺序基线,再提高并发。吞吐量上去,每个用户也可能等得更久。生产统计则应把样本量、时间窗口、负载分组放在 p50、p95 旁边,并同时观察错误率和取消率。样本太少,p95 不稳定;只统计成功请求,也可能让频繁拒绝请求的部署显得很快。
原文没有给出这些做法能带来多少收益,也没有通用阈值。这是边界清楚的工程建议,具体结论仍要靠自己的数据。
能拿来做什么
-
给 Node 中转服务补上五个时间点,同时记录前端首次展示文本的时刻。排查时先判断等待发生在本地队列、出站请求、内容转发还是 UI,避免只凭总耗时切换模型。
-
遇到 SSE 突然成批出字时,比较应用收到答案事件、浏览器解析事件和界面更新的时间。检查解析器是否保留跨读取的事件边界,再根据观测调整代理刷新和前端批量更新间隔。
-
检查 SDK 或网关是否自动重试,为逻辑操作保留一个 ID,为每次物理尝试保留独立 ID 或序号。总耗时从接受操作开始,包含排队、失败尝试和退避;调大 timeout 只能让请求多等一会儿,超时后重放还可能启动重复工作。
-
整理慢请求报告时,提供客户端及库版本、负载、确切模型、区域、UTC 时间、排队时长、两种起点的首次内容时间、完成时间、尝试次数和结果。例如原文示意值是排队 0.8 秒、接受后 2.4 秒出字、发出后 1.6 秒出字、接受后 4.0 秒完成,尝试 1 次。真实故障要全部替换为同一次操作的观测值,并移除 API key、cookie、完整 prompt 和原始请求转储。