
在實際語音交互和實時智能體項目中模型推理 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、排隊、網絡往返各自貢獻了多少時間。能準確說清“首字慢在哪一段”后續做語音智能體的低延遲優化就會順利很多。