JSON 파싱 에러를 끝내는 오픈소스 'Outlines' 도입

LLM에게 JSON 형태로 출력해달라고 요청했지만, 결과물 끝에 붙은 콤마 하나나 불필요한 설명 문구 때문에 파싱 에러가 발생해 전체 데이터 파이프라인이 멈췄던 경험이 있을 것이다. 많은 실무자가 프롬프트를 정교하게 다듬거나 여러 번 재시도하는 운에 맡기는 방식으로 대응해왔지만, 이는 대규모 시스템에서 신뢰할 수 있는 방법이 아니다. 특히 API 페이로드(전송 데이터 묶음)를 생성하거나 데이터베이스를 업데이트하는 작업에서 단 한 번의 문법 오류는 시스템 전체의 크래시로 이어진다. 이러한 불안정한 정형 출력 문제를 해결하기 위해 등장한 것이 오픈소스 라이브러리 `outlines`다. 이 도구는 모델이 텍스트를 생성하는 추론 단계에서 문법적으로 틀린 토큰을 미리 차단해 구조적 출력을 보장한다.

outlines는 LLM이 생성하는 텍스트의 무작위성을 제어해 출력 결과가 규칙에 따라 고정되도록 만든다. 기존 방식은 모델이 텍스트를 모두 생성한 뒤 후처리 코드로 수정하거나 에러 시 재생성을 요청하는 식이었다. 하지만 outlines는 모델이 다음 단어를 선택하는 순간부터 정해진 규칙을 따르도록 강제한다. 이는 단순히 텍스트를 필터링하는 것이 아니라 생성 경로 자체를 제어하는 것이다. 결과적으로 모델이 자유롭게 말하며 발생하는 환각(사실이 아닌 것을 사실처럼 말하는 현상)을 억제하고, 개발자가 설계한 정확한 데이터 스키마에 맞는 결과물만 출력하게 만든다.

실무 워크플로에 이를 적용하기 위해서는 먼저 필요한 라이브러리를 설치해야 한다. 사전 학습된 모델을 효율적으로 불러오고 관리하기 위해 transformers 라이브러리와 함께 설치하는 것이 일반적이다.

bash
pip install outlines transformers

명령어를 통해 설치가 완료되면 Python 환경에서 LLM의 출력을 제어할 준비가 끝난다. 별도의 복잡한 설정 없이 기존의 Hugging Face 생태계 모델들을 그대로 활용할 수 있다는 점이 도입 장벽을 낮춘다.

모델 로드는 `outlines.from_transformers()` 함수를 통해 수행한다. 이 함수는 내부적으로 Hugging Face의 auto 클래스를 사용하여 모델과 토크나이저(텍스트를 모델이 이해하는 단위로 나누는 도구)를 자동으로 연결한다. outlines는 로드된 모델과 토크나이저를 하나의 전용 객체로 감싸서 관리하며, 이 객체는 추론 단계에서 모델이 준수해야 할 출력 범위나 형식을 정의한 제약 조건을 전달받는 인터페이스 역할을 한다.

사후 수정이 아닌 '토큰 마스킹'으로 구현하는 결정론적 출력

Outlines는 이미 생성된 텍스트를 사후에 수정하는 것이 아니라, 추론 단계에서 문법적으로 틀린 토큰을 미리 차단하는 방식을 취한다. 작동 원리는 추론 레벨에서 문법적으로 불법인(syntactically illegal) 토큰을 마스킹(masking, 특정 값을 가려 제외함) 처리하는 것에 있다. LLM은 다음 토큰을 생성할 때 모든 단어 후보군에 대해 확률 분포를 계산하는데, Outlines는 여기서 정해진 규칙에 어긋나는 토큰의 확률을 강제로 0으로 만든다.

이를 구현하기 위해 내부적으로 유한 상태 기계(Finite State Machine, 정해진 상태 사이를 이동하며 동작하는 논리 구조)를 구축한다. 현재 생성 중인 텍스트가 어떤 상태에 있는지 추적하고, 다음에 올 수 있는 유효한 토큰 집합만을 실시간으로 계산하여 모델의 샘플링 과정에 개입한다. 모델의 가중치를 수정하는 것이 아니라, 출력되는 문을 좁히는 제어 장치를 추가한 셈이다.

이러한 제어 방식은 토큰이 선택되는 찰나의 순간부터 규칙 위반이 원천적으로 불가능하도록 설계되었으므로, 출력 형식이 깨지는 일이 사실상 발생하지 않는다. 모델이 인간처럼 자연스럽게 대화하려는 특성 때문에 임의로 내용을 덧붙이거나 JSON 문법을 파괴하려는 시도 자체가 토큰 선택 단계에서 물리적으로 차단된다. 결과적으로 모델은 정해진 스키마라는 좁은 길만을 따라 텍스트를 생성하게 된다.

실무 관점에서 이 기법은 정형 데이터를 얻기 위해 수십 번 프롬프트를 수정하며 테스트하는 소모적인 반복 작업을 없애준다. 또한 파이썬의 JSON 파서를 중단시키는 콤마나 불필요한 문자들을 제거하기 위한 별도의 정규식이나 후처리 코드를 작성할 필요가 없다. 즉시 시스템에 투입 가능한 정형 데이터를 확보함으로써 개발 효율을 높이는 것이 이 워크플로의 핵심이다.

단순 선택지부터 복잡한 JSON 스키마까지 제어하는 법

Outlines는 `generate.choice()` 함수를 통해 출력값을 완전히 제어한다. 이 함수는 모델이 미리 정의된 리터럴(Literal, 고정된 값) 중 하나만 선택하도록 강제한다. 파이썬의 `typing.Literal` 객체를 활용해 출력 제약 조건을 설정하면 모델은 지정된 선택지 외의 토큰을 생성하는 경로를 원천적으로 차단당한다. 고객 지원 티켓을 분석하는 파이프라인에서 승인된 카테고리 리스트 중 정확히 하나만 골라내야 하는 분류 작업에 이 방식을 적용하면 오답률을 0으로 줄일 수 있다.

단순한 선택지를 넘어 복잡한 데이터 구조가 필요할 때는 Pydantic(데이터 검증 및 설정 관리 라이브러리) 객체를 사용해 스키마를 정의한다. 예를 들어 가상 캐릭터의 이름, 설명, 나이를 포함하는 JSON 구조를 만들고 싶다면 Pydantic 클래스로 각 필드의 타입과 제약 조건을 먼저 설계한다. 이렇게 정의된 Pydantic 클래스를 모델에 전달하면 Outlines는 추론 단계에서 해당 구조를 엄격하게 준수하도록 유도한다. 모델은 정의된 필드 이름과 데이터 타입을 정확히 따르며 JSON 형식을 생성하며, 이는 개발자가 원하는 데이터 규격을 시스템적으로 강제하는 방식이다.

Pydantic 기반의 스키마를 적용하면 모델이 처음부터 문법적으로 완벽한 JSON만 생성하므로, 기존처럼 정규표현식으로 JSON 부분을 추출하거나 잘못된 콤마를 제거하는 후처리 과정이 필요 없다. JSON 파서를 중단시키는 불필요한 trailing characters(문장 끝에 붙는 콤마 등)가 생성 단계에서부터 차단되기 때문이다.

실무자는 이제 프롬프트에 JSON 형식을 상세히 적고 결과가 나올 때까지 반복 수정하는 대신, Pydantic 클래스로 데이터 구조를 명확히 정의하고 이를 모델에 바인딩하는 워크플로를 선택할 수 있다. 후처리 코드 없이 즉시 사용 가능한 정형 데이터를 확보함으로써 전체 시스템의 유지보수 공수를 획기적으로 줄일 수 있다.

프롬프트 엔지니어링의 '운'을 제거하는 실무적 가치

표준 LLM은 정형 데이터를 생성할 때 문장 끝에 불필요한 trailing characters를 남기는 경우가 빈번하며, 이 작은 문법 오류는 JSON 파서를 즉시 중단시켜 연결된 전체 데이터 파이프라인을 멈추게 만든다. Outlines는 프롬프트 작성 기술이나 운에 의존하지 않는 결정론적 확실성(deterministic certainty)을 확보해 준다. 기존의 프롬프트 엔지니어링이 모델에게 형식을 지켜달라고 요청하는 방식이었다면, 이 라이브러리는 출력 단계에서 문법적 규칙을 물리적으로 강제한다.

특히 데이터베이스 업데이트를 위한 API 페이로드(payload, 전송 데이터 묶음)를 생성하는 환경에서 이러한 신뢰도는 시스템 안정성과 직결된다. API 서버는 엄격한 스키마를 요구하므로 단 하나의 오타나 잘못된 괄호 배치만으로도 데이터 업데이트가 실패한다. Outlines를 통해 생성된 데이터는 정의된 구조를 완벽히 준수하므로 별도의 후처리 검증 코드 없이 즉시 데이터베이스에 입력할 수 있다. 이는 AI 모델을 단순한 채팅 인터페이스가 아니라 신뢰할 수 있는 데이터 엔지니어링 부품으로 통합하는 기준이 된다.

LLM은 기본적으로 사람처럼 대화하도록 학습되었기에 문법 파괴나 환각 현상이 빈번하다. Outlines는 추론 단계에서 이러한 대화형 특성을 억제하고 오직 정의된 토큰만 생성하도록 제어하여 불필요한 텍스트 없이 정의된 데이터만 출력한다. 결과적으로 개발자는 정규 표현식으로 텍스트를 깎아내거나 `try-except` 문으로 파싱 에러를 잡는 수고를 덜 수 있다.

실무자의 경쟁력은 프롬프트를 정교하게 깎는 기술이 아니라, 후처리 코드 없이 즉시 사용 가능한 정형 데이터를 확보하는 설계 능력에서 결정된다. 프롬프트 튜닝에 쏟던 에너지를 명확한 타입 정의와 스키마 설계로 옮기는 것만으로도 데이터 파이프라인의 안정성은 크게 향상된다.

LLM에게 JSON 출력을 요청하고 콤마 하나 때문에 파싱 에러를 겪으며 프롬프트를 수정하던 시간은 이제 낭비에 가깝다. Outlines의 `generate.choice()`와 Pydantic 클래스를 활용하면 모델이 생성할 토큰의 범위를 물리적으로 제한해 구조적 결함을 원천 차단할 수 있다.