초안 "에이전틱 디자인 패턴 - 가드레일과 안전성"
유해한 출력을 방지하고 프롬프트 인젝션을 차단하며 TypeScript/Vercel 프로덕션 환경에서 안정적인 작동을 유지하기 위한 강력한 LLM 에이전트 안전 가드레일 구현.
멘탈 모델: AI 시스템을 위한 심층 방어
에이전트 안전성을 안전한 뱅킹 애플리케이션 구축과 같이 생각해보세요. 여러 보안 계층이 필요합니다: 입력 검증(SQL 입력 소독 같은), 런타임 모니터링(사기 탐지 같은), 출력 필터링(PII 마스킹 같은), 그리고 회로 차단기(속도 제한 같은). 각 계층은 서로 다른 위협을 포착합니다 - 프롬프트 인젝션은 SQL 인젝션이고, 환각은 데이터 손상 버그이며, 독성 출력은 보안 침해입니다. 단일 방화벽에만 의존하지 않는 것처럼, 효과적인 AI 안전성은 협력하여 작동하는 조율된 방어 계층이 필요합니다.
기본 예제: 입력 검증과 출력 필터링
1. 간단한 키워드 기반 안전 가드
// lib/guards/basic-safety.ts
import { isEmpty, includes, some, toLower } from 'es-toolkit';
interface SafetyCheckResult {
safe: boolean;
violations: string[];
confidence: number;
}
export class BasicSafetyGuard {
private blockedTerms = [
'ignore instructions',
'disregard previous',
'system prompt',
'reveal instructions'
];
private suspiciousPatterns = [
/\bAPI[_\s]KEY\b/i,
/password\s*[:=]/i,
/DROP\s+TABLE/i,
/\<script\>/i
];
checkInput(input: string): SafetyCheckResult {
if (isEmpty(input)) {
return { safe: true, violations: [], confidence: 1.0 };
}
const lowerInput = toLower(input);
const violations: string[] = [];
// Check blocked terms
const foundBlockedTerms = this.blockedTerms.filter(term =>
includes(lowerInput, term)
);
violations.push(...foundBlockedTerms.map(t => `Blocked term: ${t}`));
// Check suspicious patterns
const matchedPatterns = this.suspiciousPatterns.filter(pattern =>
pattern.test(input)
);
violations.push(...matchedPatterns.map(p => `Suspicious pattern: ${p.source}`));
return {
safe: violations.length === 0,
violations,
confidence: violations.length === 0 ? 1.0 : 0.2
};
}
}
키워드 매칭과 정규식 패턴을 사용하여 일반적인 프롬프트 인젝션 시도를 최소 지연 시간으로 포착하는 기본 안전 가드.
2. API 라우트와의 통합
// app/api/chat/route.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { BasicSafetyGuard } from '@/lib/guards/basic-safety';
import { NextResponse } from 'next/server';
export const runtime = 'nodejs';
export const maxDuration = 60;
const guard = new BasicSafetyGuard();
export async function POST(req: Request) {
try {
const { message } = await req.json();
// Input validation
const inputCheck = guard.checkInput(message);
if (!inputCheck.safe) {
return NextResponse.json(
{
error: 'Input contains prohibited content',
violations: inputCheck.violations
},
{ status: 400 }
);
}
const model = new ChatGoogleGenerativeAI({
modelName: 'gemini-2.5-flash',
temperature: 0.3,
maxOutputTokens: 2048,
});
const response = await model.invoke(message);
// Output validation
const outputCheck = guard.checkInput(response.content as string);
if (!outputCheck.safe) {
console.error('Output safety violation:', outputCheck.violations);
return NextResponse.json(
{ content: 'I cannot provide that information.' },
{ status: 200 }
);
}
return NextResponse.json({ content: response.content });
} catch (error) {
console.error('Chat error:', error);
return NextResponse.json(
{ error: 'Processing failed' },
{ status: 500 }
);
}
}
사용자에게 응답을 반환하기 전에 입력과 출력을 모두 확인하는 이중 계층 보호가 있는 API 라우트.
3. 토큰 추적을 통한 속도 제한
// lib/guards/rate-limiter.ts
import { groupBy, sumBy, filter } from 'es-toolkit';
import { differenceInMinutes } from 'es-toolkit/compat';
interface TokenUsage {
userId: string;
tokens: number;
timestamp: Date;
}
export class TokenRateLimiter {
private usage: Map<string, TokenUsage[]> = new Map();
private readonly maxTokensPerMinute = 10000;
private readonly maxTokensPerHour = 50000;
async checkLimit(userId: string, estimatedTokens: number): Promise<boolean> {
const now = new Date();
const userUsage = this.usage.get(userId) || [];
// Clean old entries
const relevantUsage = filter(userUsage, entry =>
differenceInMinutes(now, entry.timestamp) < 60
);
// Calculate usage in different windows
const lastMinute = filter(relevantUsage, entry =>
differenceInMinutes(now, entry.timestamp) < 1
);
const lastHour = relevantUsage;
const minuteTokens = sumBy(lastMinute, 'tokens');
const hourTokens = sumBy(lastHour, 'tokens');
if (minuteTokens + estimatedTokens > this.maxTokensPerMinute) {
throw new Error(`Rate limit exceeded: ${minuteTokens}/${this.maxTokensPerMinute} tokens/min`);
}
if (hourTokens + estimatedTokens > this.maxTokensPerHour) {
throw new Error(`Hourly limit exceeded: ${hourTokens}/${this.maxTokensPerHour} tokens/hour`);
}
// Record usage
relevantUsage.push({ userId, tokens: estimatedTokens, timestamp: now });
this.usage.set(userId, relevantUsage);
return true;
}
}
버스트 트래픽을 허용하면서 남용을 방지하기 위해 여러 시간 창에서 사용량을 추적하는 토큰 기반 속도 제한기.
4. 안전 피드백이 있는 프론트엔드 통합
// components/SafeChatInterface.tsx
'use client';
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
export default function SafeChatInterface() {
const [input, setInput] = useState('');
const [messages, setMessages] = useState<Array<{role: string, content: string}>>([]);
const sendMessage = useMutation({
mutationFn: async (message: string) => {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.violations?.join(', ') || 'Failed to send message');
}
return response.json();
},
onSuccess: (data) => {
setMessages(prev => [
...prev,
{ role: 'user', content: input },
{ role: 'assistant', content: data.content }
]);
setInput('');
}
});
return (
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">안전한 AI 어시스턴트</h2>
<div className="h-96 overflow-y-auto space-y-4 p-4 bg-base-200 rounded">
{messages.map((msg, i) => (
<div key={i} className={`chat chat-${msg.role === 'user' ? 'end' : 'start'}`}>
<div className={`chat-bubble ${msg.role === 'user' ? 'chat-bubble-primary' : ''}`}>
{msg.content}
</div>
</div>
))}
</div>
{sendMessage.isError && (
<div className="alert alert-error">
<span>{sendMessage.error?.message}</span>
</div>
)}
<div className="join w-full">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage.mutate(input)}
placeholder="안전하게 입력하세요..."
className="input input-bordered join-item flex-1"
disabled={sendMessage.isPending}
/>
<button
onClick={() => sendMessage.mutate(input)}
className="btn btn-primary join-item"
disabled={sendMessage.isPending || !input.trim()}
>
{sendMessage.isPending ? (
<span className="loading loading-spinner"></span>
) : '전송'}
</button>
</div>
</div>
</div>
);
}
안전 위반 사항을 사용자에게 친근한 방식으로 표시하는 내장 오류 처리가 있는 React 컴포넌트.
고급 예제: LangGraph를 사용한 다중 계층 방어
1. 고급 콘텐츠 조절 시스템
// lib/guards/content-moderator.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { z } from 'zod';
import { memoize, debounce } from 'es-toolkit';
import { StructuredOutputParser } from '@langchain/core/output_parsers';
const ModerationResultSchema = z.object({
safe: z.boolean(),
categories: z.object({
toxic: z.number().min(0).max(1),
violent: z.number().min(0).max(1),
sexual: z.number().min(0).max(1),
harmful: z.number().min(0).max(1),
pii: z.number().min(0).max(1)
}),
explanation: z.string(),
suggestedAction: z.enum(['allow', 'block', 'review', 'redact'])
});
export class ContentModerator {
private model: ChatGoogleGenerativeAI;
private parser: StructuredOutputParser<z.infer<typeof ModerationResultSchema>>;
private cache = new Map<string, z.infer<typeof ModerationResultSchema>>();
constructor() {
this.model = new ChatGoogleGenerativeAI({
modelName: 'gemini-2.5-flash',
temperature: 0,
maxOutputTokens: 500
});
this.parser = StructuredOutputParser.fromZodSchema(ModerationResultSchema);
}
// Memoized moderation for performance
moderate = memoize(async (content: string) => {
if (this.cache.has(content)) {
return this.cache.get(content)!;
}
const prompt = `
다음 콘텐츠의 안전성 문제를 분석하세요:
"${content}"
${this.parser.getFormatInstructions()}
`;
const response = await this.model.invoke(prompt);
const result = await this.parser.parse(response.content as string);
this.cache.set(content, result);
return result;
}, {
getCacheKey: (content) => content.substring(0, 100) // Cache by first 100 chars
});
async moderateWithFallback(content: string): Promise<z.infer<typeof ModerationResultSchema>> {
try {
return await this.moderate(content);
} catch (error) {
console.error('Moderation failed, using fallback:', error);
// Fallback to basic checks
return {
safe: !this.hasObviousIssues(content),
categories: {
toxic: 0,
violent: 0,
sexual: 0,
harmful: 0,
pii: this.detectPII(content) ? 1 : 0
},
explanation: '기본 조절 사용',
suggestedAction: 'review'
};
}
}
private hasObviousIssues(content: string): boolean {
const issues = [
/\b(kill|murder|die)\b/i,
/\b(hate|racist|sexist)\b/i
];
return issues.some(pattern => pattern.test(content));
}
private detectPII(content: string): boolean {
const piiPatterns = [
/\b\d{3}-\d{2}-\d{4}\b/, // SSN
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, // Email
/\b\d{16}\b/, // Credit card
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/ // Phone
];
return piiPatterns.some(pattern => pattern.test(content));
}
}
신뢰성을 위해 정규식 패턴으로 폴백이 있는 LLM 기반 분석을 사용하는 정교한 콘텐츠 조절.
2. LangGraph를 사용한 상태 유지 안전 워크플로
// lib/workflows/safety-workflow.ts
import { StateGraph, END } from '@langchain/langgraph';
import { BaseMessage, HumanMessage, AIMessage } from '@langchain/core/messages';
import { ContentModerator } from '@/lib/guards/content-moderator';
import { TokenRateLimiter } from '@/lib/guards/rate-limiter';
import { partition, map } from 'es-toolkit';
interface SafetyState {
messages: BaseMessage[];
userId: string;
safetyChecks: {
input: boolean;
rateLimit: boolean;
content: boolean;
output: boolean;
};
violations: string[];
finalResponse?: string;
}
export function createSafetyWorkflow() {
const moderator = new ContentModerator();
const rateLimiter = new TokenRateLimiter();
const workflow = new StateGraph<SafetyState>({
channels: {
messages: {
value: (x: BaseMessage[], y: BaseMessage[]) => [...x, ...y],
default: () => []
},
userId: {
value: (x: string, y: string) => y || x,
default: () => 'anonymous'
},
safetyChecks: {
value: (x, y) => ({...x, ...y}),
default: () => ({
input: false,
rateLimit: false,
content: false,
output: false
})
},
violations: {
value: (x: string[], y: string[]) => [...x, ...y],
default: () => []
},
finalResponse: {
value: (x: string | undefined, y: string | undefined) => y || x,
default: () => undefined
}
}
});
// Input validation node
workflow.addNode('validateInput', async (state) => {
const lastMessage = state.messages[state.messages.length - 1];
const moderation = await moderator.moderateWithFallback(lastMessage.content as string);
if (!moderation.safe) {
return {
safetyChecks: { ...state.safetyChecks, input: false },
violations: [`입력 위반: ${moderation.explanation}`]
};
}
return {
safetyChecks: { ...state.safetyChecks, input: true }
};
});
// Rate limiting node
workflow.addNode('checkRateLimit', async (state) => {
try {
const estimatedTokens = (state.messages[state.messages.length - 1].content as string).length / 4;
await rateLimiter.checkLimit(state.userId, estimatedTokens);
return {
safetyChecks: { ...state.safetyChecks, rateLimit: true }
};
} catch (error) {
return {
safetyChecks: { ...state.safetyChecks, rateLimit: false },
violations: [`속도 제한: ${error.message}`]
};
}
});
// Process with LLM node
workflow.addNode('processLLM', async (state) => {
const model = new ChatGoogleGenerativeAI({
modelName: 'gemini-2.5-flash',
temperature: 0.3
});
const response = await model.invoke(state.messages);
return {
messages: [response],
finalResponse: response.content as string
};
});
// Output validation node
workflow.addNode('validateOutput', async (state) => {
if (!state.finalResponse) {
return {
safetyChecks: { ...state.safetyChecks, output: false },
violations: ['출력이 생성되지 않음']
};
}
const moderation = await moderator.moderateWithFallback(state.finalResponse);
if (!moderation.safe) {
return {
safetyChecks: { ...state.safetyChecks, output: false },
violations: [`출력 위반: ${moderation.explanation}`],
finalResponse: '안전 문제로 인해 해당 응답을 제공할 수 없습니다.'
};
}
return {
safetyChecks: { ...state.safetyChecks, output: true }
};
});
// Safety violation handler
workflow.addNode('handleViolation', async (state) => {
console.error('안전 위반:', state.violations);
return {
finalResponse: '안전 정책으로 인해 요청을 처리할 수 없습니다.',
messages: [new AIMessage('안전상의 이유로 요청이 차단됨')]
};
});
// Conditional routing
workflow.addConditionalEdges('validateInput', (state) => {
return state.safetyChecks.input ? 'checkRateLimit' : 'handleViolation';
});
workflow.addConditionalEdges('checkRateLimit', (state) => {
return state.safetyChecks.rateLimit ? 'processLLM' : 'handleViolation';
});
workflow.addEdge('processLLM', 'validateOutput');
workflow.addConditionalEdges('validateOutput', (state) => {
return state.safetyChecks.output ? END : 'handleViolation';
});
workflow.addEdge('handleViolation', END);
workflow.setEntryPoint('validateInput');
return workflow.compile();
}
적절한 오류 처리와 위반 추적을 통해 여러 검증 단계를 조율하는 완전한 안전 워크플로.
3. 인간 개입 안전 검토
// lib/guards/human-review.ts
import { z } from 'zod';
import { throttle } from 'es-toolkit';
const ReviewDecisionSchema = z.object({
approved: z.boolean(),
reason: z.string().optional(),
modifications: z.string().optional()
});
export class HumanReviewSystem {
private pendingReviews = new Map<string, {
content: string;
resolve: (decision: z.infer<typeof ReviewDecisionSchema>) => void;
timestamp: Date;
}>();
async requestReview(
reviewId: string,
content: string,
context: Record<string, any>
): Promise<z.infer<typeof ReviewDecisionSchema>> {
return new Promise((resolve) => {
this.pendingReviews.set(reviewId, {
content,
resolve,
timestamp: new Date()
});
// Notify reviewers (webhook, email, etc.)
this.notifyReviewers(reviewId, content, context);
// Auto-reject after timeout
setTimeout(() => {
if (this.pendingReviews.has(reviewId)) {
this.completeReview(reviewId, {
approved: false,
reason: '검토 시간 초과'
});
}
}, 30000); // 30 second timeout
});
}
completeReview(
reviewId: string,
decision: z.infer<typeof ReviewDecisionSchema>
) {
const review = this.pendingReviews.get(reviewId);
if (review) {
review.resolve(decision);
this.pendingReviews.delete(reviewId);
}
}
private notifyReviewers = throttle(
async (reviewId: string, content: string, context: Record<string, any>) => {
// Send to review dashboard
await fetch(process.env.REVIEW_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reviewId, content, context })
});
},
1000 // Throttle notifications to 1 per second
);
getPendingReviews() {
return Array.from(this.pendingReviews.entries()).map(([id, review]) => ({
id,
content: review.content,
timestamp: review.timestamp
}));
}
}
자동 시간 초과 및 알림 메커니즘이 있는 고위험 콘텐츠용 인간 검토 시스템.
4. 완전한 안전 파이프라인이 있는 API 라우트
// app/api/safe-agent/route.ts
import { createSafetyWorkflow } from '@/lib/workflows/safety-workflow';
import { HumanMessage } from '@langchain/core/messages';
import { HumanReviewSystem } from '@/lib/guards/human-review';
import { NextResponse } from 'next/server';
export const runtime = 'nodejs';
export const maxDuration = 300;
const reviewSystem = new HumanReviewSystem();
export async function POST(req: Request) {
try {
const { message, userId, sessionId } = await req.json();
const workflow = createSafetyWorkflow();
// Run safety workflow
const result = await workflow.invoke({
messages: [new HumanMessage(message)],
userId,
safetyChecks: {
input: false,
rateLimit: false,
content: false,
output: false
},
violations: []
});
// Check if human review needed
const needsReview = result.violations.length > 0 &&
result.violations.some(v => v.includes('검토'));
if (needsReview) {
const reviewId = `${sessionId}-${Date.now()}`;
const decision = await reviewSystem.requestReview(
reviewId,
result.finalResponse || message,
{ userId, violations: result.violations }
);
if (!decision.approved) {
return NextResponse.json({
content: '콘텐츠가 검토를 필요로 하며 승인되지 않았습니다.',
reviewId
});
}
result.finalResponse = decision.modifications || result.finalResponse;
}
return NextResponse.json({
content: result.finalResponse,
safetyChecks: result.safetyChecks,
violations: result.violations
});
} catch (error) {
console.error('에이전트 오류:', error);
return NextResponse.json(
{ error: '처리 실패', details: error.message },
{ status: 500 }
);
}
}
민감한 콘텐츠에 대한 인간 검토 에스컬레이션과 함께 모든 안전 계층을 통합하는 완전한 API 라우트.
5. 모니터링 대시보드 컴포넌트
// components/SafetyMonitorDashboard.tsx
'use client';
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { groupBy, map, filter } from 'es-toolkit';
interface SafetyMetric {
timestamp: Date;
type: 'input' | 'output' | 'rate_limit';
blocked: boolean;
userId: string;
}
export default function SafetyMonitorDashboard() {
const [metrics, setMetrics] = useState<SafetyMetric[]>([]);
const { data: pendingReviews } = useQuery({
queryKey: ['pending-reviews'],
queryFn: async () => {
const res = await fetch('/api/admin/pending-reviews');
return res.json();
},
refetchInterval: 5000
});
const { data: recentViolations } = useQuery({
queryKey: ['violations'],
queryFn: async () => {
const res = await fetch('/api/admin/violations');
return res.json();
},
refetchInterval: 10000
});
const violationsByType = groupBy(
recentViolations || [],
'type'
);
return (
<div className="p-6 space-y-6">
<h1 className="text-3xl font-bold">안전 모니터링 대시보드</h1>
{/* Metrics Grid */}
<div className="stats shadow w-full">
<div className="stat">
<div className="stat-figure text-primary">
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="stat-title">안전 점수</div>
<div className="stat-value text-primary">98.5%</div>
<div className="stat-desc">지난 24시간</div>
</div>
<div className="stat">
<div className="stat-figure text-secondary">
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div className="stat-title">차단된 요청</div>
<div className="stat-value text-secondary">{recentViolations?.length || 0}</div>
<div className="stat-desc">↗︎ 3 (2%)</div>
</div>
<div className="stat">
<div className="stat-figure text-warning">
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="stat-title">대기 중인 검토</div>
<div className="stat-value text-warning">{pendingReviews?.length || 0}</div>
<div className="stat-desc">평균 응답: 45초</div>
</div>
</div>
{/* Pending Reviews Table */}
{pendingReviews?.length > 0 && (
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">대기 중인 인간 검토</h2>
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>콘텐츠 미리보기</th>
<th>대기 시간</th>
<th>작업</th>
</tr>
</thead>
<tbody>
{pendingReviews.map((review: any) => (
<tr key={review.id}>
<td>{review.id.substring(0, 8)}...</td>
<td className="max-w-xs truncate">{review.content}</td>
<td>{Math.round((Date.now() - new Date(review.timestamp).getTime()) / 1000)}초</td>
<td>
<div className="btn-group">
<button className="btn btn-sm btn-success">승인</button>
<button className="btn btn-sm btn-error">거부</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Recent Violations */}
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">유형별 최근 위반 사항</h2>
<div className="grid grid-cols-2 gap-4">
{Object.entries(violationsByType).map(([type, violations]) => (
<div key={type} className="stat bg-base-200 rounded-box">
<div className="stat-title capitalize">{type}</div>
<div className="stat-value text-2xl">{violations.length}</div>
<div className="stat-desc">
{violations.slice(0, 2).map((v: any) => (
<div key={v.id} className="text-xs truncate">
{v.reason}
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}
안전 지표, 대기 중인 검토 및 위반 동향을 표시하는 실시간 모니터링 대시보드.
결론
LLM 에이전트를 위한 가드레일과 안전 패턴 구현은 입력 검증, 콘텐츠 조절, 속도 제한, 인간 감독을 결합한 다중 계층 접근 방식이 필요합니다. 기본 패턴은 최소한의 지연 시간 영향으로 빠른 승리를 제공하며, LangGraph를 사용한 고급 워크플로는 정교한 안전 조율을 가능하게 합니다. 주요 시사점으로는 성능을 위한 메모이제이션 사용, 신뢰성을 위한 폴백 메커니즘 구현, 컴플라이언스를 위한 포괄적인 감사 추적 유지가 있습니다. 안전성은 일회성 구현이 아니라 새로운 공격 벡터가 등장함에 따라 지속적인 모니터링, 조정 및 개선이 필요한 지속적인 프로세스라는 점을 기억하세요.