댓글이 쌓이고 있었다
카페 방명록에 댓글이 달리면, 사람이 직접 보는 게 아니라 로컬 LLM이 먼저 검수를 합니다. 욕설인지, 광고인지, 그냥 좋은 후기인지 판단해서 자동으로 처리하는 구조예요.
https://cafepurplemint.com (깨알같은 홍보)
그런데 어느 날 로그를 보다가 이상한 걸 발견했습니다.
[MOD] Processing 5 pending entries...
[MOD] Processing 5 pending entries...
[MOD] Processing 5 pending entries...
같은 로그가 계속 찍히고 있었어요. 처리가 끝나지 않은 채로 다음 폴링이 돌고 있었습니다.
원인은 단순했습니다. 댓글 하나 검수하는 데 평균 5.6초. 폴링 주기는 1초. 수학적으로 댓글이 조금만 몰려도 처리 큐가 쌓일 수밖에 없었습니다. (사실 카페 홈페이지를 만들고. 지인 단체방에 댓글 한번 해보라고 홍보아닌 홍보를 했었죠.)
프롬프트부터 뭔가 잘못됐다
모델을 바꾸기 전에 먼저 프롬프트를 들여다봤습니다.
기존 방식은 이랬어요.
아래 기준에 하나라도 해당하면 pass: false 로 판단하세요:
- 욕설 또는 비속어
- 광고, 스팸, 외부 링크 포함
- 특정 인물 또는 업체 비방, 허위 사실
- 개인정보 포함 (전화번호, 주소, 이메일 등)
- 혐오 표현, 차별적 발언
금지 목록을 나열하는 방식이었죠. 근데 카페 방명록이라는 특성을 생각해보면, 사실 올라와야 할 댓글이 훨씬 더 좁습니다. 방문 후기, 칭찬, 감사 인사. 그게 전부예요.
그래서 정책을 뒤집었습니다.
❌ 기존: "이것들은 거절해" (금지 목록)
✅ 변경: "이것만 허용해" (허용 목록)
카테고리도 세분화했고요.
| category | 처리 방식 |
|---|---|
clean |
자동 승인 |
profanity, spam, advertising, defamation |
자동 거절 |
negative, meaningless, other |
Telegram으로 관리자 판단 |
meaningless는 이번에 새로 추가한 카테고리예요. ㅋㅋㅋㅋㅋ, abcde, 이모지만 있는 댓글들. 나쁜 것도 아니고 좋은 것도 아닌, 그냥 의미 없는 것들. 자동 거절은 좀 억울할 것 같아서 관리자한테 넘깁니다.
프롬프트 끝에는 category 허용값을 명시하는 줄도 추가했습니다. 소형 모델 테스트 중에 "offensive" 같은 스펙에 없는 값을 반환하는 걸 발견했거든요.
category 는 반드시 다음 중 하나여야 합니다:
"clean", "profanity", "spam", "advertising", "defamation", "negative", "meaningless", "other"
세 모델을 줄 세워봤다
프롬프트를 정비하고 나서, 후보 모델 세 개를 동일한 8개 케이스로 테스트했습니다. curl로 직접 날려서 응답시간, 정확도, 토큰 수를 비교했어요.
정확도
| 케이스 | gpt-oss:20b | gemma3:4b-it-qat | qwen2.5:3b-instruct |
|---|---|---|---|
| clean (정상 후기) | ✅ | ✅ | ✅ |
| profanity (욕설) | ✅ | ✅ | ✅ |
| spam (스팸 링크) | ✅ | ✅ | ❌ advertising |
| advertising (타 업체 홍보) | ✅ | ❌ clean | ❌ clean |
| defamation (비방) | ✅ | ❌ negative | ❌ negative |
| negative (부정 의견) | ✅ | ✅ | ✅ |
| meaningless (ㅋㅋㅋ) | ✅ | ❌ clean | ❌ other |
| other (화장실 비번?) | ✅ | ✅ | ❌ profanity |
| 합계 | 8/8 | 5/8 | 3/8 |
qwen2.5:3b는 “화장실 비번이 뭔가요?“를 profanity로 분류했습니다. 욕설이라고요. 이건 그냥 못 쓰는 모델입니다.
속도 및 토큰
| gpt-oss:20b | gemma3:4b | qwen2.5:3b | |
|---|---|---|---|
| 평균 응답시간 | 5,584ms | 1,394ms | 1,187ms |
| 평균 prompt 토큰 | 594 | 359 | 402 |
| 평균 completion 토큰 | 23 | 37 | 31 |
소형 모델들은 4~5배 빠릅니다. 근데 틀립니다. 특히 advertising은 두 모델 모두 통과시켜버렸어요.
2단계 파이프라인을 시도해봤는데
“소형 모델이 빠르니까 1차로 쓰고, 걸리면 gpt-oss가 재확인하면 되지 않나?”
그럴듯한 생각이었습니다. 먼저 clean 오탐률을 확인했어요. 좋은 댓글을 잘못 거절하는 경우가 있으면 쓸 수가 없으니까요.
10개의 정상 댓글을 넣어봤더니, gemma3:4b와 qwen2.5:3b 모두 오탐 0% 였습니다. 좋은 신호였죠.
그런데 반대 방향이 문제였어요. 소형 모델이 나쁜 댓글을 clean으로 통과시키면, gpt-oss를 거치지 않고 바로 게시됩니다. 이 “누락 위험"을 측정해봤습니다.
| gemma3:4b | qwen2.5:3b | |
|---|---|---|
| 나쁜 댓글 감지율 | 4/7 (57%) | 6/7 (85%) |
| gpt-oss 없이 게시되는 나쁜 댓글 | 3건 | 1건 |
qwen2.5가 더 나았지만, advertising은 두 모델 모두 어떤 프롬프트 변형을 써도 잡지 못했습니다. 7가지 변형을 시도했는데, 전부 실패였어요. 3~4b 모델이 “이 카페가 아닌 다른 곳을 홍보하는 글"이라는 개념 자체를 이해하지 못하는 것 같았습니다.
2단계 파이프라인은 포기했습니다.
gemma3:12b가 모든 걸 해결했다
마지막으로 gemma3:12b-it-qat를 테스트했습니다. 12b짜리니까 당연히 4b보다는 나을 거라 예상했는데, 결과가 생각보다 훨씬 좋았어요.
advertising 패턴 5개를 포함한 12개 케이스 전부를 기본 프롬프트 그대로 맞혔습니다.
✅ clean → clean (정상 후기)
✅ profanity → profanity (욕설)
✅ spam → spam (스팸 링크)
✅ advertising → advertising (타 업체 홍보) ← 4b가 못 잡던 것
✅ defamation → defamation (비방)
✅ negative → negative (부정 의견)
✅ meaningless → meaningless (ㅋㅋㅋ)
✅ other → other (화장실 비번)
✅ advertising → advertising (@SNS계정 포함)
✅ advertising → advertising (저희 가게도 오세요)
✅ advertising → advertising (외부 링크)
✅ advertising → advertising (암묵적 홍보)
그리고 빠릅니다.
| gpt-oss:20b | gemma3:12b-it-qat | |
|---|---|---|
| 평균 응답시간 | 5,584ms | 3,072ms |
| 정확도 | 8/8 | 12/12 |
| 평균 prompt 토큰 | 594 | 371 |
기존 모델보다 45% 빠르고, 더 정확하고, 토큰도 적게 씁니다. 2단계 파이프라인 같은 복잡한 구조도 필요 없어요.
gemma3 모델 계열 비교
같은 gemma3 계열인데 4b와 12b의 차이가 이렇게 크다는 게 흥미로웠습니다.
| gemma3:4b-it-qat | gemma3:12b-it-qat | |
|---|---|---|
| 전체 정확도 | 5/8 | 12/12 |
| advertising 감지 | 2/5 | 5/5 |
| clean 오탐 | 0% | 0% |
| 평균 응답시간 | 1,394ms | 3,072ms |
| 프롬프트 변형 필요 | 있어도 안 됨 | 불필요 |
4b는 아무리 프롬프트를 다듬어도 advertising을 안정적으로 잡지 못했어요. 12b는 기본 프롬프트에서 바로 됐고요. 모델 크기 차이가 단순히 속도의 차이가 아니라 개념 이해 수준의 차이라는 걸 이번 테스트에서 확실히 체감했습니다.
결론 — .env 한 줄
2단계 파이프라인도, 프롬프트 튜닝도 필요 없었습니다.
# before
LLM_MODEL=gpt-oss:20b
# after
LLM_MODEL=gemma3:12b-it-qat
한 줄 바꿨습니다.
응답시간 5.6초 → 3.1초. 정확도 8/8 → 12/12. 댓글 큐가 쌓이는 문제도 사라졌고요.
로컬 LLM은 결국 모델 선택이 전부라는 걸 다시 한번 확인했습니다. 프롬프트 엔지니어링으로 할 수 있는 것과 없는 것이 있는데, 모델이 개념 자체를 이해하지 못하면 프롬프트로는 한계가 있습니다. 그 한계를 만났을 때 답은 생각보다 단순했어요. 더 큰 모델.
아직 advertising 중에서도 암묵적인 홍보(“여기도 좋지만 저희 가게도…”) 같은 케이스는 규칙 기반 pre-filter를 추가하면 더 좋아질 것 같습니다. 하지만 그건 다음 이야기고요. 일단 지금은 충분합니다.