Text-to-SQL

Text-to-SQL

출처: Text-to-SQL — Hugging Face smolagents 공식 문서

이 튜토리얼에서는 smolagentsSQL을 활용하는 에이전트를 어떻게 구현하는지 볼게요.

먼저 중요한 질문부터 할게요: 왜 단순하게 표준 text-to-SQL 파이프라인을 쓰지 않을까요?

표준 text-to-SQL 파이프라인은 취약해요. 생성된 SQL 쿼리가 틀릴 수 있거든요. 더 나쁜 건, 쿼리가 틀렸는데 에러는 나지 않아서 아무 경고 없이 틀리거나 쓸모없는 결과를 낼 수도 있다는 점이에요.

👉 반면 에이전트 시스템은 출력물을 비판적으로 살펴보고 쿼리를 바꿔야 할지 스스로 판단할 수 있어서, 성능이 크게 좋아져요.

이 에이전트를 만들어 볼게요! 💪

필요한 의존성을 설치하려면 아래 줄을 실행해요.

!pip install smolagents python-dotenv sqlalchemy --upgrade -q

Inference Providers를 호출하려면 환경 변수 HF_TOKEN으로 유효한 토큰이 필요해요. python-dotenv로 이걸 로드해요.

from dotenv import load_dotenv
load_dotenv()

그런 다음 SQL 환경을 설정해요.

from sqlalchemy import (
    create_engine,
    MetaData,
    Table,
    Column,
    String,
    Integer,
    Float,
    insert,
    inspect,
    text,
)

engine = create_engine("sqlite:///:memory:")
metadata_obj = MetaData()

def insert_rows_into_table(rows, table, engine=engine):
    for row in rows:
        stmt = insert(table).values(**row)
        with engine.begin() as connection:
            connection.execute(stmt)

table_name = "receipts"
receipts = Table(
    table_name,
    metadata_obj,
    Column("receipt_id", Integer, primary_key=True),
    Column("customer_name", String(16), primary_key=True),
    Column("price", Float),
    Column("tip", Float),
)
metadata_obj.create_all(engine)

rows = [
    {"receipt_id": 1, "customer_name": "Alan Payne", "price": 12.06, "tip": 1.20},
    {"receipt_id": 2, "customer_name": "Alex Mason", "price": 23.86, "tip": 0.24},
    {"receipt_id": 3, "customer_name": "Woodrow Wilson", "price": 53.43, "tip": 5.43},
    {"receipt_id": 4, "customer_name": "Margaret James", "price": 21.11, "tip": 1.00},
]
insert_rows_into_table(rows, receipts)

에이전트 만들기

이제 SQL 테이블을 도구로 검색 가능하게 만들어 볼게요.

도구의 description 속성은 에이전트 시스템이 LLM 프롬프트에 포함시켜서, LLM이 이 도구를 어떻게 써야 하는지 알게 해줘요. 여기서 SQL 테이블을 설명해 주면 돼요.

inspector = inspect(engine)
columns_info = [(col["name"], col["type"]) for col in inspector.get_columns("receipts")]

table_description = "Columns:\n" + "\n".join([f"  - {name}: {col_type}" for name, col_type in columns_info])
print(table_description)
Columns:
  - receipt_id: INTEGER
  - customer_name: VARCHAR(16)
  - price: FLOAT
  - tip: FLOAT

이제 우리 도구를 만들어요. 필요한 건 다음과 같아요: (자세한 내용은 도구 문서를 참고하세요)

  • 인자를 나열하는 Args: 부분이 있는 docstring
  • 입력과 출력 모두에 타입 힌트
from smolagents import tool

@tool
def sql_engine(query: str) -> str:
    """
    Allows you to perform SQL queries on the table. Returns a string representation of the result.
    The table is named 'receipts'. Its description is as follows:
        Columns:
        - receipt_id: INTEGER
        - customer_name: VARCHAR(16)
        - price: FLOAT
        - tip: FLOAT

    Args:
        query: The query to perform. This should be correct SQL.
    """
    output = ""
    with engine.connect() as con:
        rows = con.execute(text(query))
        for row in rows:
            output += "\n" + str(row)
    return output

이제 이 도구를 활용하는 에이전트를 만들어요.

CodeAgent를 사용하는데, 이건 smolagents의 주요 에이전트 클래스예요. 코드로 액션을 작성하고 ReAct 프레임워크에 따라 이전 출력을 반복적으로 개선하는 에이전트예요.

모델은 에이전트 시스템을 구동하는 LLM이에요. InferenceClientModel은 HF의 Inference API를 통해 Serverless나 Dedicated 엔드포인트로 LLM을 호출하게 해주는데, 다른 상용 API를 쓰는 것도 가능해요.

from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(
    tools=[sql_engine],
    model=InferenceClientModel(model_id="meta-llama/Llama-3.1-8B-Instruct"),
)
agent.run("Can you give me the name of the client who got the most expensive receipt?")

레벨 2: 테이블 조인

이제 더 어렵게 만들어 볼게요. 에이전트가 여러 테이블 간의 조인을 처리하게 하고 싶어요.

그래서 각 receipt_id에 대한 웨이터 이름을 기록하는 두 번째 테이블을 만들어요!

table_name = "waiters"
waiters = Table(
    table_name,
    metadata_obj,
    Column("receipt_id", Integer, primary_key=True),
    Column("waiter_name", String(16), primary_key=True),
)
metadata_obj.create_all(engine)

rows = [
    {"receipt_id": 1, "waiter_name": "Corey Johnson"},
    {"receipt_id": 2, "waiter_name": "Michael Watts"},
    {"receipt_id": 3, "waiter_name": "Michael Watts"},
    {"receipt_id": 4, "waiter_name": "Margaret James"},
]
insert_rows_into_table(rows, waiters)

테이블이 바뀌었으니, LLM이 이 테이블의 정보를 제대로 활용하도록 SQLExecutorTool의 설명을 이 테이블로 갱신해요.

updated_description = """Allows you to perform SQL queries on the table. Beware that this tool's output is a string representation of the execution output.
It can use the following tables:"""

inspector = inspect(engine)
for table in ["receipts", "waiters"]:
    columns_info = [(col["name"], col["type"]) for col in inspector.get_columns(table)]

    table_description = f"Table '{table}':\n"

    table_description += "Columns:\n" + "\n".join([f"  - {name}: {col_type}" for name, col_type in columns_info])
    updated_description += "\n\n" + table_description

print(updated_description)

이 요청은 이전보다 조금 어려우니, 더 강력한 Qwen/Qwen3-Next-80B-A3B-Thinking으로 LLM 엔진을 바꿔요!

sql_engine.description = updated_description

agent = CodeAgent(
    tools=[sql_engine],
    model=InferenceClientModel(model_id="Qwen/Qwen3-Next-80B-A3B-Thinking"),
)

agent.run("Which waiter got more total money from tips?")

바로 동작해요! 설정이 놀라울 만큼 단순했죠?

이 예제는 여기까지예요. 우리는 이런 개념들을 다뤘어요:

  • 새 도구 만들기
  • 도구의 description 갱신하기
  • 더 강한 LLM으로 바꾸면 에이전트 추론이 좋아진다는 점

✅ 이제 여러분이 항상 꿈꿔 왔던 text-to-SQL 시스템을 만들어 보세요! ✨