この記事は、Qiita Tech Festa 2026の「AI時代のデータベース、何が変わる?」への参加記事です。
この記事のコード・SQL・1M件ベンチ結果はGistから再現できます。
DDL、データ生成スクリプト、9本の監査SQL、1,000,000行の実測TSV、EXPLAIN indexes = 1の結果をまとめて置いています。
AI時代のデータベースで変わるのは、「ベクトル検索ができるか」だけではありません。
実運用で困るのは、次のような問いに答えられないことです。
- どのプロンプト版から幻覚率が上がったのか
- 検索スコアが低いとき、回答品質はどれくらい落ちるのか
- どのナレッジ文書が失敗回答に紐づきやすいのか
- 新しいエージェント版は本当にリリースしてよいのか
- コスト・遅延・引用漏れを同じ画面で追えているか
今回は、ローカルDocker上のClickHouseに合成RAGトレース100万件を投入し、RAGの失敗を監査するためのスキーマとSQLを作りました。
先に重要な前提を書きます。
この記事のログは実顧客データではなく、再現可能な合成データです。
モデル名・コスト・失敗率も検証用に作った値であり、特定サービスの実測品質を示すものではありません。
この記事の主眼は、AIアプリの失敗を観測するために、どの列を残し、どのSQLで意思決定するかです。
この記事で持ち帰れるものは3つです。
- RAG/AIエージェントの監査ログをClickHouseに入れるためのDDL
- 100万件ログでプロンプト劣化、検索品質、文書汚染、遅延、コストを見るSQL
-
EXPLAIN indexes = 1でPrimaryKeyが効いているか確認するリリース前チェック
結論
| 観測したいこと | ベクトルDBだけで弱い点 | ClickHouseに残す列 | SQLの出口 |
|---|---|---|---|
| プロンプト劣化 | 検索結果は追えても、プロンプト版別の品質差が見えにくい |
prompt_version, hallucination_flag, citation_missing
|
リリース判定 |
| 検索品質 | top-kの中身だけでは失敗率との相関が出ない |
retrieval_top_score, retrieval_score_gap, feedback_score
|
検索閾値の見直し |
| 遅延 | 生成・検索・キャッシュのどこが遅いか分解しにくい |
latency_ms, retrieval_ms, generation_ms, cache_hit
|
SLO監視 |
| ナレッジ汚染 | どの文書が失敗回答に出やすいか集計しにくい |
retrieved_doc_ids, failure_reason
|
文書改修の優先順位 |
| コスト | 1回答単位では見えても、版別・日別の増加が追いにくい |
prompt_tokens, completion_tokens, cost_usd
|
予算ゲート |
結論として、AI時代のDBは「検索対象の保存場所」だけでは足りません。モデル・プロンプト・検索・評価・コストを同じ時系列ログとして監査できることが重要になります。
実行環境
今回の検証環境です。
| 項目 | 値 |
|---|---|
| OS | macOS + Docker Desktop |
| Docker | 29.5.2 |
| ClickHouse | 26.5.1.882 |
| データ件数 | 1,000,000 rows |
| データ期間 | 2026-05-23 00:00:00.555 から 2026-06-05 23:59:58.632 |
| 入力形式 | JSONEachRow |
| テーブルエンジン | MergeTree |
| 全体実行時間 | 13.37 sec |
ClickHouse公式ドキュメントでは、MergeTree系エンジンは高い取り込みレートと大規模データ向けのテーブルエンジンとして説明されています。今回のような追記中心のAIログには相性が良いです。
作った検証ワークスペース
再現用コードと1M件の実測結果は、以下の公開Gistに置きました。
ClickHouse RAG observability benchmark Gist
Gistには生成済みJSON本体は含めていません。data/rag_traces.jsonlは約816MiBになり、かつスクリプトから再生成できるためです。
ローカルでは次の構成にしました。
tmp/clickhouse-rag-observability/
├── Makefile
├── docker-compose.yml
├── scripts/
│ ├── generate-rag-traces.mjs
│ └── run-benchmark.sh
├── sql/
│ ├── 00_schema.sql
│ └── queries/
│ ├── 01_prompt_regression.sql
│ ├── 02_retrieval_score_failure.sql
│ ├── 03_latency_outliers.sql
│ ├── 04_hallucination_sources.sql
│ ├── 05_daily_quality_cost.sql
│ ├── 06_release_gate.sql
│ ├── 07_filtered_tenant_prompt_slice.sql
│ ├── 08_storage_parts.sql
│ └── 09_explain_primary_key.sql
├── data/
└── results/
実行コマンドはこれだけです。
curl -L -o clickhouse-rag-observability-source.tgz \
https://gist.github.com/YushiYamamoto/0fcd4fc2cba5f44998bd39d420fd41a8/raw/clickhouse-rag-observability-source.tgz
tar -xzf clickhouse-rag-observability-source.tgz
cd clickhouse-rag-observability
make all ROWS=1000000
やっていることは単純です。
-
clickhouse/clickhouse-server:latestをDockerで起動する - Node.jsで合成RAGトレースをJSONEachRow形式で生成する
- ClickHouseに投入する
- 9本の監査SQLを実行してTSVと実行時間を保存する
ClickHouseのJSONEachRowは、1行1JSONのNDJSON/JSONLines形式として扱えるため、アプリケーションログの投入に向いています。
参考: JSONEachRow
RAGトレースのスキーマ
テーブルは次のようにしました。
CREATE DATABASE IF NOT EXISTS rag_observability;
CREATE TABLE rag_observability.rag_traces
(
event_time DateTime64(3),
event_date Date MATERIALIZED toDate(event_time),
trace_id String,
session_id String,
tenant_id LowCardinality(String),
app LowCardinality(String),
prompt_version LowCardinality(String),
model LowCardinality(String),
embedding_model LowCardinality(String),
query_intent LowCardinality(String),
query String,
retrieved_doc_ids Array(String),
retrieval_top_score Float32,
retrieval_score_gap Float32,
retrieval_doc_count UInt8,
answer_chars UInt16,
latency_ms UInt32,
retrieval_ms UInt32,
generation_ms UInt32,
cost_usd Float64,
hallucination_flag UInt8,
citation_missing UInt8,
feedback_score Int8,
failure_reason LowCardinality(String),
prompt_tokens UInt32,
completion_tokens UInt32,
safety_redacted UInt8,
cache_hit UInt8,
INDEX idx_hallucination hallucination_flag TYPE set(2) GRANULARITY 4,
INDEX idx_prompt_version prompt_version TYPE set(0) GRANULARITY 4,
INDEX idx_retrieval_score retrieval_top_score TYPE minmax GRANULARITY 4
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (tenant_id, app, prompt_version, event_time, trace_id)
SETTINGS index_granularity = 8192;
ポイントは、queryやanswerの全文を中心にしないことです。全文は別ストレージに逃がしてもよく、ClickHouse側には監査に必要な列を残します。
-
prompt_version: プロンプト変更の影響を見る -
retrieval_top_score: 検索品質と失敗率の相関を見る -
retrieved_doc_ids: 失敗に絡む文書を特定する -
hallucination_flag: 評価結果を集計可能にする -
citation_missing: 引用漏れを別指標として扱う -
latency_ms,cost_usd: 品質だけでなく運用面の劣化を見る
実測1: プロンプト版ごとの劣化を探す
まず、プロンプト版ごとに幻覚率、引用漏れ率、遅延、コストを見るSQLです。
SELECT
prompt_version,
count() AS traces,
round(avg(hallucination_flag) * 100, 2) AS hallucination_rate_pct,
round(avg(citation_missing) * 100, 2) AS citation_missing_rate_pct,
round(avg(latency_ms), 1) AS avg_latency_ms,
round(quantile(0.95)(latency_ms), 1) AS p95_latency_ms,
round(sum(cost_usd), 4) AS total_cost_usd,
round(avg(feedback_score), 2) AS avg_feedback,
topKIf(3)(failure_reason, failure_reason != 'none') AS top_failure_reasons
FROM rag_traces
GROUP BY prompt_version
ORDER BY hallucination_rate_pct DESC;
結果です。
| prompt_version | traces | hallucination_rate_pct | citation_missing_rate_pct | avg_latency_ms | p95_latency_ms | total_cost_usd | avg_feedback | top_failure_reasons |
|---|---|---|---|---|---|---|---|---|
| v1_baseline | 239,518 | 18.94 | 14.68 | 1208.1 | 2022.4 | 128.8580 | 3.39 | missing_citation, stale_doc, tool_boundary |
| v3_agentic | 240,554 | 16.16 | 13.11 | 1448.4 | 2274.4 | 187.1723 | 3.77 | missing_citation, stale_doc, ambiguous_owner |
| v4_cost_guard | 199,885 | 15.30 | 12.65 | 1174.1 | 1990.0 | 87.9558 | 3.86 | missing_citation, stale_doc, ambiguous_owner |
| v2_grounded | 320,043 | 14.38 | 12.20 | 1291.7 | 2084.0 | 192.8952 | 3.97 | missing_citation, stale_doc, tool_boundary |
ここで重要なのは、平均フィードバックだけならv3_agenticは悪く見えないことです。しかし、v2_groundedと比べると幻覚率・引用漏れ率・遅延が同時に悪化しています。AI機能のリリース判定では、単一指標ではなく複数指標の差分を見る必要があります。
実測2: 検索スコアと失敗率の相関を見る
次に、検索スコアをバケット化して失敗率を見ます。
WITH
multiIf(
retrieval_top_score < 0.55, 'low(<0.55)',
retrieval_top_score < 0.70, 'mid(0.55-0.70)',
retrieval_top_score < 0.85, 'high(0.70-0.85)',
'very_high(>=0.85)'
) AS score_bucket
SELECT
score_bucket,
count() AS traces,
round(avg(hallucination_flag) * 100, 2) AS hallucination_rate_pct,
round(avg(citation_missing) * 100, 2) AS citation_missing_rate_pct,
round(avg(feedback_score), 2) AS avg_feedback,
round(avg(latency_ms), 1) AS avg_latency_ms
FROM rag_traces
GROUP BY score_bucket
ORDER BY min(retrieval_top_score);
結果です。
| score_bucket | traces | hallucination_rate_pct | citation_missing_rate_pct | avg_feedback | avg_latency_ms |
|---|---|---|---|---|---|
| low(<0.55) | 52,291 | 36.40 | 24.03 | 3.26 | 1290.6 |
| mid(0.55-0.70) | 274,467 | 21.56 | 16.05 | 3.63 | 1285.6 |
| high(0.70-0.85) | 446,038 | 12.54 | 11.21 | 3.85 | 1286.6 |
| very_high(>=0.85) | 227,204 | 11.75 | 10.75 | 3.87 | 1283.6 |
retrieval_top_score < 0.55のバケットでは幻覚率が36.40%まで上がりました。この結果から、例えば次のようなガードを設計できます。
- top scoreが0.55未満なら回答を生成せず、追加質問に切り替える
- 0.55から0.70は「根拠が弱い」ラベルを出す
- セキュリティや契約系のintentでは閾値を上げる
- 検索スコアと引用漏れを別々のメトリクスとして監視する
ベクトルDBは検索候補を返します。しかし、検索候補が最終回答の失敗にどう効いたかを長期的に見るには、検索ログと評価ログを同じ分析基盤に残す必要があります。
実測3: 遅延の外れ値を見る
AIエージェントは便利でも、遅くなりすぎるとプロダクトに載りません。モデルとプロンプト版ごとにp95/p99を見ます。
| model | prompt_version | traces | avg_latency_ms | p95_latency_ms | p99_latency_ms | max_latency_ms |
|---|---|---|---|---|---|---|
| reasoning-large | v3_agentic | 43,099 | 2044.2 | 2472.0 | 2585.1 | 2805 |
| reasoning-large | v2_grounded | 57,822 | 1885.4 | 2296.0 | 2410.1 | 2691 |
| reasoning-large | v1_baseline | 43,254 | 1800.9 | 2217.0 | 2336.3 | 2587 |
| reasoning-large | v4_cost_guard | 35,861 | 1764.4 | 2167.0 | 2284.2 | 2556 |
| balanced-pro | v3_agentic | 81,874 | 1464.3 | 1855.0 | 1964.0 | 2253 |
| local-edge | v3_agentic | 43,531 | 1325.1 | 1706.4 | 1820.0 | 2059 |
ここではreasoning-large + v3_agenticが最も重く、p95が2472msでした。リリース判定では「精度が少し良い」だけでなく、p95遅延の増分を予算化する必要があります。
実測4: 失敗に絡む文書をランキングする
RAGの運用で見落としやすいのが、文書単位の品質です。
回答が壊れたとき、プロンプトだけを直しても、古い文書や曖昧なrunbookが混ざっていると再発します。
ClickHouseのarrayJoinを使うと、retrieved_doc_idsの配列を文書単位の行に展開できます。
公式ドキュメントでも、arrayJoinは1行を配列要素の数だけ複数行へ展開する関数として説明されています。
SELECT
doc_id,
count() AS retrieved_count,
sum(hallucination_flag) AS hallucination_count,
round(avg(hallucination_flag) * 100, 2) AS hallucination_rate_pct,
round(avg(retrieval_top_score), 3) AS avg_top_score,
topK(4)(query_intent) AS frequent_intents,
topKIf(3)(failure_reason, failure_reason != 'none') AS top_failure_reasons
FROM
(
SELECT
arrayJoin(retrieved_doc_ids) AS doc_id,
hallucination_flag,
retrieval_top_score,
query_intent,
failure_reason
FROM rag_traces
)
GROUP BY doc_id
HAVING retrieved_count >= 20
ORDER BY hallucination_count DESC
LIMIT 15;
結果の一部です。
| doc_id | retrieved_count | hallucination_count | hallucination_rate_pct | avg_top_score | frequent_intents | top_failure_reasons |
|---|---|---|---|---|---|---|
| kb_current_migration_002 | 2,862 | 544 | 19.01 | 0.757 | migration | missing_citation, stale_doc, ambiguous_owner |
| kb_current_migration_064 | 2,884 | 540 | 18.72 | 0.758 | migration | missing_citation, stale_doc, ambiguous_owner |
| kb_current_migration_008 | 2,790 | 532 | 19.07 | 0.751 | migration | missing_citation, stale_doc, ambiguous_owner |
| kb_current_migration_067 | 2,779 | 522 | 18.78 | 0.755 | migration | missing_citation, stale_doc, tool_boundary |
このランキングがあると、「プロンプトを直す」だけでなく「どの文書を直すか」まで落とせます。
特にsecurityやmigrationのような高リスクintentでは、文書レビューの優先順位をデータで決められます。
実測5: リリースゲートをSQLにする
最後に、プロンプト版のリリース判定をSQLにします。ここではv3_agenticをv2_groundedと比較しました。
SELECT
'v3_agentic_vs_v2_grounded' AS comparison,
round((avgIf(hallucination_flag, prompt_version = 'v3_agentic') - avgIf(hallucination_flag, prompt_version = 'v2_grounded')) * 100, 2) AS hallucination_delta_pct,
round((avgIf(citation_missing, prompt_version = 'v3_agentic') - avgIf(citation_missing, prompt_version = 'v2_grounded')) * 100, 2) AS citation_missing_delta_pct,
round(quantileIf(0.95)(latency_ms, prompt_version = 'v3_agentic') - quantileIf(0.95)(latency_ms, prompt_version = 'v2_grounded'), 1) AS p95_latency_delta_ms,
round((sumIf(cost_usd, prompt_version = 'v3_agentic') / countIf(prompt_version = 'v3_agentic') * 1000) - (sumIf(cost_usd, prompt_version = 'v2_grounded') / countIf(prompt_version = 'v2_grounded') * 1000), 6) AS cost_delta_per_1000_traces,
if(
hallucination_delta_pct <= 1.0
AND citation_missing_delta_pct <= 1.0
AND p95_latency_delta_ms <= 250,
'ship',
'hold'
) AS release_decision
FROM rag_traces;
結果です。
| comparison | hallucination_delta_pct | citation_missing_delta_pct | p95_latency_delta_ms | cost_delta_per_1000_traces | release_decision |
|---|---|---|---|---|---|
| v3_agentic_vs_v2_grounded | 1.78 | 0.91 | 190.4 | 0.175372 | hold |
この例ではholdです。
理由は、p95遅延は許容内でも、幻覚率と引用漏れ率の増分がしきい値を超えているからです。
この形にしておくと、AI機能のレビューが「なんとなく良さそう」から「このSQLを通ったらship」に変わります。
実測6: ClickHouseのPrimaryKeyが効いているか見る
ここまでの集計は「SQLでRAGログを分析した」だけにも見えます。
ClickHouseを使う理由を出すには、ORDER BYとPrimaryKeyが実際に読み取り範囲を絞っているかを確認する必要があります。
今回のテーブルでは、次の順に並べています。
ORDER BY (tenant_id, app, prompt_version, event_time, trace_id)
これは、AIログでよくある調査単位に合わせています。
- どのテナントか
- どのAIアプリか
- どのプロンプト版か
- いつの期間か
- どのtraceか
例えば、jp-smbテナントのsupport-ragアプリで、v3_agenticの2026-06-01以降だけを見るSQLを用意しました。
SELECT
tenant_id,
app,
prompt_version,
count() AS traces,
round(avg(hallucination_flag) * 100, 2) AS hallucination_rate_pct,
round(quantile(0.95)(latency_ms), 1) AS p95_latency_ms,
round(sum(cost_usd), 5) AS total_cost_usd,
topKIf(3)(failure_reason, failure_reason != 'none') AS top_failure_reasons
FROM rag_traces
WHERE
tenant_id = 'jp-smb'
AND app = 'support-rag'
AND prompt_version = 'v3_agentic'
AND event_time >= toDateTime64('2026-06-01 00:00:00', 3)
GROUP BY
tenant_id,
app,
prompt_version;
結果です。
| tenant_id | app | prompt_version | traces | hallucination_rate_pct | p95_latency_ms | total_cost_usd | top_failure_reasons |
|---|---|---|---|---|---|---|---|
| jp-smb | support-rag | v3_agentic | 5,507 | 16.67 | 2258.7 | 4.2556 | missing_citation, stale_doc, tool_boundary |
このクエリは100万件のテーブルに対して0.004 secでした。さらにEXPLAIN indexes = 1を見ると、PrimaryKeyで読み取りgranuleが絞られています。
PrimaryKey
Keys:
tenant_id
app
prompt_version
event_time
Parts: 4/4
Granules: 7/124
Search Algorithm: binary search
ここがClickHouseらしいポイントです。
RAGログは「あとで全部読むログ」ではなく、障害対応やリリース判定で特定のテナント、アプリ、プロンプト版、期間を高速に切るデータです。
ORDER BYを調査軸に合わせると、同じテーブルから全体傾向と個別調査の両方を引けます。
実測7: 100万件ログの保存サイズを見る
保存サイズも確認しました。
SELECT
sum(rows) AS rows,
formatReadableSize(sum(bytes_on_disk)) AS bytes_on_disk,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_data,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_data,
round(sum(data_uncompressed_bytes) / nullIf(sum(data_compressed_bytes), 0), 2) AS compression_ratio,
count() AS active_parts
FROM system.parts
WHERE
database = 'rag_observability'
AND table = 'rag_traces'
AND active;
結果です。
| rows | bytes_on_disk | compressed_data | uncompressed_data | compression_ratio | active_parts |
|---|---|---|---|---|---|
| 1,000,000 | 96.37 MiB | 96.34 MiB | 343.51 MiB | 3.57 | 4 |
生成元のJSONEachRowファイルは約816MiBでしたが、ClickHouse上では約96MiBでした。
実データでは列の種類や文字列長で変わりますが、AIログのように列が多く、低カーディナリティな分類列が多いデータでは、列指向DBの圧縮と集計の恩恵を受けやすいです。
クエリ実行時間
100万件の手元Docker環境でも、監査クエリはすべて短時間で終わりました。
| Query | Elapsed seconds |
|---|---|
| prompt regression | 0.016 |
| retrieval score failure | 0.011 |
| latency outliers | 0.017 |
| hallucination sources | 0.125 |
| daily quality cost | 0.012 |
| release gate | 0.007 |
| filtered tenant prompt slice | 0.004 |
| storage parts | 0.004 |
| explain primary key | 0.002 |
もちろん、本番では件数、パーティション、ORDER BY、圧縮、マテリアライズドビュー、分散構成で数字は変わります。それでも、RAGログのような追記型・時系列・集計中心のデータでは、ClickHouseを「AIの観測DB」として使う価値があります。
SQLite/PostgreSQLだけだと何が厳しいか
誤解を避けるために書くと、SQLiteやPostgreSQLでもAIログの保存や分析はできます。
小規模なPoC、管理画面の絞り込み、業務レコードとのJOINが主目的なら、むしろ既存のPostgreSQLに寄せた方が運用は簡単です。
ただし、今回のようなRAG監査ワークロードでは、見る軸が増えます。
- 追記中心の大量イベント
-
tenant_id,app,prompt_version,model,query_intentの多軸集計 - p95/p99の遅延監視
- 文書ID配列を展開した失敗ランキング
- 日別・版別のコスト集計
- リリース前後の差分判定
この条件では、SQLite/PostgreSQLだけで押し切ると次の設計が重くなります。
| 観点 | SQLiteで厳しい点 | PostgreSQLで厳しい点 | ClickHouseで楽になる点 |
|---|---|---|---|
| 追記型ログ | 単一ファイルDBとしてローカル検証には強いが、継続的な高頻度ログ集計には向きにくい | OLTP本体と分析クエリが同居すると負荷分離が必要 | 追記・集計中心の列指向テーブルとして分けやすい |
| 多軸集計 | 100万件以上でp95やgroup byを何度も切る用途は重くなりやすい | index、partition、materialized viewをかなり設計する必要がある |
LowCardinality, MergeTree, ORDER BYで集計軸を設計しやすい |
| 配列展開 |
retrieved_doc_idsのような配列ログを高速に分析する設計が難しい |
JSONB/配列/正規化の選択で設計が割れやすい |
Array(String) + arrayJoinで文書別ランキングを作りやすい |
| p95/p99 | percentile系を繰り返す分析は用途外になりがち | 可能だが、運用DBで重い分析を回すと影響範囲が広い |
quantile系の集計を監査SQLに組み込みやすい |
| 圧縮と保管 | ログ保管がファイル肥大化しやすい | 圧縮や古いpartition管理を別途考える | 今回はJSONEachRow約816MiB相当をテーブル上約96MiBで保持できた |
つまり、SQLite/PostgreSQLが悪いという話ではありません。役割を分けるのが現実的です。
| 役割 | 向いているDB |
|---|---|
| ユーザー、権限、業務レコード | PostgreSQL |
| ローカルPoC、軽量な再現テスト | SQLite |
| RAG/AIエージェントの大量監査ログ | ClickHouse |
| ベクトル検索そのもの | vector DB / PostgreSQL pgvector / 専用検索基盤 |
AI時代のデータベース設計では、「全部を1つのDBに入れる」よりも、業務状態はOLTP、検索は検索基盤、監査ログはClickHouseのように役割を分けた方が、リリース判定と障害調査がやりやすくなります。
実務に入れるときのチェックリスト
RAG/AIエージェントのログをClickHouseに入れるなら、最初に次を決めておくと後で効きます。
-
trace_idを全サービスで共通にする -
prompt_versionとmodelを必ず残す - 検索結果は本文ではなく
doc_idとscoreを残す -
hallucination_flagは人手評価、LLM評価、自動テストのどれかを明記する -
citation_missingを幻覚とは別指標にする -
latency_msを検索・生成・ツール実行に分ける -
cost_usdまたは推定token数を残す - リリース前後を比較できるように、実験群と対照群を同じテーブルに入れる
- 個人情報や機密文はClickHouseに直接入れず、マスキング後のメタデータを入れる
- 失敗文書ランキングを週次のナレッジ改善タスクに接続する
本番導入テンプレ
本番に入れるなら、最初から巨大な基盤を作るより、次の4層で始めるのが現実的です。
| 層 | 役割 | 最初に作るもの |
|---|---|---|
| 収集 | アプリからAI実行ログを送る |
trace_id, tenant_id, prompt_version, model, latency_ms
|
| 評価 | 回答品質を後付けで更新する |
hallucination_flag, citation_missing, feedback_score
|
| 監査 | リリース可否を見る | prompt版比較SQL、p95遅延SQL、コストSQL |
| 改善 | 改修タスクへつなぐ | 文書別失敗ランキング、低score回答リスト |
最初の運用ルールはこのくらいで十分です。
release gate:
hallucination_delta_pct <= 1.0
citation_missing_delta_pct <= 1.0
p95_latency_delta_ms <= 250
retrieval guard:
retrieval_top_score < 0.55 -> do not answer, ask a clarification
0.55 <= retrieval_top_score < 0.70 -> answer with weak-evidence warning
weekly knowledge review:
sort documents by hallucination_count desc
review top 20 documents
fix stale_doc and missing_citation first
このテンプレの狙いは、AIを「チャット機能」として見るのをやめ、リリース判定・障害調査・ナレッジ改善が回るプロダクト機能として扱うことです。
失敗パターン
今回の検証から、RAG運用で起きがちな失敗を5つに整理できます。
| 失敗パターン | 症状 | 対策 |
|---|---|---|
| 検索スコアだけを見る | top-kは返るが回答品質が改善しない |
retrieval_top_scoreとhallucination_flagを同じテーブルで見る |
| プロンプトだけを直す | 失敗文書が残り続ける |
arrayJoin(retrieved_doc_ids)で文書別に失敗率を見る |
| 平均遅延だけを見る | p95/p99でUXが壊れる |
quantile(0.95), quantile(0.99)をリリースゲートに入れる |
| 引用漏れを幻覚に混ぜる | どの品質問題なのか判断できない |
hallucination_flagとcitation_missingを分ける |
| コストを後で見る | 品質改善と同時に運用費が跳ねる |
cost_usdを版別・日別に集計する |
まとめ
AI時代のデータベースは、単に「ベクトルを保存できるDB」ではありません。
RAGやAIエージェントを本番運用するなら、検索、生成、評価、遅延、コスト、文書品質を横断して見る必要があります。
今回の100万件ベンチマークでは、ClickHouseで次の判断ができました。
- プロンプト版ごとの幻覚率・引用漏れ率・遅延を比較する
- 検索スコアが低いバケットの失敗率を確認する
- 失敗に絡む文書をランキングする
- PrimaryKeyで特定テナント・アプリ・プロンプト版・期間を高速に切る
- 100万件ログの保存サイズと圧縮比を確認する
- p95遅延と品質差分でリリース可否を決める
- AIログを「後から読むログ」ではなく「ship/holdを決めるデータ」に変える
RAGの精度改善は、モデル選定やプロンプト改善だけでは足りません。
AIの失敗をSQLで監査できる状態にすることが、AI時代のデータベースに求められる役割だと考えます。
参考リンク
- Qiita Tech Festa 2026: 「AI時代のデータベース、何が変わる?」について語ろう!
- ClickHouse Docs: MergeTree table engine
- ClickHouse Docs: JSONEachRow
- ClickHouse Docs: arrayJoin function
- ClickHouse Docs: topK
- ClickHouse Docs: quantile
- ClickHouse Docs: EXPLAIN Statement
- ClickHouse Docs: system.parts
- 再現用Gist: ClickHouse RAG observability benchmark
この記事を書いた人✏️@YushiYamamoto
ITPRODX.com代表 / AIアーキテクト
Next.js / TypeScript / n8nを活用した自律型アーキテクチャ設計を専門としています。
日々の自動化の検証結果や、ビジネス側の視点(ROI等)に関するより深い考察は、以下の公式サイトおよびnoteで発信しています。
