Prologue: 만능 천재 모델은 없다
사학과 학부 시절 팀 프로젝트를 할 때 가장 피해야 할 최악의 조장은 모든 일을 혼자 다 하겠다고 나서는 사람이었습니다. 아무리 똑똑한 사람이라도 리서치, 발표 자료 작성, 대본 리딩, 피드백 수렴을 혼자 다 하려고 하면 결국 과부하가 걸려 결과물의 질이 뚝 떨어지기 마련입니다.
최근 거대 언어 모델(LLM)을 활용해 자동화 개발 도구를 만들면서 이와 완전히 같은 상황을 마주쳤습니다.
처음에는 강력한 하나의 모델(예: Claude 3.5 Sonnet 이나 GPT-4o)에 모든 책임을 부여하는 '단일 거대 프롬프트' 방식을 썼습니다. 프롬프트 안에 "너는 기획자이자, 코드 작성자이자, 보안 검토관이자, 테스트 엔지니어다"라고 지시했죠.
하지만 작업이 조금만 길어지고 복잡해지면 모델은 처참하게 무너졌습니다:
- 규칙 유실: 프롬프트 뒤쪽 내용을 작성하다 보면 앞쪽에서 정한 보안 코딩 규칙을 까먹고 누락시켰습니다.
- 무한 루프: 코드 에러가 나면 같은 실수를 반복하며 똑같은 코드를 수정 없이 계속 다시 뱉는 루프에 빠졌습니다.
- 컨텍스트 팽창: 대화 히스토리가 길어지면서 입력 비용이 폭증하고 추론 속도는 기어갔습니다.
"한 번에 한 놈만 패게 하자. 전문 분야를 나눈 작고 날렵한 에이전트 팀을 구성하고, 그들을 조율하는 시스템을 설계하면 어떨까?"
이 깨달음을 기점으로 저는 단일 모델 호출 방식에서 Multi-Agent Orchestration(다중 에이전트 오케스트레이션) 아키텍처로 방향을 완전히 바꾸게 되었습니다.
Concept: 전문적 역할 분담과 협업 아키텍처
다중 에이전트 오케스트레이션의 핵심은 **"복잡한 문제를 작게 쪼개어 각각 전문성을 가진 하위 에이전트(Agent)들에게 위임하고, 이들의 실행 흐름과 대화를 체계적으로 조율(Orchestrate)하는 시스템 디자인"**입니다.
이 구조는 회사 조직도와 매우 유사합니다:
- 기획자 에이전트 (Planner): 사용자의 추상적인 입력을 분석해 세부 구현 태스크 리스트를 작성합니다.
- 코더 에이전트 (Coder): 플래너가 나눈 태스크를 받아 실제로 파일을 생성하고 코드를 작성합니다.
- 리서처 에이전트 (Researcher): 코드 작성 중 API 스펙이나 외부 문서를 읽어야 할 때 웹 서치나 파일 검색을 대신 수행합니다.
- 리뷰어 에이전트 (Verifier/Linter): 코더가 작성한 코드를 읽고 린트 에러, 논리적 결함, 보안 취약점을 검증하여 코더에게 반려하거나 패스시킵니다.
이렇게 역할을 나누면 각 에이전트는 **자신의 프롬프트(System Prompt)와 꼭 필요한 최소한의 도구(Tool)**만 쥐고 가볍게 작동하므로 컨텍스트 낭비가 없고 지시 사항을 엄격히 준수합니다.
Deep Dive: 3대 오케스트레이션 패턴과 예외 제어
다중 에이전트 시스템을 설계할 때 가장 많이 사용되는 세 가지 흐름 조율 패턴과 실무적 설계 고려 사항을 정리했습니다.
1. 라우터/디스패처 패턴 (Router/Dispatcher)
가장 단순한 구조로, 중앙의 '컨트롤러' 에이전트가 유저의 입력을 보고 어떤 하위 에이전트에게 전담시킬지 결정을 내리는 허브 앤 스포크(Hub-and-Spoke) 구조입니다. 단순 문의 응대나 조건별 태스크 분기에 유용합니다.
2. 순차적 파이프라인 패턴 (Sequential Pipeline)
A 에이전트의 출력이 B 에이전트의 입력이 되고, 다시 C 에이전트로 순차 전달되는 체인 형태입니다. "리서치 보고서 초안 작성 -> 번역 -> 마크다운 변환 -> DB 저장" 같은 고정 워크플로우에 적합합니다.
3. 상태 머신 / 그래프 패턴 (State Machine / Graph)
가장 정교하고 복잡한 프로덕션 수준의 패턴입니다. 에이전트의 협업 과정을 **상태(State)와 전이(Transition)**로 이루어진 그래프로 정의합니다.
예를 들어, 코더 에이전트가 코드를 작성하면(State: Code), 리뷰어 에이전트가 검증을 수행하고(State: Review), 통과하면 완료(State: Complete), 실패하면 다시 코더에게 수정 지시와 함께 돌려보내는(State: Code) 순환 구조를 만들 수 있습니다.
graph TD
User([유저 요청]) --> Planner[1. 플래너 에이전트]
Planner --> Coder[2. 코더 에이전트]
Coder --> Verifier[3. 리뷰어 에이전트]
Verifier -- "반려 (에러 발견)" --> Coder
Verifier -- "승인" --> Complete([태스크 완료])
4. 무한 루프 차단 (Infinite Loop Prevention)
에이전트가 서로 질문과 답변을 끊임없이 주고받으며 토큰을 무한정 소비하는 루프는 가장 경계해야 할 버그입니다. 실무적으로는 최대 반복 횟수(Max Iterations) 제한을 걸어두고, 상태 머신의 전이 카운트가 임계치(예: 10회)를 넘어가면 시스템을 강제 정지시키고 인간 개발자에게 중재를 요청하도록 탈출구를 만들어야 합니다.
Application: Coder-Reviewer 협업 에이전트 구현
내 프로젝트 내에서 코드 생성 및 자동 검토를 수행하는 2인 에이전트 협업 시스템을 가볍게 구현해 보았습니다.
type AgentState = "PLAN" | "CODE" | "REVIEW" | "FINISH";
interface TaskContext {
state: AgentState;
instruction: string;
code: string;
feedback: string;
iterationCount: number;
}
// 1. 코더 에이전트 호출
async function runCoderAgent(context: TaskContext): Promise<string> {
console.log("-> [Coder] 코드 작성 중...");
const prompt = `당신은 코더입니다. 아래 요구사항에 맞게 깔끔한 TypeScript 코드를 작성하세요.
만약 리뷰어의 피드백이 있다면 적극 반영하여 코드를 수정하세요.
요구사항: ${context.instruction}
피드백: ${context.feedback}
현재 코드: ${context.code}`;
// LLM API 호출 모킹 (실제 환경에서는 SDK 활용)
const code = await callLLM(prompt);
return code;
}
// 2. 리뷰어 에이전트 호출
async function runReviewerAgent(context: TaskContext): Promise<{ passed: boolean; feedback: string }> {
console.log("-> [Reviewer] 코드 검토 중...");
const prompt = `당신은 선임 개발자이자 코드 리뷰어입니다.
아래 코드가 요구사항을 충족하는지 검증하고 버그를 찾아내세요.
통과했다면 반드시 'PASSED'로 대답을 시작하고, 실패했다면 개선 요구사항 피드백만 자세히 남기세요.
요구사항: ${context.instruction}
코드: ${context.code}`;
const response = await callLLM(prompt);
const passed = response.startsWith("PASSED");
return { passed, feedback: response };
}
// 3. 오케스트레이터 루프 (상태 머신 조율)
async function orchestrateCodeGeneration(userRequest: string) {
const context: TaskContext = {
state: "CODE",
instruction: userRequest,
code: "",
feedback: "",
iterationCount: 0
};
const MAX_LIMIT = 5;
while (context.state !== "FINISH") {
// 무한 루프 방지 안전장치
if (context.iterationCount >= MAX_LIMIT) {
console.warn("!! 최대 반복 횟수를 초과했습니다. 중지합니다.");
context.state = "FINISH";
break;
}
switch (context.state) {
case "CODE":
context.code = await runCoderAgent(context);
context.state = "REVIEW";
break;
case "REVIEW":
const result = await runReviewerAgent(context);
if (result.passed) {
console.log("✓ 검토 완료! 코드가 통과되었습니다.");
context.state = "FINISH";
} else {
console.log(`✗ 반려됨. 피드백 반영 후 재시도합니다. (시도: ${context.iterationCount + 1})`);
context.feedback = result.feedback;
context.state = "CODE";
context.iterationCount++;
}
break;
}
}
return context.code;
}
이 미니 오케스트레이터를 돌려본 결과는 흥미로웠습니다.
- 단순 LLM 단독 호출 시에는 종종 타입 스펙 오류나 세세한 린트 버그가 포함된 코드가 생성되어 개발자가 손수 수정해야 했습니다.
- 2인 협업 구조를 돌리자, 리뷰어가 잡아낸 타입 불일치 에러를 코더가 받아서 조용히 코드를 수정한 뒤 완성본을 개발자에게 전달해 주었습니다.
- 개발자는 오직 최종적으로 '인증 마크가 찍힌' 안정적인 결과물만 손쉽게 받아볼 수 있었습니다.
Summary: 프롬프트 엔지니어링에서 시스템 아키텍처로
인공지능을 다루는 기술의 초기 트렌드가 "어떻게 단일 프롬프트를 멋지게 쥐어짜서 LLM의 잠재력을 이끌어낼 것인가"라는 프롬프트 비술(Alchemy)에 머물렀다면, 지금의 기술은 **"어떻게 검증 가능한 소프트웨어 엔지니어링 컴포넌트로 에이전트들을 구조화할 것인가"**로 확실히 이행하고 있습니다.
다중 에이전트 오케스트레이션은 복잡한 도메인 지식을 코딩으로 풀어나가는 가장 확실한 열쇠입니다.
문제를 나누어 정복(Divide and Conquer)하고, 컴포넌트 간의 대화 인터페이스를 규격화하며, 상태 머신을 통해 비즈니스 프로세스를 통제하는 구조는 지난 수십 년 동안 우리가 다져온 소프트웨어 공학의 대원칙과 완벽하게 맞닿아 있습니다.
만능 천재 한 명을 길러내려고 애쓰기보다 규칙적이고 헌신적으로 협업하는 훌륭한 시스템 팀을 빌드하는 것, 그것이 바로 AI 시대에 우리 개발자가 추구해야 할 새로운 아키텍처 패러다임임을 깊이 배웠습니다.