업무용 스크립트 요청문에 입력·출력·오류 원칙 담기
“자동화해 줘”를 실행 가능한 작업 설명으로 바꿉니다. 민감정보 없는 합성 샘플과 손으로 확인한 기대 결과를 붙여, 팀별 작업 기록 집계 스크립트를 요청하는 문장을 완성합니다.
AI는 고객지원 티켓에 빠르게 라벨을 붙일 수 있지만, 실제로 사용하기 전에 사람이 직접 라벨링한 소규모 기준 데이터와 비교해 성능을 측정해야 합니다. 이 글에서는 작은 합성 예제를 사용해 AI 예측을 정답 라벨과 비교하고, Python으로 정확도와 혼동행렬을 계산합니다.
이런 분께AI 채팅 도우미로 고객지원 티켓 같은 짧은 텍스트를 분류하고, 분류 정확도를 간단하게 측정하고 싶은 사람을 위한 내용입니다.
AI 채팅 도우미는 고객지원 티켓, 이메일, 댓글, 짧은 메모에 카테고리를 빠르게 붙일 수 있습니다. 하지만 몇 개의 예측이 그럴듯해 보인다고 해서 전체 분류 정확도를 알 수 있는 것은 아닙니다. 중요한 것은 개별 예측이 자연스러워 보이는지가 아니라, 사람이 미리 라벨링한 기준 데이터와 AI가 얼마나 자주 일치하는지를 확인하는 것입니다.
작은 수작업 라벨 데이터셋을 사용하면 고정된 정답표를 만들 수 있습니다. 각 AI 예측을 그 정답과 비교하고 정확도를 계산한 뒤, 혼동행렬을 통해 어떤 클래스끼리 자주 혼동되는지 확인할 수 있습니다. 정확도는 전체 성능을 하나의 숫자로 보여주고, 혼동행렬은 오류가 발생한 구체적인 패턴을 보여줍니다.
다음 8개의 고객지원 티켓은 이 튜토리얼을 위해 만든 합성 예제입니다. 라벨은 billing, login, bug 세 개를 사용합니다. billing은 결제나 청구서 문제, login은 인증이나 계정 접근 문제, bug는 소프트웨어가 정상적으로 동작하지 않는 문제를 의미합니다. hand_label 열은 AI에게 분류를 요청하기 전에 사람이 미리 지정한 기준 라벨입니다.
| id | ticket | hand_label |
|---|---|---|
| T1 | I was charged twice for the same order. | billing |
| T2 | My password reset link says it has expired. | login |
| T3 | The app closes whenever I open the reports page. | bug |
| T4 | Where can I download last month's invoice? | billing |
| T5 | I cannot sign in after changing my email address. | login |
| T6 | The export button does nothing when I click it. | bug |
| T7 | My card was declined but the bank says it is fine. | billing |
| T8 | The dashboard shows a blank screen after login. | bug |
데이터셋이 작기 때문에 예상 라벨을 모두 사람이 직접 검토할 수 있습니다. 따라서 더 큰 라벨 데이터셋에 같은 방법을 적용하기 전에 평가 흐름 자체가 올바른지 확인하기에 적합합니다.
분류 프롬프트는 허용되는 라벨과 출력 형식을 고정할수록 평가하기 쉽습니다. 별도의 설명이 필요하지 않다면 자유 형식 설명을 요구하지 않는 편이 좋습니다. 불필요한 문장이 추가되면 결과 비교가 어려워집니다.
AI가 다음과 같은 합성 예측을 만들었다고 가정합니다. 8개 중 6개가 정답입니다. T7은 기준 라벨이 billing이지만 AI는 login으로 예측했고, T8은 기준 라벨이 bug지만 AI는 login으로 예측했습니다.
| id | hand_label | ai_label | correct |
|---|---|---|---|
| T1 | billing | billing | yes |
| T2 | login | login | yes |
| T3 | bug | bug | yes |
| T4 | billing | billing | yes |
| T5 | login | login | yes |
| T6 | bug | bug | yes |
| T7 | billing | login | no |
| T8 | bug | login | no |
따라서 전체 정확도는 8개 티켓 중 6개 정답입니다. 손으로 계산하면 6 ÷ 8 = 0.75이므로 정확도는 75%입니다.
혼동행렬은 기준 라벨과 예측 라벨의 조합별 개수를 세는 표입니다. 이 예제에서는 행을 hand_label, 열을 AI 예측 라벨로 둡니다. 대각선에 있는 값은 정답이고, 대각선 밖의 값은 잘못 분류된 사례입니다.
| hand \ predicted | billing | login | bug |
|---|---|---|---|
| billing | 2 | 1 | 0 |
| login | 0 | 2 | 0 |
| bug | 0 | 1 | 2 |
이 행렬을 보면 두 오류의 형태를 바로 확인할 수 있습니다. billing 티켓 하나가 login으로 잘못 분류되었고, bug 티켓 하나도 login으로 잘못 분류되었습니다. 반대로 이 작은 예제에서는 기준 라벨이 login인 티켓을 billing이나 bug로 잘못 예측한 사례는 없습니다. 이것이 login 클래스가 일반적으로 가장 잘 분류된다는 의미는 아니며, 단지 이 8개의 합성 사례에서 나타난 결과일 뿐입니다.
비교 데이터를 id, hand_label, ai_label 열이 있는 ticket_labels.csv에 저장합니다. 아래 스크립트는 라벨이 허용된 값인지 확인하고, 정답 개수를 세고, 혼동행렬을 만든 뒤 결과를 outputs/classification_report.txt에 저장합니다. Python 3.12 표준 라이브러리만 사용하고 입력 파일은 변경하지 않으며, 출력 파일이 이미 존재하면 중단합니다.
import csv
from collections import Counter
from pathlib import Path
INPUT_FILE = Path("ticket_labels.csv")
OUTPUT_DIR = Path("outputs")
OUTPUT_FILE = OUTPUT_DIR / "classification_report.txt"
LABELS = ["billing", "login", "bug"]
def main():
if not INPUT_FILE.is_file():
raise SystemExit(f"Input file not found: {INPUT_FILE}")
if OUTPUT_FILE.exists():
raise SystemExit(f"Output already exists: {OUTPUT_FILE}")
rows = []
with INPUT_FILE.open("r", encoding="utf-8", newline="") as file:
reader = csv.DictReader(file)
required = {"id", "hand_label", "ai_label"}
if reader.fieldnames is None or not required.issubset(reader.fieldnames):
raise SystemExit("ticket_labels.csv is missing required columns.")
for row_number, row in enumerate(reader, start=2):
hand = row["hand_label"].strip()
predicted = row["ai_label"].strip()
if hand not in LABELS:
raise SystemExit(f"Invalid hand_label on row {row_number}: {hand}")
if predicted not in LABELS:
raise SystemExit(f"Invalid ai_label on row {row_number}: {predicted}")
rows.append((row["id"].strip(), hand, predicted))
if not rows:
raise SystemExit("No labeled rows found.")
correct = sum(1 for _, hand, predicted in rows if hand == predicted)
accuracy = correct / len(rows)
counts = Counter((hand, predicted) for _, hand, predicted in rows)
report = []
report.append(f"tickets={len(rows)}")
report.append(f"correct={correct}")
report.append(f"accuracy={accuracy:.3f}")
report.append("")
report.append("confusion_matrix")
report.append("hand\\predicted\t" + "\t".join(LABELS))
for hand in LABELS:
values = [str(counts[(hand, predicted)]) for predicted in LABELS]
report.append(hand + "\t" + "\t".join(values))
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
OUTPUT_FILE.write_text("\n".join(report) + "\n", encoding="utf-8")
print(f"Wrote: {OUTPUT_FILE}")
if __name__ == "__main__":
main()
75%라는 정확도 숫자보다 두 개의 오분류 사례를 직접 확인하는 것이 더 중요할 수 있습니다. T7에는 card라는 단어가 포함되어 있으므로 정의가 불명확한 프롬프트에서는 계정 접근 문제와 혼동될 수 있지만, 이 예제의 기준 분류 체계에서는 billing입니다. T8에는 after login이라는 표현이 있지만 실제로 보고된 문제는 빈 대시보드 화면이므로 기준 체계에서는 bug로 분류됩니다. 이런 사례는 개별 키워드보다 실제 문제의 유형을 기준으로 라벨 정의를 작성해야 한다는 점을 보여줍니다.
혼동행렬은 테스트한 표본에서 나타난 결과를 설명하는 도구이지 미래 성능을 증명하는 것은 아닙니다. 작은 균형 예제는 평가 방법을 익히는 데는 유용하지만, 실제 평가에서는 실제 티켓의 표현 방식, 애매한 사례, 클래스 빈도를 충분히 반영할 수 있는 규모의 수작업 라벨 데이터가 필요합니다.
2026-09-21 · hand-checked example · Python 3.12
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.