T-CREATOR

<div />

GPT-5 生成品質の監視と改善ループ:オフライン Evals× オンライン A/B のハイブリッド運用

GPT-5 生成品質の監視と改善ループ:オフライン Evals× オンライン A/B のハイブリッド運用

GPT-5 のような大規模言語モデルを本番環境で運用する際、生成品質の継続的な監視と改善は最も重要な課題の一つです。開発段階ではオフライン評価(Evals)で品質を担保し、本番環境ではオンライン A/B テストでユーザー体験を測定するのが一般的ですが、これらを単独で運用すると見落としや判断の遅れが発生しやすくなります。

本記事では、オフライン Evals とオンライン A/B テストを組み合わせたハイブリッド運用の実践方法を解説します。両手法の長所を活かし、短所を補完することで、GPT-5 の生成品質を継続的に向上させる改善ループを構築できるでしょう。

実務で直面する具体的な課題から、TypeScript と Next.js を使った実装例まで、初心者の方にもわかりやすく段階的にご紹介していきますね。

背景

GPT-5 における品質評価の必要性

GPT-5 を本番環境に導入する際、モデルの出力品質を客観的に評価する仕組みが不可欠です。ユーザーに提供する応答の正確性、安全性、有用性を担保するためには、定量的な評価指標と継続的な監視体制が求められます。

従来、モデルの品質評価には主に 2 つのアプローチが存在していました。1 つは開発環境で事前に準備したテストケースで評価する「オフライン評価」、もう 1 つは実際のユーザー環境で複数のバージョンを比較する「オンライン評価」です。

それぞれの手法には明確な特徴があり、適用シーンによって使い分ける必要があります。

オフライン評価とオンライン評価の比較

以下の図は、2 つの評価手法の基本的な違いを示しています。

mermaidflowchart TB
    subgraph offline["オフライン評価(Evals)"]
        offline_data["テストデータセット"] --> offline_model["GPT-5 モデル"]
        offline_model --> offline_eval["評価スクリプト"]
        offline_eval --> offline_result["スコア・メトリクス"]
    end

    subgraph online["オンライン評価(A/B テスト)"]
        user["実ユーザー"] --> router["トラフィック分割"]
        router --> model_a["モデル A"]
        router --> model_b["モデル B"]
        model_a --> analytics["分析基盤"]
        model_b --> analytics
        analytics --> online_result["ビジネスメトリクス"]
    end

図で理解できる要点:

  • オフライン評価は開発者がコントロールしたテストデータで評価を実施
  • オンライン評価は実ユーザーのフィードバックを直接収集
  • 両者は評価のタイミングとデータソースが大きく異なる

オフライン評価では、あらかじめ用意した質問と正解ペアのデータセットを使って、モデルの応答を自動的に採点します。実装が比較的容易で、デプロイ前にリスクを検知できるのが最大のメリットです。

一方、オンライン評価では実際のユーザートラフィックを複数のモデルバージョンに振り分け、クリック率やコンバージョン率などのビジネス指標で効果を測定します。実環境でのパフォーマンスを直接測定できる点が強みですね。

評価手法の特性比較表

#項目オフライン Evalsオンライン A/B テスト
1評価タイミングデプロイ前デプロイ後
2データソース固定テストセット実ユーザー行動
3評価速度高速(数分〜数時間)低速(数日〜数週間)
4コスト低(計算コストのみ)高(インフラ+機会損失)
5リスク低(本番影響なし)中〜高(ユーザー影響あり)
6現実性低(仮想環境)高(実環境)
7自動化容易やや困難
8測定指標BLEU, ROUGE, 正解率などCTR, CVR, 満足度など

この表からわかるように、両手法は相補的な関係にあります。オフライン評価の速さとオンライン評価の現実性を組み合わせることで、より強固な品質保証体制を構築できるのです。

課題

オフライン評価のみの運用における問題点

オフライン Evals だけで品質管理を行う場合、いくつかの深刻な問題に直面します。

最も大きな課題は、テストデータと実データのギャップです。開発者が用意したテストケースでは高スコアを記録したモデルが、本番環境では期待通りのパフォーマンスを発揮しないケースが頻繁に発生します。ユーザーの実際の質問パターンや文脈は、想定よりもはるかに多様で予測困難だからです。

また、評価指標の選択も難しい問題です。BLEU や ROUGE といった従来の自動評価指標は、テキストの表層的な一致度を測定するだけで、応答の有用性や安全性を正確に捉えられません。人手評価を併用することもできますが、コストと時間がかかりすぎて継続的な運用が困難になりますね。

さらに、ビジネス目標との乖離も見逃せません。技術的な評価指標が改善しても、それが必ずしもユーザー満足度やコンバージョン率の向上につながるとは限らないのです。

オンライン A/B テストのみの運用における問題点

一方、オンライン A/B テストだけに依存する運用にも重大なリスクがあります。

まず、ユーザーへの悪影響リスクです。品質の低いモデルを本番環境にデプロイしてしまうと、一定数のユーザーに低品質な応答を提供することになり、ブランド信頼性の低下や解約率の上昇を招きかねません。

実験期間の長さも課題です。統計的に有意な結果を得るには、通常数千〜数万のユーザーインタラクションが必要で、結果が出るまで数日から数週間かかることが一般的です。その間、品質問題のあるモデルが稼働し続ける可能性があります。

また、コストの問題も無視できません。複数のモデルバージョンを同時稼働させるためには、インフラコストが倍増しますし、実験管理の運用コストも追加で発生するのです。

以下の図は、単独運用時の問題フローを示しています。

mermaidflowchart TD
    develop["モデル開発"] --> choice{"評価手法は?"}

    choice -->|オフライン のみ| offline_test["Evals で評価"]
    offline_test --> offline_deploy["本番デプロイ"]
    offline_deploy --> offline_issue["実環境で問題発覚"]
    offline_issue --> offline_rollback["緊急ロールバック"]

    choice -->|オンライン のみ| online_deploy["いきなり A/B テスト"]
    online_deploy --> online_risk["ユーザーに低品質応答"]
    online_risk --> online_wait["結果待ち(数週間)"]
    online_wait --> online_decision["判断&修正"]

    offline_rollback --> rework["再開発"]
    online_decision --> rework

図で理解できる要点:

  • オフラインのみ:本番環境での想定外の問題に対応が遅れる
  • オンラインのみ:ユーザーリスクを取りながら長期間待機が必要
  • いずれも手戻りが大きく、改善サイクルが遅い

ハイブリッド運用の必要性

これらの課題を解決するには、オフライン Evals とオンライン A/B テストを戦略的に組み合わせる必要があります。

オフライン評価でリスクの高い変更を事前にフィルタリングし、一定の品質基準をクリアしたモデルのみをオンライン実験に進める。そして、オンライン実験の結果をフィードバックしてオフライン評価基準を改善する。この循環により、品質とスピードを両立した継続的改善ループを実現できるのです。

解決策

ハイブリッド運用の基本アーキテクチャ

オフライン Evals とオンライン A/B テストを組み合わせたハイブリッド運用の全体像を見ていきましょう。

基本的な考え方は、段階的なゲート制御です。まずオフライン評価で品質基準をクリアしたモデルのみを候補とし、次に小規模なオンライン実験で安全性を確認し、最後にフルスケールの A/B テストで効果を検証します。

以下の図は、ハイブリッド運用の全体フローを示しています。

mermaidflowchart TB
    start["モデル開発"] --> offline["オフライン Evals"]

    offline --> gate1{"品質基準<br/>クリア?"}
    gate1 -->|No| feedback1["評価結果分析"]
    feedback1 --> start

    gate1 -->|Yes| canary["カナリアリリース<br/>(1-5% トラフィック)"]

    canary --> gate2{"異常検知<br/>なし?"}
    gate2 -->|No| rollback["自動ロールバック"]
    rollback --> feedback2["障害分析"]
    feedback2 --> start

    gate2 -->|Yes| ab_test["A/B テスト<br/>(50% トラフィック)"]

    ab_test --> gate3{"統計的<br/>有意差?"}
    gate3 -->|No| analyze["結果分析"]
    analyze --> start

    gate3 -->|Yes| deploy["全体展開"]

    deploy --> monitor["継続監視"]
    monitor --> insights["インサイト収集"]
    insights --> dataset["Evals データセット更新"]
    dataset --> start

図で理解できる要点:

  • 3 段階のゲート(オフライン → カナリア → A/B)で段階的にリスクを低減
  • 各段階での失敗は即座にフィードバックループに戻る
  • 本番データから得た知見を Evals データセットに反映して継続改善

このアーキテクチャにより、リスクを最小化しながら迅速な改善サイクルを回せるようになります。

オフライン Evals の設計ポイント

オフライン評価を効果的に機能させるには、適切なテストデータセットと評価指標の設計が重要です。

テストデータセットの構築

テストデータセットは、実際のユーザー行動を反映したものでなければなりません。以下の要素を含めることをお勧めします。

#データ種別説明推奨件数
1ゴールデンセット人手で作成した高品質な質問-回答ペア100-500 件
2リグレッションセット過去に問題があった事例50-200 件
3エッジケース境界条件や例外的なケース50-100 件
4本番サンプル実際のユーザーログからサンプリング500-2000 件

本番サンプルは定期的に更新し、最新のユーザー行動パターンを反映させることが重要です。週次または月次での更新サイクルを設定すると良いでしょう。

評価指標の選定

単一の指標に頼らず、複数の観点から品質を測定します。

  • 正確性指標:F1 スコア、Exact Match など
  • 品質指標:BLEU、ROUGE、BERTScore など
  • 安全性指標:有害コンテンツ検知率、バイアススコアなど
  • 効率性指標:レスポンス時間、トークン消費量など

これらを組み合わせた総合スコアで品質基準を設定します。

オンライン A/B テストの設計ポイント

オンライン実験では、技術指標とビジネス指標の両方を追跡することが重要です。

実験設計の基本原則

安全な A/B テスト実施のために、以下の原則を守りましょう。

まず、段階的なトラフィック拡大です。初期は 1-5% の小規模トラフィックでカナリアリリースを行い、異常がないことを確認してから 50% の A/B テストに進みます。

次に、早期停止条件の設定です。主要メトリクスが一定の閾値を下回った場合、自動的に実験を停止してロールバックする仕組みを実装します。

そして、統計的厳密性の確保です。サンプルサイズ計算を事前に行い、十分な検出力(power)を持つ実験設計にします。一般的には 80% 以上の検出力を目指します。

測定すべきメトリクス

オンライン実験で追跡すべき指標には、以下のようなものがあります。

#メトリクス種別具体例測定目的
1品質メトリクス応答時間、エラー率、タイムアウト率技術的な健全性
2ユーザー行動セッション時間、メッセージ数、離脱率エンゲージメント
3満足度👍👎 評価、NPS、フィードバック数ユーザー体験
4ビジネス成果CVR、課金率、リテンション率事業インパクト

これらを統合ダッシュボードで可視化し、リアルタイムで監視できる体制を整えます。

フィードバックループの構築

ハイブリッド運用の真価は、オンライン実験から得られた知見をオフライン評価に還元する継続的なフィードバックループにあります。

具体的には、以下のような仕組みを実装します。

まず、A/B テストでパフォーマンスが悪かった事例を自動的に収集し、オフライン評価用のテストケースとして追加します。これにより、実環境で発生する問題をオフライン段階で検知できるようになりますね。

次に、ユーザーからの否定的なフィードバック(👎 評価など)を受けた応答を分析し、なぜ評価が低かったのかをラベリングします。このデータを使って、オフライン評価の基準を継続的に改善していくのです。

さらに、オンライン実験で得られたビジネスメトリクスと技術メトリクスの相関関係を分析し、どの技術指標がビジネス成果に最も寄与するかを明らかにします。その結果をオフライン評価の重み付けに反映させることで、より実践的な評価基準を作れます。

具体例

TypeScript による Evals フレームワークの実装

ここからは、実際に動作するコードを使ってハイブリッド運用を実装していきます。まず、オフライン評価のフレームワークから見ていきましょう。

プロジェクトのセットアップ

最初に、必要なパッケージをインストールします。

bash# プロジェクトの初期化
yarn init -y

# 依存パッケージのインストール
yarn add openai zod
yarn add -D typescript @types/node tsx

次に、TypeScript の設定ファイルを作成します。

json{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

評価データの型定義

評価に使用するデータ構造を Zod で定義します。型安全性を確保しつつ、実行時のバリデーションも可能になります。

typescript// src/types/eval.ts
import { z } from 'zod';

// 評価用のテストケース定義
export const TestCaseSchema = z.object({
  id: z.string(),
  input: z.string(),
  expectedOutput: z.string().optional(),
  category: z.enum([
    'accuracy',
    'safety',
    'performance',
    'edge_case',
  ]),
  metadata: z.record(z.unknown()).optional(),
});

export type TestCase = z.infer<typeof TestCaseSchema>;
typescript// 評価結果の定義
export const EvalResultSchema = z.object({
  testCaseId: z.string(),
  modelOutput: z.string(),
  scores: z.object({
    accuracy: z.number().min(0).max(1),
    quality: z.number().min(0).max(1),
    safety: z.number().min(0).max(1),
    latency: z.number(), // ミリ秒
  }),
  passed: z.boolean(),
  timestamp: z.date(),
});

export type EvalResult = z.infer<typeof EvalResultSchema>;

GPT-5 クライアントの実装

OpenAI API を呼び出すクライアントクラスを実装します。エラーハンドリングとリトライ機能を含めた実装にしましょう。

typescript// src/services/gpt-client.ts
import OpenAI from 'openai';

export class GPT5Client {
  private client: OpenAI;
  private model: string;

  constructor(apiKey: string, model: string = 'gpt-4') {
    // GPT-5 がリリースされたら 'gpt-5' に変更
    this.client = new OpenAI({ apiKey });
    this.model = model;
  }
typescript  /**
   * GPT-5 に質問を送信して応答を取得
   * @param prompt - 入力プロンプト
   * @param maxRetries - 最大リトライ回数
   */
  async generate(prompt: string, maxRetries: number = 3): Promise<string> {
    let lastError: Error | undefined;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        const response = await this.client.chat.completions.create({
          model: this.model,
          messages: [{ role: 'user', content: prompt }],
          temperature: 0.7,
          max_tokens: 1000,
        });

        return response.choices[0]?.message?.content || '';
      } catch (error) {
        lastError = error as Error;
        console.error(`Attempt ${attempt} failed:`, error);

        // リトライ前に待機(指数バックオフ)
        if (attempt < maxRetries) {
          await this.sleep(Math.pow(2, attempt) * 1000);
        }
      }
    }

    throw new Error(`Failed after ${maxRetries} attempts: ${lastError?.message}`);
  }
typescript  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}

このクライアントは、API エラーが発生した場合に自動的にリトライを行い、指数バックオフで待機時間を増やしていきます。実運用では必須の機能ですね。

評価エンジンの実装

テストケースを実行して評価する中核となるエンジンを実装します。

typescript// src/services/eval-engine.ts
import { GPT5Client } from './gpt-client';
import { TestCase, EvalResult } from '../types/eval';

export class EvalEngine {
  private client: GPT5Client;

  constructor(client: GPT5Client) {
    this.client = client;
  }
typescript  /**
   * 単一のテストケースを評価
   */
  async evaluateTestCase(testCase: TestCase): Promise<EvalResult> {
    const startTime = Date.now();

    // GPT-5 で応答を生成
    const modelOutput = await this.client.generate(testCase.input);

    const latency = Date.now() - startTime;

    // 各種スコアを計算
    const scores = {
      accuracy: this.calculateAccuracy(modelOutput, testCase.expectedOutput),
      quality: await this.calculateQuality(modelOutput),
      safety: await this.calculateSafety(modelOutput),
      latency,
    };

    // 合格判定(すべてのスコアが閾値以上)
    const passed =
      scores.accuracy >= 0.7 &&
      scores.quality >= 0.6 &&
      scores.safety >= 0.9 &&
      scores.latency <= 5000;

    return {
      testCaseId: testCase.id,
      modelOutput,
      scores,
      passed,
      timestamp: new Date(),
    };
  }
typescript  /**
   * 正確性スコアの計算(簡易的な実装)
   */
  private calculateAccuracy(output: string, expected?: string): number {
    if (!expected) return 1.0;

    // 単純な文字列一致率(実際は BLEU や BERTScore を使用)
    const outputWords = output.toLowerCase().split(/\s+/);
    const expectedWords = expected.toLowerCase().split(/\s+/);

    const matchCount = outputWords.filter(word =>
      expectedWords.includes(word)
    ).length;

    return matchCount / Math.max(outputWords.length, expectedWords.length);
  }
typescript  /**
   * 品質スコアの計算
   */
  private async calculateQuality(output: string): Promise<number> {
    // 実際は別の LLM や評価モデルを使用
    // ここでは簡易的な実装
    const hasMinLength = output.length >= 50;
    const hasProperStructure = /[.!?]/.test(output);
    const notTooLong = output.length <= 2000;

    const checks = [hasMinLength, hasProperStructure, notTooLong];
    return checks.filter(Boolean).length / checks.length;
  }
typescript  /**
   * 安全性スコアの計算
   */
  private async calculateSafety(output: string): Promise<number> {
    // 実際は OpenAI Moderation API や専用の安全性チェックを使用
    const harmfulPatterns = [
      /暴力/i,
      /差別/i,
      /個人情報/i,
    ];

    const hasHarmfulContent = harmfulPatterns.some(pattern =>
      pattern.test(output)
    );

    return hasHarmfulContent ? 0.0 : 1.0;
  }
}

各評価指標の計算ロジックは簡易的な実装ですが、実際のプロダクションでは OpenAI の Moderation API や BERTScore のような高度な評価手法を組み込むことをお勧めします。

バッチ評価の実装

複数のテストケースを並列実行してレポートを生成する機能を追加します。

typescript// src/services/batch-evaluator.ts
import { EvalEngine } from './eval-engine';
import { TestCase, EvalResult } from '../types/eval';

export class BatchEvaluator {
  private engine: EvalEngine;

  constructor(engine: EvalEngine) {
    this.engine = engine;
  }
typescript  /**
   * 複数のテストケースを並列評価
   */
  async evaluateBatch(
    testCases: TestCase[],
    concurrency: number = 5
  ): Promise<EvalResult[]> {
    const results: EvalResult[] = [];

    // 並列実行数を制限しながら処理
    for (let i = 0; i < testCases.length; i += concurrency) {
      const batch = testCases.slice(i, i + concurrency);
      const batchResults = await Promise.all(
        batch.map(tc => this.engine.evaluateTestCase(tc))
      );
      results.push(...batchResults);

      console.log(`Progress: ${results.length}/${testCases.length}`);
    }

    return results;
  }
typescript  /**
   * 評価結果のサマリーレポートを生成
   */
  generateReport(results: EvalResult[]): {
    totalTests: number;
    passedTests: number;
    passRate: number;
    averageScores: {
      accuracy: number;
      quality: number;
      safety: number;
      latency: number;
    };
  } {
    const totalTests = results.length;
    const passedTests = results.filter(r => r.passed).length;

    const averageScores = {
      accuracy: this.average(results.map(r => r.scores.accuracy)),
      quality: this.average(results.map(r => r.scores.quality)),
      safety: this.average(results.map(r => r.scores.safety)),
      latency: this.average(results.map(r => r.scores.latency)),
    };

    return {
      totalTests,
      passedTests,
      passRate: passedTests / totalTests,
      averageScores,
    };
  }
typescript  private average(numbers: number[]): number {
    return numbers.reduce((sum, n) => sum + n, 0) / numbers.length;
  }
}

Next.js による A/B テスト基盤の実装

次に、オンライン A/B テストの実装を見ていきます。Next.js を使った Web アプリケーションでの実装例です。

A/B テスト用のミドルウェア実装

Next.js のミドルウェア機能を使って、トラフィックを振り分けます。

typescript// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // 既存の実験グループ Cookie を確認
  let experimentGroup = request.cookies.get(
    'experiment_group'
  )?.value;

  if (!experimentGroup) {
    // 新規ユーザーをランダムに振り分け
    experimentGroup =
      Math.random() < 0.5 ? 'control' : 'treatment';

    // Cookie に保存(7日間有効)
    response.cookies.set(
      'experiment_group',
      experimentGroup,
      {
        maxAge: 60 * 60 * 24 * 7,
        httpOnly: true,
        sameSite: 'strict',
      }
    );
  }

  // カスタムヘッダーで実験グループを伝達
  response.headers.set(
    'X-Experiment-Group',
    experimentGroup
  );

  return response;
}
typescriptexport const config = {
  matcher: [
    /*
     * API ルート以外のすべてのリクエストにマッチ
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

このミドルウェアは、初回訪問時にユーザーを control(コントロール群)または treatment(実験群)にランダムに振り分け、Cookie に保存して一貫性を保ちます。

モデルルーティング機能の実装

実験グループに応じて異なる GPT-5 設定やプロンプトを使い分けるルーターを実装します。

typescript// src/lib/model-router.ts
type ModelConfig = {
  model: string;
  temperature: number;
  systemPrompt: string;
};

export class ModelRouter {
  private controlConfig: ModelConfig;
  private treatmentConfig: ModelConfig;

  constructor(
    controlConfig: ModelConfig,
    treatmentConfig: ModelConfig
  ) {
    this.controlConfig = controlConfig;
    this.treatmentConfig = treatmentConfig;
  }
typescript  /**
   * 実験グループに応じた設定を返す
   */
  getConfig(experimentGroup: string): ModelConfig {
    return experimentGroup === 'treatment'
      ? this.treatmentConfig
      : this.controlConfig;
  }
}
typescript// デフォルト設定の例
export const defaultRouter = new ModelRouter(
  {
    model: 'gpt-4',
    temperature: 0.7,
    systemPrompt: 'You are a helpful assistant.',
  },
  {
    model: 'gpt-4', // GPT-5 がリリースされたら変更
    temperature: 0.8,
    systemPrompt:
      'You are a helpful and creative assistant.',
  }
);

API エンドポイントの実装

チャット機能を提供する API エンドポイントを実装します。ここで A/B テストのロギングも行います。

typescript// app/api/chat/route.ts
import { NextRequest, NextResponse } from 'next/server';
import OpenAI from 'openai';
import { defaultRouter } from '@/lib/model-router';
import { logExperiment } from '@/lib/analytics';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});
typescriptexport async function POST(request: NextRequest) {
  try {
    const { message } = await request.json();

    // 実験グループを取得
    const experimentGroup = request.cookies.get('experiment_group')?.value || 'control';

    // グループに応じた設定を取得
    const config = defaultRouter.getConfig(experimentGroup);

    const startTime = Date.now();
typescript// GPT-5 API 呼び出し
const completion = await openai.chat.completions.create({
  model: config.model,
  messages: [
    { role: 'system', content: config.systemPrompt },
    { role: 'user', content: message },
  ],
  temperature: config.temperature,
});

const latency = Date.now() - startTime;
const response =
  completion.choices[0]?.message?.content || '';
typescript    // 実験ログを記録
    await logExperiment({
      experimentGroup,
      modelConfig: config,
      input: message,
      output: response,
      latency,
      timestamp: new Date(),
    });

    return NextResponse.json({ response, experimentGroup });
  } catch (error) {
    console.error('Chat API error:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

分析ロギング機能の実装

実験データを記録して後から分析できるようにします。実際の運用では、データベースや分析基盤に送信します。

typescript// src/lib/analytics.ts
type ExperimentLog = {
  experimentGroup: string;
  modelConfig: {
    model: string;
    temperature: number;
    systemPrompt: string;
  };
  input: string;
  output: string;
  latency: number;
  timestamp: Date;
};
typescript/**
 * 実験ログを記録
 */
export async function logExperiment(
  log: ExperimentLog
): Promise<void> {
  // 実際は PostgreSQL や BigQuery などに保存
  console.log('Experiment log:', {
    group: log.experimentGroup,
    model: log.modelConfig.model,
    latency: log.latency,
    timestamp: log.timestamp.toISOString(),
  });

  // 例:分析基盤への送信
  // await analyticsClient.track('gpt_interaction', log);
}
typescript/**
 * ユーザーフィードバックを記録
 */
export async function logFeedback(data: {
  experimentGroup: string;
  messageId: string;
  rating: 'positive' | 'negative';
  comment?: string;
}): Promise<void> {
  console.log('Feedback log:', data);

  // 実際はデータベースに保存して Evals にフィードバック
  // await db.feedback.create({ data });
}

フィードバックループの自動化

最後に、オンライン実験の結果をオフライン評価に自動的にフィードバックする仕組みを実装します。

否定的フィードバックの収集

ユーザーからの否定的評価を受けた事例をテストケースとして保存します。

typescript// src/services/feedback-collector.ts
import { TestCase } from '../types/eval';

export class FeedbackCollector {
  /**
   * 否定的フィードバックをテストケースに変換
   */
  async collectNegativeFeedback(): Promise<TestCase[]> {
    // 実際はデータベースから取得
    const negativeFeedbacks = await this.fetchNegativeFeedbacks();

    return negativeFeedbacks.map((feedback, index) => ({
      id: `feedback_${feedback.id}`,
      input: feedback.input,
      expectedOutput: undefined, // 人手でレビューが必要
      category: 'edge_case' as const,
      metadata: {
        originalOutput: feedback.output,
        userRating: feedback.rating,
        comment: feedback.comment,
        experimentGroup: feedback.experimentGroup,
      },
    }));
  }
typescript  private async fetchNegativeFeedbacks() {
    // データベースクエリの例
    // return await db.feedback.findMany({
    //   where: { rating: 'negative' },
    //   include: { interaction: true },
    // });

    // ダミーデータ
    return [
      {
        id: '1',
        input: '東京の天気は?',
        output: '申し訳ありませんが、リアルタイムの天気情報は提供できません。',
        rating: 'negative',
        comment: '役に立たない',
        experimentGroup: 'treatment',
      },
    ];
  }
}

テストデータセットの自動更新

収集したフィードバックを既存のテストセットに追加する機能です。

typescript// src/services/dataset-updater.ts
import { TestCase } from '../types/eval';
import { FeedbackCollector } from './feedback-collector';
import fs from 'fs/promises';

export class DatasetUpdater {
  private collector: FeedbackCollector;
  private datasetPath: string;

  constructor(collector: FeedbackCollector, datasetPath: string) {
    this.collector = collector;
    this.datasetPath = datasetPath;
  }
typescript  /**
   * データセットを更新
   */
  async updateDataset(): Promise<void> {
    // 既存のデータセットを読み込み
    const existingDataset = await this.loadDataset();

    // 新しいフィードバックを収集
    const newTestCases = await this.collector.collectNegativeFeedback();

    // 重複を除いて統合
    const updatedDataset = this.mergeDatasets(existingDataset, newTestCases);

    // ファイルに保存
    await this.saveDataset(updatedDataset);

    console.log(`Dataset updated: ${newTestCases.length} new cases added`);
  }
typescript  private async loadDataset(): Promise<TestCase[]> {
    try {
      const content = await fs.readFile(this.datasetPath, 'utf-8');
      return JSON.parse(content);
    } catch (error) {
      console.warn('Dataset file not found, starting fresh');
      return [];
    }
  }

  private async saveDataset(dataset: TestCase[]): Promise<void> {
    await fs.writeFile(
      this.datasetPath,
      JSON.stringify(dataset, null, 2),
      'utf-8'
    );
  }
typescript  private mergeDatasets(
    existing: TestCase[],
    newCases: TestCase[]
  ): TestCase[] {
    const existingIds = new Set(existing.map(tc => tc.id));
    const uniqueNewCases = newCases.filter(tc => !existingIds.has(tc.id));

    return [...existing, ...uniqueNewCases];
  }
}

統合実行スクリプト

すべての機能を統合して、定期的に実行するスクリプトを作成します。

typescript// src/scripts/run-evals.ts
import { GPT5Client } from '../services/gpt-client';
import { EvalEngine } from '../services/eval-engine';
import { BatchEvaluator } from '../services/batch-evaluator';
import { FeedbackCollector } from '../services/feedback-collector';
import { DatasetUpdater } from '../services/dataset-updater';
import { TestCase } from '../types/eval';
import fs from 'fs/promises';

async function main() {
  const apiKey = process.env.OPENAI_API_KEY;
  if (!apiKey) {
    throw new Error('OPENAI_API_KEY is required');
  }
typescript// 1. データセットを最新化
console.log('Step 1: Updating dataset with feedback...');
const collector = new FeedbackCollector();
const updater = new DatasetUpdater(
  collector,
  './data/test-cases.json'
);
await updater.updateDataset();
typescript// 2. テストケースを読み込み
console.log('Step 2: Loading test cases...');
const testCasesContent = await fs.readFile(
  './data/test-cases.json',
  'utf-8'
);
const testCases: TestCase[] = JSON.parse(testCasesContent);
console.log(`Loaded ${testCases.length} test cases`);
typescript// 3. 評価を実行
console.log('Step 3: Running evaluations...');
const client = new GPT5Client(apiKey);
const engine = new EvalEngine(client);
const evaluator = new BatchEvaluator(engine);

const results = await evaluator.evaluateBatch(testCases, 5);
typescript// 4. レポートを生成
console.log('Step 4: Generating report...');
const report = evaluator.generateReport(results);

console.log('=== Evaluation Report ===');
console.log(`Total tests: ${report.totalTests}`);
console.log(`Passed tests: ${report.passedTests}`);
console.log(
  `Pass rate: ${(report.passRate * 100).toFixed(2)}%`
);
console.log('Average scores:');
console.log(
  `  Accuracy: ${report.averageScores.accuracy.toFixed(3)}`
);
console.log(
  `  Quality: ${report.averageScores.quality.toFixed(3)}`
);
console.log(
  `  Safety: ${report.averageScores.safety.toFixed(3)}`
);
console.log(
  `  Latency: ${report.averageScores.latency.toFixed(0)}ms`
);
typescript  // 5. 結果を保存
  await fs.writeFile(
    './data/eval-results.json',
    JSON.stringify(results, null, 2),
    'utf-8'
  );

  // 6. 品質基準を満たしているか判定
  if (report.passRate >= 0.8) {
    console.log('\n✓ Quality gate passed! Ready for A/B testing.');
    process.exit(0);
  } else {
    console.error('\n✗ Quality gate failed. Please review failures.');
    process.exit(1);
  }
}

main().catch(console.error);

このスクリプトを CI/CD パイプラインに組み込むことで、デプロイ前に自動的に品質チェックが実行され、基準を満たさない変更を本番環境にリリースするリスクを防げます。

実装アーキテクチャ全体図

最後に、実装したシステム全体のアーキテクチャを図で確認しましょう。

mermaidflowchart TB
    subgraph dev["開発環境"]
        code["モデル開発"] --> eval_script["Evals スクリプト"]
        eval_script --> dataset["テストデータセット"]
        dataset --> engine["EvalEngine"]
        engine --> gate{"品質ゲート<br/>pass rate >= 80%"}
    end

    subgraph prod["本番環境"]
        user["ユーザー"] --> middleware["Next.js<br/>Middleware"]
        middleware --> router["ModelRouter"]
        router --> control["Control グループ<br/>GPT-4"]
        router --> treatment["Treatment グループ<br/>GPT-5"]
        control --> analytics["Analytics"]
        treatment --> analytics
        user --> feedback_ui["フィードバック UI"]
        feedback_ui --> feedback_db[("Feedback DB")]
    end

    subgraph loop["フィードバックループ"]
        feedback_db --> collector["FeedbackCollector"]
        analytics --> insights["インサイト分析"]
        collector --> updater["DatasetUpdater"]
        insights --> updater
        updater --> dataset
    end

    gate -->|Pass| prod
    gate -->|Fail| code

図で理解できる要点:

  • 開発環境で品質ゲートを通過したモデルのみが本番へ
  • 本番環境では A/B テストでユーザーフィードバックを収集
  • フィードバックループがデータセットを自動更新して継続改善

この全体アーキテクチャにより、安全性と改善スピードを両立したハイブリッド運用が実現できます。

まとめ

GPT-5 のような大規模言語モデルの生成品質を継続的に向上させるには、オフライン Evals とオンライン A/B テストを戦略的に組み合わせたハイブリッド運用が不可欠です。

本記事でご紹介した重要なポイントをまとめます。

ハイブリッド運用の 3 つの利点

まず、リスクの最小化です。オフライン評価で品質基準をクリアしたモデルのみを本番環境に投入することで、ユーザーに低品質な応答を提供するリスクを大幅に削減できます。

次に、改善サイクルの高速化です。オフライン評価なら数時間で結果が得られるため、問題の早期発見と迅速な修正が可能になります。オンライン実験だけに頼ると、結果が出るまで数週間かかることもありますからね。

そして、データ駆動の意思決定です。技術指標(正確性、安全性など)とビジネス指標(CVR、満足度など)の両方を測定することで、より総合的な判断ができるようになります。

実装時の重要ポイント

実装にあたっては、以下の点に注意してください。

段階的なデプロイ戦略を採用し、オフライン評価 → カナリアリリース(1-5%)→ A/B テスト(50%)→ 全体展開という段階を踏みましょう。各段階で異常を検知したら、即座にロールバックできる仕組みも重要です。

適切な評価指標の選定も欠かせません。単一の指標だけでなく、正確性・品質・安全性・効率性など、複数の観点から総合的に評価する必要があります。また、技術指標とビジネス指標の相関を継続的に分析し、評価基準を改善していくことも大切です。

自動化されたフィードバックループの構築により、オンライン実験で得られた知見を自動的にオフライン評価に還元できます。これにより、テストデータセットが実際のユーザー行動を反映し続け、評価の精度が向上していきます。

今後の展望

GPT-5 のリリースに伴い、モデルの能力はさらに向上すると予想されますが、それと同時に品質管理の重要性も増していくでしょう。

ハイブリッド運用の仕組みを構築しておくことで、新しいモデルがリリースされた際にも、安全かつ迅速に本番環境へ導入できるようになります。また、複数のモデルバージョンを比較評価し、自社のユースケースに最適な設定を見つけることも容易になりますね。

本記事でご紹介した実装例を参考に、ぜひ皆さんの環境でもハイブリッド運用を試してみてください。継続的な品質改善により、ユーザーに価値を提供し続けるシステムを構築できるはずです。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;