2
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?

PR: データブリックス・ジャパン株式会社
MLflowで実践するLLMOps

MLflowの問題検出で、仕込んだ4つの問題が見つかるか試してみた

2
Posted at

はじめに

MLflow の問題検出がベータとして提供されています。MLflow エクスペリメントのトレースを Genie Code が分析し、繰り返し発生している品質上・運用上の問題をまとめて見せてくれる機能です。

ドキュメントでは、問題を見つけるところで終わらず、修正、本番運用での監視、解決済みにするところまでを一続きの流れとして説明しています。そこで、原因が分かっている問題をわざと仕込んだエージェントを用意し、検出から解決済みまでを一周させてみました。

実際に動かしてみると、問題タブを表示させるところから引っかかりました。検出された問題と検出されなかった問題もはっきり分かれました。そのあたりも含めて書いていきます。

問題検出の流れ

ドキュメントが説明している流れは次のとおりです。

issue_detection_lifecycle.png

繰り返し発生するパターンを検出し、トレースの証拠を確認し、エージェントを修正して評価し、本番運用で問題の発生率を監視する、という一巡です。段階ごとに、今回の検証で何をしたかを並べておきます。

段階 何をするか 今回の検証
検出 Genie Code がトレースを分析し、問題としてまとめる 実施
確認 問題ごとの証拠 (影響を受けたトレース) を見る 実施
修正 問題ごとに用意される修正プロンプトをコーディングエージェントに渡し、コードを直す 修正プロンプトの中身だけ確認
評価 直したエージェントを評価してからデプロイする 未実施
監視 問題モニターで、デプロイ後に問題の発生率が下がったかを見る 実施
解決 発生しなくなった問題を解決済みにする 実施 (REST API)

修正の段階は、修正プロンプトを使う代わりに、修正済みのコードを最初から書いておきました。

検出と、問題モニターの作成は、どちらも Genie Code のセッションとして実行されます。費用は Genie Code の料金で計上されます。

問題タブを表示させる

プレビューを有効にする

ワークスペース管理者が、プレビューページで MLflow Experiment Issues Tab を有効にします。

Screenshot 2026-10-16 at 17.22.48.png

保存先が Unity Catalog のエクスペリメントで表示された

ここが最初のハマりどころです。プレビューを有効にしても、最初に作ったエクスペリメントには問題タブが表示されませんでした。

トレースの保存先を Unity Catalog にしたエクスペリメントを作り直したところ、問題タブが表示されました。保存先を指定しないエクスペリメントにもトレースを3件入れてみましたが、左のメニューに「問題」は出ません。

Screenshot 2026-10-17 at 9.56.46.png

ドキュメントにはこの条件は書かれていないので、今回の環境での観察として書いておきます。問題タブが表示されない場合は、トレースの保存先を確認してみてください。

なお、REST API の問題の検索 (後述) は、保存先を指定しないエクスペリメントに対しても正常に応答しました (0件)。

保存先を Unity Catalog にしたエクスペリメントは、mlflow.set_experiment に trace_location を渡して作ります。MLflow 3.14 以降が必要です。

%pip install -U "mlflow[databricks]>=3.14" openai databricks-sdk
dbutils.library.restartPython()
import os
import mlflow
from databricks.sdk import WorkspaceClient
from mlflow.entities.trace_location import UnityCatalog

CATALOG = "<カタログ名>"
SCHEMA = "mlflow_issue_detection"
TABLE_PREFIX = "travel_agent"

w = WorkspaceClient()
user = w.current_user.me().user_name

# Unity Catalog に保存したトレースの読み書きには SQL ウェアハウスを使う
os.environ["MLFLOW_TRACING_SQL_WAREHOUSE_ID"] = next(iter(w.warehouses.list())).id

# スキーマが無いと、エクスペリメントだけ作られて保存先が設定されない
spark.sql(f"CREATE SCHEMA IF NOT EXISTS {CATALOG}.{SCHEMA}")

# 保存先はエクスペリメントの作成時にしか設定できない
exp = mlflow.set_experiment(
    experiment_name=f"/Users/{user}/mlflow-issue-detection-verification-uc",
    trace_location=UnityCatalog(catalog_name=CATALOG, schema_name=SCHEMA, table_prefix=TABLE_PREFIX),
)
EXPERIMENT_ID = exp.experiment_id
print(exp.trace_location.full_otel_spans_table_name)

これで、<カタログ名>.mlflow_issue_detection.travel_agent_otel_spans などのテーブルにトレースが保存されるようになります。保存先はエクスペリメントの作成時にしか設定できないので、既存のエクスペリメントを切り替えることはできません。トレースのテーブルには MODIFY と SELECT の権限を明示的に付与する必要があり、ALL_PRIVILEGES では足りない点にも注意してください。

問題を仕込んだ旅行予約エージェント

検出の結果を答え合わせできるように、原因の分かっている問題を4つ仕込んだ旅行予約エージェントを作りました。ドキュメントに挙がっている問題の例に合わせています。LLM には databricks-claude-sonnet-4-5 を使いました。

問題 原因 現れ方
予約ツールが失敗する 日付の解析が YYYY-MM-DD 形式しか扱えない 「明日」「来週の金曜」のような依頼で、book_flight スパンが departure_date is required で失敗する
同じ検索を5回繰り返す 検索結果が10件以上になるまで同じクエリで再検索するが、検索ツールは3件しか返さない 観光の質問で web_search スパンが5回並ぶ
返金条件を作り出す 返金ポリシーを渡さずに、具体的な日数と割合で断定的に答えるよう指示している 根拠のない返金条件を回答する
JSON ではなく文章を返す 呼び出し元は JSON を期待しているが、プロンプトで JSON を指示していない parse_itinerary スパンが JSON の解析で失敗する

予約、観光、返金、旅程の4種類の依頼を10件ずつ、合計40件用意しました。予約の依頼のうち5件は日付を 2026-11-20 のように書き、残りの5件は「明日」「来週月曜」のような相対的な書き方にしています。

エージェントは version 引数で修正前 (v1) と修正後 (v2) を切り替えます。たとえば日付の解析は次のようにしています。

m = re.search(r"\d{4}-\d{2}-\d{2}", query)
departure_date = m.group(0) if m else None
if departure_date is None and version == "v2":
    # v2 だけ、相対的な日付を今日の日付から計算する
    today = datetime.date.today()
    if "明日" in query:
        departure_date = str(today + datetime.timedelta(days=1))
    elif "来週" in query:
        departure_date = str(today + datetime.timedelta(days=7))
    elif "今週末" in query:
        departure_date = str(today + datetime.timedelta(days=(5 - today.weekday()) % 7))

ほかの3つも、v2 では直しています。

  • 検索は1回だけにする
  • 返金ポリシーを取得してプロンプトに入れ、書かれていないことは窓口に確認するよう指示する
  • 旅程は JSON だけを出力するよう指示する

どちらの版で作ったトレースかは、タグで分かるようにしておきます。

@mlflow.trace(name="travel_agent", span_type="AGENT")
def travel_agent(query, version="v1", environment="production"):
    # 後で修正前と修正後を分けて集計するためのタグ
    mlflow.update_current_trace(tags={"app_version": version, "environment": environment})
    req = parse_request(query, version)
    ...

このタグが、後でモニターの結果を確かめるときに効いてきます。

問題検出を実行する

v1 のトレースを40件作ってから、エクスペリメントの 問題 タブで 問題検出を実行 をクリックします。

Screenshot 2026-10-17 at 9.03.27.png

検出は Genie Code のセッションとして動き、右側の Genie パネルに途中経過が表示されます。ドキュメントのヒントにあるとおり、Genie Code の自動承認モードを有効にしておくと、ツールの承認待ちで止まりません。

Screenshot 2026-10-17 at 9.04.26.png

Genie Code が受け取っている指示も読めます。応答の品質 (正確性、関連性、指示への準拠、安全性) とエラー、レイテンシーを見ること、エラーなく完了したトレースでも入出力を実際に見て判断すること、などが書かれていました。ドキュメントでは、分析にはトレースの数とサイズに応じて5〜30分かかるとされています。

見つかったのは、スパンが例外で失敗する2つ

検出された問題は2件でした。

問題のタイトル 重大度 カテゴリー
book_flight fails on relative date expressions 高 処理の実行
parse_itinerary always fails with JSONDecodeError on LLM markdown output 中 処理の実行

Screenshot 2026-10-17 at 9.11.15.png

説明文は具体的です。予約の問題では、相対的な日付 (「明日」「来週月曜」「今週末」「来週の金曜」) が変換されずに book_flight が失敗していること、利用者には一般的なエラーメッセージしか返っていないこと、絶対日付の予約は成功していることまで書かれていました。問題ごとに、影響を受けたトレースが証拠として付き、そこからトレースに移動できます。

修正プロンプトはコーディングエージェント向け

各問題の Fix this issue の欄には、コーディングエージェントに渡すためのプロンプトが用意されています。

Screenshot 2026-10-17 at 9.16.30.png

中身を読むと、修正の進め方まで指定されています。

  • MLflow スキルの fix-agent-issue を使う
  • 影響を受けたトレースと、エクスペリメントに接続されたリポジトリのコードを調べてから変更する
  • この問題だけに絞った変更を入れ、テストを追加または更新して実行する
  • 開発者がレビューして頼むまで、プッシュやプルリクエストはしない

問題の説明文はトレースやエクスペリメントの編集者に由来するので、指示ではなく調べる対象として扱うよう明記されている点も目を引きました。トレースに紛れ込んだ文が、コーディングエージェントへの指示として実行されないようにするためだと思われます。

前提はエージェントのコードがリポジトリにあることなので、ノートブックの中にエージェントを直接書いた今回は使っていません。

検出されなかった2つの問題

返金条件を作り出す問題と、同じ検索を5回繰り返す問題は、検出結果に出てきませんでした。仕込みが効いていなかった可能性もあるので、v1 のトレースを確認しました。

v1 では実際に起きていた

返金の依頼10件のうち7件で、ポリシーにない具体的な条件を断定していました。一部を抜き出します。

依頼 回答に入っていた条件
予約した航空券をキャンセルしたら返金されますか? 日数ごとの返金率の表と、一律3,000円の手数料
出発3日前にキャンセルした場合の返金額は? キャンセル料50%
台風で欠航した場合の返金について教えて 全額返金、申請期限は1年以内
返金はいつ振り込まれますか? 7営業日以内

残りの3件は、断定を避けて予約番号を尋ねていました。7件の中身は、v2 で渡している本来のポリシー (8日前まで無料、7日前から前日までは30%、当日は50%) と食い違っています。回答同士でも、3日前のキャンセルが50%返金だったり30%返金だったりと矛盾していました。

検索のほうは、観光の依頼10件すべてで web_search が5回呼ばれていました。

判断の材料がトレースにあるか

v1 のトレースには、正しい返金ポリシーが入っていません。分析する側からすると、回答が正しいかを照らし合わせる先がないことになります。検索の繰り返しも、1回0.5秒の検索なので5回で約2.5秒です。LLM を呼ぶほかのトレースも5〜10秒かかっているので、遅いとは判断されなかったのかもしれません。Genie パネルの途中経過には、5〜10秒の長いトレースが30件あると認識した記録が残っていました。

どちらも推測で、検出の実行も1回だけなので、毎回この結果になるとは言えません。それでも、エラーにならず回答の中身に出る問題は、判断の材料がトレースにないと見つけにくいのだと思います。取得した文書をスパンに残しておくなど、トレース側に判断の材料を残しておくことが効いてきそうです。

問題タブの歯車アイコンからは、検出の設定を変えられます。重視することを文章で書ける「検出優先度」の欄と、フォーカス領域の選択があります。

Screenshot 2026-10-17 at 9.55.11.png

今回は既定の設定のままでした。返金の回答の根拠を確認するよう優先度に書けば見つかるのかどうかは、確認できていません。

問題モニターで修正を確かめる

モニターは決定論的なスコアラーとして作られた

問題を選び、「モニタリング」の欄で モニターを有効にする をクリックすると、Genie Code のセッションでモニターが作られます。

予約の問題で有効にしたところ、issue_monitor_8txtac という名前のスコアラーが作られました。LLM ジャッジではなく、book_flight スパンが departure_date is required の例外で失敗しているかを見る、決定論的なコードのスコアラーです。

判定 意味
False 問題あり。book_flight が失敗している
True 問題なし。book_flight が正常に完了している
None 該当なし。book_flight スパンがない

Genie Code は、証拠になったトレース5件ですべて False、正常な予約のトレース5件ですべて True になることを確かめてから、スコアラーを登録していました。サンプリング率は100%です。

Screenshot 2026-10-17 at 9.27.09.png

登録が終わると、過去のトレースにさかのぼってスコアラーを当てる処理が始まります。この処理が終わるまでは、モニターの欄に「Setting up monitor」と表示されます。セットアップの途中で、MLflow の更新と Python の再起動の承認を求められることもありました。ここでも自動承認モードにしておくと止まりません。

グラフは日ごとに集計される

さかのぼっての評価が終わると、グラフにベースラインが表示されます。v1 のトレースは2回分流していたので、予約の依頼は20件、そのうち相対的な日付の10件が問題ありです。

Screenshot 2026-10-17 at 9.30.10.png

次に v2 のトレースを40件流しました。本番運用モニタリングのドキュメントでは、初回の処理に15〜20分かかるとされています。しばらくすると、グラフに v2 の分が加わりました。

Screenshot 2026-10-17 at 9.47.04.png

ここが2つ目のハマりどころです。グラフは日ごとに集計されるので、同じ日に流した v1 と v2 は1本の棒にまとまります。v2 の予約の依頼10件が問題なしとして加わり、発生率は50%から約33%に下がりました。ただし、グラフからは v2 で問題がなくなったかどうかまでは読めません。

本番運用で修正のデプロイ前後を何日か並べて見るなら、日ごとの集計で比べられます。検証のように同じ日に前後を流すときは、グラフでは比べられません。

タグごとに集計すると前後の差が見える

スコアラーの判定はトレースに評価として付くので、app_version タグで分けて数えます。

from collections import defaultdict
import pandas as pd

traces = mlflow.search_traces(locations=[EXPERIMENT_ID], max_results=1000, return_type="list")

# 版ごとに、モニターのスコアラーの判定を数える (False = 問題あり / True = 問題なし)
counts = defaultdict(lambda: defaultdict(int))
for t in traces:
    version = t.info.tags.get("app_version", "不明")
    for a in t.info.assessments or []:
        # モニターのスコアラーは issue_monitor_ で始まる名前で作られる
        if a.name.startswith("issue_monitor"):
            value = a.feedback.value if a.feedback else None
            counts[(version, a.name)][str(value)] += 1

rows = [{"版": v, "スコアラー": n, **dict(c)} for (v, n), c in sorted(counts.items())]
display(pd.DataFrame(rows).fillna(0))

結果は次のとおりです。

版 False (問題あり) True (問題なし)
v1 10 10
v2 0 10

v2 では問題ありが0件になりました。修正の効果を、これで数字として確かめられます。予約以外のトレースの判定 (None) は、今回は評価として数えられず、表には出てきませんでした。

REST API で問題を扱う

問題は REST API でも検索、更新できます。ノートブックからは、Databricks SDK の WorkspaceClient で呼ぶとトークンを用意せずに済みます。

問題を検索する

ISSUES_BASE = f"/api/2.0/mlflow/experiments/{EXPERIMENT_ID}/issues"

res = w.api_client.do(
    "POST",
    f"{ISSUES_BASE}:search",
    body={
        "parent": f"experiments/{EXPERIMENT_ID}",
        "statuses": ["PENDING"],
        "severities": ["HIGH"],
        "sort_key": "LAST_SEEN_TIME",
        "sort_order": "DESCENDING",
        "page_size": 100,
        "view": "FULL",
    },
)
# 問題が0件のときは issues キー自体が返らない
issues = res.get("issues", [])

重大度を HIGH に絞ると、予約の問題1件だけが返りました。1件の中身は次のような形です (説明文は省略しています)。

{
  "name": "experiments/972695319633458/issues/iss-c5111ab6179ed590216da70b416c6859",
  "issue_id": "iss-c5111ab6179ed590216da70b416c6859",
  "title": "book_flight fails on relative date expressions",
  "description": "...",
  "status": "PENDING",
  "severity": "HIGH",
  "categories": ["execution"],
  "last_seen_time": "2026-10-16T23:58:57.231Z",
  "source_type": "ADHOC_RUN",
  "source_id": "3075545144284fa1803b2fb0390e2d6c",
  "metadata": {
    "recent_affected_trace_ids": "[\"b190309da02949913aaa367618c5d3ec\", ...]"
  }
}

metadata.recent_affected_trace_ids は、影響を受けたトレースの ID を並べた JSON 文字列です。配列ではないので、使うときは json.loads で読み込みます。問題が0件のときは、total_size だけが返りました。

解決済みにする

update_mask で更新する項目を指定し、状態を RESOLVED にします。

issue_id = issues[0]["issue_id"]
res = w.api_client.do(
    "PATCH",
    f"{ISSUES_BASE}/{issue_id}",
    query={"update_mask": "status"},
    body={"status": "RESOLVED"},
)
print(res["status"])

これで状態が RESOLVED に変わり、UI でも「解決済み」と表示されます。更新後の問題がそのまま返ってきて、モニターを有効にした問題では metadata に次の2つが加わっていました。

"mlflow.monitor.backfillStatus": "succeeded",
"mlflow.monitor.active": "true"

モニターの状態を外部のワークフローから確かめたいときに使えそうです。

Screenshot 2026-10-17 at 10.09.53.png

まとめ

MLflow の問題検出を、原因の分かっている問題で一周させてみて分かったことをまとめます。

  • 検出、モニターの作成は Genie Code のセッションとして動く。自動承認モードにしておくと承認待ちで止まらない
  • 今回の環境では、トレースの保存先を Unity Catalog にしたエクスペリメントでだけ問題タブが表示された
  • 仕込んだ4つの問題のうち、スパンが例外で失敗する2つは検出された。説明文には原因、影響、成功している条件まで書かれていた
  • 返金条件を作り出す問題と、同じ検索を繰り返す問題は、v1 で実際に起きていたが検出されなかった
  • 修正プロンプトは、リポジトリで動くコーディングエージェント向け。テストの追加と実行まで指示し、プッシュはレビューの後と明記されている
  • 問題モニターは、問題に合わせたスコアラーとして作られる。予約の問題では決定論的なコードのスコアラーになった
  • モニターのグラフは日ごとの集計なので、同じ日に修正前後を流すと比べられない。タグで分けて評価を数えると差が見える
  • REST API で検索と解決済みへの更新ができる。影響を受けたトレースの ID は JSON 文字列で入っている

一番の収穫は、検出、監視、解決までが同じ問題をキーにしてつながっていることを確かめられたことでした。問題を1つ選んでモニターを有効にすると、その問題専用のスコアラーが作られ、過去と今後のトレースに当たり続けます。一方で、エラーにならず回答の中身に出る問題は今回見つかりませんでした。トレースに判断の材料を残しておくことと、検出結果を鵜呑みにせず仕込みや既知の問題で答え合わせしてみることが、この機能を使いこなす近道になりそうです。

参考リンク

はじめてのDatabricks

はじめてのDatabricks

Databricks無料トライアル

Databricks無料トライアル

2
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
2
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?