실무에서 적용하는 LLM 안전장치: 실제로 효과적인 방법
모델이 아닌 위험을 통제하십시오.
LLM은 예측 불가능합니다. 환각(hallucination) 현상이 발생하거나 데이터가 누출되며, 해로운 콘텐츠를 생성하거나 합법적인 요청을 거부하기도 합니다. 가드레일(Guardrails)은 모델의 성능을 희생하지 않으면서도 모델의 행동을 제한합니다.
핵심은 어떤 가드레일이 중요한지, 어떤 것은 그저 잡음(noise)인지 아는 것입니다.
가드레일은 모델을 통제하는 것이 아닙니다. 위험을 통제하는 것입니다.

입력 검증
가장 중요한 가드레일입니다. 나쁜 입력은 나쁜 출력을 낳으며, 나쁜 입력은 시스템에 프롬프트 인젝션(prompt injection)을 유발할 수도 있습니다.
전략 1: 프롬프트 정제(Sanitization)
위험한 패턴을 조기에 정제합니다:
import re
class PromptSanitizer:
def __init__(self):
self.dangerous_patterns = [
r"ignore\s+previous\s+instructions",
r"system\s+prompt",
r"you\s+are\s+now\s+free",
r"break\s+out\s+of",
]
def sanitize(self, prompt: str) -> str:
for pattern in self.dangerous_patterns:
prompt = re.sub(pattern, "[REDACTED]", prompt, flags=re.IGNORECASE)
return prompt
이 방법은 완벽하지 않습니다. 공격적인 입력은 창의적이기 때문입니다. 하지만 명백한 공격은 잡아내며, 명백한 공격이 가장 흔합니다.
전략 2: 입력 길이 제한
길이 제한은 토큰 낭비와 타임아웃을 방지합니다:
class InputValidator:
def __init__(self, max_length: int = 10000):
self.max_length = max_length
def validate(self, prompt: str) -> tuple[bool, str]:
if len(prompt) > self.max_length:
return False, f"Input too long: {len(prompt)} > {self.max_length}"
return True, "OK"
전략 3: 콘텐츠 필터링
콘텐츠 필터링은 정책 위반을 차단합니다. 여기서 사용되는 패턴은 도메인에 따라 다릅니다:
class ContentFilter:
def __init__(self):
self.blocked_topics = [
"violence", "hate speech", "self-harm",
"sexual content", "illegal activities",
]
def filter(self, prompt: str) -> tuple[bool, str]:
prompt_lower = prompt.lower()
for topic in self.blocked_topics:
if topic in prompt_lower:
return False, f"Blocked: {topic}"
return True, "OK"
간단한 문자열 매칭은 빠지만 부정확합니다. 프로덕션 환경에서는 정책 위반을 감지하기 위해 분류기 모델(classifier model)을 사용하세요. Qwen2.5-1.5B와 같은 작은 모델이라도 충분합니다. 더 정확하고 우회하기 어렵습니다.
출력 필터링
모델의 출력도 확인해야 합니다. 구조, 콘텐츠, 사실성 등을 검증합니다.
전략 1: 응답 검증
먼저 구조를 검증합니다. JSON을 기대한다면 JSON인지 확인하세요:
class ResponseValidator:
def __init__(self):
self.required_fields = ["answer", "confidence"]
def validate(self, response: dict) -> tuple[bool, str]:
for field in self.required_fields:
if field not in response:
return False, f"Missing field: {field}"
return True, "OK"
전략 2: 콘텐츠 필터링
해로운 콘텐츠를 필터링합니다:
class OutputFilter:
def __init__(self):
self.blocked_patterns = [
r"kill\s+someone",
r"bomb\s+recipe",
r"hate\s+speech",
r"self-harm",
]
def filter(self, response: str) -> tuple[bool, str]:
for pattern in self.blocked_patterns:
if re.search(pattern, response, re.IGNORECASE):
return False, f"Blocked: {pattern}"
return True, "OK"
전략 3: 사실 확인(Fact-Checking)
사실 확인은 더 어렵습니다. 모든 주장을 검증할 수는 없으므로 중요한 것만 선택해야 합니다:
class FactChecker:
def __init__(self):
self.known_facts = {
"capital of france": "Paris",
"population of usa": "330 million",
"speed of light": "299,792,458 m/s",
}
def check(self, claim: str) -> tuple[bool, str]:
claim_lower = claim.lower()
for fact, truth in self.known_facts.items():
if fact in claim_lower and truth not in claim_lower:
return False, f"Fact check failed: {fact}"
return True, "OK"
진짜 사실 확인을 위해서는 검색 파이프라인(retrieval pipeline)이 필요합니다. 하드코딩된 사전이 아닌 지식 베이스(knowledge base)와 비교하여 주장을 검증해야 합니다.
안전 메커니즘
전략 1: 속도 제한(Rate Limiting)
속도 제한은 악용을 방지합니다:
import time
from collections import deque
class RateLimiter:
def __init__(self, max_requests: int = 10, window: int = 60):
self.max_requests = max_requests
self.window = window
self.requests = deque()
def allow(self) -> bool:
now = time.time()
while self.requests and self.requests[0] < now - self.window:
self.requests.popleft()
if len(self.requests) >= self.max_requests:
return False
self.requests.append(now)
return True
전략 2: 토큰 예산 관리(Token Budgeting)
토큰 예산 관리는 요청당 비용을 상한선으로 제한합니다:
class TokenBudget:
def __init__(self, max_tokens: int = 1000):
self.max_tokens = max_tokens
def validate(self, response: str) -> tuple[bool, str]:
token_count = len(response.split())
if token_count > self.max_tokens:
return False, f"Token limit exceeded: {token_count} > {self.max_tokens}"
return True, "OK"
전략 3: 컨텍스트 윈도우 관리
컨텍스트 윈도우 관리는 오버플로우를 방지합니다:
class ContextManager:
def __init__(self, max_context: int = 4096):
self.max_context = max_context
self.context = []
def add(self, message: str):
self.context.append(message)
self.trim()
def trim(self):
while len(" ".join(self.context)) > self.max_context:
self.context.pop(0)
슬라이딩 윈도우 트리밍은 간단하지만 초기 컨텍스트를 잃게 됩니다. 더 나은 접근 방식은 요약(summarization) 또는 어텐션 기반 압축을 사용하는 것이지만, 이는 지연 시간을 증가시킵니다.
준수(Compliance)
엔터프라이즈 시스템에는 준수 가드레일이 필요합니다. 그중에서도 가장 중요한 두 가지가 있습니다:
패턴 1: 데이터 거주(Data Residency)
데이터 거주 — 데이터가 필수적인 지리적 경계 내에 머물도록 보장합니다:
class DataResidency:
def __init__(self, allowed_regions: list[str]):
self.allowed_regions = allowed_regions
def validate(self, region: str) -> tuple[bool, str]:
if region not in self.allowed_regions:
return False, f"Region not allowed: {region}"
return True, "OK"
패턴 2: 감사 로깅(Audit Logging)
감사 로깅 — 모든 모델 상호작용을 기록합니다:
import json
from datetime import datetime
class AuditLogger:
def __init__(self, log_file: str = "audit.log"):
self.log_file = log_file
def log(self, request: dict, response: dict):
entry = {
"timestamp": datetime.now().isoformat(),
"request": request,
"response": response,
}
with open(self.log_file, "a") as f:
f.write(json.dumps(entry) + "\n")
감사 로그는 디버깅과 준수를 위해 중요합니다. 구조화되고 부가 전용(append-only)이며 안전하게 저장되도록 하세요.
종합 적용
패턴 1: 간단한 가드레일
간단한 가드레일 파이프라인입니다:
class SimpleGuardrails:
def __init__(self):
self.input_validator = InputValidator(max_length=10000)
self.output_filter = OutputFilter()
def process(self, prompt: str) -> str:
valid, message = self.input_validator.validate(prompt)
if not valid:
return f"Error: {message}"
response = self.call_model(prompt)
valid, message = self.output_filter.filter(response)
if not valid:
return f"Error: {message}"
return response
패턴 2: 고급 가드레일
고급 가드레일은 정제, 속도 제한, 토큰 예산을 추가합니다:
class AdvancedGuardrails:
def __init__(self):
self.sanitizer = PromptSanitizer()
self.input_validator = InputValidator(max_length=10000)
self.content_filter = ContentFilter()
self.output_filter = OutputFilter()
self.rate_limiter = RateLimiter(max_requests=10)
self.token_budget = TokenBudget(max_tokens=1000)
def process(self, prompt: str) -> str:
prompt = self.sanitizer.sanitize(prompt)
valid, message = self.input_validator.validate(prompt)
if not valid:
return f"Error: {message}"
valid, message = self.content_filter.filter(prompt)
if not valid:
return f"Error: {message}"
if not self.rate_limiter.allow():
return "Error: Rate limit exceeded"
response = self.call_model(prompt)
valid, message = self.output_filter.filter(response)
if not valid:
return f"Error: {message}"
valid, message = self.token_budget.validate(response)
if not valid:
return f"Error: {message}"
return response
가드레일이 중요한 경우
사용자 대상 시스템을 구축하거나, 민감한 데이터를 처리하거나, 프로덕션 환경에서 실행할 때 가드레일은 중요합니다. 또한 GDPR, HIPAA, SOC 2와 같은 준수 요구 사항이 있을 때도 중요합니다.
프로토타이핑을 하거나 모델을 내부 도구에만 사용하거나, 민감한 데이터를 다루지 않는다면 가드레일은 중요하지 않습니다. 필요할 때까지 생략하세요.
이것은 항상 성능과 안전성 사이의 트레이드오프입니다. 가드레일이 많으면 실패가 적지만 성능도 제한됩니다. 시스템에 적합한 균형을 찾으세요.
트레이드오프
| 전략 | 안전성 | 성능 | 지연 시간 |
|---|---|---|---|
| 가드레일 없음 | 최저 | 최고 | 최저 |
| 입력 검증 | 높음 | 중간 | 낮음 |
| 출력 필터링 | 높음 | 중간 | 낮음 |
| 안전 메커니즘 | 최고 | 최저 | 최고 |
| 준수 | 최고 | 최저 | 최고 |
관련 자료
- Model Routing Strategies — 능력 기반, 비용 인식, 지연 시간 인식 라우팅
- Cost Optimization for LLM Systems — 토큰 예산 관리, 폴백 모델, 캐싱
- Multi-Model System Design — 다중 모델 아키텍처
- LLM Architecture — 시스템 설계의 핵심: 라우팅, 비용, 가드레일, 오케스트레이션