TestForge Blog

AI Agent 아키텍처 설계 — ReAct부터 Multi-Agent까지

프로덕션 AI Agent 시스템 설계 방법. ReAct 패턴, Tool Use, Memory 관리, Multi-Agent 오케스트레이션, 안전성 설계까지 실전 가이드.

TestForge Team ·

AI Agent란

Agent = LLM + Tools + Memory + Planning

단순 LLM 호출과의 차이:

  • LLM: 한 번의 입력 → 출력
  • Agent: 목표 달성까지 반복적으로 계획하고 행동
사용자: "오늘 서울 날씨 알려줘"
LLM: "죄송합니다, 실시간 정보가 없습니다"
Agent: 
  1. Think: 날씨 API 호출이 필요함
  2. Act: weather_tool("Seoul")
  3. Observe: {"temp": 18, "condition": "맑음"}
  4. Answer: "오늘 서울은 18°C, 맑습니다"

ReAct 패턴

Reasoning + Acting의 반복.

from anthropic import Anthropic

client = Anthropic()

tools = [
    {
        "name": "search_web",
        "description": "웹 검색으로 최신 정보 조회",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "검색 쿼리"}
            },
            "required": ["query"]
        }
    },
    {
        "name": "run_code",
        "description": "Python 코드 실행",
        "input_schema": {
            "type": "object",
            "properties": {
                "code": {"type": "string"}
            },
            "required": ["code"]
        }
    }
]

def run_agent(user_message: str, max_iterations: int = 10):
    messages = [{"role": "user", "content": user_message}]
    
    for i in range(max_iterations):
        response = client.messages.create(
            model="claude-opus-4-6",
            max_tokens=4096,
            tools=tools,
            messages=messages,
        )
        
        if response.stop_reason == "end_turn":
            return response.content[0].text
        
        if response.stop_reason == "tool_use":
            tool_calls = [b for b in response.content if b.type == "tool_use"]
            tool_results = []
            
            for call in tool_calls:
                result = execute_tool(call.name, call.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": call.id,
                    "content": str(result)
                })
            
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})
    
    raise RuntimeError("Max iterations reached")

Memory 설계

Agent에는 세 종류의 메모리가 필요합니다.

class AgentMemory:
    def __init__(self):
        # 1. Working Memory: 현재 대화 컨텍스트
        self.messages: list[dict] = []
        
        # 2. Episodic Memory: 과거 대화 요약 (Vector DB)
        self.vector_store = ChromaDB("agent_episodes")
        
        # 3. Semantic Memory: 도메인 지식 (검색 가능)
        self.knowledge_base = ChromaDB("knowledge")
    
    def add_message(self, role: str, content: str):
        self.messages.append({"role": role, "content": content})
        # 컨텍스트 길이 초과 시 요약 후 압축
        if len(self.messages) > 20:
            self._compress()
    
    def retrieve_relevant(self, query: str, k: int = 3) -> list[str]:
        """과거 에피소드에서 유사한 경험 검색"""
        return self.vector_store.similarity_search(query, k=k)
    
    def _compress(self):
        """오래된 메시지를 요약해서 벡터 DB에 저장"""
        old_messages = self.messages[:10]
        summary = summarize(old_messages)  # LLM으로 요약
        self.vector_store.add(summary)
        self.messages = self.messages[10:]

Multi-Agent 아키텍처

복잡한 작업은 전문화된 여러 Agent가 협력합니다.

Orchestrator Agent
├── Research Agent    → 정보 수집
├── Analysis Agent    → 데이터 분석
├── Code Agent        → 코드 작성/실행
└── Writer Agent      → 결과 정리
class OrchestratorAgent:
    def __init__(self):
        self.agents = {
            "research": ResearchAgent(),
            "analysis": AnalysisAgent(),
            "code": CodeAgent(),
            "writer": WriterAgent(),
        }
    
    async def run(self, task: str) -> str:
        # 1. 작업 분해
        subtasks = await self.plan(task)
        
        # 2. 병렬 실행 (독립적인 작업)
        results = await asyncio.gather(*[
            self.agents[st.agent].run(st.description)
            for st in subtasks
        ])
        
        # 3. 결과 통합
        return await self.synthesize(task, results)

안전성 설계

class SafeAgent:
    # 위험 도구는 승인 후 실행
    REQUIRES_APPROVAL = {"delete_file", "send_email", "deploy_production"}
    
    async def execute_tool(self, tool_name: str, params: dict):
        if tool_name in self.REQUIRES_APPROVAL:
            if not await self.request_human_approval(tool_name, params):
                return {"error": "사용자가 거부했습니다"}
        
        # 실행 전 입력 검증
        validated = self.validate_inputs(tool_name, params)
        
        # 타임아웃 설정
        try:
            return await asyncio.wait_for(
                self.tools[tool_name](**validated),
                timeout=30.0
            )
        except asyncio.TimeoutError:
            return {"error": "Tool execution timeout"}

프로덕션 체크리스트

  • 최대 반복 횟수 설정 (무한 루프 방지)
  • 도구 타임아웃 설정
  • 비용 상한 설정 (토큰 사용량 모니터링)
  • Human-in-the-loop: 위험 액션 전 승인 요청
  • 실행 로그 저장 (디버깅 + 감사)
  • 실패 복구: 도구 실패 시 대체 경로
  • 입력 검증: 프롬프트 인젝션 방어

비용 예측

# claude-opus-4-6 기준
# Input: $15/1M tokens, Output: $75/1M tokens

def estimate_cost(iterations: int, avg_tokens_per_turn: int = 1000):
    input_tokens = iterations * avg_tokens_per_turn
    output_tokens = iterations * 500
    
    cost = (input_tokens * 15 + output_tokens * 75) / 1_000_000
    return f"예상 비용: ${cost:.4f} / 태스크"

Agent 아키텍처는 강력하지만 비용이 빠르게 증가합니다.
캐싱과 모델 선택(Haiku vs Opus)으로 비용을 제어하세요.