
AI 에이전트의 폭주를 막기 위해 가드레일을 어떻게 구축했는가
요약
실제 운영 환경에서 AI 에이전트가 개인정보를 유출한 사례를 바탕으로, 에이전트의 안전성을 확보하기 위한 가드레일 구축 방법을 다룹니다. 입력 검증, 출력 필터링, 비용 제어, 도구 호출 검증의 4가지 핵심 보호 계층을 제안합니다.
핵심 포인트
- 에이전트 프레임워크는 오케스트레이션은 제공하지만 안전성은 직접 구축해야 함
- 가드레일은 정확도를 높이는 것이 아니라 오류 발생 시 피해 범위를 제한하는 역할
- 입력 가드레일: 프롬프트 인젝션 차단 및 PII 비식별화 수행
- 출력 가드레일: 환각 및 민감 정보 유출 여부 검증
- 비용 및 도구 검증: API 과금 폭주 방지 및 허가된 도구 호출 확인
운영 3일째, 나의 에이전트는 한 고객의 이메일 주소를 다른 고객과의 대화에 유출했습니다. 이것은 컨퍼런스 강연에서나 나올 법한 가상 시나리오가 아닙니다. 실제 운영 환경에서 돌아가던 나의 코드가, 테스트해 본 적 없는 행동을 했다는 이야기입니다.
나는 LangGraph와 GPT-4o로 서포트 에이전트를 구축했습니다. 지식 베이스(Knowledge Base)를 검색하고, 계정 정보를 불러오며, 답장을 초안 작성할 수 있는 것입니다. 스테이징 환경에서는 훌륭하게 작동했습니다. 하지만 운영 환경에서는 정확히 72시간 만에, 어떤 사용자의 PII(개인정보)를 다른 사용자의 대화에 노출시켰습니다. 원인은 민망할 정도로 단순했습니다. 모델이 데이터베이스의 생(raw) 컨텍스트를 그대로 답장에 포함해 버렸고, 나의 파이프라인에는 그것을 체크하는 장치가 아무것도 없었던 것입니다.
나중에 보니 해결 방법은 명백했습니다. AI 에이전트 프레임워크가 제공하는 것은 오케스트레이션(Orchestration), 도구 호출(Tool Calling), 메모리(Memory)입니다. 안전성(Safety)은 제공해주지 않습니다. 그 부분은 자신의 담당입니다.
LangChain, CrewAI, LangGraph, OpenAI의 Agents SDK. 무엇을 선택해도 상관없습니다. 입력 검증(Input Validation), 출력 필터링(Output Filtering), 비용 제어(Cost Control)를 처음부터 갖추고 있는 것은 하나도 없습니다. 그것들은 스스로 추가해야 하는 것이라는 전제하에 만들어졌습니다.
그리고 대부분의 팀은 결국 추가하지 않습니다.
왜 이것이 치명적인지는 간단한 산수로 설명할 수 있습니다. 1단계당 정확도가 90%라고 가정하면, 5단계의 에이전트 워크플로우가 성공할 확률은 59%입니다. 10단계라면 35%까지 떨어집니다. 20단계에서는 12%가 됩니다. 가드레일이 없는 각 단계는 실패율의 곱셈입니다.
가드레일은 정확도를 고쳐주지는 않습니다. 정확도가 무너졌을 때 피해 범위(Blast Radius)를 봉쇄하는 것입니다. "에이전트가 틀린 답을 내놓았다"와 "에이전트가 누군가의 사회보장번호를 포함한 틀린 답을 내놓았다"의 차이는 출력 검증기(Output Validator) 하나 차이입니다.
그 후 2주 동안, 나는 가드레일 스택을 계속해서 만들어 나갔습니다. 아래의 코드가 최종적으로 도달한 결과물입니다.
모든 에이전트에는 4가지 포인트에서의 보호가 필요합니다.
입력 가드레일 (Input Guardrail): LLM이 프롬프트를 보기 전에 프롬프트 인젝션(Prompt Injection) 공격을 포착하고, 민감한 데이터를 스크러빙(Scrubbing)한다.
출력 가드레일 (Output Guardrail): 사용자가 보기 전에 응답을 검증하여, 환각(Hallucination)이나 유출된 컨텍스트를 차단한다.
비용 서킷 브레이커 (Cost Circuit Breaker): 루프 스택이나 예상치 못하게 긴 대화로 인해 API 과금이 폭주하는 것을 방지한다.
도구 호출 검증기 (Tool Calling Validator): 에이전트가 허가된 도구만을 스키마 체크를 통과하는 파라미터로 호출하는지 확인한다.
4가지 모두 Python 200행 미만으로 수렴합니다. 레이턴시(Latency) 비용은 층당 10~50ms입니다. 대안은 장애를 고객으로부터 직접 듣는 것입니다.
입력 검증기는 LLM이 프롬프트를 보기 전에 실행됩니다. 하는 일은 두 가지, 인젝션 시도를 차단하고 PII를 비식별화(Redact)하는 것입니다.
import re
from dataclasses import dataclass
@dataclass
...
이것이 철벽은 아닙니다. 진지한 공격자라면 정규 표현식(Regular Expression) 기반의 인젝션 탐지는 회피할 수 있습니다. 하지만 흔히 발생하는 시도는 포착할 수 있습니다. 내 경험상 그것이 실제 인젝션 공격의 약 80%를 차지합니다. 민감한 데이터를 다루는 운영 시스템에서는 정규 표현식 경로 위에 분류기 모델(Lakera Guard나 파인튜닝된 DistilBERT 등)을 한 층 더 얹으십시오.
PII 스크러빙이야말로 3일째에 나를 구해주었어야 할 부분이었습니다. 만약 입력 가드레일이 그 이메일 주소를 데이터베이스 컨텍스트에서 대화로 들어가기 전에 제거했더라면, 그 유출은 일어나지 않았을 것입니다.
출력 검증은 대부분의 팀이 가드레일을 통째로 건너뛰는 부분입니다. 모델이 답을 내놓았고, 그것이 그럴싸해 보이면 배포합니다. 하지만 "그럴싸해 보인다"는 신뢰할 수 있는 기준이 아닙니다.
from pydantic import BaseModel, field_validator
from typing import Optional
import json
...
Pydantic 모델은 두 가지 역할을 수행합니다. 구조를 강제하고(LLM은 answer, confidence, sources를 포함하는 JSON을 반환해야 함), 동시에 콘텐츠 체크(Content Check)를 실행합니다(출력에 PII를 포함하지 않음). 검증(Validation)에 실패하면, 제 에이전트는 추가적인 컨텍스트와 함께 재시도(Retry)를 수행합니다. "당신의 이전 응답은 [이유]로 인해 거부되었습니다. 다시 시도해 주세요"라고 말이죠.
점점 더 구체화되는 지시사항을 포함한 2번의 재시도로 대부분의 검증 실패는 해결됩니다. 3번 실패하면 정형화된 폴백(Fallback) 응답을 반환하고, 인시던트(Incident)로 로그에 기록합니다.
이것은 아무도 쓰지 않는 가드레일이며, 저에게 400달러를 지불하게 만든 가드레일입니다.
에이전트가 재시도 루프(Retry Loop)에 빠지는 버그가 있었습니다. 도구 호출(Tool Call)이 실패하고, 에이전트가 재시도하고, 재시도가 약간 다른 형태로 실패하고, 다시 재시도하는 식이었죠. 각 재시도는 추론 단계에서 토큰을 소모하고, 도구 호출 또한 소모했습니다. 제가 알아차리기 전까지, 그것은 밤새 6시간 동안 계속 작동했습니다.
import time
from threading import Lock
class CostCircuitBreaker:
...
요청당(per-request) 상한선은 가장 명확한 케이스를 포착합니다. 예산을 한꺼번에 태워버리는 단일한 거대 컨텍스트 윈도우(Context Window)가 그것입니다. 세션(Session) 상한선은 하나의 대화에 대한 총 지출에 캡(Cap)을 씌웁니다. 속도(Rate) 상한선은 재시도의 폭풍을 방지합니다. 그리고 일일 지출 상한선이 당신의 절대적인 천장입니다.
저는 일일 상한선을 50달러로 설정했습니다. 그 수치에 도달하면 시스템은 API 호출을 중단하고 "서비스 일시 중단 중"이라는 응답을 반환합니다. 갑작스러운 청구서보다는 다운타임(Downtime)이 차라리 낫습니다.
에이전트가 데이터베이스, 파일 시스템, 외부 API에 접근할 수 있을 때, 도구 호출 검증(Tool Call Validation)은 선택 사항이 아닙니다. 이것이 없다면 탈옥(Jailbreak)되었거나 혼란에 빠진 에이전트가 파괴적인 작업을 수행할 수 있습니다.
class ToolCallGuardrail:
def __init__(self, allowed_tools: dict[str, dict]):
"""
...
저는 기본 거부(Default Deny) 방식을 택했습니다. 도구가 허용 목록(Allowlist)에 없다면 에이전트는 그것을 호출할 수 없습니다. 파라미터가 해당 도구의 허용 목록에 없다면 호출은 거부됩니다. 이는 탈옥 시도(모델이 execute_sql이나 delete_record를 호출하려고 하는 경우)와 환각(Hallucination)으로 인해 나타난 도구 이름(생각보다 빈번하게 발생합니다) 모두를 포착합니다.
호출 빈도 상한선은 도구마다 별도로 관리합니다. 도구마다 리스크 프로필(Risk Profile)이 다르기 때문입니다. 검색 작업은 저렴하고 안전하며 20번 호출해도 문제가 없습니다. 계정 업데이트는 한 대화에서 많아야 1~2회로 제한해야 합니다.
이 네 가지 요소가 실제 에이전트 파이프라인에서 어떻게 연결되는지:
User Input
|
[Input Guardrail] --> reject / sanitize
...
각 가드레일은 자신의 판단을 로그에 기록합니다. 거부될 때마다 이유, 원인이 된 입력, 타임스탬프를 포함하는 구조화된 로그(Structured Log) 엔트리가 남습니다. 저는 일주일에 한 번 이 로그들에 쿼리를 던져 패턴을 찾습니다. 동일한 인젝션(Injection) 시도가 50번 나타난다면, 그것은 아마도 자동화된 공격일 것입니다. 출력 가드레일이 낮은 확신도(Confidence)를 이유로 응답의 15%를 거부하고 있다면, 그것은 상류(Upstream)의 검색(Retrieval) 품질 문제이므로 그 부분을 수정해야 합니다.
4개 가드레일 전체의 총 레이턴시 오버헤드(Latency Overhead)는 40ms 미만입니다(ML 기반 분류기 제외). 사용자에게는 보이지 않습니다.
만약 처음부터 다시 시작한다면, 에이전트 로직을 한 줄이라도 쓰기 전에 가드레일을 추가할 것입니다. 여기서 보여드린 뼈대는 Python으로 약 200줄 정도입니다. 2주가 걸린 이유는 기존 시스템에 사후 적용하면서 동시에 PII 인시던트 조사도 병행했기 때문일 뿐입니다.
제 예측으로는, 12개월 이내에 주요 에이전트 프레임워크들은 가드레일을 일급 기능(First-class feature)으로 제공하게 될 것입니다. LangGraph는 중단(Interrupt) 메커니즘을 통해 이미 그 방향으로 움직이고 있습니다. 그때까지는 직접 구현하는 수밖에 없습니다.
입력 가드레일 (Input Guardrail)과 비용 서킷 브레이커 (Cost Circuit Breaker)부터 시작하세요. 이 두 가지만 있었어도 제가 겪었던 두 가지 사고(PII 유출 및 하룻밤 사이 $400 청구)를 모두 방지할 수 있었을 것입니다. 출력 검증 (Output Validation)은 신뢰도 임계값 (Confidence Threshold)을 튜닝하기 위한 실제 트래픽이 확보된 후에 추가하세요. 도구 호출 검증 (Tool Call Validation)은 에이전트가 무언가에 대한 쓰기 권한을 가지고 있다면 추가하십시오.
가드레일은 에이전트를 똑똑하게 만들어주지는 않습니다. 하지만 어리석은 순간이 사고로 이어지는 것을 막아줍니다.
이 글의 서킷 브레이커 섹션($400 하룻밤 사이 청구)에서도 언급했듯이, 에이전트를 프로덕션 환경에서 운영하면 LLM API의 비용과 프로바이더 (Provider) 관리가 은근히 큰 영향을 미칩니다. 저의 경우, 여러 모델의 전환이나 결제 통합을 위해 EvoLink와 같은 게이트웨이를 사용하고 있습니다. Claude / GPT / 기타 모델을 하나의 API로 통합하여 다룰 수 있으면, 위에서 언급한 비용 상한 관리도 한곳으로 집중할 수 있어 운영이 수월해집니다. 가드레일과 함께 운영의 안정성을 위한 요소로 검토해 보셔도 좋을 것입니다.
번역 기사입니다. 출처: https://hackernoon.com/how-i-built-guardrails-that-stopped-my-ai-agent-from-going-rogue
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기