말 안 듣는 사용자를 대비한 하루 수십 캐릭터 자동 검열 시스템 설계하기


우리는 AI 캐릭터 채팅 서비스를 운영하고 있으며, 유저가 직접 캐릭터를 생성할 수 있는 구조를 가지고 있다.

예전에는 캐릭터 생성 과정에서 선정성·폭력성 등을 사전에 강하게 제한하는 정책을 두지 않았다.

대신 정책을 과도하게 위반하는 콘텐츠가 Public 또는 Unlisted 상태로 공개되어 있을 경우, 운영자에 의해 삭제될 수 있다는 점만 명시하고 있다.

캐릭터 생성 시점에는 별도의 제약을 두지 않고, 사후 검열에 의존하는 구조였기 때문에 검열이 완료되기 전까지 매우 선정적이거나 폭력적인 캐릭터가 반복적으로 노출되는 문제가 발생했다.

일부 사용자는 여러 차례 경고를 받았음에도 불구하고 이를 중단하지 않았고, 그 결과 메인 페이지의 콘텐츠 품질이 심각하게 저하되는 상황에까지 이르렀다.

기존 검열 방식의 한계

기존에는 Admin Page를 통해 캐릭터를 수동으로 Invalidate 하는 방식으로 검열을 진행했다.

문제는 이 방식이 규모가 커질수록 현실적으로 유지하기 어렵다는 점이었다.

특히 다음과 같은 케이스에서 검열이 매우 까다로웠다.

  • 이미지나 캐릭터 설명은 정책을 위반하지 않음
  • 하지만 프롬프트 내부에 정책을 위반하는 설정이 숨어 있는 경우

이런 캐릭터들은 겉보기에는 전혀 문제가 없어 보이지만, 직접 채팅을 해보면 정책을 명확히 위반하는 방향으로 대화가 유도되는 경우가 있었다.

당시에 검열을 제대로 하려면 프롬프트를 직접 읽어봐야 했고, 경우에 따라 실제로 대화를 해봐야만 문제를 인지할 수 있었다.

하지만 서비스가 성장하면서 하루에 수십 개의 캐릭터가 생성되기 시작했고, 이 모든 캐릭터를 사람이 직접 검열하는 것은 명백한 한계에 부딪혔다.

따라서 수동 검열의 구조적 한계가 검열 시스템을 구축하게 된 첫 번째 이유이다.


앱 출시를 앞두고 드러난 또 다른 문제

두 번째 계기는 모바일 애플리케이션 출시 준비 과정에서 발생했다.

Play Store와 App Store 정책상, 선정성·폭력성이 포함된 콘텐츠는 심사 통과 자체가 불가능하다.

즉, 문제가 생기면 운영자가 나중에 처리한다는 방식으로는 앱 심사를 통과할 수 없었다.
또한 검열을 열심히 한다고 해도, 순간은 캐릭터가 공개되어 있는 순간이 있었기 때문에 이 시간 차이를 완전히 없애야 했다.

우리는 이 문제를 해결하기 위해 리소스(Character, Scenario, Lorebook)에 대해 다음과 같은 등급 체계를 도입했다.

  • Safe
  • Mature
  • 18+

Native App 이외의 환경에서는 Account Page에서 등급을 바꿀 수 있고, 같은 계정으로 애플리케이션으로 접속하는 순간 자동으로 Safe로 바뀌도록 해서 애플리케이션에서는 항상 Safe 등급의 리소스만 노출되도록 했다.

그리고 중요한 점은 Creator가 이 등급을 직접 설정할 수 없도록 제한했다는 것이다.

따라서 Public/Unlisted로 공개 범위를 설정했을 경우에는 캐릭터가 Waiting Approval 상태에 머물러 검열 시스템의 통과가 있어야 비로소 공개되는 구조로 시스템을 변경했다.

이를 통해 앱에서는 항상 안전한 콘텐츠만 노출되고 운영 측에서 리소스를 체계적으로 관리할 수 있는 구조를 만들고자 했다.


검열 시스템의 목표

안전한 콘텐츠만 노출하는 것은 코드 레벨에서 Safe 등급의 리소스만 조회하도록 제한하는 방식으로 비교적 쉽게 구현할 수 있었다.

하지만 우리의 최종 목표는 캐릭터 등급 분류 자체를 자동화하는 것이었다.
이를 위해 이미지, 캐릭터 설명, 프롬프트 등 다양한 요소를 종합적으로 고려하여 AI가 콘텐츠의 등급을 판단하도록 설계했다.

이 구조를 통해, 기존에 사람이 직접 프롬프트를 하나하나 읽어가며 검열해야 했던 비효율적인 과정을 크게 줄일 수 있었다.

물론 AI가 항상 올바른 판단을 내릴 수는 없기 때문에, 최종 결정은 사람이 승인하는 구조를 유지했다.


다만 이 승인 과정이 다시 또 다른 병목이 되지 않도록, Admin Page에 직접 접속하지 않아도 Discord 등 외부 툴을 통해 간편하게 처리할 수 있는 흐름을 함께 구축하는 것을 목표로 했다.


검열 시스템의 구조

캐릭터 생성 요청이 끝난 후 CHARACTER_CREATED 이벤트가 생성되면, Listener가 Content Classification을 시작하게 된다.

LLM을 거쳐 캐릭터의 등급이 평가되었으면, CONTENT_CLSSIFIED 이벤트가 생성되고, 디스코드 웹훅으로 최종 승인 메시지와 사용자의 기기로 Waiting Approval Notification이 가게 된다.

아래는 실제 서비스에서 우리가 처리하고 있는 디스코드 웹훅 메시지이다.

분류와 분석 결과가 나오고, No Action을 눌러서 AI의 분류를 Approve 하면 된다.
이때 캐릭터의 상태가 WAITING_APPROVAL에서 PUBLISHED로 바뀌게 되어 다른 사용자가 볼 수 있게 된다.

만약 분석을 잘못 했다고 생각하면 Safe / Mature / Adult / Reject 버튼을 클릭해서 원하는 액션을 할 수 있다.

Safe 등급이 아닌 경우 자동으로 Private 캐릭터로 취급되며, 캐릭터를 수정하여 다시 분류 요청을 넣을 수 있다.

그리고 웹훅 발송 후 30분이 지나도 아무 버튼을 누르지 않으면 AI의 선택을 Confirm 한 것으로 간주되어 캐릭터의 상태가 확정된다.

이제 디스코드 알림이 오면 열고 버튼만 누르면 검열을 마칠 수 있었다.

한 캐릭터를 검열하는데 불과 5초도 걸리지 않아 수십개의 캐릭터가 만들어져도 캐릭터 검열에 10분이 채 걸리지 않았다.


검열 시스템의 프롬프트

이 시스템을 구축하는데 가장 오래 걸린 것은 제대로 분류하는 프롬프트를 만들기 위해 프롬프트 튜닝을 매우 많이 했다.

우선 이미지 분석이 가능해야 했고, 그 기준이 매우 명확하고 자세히 정의돼 있어야 제대로 된 판단을 내릴 수 있다.
또한, 안 좋은 모델의 경우에는 프롬프트도 잘 먹지 않아 원하는 결과를 내기 어려웠다.

우리는 분류 성능에 한계를 느껴 분류에는 비용을 조금 더 쓰더라도 좋은 모델을 쓰기로 결정하였다.
그리고 세 번의 LLM Call에서 모두 같은 결과가 나와야 인정한다는 정책을 세우고 Two-Step에 걸쳐 캐릭터를 분류하기로 했다.

우리는 LLM Monitoring을 겸해 Langfuse를 사용하고 있었는데, 프롬프트 테스트를 여기서 쉽게 할 수 있어서 빠르게 사이클을 돌려서 우리가 원하는 프롬프트를 얻어낼 수 있었다.


검열 프롬프트 엔지니어링

Classify the image using the Discrete 4-Tier Image Policy.
Apply the one-pass decision tree and hard-edge rules exactly.

Text/vibe alone never escalates. Always choose the highest applicable tier.

Return the JSON object exactly as specified.

# Discrete 4-Tier Image Policy

Decision method: one-pass, top-down. Stop at the first matching tier.

Precedence: REJECTED > ADULT > MATURE > SAFE

Visible: includes through sheer/wet/see-through only if anatomical detail is discernible.

No text-only triggers: suggestive text/emoji/vibe alone never escalates a tier.
  • 이미지를 정확히 4단계(SAFE / MATURE / ADULT / REJECTED)로 자동 분류하고, 주관적 해석이나 텍스트 분위기에 흔들리지 않게 만드는 강한 하드 룰 프롬프트를 만들었다.
## Step 0 — Quick glossary / hard edges
  • 다음으로는 분석 시 고려할 사항에 대해 아주 자세히 명시하였다.
  • 주관이 들어갈 수 있는 요소, 즉 분류 시 매번 달라질 수 있는 요소를 정확히 기준을 정해서 일관적인 분류 결과를 낼 수 있도록 하였다.
[예시]
  • 단순한 반투명함, 색감 변화, 전체적인 윤곽이나 형태만으로는 보이는 것으로 간주하지 않는다.
  • 팔, 손, 머리카락, 소품, 크롭(프레이밍), 비시스루 원단 등은 해부학적 디테일이 보이지 않도록 실질적으로 차단한다면 Coverage으로 인정한다.
  • 인접한 등급 사이에서 판단이 불확실할 경우, 상위 등급에 대한 하드 증거가 있을 때만 상향한다.
  • Vibe만으로는 등급을 올리지 않는다.
  • 등등…
## A. REJECTED

Trigger if any of:

A1. Explicit exposure
A2. Sexual acts or fluids
A3. Sexualized depiction of a child clearly <10
A4. Fully see-through exposure
...

## B. ADULT

Apply only if none of A. Trigger if any of:
...
  • 각 분류에 속할 수 있는 Trigger를 판단하도록 하였다.
  • 위 프롬프트 예시에서는 각 Trigger에 대한 설명은 생략하였다.
## One-Pass Checklist (binary, stop at first "yes")

1. [Checklist 1] → **REJECTED (A1/A4)**
2. [Checklist 2] → **REJECTED (A2)**
3. [Checklist 3] → **REJECTED (A3)**
4. [Checklist 4] → **ADULT (B)**
5. [Checklist 5] → **MATURE (C)**
6. Otherwise → **SAFE (D)**
  • 트리거 포인트에 대한 체크 리스트를 제시하여 최종 결과를 낼 수 있도록 하였다.
  • 검열 단계를 낮춰가면서 체크 리스트를 제시하여 Binary, Stop At First Yes 로 애매한 결과를 내지 않도록 유도하였다.
  • 위 프롬프트 예시에서는 자세한 체크리스트 설명은 생략하였다.
## Output contract (return exactly this shape)

\`\`\`json
{
  "category": "REJECTED | ADULT | MATURE | SAFE",
  "triggers": [
    "A1_explicit_exposure",
    "A1_detailed_contour",
    ...
  ],
  "notes": "short, user-friendly ..."
}
\`\`\`
  • 마지막으로는 분류 결과 Output Format을 지정해주었다.
  • Json으로 결과를 받아 Discord Webhook에 바로 사용할 수 있도록 하였다.

마무리

AI 콘텐츠 서비스를 운영하면서 검열은 항상 가장 어려운 문제 중 하나였다.

표현의 자유와 플랫폼의 책임 사이에서 균형을 잡아야 했고, 사람의 판단에만 의존하기에는 서비스 규모가 점점 커지고 있었다.

글 초반에 언급했듯, 여러 차례 경고에도 불구하고 선정적인 캐릭터를 반복적으로 업로드하던 사용자는 결국 모든 시도가 Rejected 처리되었고, 같은 캐릭터로 10회 이상 재시도했음에도 매번 자동으로 걸러졌다.

사람이 직접 개입하지 않아도 시스템이 일관된 기준으로 즉시 대응해주니, 운영 측에서는 감정 소모 없이 매우 편리하게 검열을 진행할 수 있었다.

  • 이후 그 사용자에게 왜 내 캐릭터만 계속 리젝되냐면서 직접 컴플레인이 들어왔던 기억도 있다..

등급 체계 도입과 검열 자동화 이후, 앱 심사 과정에서도 콘텐츠의 선정성·폭력성 문제에 대해 어려움을 겪는 일은 발생하지 않았다.

이는 단순히 심사를 통과했다는 의미를 넘어, 플랫폼이 요구하는 기준을 기술적으로 충족할 수 있는 구조를 갖췄다는 점에서 의미가 컸다.

프롬프트 엔지니어링을 상당히 보수적이고 집요하게 설계한 덕분에, 지금까지도 검열 시스템은 우리가 의도한 방향 그대로 안정적으로 동작하고 있다.
여기에 더해 어디서든 즉시 처리할 수 있는 원클릭 검열 플로우를 구축한 점은 운영 측면에서 가장 큰 장점 중 하나라고 생각한다.

결과적으로 이 검열 시스템은 AI 서비스로서의 안정성과 앱 심사 대응이라는 두 가지 측면 모두에서 핵심적인 역할을 해주고 있다.

단순히 문제를 막기 위한 장치가 아니라, 서비스가 성장하더라도 흔들리지 않도록 지탱해주는 기반 시스템을 만들었다는 점에서 도입하기를 정말 잘한 기능이라고 느끼고 있다.

답글 남기기

Dalmeng's Footprints에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기