降低 LLM 延遲是建構反應迅速的 AI 應用的工程師所面臨最關鍵的挑戰之一。雖然大型語言模型(LLM)的能力持續增強,但其逐 token 的生成方式會為終端使用者帶來令人沮喪的瓶頸,而漫長的等待時間會直接導致參與度下降與應用流失。因此,最佳化推論管線的速度是開發者的一項核心要求。本指南概述如何設定提示詞快取、實作回應串流傳輸、建構邊緣網路路由,以及使用無伺服器組態來削減處理延遲。
[!TIP] 效能指標提示: 在測量 API 延遲時,請將首個 Token 時間(TTFT)與整體生成速度區分開來。較低的 TTFT 會讓應用對使用者產生即時的感覺,即便整個輸出生成需要數秒,因為文字會立即開始算繪。
重點摘要:
- 提示詞快取: 重複使用靜態前綴標頭以繞過解析狀態,將 TTFT 降低 80%。
- 回應串流傳輸: 透過伺服器傳送事件(SSE)推送 token,讓使用者看到即時的文字生成。
- 邊緣 Worker: 在靠近使用者的區域邊緣中心執行授權與請求路由。
- 模型路由: 將簡單的使用者請求分流至輕量模型以最佳化速度。
LLM API 延遲的構成要素
要縮短回應時間,你必須先了解哪些要素決定了整體 API 延遲。整體延遲是三個不同變數的累計總和。
首先,網路傳輸時間衡量一個請求從用戶端到你的伺服器,再到模型供應商的 API 所花費的時長。這使得傳輸距離成為一個主要瓶頸。
其次,首個 Token 時間(TTFT)表示模型從接收請求到生成第一個輸出 token 之間的時長,這正是提示詞快取如此重要的原因。
最後,token 生成速度衡量硬體輸出後續 token 的速率。硬體限制決定了生成速度,但開發者仍完全掌控傳輸時間與 TTFT,因此聰明的路由與快取能夠大幅降低 LLM 延遲。
加速你的 LLM 整合前置條件
在開始接入下面的最佳化之前,請確保你已備妥以下各項。它們都並不奇特,但漏掉其中任何一項往往會在後續引發令人困惑的故障。
- 一個由你控制的中介軟體或邊緣執行環境。 範例使用 Cloudflare Workers,但任何能夠代理請求的無伺服器平台都可行。你需要一個位於瀏覽器與模型供應商之間的落腳點。
- 支援串流傳輸與快取的供應商的 API 憑證。 OpenAI 與 Anthropic 都支援。請將金鑰儲存為 secret(Wrangler secret 或環境變數),絕不要放在用戶端程式碼中。
- Node.js 18 或更新版本 —— 如果你想用
wrangler dev在本機測試 Workers。全篇使用的全域fetch與ReadableStreamAPI 在該執行環境與現代瀏覽器中均可用。 - 一個基準測量值。 在變更任何內容之前,先記錄你目前的首個 Token 時間與總回應時間,以便證明每一項最佳化確實帶來了幫助。下面的基準測試部分會說明具體做法。
- 對伺服器傳送事件(SSE)的熟悉。 串流回應以一系列
data:行的形式抵達,你將在用戶端解析它們。
實作提示詞快取
對於圍繞大型系統提示詞建構的應用,提示詞快取是最佳化 TTFT 最有效的方式。當一個請求包含一段冗長的靜態指令區塊(例如代理的系統提示詞或 RAG 參考文件)時,模型供應商必須在每次執行時解析並編碼這些 token。Anthropic 與 OpenAI 都支援提示詞快取,它將解析後的 token 狀態儲存在記憶體中。隨後共用相同前綴的請求便會繞過解析階段,將 TTFT 降低多達 80%。
快取的存活時間因供應商而異。Anthropic 在大約五分鐘無活動期間維持快取,而 OpenAI 使用動態衰減模型。因此,安排定期的背景 fetch ping 可以讓關鍵的系統指令在伺服器記憶體中保持活躍。
兩家供應商的機制略有不同,而把請求結構搭對正是決定快取能否真正命中的關鍵。使用 Anthropic 時,你用 cache_control 明確標記一個快取斷點。斷點之前的所有內容都會被儲存,因此穩定的靜態內容必須放在前面,而每次請求都會變化的易變內容必須放在最後:
1import Anthropic from "@anthropic-ai/sdk";
2
3const anthropic = new Anthropic();
4
5const response = await anthropic.messages.create({
6 model: "claude-opus-4-8",
7 max_tokens: 1024,
8 system: [
9 {
10 type: "text",
11 text: SYSTEM_INSTRUCTIONS // small, sent on every request
12 },
13 {
14 type: "text",
15 text: KNOWLEDGE_BASE, // large, static reference block
16 cache_control: { type: "ephemeral" }
17 }
18 ],
19 messages: [
20 { role: "user", content: userQuestion } // volatile — after the breakpoint
21 ]
22});
23
24// Confirm the cache is working
25console.log(response.usage.cache_read_input_tokens);
最常見的錯誤是把時間戳記、請求 ID 或任何每次請求都變化的字串放在被快取區塊的前面。由於快取是前綴比對,斷點之前任何位置只要有一個位元組發生變化,就會使其後的所有內容失效,而快取會悄無聲息地永遠不命中。可透過讀取回應中的 usage.cache_read_input_tokens 來驗證它是否運作:如果在相同請求間該值始終為零,說明有動態內容混入了前綴。還需注意,被快取的前綴必須超過某個最小長度(視模型而定,大約在 1,024 到 4,096 個 token 之間),快取才會啟用。
OpenAI 採用更簡單的方式:對於超過大約 1,024 個 token 的提示詞,快取是自動的,沒有 cache_control 旗標需要設定。不過同樣的紀律仍然適用。請把靜態指令區塊放在 messages 陣列的最開頭,並把變化的使用者輸入附加在末尾,讓可重複使用的前綴在請求之間逐位元組保持一致。
要了解提示詞快取的定價與參數結構,請查閱 Anthropic 提示詞快取指南 。
邊緣運算與無伺服器路由
在單一集中式伺服器上處理 LLM 請求,會為全球使用者引入巨大的網路跳數。將你的 API 中介軟體部署在無伺服器邊緣網路(如 Cloudflare Workers)上,能夠極大地縮短這些路徑。
邊緣 Worker 接收用戶端請求,對工作階段進行授權,並將其路由至最近的模型供應商資料中心。這種無伺服器結構在 token 被運算出來的那一刻就將其傳送到使用者螢幕,因此介面感覺高度靈敏。下面的 JavaScript 中介軟體示範如何直接從邊緣執行環境設定串流回應:
1export default {
2 async fetch(request, env) {
3 const payload = await request.json();
4
5 // Call the streaming LLM endpoint
6 const response = await fetch("https://api.openai.com/v1/chat/completions", {
7 method: "POST",
8 headers: {
9 "Authorization": `Bearer ${env.OPENAI_API_KEY}`,
10 "Content-Type": "application/json"
11 },
12 body: JSON.stringify({
13 model: "gpt-4o-mini",
14 messages: payload.messages,
15 stream: true
16 })
17 });
18
19 // Forward the stream directly to the client browser
20 return new Response(response.body, {
21 headers: { "Content-Type": "text/event-stream" }
22 });
23 }
24};
這種無伺服器結構在 token 被運算出來的瞬間就將其傳送到使用者螢幕。要了解如何建構邊緣最佳化的後端,請閱讀我們關於使用 Cloudflare Workers 建構無伺服器 API 的指南。
在用戶端解析串流
從邊緣轉發串流只完成了一半工作。瀏覽器仍然必須在這些區塊抵達時讀取它們並算繪每個 token,否則回應會在緩衝區中累積並一次性出現,從而失去意義。這一步是大多數教學略過的,也正是感知效能真正決勝負的地方。
回應主體是一個由原始位元組組成的 ReadableStream。SSE 訊框以 data: 行的形式抵達,但單個網路區塊可能包含多個訊框,或者把一個訊框拆分到兩個區塊中,因此你必須緩衝不完整的行,而非假設每個區塊都是一則完整的訊息:
1async function streamCompletion(messages, onToken) {
2 const response = await fetch("/api/chat", {
3 method: "POST",
4 headers: { "Content-Type": "application/json" },
5 body: JSON.stringify({ messages })
6 });
7
8 const reader = response.body.getReader();
9 const decoder = new TextDecoder();
10 let buffer = "";
11
12 while (true) {
13 const { value, done } = await reader.read();
14 if (done) break;
15
16 buffer += decoder.decode(value, { stream: true });
17 const lines = buffer.split("\n");
18 buffer = lines.pop(); // keep the trailing partial line
19
20 for (const line of lines) {
21 if (!line.startsWith("data: ")) continue;
22 const payload = line.slice(6).trim();
23 if (payload === "[DONE]") return;
24
25 try {
26 const json = JSON.parse(payload);
27 const token = json.choices?.[0]?.delta?.content;
28 if (token) onToken(token);
29 } catch {
30 // ignore keep-alive comments and malformed partial frames
31 }
32 }
33 }
34}
buffer.split("\n") 後面跟著 lines.pop() 是關鍵細節:它會保留任何不完整的行,直到下一個區塊將其補全。將 JSON.parse 包裹在 try/catch 中,可以在一個 keep-alive 註解或一個只收到一半的訊框抵達時保持迴圈存活。隨後 onToken 回呼會把每個片段附加到 DOM,因此文字會在模型產出它的那一刻出現。
測量與基準測試延遲
你無法最佳化你沒有測量過的東西。在每次變更前後,記錄首個 Token 時間與總生成時間,以便把改進歸因於正確的原因。取樣 TTFT 最快的方式是使用 curl,用 time_starttransfer 作為第一個位元組抵達用戶端時刻的近似值:
1curl -w "TTFT: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
2 -X POST https://your-worker.example.com/chat \
3 -H "Content-Type: application/json" \
4 -d '{"messages":[{"role":"user","content":"Hello"}]}' \
5 -o /dev/null -s
對於應用層級的數字,請直接在用戶端解析器中埋點。在請求發出時打一次時間戳記,在第一個 token 抵達時再打一次:
1const start = performance.now();
2let firstTokenAt = null;
3
4await streamCompletion(messages, (token) => {
5 if (firstTokenAt === null) firstTokenAt = performance.now();
6 render(token);
7});
8
9console.log(`TTFT: ${Math.round(firstTokenAt - start)}ms`);
每項測量執行多次並取中位數,而非單次取樣,因為網路波動與冷啟動會扭曲一次性讀數。下表給出了一個運作良好的全球請求中時間通常花在何處的範例範圍;請把它們當作用於比較的形態,而非固定數值,因為你自己的數字將取決於區域、模型與提示詞大小。
| 階段 | 典型占比 | 是否可控? |
|---|---|---|
| 網路傳輸(用戶端到邊緣) | 10–60 ms | 是 —— 邊緣路由可縮短它 |
| 邊緣上的中介軟體處理 | 1–15 ms | 是 —— 保持 Worker 精簡 |
| 首個 Token 時間(冷提示詞) | 400–1,200 ms | 部分 —— 快取可大幅削減 |
| 首個 Token 時間(已快取前綴) | 100–400 ms | 是 —— 透過提示詞快取 |
| 每 token 生成 | 10–50 ms/token | 否 —— 由模型與硬體決定 |
值得盯著看的兩行是已快取與冷啟動的 TTFT 數字。這一差距是大多數應用可獲得的最大單項收益,這正是提示詞快取位居最佳化清單之首的原因。相比之下,生成速度由供應商固定,因此把較簡單的請求路由至較小的模型是那裡唯一的槓桿。
逐步最佳化工作流程
要最佳化你的軟體應用速度,先從把靜態系統指令與動態使用者輸入分離開始。這種劃分讓你能夠乾淨俐落地瞄準快取進入點。
接下來,在你的 API 酬載中啟用提示詞快取旗標,以確保模型供應商把你的文字 token 儲存在記憶體中。
始終使用標準 SSE 端點設定回應串流傳輸。透過撰寫輕量的前端解析器在區塊抵達時處理它們,你可以改善使用者感知的效能。然後建立模型回退路徑:把基礎的客戶輸入路由至較小的模型,將較大的推論模型保留給進階任務。最後,剖析網路跳數以確認無伺服器 Worker 減少了傳輸延遲。有關資料庫最佳化的細節,請查看我們關於 WordPress 與客製化網頁開發比較 的指南。
常見陷阱與疑難排解
當團隊首次推出串流傳輸與快取時,有少數幾種故障會一再出現。辨識其症狀可以省去數小時的猜測。
| 症狀 | 可能原因 | 解決方法 |
|---|---|---|
cache_read_input_tokens 始終為零 | 時間戳記、UUID 或工作階段 ID 位於快取斷點之前,導致前綴每次請求都變化 | 把所有動態內容移到靜態區塊之後;對任何 JSON 進行確定性序列化 |
| Token 一次性抵達,而非逐步抵達 | 中介代理或 CDN 正在緩衝回應 | 傳送 Cache-Control: no-transform 與 X-Accel-Buffering: no;確保設定了 text/event-stream 內容類型 |
| 串流中途中斷 | Worker 在上游回應主體結束前就返回了,或觸及了 max_tokens | 直接返回 response.body 而非等待完整文字;提高 max_tokens |
| 儘管已快取但首個 token 仍然很慢 | 靜態前綴低於供應商的最小可快取長度 | 整合指令使被快取區塊超過約 1,024 token 的下限 |
| 用戶端解析器在某些區塊上擲出例外 | 一個訊框被拆分到兩個網路區塊中 | 如上所示緩衝不完整的行,並把 JSON.parse 包裹在 try/catch 中 |
一個更隱蔽的陷阱是在邊緣本身進行緩衝。如果你在 Worker 內部於返回之前執行 await response.text(),你就悄悄地把一個串流回應變回了阻塞式回應。請始終把串流主體直接透傳。同樣地,留意 Worker 的 CPU 限制:中介軟體中每次請求的繁重工作會直接疊加到 TTFT 上,因此要讓授權與路由邏輯保持精簡,並延後任何昂貴的操作。
生產環境考量
讓一個示範在瀏覽器中串流執行很簡單;但要在真實流量下可靠執行則需要多幾道防護。
在上游呼叫上設定一個合理的請求逾時,使一個停滯的供應商連線無法無限期地占用一個 Worker,並將其與一個重試機制配對,當主供應商逾時時回退到第二個供應商或較小的模型。由於提示詞快取對快取寫入收取少量溢價,對讀取給予大幅折扣,只有當前綴被重複使用時它才划算;每隔幾分鐘的背景 ping 可以讓一個熱門系統提示詞常駐,而無需在每次使用者請求時都付費重寫它。
要持續埋點,而不只是在上線時。為每次請求記錄 TTFT 與每秒 token 數,並在中位數漂移時告警,因為供應商端的回退或提示詞大小的變化會最先在那裡顯現。最後,尊重供應商的速率限制:一波並行串流可能觸及它們,因此要優雅地排隊或卸載負載,而非讓請求悄然失敗。這些措施能把一個快速的原型轉變為一個在關鍵時刻仍保持快速的應用。
重點摘要
- 瞄準首個 Token 時間(TTFT)與傳輸時間以降低 LLM 延遲。
- 利用模型 API 上的提示詞快取以繞過系統指令的解析開銷。
- 使用回應串流傳輸即時交付 token,改善感知速度。
- 將 API 中介軟體部署在無伺服器邊緣執行環境上,以縮短全球網路路徑。
- 將較簡單的使用者查詢路由至輕量模型以最佳化執行速度。
- 設定效能監控工具,在真實使用者條件下持續分析並降低延遲。
常見問題(FAQ)
我如何在生產環境中降低 LLM 延遲? 要在生產環境中降低 LLM 延遲,你應當為靜態指令實作提示詞快取、啟用 token 串流傳輸,並部署邊緣 Worker 以最佳化請求路由。透過將無伺服器編排執行環境部署到更靠近全球用戶端的位置,開發者繞過了多個網路路由跳數,並即時交付首個回應 token。
什麼是提示詞快取? 提示詞快取是一項 API 功能,它把解析後的文字狀態儲存在伺服器記憶體中,使得使用相同前綴的後續請求執行得快得多。透過繞過針對大型指令資料集的系統解析週期,這一最佳化將首個 Token 時間(TTFT)降低多達百分之八十。
模型大小會影響延遲嗎? 會,較小的模型具有快得多的 token 生成速度,使其非常適合以延遲為首要考量的簡單任務。將直接的分類或擷取請求路由至專門的邊緣模型,可確保快速的處理時間,同時把稠密模型保留給推論任務。
伺服器傳送事件(SSE)串流傳輸如何幫助降低感知延遲? SSE 串流傳輸在文字輸出 token 被編譯出來時,即時地把它們從模型主機推送到用戶端螢幕。儘管這不會減少總執行時長,但它最小化了首個 Token 時間(TTFT),並給使用者帶來一個靈敏、活躍的應用介面。
我如何在邊緣快取動態的 LLM 回應? 你可以使用帶有較短 TTL(存活時間)限制的 KV 資料庫或 Redis 實例在邊緣快取動態回應。快取動態回應對於重複的使用者查詢或常見的客服意圖很有效,可完全避免對模型供應商的網路呼叫。
評論