본문으로 바로가기
Archive for AI Study
Project

광고 안전성 필터에서 과차단을 줄인 과정

캐릭터챗은 유저가 자유롭게 스토리를 만드는 서비스다. 광고 제작을 자동화하려면 이미지를 잘 만드는 것만큼, 만들면 안 되는 광고를 먼저 거르는 것도 필요했다. 서비스에 있는 스토리라고 모두 광고로 내보낼 수 있는 건 아니었다.

캐릭터챗은 유저가 자유롭게 스토리를 만드는 서비스다. 광고 제작을 자동화하려면 이미지를 잘 만드는 것만큼, 만들면 안 되는 광고를 먼저 거르는 것도 필요했다. 서비스에 있는 스토리라고 모두 광고로 내보낼 수 있는 건 아니었다.

문제가 있는 소재를 놓치는 것은 피해야 했지만, 민감한 단어가 한 번 등장했다는 이유로 모두 차단하면 정상적인 판타지까지 빠졌다. 처음에는 보수적으로 잡고, 실제 판정 사례를 보면서 범위를 좁혀 갔다.

살아 있는 스토리인지 먼저 확인했다

BigQuery에 적재된 데이터에는 삭제됐거나 비공개로 바뀐 스토리도 있었다. 광고를 클릭하면 딥링크로 해당 스토리에 들어가야 하는데, 서비스에 없는 작품을 광고하면 유입이 끊긴다. 그래서 공개·활성 상태, 삭제·차단 여부, 국가코드 등은 쿼리 단계에서 먼저 확인했다.

제작할 때는 살아 있었지만 집행 중에 제작자가 내리거나 차단되는 경우도 있었다. 전처리 한 번으로 끝낼 수 없는 문제여서 주간 크론으로 라이브 상태를 다시 조회하는 흐름을 뒀다.

성인물과 2차창작 여부도 데이터에 필드가 있으면 먼저 제외했다. 매체 가이드라인을 참고한 생성 가드레일과 별개로, 구조화된 데이터로 바로 판별할 수 있는 조건은 앞단에서 처리했다.

어느 단계에서 무엇을 거를 것인가

필터는 한 곳에만 두지 않았다. 데이터 적재에서는 스토리 자체의 광고 적합성을 보고, 이미지 후보에서는 광고에 쓸 수 없는 그림을 걸렀다. 카피 생성 프롬프트에도 안전 가이드를 넣었다. 원문 스토리가 통과해도 생성 과정에서 표현이 달라질 수 있기 때문이다.

LLM 분류의 결과는 safe와 blocked로 받았다. 초기에는 애매하면 blocked로 분류하고, 한 축이라도 문제에 해당하면 차단하는 방향이었다. 오리지널 스토리는 서비스에서 별도 검수한 공식 작품이라 이 검사에서 면제했다. 자동 판정이 오리지널이면 안전하다고 알아낸 것이 아니라, 이미 검수한 콘텐츠를 별도 경로로 처리한 정책이었다.

판정축 무엇 크랙 OOC 카라푸
minor_sexual 미성년 성적 묘사 ● ● ●
discrimination 차별 + 노예·인신매매의 노골적 성적 묘사 ● ● ●
self_harm 자해 ● ● ●
gambling 도박이 스토리의 중심 주제일 때만 ● ● ●
politically_sensitive 선거·진영 갈등 ● — —
religion_political 2026-08-05 전 스코프 은퇴 — — —

분류축과 적용 서비스는 운영 중에 바뀌었다. 정치 관련 기준은 한국 서비스와 해외 서비스에서 같지 않았고, 종교축은 이후 제거했다. 아래 실험도 당시 사용했던 기준 안에서 읽어야 한다.

문제 사례를 잡으면서 정상 스토리의 오차단 줄이기

초기 테스트셋은 문제가 있었던 스토리 세 개와 랜덤으로 뽑은 정상 스토리 97개였다. 당시에는 소재 수가 줄어드는 것보다 문제가 있는 광고가 나가는 것이 더 치명적이라고 봤다. 그래서 blocked recall을 우선했다.

Claude Opus 4.6으로 분류했을 때 문제 사례는 3/3건을 잡았다. 반면 정상으로 라벨링한 97건 중 일곱 건도 blocked로 분류됐다. 이 일곱 건이 평가셋 기준의 False Positive, 즉 오차단이었다. 문제 사례를 놓치지 않는 수준을 유지하면서, 정상 소재가 어떤 이유로 걸렸는지 확인해 과차단을 줄이는 것이 다음 과제였다. blocked 예측 열 건 중 실제 문제 사례가 세 건이라 precision은 0.30이었다.

지표 값 의미
blocked recall = 1.00 3/3 🎯 핵심 성과 – GT 누수 0건
safe recall = 0.928 90/97 7건 false positive (오차단)
blocked precision = 0.30 3/10 모델 blocked라 한 10건 중 3건만 진짜
F1 = 0.46 – precision이 낮아 F1 저하
MCC = 0.528 – 불균형 데이터 고려 시 양호
Accuracy = 0.93 – 전체적으로 잘 분류

평가에서 차단 대상으로 지정한 세 사례는 장애를 대상화하는 맥락, 실존 정치인과 정치적 갈등을 중심으로 한 스토리, 영토 분쟁을 소재로 한 스토리였다. 정상 라벨과 판정이 달랐던 일곱 건의 이유도 따로 읽었다. 판타지 종교 설정, 역사적 배경, 본문 안의 자해나 미성년 관련 표현 등이 있었다.

처음 붙인 정상 라벨과 모델의 판단이 다르면, 모두 모델의 잘못이라고 바로 결론 내릴 수는 없다. 판정 기준과 라벨이 일치하는지도 다시 봐야 했다. 중요한 것은 모델이 어떤 근거로 차단했는지 남겨 다음 기준 조정에 쓸 수 있다는 점이었다.

[오체불만족] (gt_category=discrimination)
    pred_status    : blocked
    pred_category  : discrimination
    reason         : 장애(사지결손)를 핵심 소재로 다루며, 태그·시스템 프롬프트에서 장애를 대상화하는 맥락이 드러나 discrimination으로 판단함.
    latency        : 59.4s
    → status ✓ | category ✓

[리버스 1979] (gt_category=politically_sensitive)
    pred_status    : blocked
    pred_category  : politically_sensitive
    reason         : 박정희·김대중·전두환·김재규 등 실존 한국 정치인 실명이 다수 등장하고 정치적 사건·갈등을 스토리의 핵심 소재로 삼아 politically_sensitive로 판단함.
    latency        : 59.4s
    → status ✓ | category ✓

[독도는 일본땅] (gt_category=politically_sensitive)
    pred_status    : blocked
    pred_category  : politically_sensitive
    reason         : 제목·시스템 프롬프트·대사에서 독도/다케시마 영토 분쟁을 핵심 소재로 삼고 있어 politically_sensitive로 판단함.
    latency        : 59.4s
    → status ✓ | category ✓
False Positive: 7 건

- [사랑하는 나의 증오에게] → self_harm
  reason: content_summary에서 '시체를 끌어안은 채 제 심장을 찌를 겁니다'라며 직접적인 자해 표현을 사용해 self_harm으로 분류함.

- [1933년 독일에서 살아남기s] → politically_sensitive
  reason: title·system_prompt·initial_messages에서 '1933년 독일'이라는 시대·정치적 배경이 명시되어 정치적으로 민감한 설정으로 판단함.

- [지옥과 천국에서의 삶] → religion_political
  reason: userPrompt 및 세계관 설정에서 기독교의 실존 천사(미카엘, 가브리엘 등)와 종교 세계관이 등장해 religion_political로 분류함.

- [조정혀] → minor_sexual
  reason: userPrompt 필드에서 중학교(꿈결중학교) 재학생 캐릭터가 '야한 드립을 한다'는 성적 대화 설정을 갖고 있어 미성년자와 성적 맥락의 결합으로 판단함.

- [미래를 아는 자 과거를 바꾸다] → politically_sensitive
  reason: content_summary 및 initial_messages에서 '박정희 대통령 시해' 등 실존 정치인과 정치적 사건을 핵심 소재로 제시하여 politically_sensitive로 판단함.

- [원효] → religion_political
  reason: title, tags, content_summary 등에서 실존 불교 승려 '원효대사'를 중심인물로 제시해 religion_political로 분류함.

- [천국 시뮬레이션] → religion_political
  reason: customPrompt의 테마 필드에 '종교'가 명시되어 있고, 스토리 전체가 천국이라는 종교적 세계관을 중심으로 구성되어 religion_political로 분류함.

이 실험의 총 비용은 5.07달러, 스토리당 평균은 0.0507달러였다. 이후 Gemini 3.1 Flash Lite로 바꿔 비교했고, 당시 기록에서는 비용이 약 52배 저렴하면서 문제 세 건의 recall을 유지했다.

안전성 분류 모델의 비용·판정 결과 비교

안전성 분류 모델의 비용·판정 결과 비교

비교 실험의 blocked precision·recall·F1

비교 실험의 blocked precision·recall·F1

비교 이미지에는 blocked precision 0.375, recall 1.000, F1 0.545가 남아 있다. 다만 문제 사례가 세 개뿐인 실험의 recall 1을 일반적인 운영 성능으로 볼 수는 없다. 더 다양한 문제가 들어오면 놓치는 사례가 생길 수 있고, 모델을 바꾸는 것과 기준을 바꾸는 효과도 따로 봐야 한다.

엄격한 검사에서 SF·판타지까지 과차단됐다

검사를 엄격하게 적용하자 광고로 쓸 수 있는 SF·판타지 스토리까지 과차단됐다. 민감한 요소가 세계관에 등장하는 것과, 그 요소를 광고에 부적절한 맥락으로 다루는 것을 구분해야 했다. 크랙 200건의 dry-run에서도 노예·도박을 등장만으로 판정하자 차단률이 11%에서 31.5%로 올랐다. 많은 사례가 신분제 세계관에 ‘노예’라는 단어가 한 번 들어 있는 정도였다. 이 비율은 전체 차단률이고, 그 자체가 오탐률은 아니다. 다만 차단 근거를 읽어 보니 기준이 넓다는 문제가 드러났다.

그래서 차별이나 노예·인신매매의 노골적인 성적 맥락을 보도록 좁히고, 도박도 스토리의 중심 주제일 때 판정하는 식으로 조정했다. 민감한 단어가 등장한 사실보다 광고에 쓰기 어려운 맥락이 실제로 있는지를 보게 했다.

OOC에서도 종교·정치축이 판타지 설정을 많이 잡았다. 3,762건 중 blocked는 196건이었고, 그중 33건의 근거가 Lucifer, 십자가 소품, 소림사, 윤회 같은 요소였다. 종교적 갈등을 광고하는 것과 종교에서 가져온 판타지 설정을 구분하지 못한 것이다. 이 사례를 확인하고 종교축을 제거했다.

기준은 운영 사례와 함께 바뀌어야 했다

처음 실험은 알려진 문제를 놓치지 않는지 확인하는 용도였다. 정상 사례 97건도 서비스 전체의 장르와 국가별 분포를 대표하도록 구성한 것은 아니었다. 따라서 낮은 비용의 모델이 비슷한 결과를 냈다는 관찰은 유용했지만, 그 테스트셋만으로 안전성 필터를 완성했다고 할 수는 없었다.

안전하게 자동화하려고 넓게 막았는데, 그 기준이 너무 넓으면 제작할 수 있는 정상 콘텐츠가 줄어든다. 판정축을 추가한 뒤에는 실제 차단 이유를 읽고, 맥락이 다른 사례를 모으고, 기준을 다시 조정해야 했다. 이후에도 신규 문제 사례를 평가셋에 추가하면서 확인하는 일이 남았다.

Project

Continue exploring

더 읽어보기

모든 글