はじめに
LLMを本番投入すると、モデルやプロンプトの改善は「感覚」では回らなくなります。
必要なのは、いつ、どの入力に、どのプロンプトとモデルで、どんな出力が出て、評価はどうだったか を継続的に追える仕組みです。
この記事では、BigQueryとデータレイクを使ってLLM評価ログを集約し、オフライン評価とオンライン監視の両方に使える基盤を作る設計を、できるだけ実装に近い形で紹介します。
この記事のゴール
- LLMアプリの推論ログと評価ログを統合して保存できる
- BigQueryで以下ができる
- オフライン評価の集計
- A/B比較
- 品質劣化やコスト異常の監視
- プロンプト、モデル、データの変更履歴まで追跡できる
なぜ「評価ログ基盤」が必要か
LLMは以下が同時に変化します。
- モデルのバージョン
- プロンプト
- システムメッセージやツール定義
- RAGの検索対象、埋め込みモデル、リランキング設定
- 入力データやユーザー属性
この状態で「良くなった気がする」「苦情が増えた気がする」は危険です。
同じ入力に対する再現評価と本番での品質監視をつなぐログが必要です。
全体アーキテクチャ
ざっくり次の2レイヤで構成します。
| レイヤ | 役割 |
|---|---|
| データレイク層 | 生ログをそのまま保存。再処理やスキーマ変更に強い |
| BigQuery層 | 正規化した分析用テーブル。評価や監視クエリがすぐ書ける |
データの流れ
ログ設計の最重要ポイント
LLM評価ログは、最低でも以下を持つと後悔しません。
1. 追跡キー
| フィールド | 用途 |
|---|---|
request_id |
1回の推論を一意に識別 |
session_id |
会話セッションの追跡 |
user_id |
ユーザー単位の分析(匿名化推奨) |
2. 再現に必要なメタ情報
モデル関連
model_name-
model_versionormodel_snapshot -
temperature,top_p,max_tokens
プロンプト関連
prompt_idprompt_versiontool_schema_version
RAG設定
embedding_modelretriever_versionreranker_versiontop_k
3. コストと性能
prompt_tokenscompletion_tokenslatency_msestimated_cost
4. 評価
-
auto_score(ルーブリックやLLM-as-a-judge) human_score-
label(pass/fail, safety flagsなど)
データモデル設計
Rawテーブル
- なるべくアプリのログ構造のまま保存
- JSONカラムを残す(後からのスキーマ変更に対応)
Coreテーブル
BigQueryで分析しやすいように正規化した以下のテーブルを用意します。
-
llm_requests- 推論リクエスト -
llm_responses- 推論レスポンス -
llm_rag_traces- RAGの検索トレース -
llm_evaluations- 評価結果 -
prompt_registry- プロンプト台帳
BigQuery スキーマ例
1. 推論リクエスト
CREATE TABLE IF NOT EXISTS `analytics.llm_requests` (
request_id STRING,
session_id STRING,
user_id STRING,
request_ts TIMESTAMP,
app_name STRING,
environment STRING, -- prod/stg/dev
feature_name STRING, -- usecase or endpoint
model_name STRING,
model_version STRING,
prompt_id STRING,
prompt_version STRING,
system_prompt_hash STRING,
user_prompt_hash STRING,
temperature FLOAT64,
top_p FLOAT64,
max_tokens INT64,
input_text STRING,
input_lang STRING,
rag_enabled BOOL,
rag_config STRUCT<
embedding_model STRING,
retriever_version STRING,
reranker_version STRING,
top_k INT64
>,
metadata JSON
)
PARTITION BY DATE(request_ts)
CLUSTER BY app_name, feature_name, model_name, prompt_id;
2. レスポンス
CREATE TABLE IF NOT EXISTS `analytics.llm_responses` (
request_id STRING,
response_ts TIMESTAMP,
output_text STRING,
finish_reason STRING,
prompt_tokens INT64,
completion_tokens INT64,
total_tokens INT64,
latency_ms INT64,
estimated_cost_usd FLOAT64,
safety_flags ARRAY<STRING>,
metadata JSON
)
PARTITION BY DATE(response_ts)
CLUSTER BY finish_reason;
3. RAGトレース
CREATE TABLE IF NOT EXISTS `analytics.llm_rag_traces` (
request_id STRING,
trace_ts TIMESTAMP,
query_text STRING,
retrieved_docs ARRAY<STRUCT<
doc_id STRING,
source STRING,
score FLOAT64,
rank INT64,
snippet STRING
>>,
final_context_text STRING,
metadata JSON
)
PARTITION BY DATE(trace_ts)
CLUSTER BY request_id;
4. 評価
CREATE TABLE IF NOT EXISTS `analytics.llm_evaluations` (
request_id STRING,
eval_ts TIMESTAMP,
eval_type STRING, -- auto/human/offline
judge_model_name STRING,
judge_model_version STRING,
rubric_name STRING,
rubric_version STRING,
score FLOAT64,
pass BOOL,
error_type STRING, -- hallucination, safety, format, etc
comment STRING,
metadata JSON
)
PARTITION BY DATE(eval_ts)
CLUSTER BY eval_type, rubric_name;
5. プロンプト台帳
CREATE TABLE IF NOT EXISTS `analytics.prompt_registry` (
prompt_id STRING,
prompt_version STRING,
created_ts TIMESTAMP,
owner STRING,
description STRING,
system_prompt STRING,
user_prompt_template STRING,
change_log STRING,
tags ARRAY<STRING>
)
PARTITION BY DATE(created_ts)
CLUSTER BY prompt_id;
取り込み戦略
A. まずはシンプルにバッチ
- アプリがJSONログを1行1レコードで出力
- 日次 or 時間単位でBigQueryにロード
- 変換SQLでCoreテーブルに投入
メリット: 速い、安い、壊れにくい
B. 速度が必要ならストリーミング
即時の劣化検知やコスト警告が必要な場合は、Pub/Sub + DataflowなどでリアルタイムにBigQueryへ流す構成も検討します。
「評価」の作り方
1. オフライン評価データセット
代表入力を固定して保管し、golden_set_id を付与します。
CREATE TABLE IF NOT EXISTS `analytics.llm_golden_inputs` (
golden_set_id STRING,
case_id STRING,
created_ts TIMESTAMP,
input_text STRING,
expected_output TEXT,
category STRING,
difficulty STRING,
metadata JSON
)
PARTITION BY DATE(created_ts)
CLUSTER BY golden_set_id, category;
2. LLM-as-a-Judge
- ルーブリックを明文化する
- ジャッジモデルとバージョンを必ず保存する
ここをログに残しておかないと「評価が良くなったのか、ジャッジが甘くなったのか」が区別できなくなります。
便利な分析クエリ
1. プロンプトバージョン別の品質
SELECT
r.prompt_id,
r.prompt_version,
COUNT(*) AS n,
AVG(e.score) AS avg_score,
SAFE_DIVIDE(SUM(CASE WHEN e.pass THEN 1 ELSE 0 END), COUNT(*)) AS pass_rate
FROM `analytics.llm_requests` r
JOIN `analytics.llm_evaluations` e
USING(request_id)
WHERE e.eval_type = "auto"
AND r.environment = "prod"
AND r.request_ts >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 14 DAY)
GROUP BY 1, 2
ORDER BY avg_score DESC;
2. モデル変更の影響比較
WITH base AS (
SELECT
r.model_name,
r.model_version,
r.feature_name,
e.score
FROM `analytics.llm_requests` r
JOIN `analytics.llm_evaluations` e USING(request_id)
WHERE e.eval_type = "auto"
)
SELECT
feature_name,
model_name,
model_version,
COUNT(*) n,
AVG(score) avg_score
FROM base
GROUP BY 1, 2, 3
ORDER BY feature_name, avg_score DESC;
3. コスト異常の検知
SELECT
DATE(r.request_ts) AS d,
r.feature_name,
SUM(resp.total_tokens) AS tokens,
SUM(resp.estimated_cost_usd) AS cost_usd,
AVG(resp.latency_ms) AS avg_latency
FROM `analytics.llm_requests` r
JOIN `analytics.llm_responses` resp USING(request_id)
WHERE r.environment = "prod"
AND r.request_ts >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
GROUP BY 1, 2
ORDER BY d DESC, cost_usd DESC;
RAG評価で見るべき指標
RAGは「生成品質」だけ見ても原因が追えません。
最低限セットで持つと強いです。
| 指標 | 説明 |
|---|---|
| 検索の当たり率 | 期待するdoc_idが上位に入っているか |
| コンテキスト長 | 長すぎるとノイズが混ざる |
| 回答根拠の一致 | 引用文がコンテキストに存在するか |
これを llm_rag_traces と llm_evaluations をJOINして見ると、改善の当たりが付けやすくなります。
スキーマ変更に強くするコツ
- Rawは JSON主体で残す
- Coreは以下のルールで運用
- 追加カラムは自由
- 破壊的変更は避ける
-
metadata JSONを逃げ場にする
セキュリティと個人情報
LLMログは機密の宝庫になりがちです。以下を基本線にします。
| 項目 | 対策 |
|---|---|
user_id |
ハッシュ化 |
input_text |
必要ならマスキング版も別保存 |
| アクセス制御 | Rawへのアクセスは最小限、分析チームはCore中心 |
コスト最適化の小技
- まずはパーティションを日付で切る
-
よく使う切り口でクラスタリング
feature_namemodel_nameprompt_id
- Rawは外部テーブルで始めて、重要指標だけCoreに落とすのもあり
この設計がハマるユースケース
- チャットボット
- FAQ自動回答
- 文章要約
- コードレビュー/補完
- 生成コンテンツの審査支援
最小実装ロードマップ
最短で価値を出す順番です。
まとめ
BigQueryとデータレイクで作るLLM評価ログ基盤は、以下を同時に実現できます。
- 再現可能な改善
- 本番の品質監視
- コスト管理
- RAGの原因切り分け
ポイントは単純で、「再現に必要なメタ情報を全部ログに残す」 これに尽きます。