はじめに
Databricksユーザーコミュニティ JEDAI のイベント「『作って終わり』を卒業する──著者と学ぶMLflow LLMOpsもくもく会」を開催します。拙共著『MLflowで実践するLLMOps──生成AIアプリケーションの実験管理と品質保証』 (技術評論社) をフィーチャーし、Fireside Chat (10分)、座学 (20分)、もくもくハンズオン (60分+発展編) の構成で、Databricks Free EditionだけでLLMOpsのコアループを1周する会です。
本記事では、当日使用する座学スライドとハンズオンノートブックの内容を先行して解説します。参加される方の予習にも、当日参加できない方の自宅もくもくにも使える構成です。
座学スライドはこちらです。
ハンズオンノートブックはこちらのリポジトリで公開しています。Databricks Free Edition (無料・クレジットカード不要) にインポートすれば、どなたでも自分のペースで1周できます。
なぜLLMOpsか: デモは動く。本番で壊れる
LLMアプリを作ったことがある方なら、こんな経験はないでしょうか。
- プロンプトを直すのが怖い: 1行変えたら何が変わるのか分からない
- 品質を測る物差しがない: レビューが「なんとなく良さそう」の雰囲気頼み
- リリース後の劣化に気づけない: ユーザー体験が静かに壊れていく
これらはすべて「作って終わり」の症状です。解決するのが、開発 → プロンプト管理 → 評価 → デプロイ → 運用・モニタリング → (評価に戻る) というLLMOpsのコアループであり、その道具がMLflowの4本柱 (Tracing / Evaluate & Monitor / Prompt Registry / AI Gateway) です。拙共著はこの4本柱を骨格にしています。
今日の題材は、架空のベーカリーチェーンの日本語顧客レビューから、感情・トピック・改善示唆をJSONで抽出する「レビュー分析エージェント」です。
{
"sentiment": "positive | negative | neutral",
"topics": ["product_quality", "service", "price", "atmosphere"],
"actionable_feedback": {
"has_feedback": true,
"summary": "改善示唆の要約"
}
}
Step 0: セットアップとエクスペリメント
カタログ・スキーマの作成に加えて、2つの仕込みをしています。
1つ目はLLMバックエンドの変数化です。エージェントが使うクライアントとモデルを差し替え可能な変数として持っておくことで、発展編でエージェントのコードを一切変えずにゲートウェイを挟めるようになります。
LLM_CLIENT = client # まずはFoundation Model APIsを直接
LLM_MODEL = MODEL_ENDPOINT
2つ目はエクスペリメントのUCトレース永続化です。トレースの保存先をUnity Catalogに指定すると、トレースの実体がDeltaテーブルになり、エクスペリメントの「概要」タブの集計表示が機能し、SQL・Genie・AI/BIダッシュボードからトレースを直接分析できます。
from mlflow.entities.trace_location import UnityCatalog
experiment = mlflow.set_experiment(
experiment_name=EXPERIMENT_NAME,
trace_location=UnityCatalog(
catalog_name=CATALOG, schema_name=SCHEMA,
table_prefix="llmops_traces",
),
)
なお、Free Editionのworkspaceカタログはデフォルトストレージのため、現時点でUCトレース保存には未対応です (Unsupported table kindエラー)。ノートブックではtry/exceptで通常のエクスペリメント保存にフォールバックしています。外部ストレージを裏付けとするカタログを持つ通常のワークスペースでは、このコードがそのまま有効になります。本番ではUC永続化が定石です。詳細はUnity CatalogにMLflowトレースを保存するを参照してください。
Step 1: 動くものに最初からトレースを仕込む
最初にやるべきは「動くものを作る」ことではなく「動くものに最初からトレースを仕込む」ことです。@mlflow.traceデコレータを付けるだけで、入出力・レイテンシがすべて記録されます。mlflow.openai.autolog()を併用すると、LLM呼び出しのスパンにトークン使用量も自動記録されます。
@mlflow.trace
def extract_insights(review: str, prompt_template: str = PROMPT_V1) -> dict:
prompt = prompt_template.replace("{{review}}", review)
response = LLM_CLIENT.chat.completions.create(
model=LLM_MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
response_format={"type": "json_object"},
)
return json.loads(response.choices[0].message.content)
Step 2: プロンプトをコードから引き剥がす
プロンプトをコードに直書きすると、改善のたびにコード変更と再デプロイが必要になります。MLflow Prompt RegistryでUnity Catalogに登録すれば、プロンプトはコードと独立した資産になります。
prompt_v1 = mlflow.genai.register_prompt(
name=PROMPT_NAME, template=PROMPT_V1,
commit_message="v1: ベースライン (指示のみ)",
)
mlflow.genai.set_prompt_alias(name=PROMPT_NAME, alias="production", version=prompt_v1.version)
few-shot例を追加したv2も登録し、production / stagingのエイリアスで管理します。呼び出し側はエイリアス経由で取得するので、エイリアスの付け替えだけで本番のプロンプトが切り替わります。
もう1つ大事な仕掛けとして、@mlflow.traceの付いた関数の中でload_promptを呼ぶと、使用したプロンプトのバージョンがトレースに自動リンクされます。「この応答はどのプロンプトのどのバージョンで生成されたのか」がトレースから辿れるようになります。
なお、登録したプロンプトはカタログエクスプローラーからは参照できません。エクスペリメント画面の「プロンプト」タブでスキーマを選択して確認します。
Step 3: 評価ループ (今日の心臓部)
ここでは3つのことをやります。
- ゴールデンデータ8件をUnity Catalog管理の評価データセットとして登録する
-
make_judgeで3観点のLLMジャッジを構築する -
mlflow.genai.evaluateでv1とv2を比較評価する
ゴールデンデータの後半4件には「味は良いのに待ち時間が長すぎる」「値段相応」のような複合感情・文脈依存の微妙なケースを意図的に入れています。判定が明白なレビューばかりだと、プロンプトの差が見えないためです。
eval_dataset = mlflow.genai.datasets.create_dataset(uc_table_name=DATASET_TABLE)
eval_dataset.merge_records(golden_records)
ジャッジはmake_judgeで構築します。感情ラベルの正解一致 (sentiment_correctness)、スキーマ準拠 (schema_compliance)、トピック妥当性 (topics_relevance) の3観点です。
sentiment_judge = make_judge(
name="sentiment_correctness",
instructions=(
"あなたは評価者です。{{ inputs }} のレビューに対するエージェントの {{ outputs }} と、"
"{{ expectations }} に含まれる expected_sentiment を比較してください。"
"一致していれば 'pass'、そうでなければ 'fail' を返してください。"
),
model=JUDGE_MODEL,
feedback_value_type=Literal["pass", "fail"],
)
v1とv2をそれぞれ評価し、エクスペリメントの「評価ラン」で両方にチェックを入れると、同じレビュー入力に対するレスポンスとジャッジ判定が並べて表示されます。
few-shotを足したのだからv2は改善のはず——本当にそうかは自分の評価結果で確かめてください。参考までに、上のスクリーンショットの実行では、判定が割れたのは複合感情のレビュー1件だけでした。なお、LLMの出力もLLMジャッジの判定も完全には決定的ではないため、評価結果は実行のたびに多少ぶれます。特に件数が少ないと1件の差が割合として大きく見えます(8件なら1件で12.5%)。ぶれが気になる場合は、ゴールデンデータの件数を増やす、評価を複数回実行する、ジャッジを人間の判断にアラインさせる(チャレンジ3)といった対処があります。差がほとんど出なければ、それも「few-shot 2件の効果はこの程度」という立派な計測結果です。大事なのは結果の向きではなく、どちらに転んだかを昇格前に事実として知れること。「改善したはず」を無検証で本番に出さずに済むのが評価ループの価値です。
Step 4: デプロイと運用メトリクス
デプロイ手段はSQLからFoundation Modelsを直接呼べるai_query関数です。ポイントは、SQLに埋め込むプロンプトもPrompt Registryのproductionエイリアスから取得すること。ノートブック側とバッチ側のプロンプトが常に一致します。
prod_template = mlflow.genai.load_prompt(f"prompts:/{PROMPT_NAME}@production").template
prefix, suffix = prod_template.split("{{review}}")
insights_df = spark.sql(
f"""
SELECT review_id, review,
ai_query('{MODEL_ENDPOINT}', CONCAT(:prefix, review, :suffix)) AS insight
FROM {REVIEW_TABLE}
""",
args={"prefix": prefix, "suffix": suffix},
)
ここで運用のリアルなハマりどころに遭遇します。ノートブック実行ではresponse_formatでJSON出力を強制していましたが、ai_query経由ではこの指定がないため、モデルはコードフェンスや前置き文を付けて返すことがあります。そのままfrom_jsonに渡すとパースが全滅し、集計結果がすべてnullになります。
そこで「最初の{から最後の}までを抜き出してからパースする」防御的な処理を挟み、さらにパース成功率そのものを運用メトリクスとして監視します。この数値の劣化は「プロンプトかモデルの挙動が変わった」シグナルであり、評価ループに戻るトリガーになります。
parsed_df = (
spark.table(f"{CATALOG}.{SCHEMA}.review_insights")
.withColumn("insight_json", F.regexp_extract("insight", r"(?s)\{.*\}", 0))
.withColumn("i", F.from_json("insight_json", INSIGHT_SCHEMA))
)
一次対策はソース側の構造化出力 (ai_queryのresponseFormat => '{"type": "json_object"}') です。ハンズオンでは体感のためあえて素の出力を受けていますが、本番ではresponseFormat + 防御的パース + 成功率監視の多層防御が定石です。
Step 5: 本番モニタリングでループを閉じる
スコアラーには2つの使い方があります。
| オフライン評価 (Step 3) | オンラインモニタリング (Step 5) | |
|---|---|---|
| 実行方法 | mlflow.genai.evaluate(data=..., scorers=...) |
.register()して.start()
|
| 実行タイミング | 開発者が任意のタイミングで | バックグラウンドジョブが約15分ごとに自動で |
| 評価対象 | ゴールデンデータ (期待値あり) | エクスペリメントに新しく届いたトレースのサンプル (期待値なし) |
| 使えるスコアラー | 期待値と比較するジャッジもOK | 参照不要 (reference-free) なもののみ |
from mlflow.genai.scorers import Safety, ScorerSamplingConfig
safety_monitor = Safety().register(name="safety_monitor")
safety_monitor = safety_monitor.start(
sampling_config=ScorerSamplingConfig(sample_rate=1.0)
)
オンライン側が紐づくのはサービングエンドポイントではなくエクスペリメントです。本番のアプリはエンドポイント、Databricks Apps、外部ホストなど実行場所が様々ですが、いずれも「トレースをエクスペリメントに送る」点で合流するため、監視をエクスペリメントに紐づければ発生源を問わずカバーできる設計です。実運用では開発・評価用と本番用でエクスペリメントを分け、本番専用エクスペリメントを監視するのが定石です (ハンズオンでは簡略化して同居させています)。
モニタリングジョブの初回実行は15〜30分後です。しばらくしてからTracesタブを開くと、各トレースにsafety_monitorのフィードバックが自動付与されています。
なお、Step 3のsentiment_judgeは正解との比較が必要なためオンラインでは動かせません。オンラインでは参照不要な代理指標で監視し、怪しいトレースに人間がフィードバックを付与してゴールデンデータへ昇格させ、オフライン評価で正確性を担保する——この還流もループの一部です。詳細は本番運用の品質モニタリングを参照してください。
発展編A: AIゲートウェイとガードレール
ここまでエージェントはFoundation Model APIsを直接呼んでいました。ゲートウェイを挟む場所には2つの境界があります。
| 境界 | 守るもの | Databricksでの実現 |
|---|---|---|
| 1. 利用者 → エージェント | エージェントを製品として守る: 呼び出し権限、入出力ガードレール、利用者ごとのレート制限 | エージェントをエンドポイントとしてデプロイし、そこにゲートウェイを構成 |
| 2. エージェント → LLM | 組織のLLM利用を統制する: 使えるモデルの管理、PII保護、コスト・フォールバック | モデルサービス |
エージェントがノートブック内のPython関数である現時点では、存在する唯一のネットワーク境界であるエージェントのLLM呼び出しの前段 (境界2) にゲートウェイを挟みます。境界1はエージェントをデプロイして初めて生まれるので、チャレンジ課題としています。
Unity AI Gateway (Beta) では、LLMへのアクセス口をモデルサービスというUnity Catalog上のオブジェクトとして管理します。「モデル」タブに並んでいるDatabricksホストモデル群も、system.aiに登録されたシステム提供のモデルサービスです。
「+ Model」からモデルサービスreview_llmを作成し (プライマリモデルはノートブックで使ってきたdatabricks-meta-llama-3-3-70b-instruct)、そこにガードレールをサービスポリシーとしてアタッチします。
モデルサービス画面でポリシーのセットアップをクリックします。
ガードレールタイプは「PIIブロッキング」、フェーズは入力・出力の両方です。
ここでFree Edition固有の重要な注意点があります。詳細オプションの評価者モデルサービスを、Free Editionで利用可能なもの (例: system.ai.meta-llama-3-1-8b-instruct) に変更してください。デフォルトのままだと評価が失敗し、ガードレールはフェイルクローズ設計のためすべてのリクエストがブロックされます。
準備ができたら、Step 0で仕込んだバックエンド変数を差し替えます。エージェントのコードには一切手を入れません。
LLM_CLIENT = OpenAI(
api_key=_token,
base_url=f"https://{WORKSPACE_URL}/ai-gateway/mlflow/v1",
)
LLM_MODEL = MODEL_SERVICE # workspace.llmops_mokumoku.review_llm
# Step 1〜3で使ってきたエージェントを、そのまま呼ぶ
extract_with_alias(sample_review)
Prompt Registryのproductionプロンプトも、MLflow Tracingもそのまま生きた状態で、すべてのLLM呼び出しだけがゲートウェイを通るようになります。個人情報を含むレビューを流すと、モデルに届く前に拒否されます。
[ブロック] ガードレールによりリクエストが拒否されました
Error code: 400 - {'error_code': 'BAD_REQUEST', 'message': "Request blocked by
input policy 'review_llm-pii-blocking': ...", 'reason': 'REQUEST_BLOCKED_BY_POLICY',
'metadata': {'phase': 'input', 'policy_name': 'review_llm-pii-blocking'}}
どのフェーズで、どのポリシーによってブロックされたかまで返るので、切り分けも容易です。一方、電話番号やメールアドレスを含まず日本語の人名だけのケースは、ビルトインPII検出が取りこぼす場合があります。日本語固有の機密情報 (人名、マイナンバーなど) はカスタムガードレール (LLMが評価者としてポリシー判定) で補強するのが実践パターンです。詳細はAIセキュラブルのサービスポリシーを参照してください。
これで、書籍の4本柱 Tracing / Evaluate & Monitor / Prompt Registry / AI Gateway をすべて体験したことになります。
エージェントの現在地と、その先
ハンズオン終了時点で、エージェントの「部品」はすべてガバナンス下に入りました。一方、エージェント本体はまだノートブック内のPython関数のままです。
| 構成要素 | 終了時点 |
|---|---|
| プロンプト | UC管理 (Prompt Registry、エイリアスで本番制御) |
| 評価データ | UC管理 (評価データセット) |
| トレース | エクスペリメントに記録 + 自動モニタリング |
| LLMアクセス | モデルサービス経由 (ガードレール適用) |
| エージェント本体 | ノートブック内の関数のまま (未デプロイ) |
エージェント本体をUnity Catalogのモデルとして登録しagents.deploy()でエンドポイント化すると、Unity AI Gatewayの「エージェント」タブの管理対象になり、境界1 (利用者 → エージェント) のガバナンスが実装できます。「利用者 → ゲートウェイ → エージェント → ゲートウェイ (モデルサービス) → LLM」という、両方の境界が守られた本番アーキテクチャの完成です。ここはチャレンジ課題として、プロンプトv3での評価ループ自走、ジャッジのアラインメント、AI/BIダッシュボード作成とともにノートブックに用意してあります。
詰まりやすいポイント
事前検証で確認済みの、詰まりやすいポイントをまとめておきます。
- ガードレール (サービスポリシー) の評価者モデルサービスはFree Editionで利用可能なものへ変更する。忘れるとフェイルクローズで全リクエストがブロックされる
- ガードレール設定の反映には約1分かかる
- モニタリングの初回実行は15〜30分後。待ち時間に発展編へ進む
- トレースのUC永続化はFree Editionでは未対応のため自動フォールバックする (通常ワークスペースではそのまま有効)
おわりに
LLMOpsの考え方とツールチェーンは、本番と全く同じものをFree Editionで学習・実践できます。「作って終わり」を卒業する第一歩として、ぜひノートブックを1周してみてください。
体系的に学びたい方には、拙共著『MLflowで実践するLLMOps──生成AIアプリケーションの実験管理と品質保証』 (技術評論社・エンジニア選書) をお勧めします。MLflow 3の4本柱を軸に、「運用し続けられる」LLMアプリケーションの構築方法を一冊にまとめています。
もくもく会当日は、Fireside Chatと座学のあと、このノートブックを各自のペースで進めます。イベントの詳細・お申し込みはconnpassページからどうぞ。会場でお会いできるのを楽しみにしています。







