0Pricing
AI Agents · レッスン

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エージェントは、同じパイプラインに従います。

  1. スキーマの注入 — DBの構造をプロンプトに埋め込む
  2. LLMによるSQL生成 — モデルがクエリを生成する
  3. 実行 — データベースに対してクエリを実行する
  4. 結果の整形 — 行を読みやすいテキストに変換する
  5. 回答の返却 — ユーザーに応答する
# 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フィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. NL-to-SQLエージェントの仕組み
  2. スキーマの理解と注入
  3. SQLクエリの生成と検証
  4. 曖昧なデータベース質問への対応
← AI Agentsに戻る