0Pricing
AI Engineering Academy · Lekcja

Budowanie interfejsu bazy danych w języku naturalnym

Utwórz system, w którym użytkownicy zadają pytania w prostym języku angielskim, model generuje SQL za pomocą wywoływania funkcji, aplikacja bezpiecznie wykonuje zapytanie, a model opisuje wyniki.

Budowanie interfejsu bazy danych w języku naturalnym to bezpłatna lekcja AI Engineering Academy na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Engineering Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Język naturalny na SQL: wizja

Proszę wyobrazić sobie zadanie pytania bazie danych: „Którzy klienci wydali w zeszłym miesiącu więcej niż 1000 USD?” — i otrzymanie odpowiedzi bez napisania ani jednego zapytania SQL. Interfejs bazy danych w języku naturalnym wykorzystuje wywoływanie funkcji, aby umożliwić LLM wygenerowanie kodu SQL; aplikacja bezpiecznie go wykonuje, a model opisuje wyniki zwykłym językiem. Ten wzorzec demokratyzuje dostęp do danych dla użytkowników nietechnicznych.

Przegląd architektury systemu

Potok NL-to-SQL składa się z czterech współpracujących ze sobą komponentów:

  • Kontekst schematu: LLM otrzymuje schemat bazy danych, dzięki czemu wie, jakie tabele i kolumny istnieją.
  • Generowanie SQL: model generuje zapytanie SQL jako argument wywołania funkcji.
  • Bezpieczne wykonanie: aplikacja sprawdza i wykonuje zapytanie, a następnie zwraca wyniki.
  • Opis wyników: model otrzymuje wyniki zapytania i wyjaśnia je w języku naturalnym.

Definiowanie narzędzia Query Database

Należy zdefiniować funkcję query_database, która przyjmuje instrukcję SQL SELECT. Opis schematu w definicji funkcji informuje model, które tabele i kolumny są dostępne, dzięki czemu generuje on poprawne zapytania bez zgadywania.

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']
        }
    }
}

Bezpieczne wykonywanie SQL

Nigdy nie należy wykonywać surowego kodu SQL wygenerowanego przez model bez uprzedniej walidacji. Należy wdrożyć warstwę bezpieczeństwa, która: zezwala wyłącznie na instrukcje SELECT, odrzuca niebezpieczne słowa kluczowe, ogranicza liczbę wierszy wynikowych, aby zapobiec problemom z pamięcią, oraz wykonuje zapytania w transakcji bazy danych tylko do odczytu. Obrona warstwowa ma kluczowe znaczenie podczas wykonywania kodu wygenerowanego przez 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]

Wstrzykiwanie kontekstu schematu do komunikatu systemowego

Model generuje lepszy kod SQL, gdy może zobaczyć pełny schemat bazy danych. Należy utworzyć komunikat systemowy zawierający definicje tabel, nazwy i typy kolumn oraz przykładowe wartości dla kolumn kategorialnych. Dzięki temu model wie, czy użyć country = 'US', czy country_code = 'US', zamiast zgadywać.

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.
'''

Formatowanie wyników zapytania dla modelu

Surowe wyniki bazy danych (listy słowników) należy przed odesłaniem do modelu sformatować jako czytelny tekst. Zbiór wyników należy przekształcić w zwięzłą reprezentację — tabelę lub podsumowanie w formacie JSON — do której model może się odwołać podczas opisywania odpowiedzi. Należy unikać wysyłania tysięcy wierszy; duże zbiory wyników trzeba podsumować.

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

Implementacja pełnego potoku

Połączmy wszystkie elementy: funkcję, która przetwarza pytanie użytkownika, wywołuje model w celu wygenerowania SQL, bezpiecznie wykonuje zapytanie i przekazuje wyniki z powrotem do opisania. Model otrzymuje zarówno pierwotne pytanie, jak i wyniki zapytania, a następnie tworzy odpowiedź w zwykłym języku angielskim.

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

Obsługa wieloetapowych pytań dotyczących danych

Złożone pytania mogą wymagać wielu zapytań. Pytanie „Kim jest 5 naszych klientów o najwyższych przychodach i jakie były ich ostatnie zamówienia?” wymaga dwóch zapytań: pierwszego do znalezienia klientów o najwyższych przychodach, a następnie drugiego do pobrania ich zamówień. Należy zezwolić modelowi na wykonywanie wielu sekwencyjnych wywołań narzędzi, uruchamiając pętlę obsługi wielokrotnie, aż do momentu, gdy 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.'

Zapobieganie ryzyku wstrzyknięcia SQL

Nawet przy zabezpieczeniu zezwalającym wyłącznie na SELECT sprytny model (lub atakujący użytkownik) może próbować wyprowadzić dane za pomocą podzapytań albo sztuczek opartych na komentarzach. Dodatkowe zabezpieczenia obejmują: użycie użytkownika bazy danych tylko do odczytu, który ma wyłącznie uprawnienie SELECT, działanie w osobnej puli połączeń oraz sprawdzanie, czy nazwy tabel w zapytaniu są zgodne z białą listą schematu.

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

Buforowanie często używanych zapytań

Wiele pytań biznesowych jest zadawanych wielokrotnie i ma tę samą odpowiedź: „Ilu mamy klientów?” albo „Jakie były przychody w zeszłym miesiącu?”. Należy buforować te wyniki w Redisie z krótkim TTL. Przed wykonaniem zapytania trzeba sprawdzić pamięć podręczną — zmniejsza to obciążenie bazy danych i przyspiesza odpowiedzi na często zadawane pytania analityczne.

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

Wyjaśnianie zapytań użytkownikom

Buduj zaufanie, pokazując użytkownikom wygenerowane zapytanie SQL obok odpowiedzi w języku naturalnym. Gdy użytkownicy widzą „Wykonano to zapytanie: SELECT COUNT(*) FROM customers WHERE country = ?UK?”, mogą zweryfikować poprawność odpowiedzi i poznać wzorce SQL. Pole explanation w naszym schemacie narzędzia doskonale się do tego nadaje.

Szybkie sprawdzenie

Sprawdź swoją wiedzę na temat tworzenia interfejsu bazy danych w języku naturalnym.

Podsumowanie lekcji

W tej lekcji nauczyli się Państwo, że: schemat narzędzia query_database wstrzykuje kontekst schematu, dzięki czemu model generuje poprawny kod SQL, walidacja bezpieczeństwa musi blokować instrukcje inne niż SELECT oraz niebezpieczne słowa kluczowe przed wykonaniem, a pętla wywołań modelu umożliwia wieloetapową analizę danych wymagającą sekwencyjnych zapytań. Następnie poznamy Model Context Protocol (MCP), otwarty standard łączenia AI z zewnętrznymi narzędziami.

Często zadawane pytania

Czy lekcja „Budowanie interfejsu bazy danych w języku naturalnym” jest bezpłatna?

Tak — pełny tekst „Budowanie interfejsu bazy danych w języku naturalnym” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Engineering Academy, przejdź na CoddyKit PRO. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Budowanie interfejsu bazy danych w języku naturalnym”?

Utwórz system, w którym użytkownicy zadają pytania w prostym języku angielskim, model generuje SQL za pomocą wywoływania funkcji, aplikacja bezpiecznie wykonuje zapytanie, a model opisuje wyniki. Ćwiczysz AI Engineering Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Engineering Academy?

Nie wymagamy żadnego doświadczenia. AI Engineering Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Budowanie interfejsu bazy danych w języku naturalnym”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Engineering Academy?

Tak. Każda lekcja AI Engineering Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Definiowanie schematów funkcji dla API
  2. Przetwarzanie wywołań narzędzi w aplikacji
  3. Równoległe wywoływanie funkcji
  4. Budowanie interfejsu bazy danych w języku naturalnym
← Powrót do AI Engineering Academy