본문 바로가기

AI 콘텐츠 자동화

LLM과 Pydantic: 프롬프트 대신 코드 레벨에서 JSON 에러 잡기

LLM과 Pydantic: 프롬프트 대신 코드 레벨에서 JSON 에러 잡기


LLM(대규모 언어 모델)에게 JSON 형태로 출력을 요구할 때 흔히 겪는 파싱 에러와 이를 해결하기 위한 과정이다. 단순히 프롬프트를 튜닝하는 것을 넘어, 코드 레벨에서 구조적인 검증을 통해 시스템의 안정성을 높여야 한다는 깨달음을 얻었다.

위 화면은 LLM에게 프롬프트로 엄격하게 형식을 지시했음에도 불구하고 잘못된 응답을 반환하여 시스템 에러가 발생한 상황을 보여주는 예시다. LLM의 자유도를 통제하지 못하면 파이프라인 전체가 무너질 수 있음을 보여준다.

프롬프트 한 줄이면 JSON이 예쁘게 나올 줄 알았다

최근 인공지능을 활용해 다양한 비정형 텍스트 데이터를 분석하고, 이를 서비스 데이터베이스에 맞게 구조화하는 파이프라인 구축을 시작했다. 이 시스템의 핵심은 대규모 언어 모델(LLM)이 텍스트를 읽고 우리가 원하는 형태의 JSON 데이터로 요약 및 분류해 주는 것이었다.

처음엔 꽤 낙관적이었다. "요즘 모델들이 얼마나 똑똑한데." 프롬프트 마지막에 "반드시 JSON 형식으로만 출력해 줘. 키값은 title, summary, tags를 사용할 것"이라고 적어두기만 하면 알아서 매끄럽게 돌아갈 줄 알았다. 실제로 초기 테스트 몇 번은 내가 딱 원하는 형태의 깔끔한 JSON 응답을 뱉어냈다. 별도의 후처리 없이 파이썬의 json.loads()가 이 데이터를 찰떡같이 파싱하는 것을 보며, 당장 실무에 적용해도 되겠다는 섣부른 확신에 빠졌다.

새벽 2시의 불청객, JSONDecodeError

하지만 서비스가 본격적으로 가동되고 트래픽이 늘어나자 평화는 산산조각 났다. 잘 돌아가던 파이프라인이 심야 시간대마다 멈춰버렸고, 황급히 열어본 로그에는 어김없이 붉은 글씨의 json.decoder.JSONDecodeError가 찍혀 있었다.

원인은 다양하고 기발했다. * 과도한 친절함: "네, 요청하신 JSON 데이터를 작성해 드립니다:" 같은 마크다운 텍스트를 데이터 위아래로 덕지덕지 붙여놓았다. * 사소한 문법 오류: 리스트 마지막 항목 뒤에 불필요한 쉼표(Trailing comma)를 남겨 파서(Parser)를 고장 냈다. * 치명적인 타입 오류: 형식을 맞췄으나 알맹이가 틀린 경우다. tags 키에는 문자열 리스트(List)가 와야 하는데, 쉼표로 이어진 단일 문자열(String)을 던졌다. 리스트를 기대하고 루프를 돌리려던 시스템은 즉시 타입 에러를 뿜으며 장렬히 전사했다.

파이선생 분석
파이선생 분석

이러한 오류의 원인을 분석해 본 결과, LLM의 확률론적 특성을 이해하고 코드 레벨의 방어 로직을 마련해야 한다는 결론에 도달했다.

프롬프트 협박의 늪에 빠지다

이 난장판을 수습하기 위해 처음 취한 행동은 '프롬프트 튜닝'이었다. 에러가 날 때마다 프롬프트에 경고성 문구를 이어 붙였다.

"결코 다른 설명은 덧붙이지 마라." "마지막 쉼표는 빼라." "tags는 반드시 배열(Array) 형태로 작성하라."

프롬프트는 끝없이 길어졌고, 묘하게 협박성(?) 짙은 문장들로 채워졌다. 이를 수정하면 일시적으로는 문제가 해결되는 듯 보였다. 하지만 며칠 뒤 모델은 내가 미처 예상하지 못한 창의적인 방법으로 다시 규칙을 어겼다. 필요 키값인 summary를 통째로 빼먹거나, 존재하지도 않는 새로운 키를 임의로 창조해 냈다. 프롬프트에 아무리 촘촘하게 규칙을 나열해도, 모든 엣지 케이스를 방어하는 것은 불가능에 가깝다는 것을 뼈저리게 깨달았다.

진짜 문제는 LLM이 아니라 나의 '안일함'이었다

근본적인 원인을 곱씹어보았다. 대규모 언어 모델은 기본적으로 '확률에 기반해 다음 단어를 예측하는 텍스트 생성기'다. 내가 아무리 엄격한 규칙을 제시해도, 모델 입장에서 그것은 하나의 확률적 가이드라인일 뿐 런타임에 엄격히 통제되는 강제 사항이 아니었다.

결국 진짜 문제는 변덕스러운 모델이 아니었다. 변덕스러운 확률 모델의 출력을 아무런 안전장치 없이 시스템의 핵심 로직으로 직행시킨 나의 안일한 설계가 문제였다. 외부 API 응답이나 유저 입력값을 받을 때는 당연하다는 듯 데이터 검증(Validation)을 철저히 하면서, 왜 LLM의 출력은 반드시 신뢰했을까?

"프롬프트는 방향을 제시할 뿐, 데이터의 무결성을 보장하는 방어선이 될 수 없다."

Pydantic으로 세우는 데이터의 최전선

해결책을 찾던 중, 파이썬 생태계의 Pydantic을 본격적으로 도입하기로 했다. Pydantic은 파이썬의 타입 힌트(Type Hint)를 활용해 데이터의 구조와 타입을 강제하고 검증해 주는 강력한 도구다.

1. 엄격한 명세서 정의 가장 먼저 LLM으로부터 받아야 하는 JSON 데이터의 명세서를 Pydantic의 BaseModel로 정의했다. title은 문자열, summary는 최소 10자 이상의 문자열, tags는 문자열 리스트 등 엄격하게 타입을 묶어두었다.

2. 코드 레벨의 검문소 설치 이제 LLM이 응답을 보내면 시스템에 바로 반영하지 않고 이 Pydantic 모델을 먼저 통과시켰다. 만약 모델이 tags를 리스트가 아닌 단순 문자열로 보낸다면, Pydantic은 즉시 어디가 어떻게 틀렸는지 상세한 ValidationError를 뱉어낸다.

3. 자가 치유(Self-Healing) 로직의 구현 여기서 한 걸음 더 나아가 자동 재시도(Retry) 로직을 엮었다. 에러가 발생하면 시스템을 멈추는 대신, Pydantic이 뱉어낸 에러 메시지를 그대로 다시 LLM에게 던졌다. "네가 준 데이터에서 이런 타입 에러가 발생했어. 이것을 참고해서 다시 올바른 JSON으로 고쳐서 보내." 놀랍게도 모델은 자신이 틀린 부분을 스스로 인지하고, 요구사항에 안전하게 들어맞는 데이터를 다시 생성해 냈다.

이 구조를 도입한 후, 파이프라인의 에러 발생률은 수직 낙하했고 데이터 품질은 놀랍도록 안정화되었다.

신뢰하되, 검증하라 (Trust, but verify)

이번 경험을 통해 프롬프트 엔지니어링의 한계를 명확히 알게 되었다. 프롬프트를 정교하게 깎아 좋은 결과물을 유도하는 것도 중요하지만, 실제 서비스 환경에서는 그것만으로 턱없이 부족하다. '믿음의 영역'에 있던 LLM의 응답을 '엄격한 코드 레벨의 검증 영역'으로 끌어내려야만 비로소 무너지지 않는 시스템을 구축할 수 있다.

LLM은 강력한 엔진이지만, 그 엔진이 올바른 방향으로 질주하게 하려면 튼튼한 프레임과 브레이크가 필요하다. 나에겐 Pydantic이 바로 그 프레임이었다. 데이터를 다루는 백엔드 개발자에게 "신뢰하되 검증하라"는 격언은 인공지능 시대에 더욱 유효하다.

현재는 텍스트를 분석해 요약하는 일부 모듈에만 이 검증 로직을 적용한 상태다. 앞으로 사내에서 돌아가는 모든 LLM 기반 자동화 파이프라인에 이 패턴을 이식할 계획이다. 또한, OpenAI의 '구조화된 출력(Structured Outputs)' 기능이나 LangChain의 출력 파서(Output Parsers)를 Pydantic과 결합해 볼 생각이다. 프롬프트 창 안에서 씨름하던 시간을 줄이고, 파이썬 코드 레벨에서 더욱 견고하고 촘촘한 그물을 짜는 데 집중하는 것. 그것이 실무형 AI 파이프라인을 구축하는 가장 빠른 지름길일 것이다.