0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vertex AI Gen AI EvaluationでLLMの回答品質を評価する

0
Posted at

Vertex AIの Gen AI Evaluation Service は、LLMの回答品質を指標にもとづいて定量評価するサービスです。この記事では、評価方式の違い、日本語カスタムルーブリックの実装、モデル比較によるリグレッション判定、そしてCIへの組み込みまでを扱います。

検証環境は基本 us-central1 で、Gemini 3.5系を呼ぶ箇所のみ global です。SDKは google-cloud-aiplatform 1.148.1を使用しています。

評価方式は2つ — judgeがいるかどうか

最初に押さえるべきは、「評価対象のLLM」と「評価するLLM(judge)」の区別です。ここを分けて考えないと、後のコスト設計もモデル選定も噛み合いません。

方式1: Computation-based(exact matchなど)

正解が一意に決まるタスク向けです。分類、ラベリング、構造化出力。

プロンプト①          評価対象LLM        ルール比較           結果
分類指示+入力文  →   「経費」と回答  →  正解ラベルと文字列一致  →  1/0

judgeがいません。 評価対象LLMが出した回答を、決定的アルゴリズムで正解と照合するだけです。採点にLLM推論が走らないので安く、実行するたび同じ結果が出ます

ただし「無料」ではありません。公式価格表では computation-based も Automatic Metric として文字数課金の対象です。

Computation-based metrics are charged at $0.00003 per 1k characters for input and $0.00009 per 1k characters for output.

加えて、その場でモデルに回答を生成させる場合は候補モデルの推論費用もかかります。judge の推論費用が乗らない、というのが方式1のコスト上の利点です。

用意されている指標は3つです。SDKでは識別子で指定し、コンソールでは日本語で表示されます。

SDKでの指定 コンソール表示 何を測るか 主な用途
exact_match 完全一致 回答が正解と完全に一致するか 分類・ラベリングなど正解が1つに決まるタスク
bleu BLEU 正解と一致するn-gram(連続する単語列)の数 翻訳品質の標準指標
rouge_l ROUGE 正解と回答で重なる語の割合(最長共通部分列ベース)。再現率寄り 要約が正解の要点を拾えているか

後半のリグレッションテストでは exact_match を使います。分類タスクなので、正解ラベルとの文字列一致で判定できます。

方式2: LLM-as-a-Judge(model-based)

要約や自由記述など、正解が一つに定まらないタスク向けです。

プロンプト①      評価対象LLM     プロンプト②              judge LLM
質問・作業指示 →  回答を生成  →  ルーブリック+質問+回答 →  基準に沿って採点

方式2で初めてプロンプトが2つになります。 1つ目は回答を作らせるためのもの、2つ目は採点基準(ルーブリック)をjudgeに渡すためのものです。judgeの推論分が課金されます。

採点基準(ルーブリック)の作り方には2種類あります。

静的ルーブリック — 全プロンプトに同じ採点基準を適用します。「1〜5で採点、5は非常に丁寧、1は失礼」のような基準をあらかじめ決めておき、すべての回答を同じものさしで測ります。流暢さ、安全性、ソースに基づいているか、といった定番の基準は用意されていますし、自分で書くこともできます。

適応型ルーブリック — プロンプトごとに合否テストを自動生成します。「4文で要約せよ」というプロンプトなら「4文になっているか」というテストが作られ、Pass/Failで判定されます。ソフトウェアのユニットテストに近い考え方で、公式が推奨しているのはこちらです。

用途が違います。全プロンプトを同じ軸で比較したいなら静的、プロンプトごとに「要求を満たしているか」を見たいなら適応型です。

この記事では静的ルーブリックを使います。日本語の採点基準を自分で書いて、judgeがどう採点するかを見ていきます。

評価対象のモデルはGeminiに限らない

方式1でも方式2でも、評価される側のLLMは自由に選べます。回答の用意には2通りあります。

# その場でモデルを呼んで生成させる
task.evaluate(model="gemini-2.5-flash")

# 生成済みの回答を持ち込む(BYO response)
#   dataset に response 列を入れておけば model= は不要
task.evaluate()

後者ならGen AI Evaluationがモデルを呼ばないので、Claudeでも OpenAI でもローカルモデルでも、人間が書いた文章でも評価できます。実際、コンソールのモデル選択ドロップダウンにも claude-sonnet-5 claude-opus-5 が並びます。

一方で採点する側(judge)は、サービスが用意する範囲ではGemini中心です。翻訳評価用の例外はありますが、汎用のテキスト評価でGemini以外を選ぶ選択肢は組み込みでは用意されていません(自前で実装する道はあります。後述)。この非対称性と、judge指定でつまずく箇所は記事の後半でまとめて扱います。

動かしてみる① 日本語カスタムルーブリックで品質評価

まず方式2から。judgeに採点させるパターンです。ここでは評価対象LLMを呼ばず、事前生成済みの応答を持ち込んで(BYO response)、judgeの採点部分だけを動かします。

その前に、SDKの入口が2つあることに触れておきます。同じサービスを呼ぶのに書き方が2通りあり、ドキュメントでもコード例が混在しているためです。

EvalTask — 評価データと指標をまとめてインスタンスにして、.evaluate() で実行します。

from vertexai.evaluation import EvalTask

task = EvalTask(dataset=df, metrics=["exact_match"])
result = task.evaluate(model="gemini-2.5-flash")

vertexai.Client — クライアントを作り、そこから評価を呼びます。

from vertexai import Client

client = Client(project=PROJECT_ID, location=LOCATION)
result = client.evals.evaluate(dataset=eval_dataset, metrics=[...])

公式ドキュメントは EvalTask を「古いインターフェース、開発は行われていない」、Client を「推奨、ただしプレビュー」と位置づけています。前者はGA(正式提供)です。

この記事では EvalTask を使います。 静的ルーブリックはどちらでも書けます。前節で触れた適応型ルーブリックは Client 専用なので、この記事では扱いません。

import pandas as pd
import vertexai
from vertexai.evaluation import EvalTask, PointwiseMetric, MetricPromptTemplateExamples

PROJECT_ID = "your-project-id"
LOCATION = "us-central1"

vertexai.init(project=PROJECT_ID, location=LOCATION)

eval_df = pd.DataFrame(
    {
        "prompt": [
            "クラウドとオンプレミスの違いを一文で説明して。",
            "BigQuery とは何ですか?",
            "有給休暇の申請方法を教えて。",
        ],
        "response": [
            "クラウドは事業者が保有する計算資源をネットワーク経由で利用する形態で、オンプレミスは自社で機器を保有・運用する形態です。",
            "BigQuery は Google Cloud のフルマネージドなサーバーレスデータウェアハウスで、SQL で大規模データを高速に分析できます。",
            "知らんけど、たぶん上司に聞けばいいんじゃないですかね。",
        ],
    }
)

3件目にわざと雑な応答を混ぜてあります。これをjudgeがどう採点するかを見ます。

指標は2つ使います。ひとつは組み込みの FLUENCY(流暢さ)、もうひとつは自作の採点基準です。

ポイントは、カスタム指標を日本語のルーブリックで定義できることです。

politeness = PointwiseMetric(
    metric="politeness_ja",
    metric_prompt_template=(
        "あなたは応答品質の評価者です。以下の AI の応答が、社内ヘルプデスクとして"
        "適切な丁寧さ・プロフェッショナルさを持つかを 1〜5 で採点してください。\n"
        "5: 非常に丁寧で信頼できる / 3: 普通 / 1: 失礼または無責任\n\n"
        "# 質問\n{prompt}\n\n# 応答\n{response}\n\n"
        "まず理由を説明し、最後に score: <数値> の形式で採点してください。"
    ),
)

eval_task = EvalTask(
    dataset=eval_df,
    metrics=[
        MetricPromptTemplateExamples.Pointwise.FLUENCY,  # 組み込み指標
        politeness,                                       # カスタム指標
    ],
    experiment="genai-eval-sample",
)

result = eval_task.evaluate()

FLUENCY のほかにも組み込みの静的ルーブリックが用意されています。MetricPromptTemplateExamples.Pointwise から選べます。

指標 何を測るか
FLUENCY 対象言語として流暢か。文法・語彙の自然さ
GROUNDEDNESS 回答が提供されたソースに基づいているか。RAGのハルシネーション検出に使う
SAFETY ヘイトスピーチや危険なコンテンツなど、安全性ポリシー違反がないか
INSTRUCTION_FOLLOWING プロンプトの制約や指示にどれだけ従っているか
SUMMARIZATION_QUALITY 要約の品質
QUESTION_ANSWERING_QUALITY 質問応答の品質

ほかに COHERENCE VERBOSITY MULTI_TURN_CHAT_QUALITY MULTI_TURN_SAFETY もあります。

公式ドキュメントは新しい vertexai.Client 向けに書かれていることが多く、指標名が違います。たとえばグラウンディングは、新しい方では types.RubricMetric.GROUNDING ですが、EvalTask では GROUNDEDNESS です。そのまま持ってくると AttributeError になります。

結果

応答 fluency politeness_ja
クラウドとオンプレミスの違い(真面目な回答) 5.0 5.0
BigQueryとは(真面目な回答) 5.0 4.0
「知らんけど、たぶん上司に聞けばいいんじゃないですかね。」 3.0 1.0

狙いどおり雑な応答がpoliteness_jaで1.0になりました。judgeの説明はこうです。

「知らんけど」「たぶん上司に聞けばいいんじゃないですかね」といった表現は社内ヘルプデスクとして極めて不適切で、情報提供の責任感がなく、失礼な印象を与えます。

気づいた点2つ

1. 組み込み指標の説明は英語、カスタムルーブリックは日本語で返る

同じ応答に対する fluency の説明はこうでした。

The response is grammatically correct in Japanese, and its flow is natural for a very casual, informal remark. However, the word choice, specifically '知らんけど' ... creates an informal, uncertain, and almost dismissive tone which is highly inappropriate for an AI assistant ...

組み込み指標のプロンプトテンプレートが英語なので、説明も英語で返ります。ルーブリックを日本語で書けばjudgeも日本語で返すので、日本語ユースケースの評価結果を人間がレビューする運用なら、カスタム指標にしておくほうが読みやすいです。

2. 2つの指標は別のものを見ている

雑な応答のfluencyは3.0で、そこまで低くありません。日本語として破綻していないからです。「流暢さ」と「業務上の適切さ」は別物なので、組み込み指標だけでは業務要件を満たしているか判断できないことがわかります。ここはカスタムルーブリックを書く価値がある部分です。

なお、BigQueryの回答がpoliteness 4.0になった理由は「挨拶や追加の問いかけがなく、人間らしい丁寧さに一歩及ばない」でした。ルーブリックの書き方次第で判定はいくらでも動くので、judgeの説明文を読んで意図とズレていないか確認する工程は省略できません。

動かしてみる② リグレッションテスト本体

ここからが本題です。ゴールデンデータセット(入力と正解のペアを集めた評価用データ)を1つ用意し、現行モデルと移行候補モデルの両方に同じ問題を解かせて、正解率を比較します。ソフトウェアのリグレッションテストと同じ発想です。

分類タスクなので computation-based指標のみで回せます。judgeの推論費用がかからない分、方式2より安く済みます。

import pandas as pd
import vertexai
from vertexai.evaluation import EvalTask

PROJECT_ID = "your-project-id"
LOCATION = "global"                    # 検証環境では3.5系がここでしか呼べなかった(後述)

vertexai.init(project=PROJECT_ID, location=LOCATION)   # 前節から location を変えるので再実行する

BASELINE_MODEL = "gemini-2.5-flash"    # 現行モデル
CANDIDATE_MODEL = "gemini-3.5-flash"   # 移行候補モデル
MAX_ACCURACY_DROP = 0.05               # 精度低下の許容閾値

LABELS = ["経費", "勤怠", "IT", "その他"]
golden = pd.DataFrame(
    {
        "text": [
            "出張の新幹線代はどうやって精算すればいいですか?",
            "先月の残業時間の集計が合っていない気がします。",
            "ノートPCが起動しなくなりました。",
            "VPNに接続できずリモートワークができません。",
            "有給休暇の残日数はどこで確認できますか?",
            "会社の駐車場は使えますか?",
        ],
        "reference": ["経費", "勤怠", "IT", "IT", "勤怠", "その他"],
    }
)

# 入力文を、ラベル名だけを返させるプロンプトに変換する
golden["prompt"] = golden["text"].apply(
    lambda t: (
        f"次の社内問い合わせを {' / '.join(LABELS)} のいずれか1つに分類してください。"
        f"ラベル名のみを出力すること。\n\n問い合わせ: {t}"
    )
)

EvalTask に渡すデータセットには prompt 列(モデルへの入力)と reference 列(正解)が必要です。text はもとの問い合わせ文で、そこからプロンプトを組み立てています。exact_match は前後の空白や改行も含めて比較するので、「ラベル名のみを出力すること」で余計な文字を出させないようにしています。

評価の実行部分です。

def run_eval(model_name: str):
    task = EvalTask(
        dataset=golden[["prompt", "reference"]],
        metrics=["exact_match"],
    )
    return task.evaluate(model=model_name)


def accuracy(result):
    """評価結果から正解率と明細を取り出す"""
    df = result.metrics_table.copy()
    df["correct"] = df["exact_match/score"] == 1.0
    return result.summary_metrics["exact_match/mean"], df


base_acc, base_df = accuracy(run_eval(BASELINE_MODEL))
cand_acc, cand_df = accuracy(run_eval(CANDIDATE_MODEL))

EvalTask には実行履歴を残す experiment= という引数がありますが、location="global" では指定できません。指定すると 400 MetadataStore is not supported in region global で落ちます。理由は後述します。

総合精度だけを見てはいけない

ここが設計上いちばん大事なところです。

diff = pd.DataFrame({
    "text": golden["text"],
    "reference": golden["reference"],
    "baseline": base_df["response"],
    "candidate": cand_df["response"],
    "baseline_ok": base_df["correct"],
    "candidate_ok": cand_df["correct"],
})
regressed = diff[diff["baseline_ok"] & ~diff["candidate_ok"]]
improved  = diff[~diff["baseline_ok"] & diff["candidate_ok"]]

総合精度が同じでも、中身が入れ替わっていることがあります。 3件間違っていたのが3件間違っている、でも間違っているケースが別、という状況です。

このとき「今まで正解していたケースが不正解になった」= regressed が実質的なリグレッションです。総合精度が維持されていても、ここに該当するケースがあれば人手レビューに回すべきです。逆に improved だけなら安心して移行できます。

判定はこの2段構えにしています。

if base_acc - cand_acc > MAX_ACCURACY_DROP:
    print(f"NG: 精度低下 {base_acc - cand_acc:.1%} が閾値 {MAX_ACCURACY_DROP:.0%} を超過。移行不可。")
elif len(regressed):
    print("条件付きOK: 総合精度は維持だがリグレッションケースあり。個別確認を推奨。")
else:
    print("OK: 精度維持・リグレッションなし。移行可能。")

実行結果

== baseline: gemini-2.5-flash
Generating a total of 6 responses from Gemini model gemini-2.5-flash using genai module.
Multithreaded Batch Inference took: 3.33 seconds.
Evaluation Took: 1.58 seconds

== candidate: gemini-3.5-flash
Warning: there are non-text parts in the response: ['thought_signature'],
returning concatenated text result from text parts.
(同じ警告が6件分すべてに出る)
Generating a total of 6 responses from Gemini model gemini-3.5-flash using genai module.
Multithreaded Batch Inference took: 3.52 seconds.
Evaluation Took: 1.44 seconds

===== 精度比較 =====
baseline  (gemini-2.5-flash): 100.0%
candidate (gemini-3.5-flash): 100.0%

リグレッション: 0 件 / 改善: 0 件

===== 判定 =====
OK: 精度維持・リグレッションなし。移行可能。

6件・両モデルとも満点という、面白みのない結果になりました。データセットが小さく問題も素直なので当然ではあります。

見てほしいのは精度の数字ではなく所要時間です。6件の推論と評価が1モデルあたり5秒前後で終わっています。この規模感なら CIに組み込んでも十分許容できることがわかります。実運用では1ユースケースあたり100〜300件程度のゴールデンデータセットを想定していますが、それでも数分のオーダーに収まる見込みです。

候補モデル側にだけ出ている thought_signature の警告は、Gemini 3系が思考の署名を応答に含めるためです。SDKがテキスト部分だけを連結して返した、という通知で、エラーではありません。2.5系では出ませんでした。分類タスクのように最終テキストだけが要る用途では無視して構いませんが、ログが件数分埋まるので気になる場合はロガーのレベルを調整してください。

この記事の検証はゴールデンデータセット6件、品質評価3件という小規模なものです。仕組みの動作確認が目的で、モデル間の精度差を論じられる規模ではありません。

実行結果はコンソールに残る — ただし global では残らない

EvalTaskexperiment を指定しておくと、各runがVertex AI Experiments(コンソール上は「テスト」)に記録されます。run名、model_nameexact_match/mean が一覧で並びます。

ただし、上のコードのように location="global" で実行するとこれが使えません。

400 POST .../locations/global/metadataStores?metadataStoreId=default:
MetadataStore is not supported in region global

experiment を指定すると裏でVertex ML MetadataのMetadataStoreを作ろうとしますが、これが global に存在しないため、評価が始まる前に落ちます。experiment 引数を外せば評価自体は通ります。

つまり今回のケースでは、us-central1 では gemini-3.5-flash が404で呼べず、global ではExperimentsが400で使えない、という板挟みになりました。前者は検証環境固有の結果で、公式には3.5 Flashの対応リージョンは Global, Multi-region, Americas, Europe, Asia Pacific とされています(リージョン別の結果はjudgeの章の表にまとめます)。一方、global でExperimentsが使えないのは仕様です。

global を選ばざるを得ない状況になると実行履歴がコンソールに残らないので、後述する「評価結果を自分で保存しておく」という話は設計上の理想論ではなく、実務上の必然になります。

以下は us-central1 で 2.5系同士を比較したときのrun一覧です。

run model_name exact_match/mean
candidate-gemini-2-5-flash-20260920-080946 gemini-2.5-flash 1
baseline-gemini-2-5-flash-lite-20260920-080946 gemini-2.5-flash-lite 1
(中略) 1
baseline-gemini-2-0-flash gemini-2.0-flash 0.0

最後の1行だけ 0.0 です。以前 gemini-2.0-flash をベースラインにしようとして実行したrunで、提供終了により全件失敗した記録が残っています。

実際、今このモデルを呼ぶと404が返ります。

google.api_core.exceptions.NotFound: 404 Publisher model
`projects/{PROJECT_ID}/locations/us-central1/publishers/google/models/gemini-2.0-flash`
was not found or your project does not have access to it.

「現行モデルと比較する」という発想でパイプラインを組んでいると、旧モデルが引退した瞬間にこうなります。比較対象そのものが消える。 この問題への対処は後述します。

本番運用で詰めるべき3点

ここまでは「動かしてみた」の範囲です。実際に基盤の品質チェックとして機能させるには、もう少し詰める必要があります。設計を検討する中で重要だと考えた3点を挙げます。

1. 評価対象は「本番同等の推論経路」にする

素のモデルを評価しても意味がありません。評価すべきは、本番と同じsystem prompt / RAG / tool calling / 後処理パーサを通した最終出力です。

各ユースケースの推論エンドポイント(またはそれと同一コードパスの評価用エントリポイント)にゴールデンデータセットを流し、得られた最終出力をBYO response方式でGen AI Evaluationに渡す構成にします。

これをやらないと「モデル単体の品質は通ったがアプリが壊れた」を防げません。パーサ例外やスキーマ不正も評価結果として捕捉できるようになります。

ただしこの構成には条件がつきます。評価ジョブが本番データを書き換えないようにすることです。データを作成・更新するtoolや外部API呼び出しは、評価時にはダミー実装や検証用バックエンドへ差し替える必要があります。評価を回すたびに本番のデータが増えていく、といった事故を防ぐためです。差し替えできる構成になっていることを、評価対象に加える条件にしておくべきだと考えています。

2. 評価結果を保存し、条件が一致するときだけ比較する

これが設計上いちばん引っかかった点です。

旧モデルが提供終了すると、比較対象そのものが呼び出せなくなります。 gemini-2.0-flash の404がまさにそれで、「前はどうだったか」を再取得する手段が消えます。先ほどのExperimentsに残っていた exact_match/mean = 0.0 のrunは、その状態で実行するとどうなるかの実例です。

なので、評価を実行するたびに全ケースの出力そのものをGCSやBigQueryに保存しておきます。EvalTask(output_uri_prefix="gs://...") を指定すれば metrics_table 相当をGCSへ書き出せるので、まずはこれを使うのが手軽です。現行構成の結果を「基準」として固定し、以後の比較はこの保存済み出力に対して行う。こうしておけば旧モデルが呼べなくなっても、ケース単位のリグレッション判定を再現できます。

もうひとつ。出力を保存するだけでは足りません。比較したときに出た差分がモデル由来なのか、それ以外の何かが変わったせいなのかを切り分ける必要があります。

そこで、そのとき何を使って評価したのか、条件一式を結果と一緒に保存します

  • モデルID
  • プロンプト版
  • データセット版
  • アプリコードSHA
  • RAGの参照ドキュメントの版
  • tool呼び出し先の構成(ダミー実装の版)

そして モデルID以外の条件がすべて一致している場合のみ、比較を有効とするというルールにします。一致しないなら比較不成立として扱う。

ここで重要なのは、これを標準機能に任せられないことです。今回の構成でExperimentsのrunに記録されたパラメータは model_name だけでした。

パラメータ: model_name → publishers/google/models/gemini-2.5-flash

SDKが自動で記録する項目は限られています。

記録される項目 条件
model_name 常に
prompt_template evaluate(prompt_template=...) を使った場合
generation_config の各値 / safety_settings モデルを GenerativeModel で渡し、かつ generation_configdict で渡した場合
output_file output_uri_prefix 指定時

generation_configGenerationConfig オブジェクトで渡すと条件を満たさず記録されませんし、上のコードのようにモデル名を文字列で渡す場合はそもそも generation_config がありません。

いずれにせよプロンプトの版も、アプリコードのSHAも、参照ドキュメントの版も残りません。つまり後からrunを並べても「モデル以外は同じ条件だったのか」を確認する術がない。runの中で vertexai.preview.log_params() を自分で呼んで残すか、評価ジョブ側で成果物と一緒に保存するか、いずれにせよ自分で持つ必要があります

一見すると厳しすぎるルールに見えますが、逆をやると詰みます。RAGの参照ドキュメントが更新された状態で新旧モデルを比較して精度が落ちていたとき、モデルが悪いのかドキュメントが変わったせいなのか、後から切り分ける方法がありません。「なんとなく精度が落ちた気がする」という報告だけが残って、原因究明に何日も溶かすことになります。比較不成立として弾き、参照ドキュメントを固定して取り直すほうが、結果的にずっと速い。

3. CIで自動的にブロックする

運用ルールとして「モデル変更時は評価を回すこと」と決めても、忙しくなれば飛ばされます。仕組みで縛ります。

  • 各ユースケースの利用モデル設定(モデルID・バージョン)はGit管理の設定ファイルに集約し、基盤経由でのみ反映する
  • モデル設定の変更PRに対してCIで評価パイプラインを実行し、判定に合格しない限りマージ/デプロイ不可にする(required check)
  • 緊急時のオーバーライドは承認付きの例外手続きとして監査ログに残す

ここでもうひとつ落とし穴があります。既存の評価結果を流用する場合、「直近の実行が通っているからOK」では済ませてはいけません。

その結果が通ったときの条件(モデルID / プロンプト版 / アプリコードSHA / データセット版 / 参照ドキュメントの版)が、PRが適用しようとしている構成と機械的に一致することをCIが検証する必要があります。そうしないと、アプリコードが変わっているのに古い結果で通過してしまう、という事故が起きます。

判定基準は「必須fail条件」と「精度閾値」の2段構え

判定の中身も整理しておきます。

必須fail条件(1件でも該当したら即NG。精度閾値より優先)

  • 構造化出力のスキーマ不正(JSON invalid / 必須フィールド欠落 / 未定義ラベルの出力)
  • function callingの呼び出し形式の破壊、後処理パーサの例外
  • 安全性フィルタによる想定外のブロック

精度閾値(ユースケースの重要度に応じてチームごとに設定)

  • 総合精度の低下が閾値以内(例: -5% 以内)
  • ケース単位の「保存済みの基準では正解 → 候補モデルで不正解」の件数

総合精度が1% しか落ちていなくても、スキーマ不正が1件でもあればアプリは落ちます。精度の話とアプリが壊れる話は別軸なので、閾値の中に混ぜずに分けています。

なお、3つ目の「安全性フィルタによる想定外のブロック」はModel Armorなどのガードレールを併用する場合に効いてきます。ガードレールの設定変更で正常な業務プロンプトが誤検知でブロックされるようになれば、それもリグレッションです。この話は別記事で扱う予定です。

ハマりどころ

最後に、検討・検証の過程で引っかかった点をまとめておきます。

judgeモデル自体のバージョンが動く

autoraterのモデルを明示的に固定しないと、評価者が勝手に変わります。「モデルは変えていないのにスコアが変わった」が起きるので、ここは押さえておくべきです。詳細は次章にまとめました。

Gemini 3系では temperature を渡さない

出力を安定させるために temperature=0 を指定したくなりますが、Gemini 3系では非推奨です。公式ガイドにこうあります。

The following sampling parameters are no longer recommended for Gemini 3 models: temperature, top_p, top_k
The model manages its own sampling for optimal results. Remove these parameters from all requests. To ensure determinism, define a system instruction with explicit rules for your specific use case.

指定してもエラーにはならないので気づきにくい箇所です。出力を安定させたいなら、サンプリングパラメータではなくsystem instructionで出力形式を厳密に指示する、構造化出力のスキーマを定義する、後処理でバリデーションする、といった方法をとります。

そのうえで、LLMの出力が完全に決定的になることは期待しないほうがいいです。閾値に余裕を持たせるか、境界付近のケースは複数回実行して判断します。1回の実行結果を絶対視しないことです。

プロンプトもバージョン管理の対象にする

モデル変更とプロンプト変更を同時に行うと切り分けが不能になります。変更は片方ずつ評価する。条件を揃えて比較するという話と同じ理屈です。

SDKの非推奨化

GenerativeModel のインスタンスを evaluate(model=) に渡すと、SDKからこの警告が出ます。上のコードではモデル名を文字列で渡しているので出ません。

vertexai.generative_models.GenerativeModel is deprecated for evaluation
and will be removed in June 2026. Please pass a string model name instead.

既存のスクリプトで GenerativeModel を渡している場合は、文字列のモデル名を渡す形に移行します。評価基盤そのものも定期的なメンテナンス対象です。

judgeを固定する — ドキュメントに書かれていない落とし穴

「judgeのバージョンを固定すべき」と書きましたが、実際にやろうとすると躓きます。ここは検証にいちばん時間を使ったので、結果をまとめておきます。

judgeの指定は AutoraterConfig で行います。

from vertexai.preview.evaluation import AutoraterConfig, EvalTask

autorater_config = AutoraterConfig(
    autorater_model=f"projects/{PROJECT}/locations/{LOCATION}/publishers/google/models/gemini-2.5-flash"
)

EvalTask(
    dataset=df,
    metrics=[my_metric],
    autorater_config=autorater_config,
).evaluate()

AutoraterConfig では他に sampling_count(judgeを何回呼ぶか。1〜32、既定4)と flip_enabled(pairwiseで候補と基準を入れ替えてバイアスを減らす)も設定できます。

落とし穴1: 短縮名は通らない

EvalTask.evaluate(model=...) では "gemini-2.5-flash" という短縮名が使えます。しかし autorater_model では通りません

autorater_model="gemini-2.5-flash"
→ 400 Field: autorater_config.autorater_model;
      Message: Invalid autorater model resource name.

フルリソースパス(projects/{p}/locations/{l}/publishers/google/models/{model})が必須です。同じSDKの中で書式が違うので、ここは引っかかりやすいところです。

落とし穴2: judgeにできるかはlocationに左右される

ここが本題です。最新のGeminiをjudgeに指定しようとすると404になりました。

autorater_model=".../locations/us-central1/publishers/google/models/gemini-3.5-flash"
→ 404 Failed to make GenerateContent request to autorater model ...
      Autorater model not found.

一見「新しいモデルはjudgeに対応していない」と読めますが、そうではありません。そもそもこの環境の us-central1 から gemini-3.5-flash が呼べないのが原因でした。judge以前に、通常の生成でも同じ404になります。

location gemini-3.5-flash gemini-2.5-flash
global OK OK
us-central1 404 OK
us-east5 404 OK

これは検証環境での結果です。公式の Gemini 3.5 Flash ガイドでは対応リージョンが Global, Multi-region, Americas, Europe, Asia Pacific とされており、global 専用ではありません。404の原因が提供リージョンなのかプロジェクト側の権限・有効化なのかは切り分けられていません。

ここで言いたいのは「3.5系はglobal専用」ということではなく、judgeに指定したモデルがそのlocationから呼べないと404になり、エラー文からは原因が判別できないという点です。

そして重要なのは、効くのが autorater_model のパスに書くリージョンではなく vertexai.init() のlocationだという点です。

vertexai.init(location=) autorater_modelのパス gemini-3.5-flash
us-central1 .../locations/global/... 失敗
us-central1 .../locations/us-central1/... 失敗
global .../locations/global/... OK
global .../locations/us-central1/... OK

パスに global と書いても、初期化が us-central1 なら失敗します。逆に global で初期化すれば、パス側のリージョン表記は影響しません。

vertexai.init(project=PROJECT, location="global")   # ← ここが効く

autorater_config = AutoraterConfig(
    autorater_model=f"projects/{PROJECT}/locations/global/publishers/google/models/gemini-3.5-flash"
)

設計への影響

global は処理リージョンをGoogle側に委ねるエンドポイントです。新しいモデルが先に来る一方で、データレジデンシー要件がある案件では使えません

さらに前半で触れたとおり、global ではExperimentsへの記録もできませんMetadataStore is not supported in region global)。

location の選択で、次の3つが連動して決まります。

リージョン指定(us-central1 等) global
モデルの可用性 そのリージョンで提供されているものに限る 新しいモデルが先に来やすい
データレジデンシー 満たせる 満たせない
Experimentsへの記録 残る 残らない

Experimentsが使えないのは仕様として確定しています(MetadataStore is not supported in region global)。モデルの可用性はリージョンと時期によって変わるので、使いたいモデルが対象リージョンで提供されているかは都度確認が必要です。

実務上の判断はこうなります。

  • データレジデンシー要件がある → リージョン固定。使えるモデルがそのリージョンの提供状況に縛られる
  • とにかく最新モデルを使いたいglobal も選択肢。ただし実行履歴が残らないので、評価結果の保存を自前で実装する必要がある

評価基盤のリージョンを決めるとき、この3点を後から知ると作り直しになります。モデル選定、データレジデンシー要件、実行履歴の保存方針はセットで検討しておくべきです。

judgeをGemini以外にできるか

マネージドな範囲では、Gemini以外が公式に認められているのは翻訳評価用のMetricXとCOMETだけです(いずれもプレビュー)。

judge 用途 スコア
Gemini 汎用のテキスト評価。既定 指標による
チューニング済みGemini 独自基準での評価 指標による
MetricX 翻訳品質(Google開発、エラーベース) 0〜25、低いほど良い
COMET 翻訳品質(参照ベースの回帰) 0〜1、1が完璧

MetricXとCOMETはポイントワイズ専用で、NMT・TranslationLLM・Geminiのいずれの出力でも評価できます。

そしてペアワイズにはGemini限定の明示的な制限があります

Pairwise metrics are only supported with Gemini as a judge model.

つまり2つのモデルを直接比較させる評価は、judgeをGemini以外にできません。

autorater_model にClaudeを指定できるかは未確認です。 書式は publishers/*/models/* とワイルドカードなので publishers/anthropic/models/claude-opus-5 も受理され、サーバはそのモデルへの GenerateContent リクエストを試みます。しかし検証環境ではAnthropicのモデルが有効化されておらず、judge以前に通常の生成でも404だったため、拒否されたのかモデルが見えていないだけなのか切り分けられませんでした。

逃げ道: CustomMetric で自前のjudgeを使う

ここまではサービスが管理するjudgeの話です。どうしても別のモデルで採点したい場合は、CustomMetric という逃げ道があります。

If you need to further customize your metrics, like choosing a different judge model for model-based metrics, or define a new computation-based metric, you can use the CustomMetric class in the Agent Platform SDK.

公式に「Bring your own judge model using Custom Metric」というノートブックが用意されています。採点ロジックを自分で書くことになるので、AutoraterConfig で得られるマネージドな利点(人間の評価者によるキャリブレーション、Chain-of-Thoughtの説明)は自前で面倒を見る必要がありますが、judgeのモデル選択そのものは塞がれていません。

整理するとこうです。

judgeに使えるモデル
組み込み指標 + AutoraterConfig Gemini(翻訳用途のみMetricX / COMET)
ペアワイズ比較 Geminiのみ(明示的な制限)
CustomMetric で自前実装 任意のモデル

なおコンソールにjudgeを差し替えるUIはありません。前半で見た「評価候補の追加」のモデル選択は評価される側であり、「指標を追加」の設定画面も適応型・静的のどちらを選んでも判定モデルの選択欄は出てきません。judgeを固定したいならSDK一択です。

まとめ

  • モデルの提供終了は不可避なので、移行時の品質担保を仕組みにしておく
  • 評価方式はjudgeがいるかどうかの2つ。分類系をComputation-based中心に設計すれば、judgeの推論費用を避けられる(ただし評価API自体の文字数課金はある)
  • 総合精度だけでなくケース単位のリグレッションを見る
  • 旧モデルが消えても判定できるよう評価結果を保存し、条件が一致するときだけ比較する。Experimentsが自動で残すのはモデル名程度なので自前で持つ
  • CIのrequired checkで自動的にブロックする。古い結果を流用したすり抜けを防ぐ
  • judgeのモデルを固定するautorater_model はフルリソースパス必須で、短縮名は400で弾かれる。マネージドなjudgeはGemini中心(ペアワイズはGemini限定)だが、CustomMetric を使えば任意のモデルにできる
  • Gemini 3系では temperature を渡さない。非推奨なのでsystem instructionで出力を縛る
  • location の選択が3つのことを同時に決める。使えるモデル / データレジデンシー / Experimentsに履歴が残るか。global では履歴が残らないので、評価結果の保存を自前で持つ必要がある

「動かしてみた」だけなら数十行で終わりますが、基盤の品質チェックとして信頼できるものにするには、比較の妥当性をどう担保するかの設計が要ります。同じような仕組みを検討している方の参考になれば幸いです。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?