在 LLM 聊天 UI 中送出訊息後會發生什麼事

最後更新: 2026-08-13 13:13:57 +0800 +0800
tags: Ai | Llm | Chat | Inference | Streaming | Web
原文以英文撰寫。您正在閱讀AI翻譯版本。

聊天介面讓人與大型語言模型互動看起來很簡單: 輸入訊息、按下 Enter,然後等待回答出現。在這次互動背後, 執行的是一條由瀏覽器、應用程式伺服器、資料庫、策略系統、模型閘道、 推論工作程序,以及工具或檢索服務共同參與的分散式請求管線。

在既有對話中,新文字只是模型輸入的一部分。應用程式必須重建足夠的對話內容, 將它與隱藏指示和其他上下文結合,在有限的上下文視窗中容納所有內容,執行推論, 串流傳送結果,並持久化新的狀態。

一條具代表性的路徑如下:

KDSRRSRereeeaeyancnvcsfdedeottierBraBvrfdroscreiokttopCnuweaiweahasstosvralaeeneettgrrniaetaBsslasacintksdeetxnaftdnitnimsehsssatgaASLAARPDetuaosporeetvaspIuechedelntfoemyfeidCnuabelehtsclbrtlaieteueootcridnpuavmgcwrtBtmeoeeoopaeedtrmucsbeSkptkrsrlaeeteeaanrrtnqgnidvoduecnikehppcesuoenttlsicy

具體產品可能會合併、重新排序或省略其中一些階段。模型 API 也可能位於應用程式內部, 而不是作為獨立服務存在。一般原則不變:Web 應用程式擁有對話,而一般的語言模型呼叫 則將有界的輸入 token 序列轉換為輸出 token 序列。

送出訊息之前 #

使用者輸入時,瀏覽器通常會更新文字區域,並將草稿保留在元件狀態中。 它也可能:

  • 調整編輯器大小;
  • 將草稿儲存到本機儲存空間;
  • 在送出訊息前上傳並掃描附件;
  • 辨識斜線指令或提及;
  • 估算輸入是否超過產品限制;以及
  • 啟用或停用送出按鈕。

這些工作通常不涉及語言模型。自動完成、拼字檢查和輸入法編輯器可以產生自己的瀏覽器或 作業系統事件,但聊天應用程式仍會接收最終文字。

按下 Enter、點擊 Send,或點選行動版的等效控制項後,會觸發送出處理常式。 由於編輯器可能立即清空以便輸入下一則訊息,處理常式會擷取目前草稿的不可變版本。 它也會建立用於關聯樂觀 UI、網路請求和最終資料庫記錄的識別碼。

常見識別碼包括:

識別碼用途
Conversation ID選取既有對話
Message ID識別這則使用者訊息
Parent message ID選取分支或前置訊息
Request or turn ID關聯一次生成嘗試
Idempotency key防止重試造成訊息建立兩次
Client trace ID連接瀏覽器與伺服器遙測

瀏覽器通常會立即將使用者訊息加入逐字稿。 這是樂觀更新:在伺服器確認之前,UI 先顯示預期狀態。 如果送出失敗,可以將訊息標記為失敗並提供重試,而不是讓它消失。

送出這一輪互動 #

瀏覽器送出經過驗證的 HTTPS 請求,其中包含對話識別碼、新訊息、附件參照、選取的模型與產品特定選項。 它可能使用一般的 POST 請求:

http
POST /api/conversations/conv_123/messages HTTP/1.1
Authorization: Bearer ******
Content-Type: application/json
Idempotency-Key: turn_789

{
  "message_id": "msg_456",
  "parent_id": "msg_455",
  "content": [
    {
      "type": "text",
      "text": "Compare that approach with optimistic locking."
    }
  ],
  "model": "example-chat-model"
}

這是應用程式自己的 API,不一定是模型提供者的 API。 後端稍後可能會將這一輪轉換為 OpenAI 相容請求、提供者專用協定, 或直接呼叫推論執行環境。

請求會經過內容傳遞網路、Web 應用程式防火牆、負載平衡器和 API 閘道等一般 Web 基礎設施。 這些層可以終止 TLS、拒絕過大的本文、套用請求速率限制,或將請求路由到聊天服務。

聊天服務會驗證使用者身分,並授權其存取對話。僅知道對話 ID 不得足以讀取或延伸其他使用者的對話。 它還會驗證父訊息、模型選擇、功能旗標、附件擁有權和帳戶配額。

記錄使用者訊息 #

應用程式通常會在開始高成本生成之前持久化使用者訊息。 簡化的資料模型可能包含:

iotscdwierCntleoeleanrectv_teeiedrdd_s_aamttoidoenl1icprcsciuamstfdoaootrdssotoinrlnaeesdaknveettarieteieneut_slunsrtnseGmts_hMs_tdeeau_eai_nsnsrstdaestaesitra_gaaoagmesgn1teeoe_i_snioisdndage_id

先持久化訊息,可以在瀏覽器中斷連線或推論工作程序失敗時復原這一輪互動。 訊息或生成狀態可能會經過 pending、running、completed、failed 和 cancelled 等狀態。

既有對話不一定只是簡單清單。編輯舊訊息、重新生成回答,或在替代回答之間選擇, 都會建立一棵樹:

UserAAssiAsstsUaisnsettraBnFtDUserCAssUissetranGtE

parent_id 會告訴後端目前哪條路徑是作用中的。當使用者送出 F 時, 上下文通常應包含 A, B, C, D, F,而不包含兄弟回答 E 或其子孫 G。

有些系統會將分支複製成各自獨立的線性對話。無論採用哪種表示方式, 都必須在呼叫模型前解析為一段有序歷史。

重建對話 #

模型並不會自行知道瀏覽器正在顯示一個既有對話。大多數聊天推論端點都是無狀態的: 每次呼叫都必須提供該次呼叫所需的上下文。對話 ID 對應用程式有用, 但除非有狀態模型服務明確實作相關機制,否則它對模型沒有意義。

後端載入作用中的訊息路徑,並建立類似以下的邏輯輸入:

text
system: You are a helpful assistant.
developer: Follow the application's formatting and tool-use rules.
user: How does optimistic concurrency control work?
assistant: It detects conflicts rather than holding a lock...
user: Compare that approach with optimistic locking.

可見的逐字稿不一定就是模型實際接收的輸入。後端可能加入:

  • system 和 developer 指示;
  • 帳戶、工作區或自訂指示;
  • 目前日期和地區設定;
  • 模型特定的控制 token;
  • 工具定義和工具結果;
  • 擷取的文件或 Web 結果;
  • 附件內容或影像參照;
  • 較舊訊息的摘要;
  • 輸出格式限制;以及
  • 安全或產品政策指示。

相反地,某些可見資料可能不會送出。僅供 UI 使用的狀態訊息、錯誤橫幅、引用、反應按鈕和隱藏分支, 是應用程式狀態,而不是對話上下文。

配合上下文視窗 #

每個模型都有以 token 計量的最大上下文。輸入 token、生成的輸出 token,以及有時存在的模型特定推理 token, 會共同競爭這項預算。資料庫中的完整歷史不能無限追加。

上下文管理器首先使用與選取模型相關的 tokenizer 估算或計算 token 數量, 接著為回答保留空間:

PrMRMoaeatxtxoiaicmimounulmemdoCOroIunntMtppoeuudxttetlTTWooOikkvneeednnrosshwead

如果輸入過大,應用程式可以:

  • 捨棄最早的對話輪次;
  • 保留最初的指示和最近的輪次;
  • 摘要較舊的輪次並加入摘要;
  • 只擷取與新訊息相關的歷史;
  • 減少擷取的文件或工具描述;
  • 拒絕請求並要求使用者縮短輸入;或
  • 將這一輪路由至具有更大上下文視窗的模型。

直接刪除舊訊息可能會破壞「將它與第一個設計比較」這類參照。 摘要可以節省 token,但可能遺失精確措辭、程式碼、數字、不確定性或歸屬資訊。 因此,上下文管理是應用程式策略,而不只是 tokenizer 操作。

附件需要獨立處理。文字檔案可能會被解析、切分、建立索引,或直接插入。 影像可能會成為多模態模型的編碼影像輸入。大型文件集合通常會先進行搜尋, 使只有相關摘錄進入模型上下文。

審核、限制與排程 #

推論前,一個或多個服務可能會評估請求。視產品而定,這些檢查可能包括:

  • 濫用與內容分類;
  • 針對外部內容的提示注入防禦;
  • 附件惡意軟體掃描;
  • 每位使用者和每個組織的配額;
  • token 與支出限制;
  • 區域資料處理限制;
  • 模型可用性與使用資格;以及
  • 並行或速率限制。

策略可以在提示建構前、提示建構後、輸出串流期間,或三個階段全部執行。 提早拒絕可避免浪費推論容量,而輸出檢查能捕捉直到生成時才出現的內容。

通過驗證的請求會進入排程器或模型閘道。它會依據可用性、延遲、成本、硬體容量和租戶策略, 選擇提供者、區域、模型部署與推論工作程序。託管產品可能會將同一個對話中的不同輪次路由到不同的實體機器。

排程器可以批次處理不相關使用者的請求,讓 GPU 有效率地處理它們。 連續批次處理會在其他序列完成時加入或移除序列,而不是等待整個固定批次完成。 隔離仍然必須維持,以防止一個請求觀察到另一個請求的 token 或資料。

將訊息轉換為模型輸入 #

聊天服務通常會將一輪互動表示為結構化資料:

json
[
  {
    "role": "system",
    "content": "You are a helpful assistant."
  },
  {
    "role": "user",
    "content": "Why is the sky blue?"
  }
]

Transformer 無法直接消費 JSON 物件、角色名稱或 JavaScript 字串。 在推論前,服務必須將這個結構轉換成特定模型所需的整數 token 序列:

SSSItepnrretuicecaigtlaeuilrrzeeCTddoonkMTteeernsxostlIaDgAaTTsepnoospdkkleeynnsiCzheatTemplate

聊天範本 #

聊天範本定義角色、內容部分、工具呼叫和輪次邊界的序列化方式。 簡化的類 Jinja 範本可能如下:

jinja
{{ bos_token }}
{% for message in messages %}
<|{{ message.role }}|>
{{ message.content }}
<|end|>
{% endfor %}
{% if add_generation_prompt %}
<|assistant|>
{% endif %}

將它套用到上面的 JSON,可能產生:

text
<|begin|><|system|>
You are a helpful assistant.
<|end|><|user|>
Why is the sky blue?
<|end|><|assistant|>

這是用於說明的範例,不是特定商業模型的範本。實際範本在特殊 token、空白、角色順序、多模態部分、 工具呼叫編碼方式,以及生成提示位置方面都可能不同。有些範本使用明確的角色 token; 其他範本使用標頭或指令分隔符。tokenizer 必須識別範本使用的任何特殊 token。

最後的 assistant 標記會告訴模型,下一個 token 應屬於 assistant。 解碼期間輸出的結束標記可以告訴執行環境這一輪已完成。 即使所有可見文字都存在,使用錯誤的範本仍可能顯著降低行為品質, 因為模型經過訓練後會將自己的分隔符與特定角色和邊界建立關聯。

聊天範本與供應商回應結構 #

聊天範本本身無法解釋為何同一供應商的每個回應都有相同的 JSON 形狀。 這些結構位於不同層次:

VVMaMGTPVeeonoeeaenndddnxrnddeeetsdoolTlreorr-oaorrSkTtrAGpeoeAPaenkdSPItcietIeiznTrRwfeouReairIkceqycDetsusnupeCroshIentaDdsts-eOTuetmppultate

供應商的 API 契約定義訊息角色、內容、完成原因、使用量、引用或工具呼叫等穩定欄位。 閘道可以在將請求路由至多個內部範本和 tokenizer 不同的模型版本時,仍然公開相同的契約。 它會將通用請求轉換成所選模型的格式,再將結果包裝回通用回應格式。

以下幾種不同類型的結構很容易混淆:

結構限制內容由誰強制執行
API request schema允許的請求欄位、型別與選項API 閘道
API response schema回應封套、內容部分、使用量與錯誤供應商後端
Tool input schema模型可以向工具要求的引數模型執行環境與主機
Structured-output schema要求 assistant 產生的 JSON 內容解碼器、驗證器或重試迴圈
Browser event schema開始、增量、引用、狀態與結束事件聊天後端與 Web UI

這些結構通常使用 JSON Schema,或類似 JSON Schema 的供應商特定子集描述。 例如,應用程式可以要求 assistant 內容符合:

json
{
  "type": "object",
  "properties": {
    "answer": {
      "type": "string"
    },
    "confidence": {
      "type": "number",
      "minimum": 0,
      "maximum": 1
    }
  },
  "required": ["answer", "confidence"],
  "additionalProperties": false
}

支援範圍不一定與完整發布的 JSON Schema 草案相同。 提供者可能只接受特定關鍵字,要求每個屬性都列在 required 中,限制遞迴或結構大小, 或拒絕不支援的組合。應用程式的 SDK 可以在送出前驗證請求,但伺服器仍然是最終權威。

API 回應封套是有效 JSON,並不代表 assistant 的文字也是 JSON。 一般回應可以有由結構定義的外層物件,而其 content 欄位包含任意 Markdown。 反過來,結構化輸出模式會將該內容限制為物件或其他 JSON 值。 工具呼叫是另一個結構化通道:模型提出符合工具結構的引數,但主機仍必須在執行前解析並驗證它們。

Web UI 還可以在上層加入另一個專有協定。ChatGPT、Gemini 和 Claude 不必使用與其公開 API 相同的內部請求格式, 它們的瀏覽器事件串流也可能包含 UI 專用資料。單一產品的回應仍然看起來一致, 是因為其前端與後端共同遵循一個應用程式結構。原始 Transformer 產生的是 token 機率, 而不是供應商回應物件。

結構化輸出也有多種實作方式。模型可能產生一般文字,再由閘道解析; 產生特殊的工具呼叫 token;或在解碼期間受到約束,以符合 JSON 文法。 無論採用哪種方式,閘道都會在回傳用戶端前驗證並包裝結果。

供應商的回答為何具有可辨識的形式 #

JSON 回應結構可以解釋機器可讀的封套,但無法解釋 assistant 訊息內反覆出現的文字模式。 某個產品的回答可能會反覆使用先摘要後列點的結構、粗體標籤、注意事項, 或以「如果你願意,我也可以……」結尾。這種風格可能來自多個層次:

  • 預訓練、指令微調和偏好最佳化中學到的行為;
  • 設定語氣和格式的隱藏 system 或 developer 指示;
  • 產品特定的自訂指示或對話記憶;
  • 提示中提供的範例;
  • 詳細程度和 Markdown 使用方式等模型預設值;以及
  • 加入引用、通知或其他 UI 元素的後處理。

聊天範本提供角色和邊界標記,但通常不包含每個回答的文字藍圖。 範本可以實質上表示「assistant 輸出從這裡開始」;system 指示可以表示「以提供有用的下一步結束」; 而訓練可以使這種行為即使在目前提示中沒有該句話時仍容易出現。

反覆出現的措辭不是協定保證。ChatGPT 不必在每個回答結尾都提供摘要建議, Gemini 或 Claude 也沒有固定的文字結尾。文字會隨選取的模型、system 指示、使用者請求、 對話歷史、產品實驗和取樣方式而變化。需要保證結構的應用程式應要求結構受限的輸出, 或驗證明確格式,而不是解析供應商慣用的散文形式。

tokenizer 的實作方式 #

tokenizer 由演算法與模型特定資料組成。常見演算法包括 byte-pair encoding(BPE)、WordPiece、Unigram/SentencePiece, 以及位元組層級的變體。資料通常定義:

  • 將 token 位元組字串或片段對映至整數 ID 的詞彙;
  • 合併規則、分數或分段模型;
  • 正規化和預先分詞規則;
  • 特殊 token 及其 ID;以及
  • 加入模型控制 token 的後處理規則。

它不一定是單一可執行二進位檔。在瀏覽器或 SDK 中,它可以是 JavaScript、Python、Rust、Java 或原生函式庫。 推論伺服器通常會將快速的 Rust 或 C++ 實作連結到伺服器程序中。 另一種架構會在 API 工作程序或專用服務中執行 tokenization,再將 token ID 傳送給 GPU 工作程序。 命令列 tokenizer 執行檔也是可行的,但每次請求都啟動程序會增加可避免的額外成本。

開放模型儲存庫通常會將 tokenizer 資料與模型放在一起,使用如下檔案:

檔案常見內容
tokenizer.json完整序列化的 tokenizer 管線
vocab.json and merges.txtBPE 詞彙與合併規則
tokenizer.modelSentencePiece 模型
tokenizer_config.json執行階段選項與聊天範本中繼資料
special_tokens_map.json命名特殊 token 的指派

名稱和配置會因框架而異。託管供應商可能會將這些成果儲存在內部模型登錄檔、物件儲存、 容器映像或本機工作程序快取中。部署會將 tokenizer 修訂版固定至模型修訂版。 這種配對是模型契約的一部分:如果保留相同權重卻變更對映,整數 ID 將會指向錯誤的學習嵌入。

應用程式和模型服務也必須在計算限制或計費所需的 token 數量時保持一致。 用相似但不同的 tokenizer 進行的用戶端估算,可能與提供者實際處理的 token 不一致。

具體的 token ID 範例 #

對公開的 GPT-2 位元組層級 BPE tokenizer 而言,下列精確文字:

text
Hello, world!

會編碼為:

text
text pieces: ["Hello", ",", " world", "!"]
token IDs:   [15496, 11, 995, 0]

開頭的空白屬於 " world" token。這是 token 邊界不等於單字邊界的原因之一。 另一個 tokenizer 可能會為相同文字指派完全不同的序列。大小寫與空白也可能改變結果:

text
"Hello"  != "hello"
"world"  != " world"

這些不等式表示「可能被 tokenization 為不同 ID」,不表示這些單字一定會是單一 token。 程式碼、不常見的名稱、拼字錯誤和某些書寫系統常常會被拆成更多片段。 現代具備位元組感知能力的 tokenizer 可以表示任意輸入,即使不存在方便的完整單字 token。

實際的聊天請求還會為序列化 system 訊息、角色分隔符、輪次分隔符和 assistant 生成標記加入額外 ID。 token ID 只有搭配指定的 tokenizer 和修訂版才具有意義,因此文章或日誌不應在未標示該對映時呈現 ID 序列。

影像與音訊通常不會通過文字 tokenizer。獨立的編碼器或 tokenizer 會將它們轉換成影像區塊、 音訊特徵或模態特定 token,再與文字表示結合。

拼字錯誤會發生什麼事 #

tokenizer 通常不會更正拼字。它會忠實編碼接收到的位元組或字元, 因此拼字錯誤可能成為不常見的 token 或多個較小的 token。 拼錯的單字仍然可以由子詞或位元組片段表示。 語言模型可能根據上下文推斷預期單字,但那是學習到的預測,而不是 tokenizer 的更正,且不受保證。

拼字錯誤可能在其他階段被變更:

  • 瀏覽器的拼字檢查器可以在送出前修改編輯器內容;
  • 應用程式可以執行明確的拼字或查詢重寫步驟;
  • 檢索系統可以搜尋拼字變體或使用語意嵌入;
  • 擷取管線可以對文件套用 OCR 更正或正規化;以及
  • 可以指示模型默默解讀可能的錯誤,或明確更正錯誤。

即使應用程式為搜尋建立正規化副本,也應保留原始使用者訊息和來源文件。 默默重寫可能改變名稱、原始碼、識別碼、引用文字、醫學術語或使用者實際表達的含義。 檢索系統通常會同時保留原始欄位和正規化欄位:正規化形式改善比對, 原始形式則顯示給使用者並用於精確引用。任何生成的更正都應保留來源; 當歧義很重要時,應要求澄清,而不是自行捏造替代內容。

Prefill:讀取輸入 #

對典型的 decoder-only 聊天模型而言,prefill 會在套用聊天範本和 tokenization 後開始。 主要輸入是完整的輸入 token ID 序列:

text
token IDs: [1, 32001, 887, 526, 263, 8444, 20255, 13, 32002, ...]
positions: [0,     1,   2,   3,   4,    5,     6,  7,     8, ...]

執行環境還會提供批次中繼資料、序列長度、位置資訊,以及因果注意力遮罩或等效的最佳化中繼資料。 在批次中,較短的序列可能會加上填充或打包在一起。上面的確切 token ID 僅為說明, 因為 ID 只有對單一 tokenizer 修訂版才有意義。

模型首先從學習到的嵌入表查找每個 token ID,產生向量而不是單字:

TSDESDPSDohtmhtohtkaybaysayeppeppippneedeetee-::d::i::IioD[in[Bn[BSngSF-SFV]t,1A,1e3M6w6c2aH/aH/tt]Fr]FoorPePrrEi1eX[N1mx6l[SOM6ib/ei,Ta/neEFm]tFtdPeHFrP6d3n=]Ei34i2tAx2n-E+Tgw[UX-it[RTsoSEaek,beClnnHOeu_]NmiCRe]AorTwi+Ec[NLaPSAol[,ToESDPSDiIkmhtohta]HOubaysayd]Npeppippddeeteeid::i::tioin[Fn[BogVPSFn,T,1Toa6aHrbH/b]l]FlqePeu1aP6n/tFiPz3e2d

其中:

  • S 是序列長度,也就是輸入 token 位置的數量;
  • V 是詞彙大小,也就是可以索引該表的 token ID 數量;以及
  • H 是模型的隱藏維度,也就是每個 token 嵌入中的學習標量元件數量。

因此,token ID 輸入是形狀為 [S] 的一維整數向量。 嵌入表是學習到的浮點矩陣,每個詞彙項目有一列。 查找所有 S 個 ID 會選取 S 列,產生形狀為 [S, H] 的浮點矩陣。 加入批次維度 B 後,相應形狀為 [B, S]、[V, H] 和 [B, S, H]; 相同的嵌入表會在整個批次中共用。 圖中顯示的資料型別是常見範例,而不是必要條件。 量化嵌入表通常會在使用選取的列時反量化,或轉換為模型的計算資料型別。

位置資訊會被加入或套用,使相同 token 位於不同位置時能夠有不同表現。 在上述加法設計中,E 和 P 具有相同形狀,並逐元件相加。 P 不會被附加為額外特徵欄位:串接會產生 [S, 2H],而數值加法會保留 [S, H]。

例如,假設 token ID 995 出現在位置 2 和 9。 嵌入查找最初會在兩個位置都回傳相同的學習向量 E[995]。 使用學習式絕對位置嵌入的模型,可以將下列不同向量傳遞至第一層:

text
position 2: E[995] + P[2]
position 9: E[995] + P[9]

這也能區分 "dog bites man" 與 "man bites dog"。 兩個序列包含相同的三個 token 嵌入,但這些嵌入位於不同位置。 沒有位置資訊時,self-attention 就沒有直接表示該順序的方式。

並非每個模型都會直接相加位置向量:

  • 對於旋轉位置嵌入,也就是 RoPE,執行環境會依據位置旋轉每個 query 和 key 的元件。 基本 token 嵌入可以保持不變,但注意力計算會對絕對位置與相對位置敏感。
  • 其他架構會使用相對位置偏差或相關機制。

具備位置資訊的向量會通過每個 Transformer 層。在簡化的 decoder 層中:

QPNFurCoeeoarerjuRmdRyesea-e,caslFsHNtliioiioKidzrddreoSuawudmyneataaea,sllirlnlfodia-CnCSznAoNotadtnaenattnntntiVeedweeoancocsnlttrtuiikieooonnn

因果規則允許位置 i 的表示只注意至 i 為止的位置,而不能注意後續輸入位置。

  • 雖然這種相依性具有因果性,GPU 仍然可以透過大型平行矩陣運算計算所有輸入位置的 prefill 表示。
  • 這與解碼不同:解碼必須先取得新選取的 token,才能開始下一步。

對每一層和輸入位置而言,注意力會計算 key 向量和 value 向量。 執行環境會將這些向量保留在 KV cache 中:

L01NayerKKKKe[[[yaaasllllllpppooosssiiitttKiiiVooonnnCsssa]]]cheVVVVa[[[laaaulllelllspppooosssiiitttiiiooonnnsss]]]

推論後通常不會保留 prefill 產生的所有暫時啟用值。 KV cache 是重要的持久化結果,因為後續每個解碼步驟都可以注意既有序列, 而不必為所有較早的 token 重新計算 key 和 value。

在最後一層,最後一個輸入位置的隱藏狀態會經過語言模型輸出頭投影, 為詞彙中的每個 token 產生一個分數,即 logit。 在懲罰、限制和取樣轉換處理這些 logit 後,它們便定義第一個生成 token 的候選項。 因此,prefill 最終產生兩項結果:

  1. 輸入前綴的逐層 KV cache;以及
  2. 用於選取第一個輸出 token 的 logit。

選取第一個 token 後,執行環境會進入解碼。 每個解碼步驟都會將一個位置的 key 和 value 加入既有快取,並產生下一個 token 的 logit。 Encoder-decoder 模型會以不同方式劃分這項工作,但大多數現代文字聊天模型使用這裡所述的 decoder-only 模式。

prefill 的工作量會隨著輸入長度增加,且通常需要大量計算。 因此,即使回答本身很短,長對話也可能增加回答開始出現前的延遲。

前綴快取範例 #

假設應用程式傳送一份 10,000-token 的手冊,後面接著一個問題:

SystemInstSrhuacrteidonPsrefix1MR0ae,nq0uu0ae0ls-tTo1keTnokenIDs"HowdoIresetit?"AssistantMarker

在請求 1 期間,伺服器會計算整個序列的 KV 項目。 前綴快取可以保留穩定的 system 指示和手冊所對應的 KV 區塊。 後續請求可能是:

SystemInIsdternutcitciaolnsCache1MdR0ae,nPq0uru0aee0lfs-itTxo2keTnokenIDs"WhatdoeserroNrewE4S2ufmfeiaxn?"AssistantMarker

如果序列化後的位元組產生相同的前導 token ID,且部署使用相同的模型、tokenizer、範本與相關推論設定, 執行環境就可以接上已快取的 KV 區塊,只對新的後綴執行 prefill。 模型仍然會注意手冊;伺服器只是重用已經計算好的 key 和 value,並沒有省略文件。

生產環境中的快取通常會將前綴分成固定大小的 token 區塊,並將每個區塊與模型和租戶中繼資料一起雜湊。 因此,可重用長度可能會在最後一個相符的區塊邊界結束,而不是最後一個個別相符的 token。

如果開頭附近的 token 發生變更,快取通常無法提供協助:

CNaecwhedSSyysstteemmFirsMMtaannCuuhaaalXlngVVeeedrrssTiioooknnen78QQuueessttiioonn

第一個差異之後的所有 token 都具有不同的注意力上下文,因此無法直接拼接其快取狀態。 同樣地,即使可見問題未變更,修改空白、工具定義、隱藏指示、附件順序或聊天範本, 也可能改變 token 前綴。

前綴快取不同於快取最終回答。它會重用內部注意力狀態,但仍然執行解碼, 因此取樣後的回答可能不同。 它也不同於一般解碼期間使用的每請求 KV cache: 前綴快取會跨請求保留可重用的 KV 狀態,而一般 KV cache 只需要在一次作用中的生成期間存在。

提供者會訂定容量、到期、最小前綴長度、隔離和計費規則。 有些會自動填充快取;其他則要求明確的快取物件或標記。 僅僅重用對話 ID 並不保證命中,且快取鍵必須防止一個租戶取得另一個租戶的私有前綴狀態。

解碼:產生回答 #

prefill 後,模型會預測下一個 token 的機率分布。 解碼策略選取一個 token,將它附加到序列,然後執行另一個模型步驟:

CaonndteCxatcheNPerxotb-aTbAoipklpeientnidesTokenSelectToken

迴圈會持續,直到模型:

  • 輸出停止 token 或停止序列;
  • 達到輸出 token 上限;
  • 要求使用工具;
  • 被策略系統停止;
  • 遇到錯誤;或
  • 收到取消訊號。

選取可以是貪婪解碼,也可以使用 temperature 和 top-p 等取樣控制。 產品也可能套用重複懲罰、受限解碼、文法強制、推測解碼或供應商特定的推理機制。

模型通常不會在一次操作中產生整個段落,而是逐步產生 token。 token 可以是完整單字、單字的一部分、標點符號或空白。 由於字元可能跨越 token 位元組邊界,伺服器必須謹慎地將 token ID 解碼回文字。

串流傳回瀏覽器 #

推論工作程序會將 token 或文字增量輸出至模型閘道。 閘道會將它們轉換為應用程式事件,並透過聊天服務轉送到瀏覽器。

常見傳輸方式包括:

  • 串流 fetch() 回應;
  • Server-Sent Events(SSE);
  • WebSocket 訊息;或
  • 由後端消費並透過另一個協定轉送的提供者串流。

類似 SSE 的串流可能包含:

text
event: message_start
data: {"message_id":"msg_457"}

event: content_delta
data: {"text":"Optimistic locking"}

event: content_delta
data: {"text":" is a common form"}

event: content_delta
data: {"text":" of optimistic concurrency control."}

event: message_end
data: {"finish_reason":"stop"}

傳輸內容可能遠不只是文字,還包括推理摘要、引用、工具呼叫、使用量計數器、警告或心跳事件。 序號有助於在重新連線後偵測重複或遺失事件。

串流可以讓使用者在生成結束前開始閱讀,改善感知延遲。 但它不一定能降低整體生成時間。重要的延遲測量包括:

測量意義
Request latency後端接受這一輪互動前所需的時間
Queue time等待推論容量的時間
Time to first token從送出到第一個輸出抵達的時間
Inter-token latency連續輸出 token 之間的延遲
Total latency生成完成前所需的時間

即使解碼已經開始,多個緩衝區仍可能延遲可見輸出:推論伺服器、反向代理、 壓縮層、內容過濾器和瀏覽器串流解析器。串流端點通常會停用代理緩衝,並刻意刷新小型事件。

渲染部分回答 #

瀏覽器讀取事件並更新待處理的 assistant 訊息。 每個微小 token 都更新一次文件物件模型可能成本很高,因此 UI 通常會累積增量, 並以動畫影格或短計時器間隔進行渲染。

Markdown 會帶來另一項問題:部分串流通常不是有效的 Markdown。 程式碼圍欄、連結、表格或強調標記可能尚未閉合。 渲染器必須容忍不完整輸入,並在更多文字抵達時重新渲染。 語法突顯和數學排版可能延後或節流,因為重複處理整個回答會造成明顯卡頓。

瀏覽器應將生成的 HTML 視為不受信任。 Markdown 渲染器必須跳脫或清理危險輸出,避免模型生成的腳本、事件處理器屬性或不安全 URL 在頁面中執行。

自動捲動應是有條件的。 如果使用者已接近底部,檢視區可以跟隨回答; 但不應反覆將使用者從正在閱讀的舊訊息拉走。

工具呼叫與多步驟互動 #

回應不一定是使用者可見的散文。 如果提示中提供了工具,模型可能會輸出結構化請求:

json
{
  "name": "get_order",
  "arguments": {
    "order_id": "42"
  }
}

執行工具的是應用程式,而不是模型。典型的代理迴圈如下:

VAAaupltpiheMdonISoardntdtivreezRoeleEekaNxsemRaoeuemrclMFqeutoiuAtdneaseteasnkolltdTsfoCAAAooognTrrlnasogtiwouAenelmpxreptnrtosval

因此,一輪可見的使用者互動可能包含多次模型呼叫,以及對資料庫、搜尋引擎、程式碼直譯器或遠端 API 的多次呼叫。 UI 可能會顯示「搜尋中」、「讀取文件」或「等待核准」等中間狀態。

工具結果會消耗上下文 token,而且本身也可能不受信任。 主機必須強制執行權限、逾時、結果大小限制和副作用控制。 破壞性操作不會只因為模型提出要求就變得安全。

檢索遵循相關模式。 應用程式可能將使用者問題改寫為搜尋查詢、擷取候選文件、重新排序, 將選取的摘錄插入上下文,然後才呼叫回答模型。 有些系統會在第一次模型呼叫前進行檢索; 其他系統則讓模型在這一輪互動中選擇搜尋工具。

完成並持久化回答 #

解碼完成時,模型服務會發出完成原因和最終中繼資料。 應用程式會將所有增量組合成標準的 assistant 訊息,並記錄:

  • 生成內容和結構化內容部分;
  • 模型和模型版本識別碼;
  • 輸入、輸出、快取或推理 token 使用量;
  • 工具呼叫和工具結果;
  • 引用或來源參照;
  • 完成原因;
  • 延遲和追蹤識別碼;以及
  • 策略決策或錯誤詳細資訊。

最終資料庫寫入應具備冪等性。 重試和重複的串流事件不得建立多個 assistant 訊息。 有些系統會在生成期間持久化部分檢查點,以便仍能顯示中斷的回答; 其他系統只儲存最終組合的訊息。

伺服器只有在知道結果後才會傳送終端事件。 瀏覽器接著會將 assistant 訊息標記為完成、停止活動指示器、 啟用複製或重新生成等操作,並將樂觀 ID 與伺服器發出的 ID 進行協調。

回答出現後,背景工作仍可能繼續。 應用程式可能生成對話標題、更新搜尋索引、計算安全性分析、擷取長期記憶、記錄計費或輸出品質指標。 除非其結果是這一輪互動所必需,否則這些工作不應延長使用者可見的延遲。

取消、重試與重新生成 #

按下 Stop 通常會取消生成,而不是反轉已生成的文字。 瀏覽器會傳送取消請求或關閉串流;後端則將取消訊號傳播給排隊工作、工具呼叫和推論執行環境。 提供者可能已經生成並正在傳輸額外 token,計費語義也各不相同。

部分回答可以保留,並使用 cancelled 完成原因。 如果它會保留在後續上下文中,應用程式應將它與正常結束的回答區分開來。

網路中斷不等同於取消。 瀏覽器可能消失,但伺服器端的生成仍會繼續。 應用程式可以選擇以下策略:

  • 用戶端中斷連線時立即取消工作;
  • 讓生成完成並持久化回答;
  • 保留可恢復的事件串流,以便重新連線;或
  • 只持續到短暫的中斷寬限期結束。

重試送出時應重用冪等鍵,以免不確定的網路結果導致使用者訊息被附加兩次。

Regenerate 通常會在同一則使用者訊息下建立新的 assistant 兄弟節點。 它可以重用相同提示,同時變更隨機取樣、模型或選項。 它不必覆寫先前回答。 Edit and resubmit 會從編輯後訊息的父節點建立分支, 因為所有後續訊息都是以原始措辭為條件生成的。

不同層級的故障 #

UI 不應將每個故障都簡化為「模型失敗」。 不同階段有不同的復原行為:

階段範例一般處理方式
Browser離線或上傳中斷保留草稿並重試
Gateway驗證或速率限制重新驗證或等待
Conversation service無效分支或權限錯誤不執行推論,直接拒絕
Context assembly輸入超過模型限制安全裁剪或要求較短輸入
Scheduler沒有模型容量排隊、重試或改道
Inference工作程序崩潰或逾時將生成標記為失敗,謹慎重試
Tool無效引數或遠端錯誤向迴圈回傳明確的工具錯誤
Stream回答中途連線中斷恢復、保留部分輸出或重新連線
Persistence最終寫入衝突使用穩定 ID 和冪等性進行協調

只有當操作具備冪等性或其影響已知時,自動重試才安全。 重試文字生成可能產生不同回答。 重試會寄送電子郵件、下訂單或刪除檔案的工具,可能重複真實世界的副作用。

錯誤應與 assistant 散文分開表示。 生成的「發生錯誤」句子不等同於傳輸或工具故障,也不應控制應用程式狀態。

隱私、安全性與可觀測性 #

請求可能包含比使用者剛輸入的文字更多的內容。 它可能包括先前訊息、上傳檔案、擷取的私人文件、工具輸出和隱藏指示。 路徑上的每個服務都應只接收其需要的資料。

重要控制措施包括:

  • 傳輸中和靜態儲存中的加密;
  • 每次對話和附件查找的授權;
  • 儲存、快取、佇列、日誌和推論中的租戶隔離;
  • 對遙測中的敏感提示資料進行遮蔽或排除;
  • 保留與刪除政策;
  • 可稽核的工具執行與使用者核准;
  • 瀏覽器中的輸出清理;以及
  • 防止擷取內容中的嵌入式指示發揮作用。

可觀測性仍然重要。 分散式追蹤可以將瀏覽器的一輪互動連接到閘道、檢索、工具和推論 span。 有用的指標包括佇列時間、首次 token 時間、每秒 token 數、上下文大小、快取命中率、 完成原因、取消率和故障階段。 記錄完整提示對除錯很方便,但有風險;中繼資料、雜湊、取樣後的已遮蔽內容,以及受存取控制的診斷儲存, 是更安全的預設方式。

基本心智模型 #

既有 LLM 聊天最好理解為一個以有狀態應用程式包覆一個或多個有界模型呼叫的系統:

------OSSEESwtenxtnolfersreoceSecruatcstctmaoseestnmssevecafesoitnursndodlsateoagenlpAtextsepistirpotslnyiisscataanstndidropenbosrluailnctcyshes---StRtCrRtaeooeeotckmptoeeepeullMinuareovttnrsdeseesesesesdqlqltuooutyeerInehxsnenettevcsxocenopcoerlanxitstscittiortrtnuoulckcytetenucdraecdhed

對話表面上的連續性,是由應用程式重建並重新傳送上下文所產生的。 看似打字的效果,是由串流傳送增量解碼所產生的。 看似能夠搜尋、計算或採取行動的能力,則是由應用程式控制模型周圍的迴圈所產生。

這種分離解釋了許多看似令人意外的行為:舊細節在超出選定上下文後消失, 長對話需要更久才開始,重新生成會產生不同分支,被停止的回答可能仍部分可見, 而且除非周圍的應用程式明確提供該資訊或能力,模型無法存取對話、資料庫或網站。

評論

在 GitHub 上閱讀或評論(發表評論需要登入)