NL-to-SQLエージェントの仕組み
スキーマの注入、クエリ生成、実行、結果の整形を学びます。
「NL-to-SQLエージェントの仕組み」はCoddyKit上の無料AI Agentsレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Agents学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Agentsコースには全4レッスンが含まれています。
NL-to-SQLエージェントとは
自然言語からSQLへのエージェントは、自然な英語の質問をSQLクエリに変換し、データベースに対して実行して、人間が読みやすい回答を返します。
SELECT COUNT(*) FROM orders WHERE status='pending'と書く代わりに、ユーザーは単に「保留中の注文はいくつありますか?」と尋ねるだけで済みます。
基本アーキテクチャ
すべてのNL-to-SQLエージェントは、同じパイプラインに従います。
- スキーマの注入 — DBの構造をプロンプトに埋め込む
- LLMによるSQL生成 — モデルがクエリを生成する
- 実行 — データベースに対してクエリを実行する
- 結果の整形 — 行を読みやすいテキストに変換する
- 回答の返却 — ユーザーに応答する
# High-level pipeline
def nl_to_sql_agent(user_question, db_connection):
schema = get_schema(db_connection)
sql = llm_generate_sql(user_question, schema)
rows = execute_query(db_connection, sql)
answer = format_results(rows, user_question)
return answerスキーマ注入の概要
LLMは、あなたのデータベース構造を知りません。モデルが存在するテーブルとカラムを把握できるよう、すべてのプロンプトにスキーマを注入する必要があります。
簡潔なスキーマの説明によって、モデルに「ordersテーブルには、id、user_id、status、total、created_atというカラムがあります」と伝えられます。
def build_schema_prompt(schema_info):
lines = []
for table in schema_info:
cols = ', '.join(
f"{c['name']} ({c['type']})"
for c in table['columns']
)
lines.append(f"Table {table['name']}: {cols}")
return '\n'.join(lines)
# Output:
# Table users: id (INT), email (VARCHAR), created_at (TIMESTAMP)
# Table orders: id (INT), user_id (INT), status (VARCHAR), total (FLOAT)
if __name__ == '__main__':
demo_schema = [
{'name': 'users', 'columns': [{'name': 'id', 'type': 'INT'}, {'name': 'email', 'type': 'VARCHAR'}]},
{'name': 'orders', 'columns': [{'name': 'id', 'type': 'INT'}, {'name': 'user_id', 'type': 'INT'}]},
]
print(build_schema_prompt(demo_schema))
LLMによるSQL生成プロンプト
プロンプトには、スキーマ、質問、そして有効なSQLだけを返すよう明示する指示の3つを含める必要があります。
SELECTのみを許可することと、対象とするSQL方言(PostgreSQL、MySQL、SQLite)を明示することは、安全性と正確性のために重要です。
SYSTEM_PROMPT = '''You are a SQL expert. Given a database schema and a question,
generate a valid {dialect} SELECT query. Return ONLY the SQL query, no explanation.
Do not use INSERT, UPDATE, DELETE, or DROP.
Schema:
{schema}
'''
def llm_generate_sql(question, schema, dialect='PostgreSQL'):
prompt = SYSTEM_PROMPT.format(schema=schema, dialect=dialect)
response = client.chat.completions.create(
model='gpt-4o',
messages=[
{'role': 'system', 'content': prompt},
{'role': 'user', 'content': question}
]
)
return response.choices[0].message.content.strip()生成されたSQLの実行
LLMがSQLを返したら、実際のデータベースに対して実行します。可能な場合はパラメーター化クエリを使用し、必ず例外を捕捉します。LLMが無効なSQLを生成する可能性があるためです。
実行をtry/exceptで囲むと、エラーのヒントをLLMに返して再試行できます。
import psycopg2
def execute_query(conn, sql):
try:
with conn.cursor() as cur:
cur.execute(sql)
columns = [desc[0] for desc in cur.description]
rows = cur.fetchmany(100) # limit rows
return {'columns': columns, 'rows': rows}
except psycopg2.Error as e:
return {'error': str(e), 'sql': sql}ユーザー向けの結果の整形
データベースから取得した生の行は、ユーザーにとって読みやすいものではありません。エージェントは、それらを自然言語の回答に変換する必要があります。
結果が少ない場合は、行をLLMに返して解釈させます。結果が多い場合は、まず要約統計量を計算します。
def format_results(result, original_question):
if 'error' in result:
return f'Query failed: {result["error"]}'
rows = result['rows']
columns = result['columns']
if not rows:
return 'No results found.'
# For simple counts/aggregates — just return the value
if len(columns) == 1 and len(rows) == 1:
return f'Result: {rows[0][0]}'
# For multi-row results — summarize
summary = f'Found {len(rows)} rows.\n'
for row in rows[:5]: # show first 5
summary += ', '.join(f'{columns[i]}: {row[i]}' for i in range(len(columns))) + '\n'
return summary
if __name__ == '__main__':
demo_result = {'rows': [[42]], 'columns': ['count']}
print(format_results(demo_result, 'How many users signed up?'))
demo_result2 = {'rows': [], 'columns': ['id']}
print(format_results(demo_result2, 'Any orders today?'))
NL-to-SQLが難しい理由:曖昧さ
曖昧さが最大の課題です。たとえば、「優良顧客を表示してください」という質問を考えてみましょう。
- 売上金額の上位ですか。注文数の上位ですか。それとも最近利用した顧客ですか。
- 先月ですか。全期間ですか。
- 上位10件ですか。上位100件ですか。
人間は文脈を理解しますが、LLMは推測で補います。エージェントには、曖昧な質問に対応したり、明確化したりするための戦略が必要です。
AMBIGUITY_PROMPT = '''If the question is ambiguous, respond with JSON:
{"needs_clarification": true, "question": "your clarifying question"}
If clear, respond with the SQL query directly.
User question: {question}
'''
def generate_or_clarify(question, schema):
response = llm_call(AMBIGUITY_PROMPT.format(
question=question, schema=schema
))
if '"needs_clarification"' in response:
import json
return json.loads(response)
return {'sql': response}NL-to-SQLが難しい理由:スキーマのサイズ
エンタープライズデータベースには、数百のテーブルと数千のカラムが存在することがあります。スキーマ全体を注入すると、LLMのコンテキストウィンドウを超えてしまいます。
解決策には、スキーマ検索(テーブルの説明を埋め込み、関連するものを取得する)、テーブルの絞り込み(まず必要なテーブルをLLMに尋ねる)、スキーマの圧縮(インデックスや監査用のカラムを省略する)があります。
# Two-phase approach for large schemas
def get_relevant_tables(question, all_tables):
prompt = f'''Given these tables: {all_tables}
Which 3-5 tables are most relevant to answer: "{question}"?
Return a JSON list of table names only.'''
response = llm_call(prompt)
import json
return json.loads(response)
def nl_to_sql_large_db(question, conn):
all_tables = list_all_tables(conn) # just names
relevant = get_relevant_tables(question, all_tables)
schema = get_schema_for_tables(conn, relevant)
return llm_generate_sql(question, schema)NL-to-SQLが難しい理由:SQL方言の違い
SQLはどのデータベースでも同じではありません。PostgreSQLやMySQLのLIMITは、SQL ServerではTOPになります。日付関数もデータベースによって異なります。LLMは使用する方言を把握しなければなりません。
システムプロンプトには必ず対象の方言を含め、few-shotプロンプトに方言固有の例を追加することも検討してください。
DIALECT_EXAMPLES = {
'postgresql': 'Use LIMIT for row limits. Use NOW() for current time.',
'mysql': 'Use LIMIT for row limits. Use NOW() for current time.',
'sqlite': 'Use LIMIT. Use datetime("now") for current time.',
'mssql': 'Use TOP N for row limits. Use GETDATE() for current time.',
'bigquery': 'Use LIMIT. Use CURRENT_TIMESTAMP() for current time. Use backtick for table names.'
}
def get_dialect_hint(dialect):
return DIALECT_EXAMPLES.get(dialect.lower(), '')
if __name__ == '__main__':
for dialect in ['postgresql', 'sqlite', 'mssql']:
print(f'{dialect}: {get_dialect_hint(dialect)}')
エラーリカバリーループ
生成されたSQLは、最初の試行で失敗することがよくあります。堅牢なエージェントでは、エラーリカバリーループを実装します。失敗したSQLとエラーメッセージをLLMに返し、クエリを修正するよう求めます。
修正できないクエリで無限ループに陥らないよう、再試行回数は2~3回に制限します。
def nl_to_sql_with_retry(question, schema, conn, max_retries=3):
sql = llm_generate_sql(question, schema)
for attempt in range(max_retries):
result = execute_query(conn, sql)
if 'error' not in result:
return format_results(result, question)
# Ask LLM to fix the error
fix_prompt = f'The SQL query failed with error: {result["error"]}\n'\
f'Original SQL: {sql}\n'\
f'Please fix the SQL query.'
sql = llm_call(fix_prompt)
print(f'Retry {attempt + 1} with fixed SQL')
return 'Could not generate a valid query after retries.'すべてを組み合わせる
本番環境のNL-to-SQLエージェントは、スキーマの取得、プロンプトの構築、SQL生成、検証、実行、エラーリカバリー、結果の整形というすべての要素を組み合わせます。
クエリキャッシュ(同じ質問 → 同じSQL)を追加すると、繰り返し実行されるクエリのレイテンシーとLLMコストを大幅に削減できます。
import hashlib
query_cache = {}
def cached_nl_to_sql(question, schema_hash, conn):
cache_key = hashlib.md5((question + schema_hash).encode()).hexdigest()
if cache_key in query_cache:
print('Cache hit!')
sql = query_cache[cache_key]
else:
schema = get_schema(conn)
sql = llm_generate_sql(question, schema)
query_cache[cache_key] = sql
result = execute_query(conn, sql)
return format_results(result, question)理解度チェック
NL-to-SQLエージェントのパイプラインにおける、正しい手順の順番はどれですか。
まとめ:NL-to-SQLアーキテクチャ
NL-to-SQLエージェントは、構造化されたパイプライン、つまりスキーマを注入 → SQLを生成 → 実行 → 整形 → 返却という流れで、自然言語の質問を実行可能なSQLクエリに変換します。
主な課題は、ユーザーの質問に含まれる曖昧さ、コンテキストウィンドウを超えるほど大きなスキーマ、データベース間のSQL方言の違いです。エラーリカバリーループによって、最初の実行で失敗したLLM生成SQLに対応できます。
よくある質問
「NL-to-SQLエージェントの仕組み」レッスンは無料ですか?
はい。「NL-to-SQLエージェントの仕組み」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Agentsコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Agentsコースには全4レッスンが含まれています。
「NL-to-SQLエージェントの仕組み」で何を学びますか?
スキーマの注入、クエリ生成、実行、結果の整形を学びます。 ブラウザで直接実行するハンズオンコードでAI Agentsを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Agentsを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Agentsは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。
「NL-to-SQLエージェントの仕組み」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Agentsレッスンでコードを書いて実行できますか?
はい。すべてのAI Agentsレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- NL-to-SQLエージェントの仕組み
- スキーマの理解と注入
- SQLクエリの生成と検証
- 曖昧なデータベース質問への対応