텍스트-JSON 파싱에서 JobSearch의 Claude 도구 사용(Tool Use)까지
요약
텍스트-JSON 파싱 오류를 해결하기 위해 수동으로 작성했던 복잡한 파싱 함수들을 Anthropic의 Tool Use 기능을 활용하여 구조화된 데이터 추출 방식으로 전환하는 과정을 다룹니다.
핵심 포인트
- 수동 파싱 로직은 유지보수가 어렵고 비용/지연 시간 문제를 야기함
- 마크다운 펜스, 따옴표, 줄바꿈 등 다양한 JSON 포맷 오류 발생
- Anthropic의 Tool Use 기능을 활용하면 엄격한 스키마 준수 가능
- 복잡한 파싱 레이어 대신 모델의 도구 호출 기능을 통한 데이터 구조화 권장
나의 구직(Job-search) 도구에는 Claude가 방금 작성한 JSON을 수정하는 것만을 목적으로 하는 다섯 가지 함수가 있었습니다. _clean_json_text, _fix_unescaped_newlines, _fix_single_quotes, _strip_markdown_wrapper, _extract_and_parse_json이 그것입니다. 또한, 망가진 JSON을 가져와서 스스로 저지른 실수를 고쳐달라는 정중한 요청과 함께 모델에게 다시 보내는 여섯 번째 함수인 _retry_json_fix도 있었습니다. 나는 몇 주에 걸쳐 한 번에 하나의 버그를 해결하며 이 함수들을 하나하나 직접 작성했습니다. 나는 그것들이 조금 자랑스러웠습니다.
그것이 문제였습니다.
다섯 개의 파서(Parser)를 갖게 되는 과정
JobSearch는 나의 개인적인 도구이며, 실제 운영(Production) 중이고, 단일 사용자(나 자신)를 위한 것입니다. 이 도구는 9개의 게시판에서 채용 공고를 가져오며, 내가 "Analyze"를 누르면 Claude가 내 이력서(CV)와 대조하여 공고를 읽고 구조화된 판결을 반환합니다: 점수, 추천 여부, 커리어 트랙, 그리고 공고가 실제로 요구하는 영어 수준입니다. 그 판결은 반드시 JSON 형태여야 합니다. 왜냐하면 그 이후의 모든 과정은 산문(Prose)이 아닌 데이터베이스 행(Database row)이기 때문입니다.
첫 번째 버전은 모든 튜토리얼이 하는 방식을 따랐습니다. 프롬프트(Prompt)에서 모델에게 JSON을 요청하고, response.content[0].text를 가져와 json.loads를 실행하는 것이었습니다. 데모에서는 잘 작동했지만, 실제 운영 단계에 들어서면서 운영 환경이 나에게 여러 가지를 가르쳐주기 시작했습니다.
모델이 JSON을 마크다운 펜스(Markdown fences)로 감싸기 시작해서, 나는 그것을 제거하는 함수를 작성했습니다. 때때로 모델이 작은따옴표(Single quotes)를 사용하기에, 그것을 수정하는 함수를 작성했습니다. 그다음에는 문자열 내부에 줄바꿈이 포함된 설명이 나타났고, 그래서 _fix_unescaped_newlines를 작성했습니다. 그다음에는 숫자가 있어야 할 자리에 NaN이 나타났습니다. 모든 수정 사항은 5줄 정도였고, 분명히 올바르게 작동했으며, 각각의 테스트를 동반했습니다. 나는 아직도 git 히스토리에 그 테스트 이름들을 가지고 있는데, 마치 자백처럼 읽힙니다: test_removes_trailing_commas, test_replaces_nan_with_null, test_replaces_infinity, test_unclosed_fence_still_strips_opening.
4월쯤 파싱 레이어(parsing layer)는 7개의 전략이 체인(chained) 형태로 연결된 약 250줄 규모였으며, 각 전략은 이전 단계에서 걸러지지 않은 것을 잡아내는 방식이었습니다. 마지막 수단은 AI 자체 복구(self-repair) 호출이었습니다. 만약 아무것도 파싱되지 않으면, 망가진 출력을 다시 보내 모델에게 이를 수정하도록 요청하는 것이었습니다. 첫 번째 호출에서 결코 발생해서는 안 되었을 포맷팅 문제를 해결하기 위해, 실제 지연 시간(latency)과 실제 비용이 발생하는 두 번째 API 호출을 수행하는 방식이었습니다.
저는 누구도 생성해서는 안 될 출력이 나와도 제 코드가 버틸 수 있는지 확인하는 테스트 스위트(test suite)를 가지고 있었습니다. 그것은 강건함(robustness)이 아닙니다. 그것은 잘못된 수신자에게 보내진 버그 리포트일 뿐입니다.
실제 해결책
Anthropic의 API에는 도구 사용(tool use) 기능이 있습니다. 보통은 모델이 사용자의 함수를 호출할 수 있도록 하기 위해 이 기능을 사용합니다. 하지만 이 기능에는 더 엄격한 활용법이 있습니다. 만약 당신이 원하는 답변의 형태와 일치하는 입력 스키마(input schema)를 가진 도구를 정확히 하나만 정의하고, tool_choice를 통해 이를 강제하면, 모델은 다른 방식으로 답변할 수 없습니다. JSON은 이미 파싱된 상태로 도착하며, API 자체에 의해 스키마 검증(validation)을 거쳐 응답 객체 내의 Python 딕셔너리(dict)로 제공됩니다.
저는 이미 분석을 위한 Pydantic 모델을 가지고 있었습니다. DB 행(row)에 모델이 필요했기 때문입니다. 따라서 스키마는 이미 준비되어 있었습니다:
def _schema_from_model(model_cls: type[BaseModel]) -> dict[str, Any]:
"""Pydantic 모델로부터 input_schema에 적합한 JSON Schema를 생성합니다."""
...
...
리팩터링(refactor)은 4월 14일에 단 하나의 커밋으로 완료되었습니다. 코드베이스의 모든 AI 호출이 마이그레이션되었고, 5개의 파서(parser)가 모두 삭제되었으며, 자체 복구 폴백(self-repair fallback)과 함께 쓰레기 JSON(garbage-JSON) 테스트 파일도 삭제되었습니다. 커밋 메시지에는 200줄이 줄어들었다고 적혀 있지만, 이는 실제보다 과소평가된 것입니다. 삭제된 줄들은 무언가 고장 날 때마다 제가 매번 다시 읽어야 했던 바로 그 줄들이었기 때문입니다.
그것은 배치(batch) 작업의 무시무시한 PR(Pull Request)이었습니다. Claude와 제가 오후 시간 동안 13개의 PR을 프로덕션(production)에 배포하던 중이었고, 저는 그날에 대해 별도로 글을 쓴 적이 있습니다. 그중 12개의 PR은 일상적인 작업이었습니다. 하지만 이 PR은 동일한 디프(diff) 내에서 안전망을 삭제하고 이를 API의 약속으로 대체하는 작업이었습니다.
제가 유지한 한 가지는 호출 이후의 Pydantic 검증(validation)이었습니다. 스키마(schema)는 형태(shape)를 보장할 뿐, 의미(sense)를 보장하지는 않습니다. 0-100 범위의 필드에 950이라는 점수가 들어온다면, 이는 스키마상으로는 유효한 JSON이지만 여전히 쓰레기 데이터이며, 모델의 출력은 전달 방식과 상관없이 신뢰할 수 없는 입력값(untrusted input)으로 남습니다. 계약(contract)은 API로 이동했지만, 검증은 여전히 제 쪽에서 수행되었습니다.
출시 후 찾아온 뜻밖의 상황
여기가 제가 예상하지 못했던 부분이며, 이 글이 단순히 "도구 사용(tool use)을 사용하고 파서(parser)를 삭제하라"는 내용에 그치지 않는 이유입니다.
텍스트 출력(text output) 방식에서는 모델이 제 프롬프트 규칙을 다소 느슨하게 다루었고, 파싱(parsing)의 혼란이 이를 가려주었습니다. 하지만 강제된 스키마(forced schema) 환경에서는 모델이 훨씬 더 문자 그대로 규칙을 준수합니다. 저는 프리랜서 직무에 대한 폴백(fallback) 규칙을 가지고 있었습니다. 이탈리아 구인 광고는 때때로 P.IVA(부가가치세 번호)를 요구하는데, 이는 해당 제안이 저에게 유효한지 여부를 결정짓는 중요한 요소입니다. 규칙은 대략적으로 "프리랜서 상태가 모호하면 플래그(flag)를 표시하라"는 내용이었습니다. 도구 사용(tool use) 환경에서 모델은 프리랜싱을 스치듯 언급하거나, 다른 팀의 계약직에 관한 한 줄짜리 문구 등 프리랜싱이 언급된 모든 제안에 플래그를 표시하기 시작했습니다. '모호함(Ambiguous)'이 조용히 '어디에서든 언급됨'이라는 의미로 변질되어 있었던 것입니다.
스키마는 모델을 더 순종적으로 만들었고, 그 순종성은 제 지침(instructions)이 얼마나 허술했는지를 드러냈습니다. 해결책은 코드가 아니었습니다. 명시적인 우선순위를 부여하여 프롬프트를 다시 작성하는 것이었습니다. 즉, 제안서에 명시된 계약 유형이 우선하며, 폴백은 제안 내용 자체가 계약에 관한 것이고 그 내용이 확정되지 않았을 때만 작동하도록 수정했습니다.
결국 제가 비싼 대가를 치르고 배운 교훈은 이것입니다. 텍스트 파싱 (Text Parsing)에서 도구 사용 (Tool Use)으로 전환할 때는, 해석을 위한 프롬프트 규칙을 완화하는 것이 아니라 오히려 더 엄격하게 다듬어야 한다는 점입니다. 모델은 형식을 즉흥적으로 만들어내는 것을 멈추고, 당신의 말을 진지하게 받아들이기 시작합니다. 만약 당신의 규칙이 모호했다면, 바로 지금 그 사실을 깨닫게 될 것입니다.
과거의 나에게 해주고 싶은 말
파서 (Parser)들은 결코 방어적 프로그래밍 (Defensive Programming)이 아니었습니다. 그것들은 계약 (Contract)이 API 호출의 잘못된 측면에 존재한다는 증상이었으며, 매번 새로운 수정 함수를 만드는 것은 응답이 온 이후에, 즉 최악의 장소에서 한 번에 하나의 예외 케이스 (Edge Case)를 다루며 그 계약을 재협상하는 과정이었습니다.
만약 당신의 파이프라인 (Pipeline)에 _fix_single_quotes와 같은 함수가 있다면, 당신에게 필요한 것은 더 나은 파서가 아닙니다. 데이터의 형태를 input_schema로 옮기고, 도구 사용을 강제하며, 검증 (Validation) 절차는 유지하되, 박물관에나 있을 법한 구식 코드들은 삭제하십시오. 그러고 나서 당신의 프롬프트를 다시 읽어보십시오. 모델이 프롬프트의 모든 단어를 곧 믿기 시작할 것이기 때문입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기