はじめに
JAWS-UG北陸新幹線#4 in 長野 にて発表したBedrodkを使って自然言語でDBにアクセスできるエージェントを作成したというのを、本記事でもう少し詳しく書いてみようと思います。
また、当日の登壇資料は以下になります。
NL2SQLとは
登壇スライドのタイトルにもあるNL2SQL(Natural Language to SQL) とは、その名の通り「自然言語をSQLクエリに変換する技術」です。たとえば、エンジニアでない人が「今年一番売れた商品は?」といった曖昧な言葉で質問しても、それを適切なSQL文に変換してデータベースに問い合わせ、答えを返してくれる仕組みです。
主なメリットとして以下のようなものが挙げられます。
-
非エンジニアでもデータにアクセスできる
→BIツールやダッシュボードを開かなくても、日常会話に近い形でデータ取得ができるようになります。 -
SQLを書ける人でも、時短になる
→「どの月が売上ピーク?」のようなざっくりした疑問を、いちいちクエリに直さなくても答えが得られる。
構成図
構成図で示すと以下のようになっています。
ユーザーの問いかけから、Bedrockエージェントが二つのナレッジベースに対してデータを検索する形になります。
やりたかったこと
今回作ったもの構成の狙いは主に以下になります。
- 自然言語での問い合わせに対して、SQLを生成、実行できる
- SQLを書かずに「◯◯の売上を教えて」と聞いたら返ってくるような仕組み
- “それっぽい答え”ではなく、実際にDBに問い合わせて答える
- DBのような「構造化データ」だけでなく、必要に応じて「非構造化データ」も参照して質問に答える
Amazon Bedrock Agent
Amazon Bedrock Agentは、GUI上から簡単に実装できます。
ユーザーからの自然言語入力を受け取り、プロンプト、ツール、ナレッジベースなどを組み合わせて、複数ステップの処理を自動で実行することができます。
今回のNL2SQLでは以下のような役割を担ってくれます。
機能 | 内容 |
---|---|
ユーザー入力の受け取り | 例:「ごまドレッシングの返品条件を教えて」など |
自然言語→SQL変換 | モデル(Claude 3 Sonnet)によりSQLを自動生成 |
ナレッジベースへの問い合わせ | RedshiftやOpenSearchを使ったデータ参照 |
最終回答の生成 | DB応答を自然文に整形して返却 |
今回は作成時、二つのナレッジベースを登録してあげます。
複数のデータソース(構造化データと非構造化データ)を扱いたい場合でも、Agent Builderの画面上からナレッジベースを複数登録しておけば、Bedrockが自動的にマルチソース検索を行ってくれます。
これにより、モデルは「質問内容に応じて適切なソースを参照」する挙動を自然に行ってくれます。
この柔軟さがBedrock Agentの大きな強みです。
Knowledge Bases for Amazon Bedrock
おなじみのAWSにおけるRAGですね。今回は構造化データと非構造化データそれぞれに対して検索をかけたいので、ナレッジベースを二つ作成します。
OpenSearch Service
非構造化データを埋め込みで検索できるようにしてくれます。
今回は以下の用途で利用します。
項目 | 内容 |
---|---|
対象データ | 商品説明や返品ポリシーなどの非構造化テキスト |
埋め込みモデル | Bedrock組み込みの amazon.titan-embed-text-v2:0 |
検索方式 | コサイン類似度によるベクトル検索 |
主な目的 | SQLだけでは取得できない補足情報(規約文など)を参照するため |
これにより、「商品の売上データ(構造化)」だけでなく、「その商品に関するテキスト情報(非構造化)」も同時に引き出すことが可能になります。
つまり、ユーザーが「この商品の返品ルールを教えて」と尋ねた場合、
モデルは:
- Redshiftのsalesテーブルから該当商品を特定
- OpenSearchのナレッジベースから返品ポリシー文書を検索
- 両方を統合して自然文で回答生成
といったマルチソース推論を自動で行ってくれます。
Amazon Redshift
Amazon Redshift はAWSのデータウェアハウス(DWH)サービスです。
一般的にはBI用途や大規模分析に使われますが、実はBedrock Knowledge Baseの構造化データ検索バックエンドとしても対応しています
プレイグラウンド上から「構造化データストアを含むナレッジベース」を選択すると、Redshiftを利用したRAGの構築を始められます。
今回の構成ではRedshiftを「ナレッジベースの一つ」として接続し、SQLクエリの生成・実行対象として活用しました。
利用時のポイントは以下の通りです:
• ナレッジベースがRedshiftにアクセスできるよう、IAMロールに適切なポリシーを付与する
• スキーマ単位でUSAGE権限を付与し、SELECTのみ許可(DELETE等は拒否)
• 実行結果はBedrock経由でモデルに返却され、自然文として整形される
構築する上での注意点
今回の構成は「自然言語で生成したSQLをそのまま実行する」という非常にパワフルな仕組みです。
裏を返せば、少しの設定ミスで意図しないクエリが実行されるリスクもあるということです。
ここでは実際に構築して気づいたポイントを中心にまとめます。
IAMロールと権限設定
まず前提として、ナレッジベースがRedshiftに接続してクエリを実行するためには、
Bedrock Knowledge Base用のIAMロールに対して明確な権限を与える必要があります。
GRANT USAGE ON SCHEMA スキーマ名 TO "IAMR:ナレッジベースのロール名"
GRANT SELECT ON テーブル名 TO “IAMR:ナレッジベースのロール名";
上記のように、「SELECTのみに制限」 するのがベストです。
自然言語で生成されたSQLは開発者が直接書くものではないため、DELETEやUPDATEを実行できる状態にしてしまうと、
最悪の場合データ破損や誤削除のリスクがあります。
そのためあらかじめ権限をSELECTだけに絞っておくと安心です。
勝手にINSERTやDELETEされたら笑えないんで...
Bedrock上では生成されたSQLがログに出力されるため、
「どんなSQLが生成されたのか」を後から確認することは可能ですが、
実行前にロールレベルで防ぐのが最も安全です。
ちなみに、試しにDELETEを実行しようとすると、しっかりエラーになりました。
つまり、正しく権限を設定できているということです。
この動作確認は本番前に一度必ずやっておくのがおすすめです。
テーブルスキーマは明示的に教えておく
LLMは非常に柔軟ですが、曖昧なカラム名や複数の類似テーブルがあると誤ったSQLを生成することがあります。
そのため、Agent設定時に以下のようにテーブル構造を明記した説明文を与えることで精度が格段に向上しました。
テーブル: products
- product_id: 商品ID
- name: 商品名
- sales: 売上金額
テーブル: return_policies
- policy_id: ポリシーID
- return_period: 返品期間
これをしないと、反応してくれなかったり、おかしな回答してきたりする場合があるので、やっておいた方が無難です。
動作確認
構造化データの場合
まず、テーブルに対する動作確認はテーブルを二つ用意し、それぞれに対する問い合わせができているか確認します。
簡単なSELECT文
まず例として、簡単なSELECT分をやらせてみたいと思います。
下記の例では「売上が最も多い商品」を聞いています。
その問い合わせに対して、Bedrockがいい感じにSQLを勝手に生成して実行してくれています。
下記が実際に生成、実行されたSQLです。
こう見ると正しそうですね。
SELECT
"product_name", "sales"
FROM
public.products
ORDER BY
"sales"
DESC LIMIT 1;
結果として、テーブル内で最も売上金額の高い商品を正しく取得できました。
特筆すべきは、モデルがORDER BY句やLIMIT句を自発的に追加している点です。
これはプロンプト内で「上位1件を返して」と明示しなくても、
自然言語の“ニュアンス”を汲み取ってくれていることを意味します。
テーブル同士の結合が発生する場合
次に少し複雑な、テーブリ同士の結合が発生する検索をやらせてみます。
回答は正しそうではあるのですが、英語になってしまいました。
ここは何度か試しましたが、英語のままだったりと、まだ少し不安定なところがあるようですね。
SELECT
"p"."name", "r"."return_period", "r"."open_condition"
AS
"return_conditions", "r"."shipping_cost_payer"
FROM
public.products “p” INNER JOIN public.return_policies "r"
ON
"p"."policy_id" = "r"."policy_id"
WHERE
"p"."name" = 'ごまドレッシング';
JOIN条件・カラム指定ともに正しく、Redshiftのクエリ実行結果とも整合性が取れていました。
ただし、返答文が英語になるケースが何度か見られたため、言語制御は今後の調整ポイントになりそうです。
構造化データ+非構造化データの場合
次に、構造化データに加えて、非構造化データからもデータを参照するように問いかけを行います。
下の例では、商品データだけでなく、その商品に結びつくOpenSearchに格納した非構造化データ(商品説明・返品規約PDF)も組み合わせて検索を行いました。
ここで、動作フローは大まかに以下のようになります。
- Redshiftのproductsテーブルから該当商品を特定
- OpenSearchナレッジベースから関連文書を検索
- 両方の結果をまとめて自然文で回答
回答には「返品期間:30日以内」「送料:購入者負担」など、テーブルとPDF両方から取得した内容が自然に混ざって出力されました。
この動作はまさに、Bedrock Agentが複数データソースを横断的に扱えている証拠です。
最後に
今回の構成では、Amazon Bedrock単体でも実用レベルのNL2SQL環境が作れることを実感しました。
特に「Redshift × OpenSearch × Bedrock Agent × Knowledge Base」という組み合わせは、構造化・非構造化データの両方を自然言語で扱うというユースケースに非常にマッチします。
開発を通して感じたことをまとめると:
• 権限設計はやや手間がかかるが、一度整えれば非常に安全に動作する
• Bedrock AgentのGUI設定でここまで自動化できるのは本当に便利
• LLMの出力に対して安全制約を組み込む設計(SELECT限定など)は必須
• 非構造化データとの組み合わせで、回答のリッチさが大幅に増す
実際に触ってみると、プロンプトエンジニアリングだけでは到達できない実用的なLLM構成という印象でした。
AWSの各サービスが密に連携するようになったことで、
「RAG+LLM+DB」のような構成がようやく現実的に扱える段階に来たと感じます。