はじめに
先日、JAWSミートにて登壇してきました。
LLMにSQLを生成させる際に意識する点で、検証、構築する上で気づいた点など、収まりきらなかったポイントなどを記事にまとめていきたいと思います。
データストア・DB選択
Aurora か、Redshift か
Text-to-SQL のバックエンドに使う DB を選ぶとき、AWS だと大きく Aurora(RDS)か Redshift かの二択になります。
選定の指針
Redshift が向いているケース:
- 生成される SQL が
GROUP BYやJOINを多用する分析クエリ中心の場合 - 列指向ストレージによる集計パフォーマンスが重要な場合
- アイドル時のコストを限りなくゼロに近づけたい場合(Redshift Serverless は完全停止可能)
Aurora が向いているケース:
- ユーザーが「このレコードを更新して」のような更新系の操作も LLM にやらせたい場合
- テーブル数が非常に多く、リレーションが複雑な正規化された OLTP スキーマがある場合
- PostgreSQL / MySQL の標準構文に厳密に準拠したい場合
Redshift を使うなら知っておきたい SQL 方言の違い
Redshift は PostgreSQL ベースですが、サポートされていない PostgreSQL 機能がいくつかあります。LLM が一般的な PostgreSQL 知識で SQL を生成すると、以下のような構文でエラーになることがあります。
LLM へのシステムプロンプトに「Redshift を使用している。LATERAL JOIN や RECURSIVE CTE は使えない」と明示しておくと、こうしたエラーを減らせます。
LLMの権限設定
LLM にデータベースへのアクセスを与えるとき、必ず READ ONLY にしてください。
LLM は指示に従って SQL を生成しますが、ハルシネーションで DROP TABLE を実行したり、プロンプトインジェクションで意図しない DELETE が発行されるリスクはゼロではありません。勝手に INSERT や DELETE をされたら笑えないので、LLM が使う DB ユーザーには SELECT 権限だけ付与するのが鉄則です。
Redshift の場合
管理者ユーザー(admin)とは別に読み取り専用ユーザーを作成します。
-- 読み取り専用ユーザーの作成(NOCREATEDB: DB作成権限なし)
CREATE USER llm_readonly PASSWORD 'xxxx' NOCREATEDB;
-- 全テーブルへの SELECT 権限のみ付与
GRANT USAGE ON SCHEMA public TO llm_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO llm_readonly;
-- 今後作成されるテーブルにも自動で SELECT を付与
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO llm_readonly;
Data API を使う場合、execute_statement で DbUser パラメータを指定すれば、この読み取り専用ユーザーとして SQL を実行できます。
import boto3
client = boto3.client("redshift-data")
client.execute_statement(
WorkgroupName="my-workgroup",
Database="mydb",
Sql="SELECT * FROM sales LIMIT 10",
DbUser="llm_readonly", # ← 読み取り専用ユーザーを指定
)
DbUser を省略すると IAM ロールに紐づいた管理者権限で実行されるため、LLM が生成した DELETE や DROP がそのまま通ってしまいます。本番環境では必ず読み取り専用ユーザーを指定してください。
Aurora(PostgreSQL)の場合
Aurora PostgreSQL でも同様に、読み取り専用ロールとユーザーを作成します。
-- 読み取り専用ロールの作成
CREATE ROLE llm_readonly;
GRANT CONNECT ON DATABASE mydb TO llm_readonly;
GRANT USAGE ON SCHEMA public TO llm_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO llm_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO llm_readonly;
-- ユーザー作成・ロール割り当て
CREATE USER llm_user WITH PASSWORD 'xxxx';
GRANT llm_readonly TO llm_user;
Aurora の Data API では、Secrets Manager に格納した llm_user の認証情報を secretArn で指定します。
Aurora(MySQL)の場合
MySQL でも考え方は同じです。
-- 読み取り専用ユーザーの作成
CREATE USER 'llm_readonly'@'%' IDENTIFIED BY 'xxxx';
-- SELECT 権限のみ付与
GRANT SELECT ON mydb.* TO 'llm_readonly'@'%';
FLUSH PRIVILEGES;
スキーマの渡し方
LLM が SQL を書くには、テーブルやカラムの情報(スキーマ)が必要です。「どのテーブル定義をどう渡すか」は Text-to-SQL の精度を大きく左右します。
全スキーマ投入 vs スキーマ検索
| 方式 | メリット | デメリット |
|---|---|---|
| 全スキーマ投入 | 実装がシンプル。テーブル選択ミスがない | テーブル数が多いとトークン消費が大きい |
| スキーマ検索(Schema Linking) | トークン節約。無関係なテーブルのノイズ削減 | 検索ミス=誤ったテーブルで SQL を生成してしまう |
テーブル数が少ないなら全スキーマ投入
テーブル数が少ない(~20 テーブル程度)場合、全スキーマをプロンプトに含める方が精度が高いことが多いです。
BIRD ベンチマーク(Text-to-SQL の標準的なベンチマーク)で検証した際、15 テーブルの DB に対して以下の結果が出ました。
| モード | EX(Execution Accuracy) |
|---|---|
| 全スキーマ投入 | 60.8% |
| スキーマ検索あり | 15.0% |
スキーマ検索で精度が大幅に下がった主な原因は、検索ロジック(キーワードマッチング)の品質不足でした。検索が間違ったテーブルを返すと、どんなに優秀な LLM でも正しい SQL は書けません。
スキーマ検索を入れるべきケース
- テーブル数が 50 以上 あり、全スキーマのトークン数が LLM のコンテキストウィンドウを圧迫する場合
- テーブル名やカラム名が似通っていて、無関係なテーブルが SQL 生成のノイズになる場合
スキーマ検索の実装で気をつけること
スキーマ検索を導入する場合、何を返すかが精度に直結します。
| 情報 | 概要 |
|---|---|
| テーブル定義(DDL) | 最低限必要。LLM が SQL を組み立てる根拠 |
| 外部キー(FK)情報 | JOIN 条件を正確に書くために必須。FK がないと LLM がカラム名から推測して結合ミスする |
| サンプルデータ(数行) | カラム名だけでは意味が不明確な場合に有効。FRM = "Free Reduced Meal" のような略語の理解を助ける |
| カラムのコメント/説明 | テーブル定義に日本語コメントが付いていると、日本語の質問との対応づけが大幅に改善する |
また、検索方式はキーワードマッチングよりもベクトル検索(Bedrock Knowledge Bases、OpenSearch など)のほうが、自然言語の意味的な一致を捉えやすくおすすめです。
SQLの評価
LLM が生成した SQL の品質をどう測るか。正しさの基準は一つではなく、開発時の精度検証と本番稼働中の品質監視では適したアプローチが異なります。
オフライン評価とオンライン評価
両方やるのが理想です。 オフライン評価で事前に測り、オンライン評価で「本番の品質」を監視する、という併用が堅実です。
| 観点 | オフライン評価 | オンライン評価 |
|---|---|---|
| タイミング | 開発・テスト時にバッチ実行 | 本番で毎リクエスト or サンプリング |
| 正解データ | 必要(Ground Truth SQL) | 不要 |
| 指標 | EX%(実行結果が正解と完全一致するか) | LLM によるスコアリング |
| データ変化への耐性 | 弱い(DB データが変わると正解も変わる) | 強い(正解に依存しない) |
| 信頼性 | 高い(実行結果の完全一致で判定) | 中(LLM の主観的判断) |
オフライン評価: Ground Truth との比較
BIRDや Spiderといったベンチマークデータセットを使い、正解 SQL の実行結果と比較する方法です。
予測 SQL → DB で実行 → 結果セット A
正解 SQL → DB で実行 → 結果セット B
→ A と B が集合として一致すれば CORRECT(EX=1)
主な指標は EX(Execution Accuracy) で、結果セットが完全一致するかどうかを 0/1 で判定します。
オンライン評価: LLM-as-a-Judge
本番環境ではユーザーが自由に質問するため、正解 SQL は存在しません。そこで LLM 自身に SQL の品質を評価させる「LLM-as-a-Judge」アプローチが有効です。
評価基準の例(4 観点 × 1〜5 点)
| 観点 | 内容 |
|---|---|
| SQL 正確性 | SQL 文法が正しく、適切なテーブル・カラムを参照しているか |
| 回答関連性 | ユーザーの質問に対して的確に答えているか |
| SQL 効率性 | 不要なサブクエリやフルスキャンを避けた効率的なクエリか |
| 説明品質 | ユーザーにとって分かりやすい説明か |
評価用プロンプトに質問文・生成 SQL・エージェントの応答を渡し、上記の基準で JSON 形式のスコアを返させます。
単一モデルの評価バイアスへの対策
単一の LLM で採点すると、そのモデル特有のバイアスが乗ります。特に自分が生成した SQL を自分で評価する場合、スコアが甘くなりがちです。
対策として、複数モデルで並列採点して中央値を取る方法があります。
Anthropic モデル(Claude)も Amazon モデル(Nova)も同一のリクエスト形式で呼び出せるため、マルチモデル構成でも実装がシンプルです。
import boto3
bedrock = boto3.client("bedrock-runtime")
# Claude でも Nova でも同じコードで呼べる
resp = bedrock.converse(
modelId=model_id, # "us.anthropic.claude-haiku-4-5-..." でも "us.amazon.nova-lite-v1:0" でも OK
messages=[{"role": "user", "content": [{"text": prompt}]}],
inferenceConfig={"maxTokens": 300},
)
text = resp["output"]["message"]["content"][0]["text"]
AWS のマネージド評価サービスとの使い分け
Bedrock AgentCore Evaluations には Online Evaluation 機能があり、本番トラフィックの継続的モニタリングもサポートしています。ビルトイン Evaluator(Helpfulness, GoalSuccessRate 等)やカスタム Evaluator を設定し、サンプリング率を指定して自動評価できます。
ただし、AgentCore Online Evaluation の結果は CloudWatch Logs / Metrics に非同期で書き出される設計です
Online evaluation results are automatically saved to Amazon CloudWatch.
したがって、用途に応じた使い分けが重要です。
| ユースケース | 適したアプローチ |
|---|---|
| 運用チームがエージェントの品質を長期的にモニタリングしたい | AgentCore Online Evaluation(CloudWatch ダッシュボードで可視化) |
| ユーザーに即座にスコアをフィードバックしたい | 独自の LLM-as-a-Judge(API レスポンスに同期で含める) |
| 開発時にベンチマークで精度を測定したい | AgentCore Evaluations の code-based evaluator(バッチ実行) |
AgentCore Evaluations が不向きなのではなく、設計目的が異なるということです。
さいごに
Text-to-SQL を AWS で構築する際の注意点をまとめました。
LLM に SQL を書かせること自体はそこまで難しくないのですが、安全に・正確に・継続的に品質を担保する仕組みづくりが重要です。
参考
