0Pricing
AI Agents · 강의

NL-to-SQL 에이전트 작동 원리

스키마 주입, 쿼리 생성, 실행, 결과 형식 지정을 알아봅니다.

NL-to-SQL 에이전트 작동 원리은(는) CoddyKit의 무료 AI Agents 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Agents 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.

NL-to-SQL 에이전트란 무엇인가

자연어-SQL 에이전트는 일상 언어로 작성된 질문을 SQL 쿼리로 변환하고, 데이터베이스에서 실행한 다음, 사람이 읽을 수 있는 답변을 반환합니다.

SELECT COUNT(*) FROM orders WHERE status='pending'를 직접 작성하는 대신 사용자는 다음과 같이 질문하면 됩니다: "대기 중인 주문이 몇 개 있나요?"

핵심 아키텍처

모든 NL-to-SQL 에이전트는 동일한 처리 흐름을 따릅니다:

  1. 스키마 주입 — 데이터베이스 구조를 프롬프트에 주입
  2. LLM이 SQL 생성 — 모델이 쿼리 생성
  3. Execute — 데이터베이스에서 쿼리 실행
  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만 반환하라는 명시적인 지시라는 세 가지 요소를 LLM에 제공해야 합니다.

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을 반환하면 실제 데이터베이스에서 execute합니다. 가능한 경우 매개변수화된 쿼리를 사용하고 항상 예외를 처리합니다. 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은 어떤 방언을 사용해야 하는지 알아야 합니다.

시스템 프롬프트에 대상 방언을 항상 포함하고, 퓨샷 프롬프트에 방언별 예시를 추가하는 것도 고려합니다.

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 생성 → execute → format → 반환.

주요 과제는 사용자 질문의 모호성, 컨텍스트 창을 초과하는 대규모 스키마, 데이터베이스마다 다른 SQL 방언입니다. 오류 복구 루프는 첫 실행에서 실패한 LLM 생성 SQL을 처리합니다.

자주 묻는 질문

“NL-to-SQL 에이전트 작동 원리” 강의는 무료인가요?

네 — “NL-to-SQL 에이전트 작동 원리” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Agents 강의 전체를 잠금 해제할 수 있습니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.

“NL-to-SQL 에이전트 작동 원리”에서 뭘 배우나요?

스키마 주입, 쿼리 생성, 실행, 결과 형식 지정을 알아봅니다. 브라우저에서 직접 실행하는 실습 코드로 AI Agents을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

AI Agents을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 AI Agents은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“NL-to-SQL 에이전트 작동 원리” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 AI Agents 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 AI Agents 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. NL-to-SQL 에이전트 작동 원리
  2. 스키마 이해 및 주입
  3. SQL 쿼리 생성 및 검증
  4. 모호한 데이터베이스 질문 처리
← AI Agents(으)로 돌아가기