JubeeAI·에이전트생각이 쌓이는 작업 공간
AI·에이전트

SQL 에이전트, 실행 전에 멈출 수 있어야 합니다

Uber QueryGPT와 최신 LangGraph 연구를 바탕으로, Python 분석 업무에 검토·승인·재개 흐름을 붙이는 방법을 살펴봅니다.

“지난주 완료된 주문을 지역별로 비교해 주세요.” SQL을 생성하는 에이전트에게 짧은 요청이지만, 개발자가 확인할 것은 많습니다. 지난주의 시간대, 완료 상태의 정의, 취소·환불 처리, 집계할 테이블이 모두 맞아야 합니다. 쿼리가 실행된다는 사실만으로 질문에 답했다고 볼 수는 없습니다.

Python 백엔드와 데이터 분석 업무에 SQL 에이전트를 붙인다면, 첫 목표를 검토 가능한 쿼리 초안과 실행 전 대기 상태로 잡아볼 수 있습니다. 공개 운영 사례에서 문제를 찾고, LangGraph로 그 경계를 표현하는 작은 구조를 만들어 보겠습니다.

이 글의 자료 확인 기준은 2026년 10월 18일, 한국 시간입니다. 사례의 발표일과 오늘의 확인일은 다릅니다. 아래 주문 분석은 설명을 위한 가상 업무이며 개인이나 회사의 실제 데이터·경험을 사용하지 않았습니다.

공개 사례가 보여주는 두 가지 문제

어떤 데이터를 선택했는지 보여주기

Uber Engineering의 QueryGPT 소개는 2024년 9월 19일 공개된 운영 사례입니다. 업무 영역별로 SQL 예제와 테이블을 묶고, 질문의 의도를 분류한 다음 관련 테이블을 선택합니다. 사용자가 테이블 목록을 확인하거나 수정하는 단계도 설명합니다. Uber가 LangGraph를 사용했다는 근거로 인용하는 것은 아닙니다.

여기서 가져올 설계 원칙은 구체적입니다. 생성된 SQL만 보여주면 검토자는 틀린 테이블 선택까지 거슬러 올라가야 합니다. 선택한 테이블과 지표 정의를 SQL과 함께 보여주면, 잘못된 전제를 더 일찍 발견할 수 있습니다. 이는 이 글의 적용 제안이며 Uber 제품을 재현했다는 뜻은 아닙니다.

실행 오류와 잘못된 답을 따로 보기

2026년 10월 14일 개정된 LangGraph Text-to-SQL 연구의 v2 본문은 단순 파이프라인과 관련성 검사·검증·재시도를 추가한 파이프라인을 비교합니다. 이 실험에서는 관련성 검사기가 범위 안의 질문을 거절하는 문제가 있었고, 실행 가능한 SQL과 올바른 결과를 별도 지표로 평가했습니다. 검증 단계를 늘리는 것만으로 최종 답이 좋아진다고 가정할 수 없다는 사례입니다.

이 결과는 단일 데이터베이스와 모델을 대상으로 한 연구이며 다른 서비스의 성능을 보장하지 않습니다. LLM 판정에 의존하는 지표와 실행 순서에 따른 교란도 본문에서 밝힙니다. 따라서 여기서는 성과 수치를 옮기기보다 거절·실행 실패·결과 오류를 각각 관찰해야 한다는 점을 적용합니다.

질문부터 실행까지 경계를 나누기

가상 주문 분석 도구의 흐름을 다음처럼 구성할 수 있습니다. 아래 도식은 이 글에서 만든 설계안이며, 앞서 소개한 서비스의 내부 구조도가 아닙니다.

질문 → 기간·지표 정의 확인 → 허용된 스키마 검색 → SQL 초안
                                                   ↓
                                    정책 검사 → 검토 대기
                                                   ↓
                       거절: 종료 ← 승인 결정 → 읽기 전용 실행
                                                   ↓
                                   결과 검증 → 표와 설명

LLM은 질문 해석과 SQL 초안을 맡고, Python 백엔드는 접근 권한과 실행 정책을 맡습니다. React 화면에는 질문, 기간·시간대, 지표 정의, 선택한 테이블, SQL을 한 묶음으로 표시합니다. 승인은 그 묶음의 특정 버전에 연결해야 합니다. SQL이나 파라미터가 바뀌면 정책 검사와 검토를 다시 거칩니다.

예를 들어 “지난주”를 월요일부터 일요일까지의 한국 시간으로 정했다면 UTC 저장 컬럼에 적용할 경계도 함께 표시합니다. “완료 주문”의 상태 코드가 불명확하면 실행 전에 질문을 되돌려야 합니다. 모델이 임의로 정한 상태 코드로 빈 결과를 얻고 “주문이 없다”고 답하는 흐름을 피하기 위해서입니다.

LangGraph 공식 SQL 가이드는 쿼리 실행 도구를 interrupt()로 감싸 사람의 검토를 기다리는 예제를 제공합니다. 다만 검토는 DB 권한을 대신하지 않습니다. 허용된 분석 뷰, 읽기 전용 계정, 실행 시간·결과 크기 제한은 실행기에서 별도로 강제해야 합니다.

작은 Python 예제로 대기와 재개 확인하기

아래 예제는 SQL 실행 직전의 제어 흐름만 다룹니다. 모델 호출, SQL 생성, 정책 검사, 실제 DB 연결은 포함하지 않았습니다. fake_execute()는 고정된 가상 결과를 반환하므로 SQL의 정확성이나 보안을 검증하지 않습니다.

from typing import TypedDict

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt


class State(TypedDict):
    sql: str
    approved: bool
    rows: list[dict]


def review(state: State):
    decision = interrupt({"sql": state["sql"]})
    # 문자열 "true" 등은 승인으로 취급하지 않는다.
    return {"approved": decision is True}


def fake_execute(state: State):
    return {"rows": [{"region": "Seoul", "orders": 7}]}


builder = StateGraph(State)
builder.add_node("review", review)
builder.add_node("execute", fake_execute)
builder.add_edge(START, "review")
builder.add_conditional_edges(
    "review",
    lambda state: "execute" if state["approved"] else END,
)
builder.add_edge("execute", END)
graph = builder.compile(checkpointer=InMemorySaver())

config = {"configurable": {"thread_id": "demo-order-review-1"}}
initial = graph.invoke(
    {"sql": "SELECT region, orders FROM demo_summary",
     "approved": False, "rows": []},
    config,
)
assert initial["__interrupt__"]
assert initial["rows"] == []

final = graph.invoke(Command(resume=True), config)
assert final["rows"] == [{"region": "Seoul", "orders": 7}]

interrupt()에서 멈춘 실행은 checkpointer에 상태를 저장하고, 같은 thread_id로 Command(resume=...)를 전달하면 이어집니다. 재개할 때 해당 노드는 처음부터 다시 시작하므로, 중단 전에 DB 실행이나 외부 전송 같은 부수 효과를 넣지 않아야 합니다. 자세한 동작은 공식 Interrupts 문서를 기준으로 확인합니다.

운영에서는 thread_id를 서버가 발급하고 로그인한 사용자의 실행과 연결해야 합니다. ID를 안다는 이유만으로 승인할 수 있으면 안 됩니다. 재개 요청의 사용자 권한, 대기 상태, 만료 시각과 초안 버전을 확인하고, 반복 요청이 중복 실행으로 이어지지 않도록 실행 기록도 관리합니다. 예제의 InMemorySaver는 프로세스가 종료되면 상태를 유지하지 못하므로 운영용 영속 저장소와 복구 검증이 필요합니다.

실제로 확인한 범위와 적용 순서

위 코드는 임시 Python 환경에서 실행했습니다. 본문의 Python 코드 블록을 추출해 실행하는 방식으로 승인 전 빈 결과와 승인 후 가상 결과를 확인했고, 별도 실행 ID로 거절과 문자열 "true" 입력이 실행 노드로 진행하지 않는지도 검사했습니다. 정확한 환경과 명령은 아래 검증 기록에 적었습니다. 실제 DB 권한, 모델의 SQL 생성, 서버 재시작 복구와 React 화면은 이 예제에서 실행 검증하지 않았습니다.

업무에 적용할 때는 다음 순서로 범위를 넓힐 수 있습니다.

  1. 지표 하나를 고릅니다. 가상 주문 데이터와 수작업으로 확인한 정답 쿼리를 준비합니다. 완료 상태, 시간대, 중복 제거와 환불 규칙을 문서로 고정합니다.
  2. 초안만 만듭니다. 허용된 스키마와 지표 정의만 모델에 제공하고, 사람이 테이블·조인·필터를 검토합니다. 원본 데이터 전체를 프롬프트에 넣을 필요는 없습니다.
  3. 실행 정책을 연결합니다. DB 권한과 SQL 구조 검사를 적용합니다. SELECT로 시작하는지만 보는 문자열 검사는 충분하지 않습니다. LIMIT도 집계 전에 읽는 데이터량을 제한하지는 않습니다.
  4. 승인 화면과 재개 API를 연결합니다. 사용자·초안 버전·만료·중복 요청을 검사합니다. 승인 대기 중 수정한 쿼리는 새 검토 대상으로 취급합니다.
  5. 작은 평가 묶음으로 회귀를 봅니다. 정상 질문뿐 아니라 모호한 기간, 잘못된 컬럼, 권한 밖 테이블, 빈 결과, 거절과 재개 실패를 포함합니다. 모델·프롬프트·스키마 버전도 함께 기록합니다.

결과 검증에서는 “에러 없이 실행됐는가”, “정답 데이터와 같은가”, “표의 숫자가 설명문에도 그대로 남는가”를 따로 확인합니다. 순위 질문은 행 순서까지 비교하고, 합계가 맞아도 중복 조인이나 누락된 필터가 없는지 봅니다. 승인율이나 실행 성공률만으로 업무 정확도를 판단하면 이런 오류가 가려집니다.

발표 자료에서 교차 확인한 점

Ines Chami의 Operationalizing AI Agents in Data Analytics Workflows 공개 발표 전문에서도 같은 문제를 찾을 수 있습니다. 주최 측 페이지 게시일은 2024년 11월 22일입니다. 전문의 08:45–10:40 구간은 실행되는 SQL에도 업무 맥락과 지표 정의가 필요하다는 설명을, 18:19 구간은 원천 도구의 접근 권한을 존중해야 한다는 설명을 담습니다.

YouTube에서도 관련 영상을 찾으려 했지만 채널·검색 페이지 접근이 제한되었습니다. 영상·음성이나 YouTube 자막을 시청·검증했다고 주장하지 않으며, 위 내용은 주최 측이 공개한 텍스트 전문을 읽고 공식 SQL·Interrupts 문서와 대조한 것입니다. YouTube의 개별 영상 URL·채널·게시일은 확인하지 못해 적지 않았습니다.

출처와 검증 기록

예제 검증: Python 3.12.14와 임시 가상환경의 LangGraph 1.2.16에서 본문 코드를 python /tmp/jubee-sql-review-example.py로 실행했습니다. 이어 python /tmp/jubee-sql-review-check.py로 승인·거절·잘못된 타입의 재개 결정을 확인했습니다. 이는 가상 실행기의 제어 흐름 검사이며, 운영 DB나 LLM 성능 측정은 아닙니다.

오늘의 작은 배움을 기록해 두세요.
← 글 목록으로 돌아가기읽었다면, 직접 해보기
PERSONALIZE

화면 설정

나에게 편안한 Jubee.
지금의 기분에 맞는 화면을 선택하세요.

테마

변경 사항은 자동으로 저장됩니다.

시스템을 선택하면 기기의 라이트·다크 모드 설정에 맞춰 Jubee가 자동으로 전환됩니다.