在实际语音交互和实时智能体项目中模型推理 API 的“首 token 延迟TTFTTime To First Token”往往是决定体验好坏的关键指标却经常被总延迟或每秒输出 token 数掩盖。用户问一句“帮我查一下明天的会议安排”如果系统过了 2 秒才开始吐字不管后面输出多快对话体感都会被判定为“卡、慢、迟钝”。本文围绕 TTFT 展开一套可复现的推理 API 基准评测方法从概念定义、测量边界、脚本实现到参数分析和常见坑排查帮助你在语音场景里用“首字速度”而不是“总时长”来选型和评估推理 API。1. 实时语音与智能体场景为什么必须单独看 TTFT1.1 TTFT 的直观含义与技术定义TTFT 的全称是 Time To First Token中文可以理解为“从请求发出到生成出第一个 token 的时间”。在流式推理接口中客户端发起一次对话请求后服务端不是等全部内容生成完才返回而是先返回一小段增量数据再持续返回后续内容。这段从请求发出、到客户端收到第一个有效 token 数据的时间间隔就是 TTFT。从技术链路看TTFT 通常包含这些部分客户端发起请求到服务端接收请求的网络时间。服务端完成请求解析、鉴权、路由的时间。模型推理引擎执行 Prefill预填充阶段、处理完整输入提示词的时间。调度器排队的等待时间。第一个增量响应返回客户端的网络时间。和它经常一起出现的是 TPOTTime Per Output Token每个输出 token 的平均耗时。TTFT 描述的是“开口之前要等多久”TPOT 描述的是“开口之后每秒能说多快”。语音场景里两者缺一不可但优先级的排序要看具体交互形态。1.2 语音交互里“首字卡顿”比“总时长”更致命人在语音对话中的等待容忍度远低于文字阅读。文字接口即使第一屏加载慢一点用户还可以接受语音对话一旦出现明显的“没反应”窗口用户会觉得设备坏了、系统没听懂或者直接重复提问。这种现象在交互设计里通常被归为“感知延迟”问题真正决定体验的不是后台生成完整回答用了多少秒而是从用户停止说话到系统发出第一个声音之间有多久。在一个典型的实时语音智能体流水线中完整的响应链路往往是这样ASR 语音识别模块把用户语音转成文本。语义判断或 Agent 编排模块决定下一步动作。LLM 推理 API 生成回答文本。TTS 语音合成模块把文本变成音频并播放。TTFT 对应的是第 3 步中“第一批文本出来”的时间。它虽然只是整个链路中的一段却往往是变数最大的一段。ASR 和 TTS 的延迟相对稳定模型推理服务在排队、批处理、Prefill 上的波动却可能从几百毫秒放大到几秒。因此在评测语音智能体时单独拆出 LLM 推理环节的 TTFT能帮助定位“到底是识别慢、生成慢、还是合成慢”。1.3 TTFT、TPOT 与端到端延迟的分工一次完整响应的总耗时可以粗略表示为总延迟 ≈ TTFT TPOT × 输出 token 数 TTS 合成与播放延迟这个公式说明同样的总延迟背后可能有完全不同的原因如果 TTFT 高、TPOT 正常说明问题在“首字等待”要检查网络连接、排队、Prefill、服务端负载。如果 TTFT 正常、TPOT 高说明模型每生成一个 token 都偏慢要检查推理引擎的 Decode 阶段性能、量化方式、并发配置。如果总延迟高但 TTFT 和 TPOT 都正常问题可能出在输出长度、TTS 合成速度或播放缓冲上。所以评测时不要只报一个“平均响应时间”。正确做法是同时记录 TTFT、TPOT、总延迟和输出长度再根据语音交互形态决定优先优化哪一个。2. 设计一次可复现的 TTFT 基准评测2.1 先划定测量边界客户端视角还是服务端视角TTFT 的测量位置不同数值含义完全不同。客户端视角的 TTFT是从客户端发出 HTTP 请求开始到客户端收到第一个携带生成内容的流式数据为止。它直观反映用户体感但夹杂了网络延迟、TLS 握手、DNS 解析、客户端解析框架开销等因素。服务端视角的 TTFT则是从服务端收到请求、完成内部处理并发出第一个 token 的时间能反映模型推理服务的真实处理速度。做基准评测时至少要明确记录“测的是哪一段”。如果是为了对比多个推理 API 供应商客户端视角更有用因为用户最终感受到的就是这个值。如果是为了调优自建推理服务服务端视角更能定位引擎本身的瓶颈。推荐做法是同时记录三个时间点t_send客户端开始发送请求的时间。t_received客户端收到第一个 token 数据的时间。t_complete客户端收到全部响应的时间。由t_received - t_send得到客户端 TTFT由服务端日志中的相同请求 ID 可以反查服务端处理开始时间从而算出网络和其他开销。2.2 固定评测变量模型、提示词长度、并发和流式开关TTFT 不是一个固有属性它随请求内容变化而变化。做横向对比时必须把无关变量固定住否则测出来的差距无法归因。需要固定的变量至少包括评测变量为什么会影响 TTFT建议固定方式模型版本不同模型的参数量、架构、量化方式差异巨大所有对比都指向同一个确认可用的模型版本提示词长度Prefill 阶段要处理完整输入输入越长 TTFT 越高使用相同长度、相同内容的提示词模板输出长度上限一般不影响首 token但会影响内部调度预分配固定 max_tokens例如 200并发数排队和批处理会明显放大 TTFT分别测单并发、10、50 等固定档位流式开关非流式接口必须等全部生成完才返回评测语音场景时统一使用 streamtrue测速客户端位置离服务端越远网络部分占比越高固定同一地域的测试机时间窗口高峰时段和低峰时段服务端负载不同同一时间段内完成对比提示词长度特别值得注意。同样的推理 API输入 50 个 token 和输入 2000 个 tokenTTFT 可以差出几倍。语音智能体的请求往往带有历史对话、上下文压缩结果、工具调用结果提示词可能很长。因此基准评测要模拟真实请求形态把历史对话和系统提示词放在同一个 messages 数组中而不是用一个“你好”做代表。2.3 评测样本数与统计口径TTFT 属于高波动指标单次请求的数值几乎没有参考价值。网络抖动、服务端冷启动、负载均衡策略都会造成单点异常。规范做法是单并发场景至少测 30 到 50 次。并发场景每次持续 60 秒以上。报告平均值的同时必须报告 P50、P95、P99。明确剔除 warm-up 请求也就是评测前先发几次请求让服务端完成鉴权缓存、模型加载、连接池预热。统计口径上语音场景更关注高百分位而不是平均值。P95 和 P99 代表最差情况下用户会不会明显感到卡顿。平均值低但 P99 很高说明服务稳定性不足不适合承载实时语音。2.4 评测环境需要分开准备学习环境和生产环境的评测目标完全不同。学习环境通常是本地 PC可能用 Ollama、vLLM 或者一个小型量化模型。这个阶段的评测主要为了理解 TTFT 的影响因素帮助新手掌握测量方法。本地环境的网络开销很小测出来的 TTFT 更接近模型推理本身的耗时但硬件条件有限时CPU 推理和 GPU 推理的差异会被放大。生产环境则要针对真实流量形态做压测。测试机尽量放在与用户相近的网络区域请求内容使用真实对话样本的脱敏版本并发档位覆盖高峰水平并且要在服务端日志和监控面板同时记录指标用于交叉验证。不要在开发机上用 localhost 测出数据就直接评估线上 API两者之间隔着网络、负载均衡和跨地域路由TTFT 可能差出一大截。3. 用 curl 和 Python 脚本测量首 token 延迟3.1 先用 curl 做一次连通性探测在写完整评测脚本之前先用 curl 确认接口能不能流式返回顺便观察首包到达情况。以兼容 OpenAI Chat Completions 协议的接口为例curl -N -s https://api.example.com/v1/chat/completions \ -H Authorization: Bearer $API_KEY \ -H Content-Type: application/json \ -d { model: your-model, messages: [ {role: user, content: 用一句话介绍你自己} ], stream: true, max_tokens: 100 }-N参数关闭 curl 的缓冲让数据到达后立即输出。如果接口正常终端会先打印一段data: {id:...,choices:[{delta:{content:我}}]}最后出现data: [DONE]。再用-w参数把耗时输出出来curl -N -s -o /dev/null \ -w connect:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n \ https://api.example.com/v1/chat/completions \ -H Authorization: Bearer $API_KEY \ -H Content-Type: application/json \ -d { model: your-model, messages: [{role: user, content: 你好}], stream: true, max_tokens: 50 }time_starttransfer在 curl 中表示从开始到收到第一个响应字节的时间可以作为粗略的 TTFT 参考。注意它包含 TLS 握手和连接建立时间适合做初步探测但不适合做正式基准。3.2 Python 流式评测脚本逐行统计 TTFT正式评测建议用 Python 脚本。下面这段代码使用httpx的流式接口逐行读取 SSE 数据流并对时间戳做精确记录import asyncio import json import statistics import time import httpx API_KEY your-api-key BASE_URL https://api.example.com/v1 MODEL your-model TIMEOUT_SECONDS 60 MAX_TOKENS 200 SYSTEM_PROMPT 你是语音助手回答时先给出结论再补充细节。 USER_PROMPT 请用三句话介绍实时语音交互系统。 MESSAGES [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: USER_PROMPT}, ] async def measure_ttft_once(client: httpx.AsyncClient) - dict: t_send time.perf_counter() ttft None first_data chunk_count 0 content_chars 0 payload { model: MODEL, messages: MESSAGES, stream: True, max_tokens: MAX_TOKENS, temperature: 0.7, } async with client.stream( POST, f{BASE_URL}/chat/completions, headers{Authorization: fBearer {API_KEY}}, jsonpayload, timeoutTIMEOUT_SECONDS, ) as response: response.raise_for_status() async for line in response.aiter_lines(): if not line.startswith(data:): continue data line[len(data:):].strip() if data [DONE]: break t_now time.perf_counter() if ttft is None: ttft t_now - t_send first_data data[:80] chunk_count 1 try: obj json.loads(data) delta obj[choices][0].get(delta, {}) content_chars len(delta.get(content, ) or ) except (json.JSONDecodeError, KeyError, IndexError): pass t_complete time.perf_counter() return { ttft: ttft, total_latency: t_complete - t_send, chunk_count: chunk_count, content_chars: content_chars, first_data: first_data, } async def run_single_thread_benchmark(rounds: int 30, warmup: int 5) - None: async with httpx.AsyncClient(http2True) as client: for _ in range(warmup): await measure_ttft_once(client) ttft_list [] total_list [] for i in range(1, rounds 1): result await measure_ttft_once(client) ttft_list.append(result[ttft]) total_list.append(result[total_latency]) print(fround{i:02d} ttft{result[ttft]*1000:.0f}ms ftotal{result[total_latency]*1000:.0f}ms fchars{result[content_chars]}) def percentile(values, p): values sorted(values) idx min(len(values) - 1, int(len(values) * p)) return values[idx] print(\n TTFT summary (seconds) ) print(fmean{statistics.mean(ttft_list):.3f}) print(fp50{percentile(ttft_list, 0.50):.3f}) print(fp95{percentile(ttft_list, 0.95):.3f}) print(fp99{percentile(ttft_list, 0.99):.3f}) print( total latency summary (seconds) ) print(fmean{statistics.mean(total_list):.3f}) print(fp95{percentile(total_list, 0.95):.3f}) if __name__ __main__: asyncio.run(run_single_thread_benchmark())这个脚本的关键点有三个第一用aiter_lines()逐行读取 SSE 数据才能在流式场景下精确拿到“第一个 token 到客户端”的时间。如果直接用client.post等待完整响应测到的就是总延迟不是 TTFT。第二脚本通过ttft is None判断首次数据到达记录后就不再覆盖。这样可以避免把后续 token 的时间误记为首包时间。第三脚本在正式采样前先跑了 warmup 请求排除连接池、鉴权缓存和冷启动影响。需要注意httpx.AsyncClient(http2True)开启 HTTP/2 后部分服务端的连接复用行为会和 HTTP/1.1 不同。对比多个 API 时协议要统一否则差异可能来自协议本身。3.3 并发场景下的 TTFT 分布统计单线程评测只能反映稳定状态下的基础延迟。语音智能体在真实场景中往往有多个会话同时进行并发升高后TTFT 会因为排队而明显上升。下面是一个简单的并发压测版本import asyncio import statistics import time import httpx API_KEY your-api-key BASE_URL https://api.example.com/v1 MODEL your-model CONCURRENCY 10 DURATION_SECONDS 60 async def worker(client: httpx.AsyncClient, results: list, stop_event: asyncio.Event): while not stop_event.is_set(): try: result await measure_ttft_once(client) results.append(result[ttft]) except Exception as exc: results.append(None) print(ferror: {type(exc).__name__}: {exc}) async def run_concurrent_benchmark(concurrency: int 10, duration: int 60): async with httpx.AsyncClient(http2True) as client: results [] stop_event asyncio.Event() tasks [asyncio.create_task(worker(client, results, stop_event)) for _ in range(concurrency)] await asyncio.sleep(duration) stop_event.set() await asyncio.gather(*tasks, return_exceptionsTrue) valid [r for r in results if r is not None] print(ftotal_requests{len(results)} valid{len(valid)}) if valid: valid.sort() print(fp50{percentile(valid, 0.50)*1000:.0f}ms) print(fp95{percentile(valid, 0.95)*1000:.0f}ms) print(fp99{percentile(valid, 0.99)*1000:.0f}ms) print(fmax{max(valid)*1000:.0f}ms)并发测试要注意两个问题第一客户端本身不能成为瓶颈测试机 CPU 和网络带宽要足够第二异常请求也要记录数量因为高并发下超时和报错本身也是服务质量的一部分。若 P95 TTFT 大幅高于单并发时的值说明服务端排队处理能力不足需要调大实例数或调整批处理策略。4. 影响 TTFT 的链路因素和参数取舍4.1 Prefill 阶段与提示词长度LLM 推理分为 Prefill 和 Decode 两个阶段。Prefill 阶段一次性处理输入的提示词 token生成每个位置上的 Key 和 Value 缓存Decode 阶段再逐步生成新 token。TTFT 很大程度上由 Prefill 耗时决定。提示词越长Prefill 计算量越大TTFT 越高。语音智能体如果每次都把完整历史对话发送给 API而不做摘要或裁剪TTFT 会随着对话轮次增长不断恶化。这是实际项目中最容易被忽视的问题前几轮对话响应很快聊到第十轮时首字延迟明显变长原因是输入提示词已经膨胀了几倍。缓解手段包括对历史对话做摘要只保留关键信息。限制消息条数丢弃过旧的消息。使用上下文压缩工具而不是原样拼接。在评测时按“最长可能提示词”而不是“平均提示词”做压测。4.2 连接建立、TLS 与网络往返客户端视角的 TTFT 很大一部分可能消耗在网络层。实时语音智能体如果复用不活跃的 HTTP 连接每次请求都要重新经历 TCP 握手和 TLS 握手可能多出 100 到 300 毫秒。使用连接池可以显著降低这部分开销。在httpx中复用同一个AsyncClient实例就能复用底层连接在服务端网关层要合理配置 keep-alive 超时避免空闲连接频繁释放。更激进的做法是使用 HTTP/2 多路复用多个会话共享一条 TCP 连接减少握手次数。网络距离也是最容易被忽略的因素。如果用户在国内而推理 API 部署在海外仅网络往返就可能让 TTFT 增加几百毫秒。评测时建议把测试机部署在目标用户所在的区域而不是在办公室连内网测海外接口。4.3 服务端排队与批处理调度服务端的首 token 延迟通常包含调度等待时间。推理引擎为了提升吞吐会把多个请求拼成一个 batch 一起推理。如果请求到达时刚好赶上 batch 窗口TTFT 就会偏大如果请求少、batch 立即开始TTFT 就小。这也解释了为什么 TTFT 在低并发时稳定、高并发时波动剧烈。评测线上 API 时建议用固定并发持续压测一段时间观察高百分位的恶化程度。语音场景对稳定性的要求高于吞吐峰值选型时要更看重 P95 和 P99而不是总吞吐量。4.4 推理参数对 TTFT 的影响以下参数对 TTFT 的影响通常可以这样判断参数对 TTFT 的影响说明stream必须为 true非流式接口只能返回完整结果TTFT 等于总延迟max_tokens影响不明显但会参与预分配判断一般不需要为了首 token 调小temperature基本不影响采样参数在 Decode 阶段生效presence_penalty / frequency_penalty可能有轻微影响部分引擎在采样时多算一层逻辑提示词长度显著影响提示词越长Prefill 越大并发数显著影响排队时间会直接加进 TTFT输出格式约束JSON Schema 等可能有影响结构化输出约束会增加首 token 前的约束校验逻辑如果对比两个 API务必用完全相同的参数集合。尤其要确认两者都是streamtrue且max_tokens、提示词长度一致。5. 常见坑与排查路径5.1 首次请求慢后续变快现象第一次调用接口时 TTFT 高达几秒同一客户端第二次调用降到几百毫秒。可能原因服务端冷启动客户端连接池未建立鉴权信息首次加载DNS 缓存未生效共享网关实例扩缩容延迟。排查方式连续调用 5 次以上观察 TTFT 是否逐渐下降并稳定。检查服务端日志中请求到达时间与模型推理开始时间之间的间隔。确认是否使用了连接复用避免每次新建连接。处理建议评测脚本必须在正式采样前完成 warmup。生产环境则需要预热机制例如在发布后自动发送少量探测请求避免第一个真实用户承担冷启动开销。5.2 TTFT 忽高忽低现象同样提示词、同样并发TTFT 在 300 毫秒和 2 秒之间跳动。可能原因服务端负载波动批处理窗口网络路径变化共享客户端的其他租户流量干扰本机网络拥塞。排查方式统计多次请求的 P50、P95、P99判断波动是否集中在尾部。在服务端监控中对比同一时段的 GPU 利用率、队列长度和请求量。换一台测试机、换一个网络环境复测排除本地因素。处理建议如果尾部延迟高优先考虑增加实例或开启自动扩缩容。如果是共享 API 服务可能需要切换为独占实例或降级方案。5.3 返回内容很多但迟迟不吐第一个字现象接口最终能返回完整内容总延迟正常但 TTFT 很高。可能原因服务端在首 token 前做了额外处理例如检索增强、工具调用、意图识别输入提示词过长导致 Prefill 时间长网关层有完整响应缓冲。排查方式检查是否配置了 RAG 或 Agent 工具调用这些逻辑发生在模型生成文本之前。统计提示词 token 数与 TTFT 对比判断是否存在线性关系。确认网关或反向代理没有关闭流式转发导致所有响应被缓冲后一次性返回。处理建议Agent 场景中如果检索本身很慢LLM 的 TTFT 测出来再低也没用。要按完整链路拆解把检索耗时和模型首 token 耗时分开统计。5.4 并发一高 TTFT 就失控现象单并发时 P95 不高并发到 20 后 P95 翻倍甚至更多。可能原因服务端排队时间变长推理引擎 batch 过大导致单个请求等待过久测试客户端资源耗尽。排查方式记录每个请求的服务端处理开始时间区分排队时间和推理时间。观察服务端队列长度和 GPU 利用率。检查测试机 CPU、内存和网络连接数是否打满。处理建议语音智能体通常对“同时刻活跃会话数”有明确上限压测档位要覆盖真实峰值并预留 30% 以上的余量。若无论如何优化 TTFT 都压不下来就要增加实例数量做水平扩容。5.5 指标在不同机器上对不上现象本机测得 TTFT 是 400 毫秒线上监控面板显示 200 毫秒两边对不上。可能原因测量起点不一致监控面板统计的是服务端处理时间本机统计的是客户端感知时间本机离服务端网络较远监控面板只统计成功进入推理的请求而本机统计包含排队和网络。排查方式对齐测量口径明确客户端时间和服务端时间的定义。在请求日志中记录时间戳和请求 ID逐条比对。排查本机到服务端的网络 RTT。处理建议建立基准评测规范时先在文档里写清楚“客户端视角 TTFT”和“服务端视角 TTFT”两个定义所有对比都基于同一口径。6. 从基准结果到实时语音智能体选型6.1 学习环境与生产环境的评测差异本地学习环境用ollama serve或 vLLM 启动一个小模型主要用来理解 TTFT 的变化规律输入提示词变长时 TTFT 如何上升并发升高时排队怎么影响首包。这个阶段用 CPU 推理也能跑但 CPU 和 GPU 的数值没有横向可比性。生产环境评测则要考虑更多因素维度学习环境生产环境测试机位置本机 localhost与用户同区域走公网路径请求内容简单的“你好”真实对话脱敏样本含系统提示词和历史并发档位单并发为主覆盖峰值并预留余量指标平均值为主P50、P95、P99 并重协议HTTP/1.1 即可按实际客户端使用 HTTP/1.1 或 HTTP/2持续时长几十次请求至少几分钟以上的持续压测服务端日志可不看必须结合队列长度、GPU 利用率和日志交叉验证6.2 参数与方案选型速查表针对语音智能体的推理 API 评测和选型可以参考下面这张速查表场景推荐关注指标可接受的 TTFT 参考范围优化方向简单问答TTFT 平均值语音场景建议 500 毫秒以内压缩提示词、使用连接池多轮对话TTFT P95800 毫秒以内历史摘要、上下文压缩带工具调用的 AgentTTFT 工具调用耗时需要单独拆开看工具结果预加载、并行检索高并发客服TTFT P991 秒以内超过则告警水平扩容、独占实例实时语音对讲TTFT TTS 拼接延迟首字音频越快越好边生成边合成先合成第一句以上范围是经验参考值不是绝对标准。不同产品对延迟的容忍度差异很大正式立项时应该基于自己的用户调研设定目标值。6.3 发布前评测检查清单每次评估或选型推理 API 时可以按这份清单逐项确认评测前是否完成 warmup 至少 5 次。是否固定了模型版本、提示词长度、max_tokens、temperature。是否全部使用流式接口关闭了任何缓冲层。是否分别统计了 TTFT、TPOT、总延迟和输出长度。是否同时报告 P50、P95、P99而不是只看平均值。是否记录了测试机位置和网络类型。是否用真实业务提示词样本而不是简单的“你好”。是否做了单并发和多并发两轮评测。是否排除了测试客户端自身的资源瓶颈。是否将客户端观测结果与服务端日志交叉验证。把这份清单放进评测脚本的 README 里每次跑完基准就逐项打勾可以避免“测出来很漂亮上线后发现卡顿”的情况。7. 扩展方向从 TTFT 到整套实时链路评测TTFT 只是实时语音智能体延迟评测的第一步。当模型接口的首 token 足够快之后瓶颈往往会转移到完整链路的其他环节ASR 的识别等待时间、语义 VAD 的断句决策、TTS 的合成速度、音频播放的缓冲策略。评测范围应该逐步扩展成“用户说完话”到“系统发出第一个声音”的端到端延迟并把它分解成各模块耗时。在工程落地层面推荐为每个环节都建立独立埋点ASR 从音频结束到文本输出的耗时、LLM 的 TTFT 与 TPOT、TTS 从文本到音频帧的合成耗时、播放队列的缓冲时间。所有时间戳统一使用同一时钟源并通过请求 ID 串联。对新手来说最值得做的练习不是直接投入大规模压测而是先用一个本地模型和一个流式接口脚本亲手把 TTFT 的测量链路跑通理解 Prefill、排队、网络往返各自贡献了多少时间。能准确说清“首字慢在哪一段”后续做语音智能体的低延迟优化就会顺利很多。