
LLM/RAGの回帰テストをCIに組み込む設計ガイド:評価セットから運用レポートまで
大規模言語モデル(LLM)やRAG(検索で拾った文書を根拠に回答を作る仕組み)を本番で運用していると、プロンプトを1行変えたり、モデルや索引を更新した直後に、回答の「当たり」が少しずつズレていくことがあります。 ログを追っても原因が一発で特定できず、とりあえずロールバック...そんな手戻りを減らしたいチーム向けの話です。
この記事では、検索(retrieval)と生成(generation)を分けた評価設計を、CI(継続的インテグレーション)上の品質ゲートに落とし込む手順として整理します。合否基準の決め方、同じ入力でも出力が揺れる性質(非決定性)への扱い、例外運用まで含めて、回帰 テストを「属人的な確認作業」から「チームで回る型」に寄せるための判断ポイントとテンプレをまとめます。
ゴールと前提: 何をCIで止めたいか
本番運用しているチームだと、先週まで動いていたRAGチャットボットが今朝のデプロイ後に見当違いの回答を返し始める、という手戻りが起きることがあるかもしれません。ログを見ても原因がすぐ分からず、とりあえずロールバックをした経験を持つ方もいるかもしれません。
LLMやRAGを組み込んだシステムでは、従来のテストだけでは変更の影響が見えにくい場面があります。
これは、同じ入力でも出力が揺れることがある(非決定性)が原因かもしれません。
たとえばOpenAI Evalsなどでは、この前提を置いたうえで評価をテストとして扱い、変更前後の差分を見える化する考え方が紹介されています。ここでいうLLM回帰テストとは、変更前後の品質低下をCI上の品質ゲートで検知し、気づかないままのリリースを減らすための仕組みです。
品質ゲートで決めること: ブロックか警告か
CIに回帰テストを組み込むとき、最初に決めておきたいのが「何が落ちたらマージを止めるか」という線引きです。すべてのスコア低下をブロッキングとして止めてしまうと、出力の揺れによるフレーク(偽陽性: 本当は問題ないのにテストが落ちる現象)でパイプラインが止ま り続け、チームが疲弊してしまうかもしれません。逆にすべて警告にすると、サイレント劣化を見逃す原因になります。
PASHでは、このような問題に対して、次のように2段に分けると運用が回りやすいと考えています。
- ブロッキング(マージ不可): 禁止表現の出力、根拠なし回答の増加、検索ヒット率の閾値割れなど、ユーザー影響が大きい失敗
- 警告(レビュー付きで通過可): 文体の揺れ、冗長度の変化、スコアの軽微な低下など、人が見て判断すべき変化
線引きの正解はチームごとに異なりますが、「なぜブロッキングにしたか」の理由を言語化して合意を残しておくと、あとで判断がブレにくくなります。
CIゲートは最初からブロッキングにすべきか
CIゲートを最初からブロッキングにすべきかはチーム状況によって変わるかもしれません。でも、出力の揺れが大きく評価セットも育成中であれば、まずは警告中心でベースラインを集めるほうが運用は崩れにくいでしょう。
例えば、2週間ほどスコアの揺れ幅を観測してから閾値を決め、ブロッキングに昇格させる流れがスムーズです。一方で、禁止表現や根拠なし回答など、ユーザー影響が大きい失敗は最初からブロッキングに寄せておくと安心です。
スコアだけで判断しない
評価スコア(0〜1のような数値)は便利ですが、数字だけ見ていると「そもそも正しい問いを評価しているか」が抜け落ち がちです。人手の判断と組み合わせて、評価セット自体が実運用の失敗パターンを反映しているかを定期的に確認しておくとよいでしょう。評価は一度作って終わりではなく、ログから評価ケースを採掘して継続的に育てていくプロセスとして扱うほうが運用に乗りやすくなります。
「テストが通っているから安心」ではなく、「主要な失敗パターンを検知できている」と言い切れる状態を目指すのが、品質ゲートの本来の役割だと考えています。
AI検索で引用されやすい情報の作り方については、AI検索時代の情報設計ガイドでも触れています。
回帰の原因を4分類する: モデル・プロンプト・索引・データ
止めたい品質ラインが決まったら、次は「何が変わって壊れたか」の切り分けです。プロンプトを直すのか、モデルを戻すのか——分類が先にないと議論が空転しがちです。
たとえば回帰の原因を以下の4つに整理しておくと、切り分けがしやすくなります。
- モデル変更
- プロンプト変更
- RAG索引更新
- データ更新
RAGの挙動はretriever・コーパス・LLM・プロンプトの4つが絡み合って決まるため、どれが変わったかを記録していないと原因を追いにくくなってしまいます。
モデル変更
モデル変更に該当するのは、APIプロバイダのバージョンアップや、ファインチューニング済みモデルの差し替えなどです。モデル名・バージョン・切り替え日時の3点を残しておくと、切り替え前後で同一の評価セットを流して出力差分を比較しやすくなります。プロバイダ側の更新はチームの意思と無関係に起きることもあり、サイレント劣化の典型原因になりがちです。
プロンプト変更
プロンプト変更としては、システムプロンプトやフューショット例の書き換えなどがあります。Gitコミットハッシュと変更差分を押さえておき、変更の意図もコミットメッセージに残しておくとあとの切り分けが速くなります。プロンプト変更が回帰につながったかどうかを検出することは、LLM統合の評価設計で中心的な関心事とされています。
RAG索引(コーパス)更新
RAG索引更新の例としては、ベクトルストアの再構築やチャンク分割ロジックの変更などがあります。索引更新は検索結果を変え、生成にも波及するため、ビルドID・チャンク設定・埋め込みモデル名をセットで残しておくと安心です。retrieval側かgeneration側かを切り分けるには、索引単体のRecall@Kを別途記録しておくのが有効です。
データ更新
データ更新にあたるのは、FAQ追加やナレッジベースの変更、マスタデータの入れ替えなどです。更新頻度が高い割に評価セットとの整合が崩れやすいため、データソースのバージョンと変更件数を追っておかないと「正解が変わったのにテストが古いまま」という逆転が起 きがちです。
ここまで4つの分類を見てきましたが、もう1点気をつけたいのが採点器(evaluator)自体の変更です。LLMに採点させる手法(LLM-as-a-judge)を使う場合、採点器のプロンプトやモデルもバージョン管理の対象になります(詳細は後述の採点器自体の変動に注意を参照してください)。
変更が起きたとき、4分類のどれに該当するかを1行で書く欄をPRテンプレートに足しておくと、初動が速くなります。直近1週間のデプロイログを見て、最も頻繁に動いている分類から優先度をつけていくとよいでしょう。
テスト設計: retrieval と generation を分けて失敗パターンを決める
原因を4分類できても、「回答がおかしい」だけでは次の一手が見えにくいものです。検索の問題なのか、生成の問題なのか——先に切り分けておくと対処の選択肢が絞れます。
なぜ検索と生成を分けて評価するか
RAGパイプラインは大きく2段階で動きます。クエリから関連文書を取得する検索(retrieval)と、その結果をもとに回答を組み立てる生成(generation)です。この2つを一括で採点してしまうと、どちらが劣化したのか見えにくくなります。
たとえば社内ナレッジ検索で「育休の申請期限は?」に誤答が返ったとします。原因は大きく2つ考えられます。古い就業規則が上位に来てしま った検索の問題か、正しい文書を取得したのに日付を誤読した生成の問題か——両者で対処方法はまるで違ってきます。
検索側の失敗パターン
検索ステップで起きやすい失敗は主に3パターンです。
- 取得漏れ(Recall低下): 正解文書がTop-Kに入らない。索引更新やチャンク分割の変更後に発生しやすい
- ノイズ混入(Precision低下): 無関係な文書が上位を占め、生成を汚染する。クエリ書き換えロジックの変更で起きやすい
- 順位逆転: 正解文書は取得できているが順位が下がり、コンテキスト窓から外れる
評価指標はRecall@K(正解が上位K件に含まれる割合)やPrecision@K(上位K件の正解率)、MRR(正解が上位に来るほど高くなる指標)などが該当します。LLMを呼ばずルールベースで算出でき、コストが低く再現性も高い点がメリットです。
生成側の失敗パターン
生成ステップでは、検索結果が正しくても以下のような失敗が起きることがあります。
- 幻覚(ハルシネーション): 取得文書にない情報を生成する。groundedness(根拠忠実性)の低下が兆候になる
- 省略・欠落: 根拠文書中の条件や例外を落とす。answer relevanceの低下として現れる
- 形式崩れ: JSON出力の破損や禁止表現の混入。ルールベース検証で拾えるケースが多い
- トーン逸脱: 敬語崩れやガイドライン違反の表現が出る
これらはTruLensではRAG三要素として整理されており、context relevance(検索適合度)、groundedness(根拠忠実性)、answer relevance(回答適合度)の3つの視点から評価できるようになっています。
評価ワークフローへの落とし込み
失敗パターンを評価に反映するには、次の3種類のテストを組み合わせるのが一般的です。
- 検索単体テスト: 固定クエリのTop-K結果を期待文書IDと照合。LLM不要でコスト最小
- 生成単体テスト: 正解文書をコンテキストに固定し生成だけ評価。検索の変動を排除できる
- End-to-Endテスト: パイプライン全体を通す。本番に最も近いが原因切り分けが難しい
この3種類をセットで保持しておくと、E2Eが落ちたときに検索単体・生成単体の結果から原因を絞り込みやすくなります。
採点器自体の変動に注意
LLMによる採点では、採点プロンプトやモデル版を変えるとスコア自体が揺れることがあります。こうなると回帰か採点ブレか区別がつきません。
この問題を避けるには、採点器のプロンプト・モデル・パラメータをバージョン管理し、比較時は固定しておくと安心です。「回答が変わった」のではなく採点基準が動いただけ——という事故は意外とあります。
もう一つ注意すべきは比較順序バイアスです。pairwise評価(AとBを比較して良い方を選ぶ形式)では、提示順序によって判定が揺れることがあります。 対策として、順序を入れ替えて2回評価する、pointwise評価(各回答を単独で採点)に切り替える、複数回実行して集計する、などが使われます。
まずチームのパイプラインで、過去の障害が検索と生成のどちらに集中しているかをログから確認してみると、どこから手をつけるか決めやすくなります。
AIエージェントを企業で運用する際の「何を止めるか・どこで承認を入れるか」という判断軸については、企業でAIエージェントを動かすための運用の判断軸でOpenAI Frontierをベースに整理しています。
評価セット最小テンプレ: 日本語ケースの作り方
失敗パターンが決まったら、次はそれを検知するための評価セットが必要です。最初から数百件を揃える必要はなく、まずは20〜50件の質の高いケースから始めると運用に乗せやすくなります。
ここでは日本語の最小評価セットを5つの観点でテンプレ化します。
5観点のケース設計
評価セットに含めるケースは、以下の観点で分類しておくと抜け漏れを防ぎやすくなります。
- 質問タイプ: 事実質問(1文で答えられるもの)、手順質問(ステップで答えるもの)、比較質問(複数の選択肢を並べるもの)などがあります
- 禁止系: 答えてはいけない質問への拒否が正しく動くかを確認します。個人情報の問い合わせや社外秘の漏えい誘導などが該当します
- 難問: 曖昧 な質問、文脈不足の質問、索引にない情報への質問を含めます。「わかりません」と返せるかを見ます
- 期待挙動: 各ケースに「正解」だけでなく「許容範囲」も定義します。完全一致か意味的同等かを明記しておきます
- 根拠: 回答の根拠となるドキュメントIDやチャンクIDを紐づけます。retrieval の正否判定に使います
実際のケースをYAMLで書くと、たとえば次のような形になります。
case_id: JP-001
query: 年末調整の締切はいつですか
expected_answer: 毎年1月31日までに提出
acceptable_variants:
- 1/31
- 1月末
refusal_expected: false
reference_doc_id: doc_hr_0042
tags:
- factual
- hr
- date
日本語固有の落とし穴: 表記揺れ
日本語の評価で厄介なのが「表記の揺れ」です。全角半角(「1」と「1」)、送り仮名(「行う」と「行なう」)、カタカナ長音(「サーバー」と「サーバ」)など、内容は同じでも文字列としては異なるため見かけ上の差分が生まれがちです。
評価セット作成時に、以下のような正規化ポリシーを決めておくと良いでしょう。
- 比較前に全角→半角、長音の統一などの正規化を入れるか
- 同義表記(「問い合わせ」と「問合せ」)を許容するか
- 数値フォーマット(「1,000円」と「千円」)の扱い
このポリシーが未定のまま回帰テストを走らせると厄介です。回答の中身は同じなのに「差分あり」と報告され、チームが混乱する原因になってしまう可能性があります。正規化ルールは評価セットのREADMEに1ページ で書き残しておくと、きっと後から参加したメンバーにも伝わりやすいはずです。
三層の採点設計
評価セットのケースをどう採点するかは、三層で設計するのがおすすめです。
第1層: ルールベース: 正規表現・完全一致・キーワード包含で判定できるケースをここで捌きます。禁止ワードの出力チェック、必須キーワードの包含、フォーマット検証などが該当します。ほぼ追加コストなしで再現性が高いのがメリットです。まずここで拾えるものを増やしていくと、後段が楽になります。
第2層: LLMによる採点: 意味的な正否判定が必要なケースに使います。「回答が質問の意図に沿っているか」「根拠と矛盾していないか」といった判定が対象です。採点器のバージョン管理については前述の採点器自体の変動に注意を参照してください。
第3層: 人手スポット: 新ケースの追加時、第2層の判定が割れたとき、四半期ごとの校正に使う層です。全件を人手で見る運用は中長期的には継続するのは困難です。サンプリングと基準の再校正に限定すると良いでしょう。
第1層のカバー範囲を広げるほど、コストと再現性の両面で有利になります。
セキュリティと個人情報: 評価セットに本番データを入れない
評価セットを素早く作りたいとき、本番のチャットログからそのまま質問と回答を抜き出したくなることがあります。しかし実データをそのまま使うと、ユーザー名や社員番号などの個人情報(PII)がテストログやCIの出力に残りかねません。CIログは開発チーム全員がアクセスでき、外部のCI基盤に長期保存されることもあります。
対策としては次の3つが挙げられます。
- 評価セット作成時に固有名詞・日付・金額をダミーに置換しておく
- CIログの保持期間と閲覧権限を確認し、個人情報混入時の削除手順を決めておく
- 評価セットのレビューフローに個人情報チェック項目を1行加えておく
過度に注意する必要はないかもしれませんが、「開発資産だから個人情報が入っていても問題ない」という思い込みは危険です。
まずはチームの本番ログから、よく失敗する質問を5件抜き出し、上の5観点でタグ付けすることから始めてみてはいかがでしょうか。
CIゲート化: 頻度分割・コスト上限・非決定性・キャッシュと記録
評価セットを整えたら、CIゲートとして回さないと変更を見逃しやすくなります。実行頻度・コスト上限・非決定性対策・記録の4点を先に決めておくとスムーズです。
CI実行頻度の分割
全テストをPRごとに回すと、時間もコストも膨らみ形骸化しがちです。ここでは2層に分けるのが良いと考えています。
- PRトリガー(軽量): コア品質に直結する10〜20ケースだけ回し、失敗ならビルドを落とす
- 夜間・週次(重量): 全評価セット+セキュリティスキャンをcron等で定期実行し、結果をJSON/HTMLで保存する
PRゲートは「壊れたら止める」役割で、定期実行は「サイレント劣化を拾う」役割です。この分割を先に決めておかないと、実行時間が伸びるたびにテストがスキップされがちです。
コスト上限の決め方
LLM呼び出しを含むCIは、1回ごとにAPI費用が発生します。月額上限の見積もりは、たとえば次のような式で概算できます。
ケース数 × 平均トークン数 × 単価 × 実行回数
この式から、CI環境変数で上限ガードを入れておくと安心です。金額はモデルや頻度で大きく変わるため、まず1週間の実測値を取ってから月次予算を逆算する流れが手戻りが少なくなります。
出力の揺れにどう対処するか
LLMの出力は同じ入力でも揺れることがあります。OpenAIのseedパラメータは「ほぼ同じ出力」を目指す仕組みですが、公式Cookbookでも決定性は保証しないと書かれています。 バックエンド更新でsystem_fingerprint(モデルのバージョンを示す識別子)が変わることもあり、この差分を検知しておくと原因切り分けが速まります。
同一ケースを2〜3回実行して多数決で判定する方法や、閾値に揺れ幅を織り込む方法が実務で使われています。「回答が変わった」という報告が出たとき、本当の回帰かフレークかを分ける仕組みを先に用意しておくと後続タスクが楽になるかもしれません。