6.6.4 LMQL(Language Model Query Language)을 이용한 쿼리 기반 구조화 제약 설정

6.6.4 LMQL(Language Model Query Language)을 이용한 쿼리 기반 구조화 제약 설정

로컬 파운데이션 모델(Local Foundation Model)의 비정형 텍스트 생성 과정을 극단적인 구조화 출력(Structured Output) 파이프라인으로 강제 결속시키는 수많은 시도 중, 아키텍처 관점에서 가장 우아하고 혁신적인 접근법 중 하나는 스위스 취리히 연방 공과대학(ETH Zürich) 연구진이 개발하여 오픈소스로 공개한 LMQL (Language Model Query Language) 프레임워크다.

전통적인 데이터베이스 조작에 친숙한 백엔드 개발자들을 겨냥하여 SQL(Structured Query Language) 쿼리 문법의 직관성과 파이썬(Python)의 런타임 실행 제어 논리를 강력하게 결합한 이 특수 도메인 언어(DSL, Domain-Specific Language)는, 기존의 객체 지향적인 Pydantic 파서(Parser) 타입 힌팅이나 GBNF(Grammar)의 복잡한 문법 트리 주입 방식과는 근본의 궤를 달리한다. LMQL은 언어 모델의 다음 토큰 생성 과정을 ‘선언적(Declarative)’ 방식으로 완벽하게 장악(Steering)하고 제어한다.

1. SQL 쿼리(SELECT & WHERE) 메타포를 통한 생성형 데이터 추출

LMQL 아키텍처의 핵심 철학은, 불투명하고 거대한 LLM 가중치 텐서를 일종의 거대한 ’비정형 데이터베이스 탐색 엔진’으로 수학적으로 치환하여 간주하는 것이다. 그리고 우리가 최종적으로 얻어내야 할 목표 데이터의 구조화 스키마(Schema)와 하드웨어적 제약(Constraint)을 마치 DB를 탐색하듯 argmax(또는 sample), FROM, WHERE 구문을 통해 파이프라인 컴파일러에 강력히 명령한다.

# LMQL 프로덕션 쿼리 파이프라인 예시
argmax
    """
    [System Instruction]
    다음 환자의 EMR 진료 차트 텍스트에서 필수 인적 정보를 정확히 추출하시오.
    "환자 엘리스 브라운(Alice Brown)은 현재 한국 나이로 정확히 30세이며, 혈압은 정상이다."
    
    [Structured Output Template]
    Patient_Name: [NAME_VAR]
    Patient_Age: [AGE_VAR]
    """
from
    "local:llama-3-8b-instruct" # 로컬 호스팅 GGUF 혹은 OpenAI API 엔드포인트로 무봉제 교체 가능
where
    len(NAME_VAR) < 15 and type(AGE_VAR) is int and AGE_VAR > 0

위의 아름다운 쿼리 코드는 짐짓 단독 평문 프롬프트 문자열처럼 보이지만, 백엔드에 숨겨진 LMQL 컴파일러에 의해 완전히 마법 같은 방식으로 런타임 파싱되어 실행된다. 템플릿 텍스트 속의 대괄호 [NAME_VAR][AGE_VAR]은 단순 변수가 아니라, 모델이 비강제 텍스트 생성을 채워 넣어야만 하는 블랙홀 같은 ’동적 템플릿 구멍(Dynamic Holes)’을 상징한다.

2. WHERE 절의 위대한 실시간 로짓 마스킹 (Runtime Logit Masking)

전통적인 SQL 세계에서의 WHERE 구문은 이미 하드디스크에 물리적으로 존재하는 수만 개의 정적인 테이블 행(Row) 데이터를 메모리 위에서 ’사후 필터링(Post-filtering)’하는 역할을 수행한다. 하지만 아직 단 한 글자도 존재하지 않는 빈 캔버스에서 1밀리초 단위로 텍스트 분기 우주를 실시간으로 창조해 내는 오토레그레시브(Autoregressive) LLM에게 전통적 의미의 사후 필터링이란 무의미한 연산 낭비다.

따라서 LMQLWHERE 절은 필터가 아니라, 모델의 운전대를 강제로 틀어잡는 **‘실시간 조향 장치(Runtime Steering Wheel)’**로 작동한다.
오픈소스 모델이 GPU 메모리 위에서 Patient_Name: 직후의 [NAME_VAR] 토큰 배열을 생성(Decoding)하기 시작할 때, LMQL 컴파일러 엔진은 C++ 레벨에서 매 토큰 출력을 훔쳐보다가(Intercept) 응답 문자열의 누적 길이가 15바이트를 넘어가는 순간, 모델이 뱉고 싶어 하는 다음 텍스트 토큰들의 확률(Probability) 값을 강제로 마이너스 무한대(-inf)로 조작해 버린다(Logit Masking).

마찬가지로 두 번째 구멍인 [AGE_VAR] 변수를 모델이 생성할 차례가 오면, type(AGE_VAR) is int라는 무자비한 타입 제약이 걸려 있으므로 엔진은 알파벳이나 특수기호 텍스트 토큰의 생성을 Cuda 레벨에서 원천적으로 차단하고, 확률 공간을 비틀어 오직 0부터 9까지의 ‘숫자 토큰(Digit Tensor)’ 단 하나만을 토해내도록 로짓(Logit) 스페이스를 폭력적으로 조작(Bias)한다. 이것이 파서 붕괴가 시스템적으로 불가능한 이유다.

3. 다단계 추론(Multi-step Reasoning)의 구조적 결박 강점

단일한 JSON 객체 트리를 한 번에 통째로 우겨서 찍어내는 데 특화되어 있는 Pydantic 변환 기반의 타 도구(Outlines, Instructor, Pydantic-AI)들과 달리, LMQL은 복잡한 논리적 절차(CoT, Chain-of-Thought)를 긴 호흡으로 거치며 다중 턴에 걸쳐 순차적으로 변수를 결정하고 매핑해야 하는 에이전트 다단계 통제 시나리오에서 압도적이고 치명적인 아키텍처 강점을 가진다.

argmax
    "Q: 사과가 바구니에 3개 있는데 꼬마가 2개를 몰래 먹었다. 몇 개 남았는가?"
    "단계별 논리적 추론 과정: [REASONING_CHAIN]"
    "결론적으로, 위 상황에서 최종 정답 상태는 다음과 같은 범주 중 하나다: [FINAL_ANSWER]"
where
    STOPS_AT(REASONING_CHAIN, "결론적으로") and \
    FINAL_ANSWER in ["1개 남음", "모두 사라짐", "계산 불가"]

위 쿼리 파이프라인과 같이, 먼저 텅 빈 [REASONING_CHAIN] 변수에 모델의 뛰어난 논리적 추론력이 충분히 전개되도록 족쇄를 풀고 자유공간을 내어준 뒤(자연어 CoT 생성 허용), 그 논리 추론이 “결론적으로“라는 중단 시퀀스 트리거 단어에 닿자마자 칼같이 생성을 끊어버린다.
그리고 이어지는 가장 치명적인 최종 메타 오라클의 결괏값 도출부인 [FINAL_ANSWER] 구멍에서는 파이썬의 in 연산자를 활용하여, 오직 맵핑 포맷에 미리 정해진 Enum 카테고리 텍스트 배열 값 중 정확히 단 하나만을 무조건적으로 찍어내도록 데이터베이스 쿼리의 감옥에 완벽히 결박(Bind)해 버릴 수 있다.

이처럼 LMQL은, 우리에게 가장 익숙한 정형화된 소프트웨어 데이터베이스 쿼리(SQL/Python)의 우아한 제어력과 거대한 AI의 빙과 같은 비정형 텍스트 생성 능력을 문법적, 아키텍처적으로 가장 완벽하고 결점 없이 봉합(Seam)해 낸 현존하는 가장 위대한 스키마 제어 무기 프레임워크 중 하나다.