지식
AI
에이전트 안티패턴: 흔한 실수와 피해야 할 설계
AI 에이전트 개발에서 자주 발생하는 10가지 안티패턴(무한 루프, 도구 과용, 컨텍스트 오염, 프롬프트 인젝션 등)과 방어 코드 패턴을 완전 해설합니다.
지난 글에서 에이전트 성능을 측정하는 평가 방법론을 살펴봤다. 이번 글에서는 실제 프로덕션 에이전트 개발에서 반복적으로 마주치는 10가지 안티패턴과 방어 코드를 다룬다. 이 실수들은 모두 예방 가능하며, 설계 단계에서 잡는 것이 가장 저렴하다.
에이전트 10대 안티패턴 개요
안티패턴 ①: 무한 루프 (Infinite Loop)
가장 흔하고 치명적인 문제다. 에이전트가 종료 조건 없이 같은 도구를 반복 호출하거나, 답을 찾지 못해 루프에 빠진다.
from langchain.agents import AgentExecutor
from langchain_anthropic import ChatAnthropic
import time
# ❌ 안티패턴: 종료 조건 없음
bad_executor = AgentExecutor(
agent=agent,
tools=tools,
# max_iterations 미설정 → 무한 루프 가능
# handle_parsing_errors 미설정 → 파싱 오류 시 크래시
)
# ✅ 올바른 방법: 다중 안전장치
safe_executor = AgentExecutor(
agent=agent,
tools=tools,
max_iterations=15, # 최대 반복 횟수
max_execution_time=120, # 최대 실행 시간(초)
early_stopping_method="force", # 한도 초과 시 강제 종료
handle_parsing_errors=True, # 파싱 오류 처리
return_intermediate_steps=True, # 디버깅용 중간 단계 수집
)
# 중복 도구 호출 감지 (LangGraph 방식)
class LoopDetectionCallback:
def __init__(self, max_duplicate: int = 3):
self.call_history: list[str] = []
self.max_duplicate = max_duplicate
def on_tool_start(self, tool_name: str, tool_input: dict) -> None:
call_key = f"{tool_name}:{str(sorted(tool_input.items()))}"
duplicate_count = self.call_history.count(call_key)
if duplicate_count >= self.max_duplicate:
raise RuntimeError(
f"루프 감지: '{tool_name}'이 {duplicate_count}회 중복 호출됨. 강제 종료."
)
self.call_history.append(call_key)
안티패턴 ②: 과도한 도구 호출
에이전트가 필요 이상으로 많은 도구를 호출해 비용과 레이턴시가 폭증한다.
# ❌ 안티패턴: 도구 설명이 모호해 불필요한 호출 유발
@tool
def search(query: str) -> str:
"""검색한다""" # 너무 짧은 설명
...
# ✅ 올바른 방법: 언제 사용해야 하는지 명확히 설명
@tool
def web_search(query: str) -> str:
"""실시간 웹 정보를 검색합니다.
사용해야 할 때: 최신 뉴스, 현재 가격, 실시간 날씨 등 시간에 민감한 정보.
사용하지 말아야 할 때: 일반 상식, 수학 계산, 코드 작성 등 LLM이 직접 답할 수 있는 경우.
"""
...
# 도구 결과 캐싱 (동일 쿼리 재요청 방지)
from functools import lru_cache
@lru_cache(maxsize=100)
def cached_web_search(query: str) -> str:
"""캐시 적용 웹 검색 — 동일 쿼리는 API 재호출 없이 반환"""
return _actual_web_search(query)
# 시스템 프롬프트에 절약 지시 추가
SYSTEM_PROMPT = """당신은 효율적인 AI 에이전트입니다.
도구 사용 규칙:
1. 도구는 꼭 필요할 때만 사용하세요. LLM이 직접 답할 수 있으면 도구 생략.
2. 이미 얻은 정보는 다시 검색하지 마세요.
3. 한 번의 도구 호출로 여러 정보를 얻을 수 없는지 확인하세요."""
안티패턴 ③: 컨텍스트 오염
도구 결과 원문이 컨텍스트를 꽉 채워 LLM이 정작 중요한 정보에 집중하지 못한다.
# ❌ 안티패턴: 도구 결과 원문 그대로 사용
@tool
def search_documents(query: str) -> str:
"""문서 검색"""
docs = vector_db.search(query, k=10)
return "\n\n".join(doc.page_content for doc in docs) # 수천 토큰!
# ✅ 올바른 방법: 도구 레벨에서 요약·트리밍
@tool
def search_and_summarize(query: str) -> str:
"""문서를 검색하고 관련 정보를 요약해 반환합니다."""
docs = vector_db.search(query, k=5)
# 각 문서 청크 최대 300자로 제한
snippets = [doc.page_content[:300] for doc in docs]
context = "\n---\n".join(snippets)
# 핵심 정보만 추출해 반환 (선택적 LLM 요약)
return context[:1500] # 컨텍스트 전체 상한선
# 컨텍스트 창 관리 (LangChain)
from langchain_core.messages import trim_messages
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-4-6")
def trim_agent_context(messages: list, max_tokens: int = 8000) -> list:
"""에이전트 컨텍스트를 토큰 한도 내로 트리밍"""
return trim_messages(
messages,
strategy="last",
token_counter=llm,
max_tokens=max_tokens,
include_system=True, # 시스템 프롬프트 유지
start_on="human", # 사용자 메시지부터 시작
)
안티패턴 ⑧⑨: 오류 처리와 보안
import re
from typing import Optional
# 안티패턴 ⑧: 취약한 오류 처리 → 강건한 도구 래퍼
def make_safe_tool(tool_fn, fallback_message: str = "도구 실행 실패"):
"""도구를 오류 안전하게 래핑"""
def safe_wrapper(*args, **kwargs):
for attempt in range(3): # 최대 3회 재시도
try:
import signal
def timeout_handler(signum, frame):
raise TimeoutError("도구 실행 시간 초과")
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(30) # 30초 타임아웃
result = tool_fn(*args, **kwargs)
signal.alarm(0) # 타임아웃 해제
return result
except TimeoutError:
return f"[타임아웃] {fallback_message}. 다른 접근법을 시도하세요."
except ConnectionError as e:
if attempt < 2:
import time
time.sleep(2 ** attempt) # 지수 백오프
continue
return f"[연결 오류] {str(e)[:100]}. 잠시 후 다시 시도하세요."
except Exception as e:
return f"[오류] {str(e)[:200]}"
return fallback_message
safe_wrapper.__name__ = tool_fn.__name__
safe_wrapper.__doc__ = tool_fn.__doc__
return safe_wrapper
# 안티패턴 ⑨: 프롬프트 인젝션 방어
class InputSanitizer:
# 주입 시도 패턴
INJECTION_PATTERNS = [
r"ignore\s+(previous|above|all)\s+instructions?",
r"forget\s+everything",
r"새로운\s+시스템\s+프롬프트",
r"(system|assistant):\s*",
r"<\|im_start\|>",
r"\[INST\]",
]
@classmethod
def sanitize(cls, user_input: str) -> tuple[str, bool]:
"""입력 정제 및 인젝션 감지. (정제된 입력, 위험 여부) 반환"""
cleaned = user_input.strip()
# 길이 제한
if len(cleaned) > 2000:
cleaned = cleaned[:2000] + "...[잘림]"
# 인젝션 패턴 감지
is_suspicious = any(
re.search(pattern, cleaned, re.IGNORECASE)
for pattern in cls.INJECTION_PATTERNS
)
if is_suspicious:
return cleaned, True # 의심스러운 입력 플래그
return cleaned, False
# 에이전트 엔트리포인트에 검증 추가
def run_agent_safely(user_input: str, agent_executor) -> str:
sanitized, is_suspicious = InputSanitizer.sanitize(user_input)
if is_suspicious:
# 로깅 후 거부 또는 제한된 모드로 실행
print(f"⚠️ 의심스러운 입력 감지: {sanitized[:100]}")
return "보안 정책상 처리할 수 없는 입력입니다."
result = agent_executor.invoke({"input": sanitized})
return result.get("output", "오류가 발생했습니다.")
안티패턴 ⑤: 에이전트 없어도 되는 곳에 에이전트 사용
에이전트는 유연하지만 비싸고 느리다. 단순한 경우에는 필요 없다.
# 에이전트 필요성 판단 기준
def needs_agent(task: str) -> bool:
"""에이전트가 필요한지 판단하는 간단한 휴리스틱"""
# 에이전트 불필요: 단일 도구 호출, 고정 파이프라인
simple_patterns = [
"요약해줘", # → 단순 LLM 호출
"번역해줘", # → 단순 LLM 호출
"분류해줘", # → 단순 프롬프트
]
if any(pattern in task for pattern in simple_patterns):
return False
# 에이전트 필요: 다중 도구 조합, 조건 분기, 반복
agent_patterns = [
"검색하고 분석해서", # 검색 + 분석 조합
"비교하고 추천해줘", # 여러 도구 + 추론
"자동으로 실행해줘", # 반복 실행
]
return any(pattern in task for pattern in agent_patterns)
# ❌ 에이전트 불필요: 단순 RAG로 충분
# user: "이 문서에서 주요 내용을 요약해줘"
# → RAG chain = retriever | prompt | llm | output_parser (3줄 코드)
# ✅ 에이전트 필요: 동적 결정이 필요
# user: "우리 제품과 경쟁사 3곳을 비교 분석한 보고서 만들어줘"
# → 검색 도구 × N + 분석 + 구조화 출력 = 에이전트 적합
안티패턴 ⑦: 모놀리식 에이전트 (God Agent)
# ❌ 안티패턴: 하나의 에이전트에 모든 도구
god_agent = Agent(
tools=[
web_search, database_query, file_read, file_write,
send_email, slack_message, create_ticket, deploy_code,
# ... 20개 이상의 도구
],
system_message="모든 것을 할 수 있는 에이전트",
)
# 문제: 도구가 많을수록 LLM의 선택 정확도 하락 (도구 선택 혼란)
# ✅ 올바른 방법: 역할별 전문 에이전트 + 오케스트레이터
research_agent = Agent(tools=[web_search, arxiv_search], ...)
data_agent = Agent(tools=[database_query, sql_generate], ...)
comms_agent = Agent(tools=[send_email, slack_message], ...)
code_agent = Agent(tools=[file_read, file_write, deploy_code], ...)
# 오케스트레이터가 적절한 에이전트로 라우팅
orchestrator = Agent(
tools=[
transfer_to_research,
transfer_to_data,
transfer_to_comms,
transfer_to_code,
],
system_message="사용자 요청을 분석해 적절한 전문 에이전트로 라우팅하세요.",
)
프로덕션 에이전트 체크리스트
# 배포 전 필수 확인 사항
PRODUCTION_CHECKLIST = {
"안전성": [
"max_iterations 설정 (권장: 10-20)",
"max_execution_time 설정 (권장: 60-120초)",
"도구 오류 핸들링 + fallback 메시지",
"중복 도구 호출 감지",
"입력 길이 제한 및 인젝션 방어",
],
"성능": [
"도구 결과 길이 제한 (권장: 1500자 이하)",
"컨텍스트 트리밍 전략 (슬라이딩 윈도우 또는 요약)",
"도구 결과 캐싱 (동일 쿼리 반복 방지)",
"역할별 에이전트 분리 (도구 수 최소화)",
],
"관찰성": [
"LangSmith 또는 Langfuse 트레이싱 연동",
"비용 모니터링 (토큰 수 추적)",
"성공률 메트릭 수집",
"알림 설정 (비용 초과, 오류율 급증)",
],
"평가": [
"테스트 데이터셋 구축 (최소 50개 케이스)",
"자동 평가 파이프라인 (CI 통합)",
"회귀 테스트 (배포 전 성능 비교)",
],
}
for category, items in PRODUCTION_CHECKLIST.items():
print(f"\n[{category}]")
for item in items:
print(f" ☐ {item}")
정리
에이전트 안티패턴은 예방 가능한 설계 실수다:
- 무한 루프:
max_iterations+timeout+ 중복 호출 감지로 방어 - 도구 과용: 명확한 도구 설명 + 캐싱 + 시스템 프롬프트 절약 지시
- 컨텍스트 오염: 도구 결과 길이 제한 + 컨텍스트 트리밍
- 취약한 오류 처리: try/except + 재시도 + fallback 메시지
- 프롬프트 인젝션: 입력 검증 + 패턴 감지 + 권한 최소화
- God Agent: 역할별 에이전트 분리 + 오케스트레이터 패턴
에이전트 개발의 핵심은 실패에 대한 대비다. 에이전트는 LLM의 비결정성으로 인해 항상 예상치 못한 동작을 할 수 있으며, 이를 안전하게 제어하는 코드가 프로덕션 품질을 결정한다.
지난 글: 에이전트 평가: 성능 측정과 벤치마킹 방법론
다음 글: 컴퓨터 비전 심층: 딥러닝 기반 이미지 분류
읽어주셔서 감사합니다. 😊