AI한테 JSON 달라고 했다가 파싱이 터진 이야기 — Output Parser
AI 응답을 프로그램에서 쓰려면 결국 구조화된 데이터가 필요합니다. 저도 처음엔 프롬프트에 “JSON으로만 답해”라고 적고 json.loads()로 파싱했어요. 잘 되는 듯했습니다. 그런데 어느 날 배치 작업이 통째로 죽었고, 로그를 보니 모델이 ```json ... ``` 코드블록으로 감싸거나 앞에 “네, 아래와 같습니다:”를 붙여서 준 게 원인이었습니다. Output Parser는 바로 이 “형식이 들쭉날쭉한 문제”를 잡아주는 기능입니다.
왜 그냥 json.loads로는 부족한가
LLM은 확률적으로 답을 만들기 때문에, 같은 프롬프트라도 어떤 날은 순수 JSON을, 어떤 날은 설명을 덧붙인 JSON을 줍니다. 사람이 보기엔 다 맞는 답이지만 json.loads()엔 치명적이죠. Output Parser는 파싱 규칙을 프롬프트에 자동으로 심어주고, 결과를 정해진 형식으로 뽑아내는 역할을 합니다.
지금 제가 쓰는 방식 (structured output)
요즘은 Pydantic으로 스키마를 정의하고 모델에 직접 강제하는 방식을 씁니다. 파서를 따로 붙이는 것보다 실패가 적었어요.
from pydantic import BaseModel
from langchain_openai import ChatOpenAI
class Person(BaseModel):
name: str
age: int
job: str
model = ChatOpenAI(model="gpt-4o-mini").with_structured_output(Person)
result = model.invoke("이름은 홍길동, 나이 30, 직업 개발자인 사람 정보를 추출해줘")
print(result.name, result.age, result.job) # 홍길동 30 개발자
이렇게 하면 결과가 아예 Person 객체로 떨어져서, 코드블록이든 사족이든 신경 쓸 필요가 없습니다. result.age가 문자열 “30”이 아니라 정수 30으로 오는 것도 큰 차이예요.
그래도 남는 함정
구조화 출력도 만능은 아닙니다. 모델이 값을 모를 때 억지로 채워 넣는(할루시네이션) 경우가 있어서, 저는 스키마에 Optional을 두거나 “모르면 null” 지시를 함께 넣습니다. 그리고 중요한 배치에는 파싱 실패 시 1회 재시도 로직을 꼭 답니다. 형식을 강제해도 “내용이 맞는지”는 별개의 문제라는 걸 배웠거든요.
정리하며
Output Parser(또는 structured output)는 “AI의 자유로운 말투”와 “프로그램이 원하는 딱딱한 형식” 사이를 이어주는 다리입니다. 저처럼 배치가 한밤중에 죽고 나서 배우지 말고, 처음부터 스키마를 정의해 두시길 권합니다. 다음 글에서는 이렇게 뽑은 구조화 데이터를 실제 도구 호출(Tool Calling)로 이어 붙이는 방법을 다뤄보겠습니다.

댓글 남기기