이미지 생성 API 선택하기: 모더레이션 엔드포인트가 없는 경우의 프롬프트 안전성 확보
요약
이미지 생성 API 선택 시 모더레이션 엔드포인트가 없는 경우를 대비해 별도의 정책 게이트를 구축하는 방법을 제안합니다. 지연 시간, 비용, 출력 품질 및 프롬프트 안전성을 종합적으로 고려한 평가 프레임워크를 설명합니다.
핵심 포인트
- 제품 정책과 API 수락 정책을 분리하여 별도의 타입화된 정책 게이트 구축
- 단순 비용이 아닌 '수락된 결과당 비용'을 기준으로 경제성 평가
- p50, p99 지연 시간 및 거부 동작 등 운영 제어 요소 고려
- 고정된 프롬프트 세트와 블라인드 리뷰를 통한 품질 검증
측정 가능한 출력 품질과 꼬리 지연 시간 (tail latency)을 고려하여 이미지 생성 API를 선택하십시오. 그 다음, API에 모더레이션 (moderation) 엔드포인트가 없는 경우, 프롬프트 안전성을 별도의 타입화된 정책 게이트 (typed policy gate)로 직접 관리하십시오.
나의 텍el-to-image (text-to-image) 기능을 구현할 때, 가장 간단한 접근 방식은 생성 요청이 모든 안전성 결정을 내리도록 하는 것이었습니다. 이는 출시하기는 쉬웠지만, 두 가지 계약(contract)이 뒤섞여 있었습니다. 즉, 나의 제품 정책과 이미지 서비스의 수락 정책 (acceptance policy)이 혼재된 것입니다. 이제 나는 생성 전에 작은 게이트를 두고, 채팅 모델 (chat model)로부터 JSON 스키마 (JSON-schema) 형태의 판결을 요구합니다. 이것은 마법 같은 분류기 (classifier)가 아닙니다. 이미지 API를 변경하지 않고도 테스트, 버전 관리, 관찰 및 교체가 가능한 강제 가능한 경계 (enforceable boundary)입니다.
평가 제약 조건은 중요합니다. 나는 단순히 기능 체크리스트만 보고 제공업체를 선택하지 않습니다. 사용자가 실제로 제출하는 프롬프트를 실행하고, 결과로 나온 이미지를 고정된 루브릭 (rubric)으로 점수를 매기며, p50 및 p99 지연 시간 (latency), 거부 동작 (refusal behavior), 운영 제어 (operational control), 그리고 완료된 사용자 작업의 비용을 비교합니다. 승리하는 설정은 가장 긴 모델 목록을 가진 것이 아니라, 그러한 워크로드 (workload)를 견뎌내는 설정입니다.
프롬프트 모더레이션 (prompt moderation) 엔드포인트가 없을 때 어떻게 이미지 생성 API를 선택해야 할까요?
종종 하나로 뭉뚱그려지는 세 가지 질문을 분리하는 것부터 시작하십시오. 첫째, 이미지 API가 당신의 프롬프트 분포 (prompt distribution)에 대해 수용 가능한 결과를 생성할 수 있는가? 둘째, 당신의 애플리케이션이 생성 호출 비용을 지불하기 전에 프롬프트가 자체 정책에 부합하는지 결정할 수 있는가? 셋째, 출력 위험 (output risk)이 중요할 때 반환된 이미지를 검사할 수 있는가? 제공업체 측의 필터는 첫 번째 또는 세 번째 질문에 기여할 수는 있지만, 그것이 당신의 제품에 무엇이 수용 가능한지를 정의하지는 않습니다.
저의 숏리스트 테스트(shortlist test)는 고정된 프롬프트 세트와 블라인드 리뷰(blind review)를 사용합니다. 여기에는 일반적인 프롬프트, 모호한 프롬프트, 명백한 정책 위반, 다국어 입력, 오타, 그리고 긴 텍스트 안에 지침을 숨기려는 시도 등이 포함됩니다. 출력 품질(output quality)의 경우, 지침 준수(instruction following), 원치 않는 텍스트, 구도(composition), 그리고 반복 실행 시의 일관성(consistency)을 점수화합니다. 저는 하나의 통합 점수가 선택을 결정짓는다고 가정하지 않습니다. 깨끗한 다이어그램이 필요한 제품은 느슨한 컨셉 아트(concept art)를 만드는 제품과는 다른 실패 유형에 가중치를 두어야 합니다.
그다음 전체 요청 경로(request path)를 기록합니다. 첫 번째 수락된 결과까지의 시간(Time to first accepted result)은 제공업체의 개별적인 생성 지연 시간(generation latency)보다 저에게 더 유용합니다. 왜냐하면 거부된 프롬프트, 재시도, 그리고 사용할 수 없는 이미지 또한 사용자의 인내심을 소모하기 때문입니다. 수락된 결과당 비용(Cost per accepted result)이 그에 상응하는 경제적 척도입니다. 1인 창업자로서 저는 단가(unit price)를 신경 쓰지만, 세 번의 시도가 필요한 저렴한 호출은 실제로는 저렴하지 않습니다.
모든 항목에서 승리하는 단일 옵션은 없습니다:
| 접근 방식 | 정책 소유권 (Policy ownership) | 이식성 (Portability) | 주요 한계점 (Main limitation) |
|---|---|---|---|
| 생성 호출 단독 (Generation call alone) | 대부분 외부 | 낮음 | 생성 전까지 애플리케이션이 학습하는 내용이 거의 없음 |
| ... |
함정은 명확합니다. 규제 기관, 고객 계약, 또는 내부 통제에서 특정 공개 분류 체계(taxonomy)나 독립적으로 관리되는 정책 서비스(policy service)를 요구할 경우, 스키마 가이드 방식의 채팅 게이트(schema-guided chat gate)는 적합하지 않습니다. 그런 경우에는 요구되는 통제 수단을 사용하고, 애플리케이션 규칙을 대체제가 아닌 추가적인 계층(extra layer)으로 취급하십시오.
안전성 판정을 좁은 계약(narrow contract)으로 만들기
저는 게이트가 지루한 질문에 답하기를 원합니다: 허용할 것인가(allow), 다시 작성할 것인가(rewrite), 아니면 차단할 것인가(block)? 자유 형식의 산문(free-form prose)은 잘못된 반환 타입(return type)입니다. 이는 다운스트림(downstream) 동작이 문구에 의존하게 만들며, 문구는 변하기 마련입니다. 좁은 범위의 객체(narrow object)는 정책 텍스트를 전송 코드(transport code)와 분리된 상태로 유지하면서, 애플리케이션에 폐쇄된 분기 세트(closed set of branches)를 제공합니다.
스키마는 정책 문서(policy document)보다 작아야 합니다. 저는 안정적인 카테고리 식별자(category identifiers), 정책 버전, 그리고 선택 사항인 교체 프롬프트(replacement prompt)를 사용합니다. 또한 알 수 없는 속성(unknown properties)은 거부합니다. 이 마지막 세부 사항은 의도치 않은 인터페이스 확장을 방지해 줍니다. 이는 제가 빠르게 작업하며 모든 정책 수정 사항을 다른 엔지니어가 검토할 여유가 없을 때 매우 유용합니다.
type SafetyDecision = "allow" | "rewrite" | "block";
type SafetyVerdict = {
...
모델 API가 스키마를 강제한다고 주장하더라도, 저는 런타임(runtime)에 응답을 검증합니다. 타입(Types)은 컴파일 후에 사라집니다. TypeScript가 친숙한 형태를 가졌다고 해서 네트워크 입력이 신뢰할 수 있게 되는 것은 아닙니다. 또한 스키마 검증 후에 불변성(invariants)을 적용합니다. rewrite는 비어 있지 않은 교체 프롬프트를 요구하며, allow와 block은 null을 요구합니다. 잘못된 판결(malformed verdict)이 발생했을 때 원본 프롬프트가 조용히 교체되는 일은 절대 발생하지 않습니다.
프롬프트 정책을 구체적으로 유지하세요. 금지된 콘텐츠, 허용되는 예외 사례(edge cases), 그리고 불확실성을 처리하는 방법을 정의하십시오. 게이트(gate)가 "안전(safe)"하기를 요구하지 마세요. 그 단어는 테스트 가능한 계약(contract)을 만들어내지 못합니다. 저는 정책 텍스트와 스키마 버전을 평가 세트(evaluation set)와 함께 저장한 다음, 이들을 함께 승격(promote)시킵니다. 모델의 변경은 TypeScript 인터페이스가 동일하게 유지되더라도 정책 시스템의 변경이기도 합니다.
제가 실제로 배포하는 집중된 요청 경로 (The focused request path I actually ship)
핫 패스(hot path)는 네 단계로 구성됩니다: 안정적인 해싱(hashing)을 위해 충분히 정규화(normalize)하기, 판결(verdict) 얻기, 원본 또는 재작성된 프롬프트 선택하기, 그리고 추상화된 이미지 생성기(abstract image generator) 호출하기입니다. 정규화 과정에서 사용자의 프롬프트를 소문자로 바꾸거나 재작성하지 않는데, 이는 대소문자와 문장 부호가 의미를 담을 수 있기 때문입니다. 해시는 퍼지 정책 매칭(fuzzy policy matching)을 위한 것이 아니라, 상관관계 분석 및 정확한 결과 캐싱(exact-result caching)을 위한 것입니다.
다음은 제공자 중립적인(provider-neutral) 형태입니다. 주입된 두 함수는 의도적인 것입니다. 이 함수들은 채팅 모델과 이미지 모델의 세부 사항을 오케스트레이션 계층(orchestration layer) 외부에 유지하여, 제품 코드를 마이그레이션 프로젝트로 만들지 않고도 어느 쪽이든 벤치마킹하거나 교체할 수 있게 합니다.
type GateResult = {
decision: "allow" | "rewrite" | "block";
categories: string[];
...
이 예제가 하지 않는 점에 주목하세요. 이 예제는 보편적인 재시도 루프 (retry loop)를 임의로 만들어내지 않습니다. RFC 9110에 따르면, 클라이언트는 해당 요청이 관련 의미론(semantics) 하에서 멱등적 (idempotent)임을 알거나 원래 요청이 적용되지 않았음을 감지할 수 없는 한, 비멱등적 (non-idempotent) 요청을 자동으로 재시도해서는 안 됩니다. 이미지 생성은 흔히 POST 형태의 작업 뒤에 위치하므로, 저는 제 인터페이스에서 명시적인 요청 키 (request key)를 요구하며, 재시도하기 전에 제공업체의 문서화된 동작을 확인합니다. 수신 시스템이 이를 준수하지 않는 한, 로컬에서 임의로 만든 키는 아무런 효과가 없습니다.
작은 디테일이지만, 큰 비용을 초래합니다.
저는 요청 키, 프롬프트 해시 (prompt hash), 정책 버전, 결정 사항, 모델 구성 식별자, 각 단계의 소요 시간, 그리고 최종 결과를 로그로 남깁니다. 일반 로그에 사용자 프롬프트 원문을 그대로 넣지는 않습니다. 액세스 제어가 된 샘플은 검토를 지원할 수 있지만, 데이터 보관 (retention) 및 비식별화 (redaction)는 디버깅 과정에서의 실수라기보다는 제품 차원의 결정이어야 합니다.
꼬리 지연 시간 (Tail latency)이 내 아키텍처를 바꾸어 놓았다
저는 이를 노트북 (notebook) 환경이 아닌 실제 트래픽 상황에서 배웠습니다. 수동으로 진행한 워밍업 테스트 중에는 제 게이트웨이 (gate)가 무시할 만한 수준이었으나, 출시 직후 트래픽이 급증하여 분당 43개의 요청에 도달하자 p99 엔드-투-엔드 지연 시간 (end-to-end latency)이 12초에서 31초로 급증했습니다. 이러한 스파이크는 스케일 아웃 (scale-out) 이후에만 나타났습니다. 콜드 애플리케이션 인스턴스 (cold application instance)가 분류기 (classifier)를 위한 업스트림 연결 (upstream connections)을 설정하고, 해당 직렬 결과 (serial result)를 기다린 다음, 그제서야 더 느린 이미지 요청을 시작했기 때문입니다. p50은 훨씬 적게 변했기 때문에, 제가 가장 먼저 확인한 대시보드는 출시 상태가 양호한 것처럼 보이게 만들었습니다. 꼬리 지연 시간의 어느 정도가 연결 설정 때문인지 아니면 업스트림 큐잉 (upstream queueing) 때문인지는 확실하지 않습니다. 제가 파악할 수 있는 한 두 가지 모두 원인이었으며, 제공업체의 내부 큐 (internal queue)는 관찰할 수 없었습니다.
저는 이를 놓쳤습니다.
그 사건은 실험 방식을 바꾸어 놓았습니다. 이제 저는 콜드 배포 (cold deployment) 상태에서, 버스트 도착 패턴 (burst arrival patterns)을 적용하여 테스트하며, 하나의 평균값 대신 단계별 백분위수 (stage-level percentiles)를 보고합니다. 저는 게이트 지연 시간 (gate latency), 생성 지연 시간 (generation latency), 총 지연 시간 (total latency), 차단율 (block rate), 재작성율 (rewrite rate), 이미지 수락률 (image acceptance rate), 그리고 재시도 (retries)를 측정합니다. 또한 웜 패스 (warm path)를 스케일 인 투 제로 (scale-from-zero) 패스와 비교합니다. 런타임이 연결을 웜 상태로 유지하거나 이미지 요청이 다른 모든 단계를 압도하는 경우 결과가 다를 수 있지만, 게이트를 단일 엔드투엔드 (end-to-end) 타이머 안에 숨기는 것은 진단을 불필요하게 어렵게 만듭니다.
경로를 단축하는 몇 가지 안전한 방법이 있습니다. 캐시 키에 정규화된 프롬프트 (normalized prompt), 정책 버전 (policy version), 분류기 설정 (classifier configuration), 그리고 스키마 버전 (schema version)이 포함된 경우에만 정확한 판결을 캐싱하십시오. 정말로 모호하지 않은 경우에는 결정론적 규칙 (deterministic rules)을 사용하되, 이러한 규칙을 적대적 철자 (adversarial spelling) 및 유니코드 (Unicode)에 대해 테스트하십시오. 측정된 꼬리 지연 시간 (tail latency)이 정당화될 때는 소량의 컴퓨팅 자원을 웜 상태로 유지하십시오. 분류기 (classifier)와 생성기 (generator)를 병렬로 실행하지 마십시오. 판결이 내려지기 전에 생성하는 것은 안전하지 않거나 낭비되는 호출을 피하려는 목적을 무색하게 만듭니다.
재시도 (retries)에는 별도의 예산 (budget)을 할당해야 합니다. 저는 연결 실패 (connection failure), 응답 전 타임아웃 (timeout before any response), 명시적 거부 (explicit rejection), 그리고 잘못된 형식의 데이터 (malformed data)를 분리하는데, 이는 각각 다른 조치를 의미하기 때문입니다. 제 애플리케이션 관점에서 게이트는 반복 가능할 수 있지만, 생성 작업은 또 다른 과금 가능한 결과를 생성할 수 있습니다. RFC 9110이 의미론적 경고를 제공하며, 제공업체 계약 (provider contract)이 나머지 사실 관계를 제공해야 합니다. 만약 이러한 사실 관계가 불분명하다면, 추측하기보다는 요청을 실패 처리하고 조정을 위해 해당 키를 보존합니다.
패턴을 복제하기 전에 정책을 측정하십시오
타입이 지정된 응답(Typed response)은 통합(Integration)을 더 안전하게 만들지만, 그 결정이 올바르다는 것을 증명하지는 않습니다. 배포(Rollout) 전에, 저는 대표적인 평가 세트(Evaluation set)에 라벨을 지정하고 허위 허용(False allows), 허위 차단(False blocks), 재작성 수용률(Rewrite acceptance), 그리고 카테고리별 불일치(Disagreement)를 계산합니다. 모호한 예시들은 세트에서 삭제하는 대신 그대로 유지합니다. 그런 예시들이 보통 정책 언어(Policy language)의 개선이 필요한 부분이며, 단일한 종합 정확도(Top-line accuracy) 수치는 이러한 문제들을 숨길 수 있기 때문입니다.
저는 분류기(Classifier) 설정, 시스템 정책(System policy), 카테고리 또는 스키마(Schema)를 변경할 때마다 동일한 세트를 실행합니다. 그런 다음, 새로운 버전을 실제 요청 샘플에 섀도잉(Shadowing)하여 생성 과정을 제어하지는 않으면서 판결을 비교하고, 액세스 정책(Access policy) 하에 불일치 사례를 수동으로 검토합니다. 이 과정을 거친 후에야 트래픽을 전환합니다. '선 배포(Ship-first)'가 '맹목적 배포(Blind-first)'를 의미해서는 안 됩니다.
출력 검토(Output review)는 별개의 결정 사항입니다. 프롬프트 스크리닝(Prompt screening)은 이미지가 무엇을 포함할지 보장할 수 없으므로, 유의미한 출력 위험이 있는 제품은 해당 위험에 적합한 이미지 측 제어(Image-side control) 또는 휴먼 리뷰 큐(Human review queue)가 필요합니다. 고정되어 있거나 애플리케이션에서 작성된 프롬프트를 사용하는 제품은 채팅 게이트(Chat gate)가 전혀 필요하지 않을 수도 있습니다. 열거된 입력(Enumerated inputs)과 결정론적 검증(Deterministic validation)이 더 단순하고, 빠르며, 추론하기 쉽기 때문입니다. 마찬가지로, 사용량이 적은 내부 프로토타입은 수동 검토로 시작할 수 있지만, 공개 제품은 이의 제기 경로(Appeal path)와 관련 없는 코드를 재배포하지 않고도 정책을 업데이트할 수 있는 방법이 필요합니다.
저의 승인/거부(Go/no-go) 시트에는 다섯 가지 항목이 있습니다: 수용된 이미지 품질(Accepted-image quality), 허위 허용률(False-allow rate), 허위 차단율(False-block rate), p99 완료 시간(p99 completion time), 그리고 수용된 이미지당 비용(Cost per accepted image)입니다. 그 옆에는 운영 측면의 질문들을 추가합니다: 정책 테스트를 내보낼(Export) 수 있는가? 안정적인 인터페이스 뒤에서 모델 중 하나를 변경할 수 있는가? 재시도(Retries)에 대한 문서화된 의미론(Semantics)이 있는가? 민감한 내부 지침을 노출하지 않고 거절 사유를 설명할 수 있는가? 저장된 프롬프트 데이터를 일정에 따라 삭제할 수 있는가?
측정 결과가 추가적인 제어 기능이 지연 시간(latency) 및 운영 표면(operational surface)의 비용을 감수할 만큼 가치가 있다고 보여줄 때만 아키텍처를 복사하십시오. 만약 이미 요구되는 전용 모더레이션 (moderation) 시스템이 정책 계약 (policy contract)을 제공하고 있다면, 그것을 그대로 유지하십시오. 사용자 입력이 폐쇄적이고 결정론적 (deterministic)이라면, 모델 게이트 (model gate)를 건너뛰십시오. 모호한 중간 단계 — 즉, 개방형 텍스트 프롬프트와 별도의 모더레이션 엔드포인트 (moderation endpoint)가 없는 생성 API를 사용하는 경우 — 에 있어서, 버전 관리되는 JSON 판결 (verdict)은 제가 발견한 가장 결합도가 낮은 (least coupled) 접근 방식입니다. 하지만 이를 정직하게 유지해 주는 것은 스키마 (schema)가 아니라 평가 세트 (evaluation set)입니다.
참고 문헌
- RFC 9110, HTTP Semantics: https://www.rfc-editor.org/rfc/rfc9110
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기