대규모 언어 모델(LLM)은 때때로 JSON 출력을 생성할 때 오류를 발생시킬 수 있습니다. 일반적인 문제는 다음과 같습니다:
- 불필요한 설명 포함: "알겠습니다. 요청하신 결과입니다." 또는 "요청하신 JSON 형식입니다."와 같은 과도한 문구가 JSON 데이터 앞뒤에 포함될 수 있습니다.
- Markdown 태그 사용: JSON 출력이
```json태그로 묶여 있어 직접적인 파싱이 불가능합니다. - 잘못된 형식: 따옴표 대신 작은따옴표를 사용하거나, 이스케이프 문자를 잘못 사용하는 등의 문법 오류가 있을 수 있습니다.
- 구문 오류: JSON 구문이 잘못되어 닫는 대괄호가 누락되거나, 필드 뒤에 불필요한 쉼표가 추가되는 경우가 있습니다.
이러한 형식에 맞지 않는 JSON 출력은 json.load() 함수로 올바르게 파싱할 수 없어 비즈니스 코드에서 오류를 발생시킵니다.
이 문제를 해결하기 위해 LLM 호출 전, 중, 후의 세 단계에 걸쳐 적절한 조치를 취해야 합니다. 이를 통해 LLM이 안정적이고 깔끔한 JSON을 출력하도록 보장할 수 있습니다.
여기서는 '사용자 입력이 규정을 위반하는지 여부, 위반 유형은 무엇인지, 그리고 위반 단어는 무엇인지 판단하는' 요구 사항을 예시로 들어 구체적인 처리 방법을 자세히 설명합니다.
1단계: 사전 안내 - 프롬프트 최적화
프롬프트 최적화는 가장 기본적이고 일반적인 해결책입니다. 명확하고 엄격한 지침을 통해 LLM이 요구 사항에 맞는 JSON을 출력하도록 유도하는 것이 핵심입니다. 이는 '소프트 제약'으로 간주되며, 모델의 지침 이해 및 실행 능력에 의존하므로 근본적으로 형식을 강제하지는 못합니다. 하지만 사용하기 쉽고 모든 LLM에 적용 가능하다는 장점이 있습니다.
일반적인 최적화 방법은 다음과 같습니다:
- 필드 요구 사항 명확화: 단순히 JSON 출력을 요구하는 대신, 각 필드의 예상 데이터 유형과 값 범위를 상세히 설명합니다.
- Few-Shot 예시 제공: LLM의 컨텍스트 학습 능력을 활용하여 입력-출력 쌍 예시를 제공하여 형식을 직관적으로 이해하도록 돕습니다.
- 검증 지침 추가: LLM이 JSON 구문 및 내용의 유효성을 자동으로 검증하고, 불필요한 내용이나 오류가 없는지 확인하도록 요청합니다.
프롬프트 템플릿 예시는 다음과 같습니다:
# 역할
당신은 엄격한 사용자 입력 위반 판단 도우미입니다. 사용자 입력 내용을 바탕으로 위반 여부를 판정하고, 미리 정의된 규칙에 따라 엄격하게 규정을 준수하는 JSON 결과를 출력해야 합니다.
# 기술
- **핵심 위반 유형**: 사용자 입력에 '음란', '폭력', '모욕' 관련 단어가 포함되어 있거나, 불법적이거나 광고성 프로모션과 같은 명백한 위반 내용이 있는 경우 위반으로 간주합니다. 그렇지 않은 경우 위반이 아닙니다.
- **위반 단어 추출**: 사용자 입력에 명확하게 나타나는 핵심 위반 단어만 추출합니다. 추가하거나 추론하지 않습니다.
# 출력 형식
**엄격하게 다음 JSON 형식에 따라 생성합니다**:
{
"is_illegal": <boolean>,
"illegal_type": <string>,
"illegal_words": <list>
}
# 제약 조건
- **고유한 출력**: 규칙에 맞는 JSON만 출력하며, JSON 이외의 내용은 절대 포함하지 않습니다. JSON의 키 이름은 변경할 수 없으며, 추가하거나 누락해서는 안 됩니다.
- **출력 내용**:
- `is_illegal`: 소문자 `true` 또는 `false`로 표시합니다.
- `illegal_type`: `"음란"`, `"폭력"`, `"모욕"`, `"기타"`, `""` 값만 허용합니다.
- `illegal_words`: 위반 단어가 없으면 `[]`로 표시하고, 있으면 원본 입력 단어 목록을 포함합니다.
- **경계 규칙**: 사용자가 명확하게 입력한 핵심 위반 단어만 추출하며, 단어를 분리하거나 확장하거나 추론하지 않습니다.
# 핵심 요구 사항
- 위의 규칙을 엄격하게 준수하고, 출력 전 JSON 구문 및 내용의 유효성을 자동으로 검증하여 중복이나 오류가 없는지 확인합니다.
# 예시
## 예시 1 입력: 오늘 날씨가 좋아서 공원 산책하기 좋습니다.
## 예시 1 출력:
{
"is_illegal": false,
"illegal_type": "",
"illegal_words": []
}
## 예시 2 입력: 이 멍청아, 저리 꺼져!
## 예시 2 출력:
{
"is_illegal": true,
"illegal_type": "모욕",
"illegal_words": ["멍청아", "꺼져"]
}
## 예시 3 입력: 이 제품은 모든 질병을 치료할 수 있으며, 링크를 클릭하면 20% 할인됩니다!
## 예시 3 출력:
{
"is_illegal": true,
"illegal_type": "기타",
"illegal_words": ["모든 질병을 치료할 수 있으며", "링크를 클릭하면"]
}
2단계: 중간 제어 - 코드 레벨 제약
프롬프트 최적화는 '소프트 제약'으로 한계가 있습니다. 아무리 명확한 지침이라도 LLM은 환각이나 무작위성으로 인해 형식 오류를 일으킬 수 있습니다. 이를 해결하기 위해 주요 LLM 제공업체는 모델 생성 과정에서 규정을 준수하는 JSON 출력을 강제하는 기본 '하드 제약' 기능을 제공합니다.
기본 원리는 간단합니다. 시스템은 JSON 규칙을 상태 기계로 변환하여, 각 토큰 생성 전에 불법적인 내용을 필터링하고 합법적인 토큰만 확률 계산에 참여하도록 하여, 출력물이 JSON 규격 및 필드 요구 사항을 완벽하게 준수하도록 보장합니다. (예: 콜론 생성 후 다음 토큰은 절대 쉼표가 될 수 없습니다).
주의: 모든 LLM이 이러한 하드 제약 기능을 지원하는 것은 아닙니다. 대부분의 경량 모델이나 덜 알려진 모델은 아직 지원하지 않으며, OpenAI 시리즈, Anthropic Claude 등 주요 모델만 지원합니다.
2.1 JSON 모드: 기본 형식 강제
JSON 모드는 기본적인 하드 제약 기능입니다. API 매개변수를 통해 모델의 출력 인코딩 로직을 제어하여, 규정을 준수하는 JSON 형식의 텍스트를 강제로 생성합니다. 이를 통해 불필요한 설명이나 코드 블록 감싸기 문제를 원천적으로 방지할 수 있습니다.
핵심 작업: API 호출 시 response_format={"type": "json_object"} 매개변수를 추가합니다. 프롬프트에는 "JSON"이라는 용어를 명시해야 합니다. 그렇지 않으면 모델 오류가 발생할 수 있습니다.
주의: JSON 모드는 출력 형식이 합법적인 JSON임을 보장하지만, API에서 특정 스키마를 지정하는 것은 지원하지 않으므로 필드 규칙을 고정할 수 없습니다. 필드 누락이나 키 이름 오류 문제는 여전히 발생할 수 있습니다.
다음은 코드 예시입니다 (OpenAI SDK 기준):
import json
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.openai.com/v1"
)
MODEL = "gpt-4o-mini"
if __name__ == "__main__":
system_prompt = """
# 역할
당신은 엄격한 사용자 입력 위반 판단 도우미입니다. ... (이전 프롬프트 내용) ...
"""
user_prompt = "정말 돼지 같네"
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}]
response = client.chat.completions.create(
model=MODEL,
messages=messages,
response_format={"type": "json_object"}
)
result = json.loads(response.choices[0].message.content)
print(result)
2.2 구조화된 출력: 형식 및 필드 동시 강제
JSON 모드는 JSON 형식을 보장하지만, 필드의 정확성이나 완전성을 보장하지는 못합니다. 이 문제를 해결하기 위해 구조화된 출력(Structured Outputs) 기능이 등장했습니다. 이는 전체 JSON 스키마를 정의하여 지정된 필드와 유형을 강제로 출력하도록 함으로써, 키 이름 오류 및 필드 누락 문제를 완전히 제거합니다.
핵심 작업: API 호출 시 response_format에 구체적인 JSON 스키마를 전달하여 형식과 필드 모두에 대한 하드 제약을 구현합니다. 이를 통해 모델 출력물이 미리 정의된 필드 규칙과 완벽하게 일치하도록 하여, 규정 준수율을 100%로 높일 수 있습니다. 이는 공식적으로 권장되는 구조화된 출력 솔루션입니다.
다음은 코드 예시입니다 (OpenAI SDK 기준):
import json
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.openai.com/v1"
)
MODEL = "gpt-4o-mini"
if __name__ == "__main__":
system_prompt = """
# 역할
당신은 엄격한 사용자 입력 위반 판단 도우미입니다. ... (이전 프롬프트 내용) ...
"""
user_prompt = "정말 돼지 같네"
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}]
response = client.chat.completions.create(
model=MODEL,
messages=messages,
response_format={
"type": "json_schema",
"json_schema": {
"name": "illegal_judge_result",
"strict": True, # 스키마를 엄격히 준수하며 필드 누락, 타입 오류 방지
"schema": {
"type": "object",
"properties": {
"is_illegal": {
"type": "boolean",
"description": "위반 여부, true/false(소문자)만 허용"
},
"illegal_type": {
"type": "string",
"description": "위반 유형, '음란'/'폭력'/'모욕'/'기타'/''만 허용"
},
"illegal_words": {
"type": "array",
"description": "위반 단어 목록, 위반 없을 시 [] 포함, 사용자 입력 핵심 단어만 포함"
}
},
"required": ["is_illegal", "illegal_type", "illegal_words"],
"additionalProperties": False # 추가 필드 금지
}
}
}
)
result = json.loads(response.choices[0].message.content)
print(result)
2.3 함수 호출: 도구 사용
함수 호출(Function Calling) 또한 간접적으로 안정적인 JSON 출력을 달성할 수 있으며, 형식과 필드 모두에 대한 제약이 가능합니다. LLM API 호출 시 tools 매개변수를 통해 전체 도구 JSON 스키마(필드 이름, 유형, 필수 항목 등 규칙 명시)를 정의하고 모델이 해당 도구를 호출하도록 요청합니다. 모델은 도구를 정상적으로 호출하기 위해 미리 정의된 스키마에 따라 규정을 준수하는 함수 매개변수를 출력하며, 이 매개변수 자체가 규정을 준수하는 JSON이 됩니다.
핵심 작업: API 호출 시 tools를 사용하여 스키마 규칙을 정의하고, tool_choice를 통해 함수 호출을 강제합니다. 이를 통해 추출된 매개변수는 규정을 준수하는 JSON이 되며 추가 처리가 필요 없습니다.
다음은 코드 예시입니다 (OpenAI SDK 기준):
import json
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.openai.com/v1"
)
MODEL = "gpt-4o-mini"
if __name__ == "__main__":
system_prompt = """
# 역할
당신은 엄격한 사용자 입력 위반 판단 도우미입니다. ... (이전 프롬프트 내용) ...
"""
user_prompt = "정말 돼지 같네"
tools = [{
"type": "function",
"function": {
"name": "process_illegal_judge",
"strict": True, # JSON 형식 반환 필수
"description": "사용자 입력의 위반 여부를 판단하고 구조화된 JSON 결과를 반환합니다.",
"parameters": {
"type": "object",
"properties": {
"is_illegal": {
"type": "boolean",
"description": "위반 여부, true/false(소문자)만 허용"
},
"illegal_type": {
"type": "string",
"description": "위반 유형, '음란'/'폭력'/'모욕'/'기타'/''만 허용"
},
"illegal_words": {
"type": "array",
"description": "위반 단어 목록, 위반 없을 시 [] 포함, 사용자 입력 핵심 단어만 포함"
}
},
"required": ["is_illegal", "illegal_type", "illegal_words"],
"additionalProperties": False # 추가 필드 금지
}
}
}]
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": f"사용자 입력 위반 여부 판단: {user_prompt}"}]
response = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=tools,
tool_choice={ # 함수 호출 강제, 출력 형식 고정
"type": "function",
"function": {"name": "process_illegal_judge"}
}
)
if response.choices[0].message.tool_calls:
result = json.loads(response.choices[0].message.tool_calls[0].function.arguments)
print(result)
else:
raise ValueError("모델이 도구를 호출하지 않았습니다.")
3단계: 사후 복구 - JSON 복구
소프트 제약이 간혹 실패하고, 모델이 하드 제약을 지원하지 않는 경우, 사후 복구는 비즈니스 안정성을 보장하는 마지막 방어선입니다. 이는 경량 기술을 사용하여 JSON 오류를 복구하고 API 문제를 처리하는 데 중점을 둡니다. 모든 LLM에 적용 가능하며, 소프트 제약의 편차나 하드 제약의 호환성 문제를 보완합니다. JSON 형식이 잘못되었더라도 최대한 복구하고 재시도하여 서비스 가용성을 보장합니다.
3.1 정규 표현식 추출: 불필요한 내용 제거
핵심 기능: ```json 태그나 앞뒤의 불필요한 설명을 제거하고 순수한 JSON 텍스트를 추출합니다.
장점: 비용이 매우 저렴하고 속도가 빠르며 구현이 간단합니다. 후처리 단계의 기본입니다.
한계: 텍스트만 추출할 뿐, JSON 자체의 구문 오류(괄호 누락, 쉼표 초과 등)는 복구하지 못합니다.
import re
def extract_json_from_text(text: str):
"""LLM 출력 텍스트에서 순수한 JSON 내용 추출"""
# 먼저 markdown json 코드 블록을 찾습니다.
json_pattern1 = r'```json\s*(\{[\s\S]*?\})\s*```'
match1 = re.search(json_pattern1, text, re.DOTALL)
if match1:
return match1.group(1).strip()
# 마지막 수단으로 최외곽의 {}를 찾아, 앞뒤에 불필요한 내용이 있는 모든 경우에 대비합니다.
json_pattern2 = r'(\{[\s\S]*\})'
match2 = re.search(json_pattern2, text, re.DOTALL)
return match2.group(1).strip() if match2 else text
3.2 json\_repair: 구문 오류 복구
핵심 기능: JSON의 사소한 구문 오류(마지막 쉼표, 닫히지 않은 따옴표, 괄호 누락 등)를 자동으로 복구합니다.
장점: 로컬에서 복구하므로 모델 재호출이 필요 없으며 속도가 빠릅니다. 정규 표현식 추출의 부족한 부분을 효과적으로 보완합니다.
한계: 구문 오류만 복구하며, 필드 누락이나 키 이름 오류와 같은 필드 관련 문제는 해결하지 못합니다.
import json_repair
import json
def repair_invalid_json(invalid_json_str: str):
"""JSON 구문 오류(괄호 누락, 쉼표 초과 등) 복구"""
try:
json_str = json_repair.repair_json(invalid_json_str)
return json.loads(json_str)
except Exception as e:
print(f"JSON 복구 실패: {e}, LLM 원본 출력: {invalid_json_str}")
return None
3.3 Pydantic: 필드 규칙 검증
핵심 기능: Python 클래스를 사용하여 JSON 필드 규칙(형식, 유형, 값 범위 등)을 정의하고, JSON의 모든 규정 준수 여부를 검증합니다.
장점: 구문, 필드, 유형 등 모든 규정 준수 문제를 발견할 수 있어, 데이터 비규격으로 인한 운영 중단 장애를 방지할 수 있습니다.
한계: 검증 모델 작성이 필요하며, 약간의 코드 복잡성이 증가합니다.
from pydantic import BaseModel, Field, ValidationError
from typing import Literal
class IllegalJudgeResult(BaseModel, extra="forbid"):
is_illegal: bool = Field(description="위반 여부, true/false(소문자)만 허용")
illegal_type: Literal["음란", "폭력", "모욕", "기타", ""] = Field(description="위반 유형, '음란'/'폭력'/'모욕'/'기타'/''만 허용")
illegal_words: list = Field(description="위반 단어 목록, 위반 없을 시 []")
def validate_json(raw_json: str):
"""JSON 전체 검증, 위반 판단 요구 사항에 부합"""
try:
data = json.loads(raw_json)
return IllegalJudgeResult(**data).model_dump()
except (json.JSONDecodeError, ValidationError) as e:
print(f"JSON 검증 실패: {e}")
return None
3.4 재시도 메커니즘: 서비스 가용성 보장
핵심 기능: API 호출 실패 시 1초/2초/4초 간격으로 지수 백오프(exponential backoff)를 적용하여 재시도합니다.
장점: 타임아웃, 속도 제한, 형식 오류 등 예외 상황에서 서비스 가용성을 크게 향상시킵니다.
한계: 코드 복잡성이 증가하며, 재시도는 API 호출 시간을 늘릴 수 있습니다.
4단계: 요약
LLM의 JSON 출력 이상은 본질적으로 모델의 환각과 비즈니스 요구 사항의 결정성 간의 충돌입니다. '사전 안내 → 중간 제어 → 사후 복구'의 전체 루프 솔루션을 통해 JSON 출력을 안정적이고 규정을 준수하도록 만들 수 있으며, 다양한 시나리오와 LLM 유형에 적용할 수 있습니다.
- 사전 안내: 필드 규칙 명확화, Few-Shot 예시 제공, 검증 지침 추가를 통해 저비용으로 규정을 준수하는 JSON 출력을 유도합니다.
- 중간 제어:
JSON Mode,Structured Outputs,Function Calling을 활용하여 하드 제약을 구현하고, 근본적으로 예외를 방지합니다. - 사후 복구: 정규 표현식 추출,
json_repair라이브러리,Pydantic검증, 재시도 메커니즘을 결합하여, 예상치 못한 출력 오류에 대비하고 서비스 안정성을 확보합니다.