AI 에이전트 아키텍처의 진화: 소프트웨어 엔지니어링으로의 회귀
AI 에이전트 아키텍처의 진화: 소프트웨어 엔지니어링으로의 회귀
"역사는 반복되지 않지만, 운율을 맞춘다" - 마크 트웨인
서론: 새로운 패러다임의 등장
2022년 11월, ChatGPT의 등장은 소프트웨어 개발 방식에 대한 근본적인 질문을 던졌다. "이제 코드를 작성하는 방식이 완전히 바뀔 것인가?" 많은 이들이 프로그래밍의 종말을 예언했고, 또 다른 이들은 단순한 과대광고라고 일축했다.
그로부터 3년이 지난 2026년 초, 우리는 흥미로운 현상을 목격하고 있다. AI 에이전트 시스템의 발전 방향이 소프트웨어 엔지니어링이 지난 50년간 걸어온 길과 놀라울 정도로 유사하다는 것이다.
- 경계(Boundary) 정의: 각 에이전트의 책임 범위를 명확히 구분
- 역할(Role) 분담: 전문화된 에이전트들의 협업
- 오케스트레이션(Orchestration): 에이전트 간 작업 흐름 조정
- 통신 프로토콜(Communication Protocol): 표준화된 메시지 교환 방식
이것은 마이크로서비스 아키텍처, 도메인 주도 설계(DDD), 서비스 지향 아키텍처(SOA)에서 우리가 이미 배운 교훈들이다. 새로운 기술이지만, 오래된 지혜가 다시 필요한 시점이다.
이 글에서는 GenAI의 출현부터 현재까지 AI 에이전트 시스템이 어떻게 진화해왔는지, 각 단계의 한계는 무엇이었는지, 그리고 왜 우리가 결국 전통적인 소프트웨어 아키텍처 패턴으로 회귀하고 있는지를 살펴본다.
1세대: 단일 프롬프트 시대 (2022-2023 초)
ChatGPT의 등장과 초기 활용
2022년 말, ChatGPT는 개발자들에게 마법과도 같았다. 자연어로 질문하면 코드가 나왔고, 버그를 설명하면 해결책을 제시했다. 초기 활용 패턴은 단순했다:
사용자: "Python으로 CSV 파일을 읽어서 평균을 계산하는 코드 작성해줘"
AI: [코드 생성]
사용자: [복사 → 붙여넣기 → 실행]
이 시기의 특징:
- 단발성 상호작용: 하나의 프롬프트, 하나의 응답
- 컨텍스트 독립성: 각 요청이 독립적
- 수동 통합: 개발자가 직접 결과를 복사하여 사용
한계의 발견
곧 한계가 명확해졌다:
- 컨텍스트 제한: 긴 코드베이스나 복잡한 요구사항을 한 번에 처리 불가
- 일관성 부족: 같은 질문에 다른 답변, 스타일 불일치
- 복잡한 작업 실패: 여러 단계가 필요한 작업에서 중간에 길을 잃음
- 에러 복구 불가: 생성된 코드에 오류가 있어도 스스로 수정 못함
개발자들은 깨달았다: "이것은 도구일 뿐, 완전한 해결책이 아니다."
2세대: 프롬프트 엔지니어링과 체인 (2023)
프롬프트의 과학화
한계를 극복하기 위해 "프롬프트 엔지니어링"이라는 새로운 분야가 등장했다. 핵심 기법들:
Chain-of-Thought (CoT)
"단계별로 생각해보자:
1. 먼저 요구사항을 분석하고
2. 필요한 함수들을 나열하고
3. 각 함수를 구현하고
4. 테스트 케이스를 작성하자"
Few-shot Learning
"예시 1: 입력 X → 출력 Y
예시 2: 입력 A → 출력 B
이제 입력 Z에 대해 출력을 생성해줘"
Role Prompting
"당신은 20년 경력의 시니어 Python 개발자입니다.
PEP 8 스타일 가이드를 준수하고..."
프레임워크의 등장: LangChain과 LlamaIndex
2023년 중반, 체계적인 프레임워크들이 등장했다:
LangChain의 핵심 개념:
- Chain: 여러 LLM 호출을 순차적으로 연결
- Memory: 대화 컨텍스트 유지
- Prompt Template: 재사용 가능한 프롬프트 구조
# LangChain 초기 패턴
from langchain import LLMChain, PromptTemplate
template = "다음 코드를 {language}로 변환: {code}"
prompt = PromptTemplate(template=template, input_variables=["language", "code"])
chain = LLMChain(llm=llm, prompt=prompt)
result = chain.run(language="Rust", code="def hello(): print('hi')")
LlamaIndex는 문서 검색과 질의응답에 특화:
- 대용량 문서를 인덱싱
- 관련 정보만 추출하여 LLM에 전달
- 답변 생성
한계: 선형성의 벽
이 접근법도 한계가 있었다:
- 선형적 처리: A → B → C 순서로만 진행, 병렬 처리 불가
- 에러 핸들링 부재: 중간 단계 실패 시 전체 체인 중단
- 확장성 문제: 체인이 길어질수록 관리 복잡도 급증
- 동적 분기 불가: 조건에 따른 다른 경로 실행 어려움
개발자들은 더 유연한 구조가 필요함을 인식했다.
3세대: 도구 사용과 RAG (2023 중반-2024 초)
Function Calling의 혁명
2023년 6월, OpenAI가 Function Calling을 발표하면서 게임이 바뀌었다. AI가 단순히 텍스트를 생성하는 것을 넘어, 실제 도구를 사용할 수 있게 되었다.
작동 방식:
# 도구 정의
tools = [
{
"name": "get_weather",
"description": "특정 도시의 날씨 정보 조회",
"parameters": {
"city": {"type": "string", "description": "도시 이름"}
}
},
{
"name": "execute_code",
"description": "Python 코드 실행",
"parameters": {
"code": {"type": "string", "description": "실행할 코드"}
}
}
]
# AI가 도구 선택 및 호출
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "서울 날씨 알려줘"}],
tools=tools
)
# AI 응답: "get_weather" 함수를 city="Seoul"로 호출하라는 지시
이제 AI는:
- 파일 시스템 접근
- API 호출
- 데이터베이스 쿼리
- 코드 실행
- 외부 서비스 통합
이 모든 것을 자율적으로 수행할 수 있게 되었다.
RAG: 지식의 확장
Retrieval-Augmented Generation(RAG)은 또 다른 돌파구였다:
문제: LLM은 학습 데이터 이후의 정보나 비공개 문서를 모른다 해결: 필요한 정보를 검색해서 프롬프트에 포함
# RAG 파이프라인
def rag_query(question, knowledge_base):
# 1. 질문과 관련된 문서 검색
relevant_docs = vector_search(question, knowledge_base)
# 2. 검색된 문서를 컨텍스트로 포함
context = "\n".join(relevant_docs)
prompt = f"컨텍스트: {context}\n\n질문: {question}"
# 3. LLM으로 답변 생성
answer = llm.generate(prompt)
return answer
활용 사례:
- 사내 문서 기반 Q&A 시스템
- 최신 기술 문서 참조
- 코드베이스 이해 및 탐색
한계: 복잡도의 증가
도구와 RAG는 강력했지만 새로운 문제를 낳았다:
-
도구 선택 오류:
- 잘못된 도구 선택 (날씨 API 대신 뉴스 API 호출)
- 파라미터 오류 (잘못된 형식, 누락된 필수값)
-
컨텍스트 관리 복잡도:
- 검색된 문서가 너무 많으면 토큰 한계 초과
- 관련 없는 문서가 섞이면 답변 품질 저하
-
에러 전파:
- 도구 실행 실패 시 복구 메커니즘 부재
- 연쇄적 실패 (A 실패 → B 실패 → C 실패)
-
디버깅 어려움:
- 어느 단계에서 잘못되었는지 추적 곤란
- 재현 불가능한 오류
개발자들은 더 체계적인 제어가 필요함을 깨달았다.
4세대: 에이전트 프레임워크 (2024)
자율 에이전트의 등장
2024년은 "에이전트의 해"였다. 핵심 아이디어: AI가 스스로 계획하고 실행하고 평가하는 루프
ReAct (Reasoning + Acting) 패턴:
1. Thought: "사용자가 데이터 분석을 원한다. 먼저 데이터를 로드해야 한다"
2. Action: load_csv("data.csv")
3. Observation: "1000행, 5열의 데이터 로드됨"
4. Thought: "이제 기술 통계를 계산하자"
5. Action: calculate_statistics(data)
6. Observation: "평균: 42.5, 표준편차: 12.3"
7. Thought: "시각화를 만들어야 한다"
8. Action: create_plot(data, type="histogram")
9. Observation: "plot.png 생성됨"
10. Thought: "작업 완료"
대표 프레임워크들
AutoGPT (2023년 3월):
- 목표 설정 → 자율 실행 → 결과 평가 루프
- 파일 읽기/쓰기, 웹 검색, 코드 실행
- 장기 메모리 (벡터 DB 활용)
BabyAGI (2023년 4월):
- 작업 목록 생성 및 우선순위 지정
- 작업 실행 및 결과 저장
- 새로운 작업 생성 (동적 계획)
LangGraph (2024년):
- 그래프 기반 워크플로우
- 조건부 분기, 루프, 병렬 실행
- 상태 관리 및 체크포인트
# LangGraph 예시
from langgraph.graph import StateGraph
# 상태 정의
class AgentState(TypedDict):
messages: List[str]
next_action: str
# 노드 정의
def plan_node(state):
# 계획 수립
return {"next_action": "execute"}
def execute_node(state):
# 실행
return {"next_action": "evaluate"}
def evaluate_node(state):
# 평가
return {"next_action": "done"}
# 그래프 구성
workflow = StateGraph(AgentState)
workflow.add_node("plan", plan_node)
workflow.add_node("execute", execute_node)
workflow.add_node("evaluate", evaluate_node)
workflow.add_edge("plan", "execute")
workflow.add_edge("execute", "evaluate")
workflow.add_conditional_edges("evaluate",
lambda s: s["next_action"],
{"done": END, "plan": "plan"}
)
한계: 신뢰성의 벽
자율 에이전트는 인상적이었지만 프로덕션에는 부적합했다:
-
무한 루프:
- 목표 달성 실패 시 같은 시도 반복
- 종료 조건 불명확
-
비용 폭증:
- 수백 번의 LLM 호출
- 예측 불가능한 토큰 사용량
-
신뢰성 문제:
- 같은 입력에 다른 결과
- 중요한 단계 건너뛰기
- 환각(hallucination)으로 인한 잘못된 판단
-
제어 불가:
- 중간 개입 어려움
- 디버깅 거의 불가능
- 예측 불가능한 동작
실제 사례:
목표: "경쟁사 분석 보고서 작성"
AutoGPT 실행:
- 웹 검색 50회
- 파일 생성 20개
- 코드 실행 30회
- 3시간 후에도 완료 안됨
- 비용: $47
- 결과: 불완전한 초안
개발자들은 깨달았다: "완전 자율은 답이 아니다. 구조화된 협업이 필요하다."
5세대: 멀티 에이전트 시스템 (2024 중반-2025)
역할 분담의 발견
단일 에이전트의 한계를 극복하기 위해, 개발자들은 전문화된 여러 에이전트의 협업이라는 아이디어로 전환했다.
핵심 통찰:
"만능 에이전트 하나보다, 전문화된 에이전트 여럿이 낫다"
이것은 소프트웨어 팀의 구조와 유사하다:
- 기획자: 요구사항 분석, 작업 분해
- 개발자: 코드 작성
- 테스터: 품질 검증
- 리뷰어: 코드 리뷰
- 문서 작성자: 문서화
대표 프레임워크: CrewAI
CrewAI (2024년)는 이 개념을 구현한 대표적 프레임워크다:
from crewai import Agent, Task, Crew
# 에이전트 정의
researcher = Agent(
role="연구원",
goal="주제에 대한 심층 조사",
backstory="10년 경력의 기술 리서처",
tools=[web_search, read_document]
)
writer = Agent(
role="작가",
goal="명확하고 설득력 있는 글 작성",
backstory="기술 블로그 전문 작가",
tools=[write_file, format_markdown]
)
reviewer = Agent(
role="리뷰어",
goal="품질 검증 및 개선 제안",
backstory="시니어 에디터",
tools=[check_grammar, verify_facts]
)
# 작업 정의
research_task = Task(
description="AI 에이전트 아키텍처 트렌드 조사",
agent=researcher,
expected_output="조사 보고서"
)
writing_task = Task(
description="조사 내용을 바탕으로 블로그 글 작성",
agent=writer,
context=[research_task], # 이전 작업 결과 참조
expected_output="블로그 초안"
)
review_task = Task(
description="글 검토 및 개선",
agent=reviewer,
context=[writing_task],
expected_output="최종 블로그"
)
# 크루 구성 및 실행
crew = Crew(
agents=[researcher, writer, reviewer],
tasks=[research_task, writing_task, review_task],
process="sequential" # 순차 실행
)
result = crew.kickoff()
CrewAI의 특징:
- 역할 기반 설계: 각 에이전트가 명확한 역할과 목표
- 컨텍스트 공유: 이전 작업 결과를 다음 에이전트에게 전달
- 도구 전문화: 각 에이전트가 자신의 역할에 맞는 도구만 사용
- 프로세스 제어: Sequential, Hierarchical 등 다양한 실행 방식
Microsoft AutoGen: 대화 기반 협업
AutoGen은 다른 접근을 취했다: 에이전트 간 대화를 통한 문제 해결
from autogen import AssistantAgent, UserProxyAgent
# 어시스턴트 에이전트 (코드 작성)
assistant = AssistantAgent(
name="코더",
llm_config={"model": "gpt-4"},
system_message="Python 전문가. 깔끔한 코드 작성"
)
# 사용자 프록시 (코드 실행)
user_proxy = UserProxyAgent(
name="실행자",
code_execution_config={"work_dir": "coding"},
human_input_mode="NEVER"
)
# 대화 시작
user_proxy.initiate_chat(
assistant,
message="피보나치 수열 계산 함수를 작성하고 테스트해줘"
)
실행 흐름:
실행자: "피보나치 수열 계산 함수를 작성하고 테스트해줘"
코더: "알겠습니다. 다음 코드를 작성했습니다:
```python
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# 테스트
for i in range(10):
print(f"F({i}) = {fibonacci(i)}")
실행해주세요."
실행자: [코드 실행] "출력: F(0) = 0, F(1) = 1, F(2) = 1, ..."
코더: "완료되었습니다. 추가로 메모이제이션을 적용하면 성능을 개선할 수 있습니다."
**AutoGen의 강점**:
- **자연스러운 협업**: 대화를 통한 문제 해결
- **반복적 개선**: 피드백 루프
- **그룹 채팅**: 3개 이상의 에이전트 협업 가능
### LangGraph: 복잡한 워크플로우
**LangGraph**는 그래프 구조로 복잡한 에이전트 워크플로우를 표현:
```python
from langgraph.graph import StateGraph, END
# 멀티 에이전트 그래프
workflow = StateGraph(AgentState)
# 에이전트 노드들
workflow.add_node("supervisor", supervisor_agent)
workflow.add_node("researcher", research_agent)
workflow.add_node("coder", coding_agent)
workflow.add_node("tester", testing_agent)
# 슈퍼바이저가 작업 분배
workflow.add_edge("supervisor", "researcher")
workflow.add_edge("supervisor", "coder")
# 조건부 라우팅
workflow.add_conditional_edges(
"tester",
lambda state: "coder" if state["tests_failed"] else END,
{"coder": "coder", END: END}
)
workflow.set_entry_point("supervisor")
LangGraph의 특징:
- 명시적 제어 흐름: 그래프로 워크플로우 시각화
- 조건부 분기: 상태에 따른 동적 라우팅
- 체크포인트: 중간 상태 저장 및 복구
- 스트리밍: 실시간 진행 상황 모니터링
한계: 조정의 복잡성
멀티 에이전트 시스템도 새로운 도전 과제를 가져왔다:
-
조정 오버헤드:
- 에이전트 간 통신 비용
- 컨텍스트 동기화 문제
- 작업 분배 및 병합의 복잡도
-
통신 복잡도:
- 메시지 형식 불일치
- 정보 손실 (에이전트 A → B 전달 시)
- 순환 참조 및 데드락
-
일관성 문제:
- 에이전트 간 상태 불일치
- 동시성 제어 부재
- 트랜잭션 개념 없음
-
디버깅 악몽:
- 어느 에이전트가 문제인지 파악 어려움
- 에이전트 간 상호작용 추적 곤란
- 재현 불가능한 버그
실제 문제 사례:
시나리오: 5개 에이전트가 협업하여 웹 애플리케이션 개발
문제:
- 기획 에이전트: "사용자 인증 필요"
- 백엔드 에이전트: JWT 기반 인증 구현
- 프론트 에이전트: 세션 기반 인증 구현 (불일치!)
- 테스트 에이전트: 어느 것을 테스트해야 할지 혼란
- 문서 에이전트: 두 가지 방식 모두 문서화 (혼란 가중)
결과: 통합 실패, 수동 개입 필요
개발자들은 깨달았다: "에이전트를 많이 만드는 것만으로는 부족하다. 아키텍처가 필요하다."
6세대: 아키텍처 패턴의 재발견 (2025-2026)
전환점: 소프트웨어 엔지니어링으로의 회귀
2025년 중반부터, AI 에이전트 커뮤니티에서 흥미로운 변화가 일어났다. 개발자들이 전통적인 소프트웨어 아키텍처 패턴을 AI 에이전트 시스템에 적용하기 시작한 것이다.
핵심 깨달음:
"AI 에이전트 시스템도 결국 분산 소프트웨어 시스템이다. 우리는 이미 50년간 이런 시스템을 만들어왔다."
실전 사례 1: Claude Code Agent Teams (2026년 2월)
Anthropic이 Opus 4.6과 함께 발표한 Agent Teams는 이 패러다임의 완벽한 예시다.
아키텍처:
graph TD
TL[Team Lead<br/>Coordinator<br/>- 작업 분해 및 할당<br/>- 진행 상황 모니터링<br/>- 결과 통합]
T1[Teammate 1]
T2[Teammate 2]
T3[Teammate 3]
STL[Shared Task List]
TL --> T1
TL --> T2
TL --> T3
T1 --> STL
T2 --> STL
T3 --> STL
style TL fill:#4299e1,stroke:#2c5282,color:#fff
style T1 fill:#48bb78,stroke:#2f855a,color:#fff
style T2 fill:#48bb78,stroke:#2f855a,color:#fff
style T3 fill:#48bb78,stroke:#2f855a,color:#fff
style STL fill:#ed8936,stroke:#c05621,color:#fff
핵심 설계 원칙:
-
명확한 역할 분리:
- Team Lead: 오케스트레이터 (조정자)
- Teammates: 워커 (실행자)
- 각자의 책임 범위가 명확
-
독립적 컨텍스트:
- 각 teammate는 독립된 컨텍스트 윈도우
- 메모리 격리로 간섭 방지
- 병렬 실행 가능
-
표준화된 통신:
- Shared Task List: 작업 큐
- Peer-to-peer messaging: 직접 통신
- Status updates: 진행 상황 공유
-
동적 작업 할당:
- Teammates가 능동적으로 작업 선택
- 부하 분산 자동화
- 우선순위 기반 스케줄링
실제 사용 예시:
# Agent Teams 활성화
export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
# 대규모 리팩토링 작업
$ claude-code "레거시 코드베이스를 TypeScript로 마이그레이션"
Team Lead: "작업을 분석했습니다. 50개 파일, 예상 시간 2시간"
Team Lead: "3개 teammate 생성, 작업 분배 중..."
Teammate-1: "src/auth 모듈 변환 시작"
Teammate-2: "src/api 모듈 변환 시작"
Teammate-3: "src/utils 모듈 변환 시작"
[병렬 실행]
Teammate-1: "auth 모듈 완료, 테스트 통과"
Teammate-2: "api 모듈 완료, 타입 정의 추가"
Teammate-3: "utils 모듈 완료, 문서 업데이트"
Team Lead: "모든 작업 완료. 통합 테스트 실행 중..."
Team Lead: "마이그레이션 성공. 커밋 준비 완료"
소프트웨어 패턴과의 유사성:
- 마이크로서비스: 각 teammate는 독립적 서비스
- 메시지 큐: Shared Task List는 작업 큐
- 오케스트레이션: Team Lead는 오케스트레이터
- 서비스 메시: Peer-to-peer messaging
실전 사례 2: oh-my-opencode (2025-2026)
oh-my-opencode는 OpenCode를 멀티 에이전트 오케스트레이션 플랫폼으로 변환하는 플러그인이다.
아키텍처 설계:
graph TD
MA[Main Agent<br/>Coordinator<br/>Model: Claude Opus 4.6<br/>전체 작업 조율, 의사결정]
P[Planner<br/>GPT-4]
R[Researcher<br/>Gemini]
C[Coder<br/>Claude]
D[Debugger<br/>GPT-4o]
Tools[Shared Tools<br/>- LSP<br/>- AST-Grep<br/>- File System<br/>- Terminal]
MA --> P
MA --> R
MA --> C
MA --> D
P --> Tools
R --> Tools
C --> Tools
D --> Tools
style MA fill:#667eea,stroke:#5a67d8,color:#fff
style P fill:#4299e1,stroke:#2c5282,color:#fff
style R fill:#48bb78,stroke:#2f855a,color:#fff
style C fill:#ed8936,stroke:#c05621,color:#fff
style D fill:#e53e3e,stroke:#c53030,color:#fff
style Tools fill:#edf2f7,stroke:#a0aec0
핵심 특징:
-
역할별 모델 최적화:
agents: planner: model: gpt-4 role: "요구사항 분석 및 작업 계획" tools: [read_files, search_code] researcher: model: gemini-pro role: "문서 조사 및 API 탐색" tools: [web_search, read_docs] coder: model: claude-opus-4.6 role: "코드 작성 및 수정" tools: [lsp, ast_grep, write_files] debugger: model: gpt-4o role: "버그 분석 및 수정" tools: [execute_code, read_logs] -
비동기 서브에이전트:
- 백그라운드에서 병렬 실행
- 메인 에이전트는 블로킹 없이 다른 작업 수행
- 결과는 콜백으로 전달
-
도구 전문화:
- LSP (Language Server Protocol): 코드 인텔리전스
- 정의로 이동, 참조 찾기
- 자동완성, 타입 체크
- AST-Grep: 구조적 코드 검색
- 패턴 매칭
- 대규모 리팩토링
- LSP (Language Server Protocol): 코드 인텔리전스
-
MCP (Model Context Protocol) 통합:
- 표준화된 컨텍스트 공유
- 도구 간 상호운용성
실제 워크플로우:
사용자: "이 프로젝트에 인증 기능 추가해줘"
Main Agent: "작업을 분석하겠습니다"
→ Planner 호출 (비동기)
Planner: "다음 단계 필요:
1. 인증 라이브러리 조사
2. 데이터베이스 스키마 설계
3. API 엔드포인트 구현
4. 프론트엔드 통합"
Main Agent: "병렬 작업 시작"
→ Researcher: "인증 라이브러리 조사" (비동기)
→ Coder: "데이터베이스 스키마 작성" (비동기)
[병렬 실행]
Researcher: "Passport.js 추천, JWT 기반"
Coder: "User 테이블 스키마 완성"
Main Agent: "다음 단계 진행"
→ Coder: "API 엔드포인트 구현"
Coder: [LSP로 기존 코드 분석]
Coder: [AST-Grep로 패턴 검색]
Coder: "auth.ts 작성 완료"
Main Agent: "테스트 필요"
→ Debugger: "단위 테스트 작성 및 실행"
Debugger: "모든 테스트 통과"
Main Agent: "작업 완료. 커밋 메시지 생성"
소프트웨어 패턴 적용:
-
Bounded Context (DDD):
- 각 에이전트는 자신의 도메인에 집중
- Planner: 계획 도메인
- Coder: 구현 도메인
- Debugger: 품질 도메인
-
Command Pattern:
- 각 작업은 명령 객체로 캡슐화
- 큐에 저장, 비동기 실행
-
Observer Pattern:
- 서브에이전트 완료 시 메인 에이전트에 알림
- 이벤트 기반 조정
실전 사례 3: CrewAI의 진화 (2025-2026)
CrewAI도 소프트웨어 아키텍처 패턴을 적극 도입했다.
Hierarchical Process (계층적 프로세스):
from crewai import Crew, Process
crew = Crew(
agents=[manager, developer, tester, deployer],
tasks=[plan_task, code_task, test_task, deploy_task],
process=Process.hierarchical, # 계층적 구조
manager_llm="gpt-4" # 매니저 에이전트 자동 생성
)
작동 방식:
Manager (자동 생성)
│
├─→ Developer: "로그인 기능 구현"
│ └─→ 완료 보고
│
├─→ Tester: "로그인 테스트"
│ └─→ 버그 발견 보고
│
├─→ Developer: "버그 수정"
│ └─→ 완료 보고
│
├─→ Tester: "재테스트"
│ └─→ 통과 보고
│
└─→ Deployer: "배포"
└─→ 완료 보고
Conditional Tasks (조건부 작업):
from crewai import Task
test_task = Task(
description="코드 테스트",
agent=tester,
expected_output="테스트 결과"
)
fix_task = Task(
description="버그 수정",
agent=developer,
condition=lambda output: "FAILED" in output, # 조건부 실행
context=[test_task]
)
Memory Systems (메모리 시스템):
crew = Crew(
agents=[...],
tasks=[...],
memory=True, # 메모리 활성화
memory_config={
"provider": "mem0", # 메모리 제공자
"config": {
"vector_store": "qdrant",
"embedding_model": "text-embedding-3-small"
}
}
)
메모리 계층:
- Short-term Memory: 현재 작업 컨텍스트
- Long-term Memory: 과거 작업 이력 (벡터 DB)
- Entity Memory: 중요 개체 추적 (사용자, 파일, 설정)
- Contextual Memory: 에이전트 간 공유 컨텍스트
소프트웨어 패턴:
- Chain of Responsibility: 계층적 작업 위임
- Strategy Pattern: 조건에 따른 작업 선택
- Memento Pattern: 상태 저장 및 복원
7세대: 엔터프라이즈 패턴의 완전한 적용 (2026년 현재)
Bounded Context와 Domain-Driven Design
**Domain-Driven Design (DDD)**의 핵심 개념인 Bounded Context가 AI 에이전트 설계에 적용되고 있다.
Bounded Context란?
- 특정 도메인 모델이 유효한 경계
- 경계 내에서는 용어와 규칙이 일관됨
- 경계 간에는 명시적 변환 필요
AI 에이전트 적용:
graph TB
subgraph AC[Authentication Context]
UA[User Agent<br/>사용자 관리]
TA[Token Agent<br/>토큰 발급/검증]
SA[Session Agent<br/>세션 관리]
end
ACL[Anti-Corruption Layer<br/>변환 계층]
subgraph PC[Payment Context]
OA[Order Agent<br/>주문 처리]
PA[Payment Agent<br/>결제 처리]
IA[Invoice Agent<br/>청구서 생성]
end
AC --> ACL
ACL --> PC
style AC fill:#ebf8ff,stroke:#4299e1
style PC fill:#f0fff4,stroke:#48bb78
style ACL fill:#fed7d7,stroke:#e53e3e
Anti-Corruption Layer (부패 방지 계층):
class AuthToPaymentAdapter:
"""Authentication Context의 User를 Payment Context의 Customer로 변환"""
def user_to_customer(self, user: User) -> Customer:
return Customer(
id=user.id,
name=user.full_name,
email=user.email,
# Payment Context에 필요한 정보만 전달
)
# 에이전트 간 통신
auth_agent = UserAgent()
payment_agent = OrderAgent()
user = auth_agent.get_user(user_id)
customer = AuthToPaymentAdapter().user_to_customer(user)
payment_agent.create_order(customer, items)
이점:
- 명확한 책임: 각 컨텍스트는 자신의 도메인에만 집중
- 독립적 진화: 한 컨텍스트 변경이 다른 컨텍스트에 영향 없음
- 용어 일관성: 컨텍스트 내에서 명확한 의미
마이크로서비스 아키텍처와의 유사성
AI 에이전트 시스템은 마이크로서비스 아키텍처의 원칙을 따르고 있다.
마이크로서비스 원칙:
- 단일 책임: 각 서비스는 하나의 비즈니스 기능
- 독립 배포: 서비스 간 독립적 배포
- 분산 데이터: 각 서비스가 자신의 데이터 소유
- API 통신: 표준화된 인터페이스
AI 에이전트 적용:
graph LR
RA[Research Agent<br/>- Web Search<br/>- Doc Reading<br/>- Summarization<br/><br/>Own Memory:<br/>Search Cache]
CA[Coding Agent<br/>- LSP<br/>- AST<br/>- File I/O<br/><br/>Own Memory:<br/>Code Context]
MB[Message Bus<br/>Event Queue]
RA --> MB
CA --> MB
MB --> RA
MB --> CA
style RA fill:#4299e1,stroke:#2c5282,color:#fff
style CA fill:#48bb78,stroke:#2f855a,color:#fff
style MB fill:#ed8936,stroke:#c05621,color:#fff
실제 구현 예시 (LangGraph + Redis):
from langgraph.graph import StateGraph
from redis import Redis
# 메시지 버스
message_bus = Redis(host='localhost', port=6379)
class ResearchAgent:
def __init__(self):
self.memory = {} # 독립적 메모리
def execute(self, task):
# 작업 수행
result = self.research(task)
# 결과를 메시지 버스에 발행
message_bus.publish('research_complete', {
'task_id': task.id,
'result': result
})
return result
class CodingAgent:
def __init__(self):
self.memory = {} # 독립적 메모리
# 메시지 구독
pubsub = message_bus.pubsub()
pubsub.subscribe('research_complete')
def on_research_complete(self, message):
# 리서치 완료 이벤트 처리
research_result = message['result']
self.generate_code(research_result)
이점:
- 확장성: 에이전트 독립적 확장
- 장애 격리: 한 에이전트 실패가 전체 시스템 중단 안 함
- 기술 다양성: 각 에이전트가 다른 모델/도구 사용 가능
오케스트레이션 vs 코레오그래피
분산 시스템에서 서비스 조정 방식은 두 가지다:
1. 오케스트레이션 (Orchestration):
- 중앙 조정자가 모든 흐름 제어
- 명시적 순서 정의
# 오케스트레이션 예시
class OrchestratorAgent:
def process_request(self, request):
# 1단계: 리서치
research = self.research_agent.execute(request)
# 2단계: 코딩 (리서치 결과 전달)
code = self.coding_agent.execute(research)
# 3단계: 테스트 (코드 전달)
test_result = self.testing_agent.execute(code)
# 4단계: 조건부 분기
if test_result.failed:
code = self.coding_agent.fix(code, test_result)
test_result = self.testing_agent.execute(code)
# 5단계: 배포
return self.deploy_agent.execute(code)
장점:
- 명확한 흐름 파악
- 중앙 집중식 로깅/모니터링
- 디버깅 용이
단점:
- 단일 장애점 (orchestrator 실패 시 전체 중단)
- 확장성 제한
- 조정자 복잡도 증가
2. 코레오그래피 (Choreography):
- 중앙 조정자 없음
- 에이전트들이 이벤트에 반응하여 자율적으로 행동
# 코레오그래피 예시
class ResearchAgent:
def on_request(self, event):
result = self.research(event.data)
# 다음 에이전트를 직접 호출하지 않고 이벤트 발행
event_bus.publish('research_completed', result)
class CodingAgent:
def __init__(self):
# 이벤트 구독
event_bus.subscribe('research_completed', self.on_research_completed)
def on_research_completed(self, event):
code = self.generate_code(event.data)
event_bus.publish('code_generated', code)
class TestingAgent:
def __init__(self):
event_bus.subscribe('code_generated', self.on_code_generated)
def on_code_generated(self, event):
result = self.test(event.data)
if result.failed:
event_bus.publish('test_failed', result)
else:
event_bus.publish('test_passed', result)
class CodingAgent: # 재등장
def __init__(self):
event_bus.subscribe('test_failed', self.on_test_failed)
def on_test_failed(self, event):
fixed_code = self.fix(event.data)
event_bus.publish('code_generated', fixed_code)
장점:
- 느슨한 결합
- 높은 확장성
- 단일 장애점 없음
단점:
- 흐름 파악 어려움
- 디버깅 복잡
- 순환 의존성 위험
실전 권장사항:
- 간단한 워크플로우: 오케스트레이션
- 복잡하고 동적인 워크플로우: 코레오그래피
- 하이브리드: 도메인별로 오케스트레이션, 도메인 간은 코레오그래피
통신 프로토콜과 인터페이스 설계
에이전트 간 통신을 표준화하는 것이 중요해졌다.
MCP (Model Context Protocol):
- Anthropic이 제안한 표준 프로토콜
- AI 모델과 도구 간 통신 표준화
// MCP 메시지 형식
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": {
"path": "/path/to/file.txt"
}
},
"id": 1
}
// 응답
{
"jsonrpc": "2.0",
"result": {
"content": "파일 내용..."
},
"id": 1
}
에이전트 간 메시지 프로토콜:
from dataclasses import dataclass
from typing import Any, Optional
from enum import Enum
class MessageType(Enum):
REQUEST = "request"
RESPONSE = "response"
EVENT = "event"
ERROR = "error"
@dataclass
class AgentMessage:
"""표준 에이전트 메시지"""
type: MessageType
sender: str
receiver: Optional[str] # None이면 브로드캐스트
payload: Any
correlation_id: str # 요청-응답 매칭
timestamp: float
# 사용 예시
message = AgentMessage(
type=MessageType.REQUEST,
sender="research_agent",
receiver="coding_agent",
payload={
"action": "generate_code",
"context": research_result
},
correlation_id="req-12345",
timestamp=time.time()
)
인터페이스 계약 (Contract):
from abc import ABC, abstractmethod
class AgentInterface(ABC):
"""모든 에이전트가 구현해야 하는 인터페이스"""
@abstractmethod
def execute(self, task: Task) -> Result:
"""작업 실행"""
pass
@abstractmethod
def can_handle(self, task: Task) -> bool:
"""이 작업을 처리할 수 있는지 확인"""
pass
@abstractmethod
def get_capabilities(self) -> List[str]:
"""에이전트의 능력 목록 반환"""
pass
class ResearchAgent(AgentInterface):
def execute(self, task: Task) -> Result:
# 구현
pass
def can_handle(self, task: Task) -> bool:
return task.type in ["research", "search", "summarize"]
def get_capabilities(self) -> List[str]:
return ["web_search", "document_reading", "summarization"]
관찰 가능성과 모니터링
프로덕션 AI 에이전트 시스템에는 관찰 가능성이 필수다.
세 가지 기둥:
- 로깅 (Logging):
import structlog
logger = structlog.get_logger()
class ResearchAgent:
def execute(self, task):
logger.info(
"agent_execution_started",
agent="research_agent",
task_id=task.id,
task_type=task.type
)
try:
result = self.research(task)
logger.info(
"agent_execution_completed",
agent="research_agent",
task_id=task.id,
duration=result.duration,
tokens_used=result.tokens
)
return result
except Exception as e:
logger.error(
"agent_execution_failed",
agent="research_agent",
task_id=task.id,
error=str(e)
)
raise
- 메트릭 (Metrics):
from prometheus_client import Counter, Histogram, Gauge
# 메트릭 정의
agent_requests = Counter(
'agent_requests_total',
'Total agent requests',
['agent_name', 'status']
)
agent_duration = Histogram(
'agent_duration_seconds',
'Agent execution duration',
['agent_name']
)
agent_tokens = Counter(
'agent_tokens_total',
'Total tokens used',
['agent_name', 'model']
)
# 사용
class ResearchAgent:
def execute(self, task):
with agent_duration.labels(agent_name='research').time():
try:
result = self.research(task)
agent_requests.labels(
agent_name='research',
status='success'
).inc()
agent_tokens.labels(
agent_name='research',
model='gpt-4'
).inc(result.tokens)
return result
except Exception:
agent_requests.labels(
agent_name='research',
status='error'
).inc()
raise
- 트레이싱 (Tracing):
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
tracer = trace.get_tracer(__name__)
class OrchestratorAgent:
def process_request(self, request):
with tracer.start_as_current_span("orchestrator.process") as span:
span.set_attribute("request.id", request.id)
# 리서치
with tracer.start_as_current_span("research.execute") as research_span:
research = self.research_agent.execute(request)
research_span.set_attribute("tokens", research.tokens)
# 코딩
with tracer.start_as_current_span("coding.execute") as coding_span:
code = self.coding_agent.execute(research)
coding_span.set_attribute("lines_of_code", len(code.split('\n')))
span.set_status(Status(StatusCode.OK))
return code
분산 트레이싱 시각화:
Request ID: req-12345
│
├─ orchestrator.process (2.5s)
│ │
│ ├─ research.execute (1.2s)
│ │ ├─ web_search (0.8s)
│ │ └─ summarize (0.4s)
│ │
│ ├─ coding.execute (1.0s)
│ │ ├─ lsp.analyze (0.3s)
│ │ └─ generate_code (0.7s)
│ │
│ └─ testing.execute (0.3s)
이를 통해:
- 병목 지점 파악
- 에러 발생 위치 추적
- 에이전트 간 의존성 시각화
핵심 통찰: 역사는 반복된다
소프트웨어 공학 50년의 교훈
AI 에이전트 시스템의 진화를 보면, 소프트웨어 공학이 지난 50년간 겪은 여정과 놀라울 정도로 유사하다.
1970년대: 모놀리식 애플리케이션
- 하나의 거대한 프로그램
- 모든 기능이 하나의 프로세스에
- → AI 1세대: 단일 프롬프트
1980년대: 구조적 프로그래밍
- 함수와 모듈로 분리
- 재사용 가능한 컴포넌트
- → AI 2세대: 프롬프트 체인
1990년대: 객체 지향 프로그래밍
- 캡슐화, 상속, 다형성
- 명확한 인터페이스
- → AI 3세대: 도구 사용
2000년대: 서비스 지향 아키텍처 (SOA)
- 독립적 서비스들의 조합
- 표준 프로토콜 (SOAP, REST)
- → AI 4세대: 에이전트 프레임워크
2010년대: 마이크로서비스
- 작고 독립적인 서비스
- 분산 시스템
- → AI 5세대: 멀티 에이전트
2020년대: 클라우드 네이티브
- 오케스트레이션 (Kubernetes)
- 관찰 가능성
- 서비스 메시
- → AI 6-7세대: 아키텍처 패턴 적용
반복되는 문제들
각 단계에서 마주친 문제들도 유사하다:
| 소프트웨어 공학 | AI 에이전트 시스템 |
|---|---|
| 스파게티 코드 | 프롬프트 체인 복잡도 |
| 타이트 커플링 | 에이전트 간 강한 의존성 |
| 단일 장애점 | 중앙 오케스트레이터 실패 |
| 확장성 문제 | 에이전트 수 증가 시 성능 저하 |
| 디버깅 어려움 | 에이전트 상호작용 추적 곤란 |
| 일관성 문제 | 에이전트 간 상태 불일치 |
반복되는 해결책들
그리고 해결책도 유사하다:
| 소프트웨어 공학 | AI 에이전트 시스템 |
|---|---|
| 모듈화 | 에이전트 역할 분리 |
| 인터페이스 정의 | 메시지 프로토콜 표준화 |
| 의존성 주입 | 도구 주입 |
| 이벤트 버스 | 에이전트 메시지 버스 |
| 서비스 디스커버리 | 에이전트 레지스트리 |
| 로드 밸런싱 | 작업 분배 |
| 서킷 브레이커 | 에이전트 실패 격리 |
| 분산 트레이싱 | 에이전트 실행 추적 |
AI 시스템도 결국 소프트웨어다
이 모든 것이 시사하는 바는 명확하다:
AI 에이전트 시스템도 결국 소프트웨어 시스템이다.
AI가 특별해 보이지만, 시스템 설계 관점에서는:
- 입력을 받아 처리하고 출력을 내는 컴포넌트
- 다른 컴포넌트와 통신하는 분산 시스템
- 상태를 관리하고 에러를 처리해야 하는 프로그램
따라서:
- 소프트웨어 공학의 원칙이 그대로 적용됨
- 검증된 아키텍처 패턴을 활용할 수 있음
- 기존 도구와 방법론을 재사용 가능
검증된 패턴의 재적용
우리는 바퀴를 다시 발명할 필요가 없다. 이미 검증된 패턴들을 적용하면 된다:
1. SOLID 원칙:
- Single Responsibility: 각 에이전트는 하나의 책임
- Open/Closed: 새 에이전트 추가는 쉽게, 기존 에이전트 수정은 최소화
- Liskov Substitution: 같은 역할의 에이전트는 교체 가능
- Interface Segregation: 에이전트는 필요한 인터페이스만 구현
- Dependency Inversion: 구체적 구현이 아닌 추상화에 의존
2. 디자인 패턴:
- Factory: 에이전트 생성 로직 캡슐화
- Strategy: 다양한 LLM 모델 전략
- Observer: 에이전트 상태 변화 감지
- Command: 작업을 객체로 캡슐화
- Chain of Responsibility: 에이전트 체인
3. 아키텍처 패턴:
- Layered Architecture: 프레젠테이션, 비즈니스, 데이터 계층
- Event-Driven Architecture: 이벤트 기반 에이전트 통신
- CQRS: 읽기/쓰기 에이전트 분리
- Saga Pattern: 분산 트랜잭션 관리
실전 가이드: 프로덕션 AI 에이전트 시스템 설계
이제 실전에서 어떻게 적용할지 구체적으로 살펴보자.
1. 경계 설정 원칙
질문: "어디까지가 하나의 에이전트인가?"
원칙:
-
단일 책임 원칙:
- 하나의 에이전트는 하나의 명확한 역할
- "그리고(AND)"가 들어가면 분리 고려
❌ 나쁜 예: "리서치하고 코드 작성하는 에이전트" ✅ 좋은 예: "리서치 에이전트" + "코딩 에이전트" -
응집도 최대화:
- 관련된 기능은 함께
- 에이전트 내부 통신 > 에이전트 간 통신
# 높은 응집도 class DataAnalysisAgent: def load_data(self): ... def clean_data(self): ... def analyze_data(self): ... def visualize_data(self): ... # 낮은 응집도 (분리 필요) class MixedAgent: def load_data(self): ... def send_email(self): ... # 다른 도메인! def analyze_data(self): ... def deploy_model(self): ... # 다른 도메인! -
결합도 최소화:
- 에이전트 간 의존성 최소화
- 인터페이스를 통한 통신
# 타이트 커플링 (나쁨) class CodingAgent: def __init__(self): self.research_agent = ResearchAgent() # 직접 의존 def generate_code(self, topic): research = self.research_agent.research(topic) # 직접 호출 return self.code_from_research(research) # 루즈 커플링 (좋음) class CodingAgent: def __init__(self, message_bus): self.message_bus = message_bus self.message_bus.subscribe('research_complete', self.on_research) def on_research(self, message): research = message.payload code = self.code_from_research(research) self.message_bus.publish('code_generated', code) -
크기 제한:
- 에이전트가 너무 크면 분리
- 경험적 기준: 도구 5-10개, 프롬프트 500-1000 토큰
2. 역할 정의 방법론
단계별 접근:
Step 1: 도메인 분석
프로젝트: "자동화된 코드 리뷰 시스템"
도메인 식별:
- 코드 분석
- 품질 검증
- 보안 검사
- 성능 분석
- 문서 생성
Step 2: 역할 추출
각 도메인에서 역할 도출:
코드 분석:
- 코드 파서: 코드 구조 분석
- 패턴 탐지기: 안티패턴 찾기
품질 검증:
- 테스트 분석가: 테스트 커버리지 확인
- 스타일 검사자: 코딩 스타일 검증
보안 검사:
- 취약점 스캐너: 보안 이슈 탐지
- 의존성 검사자: 라이브러리 취약점 확인
성능 분석:
- 복잡도 분석가: 시간/공간 복잡도 계산
- 병목 탐지기: 성능 이슈 찾기
문서 생성:
- 요약 작성자: 변경 사항 요약
- 제안 생성자: 개선 제안 작성
Step 3: 에이전트 정의
from dataclasses import dataclass
from typing import List
@dataclass
class AgentDefinition:
name: str
role: str
goal: str
backstory: str
tools: List[str]
model: str
max_iterations: int
# 예시
code_parser = AgentDefinition(
name="code_parser",
role="코드 구조 분석가",
goal="코드의 구조와 의존성을 파악하여 다른 에이전트가 이해할 수 있는 형태로 변환",
backstory="10년 경력의 컴파일러 엔지니어. AST와 정적 분석 전문가",
tools=["ast_parser", "lsp_client", "dependency_analyzer"],
model="gpt-4",
max_iterations=3
)
vulnerability_scanner = AgentDefinition(
name="vulnerability_scanner",
role="보안 취약점 탐지 전문가",
goal="코드에서 보안 취약점을 찾아내고 심각도를 평가",
backstory="OWASP Top 10 전문가. 화이트햇 해커 출신",
tools=["semgrep", "bandit", "cve_database"],
model="gpt-4",
max_iterations=5
)
Step 4: 상호작용 설계
워크플로우:
1. 코드 파서 → 코드 구조 분석
↓
2. 병렬 실행:
- 패턴 탐지기 → 안티패턴 찾기
- 테스트 분석가 → 커버리지 확인
- 취약점 스캐너 → 보안 이슈 탐지
- 복잡도 분석가 → 성능 분석
↓
3. 요약 작성자 → 결과 통합
↓
4. 제안 생성자 → 개선 제안
3. 오케스트레이션 패턴 선택
의사결정 트리:
워크플로우가 선형적인가?
├─ Yes → Sequential (순차)
│ 예: 리서치 → 코딩 → 테스트 → 배포
│
└─ No → 병렬 작업이 있는가?
├─ Yes → 병렬 작업이 독립적인가?
│ ├─ Yes → Parallel (병렬)
│ │ 예: 여러 파일 동시 분석
│ │
│ └─ No → 조정이 필요한가?
│ ├─ Yes → Hierarchical (계층적)
│ │ 예: 매니저가 작업 분배
│ │
│ └─ No → Event-Driven (이벤트 기반)
│ 예: 에이전트가 이벤트에 반응
│
└─ No → 동적 분기가 있는가?
├─ Yes → Conditional (조건부)
│ 예: 테스트 실패 시 수정
│
└─ No → Sequential
패턴별 구현:
Sequential (순차):
from crewai import Crew, Process
crew = Crew(
agents=[agent1, agent2, agent3],
tasks=[task1, task2, task3],
process=Process.sequential
)
Parallel (병렬):
import asyncio
async def parallel_execution(agents, task):
results = await asyncio.gather(*[
agent.execute_async(task) for agent in agents
])
return results
Hierarchical (계층적):
crew = Crew(
agents=[worker1, worker2, worker3],
tasks=[task1, task2, task3],
process=Process.hierarchical,
manager_llm="gpt-4" # 매니저 자동 생성
)
Event-Driven (이벤트 기반):
class EventBus:
def __init__(self):
self.subscribers = {}
def subscribe(self, event_type, handler):
if event_type not in self.subscribers:
self.subscribers[event_type] = []
self.subscribers[event_type].append(handler)
def publish(self, event_type, data):
if event_type in self.subscribers:
for handler in self.subscribers[event_type]:
handler(data)
event_bus = EventBus()
# 에이전트 등록
event_bus.subscribe('code_analyzed', coding_agent.on_analysis)
event_bus.subscribe('code_generated', testing_agent.on_code)
event_bus.subscribe('test_failed', coding_agent.on_failure)
4. 통신 프로토콜 설계
메시지 형식 표준화:
from enum import Enum
from dataclasses import dataclass
from typing import Any, Optional
import uuid
from datetime import datetime
class MessageType(Enum):
TASK_REQUEST = "task_request"
TASK_RESPONSE = "task_response"
EVENT = "event"
ERROR = "error"
HEARTBEAT = "heartbeat"
class Priority(Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
CRITICAL = 4
@dataclass
class Message:
"""표준 메시지 형식"""
id: str
type: MessageType
sender: str
receiver: Optional[str] # None = broadcast
payload: Any
priority: Priority
correlation_id: Optional[str] # 요청-응답 매칭
timestamp: datetime
metadata: dict
@classmethod
def create_request(cls, sender, receiver, payload, priority=Priority.MEDIUM):
return cls(
id=str(uuid.uuid4()),
type=MessageType.TASK_REQUEST,
sender=sender,
receiver=receiver,
payload=payload,
priority=priority,
correlation_id=str(uuid.uuid4()),
timestamp=datetime.now(),
metadata={}
)
@classmethod
def create_response(cls, request_msg, sender, payload):
return cls(
id=str(uuid.uuid4()),
type=MessageType.TASK_RESPONSE,
sender=sender,
receiver=request_msg.sender,
payload=payload,
priority=request_msg.priority,
correlation_id=request_msg.correlation_id,
timestamp=datetime.now(),
metadata={"request_id": request_msg.id}
)
에러 처리 프로토콜:
from enum import Enum
class ErrorCode(Enum):
INVALID_INPUT = "INVALID_INPUT"
TOOL_FAILURE = "TOOL_FAILURE"
TIMEOUT = "TIMEOUT"
RATE_LIMIT = "RATE_LIMIT"
MODEL_ERROR = "MODEL_ERROR"
UNKNOWN = "UNKNOWN"
@dataclass
class ErrorMessage:
code: ErrorCode
message: str
details: dict
recoverable: bool
retry_after: Optional[int] # seconds
# 사용
try:
result = agent.execute(task)
except ToolException as e:
error = ErrorMessage(
code=ErrorCode.TOOL_FAILURE,
message=str(e),
details={"tool": e.tool_name, "args": e.args},
recoverable=True,
retry_after=5
)
message_bus.publish('error', error)
재시도 전략:
import time
from functools import wraps
def retry_with_backoff(max_retries=3, base_delay=1, max_delay=60):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except RecoverableError as e:
if attempt == max_retries - 1:
raise
delay = min(base_delay * (2 ** attempt), max_delay)
logger.warning(
f"Attempt {attempt + 1} failed, retrying in {delay}s",
error=str(e)
)
time.sleep(delay)
return wrapper
return decorator
class Agent:
@retry_with_backoff(max_retries=3)
def execute(self, task):
# 실행 로직
pass
5. 실전 체크리스트
프로덕션 AI 에이전트 시스템을 설계할 때 확인해야 할 항목들:
아키텍처 설계:
- 각 에이전트의 경계가 명확한가?
- 에이전트 간 책임이 중복되지 않는가?
- 에이전트 간 의존성이 최소화되어 있는가?
- 순환 의존성이 없는가?
- 확장 가능한 구조인가?
통신 설계:
- 메시지 형식이 표준화되어 있는가?
- 에러 처리 프로토콜이 정의되어 있는가?
- 재시도 전략이 있는가?
- 타임아웃이 설정되어 있는가?
- 메시지 순서가 보장되어야 하는가?
신뢰성:
- 단일 장애점이 없는가?
- 에이전트 실패 시 격리되는가?
- 부분 실패를 처리할 수 있는가?
- 롤백 메커니즘이 있는가?
- 서킷 브레이커가 구현되어 있는가?
관찰 가능성:
- 모든 에이전트가 로그를 남기는가?
- 메트릭이 수집되는가?
- 분산 트레이싱이 가능한가?
- 대시보드가 있는가?
- 알림 시스템이 있는가?
성능:
- 병목 지점이 파악되었는가?
- 병렬 실행이 가능한 부분이 병렬화되었는가?
- 캐싱 전략이 있는가?
- 토큰 사용량이 최적화되었는가?
- 비용 예측이 가능한가?
보안:
- 에이전트 간 인증이 있는가?
- 민감한 데이터가 암호화되는가?
- 권한 관리가 되는가?
- 감사 로그가 남는가?
- 레이트 리미팅이 있는가?
결론: 온고지신(溫故知新)
새로운 기술, 오래된 지혜
AI 에이전트 시스템의 진화를 추적하면서, 우리는 흥미로운 패턴을 발견했다. 기술은 새롭지만, 우리가 마주한 문제와 해결책은 놀라울 정도로 익숙하다.
2022년: "AI가 모든 것을 바꿀 것이다!" 2023년: "프롬프트 엔지니어링이 새로운 프로그래밍이다!" 2024년: "자율 에이전트가 개발자를 대체할 것이다!" 2025-2026년: "아, 결국 소프트웨어 아키텍처가 필요하구나..."
이것은 실패가 아니다. 성숙의 과정이다.
모든 혁신적 기술은 이런 과정을 거친다:
- 과대광고 (Hype): 무한한 가능성에 대한 환상
- 환멸 (Disillusionment): 현실의 한계 직면
- 통합 (Integration): 기존 지식과의 융합
- 성숙 (Maturity): 실용적 적용
AI 에이전트 시스템은 지금 3단계에서 4단계로 넘어가고 있다.
핵심 교훈
1. 기본 원칙은 변하지 않는다
50년 전 Edsger Dijkstra가 말한 "관심사의 분리(Separation of Concerns)"는 여전히 유효하다. AI 에이전트든 마이크로서비스든, 복잡한 시스템을 관리 가능한 부분으로 나누는 것이 핵심이다.
2. 추상화는 강력하다
"에이전트"라는 추상화는 강력하다. 하지만 그 아래에는 여전히:
- 입력과 출력
- 상태 관리
- 에러 처리
- 통신 프로토콜
이런 기본 요소들이 있다. 추상화를 이해하되, 그 아래를 무시하지 말아야 한다.
3. 복잡도는 사라지지 않는다
AI가 많은 것을 자동화하지만, 시스템 복잡도는 사라지지 않는다. 단지 다른 곳으로 이동할 뿐이다:
- 코드 복잡도 → 프롬프트 복잡도
- 함수 호출 → 에이전트 통신
- 디버깅 → 에이전트 추적
복잡도를 관리하는 방법을 알아야 한다.
4. 검증된 패턴을 활용하라
바퀴를 재발명하지 말자. 소프트웨어 공학이 50년간 축적한 지혜를 활용하자:
- SOLID 원칙
- 디자인 패턴
- 아키텍처 패턴
- 모니터링과 관찰 가능성
이것들은 AI 시스템에도 그대로 적용된다.
5. 도구는 바뀌어도 원칙은 남는다
오늘은 GPT-4, Claude, Gemini다. 내일은 다른 모델일 것이다. 하지만:
- 명확한 책임 분리
- 느슨한 결합
- 높은 응집도
- 명시적 인터페이스
이런 원칙들은 변하지 않는다.
다음 단계는 무엇인가?
AI 에이전트 시스템의 미래를 예측하는 것은 어렵다. 하지만 소프트웨어 공학의 역사를 보면 힌트를 얻을 수 있다.
단기 (2026-2027):
- 표준화: MCP 같은 프로토콜의 광범위한 채택
- 도구 성숙: 더 나은 디버깅, 모니터링 도구
- 베스트 프랙티스: 검증된 패턴의 문서화
- 프레임워크 통합: 주요 프레임워크들의 수렴
중기 (2027-2029):
- 플랫폼화: 에이전트 오케스트레이션 플랫폼 (Kubernetes for Agents?)
- 서비스 메시: 에이전트 간 통신 인프라
- 관찰 가능성: 분산 트레이싱, 메트릭, 로깅의 통합
- 거버넌스: 에이전트 행동 제어 및 감사
장기 (2029+):
- 자가 최적화: 에이전트 시스템이 스스로 성능 최적화
- 자가 치유: 장애 자동 감지 및 복구
- 적응형 아키텍처: 워크로드에 따라 동적으로 재구성
- 메타 에이전트: 에이전트 시스템을 설계하는 에이전트
하지만 이 모든 것의 기반은 여전히:
- 명확한 아키텍처
- 표준화된 통신
- 관찰 가능성
- 신뢰성
즉, 우리가 이미 알고 있는 것들이다.
실천 가이드
지금 당장 할 수 있는 것:
-
기존 시스템 리팩토링:
- 단일 에이전트를 역할별로 분리
- 명시적 인터페이스 정의
- 통신 프로토콜 표준화
-
관찰 가능성 추가:
- 구조화된 로깅 도입
- 메트릭 수집 시작
- 트레이싱 구현
-
문서화:
- 각 에이전트의 역할과 책임 명시
- 통신 프로토콜 문서화
- 아키텍처 다이어그램 작성
-
테스트:
- 에이전트 단위 테스트
- 통합 테스트
- 카오스 엔지니어링 (장애 주입 테스트)
-
학습:
- 소프트웨어 아키텍처 책 읽기
- 마이크로서비스 패턴 학습
- 분산 시스템 이해
마치며
AI 에이전트 시스템의 진화는 우리에게 중요한 교훈을 준다:
혁신은 과거를 부정하는 것이 아니라, 과거의 지혜 위에 새로운 것을 쌓는 것이다.
우리는 새로운 도구를 가졌다. LLM은 강력하고, 에이전트는 유용하다. 하지만 이것들을 효과적으로 사용하려면, 우리는 여전히:
- 좋은 아키텍처가 필요하다
- 명확한 설계가 필요하다
- 체계적인 테스트가 필요하다
- 지속적인 모니터링이 필요하다
즉, 좋은 소프트웨어 엔지니어링이 필요하다.
AI 시대에도, 아니 AI 시대이기에, 소프트웨어 공학의 기본 원칙은 더욱 중요해졌다.
온고지신(溫故知新) - 옛것을 익히고 그것을 미루어서 새것을 안다.
이것이 바로 AI 에이전트 아키텍처의 진화가 우리에게 주는 메시지다.
참고 자료
논문 및 연구
- Yao et al. (2023). "ReAct: Synergizing Reasoning and Acting in Language Models"
- Wang et al. (2024). "AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation"
- LangChain Team (2024). "LangGraph: Building Stateful Multi-Agent Applications"
프레임워크 문서
- CrewAI Documentation: https://docs.crewai.com
- LangGraph Documentation: https://langchain-ai.github.io/langgraph/
- AutoGen Documentation: https://microsoft.github.io/autogen/
- oh-my-opencode: https://github.com/code-yeongyu/oh-my-opencode
블로그 및 아티클
- Andrew Ng (2024). "Agentic Design Patterns"
- Anthropic (2026). "Building Effective Agents"
- Anthropic (2026). "Claude Opus 4.6 and Agent Teams"
소프트웨어 아키텍처 고전
- Martin Fowler. "Patterns of Enterprise Application Architecture"
- Eric Evans. "Domain-Driven Design"
- Sam Newman. "Building Microservices"
- Gregor Hohpe. "Enterprise Integration Patterns"
온라인 리소스
- Model Context Protocol (MCP): https://modelcontextprotocol.io
- OpenAI Function Calling: https://platform.openai.com/docs/guides/function-calling
- LangChain Blog: https://blog.langchain.dev
저자 노트: 이 글은 2026년 2월 현재의 AI 에이전트 생태계를 기반으로 작성되었습니다. 기술은 빠르게 발전하지만, 여기서 다룬 아키텍처 원칙들은 시간이 지나도 유효할 것입니다.
라이선스: 이 글은 CC BY-SA 4.0 라이선스 하에 배포됩니다. 자유롭게 공유하고 수정할 수 있으나, 출처를 밝혀주시고 같은 라이선스로 배포해주세요.
피드백: 의견이나 제안이 있으시면 언제든 연락 주세요. 함께 배우고 성장합시다.
