自然言語データベースインターフェースを構築する
ユーザーが平易な英語で質問し、モデルがfunction callingでSQLを生成し、アプリケーションが安全にクエリを実行して、モデルが結果を説明するシステムを作成します。
「自然言語データベースインターフェースを構築する」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。
自然言語から SQL へ:ビジョン
「先月、1,000ドルを超えて利用した顧客は誰ですか?」とデータベースに尋ね、SQL クエリを1行も書かずに回答を得られると想像してみてください。自然言語データベースインターフェースでは、function calling を使って LLM に SQL を生成させ、アプリケーションがそれを安全に実行し、モデルが結果を平易な言葉で説明します。このパターンにより、技術者でないユーザーもデータにアクセスできるようになります。
システムアーキテクチャの概要
NL-to-SQL パイプラインは、次の4つのコンポーネントが連携して動作します。
- スキーマコンテキスト:LLM はデータベーススキーマを受け取り、どのテーブルやカラムが存在するかを把握します。
- SQL 生成:モデルは function call の引数として SQL クエリを生成します。
- 安全な実行:アプリがクエリを検証して実行し、結果を返します。
- 結果の説明:モデルがクエリ結果を受け取り、自然言語で説明します。
データベースクエリツールの定義
SQL の SELECT 文を受け取る query_database 関数を定義します。関数定義に含めるスキーマの説明によって、利用可能なテーブルやカラムをモデルに伝えられるため、モデルは推測に頼らず正確なクエリを生成できます。
query_db_tool = {
'type': 'function',
'function': {
'name': 'query_database',
'description': '''Execute a read-only SQL query on the company database.
Use this to answer questions about customers, orders, and products.
Only SELECT statements are allowed. Never use DROP, DELETE, UPDATE, or INSERT.
Available tables:
- customers (id, name, email, created_at, country)
- orders (id, customer_id, total_amount, status, created_at)
- order_items (id, order_id, product_id, quantity, unit_price)
- products (id, name, category, price, stock_quantity)
''',
'parameters': {
'type': 'object',
'properties': {
'sql': {
'type': 'string',
'description': 'A valid PostgreSQL SELECT statement.'
},
'explanation': {
'type': 'string',
'description': 'One-sentence explanation of what this query does.'
}
},
'required': ['sql', 'explanation']
}
}
}安全な SQL 実行
モデルが生成した生の SQL を、検証せずに実行してはいけません。SELECT 文だけを許可し、危険なキーワードを拒否し、メモリの問題を防ぐために結果の行数を制限し、読み取り専用のデータベーストランザクションで実行する安全対策層を実装します。LLM が生成したコードを実行する場合、多層防御が極めて重要です。
import re
import psycopg2
DANGEROUS_KEYWORDS = ['DROP', 'DELETE', 'UPDATE', 'INSERT', 'TRUNCATE', 'ALTER', 'CREATE', 'EXEC']
def execute_safe_query(sql: str, max_rows: int = 100) -> list:
'''Execute a read-only SQL query with safety guards.'''
sql_upper = sql.upper().strip()
# Only allow SELECT
if not sql_upper.startswith('SELECT'):
raise ValueError('Only SELECT statements are allowed.')
# Block dangerous keywords
for keyword in DANGEROUS_KEYWORDS:
if re.search(r'\b' + keyword + r'\b', sql_upper):
raise ValueError(f'Forbidden keyword: {keyword}')
conn = psycopg2.connect('postgresql://readonly_user:pass@localhost/appdb')
with conn:
with conn.cursor() as cur:
# Enforce read-only transaction
cur.execute('SET TRANSACTION READ ONLY')
cur.execute(sql)
columns = [desc[0] for desc in cur.description]
rows = cur.fetchmany(max_rows)
return [dict(zip(columns, row)) for row in rows]システムプロンプトへのスキーマコンテキストの注入
データベーススキーマ全体を確認できると、モデルはより適切な SQL を生成できます。テーブル定義、カラム名と型、カテゴリカルなカラムのサンプル値を含むシステムプロンプトを作成します。これにより、モデルは推測せずに country = 'US' と country_code = 'US' のどちらを使うべきか判断できます。
SYSTEM_PROMPT = '''You are a data analyst assistant with access to the company database.
When users ask data questions, use the query_database tool to look up the answer.
Always explain your query in plain English before executing it.
Database schema:
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
email VARCHAR UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
country VARCHAR(2) -- ISO 2-letter code: 'US', 'UK', 'DE', etc.
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
total_amount NUMERIC(10,2),
status VARCHAR -- 'pending', 'shipped', 'delivered', 'cancelled'
created_at TIMESTAMPTZ DEFAULT NOW()
);
Only use columns that exist in the schema above.
'''モデル向けのクエリ結果の整形
データベースの生の結果(辞書のリスト)は、モデルに返す前に読みやすいテキストへ整形する必要があります。結果セットを、モデルが回答を説明する際に参照できる簡潔な形式、つまりテーブルや JSON の要約に変換します。何千行ものデータを送ることは避け、大規模な結果セットは要約してください。
import json
def format_results(rows: list, max_display: int = 20) -> str:
if not rows:
return 'The query returned no results.'
total = len(rows)
display = rows[:max_display]
# Format as a simple table
if display:
columns = list(display[0].keys())
lines = [' | '.join(columns)]
lines.append('-' * len(lines[0]))
for row in display:
lines.append(' | '.join(str(row[col]) for col in columns))
result = '\n'.join(lines)
if total > max_display:
result += f'\n... ({total - max_display} more rows not shown)'
return resultパイプライン全体の実装
ここまでの内容をまとめると、ユーザーの質問を処理し、モデルに SQL の生成を依頼し、クエリを安全に実行し、その結果を説明のために返す関数を作成します。モデルは元の質問とクエリ結果の両方を受け取り、平易な英語で回答を生成します。
from openai import OpenAI
import json
client = OpenAI()
def answer_data_question(user_question: str) -> str:
messages = [
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': user_question}
]
# First call: get SQL from model
resp = client.chat.completions.create(
model='gpt-4o', messages=messages, tools=[query_db_tool]
)
assistant_msg = resp.choices[0].message
messages.append(assistant_msg)
if resp.choices[0].finish_reason == 'tool_calls':
tc = assistant_msg.tool_calls[0]
args = json.loads(tc.function.arguments)
print(f'Executing: {args["explanation"]}')
print(f'SQL: {args["sql"]}')
try:
rows = execute_safe_query(args['sql'])
result_text = format_results(rows)
except ValueError as e:
result_text = f'Query blocked: {str(e)}'
messages.append({'role': 'tool', 'tool_call_id': tc.id, 'content': result_text})
# Second call: narrate results
final = client.chat.completions.create(model='gpt-4o', messages=messages)
return final.choices[0].message.content
return assistant_msg.content複数ステップのデータ質問への対応
複雑な質問では、複数のクエリが必要になる場合があります。「売上高上位5人の顧客は誰で、その顧客の最新の注文は何ですか?」という質問には、上位顧客を特定するクエリと、その顧客の注文を取得するクエリの2つが必要です。finish_reason='stop' になるまでディスパッチループを複数回実行し、モデルが複数のツール呼び出しを順番に行えるようにします。
def answer_complex_question(user_question: str) -> str:
messages = [
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': user_question}
]
for _ in range(5): # Max 5 query rounds
resp = client.chat.completions.create(
model='gpt-4o', messages=messages, tools=[query_db_tool]
)
msg = resp.choices[0].message
messages.append(msg)
if resp.choices[0].finish_reason == 'stop':
return msg.content # Model is done
# Process tool call and loop
tc = msg.tool_calls[0]
args = json.loads(tc.function.arguments)
try:
rows = execute_safe_query(args['sql'])
result = format_results(rows)
except Exception as e:
result = f'Error: {str(e)}'
messages.append({'role': 'tool', 'tool_call_id': tc.id, 'content': result})
return 'Could not complete the analysis within the step limit.'SQL インジェクションのリスク防止
SELECT のみを許可するガードがあっても、巧妙なモデル(または悪意のあるユーザー)が、サブクエリやコメントを使った細工によってデータを持ち出そうとする可能性があります。追加の保護策として、SELECT 権限だけを持つ読み取り専用のデータベースユーザーを使用すること、別の接続プールで実行すること、クエリ内のテーブル名がスキーマの許可リストと一致することを検証することなどが挙げられます。
ALLOWED_TABLES = {'customers', 'orders', 'order_items', 'products'}
def validate_tables_in_sql(sql: str) -> bool:
'''Check that only whitelisted tables are referenced in the query.'''
import sqlparse
parsed = sqlparse.parse(sql)[0]
table_names = set()
from_seen = False
for token in parsed.flatten():
if token.ttype is sqlparse.tokens.Keyword and token.value.upper() in ('FROM', 'JOIN'):
from_seen = True
elif from_seen and token.ttype is sqlparse.tokens.Name:
table_names.add(token.value.lower())
from_seen = False
unknown = table_names - ALLOWED_TABLES
if unknown:
raise ValueError(f'References unknown tables: {unknown}')
return Trueよく使うクエリのキャッシュ
「顧客は何人いますか?」「先月の売上はいくらでしたか?」のように、同じ回答を繰り返し求められるビジネス上の質問は数多くあります。これらの結果を短い TTL で Redis にキャッシュします。クエリを実行する前にキャッシュを確認することで、データベースの負荷を減らし、よく使われる分析系の質問への応答を高速化できます。
import redis
import hashlib
import json
r = redis.Redis.from_url('redis://localhost:6379')
def cached_query(sql: str, ttl_seconds: int = 300) -> list:
cache_key = 'nl_query:' + hashlib.sha256(sql.encode()).hexdigest()
cached = r.get(cache_key)
if cached:
return json.loads(cached)
rows = execute_safe_query(sql)
r.setex(cache_key, ttl_seconds, json.dumps(rows, default=str))
return rowsユーザーへのクエリの説明
自然言語での回答とともに、生成された SQL クエリを表示してユーザーの信頼を高めます。ユーザーが「このクエリを実行しました:SELECT COUNT(*) FROM customers WHERE country = ?UK?」と確認できれば、回答が正しいか検証でき、SQL のパターンも学べます。ツールスキーマの explanation フィールドは、この用途に最適です。
クイックチェック
自然言語データベースインターフェースの構築について、理解度を確認しましょう。
レッスンのまとめ
このレッスンでは、query_database ツールのスキーマがスキーマコンテキストを注入し、モデルが正確な SQL を生成できるようにすること、実行前に安全性の検証を行い、SELECT 以外の文や危険なキーワードをブロックする必要があること、そしてモデル呼び出しのループによって、順番にクエリを実行する必要がある複数ステップのデータ分析が可能になることを学びました。次は、AI を外部ツールに接続するためのオープン標準である Model Context Protocol(MCP)について学びます。
よくある質問
「自然言語データベースインターフェースを構築する」レッスンは無料ですか?
はい。「自然言語データベースインターフェースを構築する」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。
「自然言語データベースインターフェースを構築する」で何を学びますか?
ユーザーが平易な英語で質問し、モデルがfunction callingでSQLを生成し、アプリケーションが安全にクエリを実行して、モデルが結果を説明するシステムを作成します。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Engineering Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「自然言語データベースインターフェースを構築する」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Engineering Academyレッスンでコードを書いて実行できますか?
はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- API用の関数スキーマを定義する
- アプリケーションでツール呼び出しを処理する
- 関数の並列呼び出し
- 自然言語データベースインターフェースを構築する