LLM으로 텍스트 분류기 만들기: 라벨 정의, few-shot 예시, 혼동행렬로 평가하기

라벨 정의서, 경계 사례 예시, enum으로 출력 고정, 정답셋 평가와 혼동행렬 읽는 법, 개선 반복 순서

LLM 텍스트 분류기 만들기 썸네일

핵심 요약

  1. 라벨마다 정의·포함·제외·겹칠 때 우선순위를 적은 정의서가 분류 품질을 좌우한다.
  2. few-shot 예시는 전형 사례보다 헷갈리는 라벨 쌍의 경계 사례로 고르고 정답셋과 겹치지 않게 한다.
  3. 구조화 출력의 enum으로 라벨을 고정하고, 대소문자 차이를 대비해 비교 전에 소문자로 맞춘다.
  4. 혼동행렬 C[i, j]는 정답 i가 j로 예측된 건수이며, 대각선 밖 큰 값이 고칠 라벨 쌍이다.
  5. 한 번에 하나만 바꾸고 같은 정답셋으로 다시 평가해 변경 이력과 점수를 함께 남긴다.

고객 문의를 "결제", "배송", "계정", "기타"로 나누는 일처럼 라벨이 정해진 분류는 LLM에 맡기기 좋은 작업이다. 학습 데이터 없이 바로 시작할 수 있기 때문이다. 하지만 프롬프트에 라벨 이름만 나열하면 경계에 걸친 문의에서 결과가 흔들리고, 얼마나 정확한지도 알 수 없다. 이 글은 라벨 정의서 → 예시 → 출력 고정 → 정답셋 평가 → 혼동행렬로 개선하는 순서를 다룬다. 읽고 나면 분류 정확도를 숫자로 보고, 어느 라벨 쌍이 헷갈리는지 찾아 고칠 수 있다.

라벨 정의서를 먼저 쓴다

LLM 분류 품질은 라벨 이름이 아니라 라벨 정의에서 나온다. 사람 두 명이 같은 문의를 다르게 분류한다면 모델도 흔들린다. 라벨마다 다음 네 가지를 적는다.

항목예: billing(결제)
정의요금 청구, 결제 수단, 환불 금액에 관한 문의
포함"카드 결제가 두 번 됐어요", "환불 금액이 달라요"
제외"환불 신청은 어디서 하나요" → 결제가 아니라 절차 문의라면 account나 기타로
겹칠 때 우선순위배송 지연으로 환불을 요구하면 billing 우선

"기타" 라벨은 반드시 둔다. 기타가 없으면 모델은 어느 라벨에도 맞지 않는 문의를 가장 비슷한 라벨에 억지로 넣고, 그 오류는 평가에서도 잘 드러나지 않는다.

few-shot 예시는 경계 사례로 고른다

예시를 라벨마다 전형적인 문장으로만 채우면 모델이 이미 잘하는 부분을 반복해 보여 주는 셈이다. 효과가 큰 예시는 정의서의 "제외"와 "우선순위"에 해당하는 경계 사례다. 라벨당 1~2개의 전형 사례와 헷갈리는 쌍마다 1~2개의 경계 사례를 넣는다. 예시는 평가용 정답셋과 겹치지 않게 따로 뽑는다. 겹치면 평가 점수가 실제보다 높게 나온다.

출력을 enum으로 고정하기

자유 텍스트로 라벨을 받으면 "결제 관련", "Billing", "billing입니다" 같은 변형이 생긴다. Claude 구조화 출력 문서는 output_config.format에 JSON 스키마를 넣어 응답 형식을 고정하는 방법과 enum 지원을 안내한다. 문서에는 enum 값이 대소문자만 다르게 돌아올 수 있다는 주의가 있으므로 비교할 때는 소문자로 맞춘다.

import anthropic, json

client = anthropic.Anthropic()
LABELS = ['billing', 'shipping', 'account', 'other']
GUIDE = open('label_guide.md', encoding='utf-8').read()     # 라벨 정의서 + 예시
SCHEMA = {
    'type': 'object',
    'properties': {
        'label': {'type': 'string', 'enum': LABELS},
        'reason': {'type': 'string'},
    },
    'required': ['label', 'reason'],
    'additionalProperties': False,
}
SCHEMA_FORMAT = {'type': 'json_schema', 'schema': SCHEMA}

def classify(text: str) -> dict:
    res = client.messages.create(
        model='claude-haiku-5-5',
        max_tokens=200,
        system=GUIDE,
        messages=[{'role': 'user', 'content': text}],
        output_config={'format': SCHEMA_FORMAT},
    )
    out = json.loads(next(b.text for b in res.content if b.type == 'text'))
    out['label'] = out['label'].lower()
    return out

reason 필드는 평가 단계에서 오분류 원인을 읽기 위한 것이다. 운영 비용이 문제라면 평가가 끝난 뒤 빼도 된다. 정의서가 길다면 system에 고정해 두고 프롬프트 캐싱을 적용하면 반복 호출 비용을 줄일 수 있다. 스키마 기반 출력의 일반 원리는 LLM 구조화 출력에서 다뤘다.

정답셋으로 평가하기

  1. 실제 문의 100~300건을 뽑는다 — 라벨 분포가 실제와 비슷하게, 드문 라벨도 최소 20건 이상 들어가게 한다.
  2. 사람이 정답 라벨을 단다 — 가능하면 두 명이 따로 달고, 의견이 갈린 건은 정의서를 고치는 재료로 쓴다.
  3. 분류기를 돌려 예측을 모은다.
  4. 정확도와 혼동행렬을 계산한다.
from sklearn.metrics import confusion_matrix, classification_report

y_true = [r['label'] for r in gold]
y_pred = [classify(r['text'])['label'] for r in gold]

print(confusion_matrix(y_true, y_pred, labels=LABELS))
print(classification_report(y_true, y_pred, labels=LABELS, zero_division=0))

scikit-learn 문서에 따르면 혼동행렬 C[i, j]는 실제 라벨이 i인데 j로 예측된 건수다. 행은 정답, 열은 예측이다. labels로 순서를 지정해야 행렬을 읽기 쉽고, normalize='true'를 주면 행 단위 비율로 바뀌어 라벨별 재현율을 바로 볼 수 있다.

혼동행렬에서 고칠 곳 찾기

              예측→  billing  shipping  account  other
정답 billing          41         6        1      2
     shipping          9        58        0      3
     account           0         1       37      2
     other             4         5        3     28

위 예에서 대각선 밖의 큰 값은 billing↔shipping(6, 9)과 other가 다른 라벨로 새는 부분(4, 5, 3)이다. 고치는 순서는 다음과 같다.

  • billing↔shipping: 해당 칸의 실제 문의와 reason을 읽는다. "배송 지연 보상 요청"처럼 정의서에 우선순위가 없던 유형이 원인이라면 정의서에 규칙을 추가하고 그 유형의 경계 예시를 넣는다.
  • other가 새는 문제: 모델이 기타를 피하는 경향이면 정의서에 "확신이 없으면 other"를 명시한다. 반대로 기타에 특정 주제가 몰린다면 새 라벨이 필요한지 검토한다.
  • 정답 자체가 틀린 경우: 오분류를 읽다 보면 사람이 단 정답이 틀린 경우도 나온다. 정답셋을 고치고 그 사실을 기록한다.

한 번에 하나만 바꾸고 같은 정답셋으로 다시 평가한다. 정의서와 예시를 바꾼 이력, 점수 변화를 함께 남기면 회귀가 생겼을 때 되돌릴 수 있다. 이 관리 방법은 프롬프트를 코드처럼 버전 관리하기에서, 배포 전 평가를 CI로 자동화하는 방법은 프로덕션 배포 전 LLM 평가에서 다뤘다.

운영에 넣기 전 정할 것

분류 결과로 자동 처리(담당 팀 배정, 자동 답변)를 한다면 오분류 비용이 큰 라벨부터 정확도 기준을 정한다. 기준에 못 미치는 라벨은 사람 확인 대기열로 보낸다. 운영 중에는 담당자가 라벨을 수정한 건을 모아 정기적으로 정답셋에 추가하면, 문의 유형이 바뀌는 것을 평가에 반영할 수 있다.

초보자가 자주 실수하는 포인트

  • 라벨 이름만 나열하고 정의와 우선순위를 주지 않는 경우
  • 기타 라벨 없이 모든 문의를 정해진 라벨에 넣게 하는 경우
  • few-shot 예시를 평가 정답셋에서 가져와 점수가 부풀려지는 경우
  • 전체 정확도만 보고 드문 라벨의 낮은 재현율을 놓치는 경우

체크리스트

  • 라벨 정의서에 포함·제외·우선순위가 있는가
  • 기타 라벨이 있는가
  • 출력이 enum 스키마로 고정됐는가
  • 정답셋에 드문 라벨이 충분히 들어갔는가
  • 혼동행렬을 라벨 순서를 지정해 확인했는가

자주 묻는 질문

라벨이 여러 개 붙을 수 있는 문의는 어떻게 하나요?

스키마를 label 하나가 아니라 enum 값의 배열로 바꾸면 다중 라벨 분류가 됩니다. 평가도 라벨별로 맞았는지를 따로 집계해야 하므로 혼동행렬 대신 라벨별 정밀도·재현율을 봅니다.

작은 모델로도 충분한가요?

라벨 수가 적고 정의가 명확하면 작은 모델로도 기준을 넘는 경우가 많습니다. 같은 정답셋으로 작은 모델과 큰 모델을 모두 평가해 정확도 차이와 비용 차이를 비교한 뒤 정합니다.

참고 자료 · 검증 기준

위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.

이 글은 실제 실무 환경 테스트 및 검증된 기술 자료를 바탕으로 작성되었으며, 환경 변화에 따라 지속적으로 갱신됩니다.