🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Multi-Agent Research Assistant 실습 가이드 - 슬라이드 1/8
A

AI Generated

2025. 12. 27. · 0 Views

Multi-Agent Research Assistant 실습 가이드

여러 AI 에이전트가 협력하여 연구를 수행하는 시스템을 구축하는 방법을 배웁니다. Supervisor 패턴을 중심으로 Search, Analysis, Synthesis 에이전트를 설계하고 구현하는 과정을 단계별로 살펴봅니다.


목차

  1. Supervisor와 3 Agents 아키텍처 설계
  2. Search Agent 구현
  3. Analysis Agent 구현
  4. Synthesis Agent 구현
  5. Supervisor와 컨텍스트 격리
  6. 에이전트 간 통신 프로토콜
  7. 실제 연구 질문으로 시스템 테스트

1. Supervisor와 3 Agents 아키텍처 설계

어느 날 김개발 씨는 회사에서 새로운 프로젝트를 맡게 되었습니다. "AI를 활용해서 자동으로 리서치를 수행하는 시스템을 만들어 주세요." 처음에는 하나의 거대한 AI 모델로 모든 것을 처리하려 했지만, 선배 박시니어 씨가 다가와 조언했습니다.

"복잡한 작업은 여러 전문가에게 나눠서 맡기는 게 낫지 않겠어요?"

Multi-Agent 아키텍처란 하나의 복잡한 작업을 여러 전문화된 에이전트에게 분담시키는 설계 방식입니다. 마치 병원에서 접수 담당자, 간호사, 전문의가 각자의 역할을 수행하며 환자를 치료하는 것과 같습니다.

Supervisor가 전체 작업을 조율하고, 각 Agent는 자신의 전문 영역에만 집중합니다.

다음 코드를 살펴봅시다.

// Multi-Agent 시스템의 기본 구조를 정의합니다
import Anthropic from "@anthropic-ai/sdk";

// 각 에이전트의 역할을 명확히 정의합니다
interface AgentConfig {
  name: string;
  role: "search" | "analysis" | "synthesis";
  systemPrompt: string;
}

// Supervisor가 관리할 에이전트 목록입니다
const agents: AgentConfig[] = [
  { name: "SearchAgent", role: "search", systemPrompt: "문서를 검색하고 관련 정보를 수집합니다." },
  { name: "AnalysisAgent", role: "analysis", systemPrompt: "수집된 정보를 분석합니다." },
  { name: "SynthesisAgent", role: "synthesis", systemPrompt: "분석 결과를 종합하여 최종 보고서를 작성합니다." }
];

// Supervisor 클래스가 전체 워크플로우를 조율합니다
class ResearchSupervisor {
  private client: Anthropic;
  private agents: Map<string, AgentConfig>;

  constructor() {
    this.client = new Anthropic();
    this.agents = new Map(agents.map(a => [a.name, a]));
  }
}

김개발 씨는 입사 2년 차 개발자입니다. 이번에 맡은 프로젝트는 회사의 방대한 기술 문서에서 필요한 정보를 자동으로 찾아 분석하고, 요약 보고서까지 만들어주는 AI 시스템이었습니다.

처음에는 단순하게 생각했습니다. "ChatGPT같은 모델 하나로 다 처리하면 되지 않을까?" 하지만 실제로 구현을 시작하자 문제가 생겼습니다.

하나의 프롬프트에 모든 지시를 넣으니 응답이 일관되지 않았습니다. 어떤 때는 검색을 너무 많이 하고, 어떤 때는 분석이 너무 얕았습니다.

무엇보다 프롬프트가 너무 길어져서 수정하기도 어려웠습니다. 그때 박시니어 씨가 화이트보드 앞으로 김개발 씨를 불렀습니다.

"한번 생각해 봐요. 실제 연구소에서는 어떻게 일하죠?" 박시니어 씨가 설명을 이어갔습니다.

"연구소에는 자료 조사 담당자가 있고, 데이터 분석가가 있고, 최종 보고서를 쓰는 연구원이 있어요. 그리고 이들을 총괄하는 프로젝트 매니저가 있죠.

AI 시스템도 이렇게 설계하면 어떨까요?" 이것이 바로 Multi-Agent 아키텍처의 핵심 아이디어입니다. 하나의 만능 AI 대신, 각자 전문성을 가진 여러 AI 에이전트를 두고 이들이 협력하게 만드는 것입니다.

이 아키텍처에서 Supervisor는 오케스트라의 지휘자와 같은 역할을 합니다. 어떤 연주자가 언제 연주할지 결정하고, 전체적인 흐름을 조율합니다.

Supervisor는 사용자의 질문을 받아 어떤 에이전트가 먼저 일해야 하는지 판단하고, 에이전트 간의 정보 전달을 담당합니다. Search Agent는 도서관 사서와 같습니다.

필요한 정보가 어디에 있는지 알고, 관련 문서를 빠르게 찾아옵니다. 검색 쿼리를 최적화하고, 가장 관련성 높은 문서를 선별하는 것이 이 에이전트의 전문 영역입니다.

Analysis Agent는 데이터 과학자와 같습니다. Search Agent가 가져온 원시 데이터를 받아 패턴을 찾고, 핵심 인사이트를 추출합니다.

단순히 정보를 나열하는 것이 아니라, 의미 있는 해석을 덧붙이는 것이 이 에이전트의 역할입니다. Synthesis Agent는 테크니컬 라이터와 같습니다.

Analysis Agent의 분석 결과를 받아 사람이 읽기 쉬운 보고서로 정리합니다. 복잡한 기술적 내용을 명확하고 구조화된 문서로 변환하는 것이 목표입니다.

위 코드를 보면 각 에이전트의 설정이 명확하게 분리되어 있습니다. 이렇게 하면 나중에 특정 에이전트만 수정하거나 교체하기가 훨씬 쉬워집니다.

예를 들어 검색 성능을 개선하고 싶다면 SearchAgent의 프롬프트만 수정하면 됩니다. 김개발 씨는 고개를 끄덕였습니다.

"아, 그래서 역할을 나누는 거군요. 각자 잘하는 일에만 집중하면 전체 품질도 올라가겠네요!"

실전 팁

💡 - 에이전트의 역할은 서로 겹치지 않도록 명확하게 정의하세요

  • Supervisor에게 너무 많은 로직을 넣지 말고, 조율 역할에만 집중시키세요
  • 각 에이전트의 systemPrompt는 짧고 명확하게 작성하세요

2. Search Agent 구현

아키텍처 설계를 마친 김개발 씨는 첫 번째 에이전트 구현에 착수했습니다. "검색부터 시작해볼까요?" 박시니어 씨가 말했습니다.

"좋은 연구는 좋은 자료 수집에서 시작하니까요. Search Agent가 제대로 동작하지 않으면 나머지 에이전트들도 제 역할을 할 수 없어요."

Search Agent는 사용자의 질문을 분석하여 적절한 검색 쿼리를 생성하고, 관련 문서를 수집하는 역할을 담당합니다. 마치 숙련된 도서관 사서가 막연한 질문도 핵심 키워드로 변환하여 필요한 책을 찾아주는 것처럼, Search Agent는 자연어 질문을 효과적인 검색 전략으로 바꿔줍니다.

다음 코드를 살펴봅시다.

// Search Agent 구현 - 문서 검색 전문 에이전트
import Anthropic from "@anthropic-ai/sdk";

class SearchAgent {
  private client: Anthropic;
  private conversationHistory: Anthropic.MessageParam[] = [];

  constructor() {
    this.client = new Anthropic();
  }

  // 검색 도구 정의 - 실제 검색 기능을 수행합니다
  private tools: Anthropic.Tool[] = [
    {
      name: "search_documents",
      description: "주어진 쿼리로 문서를 검색합니다",
      input_schema: {
        type: "object" as const,
        properties: {
          query: { type: "string", description: "검색 쿼리" },
          maxResults: { type: "number", description: "최대 결과 수" }
        },
        required: ["query"]
      }
    }
  ];

  // 검색 실행 메서드
  async search(question: string): Promise<string[]> {
    const systemPrompt = `당신은 전문 문서 검색 에이전트입니다.
    사용자 질문을 분석하여 최적의 검색 쿼리를 생성하고,
    search_documents 도구를 사용해 관련 문서를 찾아주세요.`;

    const response = await this.client.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 1024,
      system: systemPrompt,
      tools: this.tools,
      messages: [{ role: "user", content: question }]
    });

    return this.processToolCalls(response);
  }
}

김개발 씨는 Search Agent 구현을 시작하면서 한 가지 고민에 빠졌습니다. "사용자가 '최신 AI 트렌드가 뭐야?'라고 물으면, 이걸 어떻게 검색 쿼리로 바꿔야 하지?" 박시니어 씨가 힌트를 주었습니다.

"도서관에 가서 사서에게 '요즘 뭐가 재밌어요?'라고 물으면 어떻게 될까요? 좋은 사서라면 몇 가지 질문을 더 하겠죠.

'어떤 분야요? 소설이요, 논픽션이요?' 하고 말이에요." 이것이 Search Agent의 첫 번째 역할입니다.

막연한 질문을 구체적인 검색 가능한 형태로 변환하는 것입니다. 위 코드에서 systemPrompt가 바로 이 역할을 지시합니다.

"사용자 질문을 분석하여 최적의 검색 쿼리를 생성하라"고 명확하게 알려주고 있습니다. tools 배열은 Search Agent가 사용할 수 있는 도구들을 정의합니다.

여기서는 search_documents라는 도구 하나만 정의했지만, 실제 프로젝트에서는 웹 검색, 데이터베이스 쿼리, 파일 시스템 탐색 등 여러 도구를 추가할 수 있습니다. 도구 정의를 자세히 살펴보면, input_schema가 있습니다.

이것은 도구에 어떤 입력값이 필요한지 정의합니다. AI 모델은 이 스키마를 보고 적절한 파라미터를 생성합니다.

query는 필수값이고, maxResults는 선택값입니다. search 메서드가 실제 검색 로직의 진입점입니다.

사용자의 질문을 받아 Claude API에 요청을 보내고, 응답을 처리합니다. 여기서 중요한 점은 tools 파라미터를 통해 사용 가능한 도구를 알려준다는 것입니다.

Claude는 질문을 분석한 뒤, 필요하다고 판단되면 tool_use 응답을 반환합니다. "이 질문에 답하려면 search_documents 도구를 이렇게 호출해야 해"라고 알려주는 것이죠.

그러면 우리 코드에서 실제로 그 도구를 실행하고 결과를 다시 Claude에게 전달합니다. 이런 방식을 Agentic Loop 또는 Tool Use Pattern이라고 부릅니다.

AI가 도구 사용을 결정하고, 우리가 도구를 실행하고, 결과를 다시 AI에게 주고, AI가 최종 응답을 생성하는 순환 구조입니다. 실무에서 Search Agent를 구현할 때 흔히 하는 실수가 있습니다.

검색 결과를 그대로 다음 에이전트에게 전달하는 것입니다. 하지만 검색 결과에는 노이즈가 많을 수 있습니다.

Search Agent가 1차 필터링을 수행하여 정말 관련 있는 문서만 전달하도록 해야 합니다. 김개발 씨가 구현을 마치고 테스트해보니, Search Agent가 생각보다 똑똑하게 동작했습니다.

"머신러닝 최적화 방법"이라는 질문에 대해 "machine learning optimization techniques", "ML model tuning best practices" 등 여러 변형 쿼리를 자동으로 생성했습니다.

실전 팁

💡 - 검색 쿼리는 여러 변형을 시도하도록 프롬프트를 설계하세요

  • 검색 결과의 관련성 점수를 함께 반환하면 다음 단계에서 우선순위를 정할 수 있습니다
  • 검색 실패 시 대체 전략을 준비해두세요

3. Analysis Agent 구현

Search Agent가 관련 문서들을 찾아왔습니다. 하지만 김개발 씨는 새로운 문제에 직면했습니다.

"문서가 10개나 있는데, 이걸 어떻게 정리하지?" 박시니어 씨가 웃으며 말했습니다. "이제 Analysis Agent의 차례예요.

원석을 보석으로 가공하는 단계죠."

Analysis Agent는 Search Agent가 수집한 원시 데이터를 받아 패턴을 찾고, 핵심 정보를 추출하며, 의미 있는 인사이트를 도출합니다. 마치 데이터 과학자가 방대한 데이터에서 숨겨진 트렌드를 발견하는 것처럼, 이 에이전트는 정보의 바다에서 진주를 건져올립니다.

다음 코드를 살펴봅시다.

// Analysis Agent 구현 - 정보 분석 전문 에이전트
class AnalysisAgent {
  private client: Anthropic;

  constructor() {
    this.client = new Anthropic();
  }

  // 분석 도구 정의
  private tools: Anthropic.Tool[] = [
    {
      name: "extract_key_points",
      description: "문서에서 핵심 포인트를 추출합니다",
      input_schema: {
        type: "object" as const,
        properties: {
          document: { type: "string", description: "분석할 문서 내용" },
          focusArea: { type: "string", description: "집중할 영역" }
        },
        required: ["document"]
      }
    },
    {
      name: "compare_sources",
      description: "여러 출처의 정보를 비교 분석합니다",
      input_schema: {
        type: "object" as const,
        properties: {
          sources: { type: "array", items: { type: "string" } }
        },
        required: ["sources"]
      }
    }
  ];

  // 분석 실행 메서드
  async analyze(documents: string[], question: string): Promise<AnalysisResult> {
    const systemPrompt = `당신은 전문 정보 분석 에이전트입니다.
    주어진 문서들을 분석하여:
    1. 핵심 포인트를 추출하고
    2. 정보 간의 관계를 파악하고
    3. 신뢰도를 평가하고
    4. 모순되는 정보가 있으면 식별해주세요.`;

    const documentsContext = documents.map((d, i) =>
      `[문서 ${i + 1}]\n${d}`).join("\n\n");

    const response = await this.client.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 2048,
      system: systemPrompt,
      tools: this.tools,
      messages: [{
        role: "user",
        content: `질문: ${question}\n\n수집된 문서:\n${documentsContext}`
      }]
    });

    return this.processAnalysis(response);
  }
}

Search Agent가 "클라우드 네이티브 아키텍처"에 관한 문서 10개를 수집했다고 가정해봅시다. 이 문서들에는 Kubernetes, Docker, 마이크로서비스, 서버리스 등 다양한 주제가 섞여 있습니다.

어떤 문서는 서로 다른 의견을 제시하기도 합니다. 김개발 씨가 처음에는 단순하게 생각했습니다.

"그냥 모든 문서를 이어 붙여서 요약하면 되지 않을까?" 하지만 테스트 결과는 실망스러웠습니다. 중요한 정보가 누락되거나, 서로 모순되는 내용이 그대로 섞여 나왔습니다.

박시니어 씨가 조언했습니다. "분석이란 단순히 요약하는 게 아니에요.

정보를 비교하고, 검증하고, 구조화하는 과정이죠." 위 코드에서 Analysis Agent는 두 가지 도구를 가지고 있습니다. extract_key_points는 개별 문서에서 핵심 내용을 뽑아내고, compare_sources는 여러 출처의 정보를 비교합니다.

이 두 도구를 조합하면 훨씬 풍부한 분석이 가능합니다. systemPrompt를 보면 네 가지 명확한 지시가 있습니다.

핵심 포인트 추출, 관계 파악, 신뢰도 평가, 모순 식별입니다. 이렇게 구체적인 지시를 주면 AI가 무엇을 해야 하는지 명확하게 이해합니다.

특히 신뢰도 평가는 실무에서 매우 중요합니다. 모든 정보가 동등하게 신뢰할 수 있는 것은 아닙니다.

공식 문서와 개인 블로그의 무게가 다르고, 최신 정보와 오래된 정보의 가치가 다릅니다. Analysis Agent가 이런 판단을 내려주면 Synthesis Agent가 더 나은 보고서를 작성할 수 있습니다.

모순 식별 기능도 핵심입니다. "A 문서에서는 마이크로서비스가 최선이라고 하고, B 문서에서는 모놀리식이 더 효율적이라고 합니다"라는 식으로 상충되는 정보를 명시해주면, 최종 보고서에서 이를 균형 있게 다룰 수 있습니다.

코드에서 documentsContext를 만드는 부분을 주목하세요. 각 문서에 번호를 붙여서 구분합니다.

이렇게 하면 AI가 "문서 3과 문서 7에서 상반된 견해가 발견됩니다"처럼 구체적으로 지칭할 수 있습니다. 실무에서 Analysis Agent를 설계할 때 중요한 점이 있습니다.

분석 결과의 형식을 미리 정의해두는 것입니다. 위 코드에서는 AnalysisResult 타입을 반환하도록 되어 있습니다.

이렇게 구조화된 출력을 요구하면 Synthesis Agent가 일관된 형식의 입력을 받을 수 있습니다. 김개발 씨가 테스트를 마치고 결과를 확인했습니다.

Analysis Agent가 자동으로 정보를 카테고리별로 분류하고, 신뢰도 순으로 정렬하고, 모순점까지 짚어주었습니다. "이제 보고서 쓰기가 훨씬 수월해지겠네요!"

실전 팁

💡 - 분석 결과는 구조화된 형식으로 반환하도록 설계하세요

  • 출처별 신뢰도 가중치를 부여하면 더 정확한 분석이 가능합니다
  • 모순되는 정보는 삭제하지 말고 명시적으로 표시하세요

4. Synthesis Agent 구현

분석이 끝났습니다. 이제 마지막 퍼즐 조각만 맞추면 됩니다.

김개발 씨가 물었습니다. "분석 결과를 어떻게 보고서로 만들죠?" 박시니어 씨가 답했습니다.

"좋은 보고서는 정보를 나열하는 게 아니에요. 스토리를 들려주는 거예요."

Synthesis Agent는 Analysis Agent의 분석 결과를 받아 사람이 읽기 쉬운 최종 보고서를 생성합니다. 마치 숙련된 테크니컬 라이터가 복잡한 기술 문서를 명확하고 설득력 있는 글로 변환하는 것처럼, 이 에이전트는 분석된 정보를 coherent한 내러티브로 엮어냅니다.

다음 코드를 살펴봅시다.

// Synthesis Agent 구현 - 정보 종합 전문 에이전트
interface AnalysisResult {
  keyPoints: string[];
  relationships: string[];
  contradictions: string[];
  confidenceScores: Record<string, number>;
}

class SynthesisAgent {
  private client: Anthropic;

  constructor() {
    this.client = new Anthropic();
  }

  // 종합 도구 정의
  private tools: Anthropic.Tool[] = [
    {
      name: "generate_report_section",
      description: "보고서의 특정 섹션을 생성합니다",
      input_schema: {
        type: "object" as const,
        properties: {
          sectionType: {
            type: "string",
            enum: ["summary", "findings", "recommendations", "caveats"]
          },
          content: { type: "string" }
        },
        required: ["sectionType", "content"]
      }
    },
    {
      name: "format_citation",
      description: "출처를 인용 형식으로 변환합니다",
      input_schema: {
        type: "object" as const,
        properties: {
          source: { type: "string" },
          quote: { type: "string" }
        },
        required: ["source", "quote"]
      }
    }
  ];

  // 보고서 생성 메서드
  async synthesize(analysis: AnalysisResult, question: string): Promise<string> {
    const systemPrompt = `당신은 전문 보고서 작성 에이전트입니다.
    분석 결과를 바탕으로 명확하고 구조화된 보고서를 작성하세요.

    보고서 구조:
    1. 요약 (Executive Summary)
    2. 주요 발견사항 (Key Findings)
    3. 권장사항 (Recommendations)
    4. 주의사항 및 한계 (Caveats)

    신뢰도가 높은 정보를 우선 배치하고,
    모순되는 정보는 균형 있게 다루세요.`;

    const response = await this.client.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 4096,
      system: systemPrompt,
      tools: this.tools,
      messages: [{
        role: "user",
        content: `원래 질문: ${question}\n\n분석 결과:\n${JSON.stringify(analysis, null, 2)}`
      }]
    });

    return this.formatFinalReport(response);
  }
}

Synthesis Agent는 연구 시스템의 마지막 단계를 담당합니다. 아무리 좋은 검색과 분석을 했어도, 최종 보고서가 읽기 어려우면 그 가치가 반감됩니다.

김개발 씨도 이 점을 잘 알고 있었습니다. "보고서를 쓴다는 건 결국 독자를 생각하는 거예요." 박시니어 씨가 말했습니다.

"바쁜 경영진은 요약만 읽을 거고, 개발자는 기술적 세부사항을 원할 거예요. 좋은 보고서는 모든 독자를 만족시켜야 해요." 위 코드의 systemPrompt에 정의된 보고서 구조를 보세요.

Executive Summary가 가장 먼저 옵니다. 이것은 의도적인 설계입니다.

바쁜 독자는 이 부분만 읽어도 핵심을 파악할 수 있어야 합니다. Key Findings는 분석에서 도출된 주요 발견사항입니다.

여기서 중요한 것은 신뢰도 순으로 정렬하는 것입니다. 가장 확실한 정보가 먼저 나오고, 불확실한 정보는 뒤에 배치됩니다.

Recommendations는 발견사항을 바탕으로 한 행동 제안입니다. "그래서 어떻게 해야 하는데?"라는 질문에 대한 답입니다.

분석만 하고 제안은 없는 보고서는 실무에서 쓸모가 없습니다. Caveats는 주의사항과 한계를 명시합니다.

정직한 보고서는 자신의 한계를 인정합니다. "이 결론은 2023년까지의 데이터에 기반합니다" 또는 "A 관점과 B 관점이 상충되며, 추가 검증이 필요합니다" 같은 문구가 여기에 들어갑니다.

generate_report_section 도구를 보면 sectionType이 enum으로 정의되어 있습니다. 이렇게 하면 AI가 정해진 구조 안에서 콘텐츠를 생성합니다.

자유도가 너무 높으면 일관성이 깨질 수 있기 때문입니다. format_citation 도구는 출처 표시를 위한 것입니다.

좋은 보고서는 주장의 근거를 명시합니다. "A라고 한다"보다 "공식 문서에 따르면 A라고 한다"가 더 신뢰감을 줍니다.

코드에서 max_tokens가 4096으로 설정된 것을 볼 수 있습니다. 다른 에이전트보다 높은 값입니다.

보고서는 길이가 길 수 있기 때문입니다. 하지만 무조건 길다고 좋은 것은 아닙니다.

systemPrompt에서 "명확하고 구조화된"이라고 강조한 이유입니다. 마지막으로 formatFinalReport 메서드가 응답을 최종 형태로 가공합니다.

마크다운 형식으로 변환하거나, HTML로 렌더링하거나, PDF로 내보내는 등의 후처리가 여기서 일어납니다. 김개발 씨가 첫 번째 보고서를 생성했을 때, 생각보다 깔끔한 결과물에 놀랐습니다.

목차가 있고, 핵심이 강조되어 있고, 출처도 명시되어 있었습니다.

실전 팁

💡 - 보고서 템플릿을 미리 정의해두면 일관된 품질을 유지할 수 있습니다

  • 독자의 기술 수준에 따라 용어 복잡도를 조절하는 옵션을 추가하세요
  • 보고서 길이 제한을 설정하여 핵심에 집중하도록 유도하세요

5. Supervisor와 컨텍스트 격리

세 에이전트가 모두 준비되었습니다. 하지만 김개발 씨에게 새로운 의문이 생겼습니다.

"이 에이전트들을 어떻게 연결하죠? 그리고 서로의 작업에 간섭하지 않게 하려면?" 박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다.

"여기서 Supervisor가 등장합니다."

Supervisor는 전체 워크플로우를 조율하는 중앙 컨트롤러입니다. 각 에이전트에게 작업을 할당하고, 에이전트 간 데이터 전달을 담당하며, 무엇보다 컨텍스트 격리를 보장합니다.

컨텍스트 격리란 각 에이전트가 자신의 역할에 필요한 정보만 접근할 수 있도록 하여, 의도치 않은 간섭을 방지하는 것입니다.

다음 코드를 살펴봅시다.

// Supervisor 구현 - 에이전트 조율 및 컨텍스트 격리
class ResearchSupervisor {
  private client: Anthropic;
  private searchAgent: SearchAgent;
  private analysisAgent: AnalysisAgent;
  private synthesisAgent: SynthesisAgent;

  // 각 에이전트의 컨텍스트를 독립적으로 관리합니다
  private agentContexts: Map<string, AgentContext> = new Map();

  constructor() {
    this.client = new Anthropic();
    this.searchAgent = new SearchAgent();
    this.analysisAgent = new AnalysisAgent();
    this.synthesisAgent = new SynthesisAgent();
    this.initializeContexts();
  }

  // 각 에이전트의 컨텍스트 초기화
  private initializeContexts(): void {
    this.agentContexts.set("search", {
      allowedData: ["query"],
      outputFormat: "documents"
    });
    this.agentContexts.set("analysis", {
      allowedData: ["documents", "query"],
      outputFormat: "analysisResult"
    });
    this.agentContexts.set("synthesis", {
      allowedData: ["analysisResult", "query"],
      outputFormat: "report"
    });
  }

  // 메인 연구 실행 메서드
  async conductResearch(question: string): Promise<string> {
    console.log("1단계: 문서 검색 시작...");
    const documents = await this.searchAgent.search(question);

    console.log("2단계: 정보 분석 시작...");
    // Search Agent의 전체 컨텍스트가 아닌, 결과물만 전달합니다
    const analysis = await this.analysisAgent.analyze(documents, question);

    console.log("3단계: 보고서 작성 시작...");
    // Analysis Agent의 결과물만 전달, 원본 문서는 전달하지 않습니다
    const report = await this.synthesisAgent.synthesize(analysis, question);

    return report;
  }
}

Supervisor는 Multi-Agent 시스템의 핵심입니다. 하지만 단순히 에이전트를 순서대로 호출하는 것만이 아닙니다.

진정한 가치는 컨텍스트 격리에 있습니다. 김개발 씨가 처음 시스템을 설계했을 때는 모든 정보를 모든 에이전트에게 전달했습니다.

Search Agent가 수집한 원본 문서를 Analysis Agent에게 주고, 그 원본 문서와 분석 결과를 Synthesis Agent에게 주었습니다. 결과는 어땠을까요?

Synthesis Agent가 이상하게 동작했습니다. 분석 결과를 참고해야 하는데, 원본 문서를 다시 분석하기 시작한 것입니다.

왜 그랬을까요? "AI에게 너무 많은 정보를 주면 혼란스러워해요." 박시니어 씨가 설명했습니다.

"Synthesis Agent는 보고서 작성에 집중해야 하는데, 원본 문서까지 보니까 자기도 분석을 하고 싶어진 거예요. 역할 혼란이죠." 이것이 바로 컨텍스트 격리가 필요한 이유입니다.

위 코드의 initializeContexts 메서드를 보세요. 각 에이전트가 접근할 수 있는 데이터를 명시적으로 제한하고 있습니다.

Search Agent는 query만 받습니다. 이전 검색 결과나 다른 에이전트의 분석 결과를 볼 필요가 없습니다.

그저 질문을 받고 관련 문서를 찾아오면 됩니다. Analysis Agent는 documentsquery를 받습니다.

Search Agent가 어떤 과정을 거쳐 이 문서를 찾았는지는 알 필요가 없습니다. 결과물인 문서만 있으면 분석할 수 있습니다.

Synthesis Agent는 analysisResultquery를 받습니다. 원본 문서는 전달하지 않습니다.

이미 Analysis Agent가 핵심을 추출했기 때문입니다. Synthesis Agent는 그 분석 결과만 가지고 보고서를 작성합니다.

conductResearch 메서드를 보면 이 흐름이 명확하게 드러납니다. 각 단계에서 이전 에이전트의 결과물만 다음 에이전트에게 전달합니다.

내부 상태나 중간 과정은 전달하지 않습니다. 이런 설계의 또 다른 장점은 디버깅입니다.

문제가 생겼을 때 어디서 잘못되었는지 찾기 쉽습니다. 각 단계의 입력과 출력이 명확하기 때문입니다.

"분석 결과는 좋은데 보고서가 이상하다"면 Synthesis Agent만 살펴보면 됩니다. 또한 확장성도 좋아집니다.

나중에 Search Agent를 더 강력한 버전으로 교체하고 싶다면, 출력 형식만 같으면 됩니다. 다른 에이전트는 수정할 필요가 없습니다.

실전 팁

💡 - 각 에이전트의 입출력 형식을 타입으로 명확히 정의하세요

  • 디버깅을 위해 각 단계의 중간 결과를 로깅하세요
  • 에이전트 간 의존성을 최소화하여 독립적으로 테스트할 수 있게 하세요

6. 에이전트 간 통신 프로토콜

시스템이 동작하기 시작했습니다. 하지만 김개발 씨는 한 가지 걱정이 생겼습니다.

"에이전트끼리 데이터를 주고받을 때 형식이 다르면 어떡하죠?" 박시니어 씨가 고개를 끄덕였습니다. "좋은 질문이에요.

그래서 우리에게 통신 프로토콜이 필요합니다."

통신 프로토콜은 에이전트 간 데이터 교환의 규약입니다. 마치 외교관들이 국제 회의에서 공식 언어를 사용하는 것처럼, 에이전트들도 미리 정의된 형식으로 소통해야 합니다.

이렇게 하면 에이전트를 교체하거나 추가할 때 호환성을 보장할 수 있습니다.

다음 코드를 살펴봅시다.

// 에이전트 간 통신 프로토콜 정의
interface AgentMessage<T> {
  messageId: string;
  timestamp: Date;
  sourceAgent: string;
  targetAgent: string;
  messageType: "request" | "response" | "error";
  payload: T;
  metadata: MessageMetadata;
}

interface MessageMetadata {
  processingTime?: number;
  tokenCount?: number;
  confidence?: number;
}

// 각 단계별 페이로드 타입 정의
interface SearchPayload {
  query: string;
  documents: DocumentResult[];
}

interface AnalysisPayload {
  keyPoints: string[];
  relationships: { from: string; to: string; type: string }[];
  contradictions: ContradictionInfo[];
  overallConfidence: number;
}

// 메시지 버스 구현
class AgentMessageBus {
  private messageLog: AgentMessage<unknown>[] = [];

  // 메시지 전송 및 로깅
  async send<T>(message: AgentMessage<T>): Promise<void> {
    message.timestamp = new Date();
    message.messageId = this.generateId();
    this.messageLog.push(message);
    console.log(`[${message.sourceAgent}] -> [${message.targetAgent}]: ${message.messageType}`);
  }

  // 디버깅용 메시지 히스토리 조회
  getHistory(): AgentMessage<unknown>[] {
    return [...this.messageLog];
  }

  private generateId(): string {
    return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

통신 프로토콜이 없으면 어떤 문제가 생길까요? 김개발 씨가 실제로 겪은 사례를 보겠습니다.

처음에 Search Agent는 검색 결과를 단순한 문자열 배열로 반환했습니다. Analysis Agent도 문자열 배열을 기대했기 때문에 문제가 없었습니다.

하지만 어느 날 Search Agent를 개선하면서, 각 문서에 메타데이터를 추가하기로 했습니다. 출처 URL, 작성일, 관련성 점수 같은 정보입니다.

Search Agent의 출력 형식을 바꾸자 Analysis Agent가 에러를 뱉기 시작했습니다. 문자열 배열을 기대했는데 객체 배열이 들어왔기 때문입니다.

이런 상황을 인터페이스 불일치라고 합니다. "처음부터 명확한 프로토콜을 정했더라면 이런 일이 없었을 거예요." 박시니어 씨가 말했습니다.

위 코드의 AgentMessage 인터페이스를 보세요. 모든 메시지는 이 형식을 따릅니다.

messageId로 각 메시지를 식별할 수 있고, timestamp로 언제 보냈는지 알 수 있습니다. sourceAgenttargetAgent는 발신자와 수신자를 명시합니다.

messageType은 세 가지입니다. request는 작업 요청, response는 작업 결과, error는 오류 발생을 알립니다.

이렇게 구분하면 에러 처리 로직을 체계적으로 작성할 수 있습니다. payload는 제네릭 타입입니다.

메시지의 실제 내용은 메시지 종류에 따라 다르기 때문입니다. Search Agent가 보내는 페이로드와 Analysis Agent가 보내는 페이로드의 구조가 다릅니다.

SearchPayloadAnalysisPayload를 보면 각 에이전트의 출력 형식이 명확하게 정의되어 있습니다. 이제 누군가 Search Agent를 수정하려면 이 인터페이스를 먼저 확인해야 합니다.

호환성을 깨뜨리는 변경은 즉시 타입 에러로 잡힙니다. MessageMetadata는 부가 정보를 담습니다.

processingTime으로 각 에이전트가 얼마나 걸렸는지 측정할 수 있고, tokenCount로 API 비용을 추적할 수 있습니다. confidence는 분석 결과의 신뢰도를 나타냅니다.

AgentMessageBus는 메시지 전달을 중앙에서 관리합니다. 모든 메시지가 이 버스를 통과하기 때문에 로깅이 자동으로 됩니다.

나중에 문제가 생기면 getHistory로 전체 통신 기록을 확인할 수 있습니다. 실무에서 이런 프로토콜은 API 문서화와도 연결됩니다.

새로운 개발자가 합류했을 때, 인터페이스 정의만 보면 시스템 구조를 빠르게 파악할 수 있습니다. 김개발 씨는 이제 프로토콜의 중요성을 깨달았습니다.

"처음에는 귀찮아 보였는데, 결국 미래의 저를 위한 투자였군요."

실전 팁

💡 - 프로토콜 변경 시 버전 관리를 도입하여 하위 호환성을 유지하세요

  • 메시지 로깅은 디버깅뿐 아니라 성능 분석에도 유용합니다
  • 에러 메시지 형식도 표준화하여 일관된 에러 처리가 가능하게 하세요

7. 실제 연구 질문으로 시스템 테스트

모든 준비가 끝났습니다. 이제 실전입니다.

김개발 씨가 떨리는 마음으로 첫 번째 연구 질문을 입력했습니다. "2024년 기준 가장 효과적인 RAG 최적화 기법은 무엇인가요?" 과연 시스템은 제대로 동작할까요?

실제 테스트는 시스템의 진가를 드러냅니다. 단위 테스트만으로는 발견하기 어려운 문제들이 통합 테스트에서 나타납니다.

여러 에이전트가 협력하는 과정에서 예상치 못한 상호작용이 발생할 수 있고, 실제 데이터는 테스트 데이터보다 훨씬 복잡합니다.

다음 코드를 살펴봅시다.

// 실제 연구 시스템 테스트 코드
async function testResearchSystem(): Promise<void> {
  const supervisor = new ResearchSupervisor();

  // 테스트 케이스 1: 기술 트렌드 분석
  const technicalQuestion = "2024년 기준 가장 효과적인 RAG 최적화 기법은?";

  console.log("=== 연구 시작 ===");
  console.log(`질문: ${technicalQuestion}\n`);

  try {
    const startTime = Date.now();
    const report = await supervisor.conductResearch(technicalQuestion);
    const elapsedTime = Date.now() - startTime;

    console.log("\n=== 최종 보고서 ===");
    console.log(report);
    console.log(`\n총 소요 시간: ${elapsedTime}ms`);

    // 보고서 품질 검증
    validateReport(report);
  } catch (error) {
    console.error("연구 중 오류 발생:", error);
    handleResearchError(error);
  }
}

// 보고서 품질 검증 함수
function validateReport(report: string): void {
  const requiredSections = ["요약", "발견사항", "권장사항", "주의사항"];
  const missingSections = requiredSections.filter(
    section => !report.includes(section)
  );

  if (missingSections.length > 0) {
    console.warn(`누락된 섹션: ${missingSections.join(", ")}`);
  } else {
    console.log("모든 필수 섹션이 포함되어 있습니다.");
  }
}

// 메인 실행
testResearchSystem();

드디어 모든 준비가 끝났습니다. 김개발 씨는 긴장된 마음으로 테스트 코드를 실행했습니다.

화면에 로그가 찍히기 시작했습니다. "1단계: 문서 검색 시작..." Search Agent가 활성화되었습니다.

RAG 최적화에 관련된 문서들을 찾기 시작합니다. 몇 초 후, 관련 문서 8개를 수집했다는 메시지가 나왔습니다.

"2단계: 정보 분석 시작..." Analysis Agent가 8개의 문서를 받아 분석을 시작했습니다. 핵심 포인트를 추출하고, 기법들 간의 관계를 파악하고, 상충되는 의견이 있는지 살펴봅니다.

"3단계: 보고서 작성 시작..." Synthesis Agent가 분석 결과를 받아 최종 보고서를 작성합니다. 몇 분 후, 최종 보고서가 화면에 출력되었습니다.

김개발 씨는 눈을 크게 뜨고 내용을 살펴봤습니다. 보고서는 깔끔하게 구조화되어 있었습니다.

요약 섹션에서는 핵심 결론을 세 문장으로 정리했습니다. 발견사항에서는 다양한 RAG 최적화 기법들을 나열하고, 각 기법의 장단점을 설명했습니다.

흥미로운 것은 주의사항 섹션이었습니다. "문서 A와 문서 C에서 청킹 크기에 대한 권장사항이 다릅니다.

문서 A는 512 토큰을, 문서 C는 1024 토큰을 권장합니다. 이는 사용 사례에 따라 최적값이 다를 수 있음을 시사합니다." Analysis Agent가 식별한 모순점이 보고서에 반영된 것입니다.

validateReport 함수는 보고서의 품질을 자동으로 검증합니다. 필수 섹션이 모두 포함되었는지 확인합니다.

이런 자동 검증이 없으면 Synthesis Agent가 가끔 섹션을 누락할 수 있습니다. 실전 테스트에서 발견된 몇 가지 문제도 있었습니다.

첫째, Search Agent가 너무 오래된 문서를 가져오는 경우가 있었습니다. 2020년 문서와 2024년 문서를 동등하게 취급했던 것입니다.

이 문제는 Search Agent의 프롬프트에 "최신 자료를 우선시하라"는 지시를 추가하여 해결했습니다. 둘째, 보고서가 너무 길어지는 경우가 있었습니다.

Synthesis Agent가 모든 정보를 다 담으려고 하니 핵심이 흐려졌습니다. 이 문제는 "보고서는 1500 단어를 넘지 않도록 하라"는 제약을 추가하여 해결했습니다.

셋째, 에러 처리가 미흡했습니다. Search Agent가 관련 문서를 찾지 못했을 때 시스템이 그냥 멈춰버렸습니다.

handleResearchError 함수를 추가하여 각 단계의 실패에 대응하도록 개선했습니다. 김개발 씨가 테스트를 마치고 박시니어 씨에게 결과를 보여드렸습니다.

"와, 생각보다 잘 동작하네요!" 박시니어 씨가 미소를 지었습니다. "하지만 이건 시작일 뿐이에요.

실제 사용자 피드백을 받으면서 계속 개선해나가야 해요."

실전 팁

💡 - 다양한 유형의 질문으로 테스트하여 시스템의 범용성을 검증하세요

  • 소요 시간과 API 비용을 추적하여 최적화 포인트를 찾으세요
  • 실패 케이스를 수집하고 분석하여 시스템을 지속적으로 개선하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#TypeScript#Multi-Agent#Claude-SDK#Supervisor-Pattern#AI-Research#AI Engineering

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

Production-Grade AI Agent System 완벽 가이드

연구 어시스턴트 기능을 갖춘 프로덕션급 AI 에이전트 시스템을 처음부터 끝까지 구축하는 방법을 다룹니다. 멀티 에이전트 아키텍처, 메모리 시스템, 평가 체계까지 실무에서 바로 적용할 수 있는 내용을 담았습니다.

LLM-as-a-Judge 평가 시스템 완벽 가이드

LLM을 활용하여 AI 에이전트의 출력을 체계적으로 평가하는 방법을 다룹니다. Direct Scoring, Pairwise Comparison, 편향 완화 기법부터 실제 대시보드 구현까지 실습 중심으로 설명합니다.

Agent-Optimized Tool Set 설계 실습 가이드

AI 에이전트가 효율적으로 사용할 수 있는 통합 도구 세트를 설계하는 방법을 배웁니다. FileSystem, Web, Database 도구를 MCP 프로토콜로 패키징하여 토큰 효율성과 명확성을 극대화하는 실전 기술을 다룹니다.

Knowledge Graph 메모리 시스템 완벽 가이드

AI 에이전트에 장기 기억 능력을 부여하는 Knowledge Graph 메모리 시스템을 구축합니다. Working, Short-term, Long-term 메모리 아키텍처부터 Neo4j를 활용한 Temporal Knowledge Graph 구현까지 실습합니다.

Context Manager 구현하기 실습 가이드

LLM 애플리케이션에서 컨텍스트를 효율적으로 관리하는 Context Manager를 직접 구현해봅니다. Progressive Disclosure와 Attention Budget 개념을 활용하여 토큰을 최적화하는 방법을 단계별로 학습합니다.