MCP 기반 멀티 에이전트 시스템의 통신 프로토콜 설계와 Rust 구현

최근 ‘ZeroClaw’ 프로젝트와 MCP(Model Context Protocol) 관련 작업을 진행하며, 단일 에이전트를 넘어선 **멀티 에이전트 시스템(Multi-Agent System)**의 필요성을 절실히 느꼈습니다. 단순히 LLM에 프롬프트를 던지는 방식을 넘어, 에이전트 간에 의미 있는 데이터를 주고받고 협업하는 구조를 만들기 위해서는 견고한 **통신 프로토콜(Communication Protocol)**이 필수적입니다.

이번 포스트에서는 MCP 아키텍처를 기반으로 한 멀티 에이전트 통신 설계 과정과, 이를 고성능 런타임인 ZeroClaw(Rust) 위에 구현하는 방법을 살펴보겠습니다.

1. 문제 정의: 왜 통신 프로토콜인가?

기존의 [blog-api-server]나 [Discord MCP] 구현에서는 주로 ‘요청-응답(Request-Response)’ 구조를 따랐습니다. 클라이언트가 요청을 보내면 서버가 처리하고 응답하는 단방향 흐름이 주를 이루었습니다. 하지만 멀티 에이전트 환경에서는 다음과 같은 이유로 이 구조만으로는 부족합니다.

  1. 비동기성(Asynchrony): 에이전트 A가 에이전트 B에게 작업을 위임하고, B가 완료될 때까지 A가 멈춰 있으면 시스템 전체의 처리량이 떨어집니다.
  2. 이벤트 기반 상호작용: 어떤 에이전트는 시스템의 상태 변화(예: 파일 생성, 로그 업데이트)를 감시하다가 특정 조건에서 다른 에이전트에게 신호를 보내야 합니다.
  3. 신뢰성: 네트워크 불안정이나 에이전트의 일시적 장애 상황에서 메시지가 유실되지 않아야 합니다.

2. 프로토콜 설계: 요청, 작업, 완료

이러한 문제를 해결하기 위해 우리는 Task-Based Messaging Protocol을 설계했습니다. 이 프로토콜은 MCP 표준 메시지 형식을 확장하여 사용하며, 크게 세 가지 주요 메시지 타입으로 구성됩니다.

  • TaskRequest: 작업의 시작. 에이전트 A가 B에게 특정 작업을 의임할 때 사용합니다.
  • TaskUpdate: 진행 상황 보고. 장기 실행 작업 시 중간 결과를 전달합니다.
  • TaskResult: 최종 결과. 성공 또는 실패 여부와 함께 데이터를 반환합니다.

3. Rust로 구현하는 ZeroClaw 통신 계층

ZeroClaw는 Rust의 강력한 타입 시스템과 비동기 런타임(Tokio)을 사용하여 이 프로토콜을 타입 안전하게 구현합니다. 아래는 간소화된 메시지 정의와 핸들러 구조입니다.

3.1. 메시지 정의 (JSON-RPC 기반)

MCP는 기본적으로 JSON-RPC 2.0을 기반으로 하므로, 이를 준수하면서 에이전트 간 통신에 필요한 메타데이터를 포함합니다.

// src/protocol/message.rs

use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentMessage {
    pub id: String,        // 메시지 고유 ID
    pub sender: String,    // 발신 에이전트 ID
    pub target: String,    // 수신 에이전트 ID
    pub timestamp: i64,    // 타임스탬프
    pub payload: Payload,  // 실제 데이터
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Payload {
    #[serde(rename = "task/request")]
    TaskRequest {
        task_id: Uuid,
        description: String,
        context: serde_json::Value,
    },
    
    #[serde(rename = "task/result")]
    TaskResult {
        task_id: Uuid,
        status: TaskStatus,
        data: Option<serde_json::Value>,
    },
    
    #[serde(rename = "system/ping")]
    SystemPing,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TaskStatus {
    Success,
    Failure(String), // 실패 사유
    Pending,
}

3.2. 에이전트 런타임 구조

ZeroClaw의 에이전트는 각각 독립적인 Tokio 태스크로 실행됩니다. 에이전트 간 통신은 채널(Channel)을 통해 이루어지며, 중재자(Mediator)가 이를 라우팅합니다.

// src/runtime/agent.rs

use tokio::sync::mpsc;
use crate::protocol::message::{AgentMessage, Payload};

pub struct Agent {
    id: String,
    tx: mpsc::Sender<AgentMessage>, // 외부로 메시지를 보내는 송신자
    rx: mpsc::Receiver<AgentMessage>, // 내부에서 메시지를 받는 수신자
}

impl Agent {
    pub fn new(id: String) -> Self {
        let (tx, rx) = mpsc::channel(100);
        Self { id, tx, rx }
    }

    // 에이전트의 메인 실행 루프
    pub async fn run(mut self) {
        println!("[{}] Agent started", self.id);
        while let Some(msg) = self.rx.recv().await {
            if let Err(e) = self.handle_message(msg).await {
                eprintln!("[{}] Error handling message: {:?}", self.id, e);
            }
        }
    }

    async fn handle_message(&self, msg: AgentMessage) -> Result<(), Box<dyn std::error::Error>> {
        match msg.payload {
            Payload::SystemPing => {
                // 핑에 퐁으로 응답하는 로직
                // 실제 환경에서는 클라이언트의 tx를 사용해 응답을 보냄
                println!("[{}] Received Ping from {}", self.id, msg.sender);
            }
            Payload::TaskRequest { task_id, description, context } => {
                println!("[{}] Received Task {}: {}", self.id, task_id, description);
                // 여기에 LLM 호출이나 도구 실행 로직이 들어갑니다.
                
                // 예: 결과 반환 로직 (가상)
                // self.send_result(task_id, ...).await;
            }
            _ => {
                println!("[{}] Received unhandled message type", self.id);
            }
        }
        Ok(())
    }
}

4. 결론 및 다음 단계

위와 같이 구조화된 통신 계층을 통해, 우리는 단순한 텍스트 생성을 넘어 작업(Task) 단위의 협업이 가능한 아키텍처를 갖추게 됩니다. [ZeroClaw] 프로젝트의 2026 상반기 목표인 ‘고성능 에이전트 런타임’은 이러한 견고한 기초 위에 LLM 추론 로직을 통합함으로써 완성될 것입니다.

다음 포스트에서는 이 통신 프로토콜 위에서 실제로 LLM을 호출하여 도구(Tool)를 사용하는 ‘플래너(Planner)’ 에이전트의 구현细节을 다루겠습니다.

참고자료

  • [ZeroClaw] 멀티 에이전트 아키텍처 설계안
  • [Claude Code] 팀 에이전트 통신 아키텍처
  • Rust Tokio Documentation
Hugo로 만듦
JimmyStack 테마 사용 중