JSON이 중간에 잘렸을 때 '수리'하면, 잘린 사실 자체를 인지하지 못하게 된다 - 853가지 절단 방식으로 측정
요약
LLM의 출력이 중간에 잘렸을 때, `json-repair` 같은 복구 라이브러리를 사용하면 에러 없이 '수리'되지만, 그 과정에서 데이터가 실제로 잘렸다는 사실 자체가 인지되지 않는 문제가 발생합니다. 본 글은 853가지 절단 방식을 시뮬레이션하여 이 문제를 측정하고, 스키마 검증의 중요성을 강조하며 해결책을 제시합니다.
핵심 포인트
- LLM 출력이 중간에 끊겨도 HTTP 상태 코드는 200으로 반환되어 오류 인지가 어렵다.
- 복구 라이브러리는 JSON 형식을 복원하는 것이 주 역할이며, 잘린 흔적 자체를 지워버린다.
- 스키마 검증을 추가해도 일부(16.1%)의 '조용히 고장 난' 데이터가 통과할 수 있다.
- API 호출 시 출력이 중단된 이유나 상태 정보를 반드시 확인해야 가장 확실하다.
LLM의 API는 출력이 상한(max_tokens)에 도달하여 중간에 멈추더라도 HTTP 상태 코드는 200으로 반환됩니다. 에러가 아니기 때문에, JSON이 중간에 잘려 있어도 호출한 쪽은 그대로 다음 단계로 진행하기 쉽습니다.
그래서 json-repair와 같은 복구 라이브러리를 끼워 넣는 사람들도 많을 것입니다. 닫히지 않은 괄호를 보충하여 읽을 수 있는 JSON으로 만들어주는 편리한 도구입니다.
하지만 중간에 잘린 JSON을 이것으로 '수리'하면, 잘렸다는 사실 자체가 눈에 띄지 않게 됩니다. 당연하게 생각할 수도 있지만, 실제로 어느 정도의 비율로 발생하는지는 알지 못했습니다. 그래서 절단 방식을 모두 시도하여 세어보았습니다.
우선 결과를 말씀드리자면 다음과 같습니다.
- 복구 라이브러리에 통과시키면 거의 전부(98.5%)가 '그럴듯한 JSON'이 되었습니다.
- 항목을 모두 필수로 지정한 스키마로 검증해도, 853가지 중 137가지(16.1%)는 빠져나갔습니다.
- JSON의 끝에 '작성을 완료했다'는 표시를 하나 두면, 빠져나가는 경우는 0이었습니다.
저는 전문 프로그래머가 아니며, AI(Claude)의 도움을 받으면서 고장 난 JSON을 수리하는 API를 만들고 있습니다. 이번 실험 역시 AI와 함께 코드를 작성하고 실행한 것입니다.
LLM이 반환할 법한 올바른 JSON 5개를 준비했습니다.
| 샘플 | 내용 | 문자 수 |
|---|---|---|
| 주문 추출 | 주문 번호・고객명・상품 3개・합계・비고 | 233 |
| ... | ||
| 이것을 첫 글자부터 마지막 글자 직전까지, 한 글자씩 이동시키며 잘라냅니다. 5개를 합쳐 853가지의 '잘린 JSON'이 만들어집니다. |
각각을 다음 세 가지에 통과시켰습니다.
- A: 그대로
json.loads - B:
json-repair(0.63.5)로 복구 - C: B에서 수리한 것을 JSON Schema로 검증(항목은 모두 필수. 타입・범위・형식도 지정)
여기서 '조용히 고장 난' 상태라 부르는 것은, 에러가 나지 않고 값이 반환되었지만 내용이 원래의 JSON과 다른 경우입니다.
| 샘플 | 절단 방식 | A 읽힘 여부 | B 복구로 조용히 고장 남 | C 검증도 빠져나감 |
|---|---|---|---|---|
| 주문 추출 | 232 | 0 | 230(99.1%) | 11(4.7%) |
json.loads는 853가지 모두에서 에러가 발생했습니다. 이것은 나쁜 것은 아닙니다. 에러가 발생하면 적어도 '뭔가 이상하다'는 것을 인지할 수 있기 때문입니다.
복구 라이브러리의 경우, 98.5%에서 어떤 JSON을 반환했습니다. 라이브러리가 나쁜 것이 아니라, 괄호를 닫아 읽을 수 있는 형태로 만드는 것이 본래의 역할이기 때문입니다. 다만, 그 작업 덕분에 잘린 흔적이 깔끔하게 사라져 버립니다.
스키마 검증까지 추가하면 상당히 멈추게 됩니다. 그래도 137가지가 통과해 버렸습니다.
빠져나간 것을 조사해보니, 고장 난 방식은 두 종류밖에 없었습니다(하나의 절단 방식에 둘 다 포함되는 경우도 있습니다).
| 고장 난 방식 | 건수 | 예시 |
|---|---|---|
| 배열 요소가 줄어듦 | 107 | 이유가 3건 있었는데 `[ |
import json
from json_repair import repair_json # pip install json-repair jsonschema
from jsonschema import Draft202012Validator
...
실행해보면 다음과 같이 나옵니다.
{"label": "spam", "confidence": 0.97, "reasons": ["差"]}
{"label": "spam", "confidence": 0.97, "reasons": ["差出"]}
{"label": "spam", "confidence": 0.97, "reasons": ["差出人"]}
...
가장 확실한 방법은 API가 반환하는 '출력이 중단된 이유'를 확인하는 것입니다. 이름은 서비스마다 다릅니다.
| 서비스 | 확인할 위치 | 중간에 잘렸을 때의 값 |
|---|---|---|
| Anthropic(Claude) | stop_reason | max_tokens |
| OpenAI(Chat Completions) | finish_reason | length |
| OpenAI(Responses API) | status 및 incomplete_details.reason | incomplete / max_output_tokens |
| Google(Gemini) | finishReason | MAX_TOKENS |
잘렸다면, 복구하려고 시도하기보다는 상한선을 높여서 다시 요청하는 것이 기본입니다.
그렇다고 하더라도 프레임워크를 거치다 보면 이 값이 잘 보이지 않을 때가 있습니다. Gemini의 경우, 상한에 도달했음에도 finishReason이 붙지 않는 버그 보고도 있었습니다(Google AI Developers Forum). 따라서 JSON 측에서도 보험을 들어두고 싶습니다.
중간에 잘릴 때는 반드시 뒤쪽부터 사라집니다. 이를 역이용합니다.
JSON의 마지막에 "_complete": true라는 항목을 두고, 스키마에서 'true가 아니면 불합격'으로 설정해 둡니다. 끝까지 작성되지 않았다면 이 항목은 존재하지 않거나, `
이 글은 AI(Claude)와 함께 작성했습니다. 코드는 AI가 직접 실행하고 결과를 확인했습니다.
이 글은 Zenn에도 게시되어 있습니다.
깨진 JSON, SQL, Mermaid를 자동으로 수리하는 API (FixMy 시리즈)를 RapidAPI에서 공개하고 있습니다. 관심 있으시면 한번 살펴보세요. → FixMyJSON
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기