Microsoft Foundry と GitHub Actions で AI 評価を自動化する


はじめに

こんにちは!最近 GenAIOps まわりのワークフローをひたすら触っている横浜情報機器株式会社のロホマン シャヒンです。

RS

ロホマン シャヒン

  • Microsoft Student Ambassador
  • GitHub Copilot User Group Japan 運営
  • Azure Solutions Architect Expert 取得

Microsoft Learn に「Foundry と GitHub Actions で AI 評価を自動化する」というモジュールが公開されていて、さっそく手を動かしてみました。

AI エージェントの出力品質を毎回手で確認するのはもう限界。自動化の仕組みを整理してみました。


1. そもそも AI 評価はなぜ自動化すべきか

手動レビューには構造的な限界があります。

500 件の応答を人間がレビューすると数日かかる。評価者が疲れると判断がブレる。プロンプトを少し変えるたびに同じ工数がかかる——これが積み重なると、改善サイクルが全然回せなくなります。

自動評価に切り替えると、同じ 500 件が 10〜15 分で終わります。毎回同じ基準で、一貫して。

ただし「自動だから万能」では当然なくて、ドメイン固有のニュアンスや暗黙的な要件は人間が補う必要があります。だから理想は Human-in-the-Loop(HITL)——自動化でボリュームをさばき、人間は「自動化が拾いきれないもの」に集中する、というモデルです。

評価の種類 強み 弱み
人間による評価 微妙なニュアンス・コンテキスト理解 低速・高コスト・一貫性にバラつき
自動評価 高速・一貫性・大量テスト 人間的なコンテキストがない
HITL 両方の長所を組み合わせ 設計コストがかかる

2. どんなシステムで使えるか——実際のユースケース

「評価の自動化」と聞くと実験フェーズのツールに聞こえますが、実運用で刺さるシナリオがいくつかあります。実際に試してみて「これは使える」と感じた場面を紹介します。

ユースケース 1:社内カスタマーサポートボット

IT ヘルプデスクや HR 向けの社内チャットボットは、プロンプトを少し変えると途端に的外れな回答を返すことがある。手動で確認するには全ケースを試す必要があって、現実的じゃないんです。

Foundry で評価を組むと、プロンプト変更のたびに「パスワードリセットの手順を教えて」「VPN が繋がらない」などの想定質問 200 件を自動でテストできます。Groundedness スコアが下がれば、「この変更でコンテキスト参照が壊れた」とすぐわかる。

ユースケース 2:RAG アプリのドキュメント更新対応

社内規定や製品マニュアルを参照する RAG(Retrieval-Augmented Generation)アプリで、ドキュメントが更新されたとき。

「ドキュメントを差し替えたら古い情報に基づいた回答が返らなくなったか」を確認するために、評価を自動で回します。Groundedness が高ければドキュメントを正しく参照している証拠。低ければインデックスの再構築や chunk 設計を見直すサインです。

ユースケース 3:プロンプトのモデル移行テスト

GPT-4o から別モデルに切り替えるとき、既存プロンプトがそのまま使えるか検証したい。

モデルごとに同じテストデータセットを流して、Coherence・Relevance のスコアを比較します。これをやらずに移行すると「なんか回答の質が落ちた」が後から発覚するリスクがある。CI/CD に組み込んでおけば移行 PR の時点で差分が見えます。

ユースケース 重視するメトリック しきい値の目安
社内サポートボット Groundedness・Relevance ≥ 4.0
RAG ドキュメント参照 Groundedness ≥ 4.5(コンプライアンス要件が高い場合)
モデル移行テスト Coherence・Relevance・Fluency ≥ 前モデルのスコア
コンシューマー向けチャット Fluency・Safety Fluency ≥ 4.0, Safety = 5.0
実際のユースケース別・評価指標の使い分け

3. Microsoft Foundry のエバリュエーターを使いこなす

Microsoft Foundry には、AI 応答の品質を測る組み込みエバリュエーターが用意されています。3 つだけ知っていれば十分……ではなくて、用途によって使い分けが大事です。

品質系エバリュエーター(コア 5 つ)

エバリュエーター 評価軸 使いどころ
Groundedness 応答がコンテキスト・ドキュメントに基づいているか RAG / ドキュメント参照系
Relevance 質問に対して適切に答えているか あらゆる Q&A ボット
Coherence 文章として論理的に一貫しているか 長文回答・レポート生成
Fluency 文法・自然な表現になっているか ユーザー向けコンテンツ生成
Similarity 期待される回答との類似度 既存の「正解」が存在するタスク

リスク・安全系エバリュエーター

企業向けに AI を使う場合、品質だけでなく「安全かどうか」も評価する必要があります。Foundry には Safety エバリュエーターも用意されていて、以下を自動検出できます:

  • Violence:暴力的なコンテンツが含まれていないか
  • Hate/Unfairness:ヘイトスピーチや差別的な表現がないか
  • Sexual Content:不適切なコンテンツが含まれていないか
  • Self-Harm:自傷を促す内容がないか

コンシューマー向けアプリや、外部公開の AI サービスではここが最重要になります。スコアは 1〜5 で返り、5 が最も安全な状態(品質系と逆方向なので注意)。

from azure.ai.evaluation import (
    evaluate,
    GroundednessEvaluator,
    RelevanceEvaluator,
    CoherenceEvaluator,
    FluencyEvaluator,
    ContentSafetyEvaluator,
)
from azure.identity import DefaultAzureCredential

credential = DefaultAzureCredential()

project_config = {
    "subscription_id": "<AZURE_SUBSCRIPTION_ID>",
    "resource_group_name": "<AZURE_RESOURCE_GROUP>",
    "project_name": "<FOUNDRY_PROJECT_NAME>",
}

evaluators = {
    "groundedness": GroundednessEvaluator(credential=credential, azure_ai_project=project_config),
    "relevance": RelevanceEvaluator(credential=credential, azure_ai_project=project_config),
    "coherence": CoherenceEvaluator(credential=credential, azure_ai_project=project_config),
    "fluency": FluencyEvaluator(credential=credential, azure_ai_project=project_config),
    "safety": ContentSafetyEvaluator(credential=credential, azure_ai_project=project_config),
}

しきい値の設定戦略

しきい値は「4.0 以上なら OK」のように固定するのが一番シンプルですが、本番運用ではもう少し工夫が要ります。

段階的なしきい値の考え方:

def evaluate_results(metrics: dict) -> dict:
    """しきい値チェックと合否判定"""
    thresholds = {
        "groundedness": 4.0,
        "relevance": 4.0,
        "coherence": 3.5,   # 長文生成は少し緩め
        "fluency": 4.0,
    }

    failed_metrics = []
    warnings = []

    for metric, threshold in thresholds.items():
        score = metrics.get(metric, 0)
        if score < threshold:
            failed_metrics.append(f"{metric}: {score:.2f} (閾値: {threshold})")
        elif score < threshold + 0.3:
            # しきい値は超えたが、マージンが小さい場合は警告
            warnings.append(f"{metric}: {score:.2f} (注意: 閾値に近い)")

    return {
        "passed": len(failed_metrics) == 0,
        "failed_metrics": failed_metrics,
        "warnings": warnings,
    }

最初はしきい値を少し低め(3.5 程度)に設定して、実際のデータで感触をつかんでから引き上げていくのが現実的です。最初から 4.5 に設定すると常に FAILED になって CID が機能しなくなります。


4. 評価データセットの作り方

評価を自動化するには、テスト用の質問と期待される回答をセットにした 評価データセット が必要です。

JSONL 形式が基本で、こんな構造です:

{"query": "パスワードをリセットするには?", "context": "パスワードリセットは社内ポータル > アカウント設定 > パスワードリセットから実施できます。", "response": "社内ポータルのアカウント設定からパスワードリセットが可能です。"}
{"query": "VPN が繋がらない場合は?", "context": "VPN 接続エラーの場合は、まず Cisco AnyConnect を再起動してください。改善しない場合は IT ヘルプデスクへ連絡。", "response": "Cisco AnyConnect を再起動してください。それでも接続できない場合は IT ヘルプデスクへご連絡ください。"}
{"query": "在宅勤務の申請手順は?", "context": "在宅勤務申請は Workday の「勤怠管理」メニューから翌週分を金曜 17:00 までに提出が必要。", "response": "Workday の勤怠管理から申請できます。翌週分は金曜 17:00 が締め切りです。"}

データの集め方:3 つのアプローチ

アプローチ 1:実運用ログからサンプリング

実際のユーザーのやりとりをそのまま使うのが最も質が高い。ただし PII(個人情報)が含まれていることが多いので、サンプリング前に必ず匿名化を通す必要があります。

import json
import random

def sample_from_logs(log_file: str, n: int = 100, seed: int = 42) -> list[dict]:
    """本番ログから評価データをサンプリングする"""
    random.seed(seed)
    logs = []
    with open(log_file) as f:
        for line in f:
            entry = json.loads(line)
            # PII フィールドを除去(メールアドレス・氏名など)
            entry.pop("user_email", None)
            entry.pop("user_name", None)
            logs.append(entry)
    return random.sample(logs, min(n, len(logs)))

アプローチ 2:合成データで量を補う

モデルに「このドキュメントに関して想定される質問と回答を 50 ペア生成して」と頼めば、データを素早く増やせます。ただし合成データは現実からズレやすいので、実データ 20〜30% + 合成データ 70〜80% くらいのブレンドが現実的です。

アプローチ 3:エッジケース専用データ

「通常のテストは通るが、特定の状況で壊れる」というケースを別途集めておく。たとえば:

  • 質問が極端に短い(「教えて」)
  • 複数のトピックが混ざった質問(「VPN と在宅勤務の申請を同時に教えて」)
  • コンテキストに答えが含まれていないケース(ハルシネーション検知用)

エッジケースを 10〜15% 混ぜておくと、モデル変更や大きなプロンプト改修で壊れるパターンを事前に拾えます。


5. Python でバッチ評価を実装する

Microsoft Foundry の Python SDK (azure-ai-evaluation) を使うと、評価をスクリプト化できます。

まず依存関係を入れます:

pip install azure-ai-evaluation azure-identity

基本の評価スクリプト

#!/usr/bin/env python3
"""run_evaluation.py — バッチ評価スクリプト"""
import argparse
import json
import sys
from azure.ai.evaluation import (
    evaluate,
    GroundednessEvaluator,
    RelevanceEvaluator,
    CoherenceEvaluator,
    FluencyEvaluator,
)
from azure.identity import DefaultAzureCredential

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--test-data", required=True, help="JSONL テストデータのパス")
    parser.add_argument("--output", default="results.json", help="結果の出力先")
    parser.add_argument("--threshold-groundedness", type=float, default=4.0)
    parser.add_argument("--threshold-relevance", type=float, default=4.0)
    parser.add_argument("--threshold-coherence", type=float, default=3.5)
    args = parser.parse_args()

    credential = DefaultAzureCredential()
    project_config = {
        "subscription_id": os.environ["AZURE_SUBSCRIPTION_ID"],
        "resource_group_name": os.environ["AZURE_RESOURCE_GROUP"],
        "project_name": os.environ["FOUNDRY_PROJECT_NAME"],
    }

    evaluators = {
        "groundedness": GroundednessEvaluator(
            credential=credential, azure_ai_project=project_config
        ),
        "relevance": RelevanceEvaluator(
            credential=credential, azure_ai_project=project_config
        ),
        "coherence": CoherenceEvaluator(
            credential=credential, azure_ai_project=project_config
        ),
        "fluency": FluencyEvaluator(
            credential=credential, azure_ai_project=project_config
        ),
    }

    print(f"⏳ 評価開始: {args.test_data}")
    raw = evaluate(
        data=args.test_data,
        evaluators=evaluators,
    )

    # メトリックの平均スコアを集計
    metrics = {
        key: raw["metrics"].get(f"groundedness.gpt_groundedness", 0)
        for key in ["groundedness", "relevance", "coherence", "fluency"]
    }
    # 実際のキー名は azure-ai-evaluation のバージョンで異なる場合がある
    metrics = raw.get("metrics", {})

    # しきい値チェック
    thresholds = {
        "groundedness": args.threshold_groundedness,
        "relevance": args.threshold_relevance,
        "coherence": args.threshold_coherence,
    }

    failed = []
    for metric, threshold in thresholds.items():
        score = metrics.get(metric, 0)
        if score < threshold:
            failed.append({"metric": metric, "score": score, "threshold": threshold})

    output = {
        "metrics": metrics,
        "passed": len(failed) == 0,
        "failed_details": failed,
        "total_examples": raw.get("row_count", 0),
        "failed_examples": len(failed),
    }

    with open(args.output, "w", encoding="utf-8") as f:
        json.dump(output, f, ensure_ascii=False, indent=2)

    print(f"✅ 評価完了: {args.output}")
    for metric, score in metrics.items():
        status = "✅" if score >= thresholds.get(metric, 0) else "❌"
        print(f"  {status} {metric}: {score:.2f}")

    if not output["passed"]:
        print("\n❌ 品質チェック FAILED")
        sys.exit(1)  # GitHub Actions を失敗にする
    else:
        print("\n✅ 品質チェック PASSED")

if __name__ == "__main__":
    main()

sys.exit(1) を入れておくと、GitHub Actions のステップが失敗扱いになります。これがないと FAILED でもワークフローが緑になってしまうので要注意。

実行結果の results.json はこんな形:

{
  "metrics": {
    "groundedness": 4.25,
    "relevance": 4.10,
    "coherence": 3.85,
    "fluency": 4.30
  },
  "passed": true,
  "failed_details": [],
  "total_examples": 150,
  "failed_examples": 0
}
AI評価パイプライン全体構成(Python SDK → Foundry → GitHub Actions)

6. GitHub Actions に評価を統合する

ここからが本番で、個人的に一番面白いところです。

プロンプトや設定ファイルを変更する PR を出すと、自動で評価ワークフローが走って、結果が PR コメントに投稿されます。「マージしていいかどうか」をメトリックが判断してくれる仕組みです。

ワークフローファイルの構成

.github/workflows/evaluate-on-pr.yml に以下を配置します:

name: Evaluate Prompt Changes

on:
  pull_request:
    branches: [main]
    paths:
      - 'prompts/**'
      - 'config/**'
      - 'src/agents/**'   # エージェントのコードも評価対象に含める

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  run-evaluation:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Azure login (キーレス認証)
        uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

      - name: Run evaluation script
        run: |
          python run_evaluation.py \
            --test-data test-data/test_dataset.jsonl \
            --output results.json \
            --threshold-groundedness 4.0 \
            --threshold-relevance 4.0 \
            --threshold-coherence 3.5
        env:
          AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
          AZURE_RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP }}
          FOUNDRY_PROJECT_NAME: ${{ vars.FOUNDRY_PROJECT_NAME }}

      - name: Post results to PR
        if: always()   # 失敗でも必ずコメントを投稿する
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const results = JSON.parse(fs.readFileSync('results.json'));
            
            const statusIcon = results.passed ? '✅ PASSED' : '❌ FAILED';
            
            let metricsTable = '| メトリック | スコア | 閾値 | 判定 |\n|---|---|---|---|\n';
            const thresholds = { groundedness: 4.0, relevance: 4.0, coherence: 3.5, fluency: 4.0 };
            for (const [key, score] of Object.entries(results.metrics)) {
              const threshold = thresholds[key] || '-';
              const icon = threshold !== '-' && score >= threshold ? '✅' : (threshold !== '-' ? '❌' : '—');
              metricsTable += `| ${key} | ${score.toFixed(2)} | ${threshold} | ${icon} |\n`;
            }

            const failedDetails = results.failed_details.length > 0
              ? '\n\n**失敗したメトリック:**\n' + results.failed_details
                  .map(d => `- ${d.metric}: ${d.score.toFixed(2)} (閾値: ${d.threshold})`)
                  .join('\n')
              : '';

            const comment = `## 🤖 AI 評価結果\n\n**ステータス: ${statusIcon}**\n\n${metricsTable}${failedDetails}\n\n評価件数: ${results.total_examples} 件`;
            
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: comment
            });

if: always() をつけておくと、評価スクリプトが FAILED で終わってもコメントが投稿されます。これがないと FAILED 時にコメントが来なくて「なんで落ちたか」がわからなくなります。

Azure 認証の設定(キーレス認証)

GitHub Actions から Azure に安全につなぐには、フェデレーション ID 資格情報を使います。シークレットに認証情報を直書きしないのがポイントです。

# アプリ登録の作成
az ad app create --display-name "github-actions-eval"
APP_ID=$(az ad app list --display-name "github-actions-eval" --query "[0].appId" -o tsv)

# フェデレーション資格情報の設定(main ブランチと PR トリガー両方を登録)
az ad app federated-credential create --id $APP_ID --parameters '{
  "name": "github-main",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:YOUR_ORG/YOUR_REPO:ref:refs/heads/main",
  "audiences": ["api://AzureADTokenExchange"]
}'

az ad app federated-credential create --id $APP_ID --parameters '{
  "name": "github-pr",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:YOUR_ORG/YOUR_REPO:pull_request",
  "audiences": ["api://AzureADTokenExchange"]
}'

# ロールの割り当て
az role assignment create \
  --assignee $APP_ID \
  --role "Cognitive Services User" \
  --scope /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{foundry}

ポイント: pull_request のトリガーは ref:refs/heads/main とは別のフェデレーション資格情報が必要です。main だけ登録すると PR トリガーで認証エラーになります(最初これで詰まりました)。

GitHub リポジトリの Settings > Secrets and variables > Actions > Variables に以下を追加します:

変数名
AZURE_CLIENT_ID サービスプリンシパルのアプリケーション ID
AZURE_TENANT_ID Azure テナント ID
AZURE_SUBSCRIPTION_ID サブスクリプション ID
AZURE_RESOURCE_GROUP Foundry プロジェクトのリソースグループ
FOUNDRY_PROJECT_NAME Microsoft Foundry プロジェクト名

PR コメントに結果が届く

設定が完了すると、プロンプト変更の PR を出すたびにこういうコメントが自動でつきます:

## 🤖 AI 評価結果

**ステータス: ❌ FAILED**

| メトリック    | スコア | 閾値 | 判定 |
|---|---|---|---|
| groundedness | 4.25   | 4.0  | ✅  |
| relevance    | 4.10   | 4.0  | ✅  |
| coherence    | 3.45   | 3.5  | ❌  |
| fluency      | 4.20   | 4.0  | ✅  |

**失敗したメトリック:**
- coherence: 3.45 (閾値: 3.5)

評価件数: 150 件

Coherence がしきい値を下回っているので ❌ FAILED。「どのメトリックが何点でどの閾値を下回ったか」がコメントに出るので、何を修正すればいいかがすぐわかります。これが来るだけでプロンプト変更のリスクがぐっと下がります。

GitHub Actions から Foundry 経由で PR にメトリックが届くまでのフロー

7. 環境別ワークフロー(dev / staging / prod)

本番投入まで 3 段階で評価を分けておくと、品質ゲートをより確実に機能させられます。

dev ブランチ(PR)     → ライト評価(100 件、閾値やや低め)    ← 素早くフィードバック
  ↓ マージ
staging ブランチ      → フル評価(500 件、本番相当の閾値)     ← 本番前の最終確認
  ↓ マージ
main(本番)          → デプロイ後の定期評価(毎日 0:00 実行)  ← 品質の経時変化を監視

定期評価ワークフローは schedule トリガーで動かします:

on:
  schedule:
    - cron: '0 0 * * *'   # 毎日 0:00 UTC(日本時間 9:00)
  workflow_dispatch:        # 手動トリガーも残す

定期評価の結果が下がり始めたら、「プロンプトは変わっていないのに品質が落ちている」= モデルのアップデートやデータドリフトのサイン。これを自動で拾えるのが定期評価の強みです。


8. よくあるハマりポイントと対処法

試してみてつまずいた箇所をまとめておきます。

① AZURE_SUBSCRIPTION_ID を env に渡さないと evaluate() が失敗する

vars.AZURE_SUBSCRIPTION_IDazure/login@v2 に渡しているだけでは不足で、Python スクリプトにも env: で渡す必要があります。エラーメッセージが DefaultAzureCredential failed として出るので原因がわかりにくい。

if: always() を忘れると失敗時にコメントが来ない

前のステップが exit(1) で終わると、デフォルトでは後続ステップがスキップされます。PR コメント投稿ステップには if: always() を必ず入れること。

③ フェデレーション資格情報は main と pull_request で別々に必要

セクション 6 でも触れましたが、PR トリガーと main push トリガーは subject が異なります。main のみ登録して「なぜか PR で認証エラー」になるのが一番多いパターンです。

④ しきい値が高すぎて常に FAILED になる

4.5 以上を最初から要求すると、ほぼ常に失敗します。最初は 3.5〜4.0 で様子を見てから段階的に上げていく方が現実的です。


9. まとめ

今回試してみて感じたことをまとめます。

  • 自動評価は「人間の代替」ではなく「拡張」。500 件を自動でさばいて、人間は本当に難しいケースだけ見ればいい
  • ユースケースによって使うエバリュエーターが違う。RAG なら Groundedness 最優先、コンシューマー向けなら Safety を外せない
  • GitHub Actions との統合で CI/CD に組み込める。プロンプト変更の PR に評価が自動でかかる、というのが地味に強い
  • 環境別の評価戦略(dev/staging/prod)を組んでおくと、本番の品質を長期的に維持できる
  • キーレス認証(フェデレーション資格情報)はシークレット管理不要で推奨。ただし main と pull_request で別登録が必要な点に注意
  • しきい値は最初低めに設定して段階的に引き上げる。最初から厳しくすると運用が破綻します

AI エージェントの品質管理を仕組みで解決したい方には、この構成はかなり実践的だと思います。

この記事が少しでも参考になったら、ぜひシェアしていただけると嬉しいです!


出典

技術ブログ一覧に戻る