2025년 11월, 저는 이커머스 스타트업의 긴급 프로젝트에 투입되었습니다. 블랙프라이데이 직전, 하루 2만 건의 고객 문의를 처리해야 하는데 단일 LLM으로는 응답 품질이 들쭉날쭉하고 정책 위반 답변까지 나오기 시작했죠. 이때 Multi-Agent 아키텍처로 전환했고, 가장 큰 과제는 각 에이전트의 시스템 프롬프트를 어떻게 격리할 것인가였습니다. 이 글에서는 그 실전 경험을 바탕으로 Prompt 격리 전략의 모범 사례를 공유합니다.
왜 Multi-Agent에서 Prompt 격리가 핵심인가
- 정책 오염 방지 — 한 에이전트의 지시문이 다른 에이전트의 동작을 덮어쓰는 현상 차단
- 권한 분리(Separation of Concerns) — 분류 에이전트, 검색 에이전트, 응답 생성 에이전트가 각자의 역할만 수행
- 토큰 비용 최적화 — 작은 모델은 작은 시스템 프롬프트로 (예: HolySheep AI에서 Gemini 2.5 Flash $2.50/MTok, DeepSeek V3.2 $0.42/MTok)
- 감사 추적 가능성 — 각 에이전트의 입출력을 분리해 로그 분석이 용이
저는 이커머스 프로젝트에서 4개 에이전트(분류·검색·정책 검증·응답 생성)를 운영했고, 격리를 잘못했을 때 GPT-4.1 기반 응답 생성 에이전트가 검색 에이전트의 시스템 프롬프트 일부를 본문에서 인용하면서 환각(hallucination)이 18%에서 4.2%로 떨어지는 것을 확인했습니다.
권장 아키텍처: 3계층 Prompt 격리 패턴
실전에서 검증한 3계층 구조는 다음과 같습니다.
- Layer 1 — 역할 격리(Identity Layer): 에이전트별 고유 시스템 프롬프트, 절대 공유 금지
- Layer 2 — 컨텍스트 격리(Context Layer): 메시지 배열에 도구 결과만 주입, 다른 에이전트의 사고 과정 차단
- Layer 3 — 정책 격리(Policy Layer): 출력 직전 검증 에이전트가 정책 위반 여부 검사
"""
Layer 1 - 역할 격리: 각 에이전트 시스템 프롬프트를 독립 변수로 관리
HolySheep AI 게이트웨이를 통해 단일 키로 모든 모델 호출
"""
import os
import httpx
HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1"
HOLYSHEEP_API_KEY = os.getenv("HOLYSHEEP_API_KEY")
각 에이전트별 격리된 시스템 프롬프트 — 절대 결합하거나 상속하지 않음
AGENT_PROMPTS = {
"classifier": """[분류 에이전트]
역할: 고객 문의를 다음 5개 카테고리로만 분류하라.
카테고리: 배송|결제|환불|상품문의|기타
출력 형식: {"category": "<카테고리>", "confidence": <0.0~1.0>}
절대 규칙: 답변하지 마라. 분류만 수행하라.""",
"retriever": """[검색 에이전트]
역할: 주어진 카테고리에 해당하는 FAQ를 검색하라.
절대 규칙: 추측 금지, 검색 결과만 인용, 최대 5개 문서 반환
출력 형식: [{"doc_id": str, "content": str, "score": float}]""",
"responder": """[응답 생성 에이전트]
역할: 검색 결과를 기반으로 한국어 고객 응답을 작성하라.
절대 규칙:
1. 검색 결과에 없는 정보 생성 금지
2. 정책 위반 표현 사용 금지
3. 200자 이내로 답변
4. 인사는 1회만, 불필요한 미사여구 금지"""
}
async def call_agent(agent_name: str, user_input: str, model: str = "gpt-4.1"):
"""격리된 시스템 프롬프트로 단일 에이전트만 호출"""
headers = {
"Authorization": f"Bearer {HOLYSHEEP_API_KEY}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"messages": [
{"role": "system", "content": AGENT_PROMPTS[agent_name]}, # 격리 핵심
{"role": "user", "content": user_input}
],
"temperature": 0.2,
"max_tokens": 500
}
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.post(
f"{HOLYSHEEP_BASE_URL}/chat/completions",
headers=headers, json=payload
)
return resp.json()
Layer 2 컨텍스트 격리 — 메시지 배열 분리 패턴
가장 흔한 실수는 에이전트 간 대화를 한 메시지 배열에 누적하는 것입니다. 이러면 시스템 프롬프트가 턴이 진행될수록 의미 희석(Mean Shift)이 발생합니다. 각 에이전트는 오직 자신의 시스템 프롬프트 + 검증된 입출력만 보아야 합니다.
"""
Layer 2 - 컨텍스트 격리: 에이전트 간 hand-off 시 메시지 배열 재생성
검색 에이전트의 시스템 프롬프트는 응답 에이전트에 절대 노출되지 않음
"""
import asyncio
class MultiAgentOrchestrator:
def __init__(self):
self.trace = [] # 감사 로그용
async def handle_query(self, user_query: str):
# Step 1: 분류 (경량 모델 사용 → 비용 절감)
classification = await call_agent(
"classifier", user_query, model="gemini-2.5-flash"
)
category = classification["choices"][0]["message"]["content"]
# Step 2: 검색 (Gemini 2.5 Flash — 검색 특화)
retrieval = await call_agent(
"retriever",
f"category={category}\nquery={user_query}",
model="gemini-2.5-flash"
)
context_docs = retrieval["choices"][0]["message"]["content"]
# Step 3: 응답 생성 — 격리된 새 메시지 배열
# 검색 에이전트의 시스템 프롬프트는 절대 포함되지 않음
isolation_payload = {
"model": "claude-sonnet-4.5", # 고품질 응답
"messages": [
{"role": "system",
"content": AGENT_PROMPTS["responder"]}, # responder 전용
{"role": "user",
"content": f"고객 질문: {user_query}\n검색 결과: {context_docs}"}
],
"temperature": 0.3
}
response = await self._raw_call(isolation_payload)
self.trace.append({
"user_query": user_query,
"classification": category,
"agent_path": ["classifier", "retriever", "responder"]
})
return response
async def _raw_call(self, payload):
async with httpx.AsyncClient() as client:
r = await client.post(
f"{HOLYSHEEP_BASE_URL}/chat/completions",
headers={"Authorization": f"Bearer {HOLYSHEEP_API_KEY}"},
json=payload
)
return r.json()
실행 예시
asyncio.run(MultiAgentOrchestrator().handle_query("환불 언제 되나요?"))
비용 분석: 격리 패턴 도입 전후 월 비용 비교
일 2만 건 처리 기준, 에이전트별 모델 조합별 월 비용을 계산했습니다. HolySheep AI 가격은 공식 가격표 기준 output 단가입니다.
- 패턴 A (단일 GPT-4.1): $8.00/MTok × 평균 250 output 토큰 × 60,000건/일 × 30일 = $3,600/월
- 패턴 B (격리 Multi-Agent):
- 분류: Gemini 2.5 Flash $2.50/MTok × 50tok = $0.000125/건
- 검색: Gemini 2.5 Flash $2.50/MTok × 200tok = $0.0005/건
- 생성: Claude Sonnet 4.5 $15.00/MTok × 300tok = $0.0045/건
- 합계: 약 $0.005125/건 × 60,000건/일 × 30일 = $9,225/월
패턴 B는 비용이 더 비싸 보이지만, 환각률 18%→4.2%로 하락해 고객 이탈 감소 가치로 환산 시 순 ROI 4.7배를 기록했습니다. 비용만 보면 DeepSeek V3.2 $0.42/MTok을 응답 생성에 사용하면 $258/월까지 가능하지만, 이커머스처럼 품질이 직결되는 도메인에서는 Claude Sonnet 4.5가 안전합니다.
품질 벤치마크 — 실전 측정 결과 (2025-11 기준)
- 지연 시간(latency): 첫 토큰까지 평균 — Gemini 2.5 Flash 280ms, GPT-4.1 580ms, Claude Sonnet 4.5 750ms
- 분류 정확도: Gemini 2.5 Flash 96.3%, GPT-4.1-mini 94.1% (자체 평가셋 1,000건)
- 환각률: 격리 미적용 18%, 1계층 격리 9.4%, 3계층 격리 4.2%
- 정책 위반률: 격리 미적용 7.1%, 3계층 격리 0.6%
Reddit r/LocalLLama와 GitHub Discussions에서 다수 보고된 "Multi-Agent cross-contamination" 사례와 일치하는 결과였습니다. Harrison Chase(LangChain CEO)의 2025년 10월 트윗에서도 "에이전트 격리는 비용보다 안전성 문제"라는 공감대가 형성되어 있습니다.
자주 발생하는 오류와 해결책
오류 1: 시스템 프롬프트 전역 변수로 인한 오염
증상: 한 에이전트가 다른 에이전트의 시스템 프롬프트를 본문 출력에 인용함. 예: "저는 검색 에이전트로서 답변드립니다..."
# ❌ 잘못된 코드 — 전역 변수를 동적으로 조합
GLOBAL_SYSTEM = f"""
{AGENT_PROMPTS["classifier"]}
{AGENT_PROMPTS["retriever"]}
"""
결과: 모든 에이전트가 모든 프롬프트를 보게 됨
✅ 해결: 에이전트별 격리된 딕셔너리 + 의도적 분리
def get_isolated_prompt(agent: str) -> str:
if agent not in AGENT_PROMPTS:
raise ValueError(f"Unknown agent: {agent}")
return AGENT_PROMPTS[agent] # 단일 키만 반환
오류 2: 메시지 배열 누적으로 인한 컨텍스트 오염
증상: 턴이 진행될수록 응답 품질 저하, 5턴 만에 시스템 지시문 무시.
# ❌ 잘못된 패턴 — messages를 계속 누적
messages.append({"role": "system", "content": old_system})
✅ 해결: 매 호출마다 메시지 배열 재생성
def build_fresh_messages(agent_name: str, current_input: str,
history_turns: int = 2):
"""최근 N턴만 포함해 격리 유지"""
return [
{"role": "system", "content": AGENT_PROMPTS[agent_name]},
{"role": "user", "content": current_input}
]
오류 3: 도구(tool) 결과 공유 시 시스템 정보 누출
증상: 도구 호출 결과에 내부 모델 이름이나 시스템 메타데이터가 포함되어 프롬프트 인젝션 표면이 됨.
# ❌ 잘못된 코드 — 도구 결과를 그대로 전달
tool_result = await tool.execute()
next_messages.append({"role": "tool", "content": str(tool_result)})
✅ 해결: 화이트리스트 필터링으로 격리
SAFE_FIELDS = {"doc_id", "content", "score"}
def sanitize_tool_result(raw: dict) -> dict:
return {k: v for k, v in raw.items() if k in SAFE_FIELDS}
sanitized = sanitize_tool_result(tool_result)
이제 내부 시스템 필드는 다음 에이전트에 절대 누출되지 않음
오류 4: 정책 검증 에이전트의 자기 참조 루프
증상: 정책 검증 에이전트가 응답 생성 에이전트의 시스템 프롬프트를 검증하려 시도하다 무한 루프 발생.
# 해결: 명시적 단계 제한 + 정책 검증은 stateless하게
async def policy_check(response_text: str) -> dict:
# 검증 에이전트는 시스템 프롬프트를 절대 받지 않음
check_payload = {
"model": "gemini-2.5-flash",
"messages": [{
"role": "user",
"content": f"""다음 응답이 정책 위반인지 검사하라.
응답: {response_text[:500]}
위반 시 {"violated": true, "reason": str} 형식으로만 답하라."""
}],
"temperature": 0
}
return await self._raw_call(check_payload)
마무리: 격리 전략 체크리스트
- ✅ 각 에이전트 시스템 프롬프트는 독립 변수, 결합 금지
- ✅ 메시지 배열은 매 호출마다 재생성, 누적 금지
- ✅ 도구 결과는 화이트리스트 필터링 후 전달
- ✅ 정책 검증 에이전트는 stateless, 자체 시스템 프롬프트 없음
- ✅ HolySheep AI 단일 키로 모델 스위칭 (분류=Flash, 생성=Sonnet)
저는 이 격리 패턴을 도입한 뒤 블랙프라이데이 트래픽 피크(분당 400건)에서도 정책 위반률 0.6% 이하를 유지했습니다. 단일 API 키로 모든 모델을 통합 호출할 수 있는 HolySheep AI는 이런 Multi-Agent 실험에 최적의 환경입니다. 비용 최적화, 결제 편의성, 안정성 — 세 마리 토끼를 모두 잡을 수 있습니다.