一、AI システムの変更はなぜ特に原因特定が難しいのか
通常のサービスの段階的リリースは、エンジニアリングの実践の中で何度も検証されてきた仮定の上に成り立っています。1 回のリリースで変わるのは少量かつ確定したもので、何かおかしくなったときにトラフィックを元のバージョンへ戻せば状態は復元する、と。この仮定は従来のサービスではほぼ成り立ちます。1 回のリリースが触る変数は離散的で,而且是ち列挙できるからです。新しいイメージ、増えた環境変数、フィールドだけを変更したテーブル。変更の境界ははっきりしていて、挙動の差は数秒のうちに露見する。rollback は純粋にルーティングの操作にすぎません。
LLM アプリケーションになると、この仮定は三方向から同時に崩れます。モデルが変わる、system prompt が変わる、検索インデックスが動く——しかもこの三つは並列したスイッチではなく、噛み合った歯車です。モデルを A から B に差し替え、B の出力スタイルに合わせて system prompt のテンプレートを書き換え、さらに B が長いコンテキストを使い方が変わったので top_k を 5 から 12 に上げた——ここで本番の指標が落ちたら、どの一手が原因なのか特定できるはずがありません。
もっと厄介なのは、この三層の障害モードが互いに偽装し合うことです。system prompt の変更による出力形式の劣化は、モニタリング上一次はモデルの能力低下とほとんど区別がつかない。また検索層がチャンク分割戦略を変えて検索の精度が落ちた場合、その兆候は数ターンの会話のあと、「質問と関係ない答え」として初めて現れる。すべての即時シグナルは正常に見えてしまいます。
1.1 従来型変更の原因特定は「悪い部品をひとつ見つける」ことで成り立つ
従来のシステムで問題を切り分ける方法は二分探索です。まず依存を疑い、次に自分のコードを疑い、必要ならトラフィックの半分を rollback して指標が戻るかを見ます。この方法が成り立つ前提は、事故と変更のあいだに比較的安定した因果対応があり、しかも悪い挙動は決定的であるということです。同じ入力なら、新しいバージョンでも必ず同じ形で失敗する。
一方、生成 AI システムの出力そのものがサンプリングのランダム性を含みます。同じ system prompt、同じモデルでも、temperature がゼロでなければ 2 回の出力は完全に一致しません。つまり「この失敗サンプルが変更によるものか、もともと存在する偶発的なものかを、もう一度見て確認する」という手法が使えません。切り分けの対象は「どの呼び出しが失敗したか」から「この種類のリクエストの失敗率がベースラインと比べて何ポイント上がったか」に変わり、十分なサンプル量と安定した評価基準を直接要求します。
出力がランダムである以上、従来の二分探索的な切り分けは機能しません。「もう一度走らせて再現させる」ことで問題を再現させることはできず、対照実験によってノイズを信号から分離するしかありません。
1.2 三層の結合と「半有効」状態
1 回の AI 変更を分解すると、通常は次のようになります。
- モデル層:基盤モデルのバージョン、fine-tuning のバージョン、推論パラメータ(temperature、top_p、max_tokens)、量子化と推論フレームワークのバージョン;
- オーケストレーション層:system prompt、few-shot の例、ツール定義、構造化出力の制約、リトライとフォールバックの戦略;
- 検索層:embedding モデル、チャンク分割戦略とオーバーラップウィンドウ、インデックススナップショット、ハイブリッド検索の重みと rerank モデル、top_k と閾値。
この三層のどの一層の変更も、出力に対して孤立して作用することはありません。モデルが変われば、system prompt の中の指示の言い回しが最適でなくなることがあります。チャンク分割戦略が変われば、同じ質問に対する検索結果の集合そのものが変わり、モデルが見るコンテキストが変われば出力も自然と変わります。結果として「検索だけを変更した」リリースは実際には入力分布まで変えており、「モデルだけを変更した」リリースでも、本番に特定モデル向けに調整された system prompt のテンプレートがあれば、実際には system prompt も同時に変わっていることになります。
ここには特に見落とされがちな罠があります。それが半有効状態です。段階的リリースの期間中、モデルのトラフィックを 5% だけ切り替えた場合でも、system prompt の読み込みはグローバルなものであり、検索インデックスの再構築はバッチごとに進んでいることがあります。この時点では本番上に複数の設定の組み合わせが同時に存在しているのに、モニタリングのダッシュボードはそのすべてをひとつの数値にまとめています。まとめたあとに見える低下は、ある組み合わせの問題が他の組み合わせに薄まっている,也可能である組み合わせの問題が強調されている。いずれの場合でも、原因を突き止める力は失われます。
これは理論上の議論ではありません。複数のチームで実際に記録されたトラブルシューティングのログに見られるパターンは、次のようなものです。モデルアップグレードを含むリリースによって満足度が下がり、チームは 3 日かけて原因を特定した。結果として明らかになったのは、新しいモデルの tokenizer がある特定の言語断片の扱いを変えたため、特定種類の短いクエリの検索スコア全体がずれた、というものでした。system prompt の変更は、あくまでそのずれを表面化させやすくしただけで、真の原因ではありません。3 つの変数、観測できる下流指標は 1 つ、そして 3 日。
二、まずバージョンの識別子を正しくする
段階的リリースの手段について触れる前に、必ず満たしておくべき前提がひとつあります。本番に出た任意のリクエストを、あとから「どのモデル+どの system prompt+どの検索結果」に还原できるということです。これができなければ、shadow traffic も A/B 分流も回帰ゲートも成り立ちません。いずれも、発生しても位置の特定できない比較データしか生まないからです。
多くのチームは、この段階でバージョン番号を各个の設定項目に散らばらせて保持します。モデルのバージョンはモデル設定ファイルにあり、system prompt のバージョンはプロンプトストアのテーブルにあり、インデックスのバージョンは検索サービスのメタデータテーブルにあります。それぞれのテーブルを単体で見れば正しいのですが、どのテーブルも「このリクエストが当時どのバージョンを使っていたか」には答えられません。原因の特定が必要になったときには、あるフィールドが 3 か月前の緊急作業で上書きされていて、その操作には何もログが残っていない、ということがよくあります。
2.1 バージョンの三要素と不変スナップショット
正しい方法は、1 回の変更ごとに不変の変更識別子を発行することです。これは三つではなく、四つの要素を連結して作ります。
- モデル識別子:基盤モデルと fine-tuning の重み、推論パラメータをすべて含めた完全なフィンガープリント。パラメータも計算に含めます。temperature を 0.3 から 0.7 に上げるのは実際の挙動変更だからです;
- system prompt 識別子:system prompt の本文と few-shot の例の集合、ツール定義のハッシュ;
- 検索識別子:embedding モデル、チャンク分割パラメータ、インデックススナップショットのバージョン、検索パラメータと rerank モデルのハッシュ;
- ルーティング識別子:このリクエストが実際にルーティングされたバリアント。canary release のルールバージョンとバケット結果も含みます。
4 番目はよく抜け落ちますが、段階的リリースの期間中は極めて重要です。同じ変更識別子が同時に 10%、50%、100% の 3 つのトラフィック比率に対応しているかもしれません。あるリクエストが当時どの段階にいたのかが分からなければ、正しい対照グループに分類することができません。
重要な制約は不変性です。system prompt の更新は追記 その際のバージョンでありなければなりません。新しいバージョンは新しいレコードとして書き込むのではなく、古いレコードを直接書き換えてはいけません。古いレコードは任意の時点に取得できる必要があります。3 か月後にユーザーの問い合わせをあとで振り返るとき、必要なのは 3 か月前のバージョンの system prompt であり、今日のものではないからです。
2.2 リクエストフィンガープリントの構築と永続化
変更識別子が用意できたあとは「それを具体的なリクエストにどう紐づけるか」という問題が残ります。リクエストの入口で各コンポーネントのバージョンを具体的な値に解決してフィンガープリントを計算し、trace のコンテキストに書き込んで、リクエストとともにすべての下流のサブコールへ伝播させます。
以下は最小限の実装です。ポイントは build_bundle がリクエスト開始時に解決結果を確定させることです。参照や関数ポインタを残して先送りするのではなく、リクエストが始まったときの世界を固定します。これで、設定が実行中にホットリロードされても、開始済みのリクエストは開始時点のバージョンをそのまま使い続けるため、同じリクエストの前半と後半が異なるバージョンを見てしまうような隙間が生まれません。
import hashlib
import json
from dataclasses import dataclass
from typing import Any, Dict, Optional
def digest(payload: Any) -> str:
"""对任意可 JSON 序列化结构做稳定哈希,生成不可变版本指纹。"""
blob = json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
return hashlib.sha256(blob.encode("utf-8")).hexdigest()[:16]
@dataclass
class Bundle:
"""一次请求所使用的完整配置快照,构造后不可变。"""
model_ref: str # 基座模型与微调权重的标识
infer_params: Dict[str, Any] # 推理参数,参数本身也是变更的一部分
prompt_hash: str # 系统提示词与 few-shot 的哈希
tools_hash: str # 工具定义的哈希
retrieval_hash: str # 索引快照与切分、检索参数的哈希
serving_stack: str # 推理框架与量化方式,排查性能问题时要靠它
route: str = "control" # 实际命中的灰度变体名
bucket: Optional[int] = None # 分桶结果,未分流时为空
@property
def bundle_id(self) -> str:
"""把整组配置折叠成一个短标识,日志里只需要带这一个字段。"""
return digest({
"m": self.model_ref, "i": self.infer_params,
"p": self.prompt_hash, "t": self.tools_hash,
"r": self.retrieval_hash, "s": self.serving_stack,
})
def build_bundle(resolver, variant: str = "control") -> Bundle:
"""
在请求入口处解析出完整配置快照。
resolver 是只读接口,负责把逻辑名称解析为当前生效的具体版本:
resolve_model(variant) -> (模型标识, 推理参数字典)
resolve_prompt(variant) -> 提示词正文与示例集合
resolve_tools() -> 工具定义列表
resolve_retrieval(variant) -> 索引快照与检索参数
"""
model_ref, infer_params = resolver.resolve_model(variant)
prompt_doc = resolver.resolve_prompt(variant)
ret_cfg = resolver.resolve_retrieval(variant)
# 检索要区分"配置"与"内容":配置进哈希;
# 内容不进哈希,而是单独记录命中的 doc id,否则索引重建后无法区分两次采样。
return Bundle(
model_ref=model_ref,
infer_params=dict(infer_params),
prompt_hash=digest({"sys": prompt_doc.system, "shots": prompt_doc.shots}),
tools_hash=digest(resolver.resolve_tools()),
retrieval_hash=digest({
"cfg": ret_cfg.params, # top_k、阈值、混合权重、重排模型
"embed": ret_cfg.embedding_model,
"chunk": ret_cfg.chunk_params,
"index": ret_cfg.index_snapshot, # 索引快照版本号
}),
serving_stack=resolver.resolve_serving_stack(),
route=variant,
)
検索側の実際のサンプリング結果、つまりヒットしたドキュメント id のリスト、最終スコア、rerank を通ったかどうか、コンテキスト予算で切り詰められたかどうかは、独立したレコードとして永続化しなければなりません。後付けで同じパラメータを使って走らせ直すのではなく。ベクトル DB の実装は細部が完全には決定的でないため、同じインデックススナップショットでも 2 回の呼び出しで検索結果の順序にわずかな差が生じえます。後付けの再現で得た証拠は、インシデントレビューでは説得力を持ちません。このデータの保存にはコストがかかりますが、インシデントレビューにおいて唯一信頼できる証拠でもあります。
2.3 よく踩む実装上の細かい点 3 つ
ハッシュは入れ子構造全体を覆うこと。system prompt の本文だけをハッシュする人が多く、few-shot の例を入れ替えてもハッシュが変わらず、原因特定のときに全く気づけません。正規化したシリアライズをしてからハッシュすることで、順序や空白には鈍感になりつつも、内容には確実に敏感になります。
設定の解決は「早い段階で固定する」こと。モデル呼び出しの時点で初めて system prompt のバージョンを解決していると、設定センターがその瞬間にホットリロードした場合、同じリクエストが 2 つのバージョンの system prompt を見てしまうことがあります。解決を入口へ前倒しするのは、この時間窓を取り除くためです。代償として、長寿命のリクエスト(ストリーミング出力など)では途中で設定を切り替えられなくなりますが、そもそもそれが望ましい挙動です。
インデックスの再構築には独立したバージョン番号 そのフィールドがすること。「インデックスの最終更新時刻」をバージョンとして使ってはいけません。ベクトル DB の増分更新やバックグラウンドのマージにより、1 回の再構築でも複数の内部状態が生じるため、タイムスタンプでは全く区別できません。正しい方法は、再構築のたびに単調増加するスナップショット番号を発行し、検索設定に「この設定がどのスナップショットを使うか」を明示することです。
三、shadow traffic は何に答えるのか
shadow traffic(ミラートラフィックやダブルライトとも呼ばれます)の做法はこうです。実ユーザーのリクエストは通常どおり本番の対照グループへルーティングし、同じリクエストの複製を実験グループの完全なチェーンに流します。実験グループの結果はユーザーには返さず、比較のためだけに使います。
この仕組みは地味に見えますが、ゼロリスクの条件下で「新チェーンが実際の分布上で何を出力するのか」を手に入れられる唯一の手段です。そして実際の分布そのものは手に入れられません。手元の評価セットをどれだけ丹念に構築しても、あれは本番トラフィックの一部にすぎない、偏ったサブセットです。したがって AI 変更における shadow traffic の最初の用途はごく素朴なものです。評価セットの分布の盲点を埋める。
3.1 shadow traffic が答えられる 3 つの問い
問い 1:形式と構造が壊れていないか。これは shadow traffic が最も費用対効果の高い項目です。本番のリクエストを新しいバージョンに流し、新しい出力に対して構造検証を 1 往復させます。JSON がパースできるか、必須フィールドがあるか、enum の値が正当か、コードブロックが閉じているか。この検査はほぼコストゼロで、「一応問題なさそうだが中身は全部おかしな」型の事故を相当な割合で止めてくれます。
モデルのアップグレードも system prompt の変更も、こうした劣化を起こしやすいからです。新しいモデルは指示への従い度が旧モデルほど厳密でない可能性があり、few-shot の例をある形式に変えるとモデルが安定して従わなくなり、JSON 出力の制約の説明を 1 文字変えるだけでパース成功率が大きく落ちることもあります。いずれも shadow の期間中に一度だけ集計すれば済みで、本番へ広げる前に完全に修正できる内容です。
問い 2:セマンティックドリフトはどれほど大きいか。実験グループと対照グループの出力を並べて置き、意味の類似度を計算するか、固定のスコアリングモデルで相対評価を出します。この指標は「合格か不合格か」の判定には向きませんが、「今回の変更がどれだけ動かしたか」という問いには非常に適しています。平均類似度がまだ 0.95 以上なら、今回の変更はほぼ挙動を変えていないということです。その場合は自分に問いかけるべきです。なぜこのバージョンをリリースするのかと。類似度が 0.6 まで落ちたなら、実質的な挙動変更なので、完全な回帰ゲートと A/B 手順を通さなければなりません。
非常に実用的な経験則があります。意味の類似度はリリースの「変化量の尺度」として使い、「品質の尺度」としては使わないということです。今回の変更の影響半径がどれほど大きいかを教えてくれるので、投入すべき検証リソースの量を決められます。
問い 3:レイテンシと失敗率が許容範囲か。shadow traffic はタイムアウト、レート制限、上流との接続失敗といったエンジニアリング上の問題も暴きます。計測されるレイテンシは分布として本物です。入力分布が本物だからです。ただし、実ユーザーの枠を使わずに済むぶん、やや楽観に出ます。
3.2 shadow traffic が答えられない 3 つのこと
shadow traffic の限界もはっきりしています。そしてこれらの限界こそ、shadow traffic が単独で段階的リリースの合否判定を担ってはならない理由です。
インタラクションの劣化は測れません。ユーザーが受け取るのは対照グループの結果で、実験グループの出力は破棄されます。ユーザーが体験するのは完全な旧チェーンです。新しいチェーンの出力によってユーザーが追问することはないか、困惑していないか、ノイズと見なされて諦めていないかは、観測範囲にまったく含まれません。典型的な例は、回答が冗長になることです。1 ターンだけ見れば品質の低下は明らかではありませんが、読んだあとにさらに質問が必要なら、そのバージョンの価値はマルチターンではマイナスです。shadow traffic はこのことを一切知りません。
コストと枠の実際の圧迫は測れません。shadow リクエストも実際にモデルを呼ぶので、コストは本物です。ユーザーだけが負担していないだけです。このため shadow 期間に見えるレイテンシの曲線は、実際の拡大後よりもなめらかです。本当トラフィックを切り替えたとき、実験グループは共有の推論枠プールを占有し、ピーク時の待ち時間は急激に伸びます。shadow 期間にはこの現象がまったく現れません。より悪いのは、shadow リクエストと実ユーザーのリクエストが同時に枠を奪い合うと、逆に対照グループを遅らせてしまうことです。すると自分で汚染したベースラインを見て判断を下すことになります。
「より良くなったか」には答えられません。shadow は新旧の比較しかできず、新バージョンの品質を直接判定することはできません。「より良くなったか」を判定するには、ユーザーからのフィードバックが外部の物差しとして必要です。しかしユーザーは対照グループとしかやり取りしないため、実験グループに向けたフィードバックは生まれません。shadow traffic を A/B 分流と組み合わせて使う必要があるのはこのためです。前者が「壊れるか」を、後者が「より良くなったか」を担当します。
shadow の価値は「ゼロリスクで、より広い観測面と引き換えること」にあります。拡大の根拠ではなく、拡大のための前提入力です。
3.3 shadow traffic で実際につまずく 3 つの罠
副作用による汚染。新しいチェーンに書き込み系の処理がひとつでも含まれているなら、DB への書き込み、下流 API の呼び出しによるチケット作成、通知の送信、残高の減算、外部アクションの起動。shadow traffic は本物の副作用を生みます。1 回の shadow の再生で、上流システムに数百から数千件の汚れたデータを作り出しうるため、これが最も危険な罠です。
対処は明確です。shadow リクエストはコンテキストに必ずマーカーを持たせ、すべての書き込み系パスがそのマーカーを確認して短絡させる必要があります。ここで重要なのはこのマーカーをフレームワークが一括して注入することであり、各開発者が手で書くことに依存してはいけません。自発性に依存した対策は、はじめてリリースが押されたときに必ず機能しなくなります。より隠れた変種もあります。検索インデックスへのアクセス自体が状態を変えることがあるのです。キャッシュのウォームアップの引き金、リモートシャードの取得、甚至は自動スケーリングの起動などが該当します。これらの変化は通常無害ですが、インデックスストレージがクエリ課金制であれば、また命中率を重要な指標として見ているのであれば、shadow traffic はこれらの数値を継続的かつ体系的に汚染してしまいます。
キャッシュとコネクションプールの汚染。shadow リクエストと実リクエストは、同じ推論サービス、同じコネクションプール、同じローカルキャッシュを共有しています。実験グループが別の system prompt を使いながら意味的キャッシュの key 空間を共用していると、実験グループの結果が対照グループのキャッシュを汚染するという古典的な問題が起こります。対策は、キャッシュの key に完全な bundle_id を含めることです。query のテキストだけで絶対にしてはいけません。自明な設計に聞こえますが、多くのシステムではキャッシュの key は単なる query 文字列です。かつては本番が単一バージョンの時代だったため、これで問題はなかったからです。
データ量とコストの増幅。shadow traffic は、実験グループの呼び出し量が全量トラフィックと等しいことを意味します。コストはそのまま 2 倍になります。中規模なアプリケーションでも、1 日の追加の推論費用は無視できる金額ではないことがあります。
緩和策は 3 つあります。時間帯でサンプリングする(例:全日トラフィックの 20% だけをミラーする)、リクエスト種別で層別サンプリングする(価値の高いリクエストは全量、低いものはサンプリング)、そしてモデル呼び出しの前にフィルタを掛ける(長さ超過、タイムアウト、キャッシュヒットはいずれもスキップ可)です。サンプリングは無偏でなければなりません。「最初の 20% だけをミラーする」という書き方は典型的な偏りを含むサンプリングです。リクエストの到着順は業務の負荷と関係するため、ピークとオフピークの分布はまったく異なるからです。request id をハッシュして余りを取る形でサンプリングするのが正しく、これならどの時間帯の分布も保たれます。
3.4 shadow traffic の実装のかたち
ここまでの制約を設定に落とし込むと、おおよそ次のような分流ルールになります。この設定で最も重要な 2 行は shadow.sample_rate と read_only_enforced です。前者がミラー比率を決め、後者が副作用が発生する前に遮断します。
# 影子流量配置片段:control 为线上对照组,candidate 为待验证的新组合
variants:
control:
weight: 1.0
model: prod-model-a
prompt: prompt-v7
retrieval: { index: idx-2026-09-18, top_k: 5 }
candidate:
shadow: true # 结果不返回给用户
mirror_of: control # 影子请求以对照组的入参为准
model: prod-model-b
prompt: prompt-v8
retrieval: { index: idx-2026-10-02, top_k: 8 }
routing:
# 按请求 id 哈希取模,保证每个时间段的分布都与全量一致,避免按时序截取造成偏差
shard_key: request_id
shadow:
sample_rate: 0.2 # 只镜像 20% 的真实流量
skip_if_cached: true # 命中语义缓存的请求不再重复计算
max_input_chars: 12000 # 超长请求不进影子,避免成本与延迟失真
strip_when:
- internal_probe # 内部探针与压测流量不参与镜像
safety:
read_only_enforced: true # 框架级拦截:带 shadow 标记的请求调用写接口一律拒绝
cache_key_includes: [bundle_id] # 防止实验组结果污染对照组缓存
suppress_side_effects: [email, webhook, ticket_create, billing]
ここにも判断の分岐があります。skip_if_cached は節約できるように見えますが、shadow サンプルに選択的な偏りを生みます。スキップされるのは、まさに最もキャッシュヒットしやすい種類の重複リクエストであり、その種のリクエスト恰恰と問題を露呈しやすいものです。スキップするかは目標に応じて決めてください。セマンティックの品質と形式の安定性を検証したいなら、キャッシュをスキップしてはいけません。レイテンシと失敗率だけを監視したいなら、スキップしてかまいません。
四、A/B 分流はいったい何に答えるのか
shadow traffic が答えるのが「新チェーンは壊れるか」だとすれば、A/B 分流が答えるのは、オフラインでは代替できない唯一の問題です。結果を実ユーザーに返したとき、それは本当により良いのか。この判断にはユーザーの実際の反応という物差しが必要ですが、オフライン環境ではその物差しを手に入れられません。
4.1 分流の正しい単位は「変更の組み合わせ」であって単一変数ではない
AI シーンでよくある誤りは、従来の単一変数の A/B の習慣をそのまま持ち込むことです。今回はモデルを変えるから、モデルだけ切り替えて他は全部そのままにする、と。このやり方の問題は、本来まとめて評価すべき 3 つのことを、無理に切り離してしまうことにあります。
具体的な例を挙げます。新しいモデルを評価したいとして、オフライン評価では旧モデルより 2 ポイント良いという結果が出たとします。しかし新しいモデルの出力はより長く、より冗長です。system prompt がまだ旧モデル向けに調整されたままなら、その中にはモデルの余計な説明を抑えるための指示が含まれているはずです。すると新しいモデルは表面上は「より従順」だから長くなったように見えても、実際にその余計な内容はプロダクトのシーンではノイズです。モデルだけ切り替えて system prompt を調整しないと、測定結果は「新モデルには明確な向上がなく、むしろわずかに低下した」となります。この結論自体は正しいのですが、次をどうすればいいかは教えてくれません。実際に知りたいのは、system prompt も同時に調整すれば新モデルが旧モデルを上回りうるかどうかです。
したがって正しい実験単位はユーザーが知覚する完全な組み合わせです。ユーザーはあなたの裏側でいくつのコンポーネントが入れ替わったかを知りもし気にせず、感じ取るのは「この回答は良いかどうか」だけです。実験単位はユーザーの知覚の境界に合わせるべきであり、あなたのアーキテクチャの境界に合わせるべきではありません。
この結論から重要な帰結が 1 つ導かれます。1 回の変更が複数の層にまたがって動くとき、1 回の実験で各層の貢献を分離しようとしてはいけません。その情報が欲しいなら、正しいやり方はオフラインで制御実験を使って分解することです。オンライン分流で分解しようとすると、必要となるサンプル量は指数関数的に増加します。正交実験で複数の主効果を測るため、通常は各セルを十分な量まで走らせる必要があり、AI アプリケーションのユーザー量がそれを支えられるとは限らないからです。
4.2 排他的な層分け:なぜ直交分流が必要なのか
2 つの独立した実験を同時に走らせているとします。ひとつは検索層のチャンク分割戦略の調整、もうひとつはモデルバージョンのアップグレードです。時間軸上で重なっており、それぞれがトラフィックを取ろうとしています。層分けがなければ、2 つの実験の実験グループは重なって、同じユーザー群が同時に 2 つの実験の中にいることになり、結果が互いに汚染します。両方とも良くなった場合に誰の功績か分からず、片方だけ良くなった場合には干渉効果なのかノイズなのかすら区別できません。
標準的な解決策は直交分流です。ユーザーを互いに重ならない 2 組のバケットに分けます。たとえば user id のハッシュ下位桁で 4 つに分け、実験 A はバケット 0 と 1、実験 B はバケット 0 と 2 を使います。こうすると 2 つの実験がバケット 0 を共通のベースラインとして共有しつつ、それぞれが 1 つのバケットを独占し、統合分析で正しく分解できます。
ただし AI のシーンには、もう 1 つ追加の複雑さがあります。セッション内の粘着性です。あるユーザーの 3 番目のターンが実験グループに入り、前 2 ターンが対照グループに入ると、非常に悪い体験になり、データの汚染も起きます。会話履歴にはすでに別々の設定から生成された出力が混ざっているからです。分流はセッション単位でなければならず、1 つのセッションは最初から最後まで同じバリアントに固定されます。多ターンのシーンではこれは最適化項目ではなく、硬い制約です。
見落とされがちな点がもう 1 つあります。実験グループ同士も互いに重ならないことです。実験 A と実験 B を同時にリリースする場合、両者の実験グループユーザーは重なってはいけません。A の実験グループが評価しているのは「現在の本番バージョンに対する変化」であり、同時に B の変化が乗っていれば、その差分は B の貢献を含んでしまうからです。複数の実験グループが重なってよいのは、「蓄積効果」を仮定し、かつ分析時にすべての他の要因を明示的に制御している場合だけです。
4.3 A/B が答えられない 3 つのこと
A/B の能力の境界をはっきりさせておくのは、それが何ができるかを説明するより価値があります。
ロングテールの品質問題は見えない。A/B で観測できるのは、あなたが指標として定義できるイベントだけです。「ユーザーが「役に立った」ボタンを押したか」というコンバージョンを測っているなら、そのボタンを押さなかったすべてのユーザーは未観測です。しかし実際の被害はしばしば沈黙の大多数に起こります。ユーザーは読み終え、何も押さず、文句も言わず、そして二度と来ない。この損失は A/B ダッシュボードでは全く見えません。指標の定義がそれを「シグナルなし」に分類してしまうからです。
対策は「沈黙」を観測可能なプロキシ指標に分解することです。セッションのターン数の変化、後からたずね直しがあるか、セッションが何秒後に閉じられたか、ユーザーが短時間の窓で同じテーマのセッションを繰り返していないか。これらのプロキシ指標はどれも完璧ではありませんが、沈黙の被害の一部を可視化できます。
サンプル量が足りない場合、結論は信頼できません。これは AI アプリケーションの A/B でもっともよくある落とし穴です。従来プロダクトのコンバージョン率のベースラインが数パーセントで、数ポイント上げれば統計的有意差が出るのに対し、「答えが役に立った」といった指標はベースライン自体が 60% 前後かもしれません。3 ポイントの差を検出するには必要なサンプル量が前者の 1 桁以上大きくなります。日次トラフィックが数千しかない場合、実験を 1 週間走らせても結論が出ないことがあります。
もっと厄介なのは、サンプル量が不足したまま多くのチームが「有意」な結果を読み上げてしまうことです。実験中に繰り返しデータを確認し、ある日 p 値が 0.05 を下回った途端に実験を止めるからです。これは実際に存在する方法論的な誤りであり、偶然のゆらぎを結論として扱っています。しかも毎日見ている以上、偽陽性に遭遇する可能性はほぼ確実です。
レイテンリとコストの実際の負荷は測れません。実験グループは一部のトラフィックしか占めず、共有推論リソースの占有は比例します。実験グループが 5% から 50% に増えると、推論クラスタの負荷特性は非線形に変わります。キュー、バッチのスケジューリング、KV キャッシュヒット率がいずれもまったく異なる領域に動くのです。低い比率で観測したレイテンシを高い比率に外挿することはできません。
4.4 実験設計における実践的な 3 つの提案
停止ルールを先に決めてから実験を始める。実験を始める前に、どのタイミングで必ず停止するかを書いておきます。サンプル数の達成、観測ウィンドウの満了、あるいは guardrail 指標のいずれかが閾値を下回った場合です。このルールは起動時に固定して、データを見る人の即興判断に委ねるべきではありません。guardrail 指標と最適化指標は分けて見ます。最適化指標が「役に立った率」なら、guardrail 指標は「応答時間」「苦情率」「1 セッションあたりのコスト」です。guardrail 指標がトリガーしたなら、最適化指標が何再好でも停止します。
guardrail 指標は統計検定ではなく硬い閾値にすべきです。guardrail の論理は「悪化してはいけない」であり、「差があるかもしれない」ではありません。したがって「実験グループのレイテンシが対照グループより 20% 以上、10 分間続いたら停止」という決定的なルールであるべきで、レイテンシに対して t 検定を行い p 値を見るべきではありません。
各実験に唯一の責任者を置きます。管理上の話題のように聞こえますが、エンジニアリング上は極めて重要です。複数のチームが同時に実験をリリースし、実験ダッシュボードは共有されていると、指標が動いたときに、それが特定の実験によるのか複数の実験の重ね重なった結果なのか、誰にも判断できません。責任者の仕組みは、各実験の開始と停止に明確な担当者がいることを保証します。
| 手段 | 答えられる問い | 答えられない問い |
|---|---|---|
| shadow traffic | 形式が壊れていないか、セマンティックドリフトの大きさ、レイテンシと失敗率 | より良くなったか、インタラクションの劣化、実際のコスト圧力 |
| A/B 分流 | ユーザーがより好むか、コスト効率の比較 | 沈黙の被害、ロングテールの品質、少ないサンプルでの結論 |
| 回帰ゲート | 許容できる品質の下限を割ったか | 品質が上がったか、実際のユーザー体験 |
| 本番監視 | 劣化が起きているか、影響範囲の広さ | 劣化の根本原因、どの変更が原因か |
この表は記事全体のなかで最も覚えておく価値のある部分だと考えています。4 つの手段は 4 つ異なる象限をカバーしており、代替関係ではありません。どれかが欠けても、段階的リリースの期間に死角が生まれます。実際のインシデントレビューでは、「A/B をやったから安心して拡大した」という記述のあとには、ほぼ必ず「でも shadow traffic では形式適合率が 15% 落ちていた」といった補足が続きます。
五、統合バージョン追跡:content-addressable と replay 可能性
第 2 章で、設定に識別子を付ける方法を説明しました。しかし識別子は最初の一歩にすぎず、本当の難しい問題は、その識別子が時間経過のなかで安定し続け、3 か月前の 1 本の trace を完全に再現できることです。この章では 3 つの具体的な技術選択を扱います。これらが原因特定の能力の上限を決めます。
5.1 system prompt は content-addressable でなければならない
もっともよくある誤った設計は、名前とバージョン番号で system prompt を参照することです。たとえば system_prompt@v7 です。問題は @v7 が漂う参照になることです。v7 を土台に誤字を修正したらバージョン番号は v8 になりますが、本番は依然として v7 を参照しています。緊急のため本番環境で v7 の内容を直接書き換えたなら、历史の trace に残るすべての @v7 が新しい内容を指すようになり、歴史は黙って改竄されます。
正しい方法はcontent addressingです。system prompt の識別子は、本文のハッシュそのものであり、増加していくカウンターではありません。名前は人間が読める接頭辞としてだけ存在し、人が検索する手助けをします。実際に違いを生むのはハッシュです。これにより自然に 2 つの性質が得られます。内容が変わらなければ識別子も変わらないので、同じ system prompt を複数のバリアントで安心して再利用できます。内容が変われば識別子は必ず変わるので、「内容は変えたのに識別子が変わらなかった」というケースは原理的に起きません。
代償に、「v7 が v8 になった」という直观的な履歴の感覚が失われます。緩和策は、単調増加する表示用の番号を同時に記録することです。これは人間と監査のためにだけ使い、アイデンティティの判定には一切関与しません。これで可読な履歴と content addressing の正しさを両立できます。
見落とされがちな罠がもう 1 つあります。テンプレート変数です。system prompt のなかでテンプレート構文を使っている場合、{{用户等级}} のようなプレースホルダを含んでいる場合、テンプレートの原文をハッシュにするだけでは足りません。同じテンプレートでもユーザー権限によってレンダリング結果の実際の system prompt は異なるからです。正しい方法はレンダリング後の完全な system prompt をハッシュにすることであり、加えてテンプレートのハッシュも別に記録することです。これで実際の system prompt による精密なグルーピングと、あるレンダリング分岐で問題が見つかったときのテンプレートの特定が両立します。
5.2 検索層のバージョニングは想像以上に難しい
検索層は最も抜け落ちやすく、同時に最も問題が起きやすい部分です。独立にバージョニングが必要なものが 3 種類あり、この 3 つの時間定数がまったく違います。
- 設定系:チャンクサイズ、オーバーラップ比率、top_k、類似度閾値、ハイブリッド検索の重み、rerank モデル。この種の変更は即座に効いて、追跡しやすいです。
- コンテンツ系:インデックスのスナップショット。この変更は数時間または数日単位で発生し、しかも自発的に起きます。誰もリリースしていないのに、ドキュメントが更新されればインデックスが変わります。
- モデル系:embedding モデルと rerank モデル。これらの変更は古いインデックスを無効化するため、全量再構築が必要になります。
コンテンツ系の自発性が最大の厄介さです。実験を昨日開始し、モデルも system prompt も変えていなくても、ドキュメントの自然な更新によってインデックスが変わったなら、実験の前後の対照已经不是同じシステムです。これは理論上の問題ではありません。ドキュメントの更新が頻繁なナレッジベースでは、ほぼ既定で起こります。
対策は 3 つあり、コストは増加します。受け入れて分析で制御する(インデックスバージョンを層別の変数として分析に含める)、実験を固定したインデックススナップショットに固定する(実験期間中は実験開始時のスナップショットを強制し、新しいドキュメントはインデックスに入れない)、あるいは二重インデックスを並走させる(対照グループは本番インデックス、実験グループは固定スナップショットを使う。代償は 2 組のインデックスの運用コストとクエリのレイテンシ)。
多くのチームにとって 2 番目が最も費用対効果が高いです。段階的リリースの期間中はインデックススナップショットを明示的に固定し、bundle_id に書き込むということです。これは、段階的リリースの期間中に新しく追加されたドキュメントが実験に参加しないことを意味します。拡大が完了してからまとめて追従させます。代償は、その期間中の新ドキュメントが実験グループから見えないことです。この代償はほとんどのシーンで許容でき、しかも「実験結果を解釈できない」ことよりはるかにましです。
より根本的な問題は、ドキュメントのリポジトリ自体の状態も記録されるべきだということです。インデックスのスナップショット番号は「これは何回目の再構築か」を示すだけで、「再構築のときリポジトリに 12000 個のドキュメントがあり、そのうち 340 個は前四半期に新しく追加された」とは教えてくれません。重要なドキュメントが実験の途中で削除または変更されていたなら、実験結果にはあなたが制御していない変数が混入しています。証拠の連鎖が求められるシーンでは、ドキュメントリポジトリの状態も不変のバージョンにする必要があります。つまり、コンテンツアドレスングのあの論理をドキュメントに適用するだけです。
5.3 replay 可能性:原因特定を推測から証明に変える
完全なバージョン識別子が揃ったあとは、原因特定を「ログからの推測」から「再生による検証」に引き上げる必要があります。再生の価値は、推論の不確実性を下げることにあります。同じ bundle_id で 100 回走らせ、結果の分布を観察できます。1 回のサンプリングで判断する必要がなくなるからです。
以下は再生ツールの骨格です。ポイントは 3 つ。再生リクエストは設定の解決に現行の設定ではなく、元の bundle_id を使うこと。サンプリングパラメータと乱数 seed の戦略を明示的に指定すること。同じ bundle_id の複数回のサンプリング結果を書き出せて、「決定的な失敗」と「確率的な偶発」を区別できること。
"""
离线重放工具:给定 trace_id,还原当次的完整输入与配置,重复执行以观察结果分布。
"""
from collections import Counter
from typing import Any, Dict, List
class Replayer:
def __init__(self, trace_store, bundle_store, executor):
self._traces = trace_store # 保存原始请求体、bundle_id、检索采样结果
self._bundles = bundle_store # 按 bundle_id 取回不可变配置快照
self._exec = executor # 执行一次完整链路的函数
def replay(self, trace_id: str, runs: int = 20) -> Dict[str, Any]:
trace = self._traces.load(trace_id)
# 关键:用原始 bundle_id 解析配置,而不是当前生效的版本
bundle = self._bundles.get(trace["bundle_id"])
outcomes: List[Dict[str, Any]] = []
for i in range(runs):
# 检索层必须复用原始采样结果,否则重放的输入就不是当次的输入了
ctx = {
"messages": trace["request"].messages,
"retrieved_docs": trace["retrieval"].doc_ids, # 复用原始召回,不重新检索
"retrieval_scores": trace["retrieval"].doc_scores,
# 推理参数取自快照而非当前配置,否则重放的就不是同一件事
"infer_params": bundle.infer_params,
"force_deterministic": i == 0, # 第 0 次固定种子,其余按真实分布采样
}
result = self._exec(bundle=bundle, ctx=ctx)
outcomes.append({
"ok": result.is_valid,
"format_ok": result.structured_parse_ok,
"cited_ok": result.citation_supported,
"latency_ms": result.latency_ms,
"cost_tokens": result.total_tokens,
})
return self._summarize(trace, bundle, outcomes)
def _summarize(self, trace, bundle, outcomes) -> Dict[str, Any]:
"""区分确定性失败与概率性偶发,这决定了问题该怎么修。"""
n = len(outcomes)
fmt_fail_rate = sum(1 for o in outcomes if not o["format_ok"]) / n
cite_fail_rate = sum(1 for o in outcomes if not o["cited_ok"]) / n
return {
"trace_id": trace["id"],
"bundle_id": bundle.bundle_id,
"model_ref": bundle.model_ref,
"prompt_hash": bundle.prompt_hash[:12],
"retrieval_hash": bundle.retrieval_hash[:12],
"index_snapshot": trace["retrieval"].index_snapshot,
# 关键判据:失败率是 0 还是有比例,决定了这是配置问题还是波动
"format_fail_rate": round(fmt_fail_rate, 3),
"citation_fail_rate": round(cite_fail_rate, 3),
"verdict": "确定性失败" if fmt_fail_rate == 1.0 else
"概率性不稳定" if fmt_fail_rate > 0 else "稳定通过",
"p50_latency_ms": sorted(o["latency_ms"] for o in outcomes)[n // 2],
"mean_tokens": round(sum(o["cost_tokens"] for o in outcomes) / n, 1),
}
再生結果の verdict フィールドが、実務上もっとも価値のある出力です。決定的な失敗は、何回サンプリングしても必ず失敗することを意味し、設定や構造上の制約の問題を指します。確率的な不安定さは、ゲートで真偽値ではなく成功率で測る必要がある指標だということです。安定して通過するなら、その trace はたまたま失敗に当たっただけです。この区別はトラブルシューティングの経路を大幅に短縮します。
5.4 原因特定の 3 つの粒度
データが揃ったあとは、原因特定をどの粒度で答えるべきかを整理する必要があります。実務には少なくとも 3 つの粒度があり、コストも用途も異なります。
単一 trace の粒度:「この 1 回の呼び出しで何が起こったか」に答えます。コストが最も低く、データ量が最も多く、「この種の失敗サンプルに共通する特徴は何か」という問いに向きます。bundle_id でグループ化し、各グループ内の失敗率の差を集計します。
セッションの粒度:「この一連のインタラクションの体験が劣化したか」に答えます。ユーザーの知覚により近い粒度です。多くの劣化は複数ターンで初めて現れるからです。セッションの bundle_id の列を記録する必要があり、1 つのセッションが複数の bundle をまたがるなら、そのセッション自体が分析対象になります。
層別(コホート)の粒度:「今回の変更はどの種類のユーザーに一番影響したか」に答えます。ユーザー属性や質問種別でバケット分けして指標の差を見ます。この粒度はもっとも深刻な問題を発見します。すなわち「ある垂直なシーンには重大な被害が出ているが、全体指標への寄与は小さくて見えない」という状態です。全体の指標だけを見ていると、この種の問題は完全に隠されます。
3 つの粒度はどれも必要です。全体の指標は最良の健康シグナルですが、局所的な劣化に対する感度が低すぎます。単一 trace はもっとも敏感ですが、ノイズに陥りやすい。バケット分けだけが両方を両立できる方法です。実務上の経験則は、段階的リリースの期間中は毎日少なくとも 1 回バケット別の結果を見るべきであり、全体の数字だけを見てはいけないということです。全体の数字は「問題ありません」と告げ、バケットは「ある種類のユーザーが被害を受けている」と告げます。
六、回帰ゲート:究竟どの指標で止めるべきか
回帰ゲートはリリースのパイプラインにチェックを設けるものです。基準を満たさなければ段階的リリースへ進めません。shadow traffic との違いは、ゲートの判定が二値で、強制力があるのに対し、shadow traffic の結論は人の参考用だということです。ゲート設計の核心は「何を測るか」ではなく、どの指標が本当に止めるべきものを止められ、どの指標はノイズを生むだけで、人に習慣的に無視されるだけかです。
広く受け入れられている区分は 3 つです。ブロック型ゲートは基準を満たさなければ必ずリリースを止めます。アラート型ゲートは満たさなくても通知するだけで、強制的に止めることはしません。監視型ゲートは記録するだけで、トレンドが明らかに悪化したときにだけ知らせます。すべての指標をブロック型にするのはもっともよくある誤りです。ゲートが誤報を出し、チームが赤に慣れ史上最悪には、ゲートが形式的に存在しつつ、実質的には機能しなくなります。
6.1 構造化ゲート:硬いブロックにすべきもの
構造系の指標は判定が確定的で、主観の余地がなく、誤報率が低いという特徴があります。そのため、これらは天然のブロック型ゲートの候補であり、唯一の本命です。
- パース成功率:出力が JSON や約束した構造として正常にパースできるか。これはブロック型の硬い閾値にすべきです。ひとたびある水準を下回ると、下流のすべての構造依存ロジックが無効になるからです。
- フィールド充足率:必須フィールドの欠落率を、フィールドごとに分けて集計します。全体の充足率は問題を隠す指標です。あるフィールドが 100% 失敗しても、別のフィールドが 100% 成功すれば、平均値だけを見ていては見逃されます。フィールドごとに分けて閾値を課すべきです。
- enum 適合率:分類フィールドの値が約束された enum の集合に含まれているか。モデルは訓練データにないカテゴリ名を出すことがあります。
- 引用の追跡可能性:プロダクトが出典を要求する場合、各引用マークが提供されたコンテキストの中で対応する断片を見つけられるか。これはブロック型にすべきです。もっとも重大な失敗モードである「捏造」に直結するからです。
この 4 項目の共通点は、測っている失敗がユーザーが受け入れられない失敗という点です。「品質がいまひとつ」ではありません。ユーザーは、表示が粗くても形式が正しい答えのほうを望みます。
6.2 セマンティック品質ゲート:不精密さを認めるしかない
セマンティックな品質指標(関連性、完全性、有用さ)の根本的な困難は、それ自体がノイズを含む測定だということです。スコアリングモデルでも、ルールマッチングでも、人によるアノテーションでも、測れるのは「不完全な評価者の、この出力に対する見解」にすぎません。したがってこの種の指標は「絶対値が基準を満たすか」で判定するのではなく、ベースラインからの変化量で判定すべきです。
具体的には、ゲートの正しい形は「ベースラインからの劣化が X ポイントを超えないこと」であり、「スコアが Y 点を超えること」ではありません。後者を課すと、バージョンをまたぎ、時間をまたいで安定して信頼できる絶対基準が必要になります。しかしスコアリングモデルの絶対値は、評価対象の分布が変わると漂います。新しいモデルは出力スタイルが違うため、同じ意味的な品質でも違う得点になるのです。相対量を使えば、このドリフトを自動的に吸収できます。
もう 1 つ必ず処理すべき点があります。それはスコアリング側のバージョン自体のドリフトです。LLM をスコアリングに使っているなら、スコアリングモデルもまた 1 回の変更です。あるときベースラインを再構築する際に黙ってスコアリングモデルのバージョンを差し替えると、それまでのすべてのベースライン値が無効になります。スコアリングモデルのバージョンは、評価対象のバージョンと同じように bundle_id に記録されなければなりません。そうしないと、ゲートが 1 回失敗しただけで、誰を疑えばよいか分からなくなります。
実務上は、セマンティック系のゲートについて 2 つのことを提案します。1 つは複数サンプリングで成功率を取ることで、単発の通過率を避けることです。これは前に述べたとおり、出力がランダムなので、単一サンプルによる通過率の推定ノイズは大きすぎます。実際の通過確率を p、サンプル回数を n とすると、成功率の推定の標準偏差は sqrt(p(1-p)/n) です。p が 0.9 に近く n が 20 しかなくても標準偏差は 6.7 ポイントも残ります。このノイズの量が正しい判定と誤った判定を行き来させるのに十分です。2 つ目はグループ比較ではなくペア比較を使うこと。同じ入力に対して新旧 2 つのバージョンを同時に評価し、「新版のほうが良い / 同じ / 悪い」の比率を集計します。これにより入力の難易度差による分散が消え、サンプル数が少ない条件下で感度を大きく高める標準的な手法になります。
6.3 検索品質ゲート:見落とされがちだが影響は最大
検索層の変更はもっともよくある変更種類のひとつであり、ゲートを迂回しやすい種類でもあります。理由は単純で、検索品質の変化は最終出力にすぐには現れないからです。モデルには一定の寛容性があり、誤ったドキュメントを数件検索結果に混ざっていても、モデルは自身の知識や他の正しいドキュメントから看似合理的な答えを作れます。その結果、検索の劣化は覆い隠され、必要な情報がたまたま目立たないものだったときに初めて露見します。
検索層を止めるには、最終出力だけを見るのではなく、検索という層そのものにゲートを設ける必要があります。核心となる指標はゴールデンセット上のリコール指標です。ビジネスの中に「この質問はどの複数のドキュメントが答えるべきか」という明確なアノテーションがあるなら、Recall@k はそのまま使える指標です。アノテーションがない場合は妥協として「最終回答に現れた重要エンティティが検索結果に現れるか」をプロキシ指標にできます。
検索ゲートの次元がもう 1 つ、見落とされがちです。コンテキストの利用率です。検索が返したドキュメントのうち、実際にモデルが使ったのはどれくらいでしょうか。top_k を 5 から 20 に上げたのに実際に引用されたドキュメント数がほとんど変わらなければ、余分な 15 件はコストとレイテンシを上げるだけで、ノイズを持ち込むだけです。これは「コンテキストに入ったドキュメントのうち、引用または言及された割合」を集計することで測れます。
6.4 コストとレイテンシのゲート:もっとも広く落とされている種類
多くのチームの回帰ゲートは品質だけを測り、コストとレイテンシを測りません。初期段階では大きな問題ではありません。サンプル数も小さく、影響も限定的だったからです。しかし規模に乗れば、コストゲートは必須の要件になります。理由は 2 つあります。
第 1 に、品質とコストは強く相関しているのに、方向は逆になりうるからです。きわめて自然な「最適化」として、モデルの出力をより詳細にする、top_k を大きくする、自検の書き換えを 1 段加える。どれも品質指標を改善し、どれもコストを押し上げます。ゲートで制約されていない限り、指標を追いかけたい気持ちに導かれて、コスト曲線が制御を失うまで次々と積み上がります。
第 2 に、レイテンシの劣化は拡大した後にしか表面化しないからです。同じ入力でも、新しいバージョンの出力 token 数が 40% 増えると、ゲートの固定サンプルでは P50 レイテンシが 5% 上がっただけで済むことがあります。P50 は分布の中央値を反映するからです。出力長の分布は右に極端に偏っており、中央値は影響を体系的に過小評価します。P95 と P99 を見る必要があります。
コストゲートは少なくとも 2 つの量を押さええるべきです。1 リクエストあたりの平均 token 消費量(入力と出力は別々に押さえます。最適化の方向がしばしば正反対になるからです)と成功した 1 リクエストあたりのコストです。後者のほうが意味があります。3 回リトライしてようやく成功したリクエストの実際のコストは 3 回分で、単発のコストだけを見ると過小評価になります。
6.5 ゲート設計の 4 つの原則
ゲートのベースラインは同一の評価セットから取る。対照グループと新バージョンで評価セットが異なっていれば、比較には意味がありません。評価セットは決まったら凍結し、どちら側も自分の側でサンプルを足したり削ったりしてはいけません。
ゲートは相対変化を押さえるべきで、絶対値ではない。絶対値は時間でドリフトします。またチームを「ゲートを通すために評価セットを細かくする」という悪い習慣に誘います。
ゲートの閾値は定期的に再評価する。リリース初期はベースラインが悪いため、閾値を少し広めに置くのは合理的です。システムが成熟するにつれて、閾値は徐々に狭めるべきです。絶対に狭めない回帰ゲートは、基準は上がるのに敷居が動かないため、効果が弱まり続けるだけです。
ゲートが失敗したとき、どの bundle フィールドの差が原因かを直接示せるようにする。失敗メッセージが「品質スコア 71.3 が閾値 72 を下回りました」だけなら、開発者は第 2 章で説明した原因特定の作業を最初からやり直す必要があります。ゲートレポートには、対照グループと実験グループの bundle_id の diff を直接載せるべきです。モデルが違う、system prompt が違う、検索が違う。こうすれば原因特定が 1 ステップで完了します。
| ゲート種別 | 典型的な指標 | 推奨レベル | よくある誤設定 |
|---|---|---|---|
| 構造 | パース成功率、必須フィールド欠落率、引用の追跡可能性 | ブロック | アラートにしてしまい、パース失敗のままでもリリースしてしまう |
| セマンティック | ペア比較の劣化幅、キーエンティティの網羅率 | ブロックまたはアラート | 絶対スコアで判定し、スコアラーのドリフトに惑わされる |
| 検索 | ゴールデンセットの Recall@k、コンテキスト利用率 | ブロック | 最終出力だけを見て、検索の劣化がモデルの寛容性に覆い隠される | コスト | 入力 token、出力 token、成功 1 リクエストあたりのコスト | ブロック | 平均だけを見て高パーセンタイルを止めないため、長尾のリクエストが全体を引きずる |
| レイテンシ | P95、P99、初回 token までのレイテンシ | アラート | P50 だけを見る。出力長の分布が右に偏るため過小評価になる |
七、迅速な rollback の本当のコスト
「ワンクリック rollback」はリリースシステムの中でもっとも安心できる言葉ですが、AI アプリケーションにおいてそれがどの程度成立するかは、今回の変更が実際に何を動かしたかに依存します。モデル、system prompt、検索という三層の rollback のコストはまったく異なる桁にあります。これをまとめて「迅速な rollback」と言うのは意味がありません。
7.1 rollback はトラフィックの切り替えではなく、一貫した状態の再構築である
従来のシステムで rollback が安いのは、状態がきれいだからです。rollback の後、コードは古いものになり、データ構造は古いコードと互換があり、コネクションプールに古い形式のオブジェクトが残っていません。rollback が終われば、すぐに正しい状態に戻ります。
AI アプリケーションでは、この前提がしばしば成立しません。変更期間中に生成され、システムが記憶している産物はいずれも rollback を複雑にします。この種の産物は少なくとも 5 種類あります。
- 永続化された出力:DB、セッション履歴、チケットシステムにすでに書き込まれたモデルの出力。コードを rollback してもこれらのデータは消えないので、ユーザーが次に会話を続けたときに見るのは依然として新バージョンの履歴です。
- キャッシュ:セマンティックキャッシュ、要約キャッシュ、ベクトルキャッシュ。キャッシュ key に bundle_id が含まれていなければ、rollback の後も新バージョンの結果がキャッシュに残り、不整合が継続します。
- 検索インデックス:新バージョンの embedding モデルでインデックスを再構築したなら、古いモデルに rollback すると、古いインデックスが上書きまたは削除されている可能性があり、緊急の全量再構築が必要になります。これは秒単位ではなく時間単位の操作です。
- fine-tuning の産物とオンライン学習データ:ユーザーフィードバックに基づくオンライン更新がある場合、rollback の後も本番は学習を続け、历史のフィードバックがすでに重みの更新に使われている可能性があります。
- 外部副作用:送信済みの通知、作成済みのチケット、消化済みの利用枠。これらはどのシステムでも rollback できません。AI アプリケーションは、これらがまとめて発火しやすくなるだけです。
したがって本当の rollback のコストは、トラフィックを切り替えて回復するまでの時間と、上記 5 種類の産物が再び一貫性を取り戻すまでの時間の和です。前者は通常は秒単位ですが、後者は数分から数時間までばらつくかもしれません。そしてインシデントレビューでの結論は後ろを見るべきです。ユーザーが感じているのは後ろだからです。
7.2 system prompt の rollback は安いが、無料ではない
system prompt の rollback が安いのは、system prompt が純粋なデータだからです。戻すだけですぐ反映され、外部依存がなく、すでに生成されたデータにも影響しません。
しかし見落とされがちなコストが 2 つあります。1 つは認知的コストです。system prompt は通常長く、rollback とは 1 つのパラメータを変えることではなく、テキスト全体を完全に復元することを意味します。モジュール単位で組み立てられている場合、ロールバックしたモジュールが他のモジュールと暗黙の依存を持っていると(ある場所で制約を取り除いたが、別の場所の文言がその制約の存在を前提としている)、1 つのモジュールだけを戻すことで、一度もテストされていない組み合わせが生まれる可能性があります。
これが system prompt のバージョン管理におけるもっとも隠れたリスクです。モジュール化された system prompt は組み合わせの爆発を生むが、段階的リリースのシステムがテストしたのは検証済みの組み合わせだけだからです。実験で 3 つのモジュールを動かすなら、段階的リリースの期間中に検証されるのはその 3 つの新しい組み合わせであり、rollback のときに戻るのも古い組み合わせではなく新しい組み合わせになります。
解決策は rollback に粒度を定義することです。全体を最後の既知の正常な状態に戻すか(この場合、段階的リリースの期間中のすべての実験を止める必要があります)、あるいは 1 つのモジュールだけに戻すか(この場合、この新しい組み合わせを検証する必要があります)。後者はより柔軟ですが、任意の組み合わせを迅速に検証できる能力が必要であり、ここでも回帰ゲートの能力の問題に戻ります。
2 つ目は権限とプロセスのコストです。system prompt を業務側が直接編集しているなら、rollback には業務側の再編集が必要です。しかし業務側には「どのバージョンのどの部分が壊れているか」を判断する能力は通常ありません。よって system prompt の rollback の技術的な手順はエンジニアリング側で準備し、前もって用意しておくべきです。直前の既知の正常なバージョンの完全な system prompt を候補として事前に用意しておけば、rollback は切り替え操作 1 回で済み、内容を後から探して組み立てる必要がなくなります。
7.3 検索層の rollback:もっとも高くつく層かもしれない
検索層の rollback のコストは、変更の種類によって決まります。top_k を変えるとか、ハイブリッドの重みを変えるとかなら、rollback はパラメータを 1 つ直すだけなので秒単位です。ただし、次の 3 つは本当に高くつきます。
embedding モデルを変えた。古いモデルと新しいモデルのベクトル空間は完全に互換性がないため、古いインデックスを新しいモデルでは使えません。rollback には古いインデックスを残し、GC されないようにしておく必要があり、新旧 2 組のインデックスを並行して運用しなければなりません。データ量の多い側では、緊急でインデックスを再構築するのに数時間かかることがあります。
チャンク分割戦略を変えた。チャンク分割戦略の変更は embedding モデルと同じようにインデックスの互換性を失わせます。しかしより隠れているのは、多くのベクトル DB のメタデータにチャンク分割パラメータが記録されており、混在してもエラーにならず、静かにリコール品質が落ちるからです。そのためチャンク分割戦略のバージョンはインデックスのスナップショットと強く紐づけなければならず、同じスナップショット番号で 2 つの分割戦略を混在させることを認めてはいけません。
ドキュメントそのものを変えた。これがもっとも厄介な場合です。今回の変更が「誤ったドキュメントを 1 件修正した」あるいは「古いドキュメントを 一括削除した」なら、rollback は意味論的に受け入れられません。指標を好看くするために誤った内容を戻すようなことを人はやりません。この場合の「rollback」という業務上の行為そのものが成立せず、前に直すしかありません。
したがって正しいやり方は、変更を設計する段階で各層の rollback 能力に等級を付けることであり、これを変更レビューに含めることです。以下の表を参照してください。この注記の目的は、「今回は迅速に rollback できる」という発言を、楽観的な約束ではなく検証可能な判断に変えることにあります。
| 階層 | rollback 動作 | 典型的な所要時間 | 前提条件 |
|---|---|---|---|
| system prompt モジュール | 直前の既知の正常なバージョンに戻す | 秒単位 | バージョンが content addressing されており、旧バージョンを取得できる |
| モデルの推論パラメータ | 古いパラメータの組み合わせに戻す | 秒単位 | パラメータが bundle とともに記録されている |
| モデルの重みバージョン | 古いデプロイの推論インスタンスに戻す | 分単位 | 旧バージョンが回収されておらず、並列な容量がある |
| インデックスと検索設定 | 古いインデックスのスナップショットとパラメータに戻す | 分から時間単位 | 古いインデックスが保持され、分割戦略がスナップショットと強く紐づいている |
| embedding モデル | 古いモデルに戻し、インデックスを緊急再構築する | 時間単位 | 古いモデルがデプロイ可能で、全量再構築の能力を保持している |
| ドキュメントの内容変更 | 意味論的に rollback できない | 該当なし | レビュー段階で必ず明示しておく必要がある |
7.4 rollback を 定常化 させる。事故時の例外にしない
rollback が事故のときだけ通り、本当に必要なときにたいてい機能しません。最低限 3 つのことは押さえておく必要があります。
定期的な演習。しばらくごとに、トラフィックの閑散な時間に実際に rollback を 1 回実行し、実際の所要時間を測定して記録します。この数字こそ事故時に提示できる約束であって、「たぶん速い」ではありません。多くのチームは、演習して初めて rollback スクリプトがすでに 2 つバージョンを退役した内部インターフェースに依存していたことに気づきます。
2 バージョン分の並列容量を維持する。rollback の前提は、戻す先のバージョンがなお動いていることです。モデルのバージョンをリリースした瞬間に古いインスタンスを 0 まで縮めてしまうと、「rollback」は実際には「再デプロイ」になり、所要時間は秒単位から時間単位に変わります。妥協案として、古いバージョンに最小の容量(たとえば 10%)を残すようにします。これならコストを過度にかけずに、rollback の即時性を確保できます。
rollback 可能性を設計上の制約にする。これがもっとも見落とされがちで、多くの設計判断を逆に制約します。キャッシュ key には bundle_id が含まれなければならない。system prompt は content addressing でなければならない。インデックスのスナップショットは明示的に宣言されなければならない。副作用はフレームワークのレベルで遮断可能でなければならない。これらの制約は潔癖のように聞こえますが、その唯一の役割は rollback という動作を単純に保つことです。誰かがこのうちのいずれかを簡略化しようとしたとき、投げかけるべき問いは「それは rollback を難しくするか」です。
八、段階的リリースの期間中に何を見るべきか
ここまでの 7 章で話したのは設計です。原因特定は能力であり、監視は感覚です。段階的リリースの期間中の問題は、たいてい調査する能力がないことではなく、まだ間に合うときに見ていないことにあります。この章では「シグナルが現れる速度」で監視項目を並べます。秒級から日級まで層に分けたのは、時間のスケールごとにまったく異なる対応動作があるためです。
8.1 分単位のシグナル:すぐに観測でき、快速な損害防止に使う
この種のシグナルの共通点は遅延がゼロだということです。リクエストが発生した瞬間に観測できます。「なぜ」なのかまでは教えてくれませんが、「今すぐ止めなければならない」ことは伝えます。
- エラー率と例外種の分布:タイムアウト率、レート制限率、上流への接続失敗率、そしてもっとも重要な例外種そのものの分布の変化。全体のエラー率が横ばいでも例外の構成が変わっていれば、新バージョンが新しい失敗モードを持ち込んだ可能性があり、段階的リリースの期間中にもっとも感度の高いシグナルのひとつです。
- 出力長の分布:token 数の P50 と P95。出力が急に短くなったなら、モデルが手を抜くようになったか、拒答しやすくなったことを意味し、長くなったなら同じことを繰り返していることを意味します。どちらも早期警戒信号です。
- 構造適合率:前述のパース成功率です。段階的リリースの期間中は、ゲート段階で 1 回測るのではなくリアルタイム指標として監視すべきです。
- レイテンシのパーセンタイル:P50、P95、P99、そして初回 token までのレイテンシ。段階的リリースの期間中は比率が低くリソースの負荷も小さいため、レイテンシの異常は比率が上がってから目立ちます。しかし早く現れるほどよいことです。
- 空出力と切り詰め率:max_tokens の上限に達して切り詰められた割合の上昇は、ユーザーが受け取る答えがすべて不完全であることを意味します。
分単位のシグナルは必ずbundle_id で分割したダッシュボードにしなければなりません。全体のダッシュボードしかない場合、実験グループはトラフィックの 5% しか占めず、その異常は全体の曲線上で 20 分の 1 に希釈され、ノイズにほぼ確実に埋もれます。これは段階的リリースの監視でもっとも多い失敗の形です。
8.2 時間単位のシグナル:集計されて初めて意味を持つ
この種のシグナルは安定した数値になるまで一定のサンプル量が必要です。拡大の判断に使うのに適しています。
拒答率とフォールバック率。モデルが明示的に拒否した、安全ポリシーが発動した、劣化した経路を通った、フォールバックのテンプレートを使った。这类のシグナルの変化は通常、「モデルが特定の入力を処理する方式が変わった」ということを反映しており、system prompt とモデル変更にもっとも典型的の中間層の現れです。
引用の裏付け率。出力の中で「某某のドキュメントに由来する」と主張する内容が、実際にそのドキュメントに存在するか。これは「幻覚型」の劣化を捕まえる最良のリアルタイム指標であり、しかも人手によるアノテーションは不要で、ルールだけで計算できます。その価値は、主観的なスコアリングよりずっと速く、しかも誤報がほとんどないことにあります。
検索層の直接の指標。空リコール率、検索結果数の分布、類似度スコアの分布。空リコール率の上昇はクエリとインデックスのマッチングに問題 ということであり、これは最終回答の品質低下より早く現れることが多いのです。
キャッシュヒット率と 1 リクエストあたりの token コスト。ヒット率は段階的リリースの期間中にこっそり悪化する、もっとも静かに悪化する指標です。いかなる品質指標にも現れないからです。新バージョンの system prompt が長くなれば、セマンティックキャッシュの key 空間や類似度閾値がもはや適応しない可能性があります。ヒット率が下がり、1 リクエストあたりのコストは上がる。ユーザーとビジネスが感じるのは「遅くなった、高くなった」ということです。
8.3 日単位のシグナル:本当の被害のほとんどはここで現れる
前の 2 つの分類は「壊れた」ことを捕まえられても、「悪化した」ことは捕まえません。後者は AI のリリースにおいてより一般的で、より高価な問題です。
セッションのターン数と再質問率。新しいバージョンによってユーザーがより頻繁に質問し直すようになったなら、単一ターンの品質が低下しています。多ターンのシーンでもっとも有効な品質シグナルのひとつです。完全に行動に基づくため、いかなる人手アノテーションやスコアリングモデルも必要としないからです。
自発的な離脱率とタイムアウト離脱率。ユーザーが読み途中で離脱する、あるいは生成開始後すぐ閉じる。旧バージョンで正常な範囲内にあったこの 2 つの行動が、新しいバージョンで上昇していれば、明確な品質劣化のシグナルです。
明示的なネガティブフィードバックの絶対数と分布。低評価、苦情、人によるサポートへのエスカレーション。総量だけでなく分布に注意してください。総量が横ばいでも苦情の理由の分類が変わっていれば、問題の性質が変わったということです。「解決していない」から「間違っている」になった、これはまったく異なる調査の方向性です。
シナリオごとにバケット分けした指標の差。日単位の項目でもっとも重要なものです。全体の指標は「問題ない」と告げ、バケットの指標は「あるシナリオが被害を受けている」と告げます。実務上 manejously 見落とされる重大な問題の多くは、ロングテールのシナリオの劣化が全体の数字に隠されたものです。バケットの軸としては、業務ライン、ユーザー権限、質問種別の分布、リクエストの複雑さ(入力長、複数ターンかどうか)、地域や言語などが考えられます。
8.4 拡大のリズム:指標より重要なリズムそのもの
監視リストが揃ったあとは、残った問題はいつ拡大するかを決める方法です。これは実務上非常に有効な原則がいくつかあります。
段階的に拡大し、各段階に最小の観測ウィンドウを置く。一般的な段階は 1% → 5% → 20% → 50% → 100% です。重要なのは段階そのものではなく、各段階で十分な時間を置くことです。そうすれば前述の 3 つの種類のシグナルがそれぞれ現有机会を持てます。1% は分単位のシグナル、5% から 20% は時間単位、20% 以上で初めて日単位の分布の差が見えてきます。多くの事故のメカニズムは「拡大が速すぎて観測ウィンドウが不足した」というものです。悪いシグナルがないのではなく、悪いシグナルがノイズから浮き出す前に見られたのです。
閑散時に開始しますが、閑散時に開始してそのままピークへ拡大してはいけません。直感に反して正しい提案があります。拡大の決定タイミングではトラフィックの構造を考慮すべきです。ピーク時のリクエスト分布は閑散時とは異なり、とくにロングテールのシーンで違いが出ます。閑散時に拡大して「すべて正常」と見たままピークへ進めるなら、閑散時のサンプルでピーク時の挙動を推測していることになります。
連休の前後では拡大しない。技術的な判断ではありませんが、影響は大きいです。連休中のトラフィック構造、データ分布、ユーザーの行動は通常と明らかに異なるため、このときの実験の結論を平日にそのまま移すのは難しいのです。
rollback 判断のためのウィンドウを十分に残す。50% まで拡大したあとは、少なくとも完全な 1 ビジネスサイクル(業務に明らかな日周期があれば 24 時間)を観察してから、全量にするかどうかを判断します。この項目の目的は日単位のシグナルに時間を与えることです。多くのチームは 50% に 2 時間未満しか留まらず全量にします。データ上は「問題なし」と見えますが、その「問題なし」は単に観測時間が足りなかっただけです。
8.5 よくある反パターン:単独の North Star 指標だけで段階的リリースを見張る
最後に、頻度の高い誤ったやり方を 1 つ挙げます。段階的リリースの期間中に 1 つの総合スコアだけを見るのです。このスコアは通常、いくつかのサブ指標を加重平均して得られるもので、情報密度は高く見えますが、実際の効果は局所的な劣化への感度というものを完全に失わせることです。
理由は加重平均の性質にあります。あるサブ指標が下がると別のサブ指標が上がる場合、総合スコアはほぼ動きません。しかし実際には、もうある種類のユーザーに被害を与えています。もっとも悪いのは、このやり方がチームにその総合スコアを最適化するよう促し、結果として無意識に「片方の損失をもう片方で埋める」ような変更を行うことです。
より妥当な構造は 3 層です。North Star(ただ 1 つの、今回の変更の主な目標を表すもの)、guardrail 指標(複数の指標の集合で、どれか 1 つが閾値を超えたら rollback とし、加算には一切含めない)、診断指標(bundle_id とバケットで分割し、問題が起きたときにだけ原因特定に使う)。guardrail 指標は North Star から独立して判定しなければならない。これが監視構造の全体でもっとも重要な規律です。
九、このすべてをリリースプロセスに定着させる
ここまでの 8 章で話したのはすべて原則です。原則をプロセスに落とし込まないと、特定のチームのベストプラクティスに退化し、システム的な能力にはなりません。この章では、上述の制約を個人の自覚に頼らないプロセスに変える方法を考えます。
9.1 変更レビューのチェックリスト
複数層を同時に動かす変更をレビューに提出するときには、以下のチェックリストを添付すべきです。その価値は記入すること自体ではなく、レビュアーが手を動かす前に影響範囲を考えさせることにあります。ほとんどの事故の根本原因は、手を動かす前に誰も自分が 3 層を動かしたことに気づいていなかったことです。
- 変更範囲:今回のリリースで動かした層(モデル / system prompt / 検索 / オーケストレーション)と、各層の具体的な差分。
- rollback 能力の注記:各層の rollback 動作、予想される所要時間、そして意味論的に rollback できないかどうか。rollback できない項目はレビュー段階で明示し、受け入れられなければなりません。
- 評価セットの状態:今回使った評価セットはどれか、そのバージョン番号は何か、対照グループがまったく同じ評価セットで実行されたか。
- ゲート結果:各カテゴリのゲートの相対的な変化であり、絶対スコアではないこと。同時に、どのゲートが欠けていてその理由は何かも明記します。
- 実験設計:分流の単位(単一変数ではなく組み合わせ)、分流比率、観測ウィンドウ、停止ルール、guardrail 指標とその閾値。
- インデックスへの影響:インデックスの再構築を伴うか、再構築中のクエリ戦略は何か、新旧のインデックスを並行運用できるか。
- 段階的リリースのリズム:段階的なプラン、各段階での最小滞在時間、各段階の判断を担当する人。
9.2 4 つの反パターン
実務でもっともよくある、しかももっとも高額な間違いを個別に挙げます。いずれも共通の特徴があり、短期的には効率を改善するように見えることです。
反パターン 1:3 層の変更を 3 回の独立したリリースに分割する。直感的には「リスクを下げる」手法ですが、それぞれを単独でリリースすると、評価しているのは部分的に最適化した中間状態です。この状態は最適化前より悪いことがよくあります。たとえばモデルを先に切り替え、system prompt を後から調整するなら、2 回のリリースの間には、実際に長期的には動かしたくない新旧の組み合わせが本番で走ります。その中間状態のために完全なゲートと実験を別途行うコストは 3 倍です。それなのに中間状態自体には価値がありません。
より良いやり方はこうです。オフラインでは 3 層を分解してそれぞれ検証し、オンラインでは 1 つの組み合わせとしてまとめて拡大する。オフラインの制御実験は各層の影響を理解するためにあり、オンラインの組み合わせ実験は「その組み合わせがより良いか」を検証する責任を持ちます。両者が答える問いは異なるので、混用してはいけません。
反パターン 2:最終出力だけ測り、途中の工程を測らない。これは前述の検索ゲートの問題の一般的バージョンです。最終出力だけを測ると、2 つの困難があります。ひとつはモデルの寛容性が中間層の劣化を覆い隠すこと、もうひとつは最終出力の評価ノイズが大きくシグナルが弱いことです。中間工程(検索結果、rerank の結果、構造化制約の満たされ方)の評価は決定的で、安価で、敏感なので、優先的に測るべきものです。
反パターン 3:ゲートは厳しくするが、閾値は勘で決めている。閾値の根拠は 3 つしかなく、どれも勘で決めるより明快です。過去 30 日間の分布の分位数から取る(たとえば「過去 30 日の P10 を下回らない」)、「単リクエストのコストは単位経済モデルを守るために X 以下でなければならない」という目標から逆算する、あるいは「P95 のレイテンシは 3 秒を超えてはいけない」のように、ユーザーが許容できる体験の下限から取る、というものです。勘で決めた閾値は、誤報を出しすぎて慣れさせるか、緩すぎて何も止めないかのどちらかです。
反パターン 4:段階的リリースを、リリース前の最後の工程として扱う。段階的リリースがプロセス全体の最後の一環だとしたら、それ以前の検証能力が不足していて、最後の防衛線に頼るしかないということです。理想のかたちは、段階的リリースの考え方を開発段階へ前倒しすることです。ローカルで shadow モードを立ち上げて実際のトラフィックを流し、PR のたびに固定のゴールデンセットで bundle の差分を比較し、その結果を PR の一部として残す。そうすれば、実際に段階的リリースを行うときに目の前にあるのは、繰り返しく検証された候補バージョンではなく、初めて本番に出る未知の組み合わせではありません。
9.3 インフラの優先順位
リソースが限られており、一部しか構築できないなら、妥当な順序は次のとおりです。
第一優先:bundle_id と trace の伝播。すべての能力の地盤です。これがないと、後述のすべての手段が生み出すのは位置の特定できないデータになります。このステップのコストは最も低く、効果は最も高い。通常はリクエストの入口といくつかの計測点の修正だけで済みます。
第二優先:shadow traffic と構造化ゲート。この 2 つを合わせると「壊れるか」というもっとも優先度の高いリスクをカバーできます。どちらも決定的な判定で、誤報率も低いからです。
第三優先:bundle_id で分割した監視ダッシュボード。これが損害を止める能力の保障です。コストは主にダッシュボードの構築にあります。
第四優先:回帰ゲートの完全な体系。セマンティックゲート、検索ゲート、コストゲートを含みます。このステップの投資はもっとも大きく、評価セットとスコアリング体系が先に整っている必要があります。
第五優先:直交分流とセッション単位の粘着性。複数の実験を並行して走らせるようになったときに初めて必要になりますが、一度必要になればかなり厄介な事態になります。
注目すべきはこの順序には「リアルタイムの大盤ダッシュボード」が含まれていない点です。しかも多くのチームの最初の投資はまさにそれです。これは偶然ではありません。原因特定の能力がまだできる前に、大盤ダッシュボードが教えてくれるのは「何が起きたか」だけで、「なぜ」と「どうすればいいか」は教えてくれないからです。
十、このルールを具体的な変更に当てはめてみる
これまで話したのはすべて原則です。原則は頭のなかでは成り立ちますが、実践では忘れられがちです。ここではその原則を具体的な形態の変更に一度当てはめ、それぞれの制約がどこで効くのかを見ていきます。
想定するシナリオはこうです。客服向けの質問応答アプリケーションについて、モデル A からモデル B へアップグレードし、同時にシステム prompt を調整し、自己検査の書き換えを 2 段増やし、検索の top_k を 5 から 8 に上げる。これは典型的な 3 層の同時変更であり、まさに本文の冒頭で述べたような原因特定が難しい状況そのものです。
10.1 まず、この変更を分割できるかを判断する
前述の反パターン 1 の方針に従えば、この変更を 3 回のリリースに分けるべきではありません。しかしオフラインの 4 つの実験には分割できます。まずモデル B と元の system prompt で 1 回走らせ、モデルアップグレード単独の貢献を量化します。次にモデル B と新しい system prompt で 1 回走らせ、system prompt を適合させたほうが良くなるかを見ます。さらに自己検査の書き換えを加えて、その限界的な貢献を見ます。最後に top_k を調整し、検索側の限界的な貢献を見ます。
この 4 つのオフライン実験の目的は、どの組み合わせをリリースするかを決めることではなく、各層がどう振る舞うかを理解することです。おかげでレビューでは「system prompt の適合が改善の主な原因で、自己検査の書き換えがさらに 3 ポイントの改善を加えたが、コストは 40% 上がった」といった具体的な言葉で語れるようになります。「組み合わせが少し良い気がする」で済ませる代わりに。
そしてオンラインでは、この組み合わせだけをリリースします。オンラインの実験が答えるのはまったく異なる問いです。この組み合わせは、実際のユーザーのところでもより良いのかどうかです。
10.2 この変更の rollback 能力はどのレベルか
第 7 章の表に沿って注記します。モデルの重みを A から B へ変える場合、rollback は古いデプロイの推論インスタンスに戻すことで、数分単位で、前提条件は旧バージョンの最小容量を残すことです。system prompt の変更については、rollback は直前の既知の正常なバージョンに戻すことなので秒単位ですが、新しい system prompt が自己検査の書き換えへの依存を追加していないかを確認する必要があります。依存があれば、system prompt だけ戻してオーケストレーションのロジックを戻さないと、参照が宙に浮きます。検索の top_k はパラメータを 1 つ直すだけで数秒単位で、インデックスの再構築は伴いません。
この 3 層はいずれも rollback 可能であり、組み合わせ全体を rollback することもできます。レビューの結論は、段階的リリースに進んでよい、rollback の想定最大所要時間は分単位のモデル切り替えである、というものです。この数字を書き残すのは、「迅速に rollback できる」と書くよりはるかに有益です。
もう 1 つ、組み合わせレベルの点に注意してください。今回の変更では embedding モデルは交換しておらず、チャンク分割戦略も変えていないため、インデックスの再構築は不要で、インデックスのスナップショット番号は変わりません。この点は必ずレビュー記録に明記しなければなりません。「インデックスが変わったかどうか」はもっとも見落とされがちで、しかも影響が最大のフィールドだからです。
10.3 ゲートはどう設けるか
構造系はすべてブロックします。パース成功率、必須フィールド、enum 適合、引用の追跡可能性です。これらはモデルアップグレードにおいて特に重要です。新しいモデルの指示への厳密さは旧モデルと違う可能性が高いためです。
セマンティック系は相対的な劣化幅でブロックし、閾値は 3 ポイント、判定方式はペア比較と複数サンプリングの組合せにします。検索系もブロックで、ゴールデンセットの Recall@5 がベースラインを下回ってはならず、Recall@8 の向上はコストに見合うかどうかを別途確認します。コスト系もブロックで、入力 token と出力 token に別々の閾値を設定します。自己検査の書き換えは必ず出力 token を大きく押し上げるので、受け入れる上限を事前に決めておく必要があり、リリース後にコストが倍になったことに気づくべきではありません。
レイテンシ系はアラートだけにします。段階的リリースの期間中は比率が低く、レイテンシのデータの信頼性自体が限られているためです。
10.4 段階的リリース期間中に何を見て、何が見えるべきか
今回の変更に対する合理的な期待はこうです。品質指標は少し上がり、出力 token 数は明確に増え、引用の裏付け率は自己検査の書き換えの存在によって上がり(書き換えの段階は根拠の確認と補充を行うからです)、P95 のレイテンシは上がる。実際に観測された姿がこの期待と大きく違うなら、それ自体が警戒すべきシグナルです。
シグナルごとに具体的に言うと、分単位では構造適合率と出力長の分布を見ます。自己検査の書き換えでもっとも起こりやすい問題は、出力が切り詰められることだからです。時間単位では引用の裏付け率とキャッシュヒット率を見ます。system prompt が長くなればセマンティックキャッシュとの適合性に影響する可能性が高いからです。日単位ではセッションのターン数を見ます。多ターンの中でコンテキストが自己検査の書き換えの内容に圧迫されると、質問の繰り返し率が高まるからです。
最後は 1% → 5% → 20% → 50% → 100% の順で拡大し、各段階で少なくとも完全なトラフィック周期を観察し、50% では 24 時間以上留まってから全量にするかどうかを決めます。責任者は固定し、各段階の判断は全体ダッシュボードではなく bundle_id で分割したダッシュボードを見ます。
この一連の点検に要した時間は約 20 分ですが、前述の 8 章のすべての制約をカバーしています。逆にこのプロセスを踏まなければ、もっとも起こりうる結果はこうです。リリース後に出力 token が 60% 増え、コストが想定を超え、セッションのターン数が増え、原因が自己検査の書き換えと system prompt の長さの複合作用だと特定したときには、すでに 50% のトラフィックを切り替えており、rollback してもコストは元に戻っても、品質の問題がなぜ缓慢にしか改善しないのかは説明できません。
おわりに
冒頭の問いに戻ります。1 回の変更でモデル、system prompt、検索を同時に動かしたとき、問題が起きたら確実に原因を特定するにはどうすればいいのか。
私の答えは 3 層です。最も底の層は識別能力です。任意のリクエストが「どのモデル、どの system prompt、どの検索結果」まで還元できなければならない。その還元は、漂うバージョン番号の参照ではなく、content addressing による不変のスナップショットに支えられている必要があります。この層が不確かなら、その上のすべての段階的リリースの手段が位置の特定できないデータを生み出します。
中間の層は手段の分担です。shadow traffic が「壊れるか」に答え、A/B 分流が「より良くなったか」に答え、回帰ゲートが「許容できる下限を割ったか」に答えます。3 つは異なる象限をカバーしており、代替関係ではありません。本当のエンジニアリングの判断力が問われるのは、それぞれの手段が何に答えられないかを知っていることです。shadow はインタラクションの劣化を測れず、A/B は沈黙の被害を見ず、ゲートは品質が上がったかどうかを判定できません。
一番上の層は規律です。guardrail 指標を North Star から独立して判定すること、各段階の拡大で十分な最小観測ウィンドウを置くこと、rollback できない変更をレビュー段階で印出しておくこと、そして rollback を単純に保つための設計上の制約を 1 つずつ守ることです。これらは新しい技術を必要としません。しかし、どの技術よりも実行が難しいのは、人が最も忙しいときにも手順に従うことを要求するからです。
AI アプリケーションのリリースは、本質的に一度きりの納品イベントではなく、継続的な科学的実験のプロセスです。この転換が求めるのは、いかなる時点でも「いま本番で動いているのは何の設定なのか」、そして「この設定は前回と比べて何が変わったのか」に答えられる能力です。この 2 つの問いの答えが、事故の中で選択肢を持っているのか、それとも指標が下がるのを見て祈るしかないのかを決めます。