AI 출력값 복사 붙여넣기 중단하기: LLM이 JSON으로 말하도록 강제하는 방법
요약
LLM의 출력값에서 불필요한 텍스트를 제거하고 유효한 JSON 형식을 보장하기 위한 기술적 해결책을 다룹니다. 프롬프트 엔지니어링이나 정규 표현식의 한계를 지적하며, OpenAI의 함수 호출(Function calling) 기능을 활용한 구조화된 데이터 추출 방법을 제시합니다.
핵심 포인트
- 프롬프트 엔지니어링만으로는 JSON 출력의 완벽한 구조를 보장하기 어려움
- 정규 표현식을 이용한 파싱은 복잡한 중첩 구조에서 오류 발생 가능성 높음
- OpenAI의 Function calling을 사용하면 타입이 지정된 유효한 JSON을 안정적으로 얻을 수 있음
- 프로덕션 환경에서는 재시도 방식보다 구조화된 추출 방식이 비용과 효율 면에서 유리함
몇 달 전, 저는 고객 지원 이메일에서 이벤트 세부 정보를 자동으로 추출하는 작은 도구를 만들고 있었습니다. 다들 아시다시피 그런 식이죠. 누군가 "저기, 다음 주 화요일 오후 3시에 데모 일정을 잡을 수 있을까요?"라고 쓰면, 저는 날짜, 시간, 주제를 뽑아내야 합니다. 저는 LLM (Large Language Models)이 이 작업에 완벽하다고 생각했습니다. 그냥 GPT에게 JSON을 반환하라고 요청하면 될 것 같았죠.
간단하죠?
하지만 실제로 모델로부터 받은 결과는 이랬습니다:
{
"event": "demo",
"date": "next Tuesday",
...
귀엽긴 합니다. 하지만 이제 추가 텍스트 때문에 제 json.loads()가 발작을 일으킵니다. 이것이 제 주말 3일을 앗아간 문제입니다.
순진한 접근 방식 (그리고 그것이 왜 고통스러웠는지)
저의 첫 번째 시도는 전형적이었습니다. try/except로 감싼 json.loads()에 원시 응답을 전달하는 것이었죠. 실패하면 API 호출을 재시도합니다. 일회성 작업에는 아주 잘 작동하지만, 수백 개의 이메일을 처리하는 프로덕션 파이프라인 (production pipeline)에서는 재시도가 느리고 토큰 (tokens)을 낭비합니다. 게다가 모델은 때때로 필드를 환각 (hallucinate)하거나 필수 필드를 누락하기도 합니다. 재시도는 이를 해결해주지 못합니다.
import json
from openai import OpenAI
...
첫 번째 막다른 길입니다.
다음으로 저는 유효한 JSON 구조가 아닌 모든 것을 제거하기 위해 정규 표현식 (regex)을 시도했습니다. 모델이 중괄호나 콜론이 포함된 문자열을 중첩할 때까지는 잘 작동했습니다. 저는 하루 종일 정규 표현식 패턴을 조정했지만, 결국 이스케이프된 따옴표 (escaped quote)가 포함된 완벽하게 유효한 JSON에서 작동이 깨져버렸습니다. 두 번째 막다른 길입니다.
프롬프트 엔지니어링 (Prompt-engineering)의 늪
저는 완전히 프롬프트 엔지니어 (prompt engineer) 모드로 들어갔습니다. "오직 유효한 JSON만 출력하세요, 추가 텍스트는 안 됩니다. 저는 프로그래밍 방식으로 파싱할 것입니다."라는 문구를 추가했습니다. 스키마 (schema)를 제공했습니다. 모델에게 디지털적인 꾸짖음으로 위협까지 했습니다. 하지만 여전히 10%의 확률로 실패했습니다. 대량 처리 파이프라인에서 10%의 실패율은 화재 경보와 같습니다.
사실, 모델은 90%의 확률로 명령을 따랐습니다. 하지만 그 10%는 무작위였습니다. 때로는
마커를 추가했고, 때로는 JSON을 태그로 감쌌으며, 한 번은 "물론이죠! 여기 JSON이 있습니다:"라고 말하기도 했습니다. 프롬프트 엔지니어링만으로는 덕테이프 (duct tape)와 같습니다. 바람의 방향이 바뀌기 전까지만 작동할 뿐입니다.
## 깨달음의 순간: 함수 호출 (Function calling)
OpenAI의 문서를 (다시) 읽던 중, 저는 **함수 호출 (Function calling)**이라는 기능을 발견했습니다. 이는 타입이 지정된 파라미터(parameter)를 가진 함수 시그니처(signature)를 정의할 수 있게 해주며, 모델은 사용자가 요청한 형태를 *보장하는* 구조화된 JSON 객체를 반환합니다. 만약 모델이 필수 파라미터를 채울 수 없다면 이를 null로 남겨두지만, JSON 구조 자체는 항상 유효합니다.
잠깐, 왜 이것이 구조화된 추출 (structured extraction)의 기본값이 아닌 걸까요?
저는 추출 모듈을 함수 호출을 사용하도록 다시 작성했습니다. 핵심 아이디어는 다음과 같습니다:
```python
from openai import OpenAI
...
불필요한 군더더기도, 파싱을 위한 복잡한 기술(parsing gymnastics)도 필요 없습니다. function_call.arguments는 항상 유효한 JSON 문자열입니다. 왜냐하면 모델이 스키마 (schema)와 일치하는 유효한 JSON을 출력하도록 문자 그대로 제약(constrained)되어 있기 때문입니다. 마치 치트키와 같습니다.
하지만 이것이 치팅일까요? (트레이드오프 (Trade-offs))
함수 호출은 구원투수와 같지만, 마법은 아닙니다. 제가 직접 겪으며 배운 몇 가지 사항은 다음과 같습니다:
- 토큰 비용 (Token cost): 함수 정의 자체도 토큰을 소비합니다. 복잡한 스키마(중첩된 객체, 열거형 (enums))의 경우 호출당 200~500개의 토큰이 추가될 수 있습니다. 수천 번의 호출이 쌓이면 이 비용은 상당해집니다.
- 모델 가용성 (Model availability): 함수 호출은 GPT-3.5 Turbo 및 GPT-4에서 작동합니다. 로컬 모델이나 대안 모델(예: Llama)을 사용한다면 다시 원점으로 돌아가게 됩니다. 다만, 최근 일부 오픈 소스 모델들은 도구 사용 (tool use)을 지원하고 있습니다.
- 유연성 상실 (Flexibility loss): 스키마는 경직되어 있습니다. 입력값이 일치하지 않으면 모델이 선택적 필드 (optional fields)를 건너뛸 수는 있지만, 새로운 필드를 만들어내지는 않습니다. 이는 신뢰성 측면에서는 좋지만, 모델의 창의성을 원한다면 나쁜 점입니다. 추출 작업의 경우, 이러한 경직성은 오히려 하나의 기능 (feature)입니다.
- 에러 핸들링 (Error handling): 강제된 함수 호출을 사용하더라도 모델은 여전히 값을 환각 (hallucinate)할 수 있습니다 (예: 텍스트와 일치하지 않는 날짜를 지어냄). 따라서 여전히 비즈니스 규칙에 따라 출력을 검증해야 합니다. 저는 이를 위해 커스텀 검증기 (custom validators)를 포함한
EventModel(**function_args)와 같이 Pydantic을 사용하고 있습니다.
대안 도구들은 어떨까요?
구조화된 추출 (structured extraction)이 필요한 앱을 구축 중이고 직접 함수 호출 (function-calling) 로직을 구현하고 싶지 않다면, 일부 관리형 서비스 (managed services)들이 이를 추상화하여 제공합니다. 예를 들어, Interwest Info의 AI API를 사용하면 추출 스키마 (extraction schemas)를 정의할 수 있으며, 프롬프트와 씨름할 필요 없이 깔끔한 JSON을 반환받을 수 있습니다. 하지만 솔직히 말해서, 함수 호출 (function calling)이 내부적으로 어떻게 작동하는지 배우는 것이 훨씬 더 많은 제어권과 이해도를 제공합니다.
제가 현재 프로덕션에서 사용하는 코드
다음은 제가 그 이후로 계속 사용하고 있는 재사용 가능한 함수입니다. 이 함수는 모든 추출 작업을 Pydantic 모델로 감쌉니다 (wrap).
import json
...
이 패턴은 이제 제가 구축하는 모든 추출 파이프라인 (extraction pipeline)에 적용되어 있습니다. 완벽하지는 않습니다. 만약 모델이 필수 필드에 대해 null을 반환하면 Pydantic은 검증 오류 (validation error)를 발생시키며, 저는 이를 포착하여 로그를 남깁니다. 그래도 정규 표현식 (regex)을 사용하는 것보다는 훨씬 낫습니다.
배운 점들
- 모델이 포맷팅 지침을 따를 것이라고 믿지 마세요. 출력을 제한하는 API 기능(함수 호출 (function calling), 최신 모델의 JSON 모드 (JSON mode))을 사용하세요.
- 검증은 여전히 당신의 몫입니다. 구조화된 출력 (structured output)이라 할지라도 잘못된 데이터(예: "다음 주 금요일"이 임의의 날짜로 해석되는 경우)를 포함할 수 있습니다. 그 위에 비즈니스 로직을 적용하세요.
- 스키마 설계 (Schema design)가 중요합니다. 필드 설명 (field descriptions)에 형식을 구체적으로 명시하세요. 저는 설명에 "YYYY-MM-DD"와 같은 예시를 자주 포함하는데, 이는 모델에게 큰 도움이 됩니다.
- 비용을 고려하세요. 예산이 한정되어 있다면, 함수 호출 (function calling)은 토큰 오버헤드 (token overhead)로 인해 비용이 많이 들 수 있습니다. 재시도 (retries)를 통해 가끔 발생하는 잘못된 형식의 응답을 허용할 수 있는지 사용 사례를 평가하세요.
다음에 다시 한다면 다르게 할 점
저는 첫날부터 함수 호출 (function calling)로 시작할 것입니다. 순진한 (naïve) 접근 방식 때문에 주말 세 번을 날렸고 엄청난 좌절감을 맛보았습니다. 또한, API 로직을 건드리지 않고도 스키마를 교체할 수 있도록 초기에 extract_with_model과 같은 래퍼 (wrapper)를 작성할 것입니다.
또한, 먼저 gpt-3.5-turbo와 같은 더 작은 모델로 벤치마크 (benchmark)를 해볼 것입니다. 함수 호출 (function calling)은 거기서도 작동하며, 간단한 추출의 경우 정확도가 GPT-4와 놀라울 정도로 비슷하면서도 토큰 비용은 훨씬 저렴합니다.
여러분의 차례입니다
여러분은 여전히 정규 표현식 (regex)과 요행에 기대어 AI 출력값을 다루고 계신가요? 아니면 구조화된 생성 (structured generation)을 위한 더 깔끔한 패턴을 찾으셨나요? 여러분의 프로젝트에서 무엇이 효과적이었는지 (그리고 무엇이 실패했는지) 꼭 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기