추측은 그만: Node.js에서 LLM으로부터 보장된 구조화된 출력(Structured Output) 받기
요약
LLM의 구조화된 출력(Structured Output)을 처리할 때 발생하는 파싱 오류 문제를 해결하기 위해, 각 API 제공업체별 보장 수준의 차이를 분석합니다. OpenAI, Groq, Ollama, Anthropic의 작동 방식을 비교하여 더 안정적인 백엔드 아키텍처 설계 방법을 제시합니다.
핵심 포인트
- OpenAI와 Groq는 서버 측 스키마 강제를 통해 네이티브한 출력을 보장함
- Ollama는 토큰 샘플링 단계에서 GBNF 문법 제약을 적용해 완벽한 형식을 보장함
- Anthropic은 현재 프롬프트와 파싱에 의존하는 Best-effort 방식임
- 제공업체별 보장 수준을 이해하면 재시도 로직과 에러 핸들링을 최적화할 수 있음
LLM JSON 파싱을 중단하세요. 백엔드가 이미 더 잘 알고 있습니다.
LLM을 사용하여 무언가를 만들어 보았다면, 아마 다음과 같은 코드를 작성해 보았을 것입니다:
const raw = await openai.chat.completions.create({ ... });
let parsed;
...
그다음 재시도 로직(retry logic)을 추가했습니다.
그다음 모델이 JSON을 Markdown 코드 펜스(code fences)로 감싸기 시작했습니다.
그다음 모델이 마지막에 쉼표(trailing comma)를 추가했습니다.
그다음 또 다른 정규 표현식(regex)을 추가했습니다.
결국 당신의 구조화된 출력(structured output) 파이프라인은 당신이 만들려고 했던 기능보다 더 복잡해졌습니다.
흥미로운 점은 다음과 같습니다:
이것은 사실 LLM의 문제가 아닙니다.
이것은 백엔드 역량 (backend capability) 의 문제입니다.
모든 구조화된 출력이 동일한 것은 아닙니다
대부분의 라이브러리는 어떤 모델을 사용하든 상관없이 단일한 "구조화된 출력 (structured output)" API를 노출합니다.
하지만 내부적으로 해당 제공업체들은 매우 다르게 작동합니다.
OpenAI
OpenAI는 서버 측 스키마 강제 (server-side schema enforcement) 를 수행합니다.
만약 모델이 스키마를 충족하지 못하면, API는 잘못된 형식의 JSON을 반환하지 않습니다.
보장: 네이티브 (Native).
Groq
Groq도 유사한 네이티브 구조화된 출력 메커니즘을 제공합니다.
마찬가지로, 응답이 애플리케이션에 도달하기 전에 검증이 이루어집니다.
보장: 네이티브 (Native).
Ollama
여기서부터 흥미로워집니다.
Ollama는 GBNF 문법 제약 (GBNF grammar constraints) 을 지원합니다.
생성 후에 JSON을 검증하는 대신, 문법이 토큰 샘플링 (token sampling) 중에 적용됩니다.
즉, 모델은 말 그대로 당신의 스키마를 위반하는 토큰을 내보낼 수 없습니다.
파싱할 잘못된 JSON이 존재하지 않습니다.
형식 오류로 인한 재시도 루프(retry loop)도 없습니다.
JSON.parse() 위험도 없습니다.
로컬 모델의 경우, 이것은 아마도 현재 사용 가능한 가장 강력한 구조화된 출력 보장 방식일 것입니다.
Anthropic
Anthropic은 현재 그에 상응하는 강제 메커니즘이 없습니다.
일반적인 접근 방식은 다음과 같습니다:
- 신중하게 프롬프트 작성
- 응답 파싱
- 검증
- 필요한 경우 재시도
잘 작동하지만, 여전히 최선의 노력 (best effort) 일 뿐, 보장되는 것은 아닙니다.
이것이 중요한 이유
많은 라이브러리가 모든 제공업체를 정확히 동일한 API 뒤로 추상화합니다.
그것은 편리하게 들립니다.
하지만 이는 중요한 아키텍처적 차이점을 숨기기도 합니다.
모든 제공업체를 동일하게 취급한다는 것은 다음 중 하나를 의미합니다:
- 필요 이상으로 출력값을 신뢰하거나,
- 백엔드가 이미 제공하고 있는 보장(guarantees)을 무시하거나.
보장 수준(guarantee level)을 아는 것은 더 나은 시스템을 설계할 수 있게 해줍니다.
예를 들어:
- 재시도(retries) 횟수 감소
- 더 단순한 파싱 (parsing)
- 더 나은 관찰 가능성 (observability)
- 더 예측 가능한 운영 환경(production) 동작
- 더 깔끔한 에러 핸들링 (error handling)
실질적인 예시
다음은 Zod를 사용하여 Ollama로 구조화된 출력 (structured output)을 구현한 예시입니다.
import { generate, ollama } from "@aviasole/shapecraft";
import { z } from "zod";
...
반환된 객체는 타입이 완전히 지정되어 있으며, Ollama의 경우 생성 과정에서 GBNF 문법(grammar)을 통해 스키마(schema)가 강제됩니다.
서로 다른 백엔드, 서로 다른 보장 수준
구조화된 출력을 보장 수준 (guarantee levels) 관점에서 생각하면 제공업체의 동작을 훨씬 더 쉽게 추론할 수 있습니다.
| 백엔드 | 보장 수준 | 강제 방식 |
|---|---|---|
| OpenAI | 네이티브 (Native) | 서버 측 스키마 검증 (Server-side schema validation) |
| ... |
API 인터페이스는 동일해 보일 수 있습니다.
하지만 신뢰성 특성은 그렇지 않습니다.
보장 수준을 중심으로 구축하기
저는 이러한 차이점을 숨기기보다 명시적으로 드러내는 실험을 해왔습니다.
제가 관리하는 라이브러리에서는 모든 생성(generation) 결과가 파싱된 결과와 함께 보장 수준을 반환합니다.
const result = await generate(model, schema, prompt);
result.guaranteeLevel;
...
모든 백엔드가 동일하게 동작하는 척하는 대신, 애플리케이션은 사용 가능한 실제 보장 수준을 바탕으로 정보에 입각한 결정을 내릴 수 있습니다.
마치며
구조화된 출력은 단순히 유효한 JSON을 받는 것에 관한 것이 아닙니다.
그 JSON이 어떻게 유효해졌는지를 이해하는 것에 관한 것입니다.
다음 사이에는 큰 차이가 있습니다:
- "모델이 우연히 프롬프트를 따랐을 때",
- "서버가 스키마를 강제했을 때", 그리고
- "모델이 물리적으로 유효하지 않은 토큰을 생성할 수 없었을 때".
운영 환경(production) 시스템을 구축하게 되면 이러한 차이점들이 매우 중요해집니다.
공개 사항 (Disclosure): 저는 모든 백엔드를 동일하게 취급하는 대신 OpenAI, Groq, Ollama, Anthropic 전반에 걸쳐 구조화된 출력(Structured Output) 보장 수준을 노출하는 오픈 소스 라이브러리인 ShapeCraft의 메인테이너(maintainer) 중 한 명입니다.
- GitHub: https://github.com/aviasoletechnologies/shapecraft
- npm: https://www.npmjs.com/package/@aviasole/shapecraft
여러분은 운영 환경(production)에서 구조화된 출력의 신뢰성을 어떻게 처리하고 계신지 의견을 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기