GrepBoard/매뉴얼
84 편 ·

고객 리뷰 분류 AI 로 200개 태깅하고 개수 다시 세기

·고침 ·claude-sonnet-5-5 · 2026-10-20 실행 · Python 3.12 · Ubuntu 24.04

리뷰마다 ID 와 태그 한 줄만 받고 집계는 스크립트로 냅니다. 빠진 ID·중복·태그별 개수를 검산하고, 어긋난 리뷰를 모아 태그 정의를 고치는 과정까지 실제 실행 결과로 보여 줍니다.

「불만 순위를 뽑아 줘」라고 리뷰 200개를 통째로 주면 모델은 순위와 대략의 비율을 답합니다. 그 숫자는 다시 셀 방법이 없습니다. 모델에게는 리뷰 하나당 한 줄의 태그만 받고, 세는 일은 스크립트가 합니다.

자료

가상 무선 이어폰 리뷰 200개를 id·stars·text 세 칸 TSV 로 만들었습니다(예: R003 1 팁이 커서 귀에 안 맞아요.). 스크립트로 만든 자료라 리뷰마다 정답 태그를 알고 있습니다.

프롬프트

지시·태그 목록·예시·리뷰를 각각 XML 태그로 감쌌습니다. Anthropic 의 프롬프트 작성 모범 사례는 예시 몇 개를 출력 형식을 잡는 가장 믿을 만한 방법으로 꼽습니다. 예시 ID 는 X 로 시작해 출력에 섞이면 바로 보이게 했습니다.

text
<instructions><reviews> 안의 {상품} 리뷰를 하나도 빼지 말고 한 줄씩 태그를 붙여 줘.- 태그는 <tags> 목록에 있는 것만 쓴다. 한 리뷰에 해당하는 것을 모두(최대 3개) 쉼표로.- 감정은 긍정·부정·혼합 중 하나. 칭찬과 불만이 같이 있으면 혼합.- 출력은 입력 순서대로, 리뷰 하나에 한 줄: ID|태그|감정- 집계·요약·설명은 쓰지 않는다. 표 머리줄도 쓰지 않는다.</instructions> <tags>음질: 소리·저음·고음·볼륨에 대한 말배터리: 사용 시간·충전·케이스 충전착용감: 귀 통증·빠짐·이어팁 크기배송: 도착 기간·포장 상태가격: 값·할인·가성비기타: 위 다섯에 안 맞는 것(색상·앱·설명서 등)</tags> <examples>X01|착용감|부정  ← "오래 끼면 귀가 욱신거려요."X02|배터리,가격|혼합  ← "배터리는 오래가는데 값이 좀 셉니다."X03|기타|긍정  ← "케이스 색이 예뻐요."</examples> <reviews>{리뷰 TSV}</reviews>
console
$ (cat head.ko.txt reviews.ko.tsv; echo "</reviews>") > prompt.ko.txt$ claude -p --model claude-sonnet-5-5 --max-turns 1 --tools "" --setting-sources "" --strict-mcp-config < prompt.ko.txt > out.ko.1.txt$ head -4 out.ko.1.txtR001|음질,배송|혼합R002|배터리|긍정R003|착용감|부정R004|음질|긍정

200줄이 나왔고, 두 번째 실행도 줄 단위로 같았습니다.

검산

check_tags.py 는 표준 라이브러리만 씁니다. 입력 ID 와 출력 ID 를 집합으로 비교하고, 태그·감정을 세고, 세 번째 인자로 정답 파일을 주면 무작위 20개와 전체의 일치율을 냅니다. 정답이 없는 실제 업무에서는 20개를 사람이 읽어 같은 형식의 파일로 만듭니다.

check_tags.py
#!/usr/bin/env python3"""usage: check_tags.py reviews.tsv out.txt [truth.tsv]1) ID set: missing / extra / duplicated   2) count per tag and sentiment   3) agreement on 20 random IDs"""import sys, csv, randomfrom collections import Counterids = [r['id'] for r in csv.DictReader(open(sys.argv[1], encoding='utf-8'), delimiter='\t')]lines = [l.strip() for l in open(sys.argv[2], encoding='utf-8') if l.count('|') == 2]got = {}dup = []for l in lines:    i, tags, sent = [x.strip() for x in l.split('|')]    if i in got: dup.append(i)    got[i] = (set(t.strip() for t in tags.split(',')), sent)print(f'input {len(ids)} / output lines {len(lines)}')print('missing :', sorted(set(ids) - set(got)) or '-')print('extra   :', sorted(set(got) - set(ids)) or '-')print('dup     :', dup or '-')tc = Counter(t for tags, _ in got.values() for t in tags)sc = Counter(s for _, s in got.values())print('tags    :', ', '.join(f'{k} {v}' for k, v in tc.most_common()))print('sentiment:', ', '.join(f'{k} {v}' for k, v in sc.most_common()))if len(sys.argv) > 3:    truth = {r['id']: (set(r['tags'].split(',')), r['sentiment']) for r in csv.DictReader(open(sys.argv[3], encoding='utf-8'), delimiter='\t')}    sample = random.Random(1).sample(ids, 20)    tag_ok = sum(got.get(i, (set(), ''))[0] == truth[i][0] for i in sample)    sent_ok = sum(got.get(i, (set(), ''))[1] == truth[i][1] for i in sample)    print(f'sample 20: tags match {tag_ok}/20, sentiment match {sent_ok}/20')    bad = [i for i in ids if got.get(i, (set(), ''))[0] != truth[i][0]]    print(f'all 200 : tag mismatch {len(bad)}', bad[:8])
console
$ python3 -I check_tags.py reviews.ko.tsv out.ko.1.txt truth.ko.tsvinput 200 / output lines 200missing : -extra   : -dup     : -tags    : 음질 75, 배터리 46, 착용감 43, 가격 43, 배송 41, 기타 12sentiment: 부정 128, 긍정 51, 혼합 21sample 20: tags match 19/20, sentiment match 20/20all 200 : tag mismatch 6 ['R070', 'R093', 'R110', 'R123', 'R128', 'R195']

빠진 리뷰는 없었고 감정 개수도 정답과 같았습니다. 가격만 43개로, 정답 37개보다 6개 많았습니다. 무작위 20개 표본에는 그중 1개만 걸렸습니다.

어긋난 6개는 한 문장이었다

grep 으로 6개를 보면 모두 같은 문장이 들어 있습니다.

console
$ grep -E '^(R093|R070)' reviews.ko.tsv out.ko.1.txtreviews.ko.tsv:R070	3	일주일 넘게 걸렸어요. 이 가격에 이 소리면 충분합니다.reviews.ko.tsv:R093	5	이 가격에 이 소리면 충분합니다.out.ko.1.txt:R070|음질,배송,가격|혼합out.ko.1.txt:R093|음질,가격|긍정

「이 가격에 이 소리면 충분합니다」는 소리에 대한 칭찬인데, 모델은 가격도 붙였습니다. 틀렸다기보다 태그 정의가 그 경우를 정하지 않은 것입니다. 처음 정의는 가격: 값·할인·가성비 였고, 「이 가격에 이 소리면」은 값을 따지는 말처럼 읽혀 가격 태그가 붙었습니다. 정의 한 줄을 고쳤습니다.

text
가격: 값·할인을 평가하는 말. 「이 가격에 이 소리면」처럼 다른 것을 칭찬·비판하는 기준으로만 나오면 넣지 않는다
output
…tags    : 음질 75, 배터리 46, 착용감 43, 배송 41, 가격 37, 기타 12sentiment: 부정 128, 긍정 51, 혼합 21sample 20: tags match 20/20, sentiment match 20/20all 200 : tag mismatch 0 []

어긋난 리뷰는 하나씩 고치지 말고 공통 문장을 찾아 정의를 고칩니다. 영어판에서는 같은 정의를 「넣지 않는다」까지만 써도, 「그 대신 원래 대상(sound)에 태그한다」까지 써도 불일치가 0이었습니다. 빼는 규칙만 있으면 그 문장의 음질 태그까지 빠질 수 있으니, 어디에 태그하는지까지 적어 두는 쪽이 안전합니다.

#review-analysis#claude#tagging#python