第94回 実務で回すAIのCI/CDとテスト運用 — Pythonで作るユニットテスト・統合テスト・E2E自動検証の手順

まずはじめに。AIシステムの変更やモデル更新で「何が壊れるか分からない」と感じた経験は多いはずです。本記事では、実務でのつまずき(例:プロンプト修正で出力が突然ずれた、外部APIレイテンシで処理が止まった、検索ベクトルの差で結果が劣化した)に寄り添い、実際に手を動かして検証できる手順を提示します。

1) なぜAIにテストとCI/CDが必要か(実務上の失敗事例短評)

AI運用は非決定論的な要素(モデル出力、外部API、検索結果、データ更新)を含みます。以下はよくある失敗例です。

  • プロンプトを微修正したら重要な出力が欠落した(回帰)
  • モデルプロバイダのレスポンス遅延でワークフローがタイムアウトした
  • 検索インデックス更新が漏れて古いデータで回答され続けた

これらを防ぐには、ユニット(関数単位)→統合(外部依存との結合)→E2E(業務フロー)を揃えたCIパイプラインが有効です。

2) テスト戦略の全体像

テストは目的に応じて分けます。下表は役割の一覧です。

テスト種別 目的
ユニットテスト 関数・ロジック単位の正当性 入出力が期待通りになるか
統合テスト 外部APIやDBとの結合確認 外部呼び出しのハンドリング、エラーパス
E2Eテスト 業務フロー全体の検証 ユーザ入力→RAG応答→保存まで
契約テスト 外部サービスのインタフェース保証 APIスキーマと互換性が保てるか
負荷/コストテスト 運用コストとレイテンシの検証 高頻度リクエストでの挙動

3) テストデータとモックの作り方

外部依存(モデルAPI、検索、DB)は本番を叩かずモックで代替します。ポイントは再現可能な固定データを用意することです。

APIモック

  • requests-mock等でHTTPレスポンスを固定
  • エラーケース(5xx, タイムアウト)も必ず用意

外部モデルのスタブ

モデル呼び出しをラッパー関数に抽象化しておき、テストではスタブ実装を返すと良いです。出力は期待される構造(tokens, score, content)を模した固定値にします。

ベクトル検索のフェイク

検索は距離計算の再現が不要な場合が多く、擬似スコア付きのヒット配列を返すテストデータで十分です。

4) Pythonでの具体実装(pytest構成、fixture設計、サンプルテストコード)

ここでは最小サンプルのリポジトリ構成と主要ファイル例を示します。WordPressにそのまま貼れる形でコードを掲載します。

# ディレクトリ構成(例)
myrepo/
├─ app/
│  └─ rag_service.py
├─ tests/
│  ├─ test_unit.py
│  ├─ test_integration.py
│  └─ conftest.py
├─ requirements.txt
└─ .github/workflows/ci.yml

conftest.py(requests-mockを用いた外部APIモック)

# tests/conftest.py
import pytest
from requests_mock import Mocker
from app import rag_service

@pytest.fixture
def requests_mocker():
    with Mocker() as m:
        yield m

@pytest.fixture
def fake_vector_hits():
    return [
        {"id": "doc1", "score": 0.9, "text": "重要な手順A"},
        {"id": "doc2", "score": 0.7, "text": "補助情報B"},
    ]

サンプルユニットテスト(小さなロジック確認)

# tests/test_unit.py
from app.rag_service import build_prompt

def test_build_prompt_includes_query():
    q = "請求書の書き方"
    p = build_prompt(q)
    assert q in p

統合テスト(モデルAPIと検索をモック)

# tests/test_integration.py
import requests

def test_rag_flow(requests_mocker, fake_vector_hits):
    # ベクトル検索のエンドポイントをモック
    requests_mocker.get('https://search.example/v1/query', json={"hits": fake_vector_hits})
    # モデルAPIのモック
    requests_mocker.post('https://model.example/v1/generate', json={
        "content": "回答: 重要な手順A を参照してください。",
        "score": 0.95
    })

    resp = requests.post('http://localhost:8000/answer', json={"query": "請求書"})
    assert resp.status_code == 200
    data = resp.json()
    # 業務上の受け入れ基準(期待値の例)
    assert "重要な手順A" in data['answer']

実行コマンド(ローカル):

pip install -r requirements.txt
pytest -q --maxfail=1

5) CIパイプライン例(GitHub ActionsワークフローYAML)

以下はGHAの一例です。テスト→静的解析→品質ゲート(カバレッジ閾値)→手動承認でデプロイ、という順序を取っています。

name: CI
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      - name: Install
        run: pip install -r requirements.txt
      - name: Run flake8
        run: flake8

  test:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      - name: Install deps
        run: pip install -r requirements.txt
      - name: Run tests
        run: pytest --maxfail=1 --disable-warnings -q
      - name: Upload coverage
        if: success()
        run: |-
          pytest --cov=app --cov-report=xml

  quality_gate:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check coverage
        run: |
          # 簡易的な品質ゲート: coverage.xml を解析して閾値を確認(例: 70%)
          python -c "import sys,xml.etree.ElementTree as ET; t=ET.parse('coverage.xml').getroot(); cov=float(t.get('line-rate'))*100; print('coverage',cov); sys.exit(0 if cov>=70 else 1)"

  deploy:
    needs: quality_gate
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Manual approval
        uses: chrnorm/deploy-approval@v1
      - name: Deploy (placeholder)
        run: echo "ここでデプロイ処理を呼ぶ"

ポイント: 品質ゲートは自動化の中心です。カバレッジだけでなく、統合テストの「期待値(業務ルール)」をチェックするジョブを用意してください。

6) 本番デプロイ前の安全策(スモーク・カナリア・段階的ロールアウト)

デプロイ前に次を実施します。

  • スモークテスト:基本APIが生きているかを簡易リクエストで確認
  • カナリアリリース:トラフィックの一部だけ新バージョンへ流す(例: 5% → 25% → 100%)
  • 段階的ロールアウト:ユーザ属性やワークロード別に順次切り替える

運用ルールはCIの一部として自動化し、失敗時は自動ロールバックすることを推奨します。

7) テストの自動化で注意する運用上の落とし穴

問題 対策
偽陽性(テストが失敗するが本質は問題ない) テストデータと期待値を業務ルールで定義し、安定的な条件で評価する
偽陰性(テストは通るが本番で問題が発生) 本番に近い統合テストとE2Eを用意、カナリアで実運用確認
データシフト 定期的にテストデータを見直し、代表ケースを更新する
コスト モデルAPI呼び出しはモック化、実コスト検証は限定的に行う

8) チェックリストと導入テンプレート

導入時に確認すべき項目を示します。

項目 確認ポイント
リポジトリ構成 tests/, conftest.py, CIワークフローが最小限あるか
モック方針 外部呼び出しは全て抽象化しテストで差し替え可能か
品質ゲート カバレッジ/受け入れルールがCIに組み込まれているか
デプロイ安全策 スモーク/カナリア/ロールバック方針があるか

導入テンプレート(最小ファイル一覧):

app/
  rag_service.py         # ビジネスロジック
tests/
  conftest.py
  test_unit.py
  test_integration.py
requirements.txt
.github/workflows/ci.yml

関連記事への案内

本記事は第93回(RAG運用)および第84回(プロンプト管理)を前提にしています。検索インデックスやプロンプトテンプレート変更時の回帰テスト設計は、以下を参考にしてください。

まとめ

AIを実務で安定運用するには、単なる手作業ではなくテストとCI/CDの仕組みが不可欠です。ユニット→統合→E2Eの階層を整え、外部依存はモックで制御し、CIで品質ゲートを設定することでリスクを大幅に減らせます。まずは上に示した最小構成で始め、運用の中でテストケースとデータを徐々に増やすことをおすすめします。記事中のサンプルをコピーして、まずはローカルでpytestを回してみてください。

付記: サンプルリポジトリ(テンプレート)は本記事の補助として公開予定です。公開時は本記事上部にリンクを掲載します。