LLM JSON 출력: 설정 하나가 유효한 JSON을 75%에서 4%로 떨어뜨리다
요약
LLM의 JSON 출력 신뢰성을 높이는 방법을 분석한 글입니다. 반복 방지 설정(`no_repeat_ngram_size`)을 사용하면 오히려 유효한 JSON 출력이 급격히 떨어지는 현상을 발견했습니다. 따라서 구조화된 호출 시에는 해당 설정을 끄고, 제공업체의 제약 디코딩 기능을 사용하는 것이 가장 효과적임을 강조합니다.
핵심 포인트
- 반복 방지 설정은 JSON 출력 신뢰도를 크게 저하시킬 수 있습니다.
- 구조화된 출력을 위해서는 `no_repeat_ngram_size`를 비활성화하세요.
- 제공업체의 제약 디코딩(Constrained Decoding) 기능을 최우선으로 사용해야 합니다.
- JSON 복구 도구를 활용하여 깨진 답변을 분석하고 수정할 수 있습니다.
원래 tensorust.com에 게시됨.
요약: 2026년 10월 17일, 저는 세 가지 소형 언어 모델(small language models)에게 실제 Chrome 탭에서, 제가 사용하는 브라우저 채팅 앱의 정확한 샘플링 설정을 사용하여 JSON을 432번 요청했습니다. Llama 3.2 1B는 일반적인 greedy decoding만으로도 스키마가 유효한 JSON을 **75%**의 확률로 반환했습니다. 하지만 반복 방지 설정 중 하나인 no_repeat_ngram_size: 3을 켜자, 그 비율은 **4%**로 떨어졌습니다. 모든 채팅 설정을 켰을 때는 24번 중 0번이었습니다. 모델 자체가 JSON 생성을 실패한 것이 아니었습니다. 마치 그것을 작성하는 것이 금지된 것 같았습니다.
검색에 대한 간단한 답변입니다. LLM에서 신뢰할 수 있는 JSON 출력을 얻으려면: 구조화된 호출(structured call)의 경우 no_repeat_ngram_size를 끄고 repetition_penalty는 1.0으로 유지하세요. 프롬프트에 정확한 스키마를 넣고, 유연하게 처리하는 과정(코드 울타리 제거, 산문에서 첫 {...} 잘라내기, 작은 구문 오류 수정)을 거친 후 스키마를 검증하십시오. 만약 제공업체가 제약 디코딩(constrained decoding)(OpenAI Structured Outputs, llama.cpp GBNF grammars, Outlines)을 제공한다면, 그것을 사용하세요. 그러면 이 기사의 대부분이 불필요해질 것입니다.
만약 지금 눈앞에 깨진 답변이 있다면, 무료 LLM JSON 복구 도구에 붙여넣으세요. 이 도구는 브라우저에서 이 벤치마크와 동일한 네 단계의 과정을 실행하여 어느 단계가 문제를 해결했는지 알려줍니다. 나머지 내용은 그 규칙들 뒤에 숨겨진 측정값이며, 모든 숫자는 10월 17일 오후 4시 12분부터 4시 22분 사이에 수행된 테스트를 기반으로 합니다.

One sampling knob at a time on top of greedy decoding. Transformers.js 4.2.0 on WebGPU in real Chrome, Oct 17 2026, 4:12 to 4:22 PM ET.
설정: 실제 브라우저에서 세 가지 모델을 사용한 432개의 답변
이 모델들은 BEINGS에서 가져왔는데, 이곳은 모든 AI 생명체가 방문자 자신의 브라우저 안에서 실행되는 채팅방입니다. 덕분에 추론 비용이 저에게는 전혀 들지 않습니다. 제가 어떻게 작동하는지Run an LLM in the browser: WebGPU did 63 tokens/s에 작성했습니다. 이 생명체들은 항상 한 줄의 채팅만 필요했습니다. 명확한 다음 기능은 이들이 구조화된 액션(감정, 목표, 답변)을 반환하도록 하는 것인데, 이는 JSON을 의미합니다. 이를 구축하기 전에, 저는 이 모델들이 실제로 얼마나 자주 이것을 생성할 수 있는지 알고 싶었습니다.
세 가지 모델을 Transformers.js 4.2.0과 WebGPU를 사용하여 4비트 가중치로, BEINGS가 로드하는 동일한 고정 빌드를 통해 사용했습니다:
세 가지 작업, 각 8개의 입력이 있었습니다: 채팅 메시지에 발화자, 네 가지 선택지 중 하나의 감정, 그리고 답변을 태그하기; 메시지에서 이름, 이메일, 전화번호를 추출하기; 쇼핑 메모를 정수 수량과 함께 항목 목록으로 변환하기. 모든 입력에는 알려진 정답이 있었기 때문에, 저는 세 가지 사항을 개별적으로 평가할 수 있었습니다: 파싱(parse)하는가, 스키마와 일치하는가, 그리고 정확한가.
스크립트(json_bench.mjs)는 이미 열려 있는 Chrome 창에 연결되어, BEINGS 사이트에 자체 탭을 열어 캐시된 모델들이 약 1초 만에 로드되게 한 다음, 모든 프롬프트를 두 가지 프롬프트 스타일과 두 가지 샘플링 설정으로 실행합니다. 이 두 가지 설정은 스크립트에서 복사했습니다:
const SAMPLING = {
chat: { do_sample: true, temperature: 0.75, top_p: 0.9, top_k: 40,
repetition_penalty: 1.12, no_repeat_ngram_size: 3 }, // = beings llm-worker.js
...
chat 라인은 테스트를 위해 제가 임의로 만든 것이 아닙니다. 오늘 라이브 BEINGS 워커가 보내는 실제 설정이며, 채팅방의 소형 모델들은 자기 반복을 좋아하기 때문에 이 두 가지 페널티가 표준적인 해결책입니다:
// public/llm-worker.js (BEINGS, live)
out = await pipe(msg.messages, {
max_new_tokens: msg.maxTokens,
...
'plain' 프롬프트는 문장으로 JSON을 설명합니다. 'example' 프롬프트는 스키마와 함께 하나의 입력-출력 쌍을 제공하고 코드 울타리(code fences)를 사용하지 않도록 지시합니다. 3개 모델 × 3가지 작업 × 8개 입력 × 2가지 프롬프트 × 2가지 설정은 총 288개의 응답으로, 각 응답은 최대 160개의 새 토큰으로 제한됩니다. 그런 다음 144개의 두 번째 실행에서 설정을 하나씩 변경했는데, 이것이 헤드라인의 출처가 되었습니다.
채팅 설정에서는 72개 중 0점을 기록했습니다
채팅 설정과 예시 프롬프트를 사용했을 때, 세 모델 중 어느 것도 스키마와 일치하는 응답을 하나도 만들지 못했습니다: 72개 중 0개. 그리디 디코딩(greedy decoding)과 동일한 프롬프트를 사용했을 때는 72개 중 25개가 일치했고, Llama만으로도 0개에서 24개 중 18개로 증가했습니다.
그래서 저는 채팅 설정을 분리하고 예시 프롬프트(각 막대 그래프당 24개 응답, 동부 표준시 오후 4:21~4:22) 위에서 한 번에 하나의 노브를 켜보았습니다:
| 그리디에 추가된 설정 | Llama 3.2 1B | SmolLM2 360M |
|---|---|---|
| 아무것도 아님 (greedy) | 24개 중 18개 (75%) | 24개 중 7개 (29%) |
| ... | ||
| Temperature는 여기서 무해했습니다. 심지어 Llama를 하나 올리기도 했습니다. 반복 페널티(repetition penalty)가 약간의 비용을 초래했습니다. n-gram 금지(n-gram ban)는 자체적으로 그것을 파괴했습니다. |
이유: no_repeat_ngram_size가 스키마에 필요한 토큰을 금지합니다
no_repeat_ngram_size: 3은 '이미 나타난 3개 토큰 시퀀스를 절대 생성하지 않음'을 의미합니다. Transformers.js에서 이 부분을 확인해 볼 수 있습니다 (logits_process.js, 간소화됨). 반복된 3-gram을 완성할 토큰은 점수(score)가 마이너스 무한대로 설정되어, 절대 선택될 수 없습니다:
_call(input_ids, logits) {
for (let i = 0; i < input_ids.length; ++i) {
const batch_logits_data = logits[i].data;
...
여기서 핵심은 input_ids가 무엇을 담고 있느냐입니다. generate 루프(modeling_utils.js)에서 이 변수는 전체 프롬프트로 시작하며, 새로운 토큰이 계속 추가됩니다:
const all_input_ids = input_ids.tolist();
// ...각 단계마다:
const next_tokens_scores = prepared_logits_processor(all_input_ids, logits);
이제 JSON에 대해 생각해 봅시다. 아이템 목록은 `{
" qty" 뒤에 공백이 있는 경우입니다. ".name"과 같이 점으로 시작하는 경우입니다. 작은따옴표로 감싼 'name'의 사용입니다. "qty" 대신 "quantity"를 사용하는 것입니다. "name" 대신 "type"을 사용하는 것입니다. 이 모든 것은 금지된 3-gram(세 글자)을 우회하기 위한 가장 짧은 방법들입니다. 일부는 여전히 JSON으로 파싱되는데, 이것이 위험한 부분입니다. 파서가 '예'라고 말하지만, 여러분의 코드는 item.qty를 undefined로 읽어 들일 수 있습니다.
모델 자체가 JSON을 처리하는 데 나쁜 것은 아니었습니다. 샘플러(sampler)가 스키마가 요구하는 정확한 토큰들을 금지하고 있었고, 프롬프트에 포함된 예시가 상황을 더욱 악화시켰습니다.
반복 페널티는 프롬프트도 계산합니다
repetition_penalty는 CTRL 논문 (Keskar et al., 2019)에서 도입된, 좀 더 온건한 버전입니다. 이 페널티(여기서는 1.12)로 이미 나타난 모든 토큰의 점수를 나눕니다. Transformers.js 소스 코드는 RepetitionPenaltyLogitsProcessor 위에 있는 주석에 이를 명확히 설명합니다: "대부분의 LLM과 같은 디코더 전용 모델의 경우, 고려되는 토큰에는 프롬프트가 포함됩니다."
따라서 여러분의 스키마에 있는 모든 따옴표, 콜론, 키 이름은 다시 나타날 확률이 약간 낮아집니다. 1.12라는 수치는 금지(ban)라기보다는 가벼운 경고(nudge)이며, 이는 데이터와 일치합니다: Llama는 24개 중 18에서 16으로 떨어졌습니다. 저는 여전히 구조화된 호출에는 이 값을 1.0으로 설정할 것입니다. 요청한 정확한 구두점(punctuation)에 페널티를 부여해서 얻을 수 있는 이득은 없습니다.
Hugging Face generation docs는 두 설정 모두 일반적인 품질 조절 장치로 설명하며, 채팅의 경우에도 마찬가지입니다. 제 추측으로는 많은 애플리케이션들이 BEINGS가 했던 것처럼 공유된 설정(shared config)에 이 값들을 한 번 설정하고, 모든 구조화된 호출이 이를 조용히 상속받는 것 같습니다. 오늘 코드베이스에서 검색해 볼 가치가 있습니다.
파싱 사다리가 43개의 답변을 더 구제하지만, 전부를 아닐 수도 있습니다
설정이 좋아도 작은 모델들은 JSON을 산문(prose)이나 코드 울타리 안에 감싸는 경향이 있습니다. JSON.parse는 이 모든 경우에 실패합니다. 따라서 분석은 모든 답변을 네 단계의 사다리를 따라 실행하며, 작동한 첫 번째 단계를 기록합니다:

Llama 3.2 1B, chat settings, plain prompt, Oct 17 2026 4:19 PM ET. Error messages are the real JSON.parse output at each step.
여기서 json_analyze.mjs에서 가져온 사다리(ladder)를 보여드립니다 (일부 생략). repair tool은 이 정확한 코드를 복사하여 실행합니다:
export function extractJson(t) { // first balanced {...} or [...], string-aware
const start = t.search(/[{[]/);
if (start < 0) return null;
...
{% endraw %}json fences
if (!v) { const e = extractJson(stripFences(raw)); if (e && e.closed) v = tryParse(e.text); } // 3. cut out {...}
if (!v) { const e = extractJson(stripFences(raw)); if (e) v = tryParse(repair(e.text)); } // 4. repair
return v;
}
{% raw %}
`repair()`는 다섯 가지 작은 작업을 수행합니다: `//` 주석 제거, 빈 키에 따옴표 추가, 단일 따옴표를 이중 따옴표로 교체(텍스트에 이중 따옴표가 없는 경우에만, 따라서
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기