チャットインターフェースによって、大規模言語モデルとの対話は単純に見えます。 メッセージを入力し、Enterキーを押すと、回答が表示されます。その背後では、 ブラウザ、アプリケーションサーバー、データベース、ポリシーシステム、 モデルゲートウェイ、推論ワーカー、さらにツールや検索サービスが関わる 分散リクエストパイプラインが動作しています。
既存の会話では、新しいテキストはモデル入力の一部にすぎません。アプリケーションは 会話を十分に再構成し、非表示の指示やその他のコンテキストと結合し、すべてを有限の コンテキストウィンドウに収め、推論を実行し、結果をストリーミングし、新しい状態を 永続化する必要があります。
代表的な経路は次のとおりです。
具体的な製品では、これらの段階を組み合わせたり、順序を変えたり、一部を省略したりします。 モデルAPIは別サービスではなく、アプリケーション内にローカルに存在する場合もあります。 一般原則は変わりません。Webアプリケーションが会話を所有し、通常の言語モデル呼び出しが、 有限長の入力トークン列を出力トークン列へ変換します。
メッセージを送信する前 #
ユーザーが入力すると、ブラウザは通常、テキストエリアを更新し、下書きをコンポーネント状態に保持します。 次の処理も行う場合があります。
- 入力欄のサイズを変更する
- 下書きをローカルストレージに保存する
- メッセージ送信前に添付ファイルをアップロードしてスキャンする
- スラッシュコマンドやメンションを認識する
- 入力が製品の制限を超えるか推定する
- 送信ボタンを有効化または無効化する
通常、この処理に言語モデルは関与しません。オートコンプリート、スペルチェック、入力メソッドエディターは、 独自のブラウザイベントやオペレーティングシステムイベントを生成できますが、チャットアプリケーションが 最終的なテキストを受け取ることに変わりはありません。
Enterキーを押す、Sendをクリックする、またはモバイル版の同等操作を行うと、送信ハンドラーが起動します。 作成欄は次のメッセージのためにすぐクリアされる可能性があるため、ハンドラーは現在の下書きの不変なコピーを取得します。 また、楽観的UI、ネットワークリクエスト、最終的なデータベースレコードを関連付けるための識別子も作成します。
一般的な識別子には次のものがあります。
| 識別子 | 用途 |
|---|---|
| Conversation ID | 既存の会話を選択する |
| Message ID | このユーザーメッセージを識別する |
| Parent message ID | ブランチまたは前任メッセージを選択する |
| Request or turn ID | 1回の生成試行を関連付ける |
| Idempotency key | リトライによってメッセージが二重作成されるのを防ぐ |
| Client trace ID | ブラウザとサーバーのテレメトリを接続する |
ブラウザは通常、ユーザーメッセージを直ちにトランスクリプトへ追加します。 これは楽観的更新です。サーバーが確認する前に、UIが期待される状態を表示します。 送信に失敗した場合、メッセージを失敗としてマークし、消去する代わりにリトライを提示できます。
ターンの送信 #
ブラウザは、会話識別子、新しいメッセージ、添付ファイル参照、選択したモデル、製品固有のオプションを含む
認証済みHTTPSリクエストを送信します。通常のPOSTリクエストを使う場合があります。
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を知っているだけで、 別ユーザーの会話を読み取ったり拡張したりできてはなりません。さらに、親メッセージ、モデル選択、 フィーチャーフラグ、添付ファイルの所有権、アカウント割り当て量も検証します。
ユーザーメッセージの記録 #
アプリケーションは通常、高コストな生成を開始する前にユーザーメッセージを永続化します。 簡略化したデータモデルには、次のようなものがあります。
先にメッセージを永続化すると、ブラウザが切断された場合や推論ワーカーが失敗した場合でも、
ターンを復旧できます。メッセージまたは生成の状態は、pending、running、completed、
failed、cancelledなどを経て遷移します。
既存の会話は、常に単純なリストとは限りません。古いメッセージの編集、回答の再生成、代替回答からの選択によって、 ツリーが形成されます。
parent_idは、どの経路がアクティブかをバックエンドに伝えます。ユーザーがFを送信した場合、
通常、コンテキストにはA, B, C, D, Fが含まれ、兄弟回答Eやその子孫Gは含まれません。
システムによっては、ブランチを別々の線形会話にコピーします。どちらの表現であっても、モデル呼び出し前に 1つの順序付き履歴へ解決する必要があります。
会話の再構成 #
モデルは、ブラウザが既存の会話を表示していることを本質的には知りません。多くのチャット推論エンドポイントはステートレスであり、 各呼び出しに必要なコンテキストを毎回渡す必要があります。会話IDはアプリケーションにとって有用ですが、 ステートフルなモデルサービスが明示的に実装しない限り、モデルにとって意味はありません。
バックエンドはアクティブなメッセージ経路を読み込み、次のような論理入力を構築します。
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指示
- アカウント、ワークスペース、カスタム指示
- 現在の日付とロケール
- モデル固有の制御トークン
- ツール定義とツール結果
- 取得したドキュメントまたはWeb検索結果
- 添付ファイルの内容または画像参照
- 古いメッセージの要約
- 出力形式の制約
- 安全性または製品ポリシーに関する指示
逆に、表示されているデータの一部は送信されないことがあります。UI専用の状態メッセージ、エラーバナー、 引用、リアクションボタン、非表示ブランチは、会話コンテキストではなくアプリケーション状態です。
コンテキストウィンドウへの適合 #
すべてのモデルには、トークンで測定される最大コンテキストがあります。入力トークン、生成された出力トークン、 さらにモデル固有の推論トークンが、その予算を競合する場合があります。データベース内の完全な履歴を無期限に追加することはできません。
コンテキストマネージャーは、まず選択したモデルに対応するトークナイザーを使ってトークン数を推定または計算します。 その後、回答用の領域を確保します。
入力が大きすぎる場合、アプリケーションは次の処理を行えます。
- 最も古い会話ターンを削除する
- 最初の指示と最新のターンを保持する
- 古いターンを要約して要約を含める
- 新しいメッセージに関連する履歴だけを取得する
- 取得したドキュメントやツール説明を減らす
- リクエストを拒否し、短くするようユーザーに求める
- より大きなコンテキストウィンドウを持つモデルへターンをルーティングする
古いメッセージを単純に削除すると、「最初の設計と比較して」のような参照が壊れる可能性があります。 要約はトークンを節約しますが、正確な表現、コード、数値、不確実性、帰属情報を失う可能性があります。 したがってコンテキスト管理は、単なるトークナイザー操作ではなく、アプリケーションポリシーです。
添付ファイルには独自の処理が必要です。テキストファイルは解析、分割、インデックス化、または直接挿入される場合があります。 画像はマルチモーダルモデル用のエンコード済み画像入力になることがあります。大規模なドキュメントコレクションは、 通常、関連する抜粋だけをモデルコンテキストに入れるため、先に検索されます。
モデレーション、制限、スケジューリング #
推論前に、1つ以上のサービスがリクエストを評価する場合があります。製品によって、次のチェックが含まれます。
- 不正利用およびコンテンツ分類
- 外部コンテンツ周辺のプロンプトインジェクション防御
- 添付ファイルのマルウェアスキャン
- ユーザー単位および組織単位の割り当て量
- トークンおよび支出制限
- 地域ごとのデータ処理制限
- モデルの可用性および利用資格
- 同時実行数またはレート制限
ポリシーは、プロンプト構築前、プロンプト構築後、出力ストリーミング中、またはその3箇所すべてで適用できます。 早期に拒否すると推論容量の浪費を防げます。一方、出力チェックによって、生成されるまで存在しなかったコンテンツを検出できます。
受け入れられたリクエストは、スケジューラーまたはモデルゲートウェイに入ります。可用性、レイテンシー、コスト、 ハードウェア容量、テナントポリシーに基づき、プロバイダー、リージョン、モデルデプロイメント、推論ワーカーを選択します。 ホスト型製品では、同じ会話内の異なるターンを異なる物理マシンへルーティングする場合があります。
スケジューラーは、無関係なユーザーのリクエストをバッチ化し、GPUが効率的に処理できるようにします。 継続的バッチ処理では、固定バッチ全体の完了を待つのではなく、他のシーケンスが完了するとシーケンスを追加・削除します。 ただし、あるリクエストが別のリクエストのトークンやデータを観測できないよう、分離を維持する必要があります。
メッセージをモデル入力へ変換する #
チャットサービスは、通常、ターンを構造化データとして表現します。
[
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Why is the sky blue?"
}
]Transformerは、JSONオブジェクト、ロール名、JavaScript文字列を直接処理できません。推論前に、サービスはこの構造を、 対象モデルが期待する整数トークン列へ変換する必要があります。
チャットテンプレート #
チャットテンプレートは、ロール、コンテンツ部分、ツール呼び出し、ターン境界をどのようにシリアライズするかを定義します。 簡略化したJinja風テンプレートは次のようになります。
{{ bos_token }}
{% for message in messages %}
<|{{ message.role }}|>
{{ message.content }}
<|end|>
{% endfor %}
{% if add_generation_prompt %}
<|assistant|>
{% endif %}上記のJSONに適用すると、次のようになる可能性があります。
<|begin|><|system|>
You are a helpful assistant.
<|end|><|user|>
Why is the sky blue?
<|end|><|assistant|>これは特定の商用モデルのテンプレートではなく、説明用の例です。実際のテンプレートは、特殊トークン、 空白、ロール順序、マルチモーダル部分、ツール呼び出しのエンコーディング、生成プロンプトの位置が異なります。 明示的なロールトークンを使うものもあれば、ヘッダーや命令区切り文字を使うものもあります。 トークナイザーは、テンプレートが使用する特殊トークンを認識しなければなりません。
最後のassistantマーカーは、次のトークンがassistantに属することをモデルへ伝えます。デコード中に出力される終了マーカーは、 ランタイムにターンの完了を伝えます。表示されるすべてのテキストが存在していても、誤ったテンプレートを使うと動作が大幅に低下します。 モデルは、自身の区切り文字を特定のロールや境界と関連付けるよう訓練されているためです。
チャットテンプレートとベンダーのレスポンススキーマ #
チャットテンプレートだけでは、あるベンダーからのすべてのレスポンスが同じJSON形状になる理由は説明できません。 これらの構造は異なるレイヤーに存在します。
ベンダーのAPI契約は、メッセージロール、コンテンツ、終了理由、使用量、引用、ツール呼び出しなどの安定したフィールドを定義します。 ゲートウェイは、異なる内部テンプレートとトークナイザーを持つ複数のモデルバージョンへリクエストをルーティングしながら、 同じ契約を公開できます。共通リクエストを選択モデルの形式へ変換し、結果を共通レスポンス形式へ戻します。
異なる種類のスキーマは混同しやすいものです。
| スキーマ | 制約する内容 | 適用する主体 |
|---|---|---|
| API request schema | 許可されるリクエストフィールド、型、オプション | APIゲートウェイ |
| API response schema | レスポンスエンベロープ、コンテンツ部分、使用量、エラー | ベンダーバックエンド |
| Tool input schema | モデルがツールに要求できる引数 | モデルランタイムとホスト |
| Structured-output schema | assistantに要求するJSONコンテンツ | デコーダー、バリデーター、またはリトライループ |
| Browser event schema | 開始、差分、引用、状態、終了イベント | チャットバックエンドとWeb UI |
これらのスキーマは、JSON Schemaまたはそれに似たベンダー固有のサブセットで記述されることが一般的です。 たとえば、アプリケーションは次の形式に一致するassistantコンテンツを要求できます。
{
"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固有のデータを含めることもできます。それでも単一製品のレスポンスが一貫して見えるのは、 フロントエンドとバックエンドが1つのアプリケーションスキーマに合意しているためです。生のTransformerが生成するのはトークン確率であり、 ベンダーのレスポンスオブジェクトではありません。
構造化出力にも複数の実装方法があります。モデルが通常のテキストを出力し、ゲートウェイが解析する場合もあれば、 特殊なツール呼び出しトークンを生成する場合もあり、JSON文法に一致するようデコードを制約する場合もあります。 いずれの場合も、ゲートウェイは結果を検証してラップし、クライアントへ返します。
ベンダーの回答に認識可能な形がある理由 #
JSONレスポンススキーマは機械可読なエンベロープを説明しますが、assistantメッセージ内部に繰り返し現れる文章パターンは説明しません。 ある製品の回答が、要約に続く箇条書き、太字ラベル、注意事項、または「必要であれば、さらに…」のような結びを繰り返す場合があります。 そのスタイルは複数のレイヤーから生じます。
- 事前学習、指示チューニング、選好最適化で学習された振る舞い
- トーンや形式を設定する非表示のsystemまたはdeveloper指示
- 製品固有のカスタム指示または会話メモリ
- プロンプトに含められた例
- 詳細度やMarkdown使用などのモデルデフォルト
- 引用、通知、その他のUI要素を追加する後処理
チャットテンプレートはロールと境界のマーカーを提供しますが、通常、すべての回答の文章構成を含むわけではありません。 テンプレートは実質的に「assistant出力はここから始まる」と示し、system指示は「役立つ次のステップを提示して終了する」と指示し、 訓練によって、その文が現在のプロンプトに存在しなくても、その振る舞いが起こりやすくなる可能性があります。
繰り返される表現は、プロトコル上の保証ではありません。ChatGPTはすべての回答を要約の提案で終える必要はなく、 GeminiやClaudeにも必須の文章上の結びはありません。表現は、選択されたモデル、system指示、ユーザーリクエスト、会話履歴、 製品実験、サンプリングによって変化します。保証された構造が必要なアプリケーションは、ベンダー固有の定型文を解析するのではなく、 スキーマ制約付き出力を要求するか、明示的な形式を検証すべきです。
トークナイザーの実装方法 #
トークナイザーは、アルゴリズムとモデル固有のデータで構成されます。一般的なアルゴリズムには、byte-pair encoding(BPE)、 WordPiece、Unigram/SentencePiece、バイトレベルの派生方式があります。データには通常、次のものが含まれます。
- トークンのバイト文字列またはピースから整数IDへの語彙マッピング
- マージ規則、スコア、またはセグメンテーションモデル
- 正規化および事前トークン化の規則
- 特殊トークンとそのID
- モデル制御トークンを追加する後処理規則
単一の実行可能バイナリとは限りません。ブラウザやSDKでは、JavaScript、Python、Rust、Java、またはネイティブライブラリになります。 推論サーバーは、高速なRustまたはC++実装をサーバープロセスにリンクすることがよくあります。別の構成では、APIワーカーまたは専用サービスで トークン化を実行し、GPUワーカーへトークンIDを送信します。コマンドラインのトークナイザー実行ファイルも可能ですが、 リクエストごとにプロセスを起動すると、回避可能なオーバーヘッドが追加されます。
オープンモデルのリポジトリでは、次のようなファイルを使って、モデルの隣にトークナイザーデータを保存することが一般的です。
| ファイル | 一般的な内容 |
|---|---|
tokenizer.json | シリアライズされた完全なトークナイザーパイプライン |
vocab.json and merges.txt | BPE語彙とマージ規則 |
tokenizer.model | SentencePieceモデル |
tokenizer_config.json | ランタイムオプションとチャットテンプレートのメタデータ |
special_tokens_map.json | 名前付き特殊トークンの割り当て |
名前とレイアウトはフレームワークによって異なります。ホスト型ベンダーは、これらの成果物を内部モデルレジストリ、 オブジェクトストア、コンテナイメージ、またはローカルワーカーキャッシュに保管する場合があります。デプロイメントは、 トークナイザーのリビジョンをモデルのリビジョンに固定します。この組み合わせはモデル契約の一部です。 重みを同じままマッピングを変更すると、整数IDが誤った学習済み埋め込みを参照することになります。
アプリケーションとモデルサービスは、制限や課金のためにトークンを数える時点についても合意する必要があります。 類似しているが異なるトークナイザーでクライアント側が推定した値は、プロバイダーが実際に処理したトークン数と一致しない可能性があります。
具体的なトークンIDの例 #
公開されているGPT-2のバイトレベルBPEトークナイザーでは、次の正確なテキストは、
Hello, world!次のようにエンコードされます。
text pieces: ["Hello", ",", " world", "!"]
token IDs: [15496, 11, 995, 0]先頭の空白は" world"トークンに含まれます。これが、トークン境界と単語境界が一致しない理由の1つです。
別のトークナイザーでは、同じテキストにまったく異なる列を割り当てることがあります。大文字小文字や空白も結果を変えます。
"Hello" != "hello"
"world" != " world"これらの不等式は「異なるIDにトークン化される可能性がある」という意味であり、単語が常に単一トークンになるという意味ではありません。 コード、まれな名前、タイプミス、一部の書記体系では、より多くの断片に分割されることがあります。現代のバイト対応トークナイザーは、 都合のよい完全な単語トークンが存在しない場合でも、任意の入力を表現できます。
実際のチャットリクエストには、シリアライズされたsystemメッセージ、ロール区切り文字、ターン区切り文字、assistant生成マーカー用の 追加IDが含まれます。トークンIDは、指定されたトークナイザーとリビジョンとの組み合わせでのみ意味を持つため、 記事やログでは、その対応関係を示さずにID列を提示してはいけません。
画像や音声は、通常、テキストトークナイザーを通りません。別のエンコーダーまたはトークナイザーが、それらを画像パッチ、 音声特徴量、またはモダリティ固有トークンへ変換し、テキスト表現と結合します。
タイプミスの扱い #
トークナイザーは通常、スペルを修正しません。受け取ったバイトまたは文字を忠実にエンコードするため、 タイプミスはまれなトークンや複数の小さなトークンになる場合があります。誤記された単語も、サブワードやバイトの断片から表現できます。 言語モデルはコンテキストから意図した単語を推測できますが、それは学習された予測であり、トークナイザーによる修正ではなく、 保証もされません。
タイプミスは別の段階で変更される可能性があります。
- ブラウザのスペルチェッカーが送信前に作成欄を変更する
- アプリケーションが明示的なスペル修正またはクエリ書き換えを実行する
- 検索システムがスペルの変種を検索するか、意味埋め込みを使用する
- 取り込みパイプラインがドキュメントにOCR修正または正規化を適用する
- モデルに、誤りの可能性を黙って解釈するか、目に見える形で修正するよう指示する
アプリケーションは、検索用に正規化コピーを作成する場合でも、元のユーザーメッセージとソースドキュメントを保持すべきです。 黙って書き換えると、名前、ソースコード、識別子、引用テキスト、医学用語、ユーザーの本来の意味が変わる可能性があります。 検索システムは通常、未加工フィールドと正規化フィールドの両方を保持します。正規化形式はマッチングを改善し、 未加工形式はユーザーに表示して正確な引用に使用します。生成された修正には来歴を保持し、曖昧性が重要な場合は、 置換をでっち上げずに明確化を求めるべきです。
Prefill:入力の読み取り #
典型的なdecoder-onlyチャットモデルでは、prefillはチャットテンプレート適用とトークン化の後に開始します。 主入力は、入力トークンIDの完全な列です。
token IDs: [1, 32001, 887, 526, 263, 8444, 20255, 13, 32002, ...]
positions: [0, 1, 2, 3, 4, 5, 6, 7, 8, ...]ランタイムは、バッチメタデータ、シーケンス長、位置情報、因果アテンションマスクまたは同等の最適化済みメタデータも供給します。 バッチでは、短いシーケンスにパディングを追加したり、まとめてパックしたりする場合があります。上記の正確なトークンIDは、 IDが1つのトークナイザーリビジョンでのみ意味を持つため、説明用です。
モデルはまず、各トークンIDを学習済み埋め込みテーブルから参照し、単語ではなくベクトルを生成します。
ここで、
Sはシーケンス長、つまり入力トークン位置の数です。Vは語彙サイズ、つまりテーブルのインデックスに使用できるトークンIDの数です。Hはモデルの隠れ次元、つまり各トークン埋め込みに含まれる学習済みスカラー成分の数です。
したがって、トークンID入力は形状[S]の1次元整数ベクトルです。埋め込みテーブルは、語彙エントリごとに1行を持つ学習済み浮動小数点行列です。
S個のIDをすべて検索すると、S行が選択され、形状[S, H]の浮動小数点行列が生成されます。バッチ次元Bがある場合、
対応する形状は[B, S]、[V, H]、[B, S, H]です。同じ埋め込みテーブルがバッチ全体で共有されます。
示したデータ型は一般的な例であり、必須条件ではありません。量子化された埋め込みテーブルは通常、選択した行を使用するときに
非量子化されるか、モデルの計算データ型へ変換されます。
同一のトークンが異なる位置で異なる動作をできるように、位置情報が追加または適用されます。上記の加算方式では、EとPは同じ形状を持ち、
成分ごとに加算されます。Pは追加の特徴列として付加されません。連結すると[S, 2H]になりますが、数値加算では[S, H]が維持されます。
たとえば、トークンID995が位置2と9に現れるとします。埋め込み検索は、両方の位置で同じ学習済みベクトルE[995]を最初に返します。
学習済み絶対位置埋め込みを使用するモデルでは、次の異なるベクトルを最初の層へ渡せます。
position 2: E[995] + P[2]
position 9: E[995] + P[9]これは"dog bites man"と"man bites dog"も区別します。2つのシーケンスは同じ3つのトークン埋め込みを含みますが、
埋め込みが異なる位置を占めます。位置情報がなければ、self-attentionはその順序を直接表現できません。
すべてのモデルが、位置ベクトルを文字どおり加算するわけではありません。
- 回転位置埋め込み、つまりRoPEでは、ランタイムが位置に従って各queryとkeyの成分を回転させます。 基本トークン埋め込みは同じままでも、アテンション計算は絶対位置と相対位置に敏感になります。
- その他のアーキテクチャでは、相対位置バイアスまたは関連する機構を使用します。
位置情報を持つベクトルは、すべてのTransformer層を通過します。簡略化したデコーダー層は次のとおりです。
因果規則により、位置iの表現は、後続の入力位置ではなくiまでの位置だけにattentionできます。
- 依存関係は因果的ですが、GPUは大規模な並列行列演算により、すべての入力位置のprefill表現を計算できます。
- これはデコードとは異なります。デコードでは、新しく選択されたトークンが存在してからでなければ、次のステップを開始できません。
各層および入力位置について、attentionはkeyベクトルとvalueベクトルを計算します。ランタイムはこれらをKVキャッシュに保持します。
推論後に、prefillから得られたすべての一時アクティベーションを保持することは一般的ではありません。 KVキャッシュが重要な永続的結果である理由は、後続の各デコードステップが、以前のすべてのトークンについてkeyとvalueを再計算せずに、 既存のシーケンスへattentionできるためです。
最終層では、最後の入力位置の隠れ状態が言語モデルの出力ヘッドへ投影され、語彙内の各トークンについて1つのスコア、 つまりlogitになります。ペナルティ、制約、サンプリング変換がlogitを処理した後、それらは最初に生成されるトークンの候補を定義します。 したがってprefillは、次の2つの結果で完了します。
- 入力プレフィックスについて、層ごとのKVキャッシュが構築される。
- 最初の出力トークンを選択できるlogitが生成される。
最初のトークンを選択すると、ランタイムはデコードへ移行します。各デコードステップは、1つの位置のkeyとvalueを既存のキャッシュに追加し、 次のトークン用のlogitを生成します。encoder-decoderモデルでは作業を別の方法で分割しますが、現代のテキストチャットモデルの多くは、 ここで説明したdecoder-onlyパターンを使用します。
prefillの処理量は入力長に伴って増加し、計算負荷が高くなることがあります。そのため、回答自体が短くても、 長い既存の会話では回答が表示されるまでの遅延が増加します。
プレフィックスキャッシュの例 #
アプリケーションが、10,000トークンのマニュアルに続いて質問を送信するとします。
リクエスト1では、サーバーがシーケンス全体のKVエントリを計算します。プレフィックスキャッシュは、 安定したsystem指示とマニュアルのKVブロックを保持できます。後続のリクエストは次のようになります。
シリアライズされたバイト列が同じ先頭トークンIDを生成し、デプロイメントが同じモデル、トークナイザー、テンプレート、 関連する推論設定を使用している場合、ランタイムはキャッシュ済みKVブロックを接続し、新しいサフィックスだけをprefillできます。 モデルはマニュアルに引き続きattentionします。サーバーはすでに計算したkeyとvalueを再利用しており、ドキュメントを省略しているわけではありません。
本番キャッシュは、プレフィックスを固定サイズのトークンブロックに分割し、各ブロックをモデルやテナントのメタデータとともにハッシュすることがよくあります。 そのため再利用可能な長さは、個々の最後の一致トークンではなく、最後に一致したブロック境界で終わる場合があります。
トークンの先頭付近が変わると、通常、キャッシュは役立ちません。
最初の差分より後のすべてのトークンは異なるattentionコンテキストを持つため、キャッシュ済み状態を単純に接合できません。 同様に、空白、ツール定義、非表示の指示、添付ファイルの順序、チャットテンプレートを変更すると、 表示される質問が同じでもトークンプレフィックスが変わる可能性があります。
プレフィックスキャッシュは、最終回答のキャッシュとは異なります。内部のattention状態を再利用しますが、デコードは実行するため、 サンプリングされたレスポンスは異なる可能性があります。また、通常のデコード中に使われるリクエスト単位のKVキャッシュとも異なります。 プレフィックスキャッシュはリクエスト間で再利用可能なKV状態を保持しますが、通常のKVキャッシュは1回のアクティブな生成中だけ存在すればよいものです。
プロバイダーは、容量、期限、最小プレフィックス長、分離、価格のルールを定めます。キャッシュを自動的に生成する場合もあれば、 明示的なキャッシュオブジェクトやマーカーを要求する場合もあります。会話IDを再利用するだけでヒットが保証されるわけではなく、 キャッシュキーによって、あるテナントが別テナントのプライベートなプレフィックス状態を取得できないようにする必要があります。
デコード:回答の生成 #
prefillの後、モデルは次のトークンの確率分布を予測します。デコードポリシーは1つのトークンを選択し、それをシーケンスへ追加し、 別のモデルステップを実行します。
モデルが次のいずれかになるまで、このループは続きます。
- stopトークンまたはstopシーケンスを出力する
- 出力トークン上限に達する
- ツールを要求する
- ポリシーシステムによって停止される
- エラーが発生する
- キャンセルシグナルを受け取る
選択は貪欲法でも、temperatureやtop-pなどのサンプリング制御を使用しても実行できます。 製品は、反復ペナルティ、制約付きデコード、文法強制、投機的デコード、プロバイダー固有の推論機構も適用できます。
モデルは通常、段落全体を1回の操作で生成しません。トークンを段階的に生成します。トークンは完全な単語、 単語の一部、句読点、空白のいずれにもなり得ます。文字がトークンのバイト境界をまたぐ可能性があるため、 サーバーはトークンIDを慎重にテキストへデコードする必要があります。
ブラウザへのストリーミング #
推論ワーカーは、トークンまたはテキストの差分をモデルゲートウェイへ出力します。ゲートウェイはそれらをアプリケーションイベントへ変換し、 チャットサービスを通じてブラウザへ転送します。
一般的なトランスポートは次のとおりです。
- ストリーミングされた
fetch()レスポンス - Server-Sent Events(SSE)
- WebSocketメッセージ
- バックエンドが消費し、別のプロトコルで中継するプロバイダーストリーム
SSEに似たストリームには次のような内容が含まれます。
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 | 連続する出力トークン間の遅延 |
| Total latency | 生成完了までの時間 |
デコード開始後でも、複数のバッファが表示可能な出力を遅延させます。推論サーバー、リバースプロキシ、 圧縮レイヤー、コンテンツフィルター、ブラウザのストリームパーサーなどです。ストリーミングエンドポイントでは、 通常、プロキシバッファリングを無効にし、小さなイベントを意図的にフラッシュします。
部分回答のレンダリング #
ブラウザはイベントを読み取り、保留中のassistantメッセージを更新します。小さなトークンごとにDOMを更新すると高コストになる可能性があるため、 UIは差分を蓄積し、アニメーションフレームまたは短いタイマー間隔でレンダリングすることが多いです。
Markdownには別の問題があります。部分的なストリームは、しばしば無効なMarkdownです。コードフェンス、リンク、表、 強調マーカーがまだ閉じられていない可能性があります。レンダラーは不完全な入力を許容し、テキストの追加に応じて再レンダリングする必要があります。 構文強調や数式組版は、回答全体を繰り返し処理すると目に見えるカクつきが生じるため、遅延またはスロットリングされる場合があります。
ブラウザは生成されたHTMLを信頼できないものとして扱うべきです。Markdownレンダラーは、モデルが生成したスクリプト、 イベントハンドラー属性、安全でないURLがページ内で実行されないよう、危険な出力をエスケープまたはサニタイズする必要があります。
自動スクロールは条件付きにすべきです。ユーザーがすでに下部付近にいる場合、ビューは回答を追従できます。 しかし、ユーザーが読んでいる古いメッセージから繰り返し引き離すべきではありません。
ツール呼び出しと複数ステップのターン #
レスポンスが常にユーザー向けの文章とは限りません。ツールがプロンプトに含まれている場合、モデルは構造化されたリクエストを出力できます。
{
"name": "get_order",
"arguments": {
"order_id": "42"
}
}ツールを実行するのはモデルではなくアプリケーションです。一般的なエージェントループは次のとおりです。
1つの表示上のユーザーターンに、複数のモデル呼び出しと、データベース、検索エンジン、コードインタープリター、 リモートAPIへの複数の呼び出しが含まれる場合があります。UIには、「検索中」、「ドキュメントを読み取り中」、 「承認待ち」などの中間状態が表示されることがあります。
ツール結果はコンテキストトークンを消費し、それ自体が信頼できない可能性があります。ホストは権限、タイムアウト、 結果サイズ制限、副作用制御を適用する必要があります。破壊的な操作は、モデルが要求したからといって安全にはなりません。
検索も関連するパターンに従います。アプリケーションは、ユーザーの質問を検索クエリへ書き換え、候補ドキュメントを取得し、 再ランキングし、選択した抜粋をコンテキストへ挿入してから、回答モデルを呼び出す場合があります。 最初のモデル呼び出し前に検索するシステムもあれば、ターン中にモデルが検索ツールを選択できるシステムもあります。
回答の完了と永続化 #
デコードが完了すると、モデルサービスは終了理由と最終メタデータを出力します。アプリケーションはすべての差分を正規のassistantメッセージへ組み立て、 次の情報を記録します。
- 生成されたコンテンツおよび構造化コンテンツ部分
- モデルおよびモデルバージョン識別子
- 入力、出力、キャッシュ済み、推論トークンの使用量
- ツール呼び出しとツール結果
- 引用またはソース参照
- 終了理由
- レイテンシーおよびトレース識別子
- ポリシー判断またはエラー詳細
最終データベース書き込みはべき等であるべきです。リトライや重複したストリームイベントによって、 複数のassistantメッセージが作成されてはなりません。生成中に部分的なチェックポイントを永続化し、 中断された回答も表示できるようにするシステムもあれば、最終的に組み立てたメッセージだけを保存するシステムもあります。
サーバーは、結果を把握した後でのみ終端イベントを送信します。ブラウザはその後、assistantメッセージを完了としてマークし、 アクティビティインジケーターを停止し、コピーや再生成などの操作を有効化し、楽観的IDとサーバー発行IDを調整します。
回答の表示後もバックグラウンド処理が続くことがあります。アプリケーションは、会話タイトルの生成、検索インデックスの更新、 安全性分析、長期メモリの抽出、課金記録、品質メトリクスの出力などを行う場合があります。これらのタスクの結果がターンに必要でない限り、 ユーザーに見えるレイテンシーを延長すべきではありません。
キャンセル、リトライ、再生成 #
Stopを押すと、通常、すでに生成されたテキストを元に戻すのではなく、生成をキャンセルします。ブラウザはキャンセルリクエストを送信するか ストリームを閉じ、バックエンドはキュー内の処理、ツール呼び出し、推論ランタイムへキャンセルシグナルを伝播します。 プロバイダーはすでに追加トークンを生成して送信中である可能性があり、課金の意味は異なります。
部分回答は、cancelledの終了理由とともに保持できます。後続のコンテキストに残す場合、アプリケーションは、
正常に終了した回答と区別すべきです。
ネットワーク切断はキャンセルと同じではありません。ブラウザが消えても、サーバー側の生成が続く場合があります。 アプリケーションは次のようなポリシーを選択します。
- クライアント切断時に処理を即座にキャンセルする
- 生成を完了させて回答を永続化する
- 再接続用の再開可能なイベントストリームを保持する
- 短い切断猶予期間が切れるまでだけ続行する
送信のリトライでは、ネットワーク結果が不確かな場合でもユーザーメッセージが二重に追加されないよう、 同じidempotency keyを再利用すべきです。
Regenerateは通常、同じユーザーメッセージの下に新しいassistant兄弟を作成します。同じプロンプトを再利用しながら、 乱数サンプル、モデル、オプションを変更できます。以前の回答を上書きする必要はありません。 Edit and resubmitは、編集されたメッセージの親からブランチを作成します。後続のメッセージは元の文言を条件として生成されているためです。
異なるレイヤーでの障害 #
UIは、すべての障害を「モデルが失敗した」にまとめるべきではありません。段階ごとに復旧動作が異なります。
| 段階 | 例 | 一般的な処理 |
|---|---|---|
| Browser | オフラインまたはアップロード中断 | 下書きを保持してリトライ |
| Gateway | 認証またはレート制限 | 再認証または待機 |
| Conversation service | 無効なブランチまたは権限 | 推論せず拒否 |
| Context assembly | 入力がモデル制限を超過 | 安全に削減するか短い入力を求める |
| Scheduler | モデル容量なし | キューイング、リトライ、別経路へのルーティング |
| Inference | ワーカークラッシュまたはタイムアウト | 生成を失敗としてマークし、慎重にリトライ |
| Tool | 無効な引数またはリモートエラー | ループへ明示的なツールエラーを返す |
| Stream | 回答途中で接続切断 | 再開、部分出力の保持、再接続 |
| Persistence | 最終書き込み競合 | 安定したIDとべき等性で調整 |
自動リトライは、操作がべき等であるか、その効果が既知である場合にのみ安全です。テキスト生成をリトライすると、 異なる回答が生成される可能性があります。メール送信、注文、ファイル削除を行うツールをリトライすると、 現実世界の副作用が重複する可能性があります。
エラーはassistantの文章とは別に表現すべきです。「エラーが発生しました」と生成された文は、 トランスポート障害やツール障害と同等ではなく、アプリケーション状態を制御すべきではありません。
プライバシー、セキュリティ、可観測性 #
リクエストには、ユーザーが直前に入力したテキスト以上の情報が含まれる可能性があります。以前のメッセージ、アップロードされたファイル、 取得した非公開ドキュメント、ツール出力、非表示の指示などです。経路上の各サービスは、必要なデータだけを受け取るべきです。
重要な制御には次のものがあります。
- 転送中および保存時の暗号化
- すべての会話および添付ファイル検索における認可
- ストレージ、キャッシュ、キュー、ログ、推論におけるテナント分離
- テレメトリからの機密プロンプトデータの秘匿または除外
- 保持および削除ポリシー
- 監査可能なツール実行とユーザー承認
- ブラウザでの出力サニタイズ
- 取得コンテンツに埋め込まれた指示への対策
可観測性も重要です。分散トレーシングは、ブラウザのターンをゲートウェイ、検索、ツール、推論のスパンへ接続できます。 有用なメトリクスには、キュー時間、最初のトークンまでの時間、1秒あたりのトークン数、コンテキストサイズ、 キャッシュヒット率、終了理由、キャンセル率、障害段階があります。完全なプロンプトをログに記録するとデバッグには便利ですが危険です。 メタデータ、ハッシュ、サンプリングした秘匿済みコンテンツ、アクセス制御された診断ストアが、より安全なデフォルトです。
本質的なメンタルモデル #
既存のLLMチャットは、1つ以上の有限なモデル呼び出しを状態fulアプリケーションで包んだものとして理解するのが適切です。
会話の見かけ上の継続性は、アプリケーションがコンテキストを再構成して再送することで生まれます。 入力中のように見える効果は、段階的なデコードをストリーミングすることで生まれます。 検索、計算、操作を実行できるように見える能力は、アプリケーションがモデルの周囲で制御するループによって生まれます。
この分離によって、いくつかの一見意外な動作を説明できます。古い詳細は選択されたコンテキストの範囲外になると消え、 長い会話は開始まで時間がかかり、再生成は別のブランチを作り、停止した回答は部分的に表示され続ける可能性があります。 また、周囲のアプリケーションが明示的に情報や能力を与えない限り、モデルは会話、データベース、Webサイトへアクセスできません。
コメント
GitHub で閲覧・コメントする(投稿にはログインが必要です)