본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 03. 05:07

텍스트 분류를 위한 프롬프트 작성을 멈추세요: 평가를 선언적으로 만드세요

요약

LLM을 활용한 텍스트 분류 시 발생하는 프롬프트 드리프트와 JSON 파싱 오류 문제를 해결하기 위해, 프롬프트 대신 타입이 지정된 '탐지기(detectors)'를 선언적으로 사용하는 방식을 제안합니다.

핵심 포인트

  • 프롬프트 대신 타입이 지정된 탐지기(presence, classification) 선언
  • 생성 시점에 예시를 통한 스모크 테스트 수행으로 배포 전 오류 차단
  • LLM의 출력을 신뢰하는 대신 결정론적 코드로 엄격하게 검증
  • 의미론적 오류를 방지하기 위한 선언적 평가 체계 구축

저는 똑같은 것을 여러 번 만들어 보았습니다. 유입되는 메시지(리드 양식, 고객 지원 티켓, DM 등)를 읽고 어떻게 처리할지 결정하는 단계 말입니다. 자격을 부여하거나, 에스컬레이션(escalate)하거나, 라우팅(route)하거나, 폐기하는 작업입니다.

매번 구현 방식은 동일했습니다. LLM에게 JSON을 반환하도록 요청하는 프롬프트를 직접 작성하고, JSON을 파싱한 뒤, 그 결과에 따라 분기(branch)를 나누는 방식이었습니다. 그리고 매번 똑같은 방식으로 부패했습니다.

  • 프롬프트는 테스트가 불가능했습니다. "맞아 보이네"가 유일한 QA(품질 보증)였습니다.
  • 드리프트(drift) 현상이 발생했습니다. 모델 업그레이드나 프롬프트의 단어 하나를 수정한 것이 출력을 조용히 변화시켰고, 무언가가 잘못 라우팅될 때까지 아무도 알아채지 못했습니다.
  • JSON이 거짓말을 했습니다. 모델은 제가 허용한 세트에 없는 카테고리나 제가 예상한 범위를 벗어난 숫자를 자신 있게 반환했고, 저의 다운스트림(downstream) switch 문은 기쁘게 쓰레기 데이터를 바탕으로 동작했습니다.
  • "신뢰도(Confidence)"는 연극에 불과했습니다. if confidence > 0.7은 서로 다른 입력값에 대해 아무런 의미도 갖지 못하는 마법의 숫자일 뿐이었습니다.

결국 저는 프롬프트를 쓰는 것을 그만두었습니다. 대신 제가 구축한 것과 그 과정에서 배운 점은 다음과 같습니다.

핵심 아이디어: 어떻게 물을지가 아니라, 무엇을 탐지할지 선언하세요

프롬프트 대신, 타입이 지정된 **탐지기(detectors)**를 선언합니다. 두 가지 종류가 있습니다:

  • 존재 여부 (presence) — "텍스트에 이 신호가 있는가?" → found를 반환
  • 분류 (classification) — "이것을 고정된 카테고리 세트 중 정확히 하나에 배치하라" → enum으로 검증된 카테고리를 반환
{
  "name": "Partnership routing",
  "template": "custom",
...

여러분은 프롬프트를 직접 보지 못합니다. 프롬프트는 선언으로부터 컴파일됩니다. 그 부분은 흥미로운 지점이 아닙니다. 누구나 프롬프트를 템플릿화할 수 있으니까요. 흥미로운 지점은 탐지기가 문자열이 아닌 타입이 지정된 객체이기 때문에 가능해지는 모든 것들입니다.

1. 탐지기는 프로덕션이 아니라 생성 시점에 테스트됩니다

모든 탐지기는 최소 하나 이상의 긍정적인 예시(example)를 필요로 합니다. 평가(evaluation)를 생성할 때, 이 예시들은 스모크 테스트(smoke test)로 실행됩니다. 각 긍정적 예시는 실제로 일치해야 하며, classification 예시는 선언된 categories 내에 위치해야 합니다. 만약 그렇지 않다면, 생성이 실패합니다.

이 부분은 제가 몇 년 전에 알았더라면 좋았을 내용입니다. 프롬프트는 구문론적(syntactically)으로는 완벽할 수 있지만 의미론적(semantically)으로는 망가져 있을 수 있으며, 이는 운영 환경(production)에 도달해서야 발견됩니다. 여기서는 고장 난 탐지기(detector)를 배포할 수 없습니다. 단언(assertion)이 라이브 상태가 되기 전에 실행되기 때문입니다. (non_examples는 존재 여부만 확인합니다. 분류 탐지기는 항상 어딘가에는 결과가 도달하기 때문에, 단언할 수 있는 "찾을 수 없음" 상태가 존재하지 않기 때문입니다.)

2. 출력은 신뢰하는 것이 아니라 결정론적으로 검증한다

LLM은 제안하고, 결정론적 코드(deterministic code)는 판정합니다. 검증기(validators)는 의도적으로 지루하게 설계되었습니다: present, range:0-100, enum:yes,no와 같은 식입니다. 집합을 벗어난 분류는 통과할 수 없습니다. 대신 "찾을 수 없음"으로 강제 변환되며 invalid_fields 목록에 나타나므로

내가 틀린 점 / 여전히 미흡한 점

솔직히 말씀드리면, 다음과 같은 실제적인 문제들이 있습니다:

  • 오늘날 모든 것이 LLM을 거칩니다. 명백한 키워드조차 모델 호출을 거칩니다. 계획은 결정론적 신호(deterministic signals)가 추론 비용을 발생시키지 않도록 LLM 호출 전 패턴 추출기(pre-LLM pattern extractor)를 만드는 것이지만, 아직 구축되지 않았습니다. 따라서 비용/지연 시간(latency)은 "평가당 하나의 배치 LLM 호출" 수준입니다. 인바운드 웹훅(inbound webhooks)에는 괜찮지만, 높은 QPS(초당 쿼리 수)를 가진 스트림에는 적합하지 않습니다.
  • 동기식(Synchronous) 방식입니다. 긴 전사 데이터(transcripts)는 속도가 느립니다. 그래서 5,000 토큰 분량의 스레드를 통째로 던지는 대신, 구조화된 헤더(심각도/티어)를 앞에 붙입니다.
  • 탐지기(detectors)를 하나의 프롬프트로 배치(batching)하는 것은 비용을 낮춰주지만, 한 탐지기의 문구가 다른 탐지기의 추출 결과에 영향을 줄 수 있습니다. 이를 격리하려면 N번의 호출이 필요합니다. 저는 비용을 선택했습니다. 이것이 옳은 선택인지는 확신할 수 없습니다.
  • 멀티 턴(Multi-turn) 방식이 단순합니다. 늘어나는 대화를 재평가할 때 전체 내용을 다시 전송합니다. 델타 프롬프트(Delta prompts)는 향후 계획 목록에 있습니다.

내가 실제로 가진 질문

"선언 + 검증 + 스모크 테스트(declare + validate + smoke-test)"가 적절한 추상화 수준(altitude)일까요? 아니면 이 작업을 진지하게 수행하는 사람들은 프롬프트 수준의 제어를 원하며, 엣지 케이스(edge case)를 만나는 순간 이 추상화가 감옥처럼 느껴질까요?

제 생각은 이렇습니다. 리드 자격 확인(lead qual), 티켓 분류(ticket triage), 인바운드 의도 파악(intent on inbound)과 같은 80%의 사례에 대해서는, 아무도 쿼리 플래너(query planner)를 수동으로 작성하지 않는 것과 마찬가지로 분류 프롬프트를 수동으로 관리해서는 안 됩니다. 하지만 저는 이전에도 추상화에 대해 틀린 적이 있습니다. 여러분에게는 이 방식이 어떤 부분에서 한계에 부딪히는지 궁금합니다.

저는 이것을 EchoStack 뒤에 있는 평가 API로 패키징했습니다. 데모에서 본인의 텍스트로 평가를 실행해 보거나(가입 불필요), API 퀵스타트를 훑어보실 수 있습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
1

댓글

0