커뮤니케이션 프로토콜

같은 언어로 말하지 못하는 에이전트들은 팀이 아닙니다. 허공에 대고 각자 소리치는 낯선 존재들입니다.

유형: Build 언어: TypeScript 선수 조건: Phase 14(에이전트 엔지니어링), 16.01강(왜 멀티 에이전트인가) 예상 시간: 약 120분

학습 목표

  • 에이전트가 외부 서버가 노출한 도구를 사용할 수 있도록 MCP(Model Context Protocol)의 도구 발견(tool discovery)과 호출(invocation)을 구현합니다.
  • 한 에이전트가 HTTP를 통해 다른 에이전트에게 작업을 위임할 수 있도록 A2A(Agent2Agent) Agent Card와 과제 엔드포인트(task endpoint)를 만듭니다.
  • MCP(도구 접근), A2A(에이전트 간 협업), ACP(기업 감사), ANP(탈중앙 신뢰)를 비교하고, 어떤 프로토콜이 어떤 문제를 푸는지 설명합니다.
  • 에이전트가 MCP로 도구를 발견하고 A2A로 과제를 위임하는 단일 시스템 안에서 여러 프로토콜을 연결합니다.

문제

시스템을 여러 에이전트로 나누었습니다. 연구자, 코더, 리뷰어가 있습니다. 각자는 자기 일을 잘합니다. 하지만 이제 이들이 실제로 서로 말해야 합니다.

첫 시도는 당연해 보입니다. 문자열을 주고받습니다. 연구자는 텍스트 덩어리를 반환하고, 코더는 그것을 나름대로 파싱합니다. 처음에는 작동합니다. 하지만 코더가 조사 요약을 잘못 해석하거나, 두 에이전트가 서로를 기다리며 교착(deadlock)에 빠지거나, 서로 다른 팀이 만든 에이전트들이 협업해야 하는 순간 "그냥 문자열을 넘기자"는 방식은 무너집니다.

이것이 커뮤니케이션 프로토콜(communication protocol) 문제입니다. 에이전트가 정보를 교환하는 방법에 대한 공유 계약(shared contract)이 없으면, 멀티 에이전트 시스템은 깨지기 쉽고, 감사하기 어렵고, 직접 작성한 소수의 에이전트를 넘어 확장하기 어렵습니다.

AI 생태계는 이 문제에 네 가지 프로토콜로 대응했습니다. 각 프로토콜은 문제의 서로 다른 조각을 해결합니다.

  • MCP: 도구 접근(tool access)을 위한 프로토콜입니다.
  • A2A: 에이전트 간 협업(agent-to-agent collaboration)을 위한 프로토콜입니다.
  • ACP: 기업 감사 가능성(enterprise auditability)을 위한 프로토콜입니다.
  • ANP: 탈중앙 신원과 신뢰(decentralized identity and trust)를 위한 프로토콜입니다.

이 강의는 깊게 들어갑니다. 각 사양의 실제 와이어 포맷(wire format)을 읽고, 동작하는 구현을 직접 만들어 본 다음, 네 프로토콜을 하나의 통합 시스템으로 연결해 봅니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.MCP(Model Context Protocol)는 AI 에이전트에게 어떤 문제를 해결해 주나요?

2.MCP와 A2A의 차이는 무엇인가요?

0/2 답변 완료

개념

프로토콜 지형

네 가지 프로토콜은 서로 다른 질문에 답하는 계층(layer)으로 볼 수 있습니다.

block-beta
  columns 1
  block:ANP["ANP — 낯선 에이전트를 어떻게 신뢰하는가?\n탈중앙 신원(DID), E2EE, meta-protocol"]
  end
  block:A2A["A2A — 에이전트가 목표를 두고 어떻게 협업하는가?\nAgent Cards, task lifecycle, streaming, negotiation"]
  end
  block:ACP["ACP — 감사 가능한 시스템에서 에이전트는 어떻게 대화하는가?\nRuns, trajectory metadata, session continuity"]
  end
  block:MCP["MCP — 에이전트가 도구를 어떻게 사용하는가?\nTool discovery, execution, context sharing"]
  end

  style ANP fill:#f3e8ff,stroke:#7c3aed
  style A2A fill:#dbeafe,stroke:#2563eb
  style ACP fill:#fef3c7,stroke:#d97706
  style MCP fill:#d1fae5,stroke:#059669

이 프로토콜들은 서로 경쟁자가 아닙니다. 각각 다른 수준에서 서로 다른 문제를 해결합니다.

MCP 요약

MCP는 Phase 13에서 깊게 다뤘습니다. 간단히 요약하면, MCP는 LLM이 외부 도구와 데이터 소스에 연결되는 방식을 표준화합니다. 에이전트가 클라이언트(client)가 되고, 서버가 노출한 도구를 발견하고 호출하는 클라이언트-서버(client-server) 프로토콜입니다.

sequenceDiagram
    participant Agent as Agent (client)
    participant MCP1 as MCP Server<br/>(database, API, files)

    Agent->>MCP1: list tools
    MCP1-->>Agent: tool definitions
    Agent->>MCP1: call tool X
    MCP1-->>Agent: result

MCP는 에이전트-도구(agent-to-tool) 통신입니다. 에이전트끼리 말하게 해 주는 프로토콜은 아닙니다.

A2A(Agent2Agent Protocol)

만든 곳: Google. 현재는 Linux Foundation의 lf.a2a.v1 아래에 있습니다. 사양 버전: 1.0.0 문제: 자율 에이전트가 서로 협업하고, 협상하고, 과제를 위임하려면 어떻게 해야 하는가?

A2A는 피어 투 피어 에이전트 협업(peer-to-peer agent collaboration)을 위한 프로토콜입니다. MCP가 에이전트를 도구와 연결한다면, A2A는 에이전트를 다른 에이전트와 연결합니다. 각 에이전트는 약속된 URL(well-known URL)에 Agent Card를 게시하고, 다른 에이전트는 이를 발견하고 협상하며 과제를 위임합니다.

A2A 작동 방식

sequenceDiagram
    participant Client as Client Agent
    participant Remote as Remote Agent

    Client->>Remote: GET /.well-known/agent-card.json
    Remote-->>Client: Agent Card (skills, modes, security)

    Client->>Remote: POST /message:send
    Remote-->>Client: Task (submitted/working)

    alt Polling
        Client->>Remote: GET /tasks/{id}
        Remote-->>Client: Task status + artifacts
    else Streaming
        Client->>Remote: POST /message:stream
        Remote-->>Client: SSE: statusUpdate
        Remote-->>Client: SSE: artifactUpdate
        Remote-->>Client: SSE: completed
    end

실제 Agent Card

A2A Agent Card는 실제 환경에서 다음과 같은 JSON 문서입니다. GET /.well-known/agent-card.json에서 제공됩니다.

{
  "name": "Research Agent",
  "description": "Searches documentation and summarizes findings",
  "version": "1.0.0",
  "supportedInterfaces": [
    {
      "url": "https://research-agent.example.com/a2a/v1",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    },
    {
      "url": "https://research-agent.example.com/a2a/rest",
      "protocolBinding": "HTTP+JSON",
      "protocolVersion": "1.0"
    }
  ],
  "provider": {
    "organization": "Your Company",
    "url": "https://example.com"
  },
  "capabilities": {
    "streaming": true,
    "pushNotifications": false
  },
  "defaultInputModes": ["text/plain", "application/json"],
  "defaultOutputModes": ["text/plain", "application/json"],
  "skills": [
    {
      "id": "web-research",
      "name": "Web Research",
      "description": "Searches the web and synthesizes findings",
      "tags": ["research", "search", "summarization"],
      "examples": ["Research the latest changes in React 19"]
    },
    {
      "id": "doc-analysis",
      "name": "Documentation Analysis",
      "description": "Reads and analyzes technical documentation",
      "tags": ["docs", "analysis"],
      "inputModes": ["text/plain", "application/pdf"],
      "outputModes": ["application/json"]
    }
  ],
  "securitySchemes": {
    "bearer": {
      "httpAuthSecurityScheme": {
        "scheme": "Bearer",
        "bearerFormat": "JWT"
      }
    }
  },
  "security": [{ "bearer": [] }]
}

주의해서 볼 점은 다음과 같습니다.

  • 스킬(Skills)은 에이전트가 할 수 있는 일을 정의합니다. 각 스킬에는 ID, 태그, 그리고 지원하는 입력/출력 MIME 타입이 있습니다. 클라이언트 쪽 에이전트는 이 정보를 보고 원격 에이전트가 자신의 요청을 처리할 수 있는지 판단합니다.
  • supportedInterfaces는 여러 프로토콜 바인딩(protocol binding)을 나열합니다. 한 에이전트가 JSON-RPC, REST, gRPC를 동시에 말할 수 있습니다.
  • 보안(Security)은 카드 안에 함께 들어 있습니다. 클라이언트는 요청을 한 번 보내기 전부터 어떤 인증이 필요한지 미리 알 수 있습니다.

과제 생명주기

과제(Task)는 A2A의 핵심 작업 단위입니다. 과제는 정의된 상태(state)를 따라 이동합니다.

stateDiagram-v2
    [*] --> submitted
    submitted --> working
    working --> input_required: needs more info
    input_required --> working: client sends data
    working --> completed: success
    working --> failed: error
    working --> canceled: client cancels
    submitted --> rejected: agent declines

    completed --> [*]
    failed --> [*]
    canceled --> [*]
    rejected --> [*]

    note right of completed: Terminal states are immutable.\nFollow-ups create new tasks\nwithin the same contextId.

전체 8개 상태입니다. 사양에는 센티넬(sentinel) 값인 UNSPECIFIED도 정의되어 있지만 여기서는 다루지 않습니다.

상태종료 상태인가?의미
TASK_STATE_SUBMITTED아니요접수되었지만 아직 처리되지 않음
TASK_STATE_WORKING아니요처리 중
TASK_STATE_INPUT_REQUIRED아니요에이전트가 클라이언트에게 추가 정보를 요구함
TASK_STATE_AUTH_REQUIRED아니요인증 필요
TASK_STATE_COMPLETED성공적으로 완료됨
TASK_STATE_FAILED오류로 종료됨
TASK_STATE_CANCELED완료 전에 취소됨
TASK_STATE_REJECTED에이전트가 과제를 거절함

과제가 종료 상태(terminal state)에 도달하면 불변(immutable)이 됩니다. 더 이상 메시지가 추가되지 않으며, 후속 작업은 같은 contextId 안에서 새 과제로 생성됩니다.

와이어 포맷

A2A는 JSON-RPC 2.0을 사용합니다. 실제 메시지 교환은 다음과 같이 이루어집니다.

클라이언트가 과제를 보냅니다.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "msg-001",
      "role": "ROLE_USER",
      "parts": [{ "text": "Research React 19 compiler features" }]
    },
    "configuration": {
      "acceptedOutputModes": ["text/plain", "application/json"],
      "historyLength": 10
    }
  }
}

에이전트가 과제로 응답합니다.

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "task": {
      "id": "task-abc-123",
      "contextId": "ctx-xyz-789",
      "status": {
        "state": "TASK_STATE_COMPLETED",
        "timestamp": "2026-03-27T10:30:00Z"
      },
      "artifacts": [
        {
          "artifactId": "art-001",
          "name": "research-results",
          "parts": [{
            "data": {
              "findings": [
                "React 19 compiler auto-memoizes components",
                "No more manual useMemo/useCallback needed",
                "Compiler runs at build time, not runtime"
              ]
            },
            "mediaType": "application/json"
          }]
        }
      ]
    }
  }
}

SSE(Server-Sent Events)를 통한 스트리밍

POST /message:stream HTTP/1.1
Content-Type: application/json
A2A-Version: 1.0

data: {"task":{"id":"task-123","status":{"state":"TASK_STATE_WORKING"}}}

data: {"statusUpdate":{"taskId":"task-123","status":{"state":"TASK_STATE_WORKING","message":{"role":"ROLE_AGENT","parts":[{"text":"Searching documentation..."}]}}}}

data: {"artifactUpdate":{"taskId":"task-123","artifact":{"artifactId":"art-1","parts":[{"text":"partial findings..."}]},"append":true,"lastChunk":false}}

data: {"statusUpdate":{"taskId":"task-123","status":{"state":"TASK_STATE_COMPLETED"}}}

ACP(Agent Communication Protocol)

만든 곳: IBM / BeeAI 사양 버전: 0.2.0(OpenAPI 3.1.1) 상태: Linux Foundation 아래에서 A2A로 병합 중 문제: 에이전트가 완전한 감사 가능성(auditability), 세션 연속성(session continuity), 궤적 추적(trajectory tracking)을 갖고 통신하려면 어떻게 해야 하는가?

ACP는 기업용 프로토콜(enterprise protocol)입니다. 많은 요약과 달리 ACP는 JSON-LD를 사용하지 않습니다. OpenAPI로 정의된 단순한 REST/JSON API입니다. 특별한 점은 TrajectoryMetadata입니다. 모든 에이전트 응답에 그 응답을 만들어 낸 추론 단계와 도구 호출의 상세 로그를 함께 담을 수 있습니다.

sequenceDiagram
    participant Client
    participant ACP as ACP Agent
    participant Audit as Audit Log

    Client->>ACP: POST /runs (mode: sync)
    ACP->>ACP: Process request...
    ACP->>Audit: Log trajectory:<br/>reasoning + tool calls
    ACP-->>Client: Response + TrajectoryMetadata
    Note over Audit: Every step recorded:<br/>tool_name, tool_input,<br/>tool_output, reasoning

ACP의 에이전트 발견

ACP는 네 가지 발견 방식을 정의합니다.

graph LR
    A[Agent Discovery] --> B["Runtime<br/>GET /agents"]
    A --> C["Open<br/>.well-known/agent.yml"]
    A --> D["Registry<br/>Centralized catalog"]
    A --> E["Embedded<br/>Container labels"]

    style B fill:#dbeafe,stroke:#2563eb
    style C fill:#d1fae5,stroke:#059669
    style D fill:#fef3c7,stroke:#d97706
    style E fill:#f3e8ff,stroke:#7c3aed

AgentManifest는 A2A의 Agent Card보다 단순합니다.

{
  "name": "summarizer",
  "description": "Summarizes documents with source citations",
  "input_content_types": ["text/plain", "application/pdf"],
  "output_content_types": ["text/plain", "application/json"],
  "metadata": {
    "tags": ["summarization", "RAG"],
    "framework": "BeeAI",
    "capabilities": [
      {
        "name": "Document Summarization",
        "description": "Condenses long documents into key points"
      }
    ],
    "recommended_models": ["llama3.3:70b-instruct-fp16"],
    "license": "Apache-2.0",
    "programming_language": "Python"
  }
}

실행 생명주기

ACP는 "Tasks" 대신 "Runs"라는 단위를 사용합니다. 실행(Run)은 세 가지 모드를 가진 에이전트 실행 단위입니다.

모드동작
sync블로킹 방식입니다. 응답에 전체 결과가 들어 있습니다.
async즉시 202를 반환합니다. GET /runs/{id}로 상태를 폴링합니다.
streamSSE 스트림입니다. 에이전트가 작업하는 동안 이벤트가 발생합니다.
stateDiagram-v2
    [*] --> created
    created --> in_progress
    in_progress --> completed: success
    in_progress --> failed: error
    in_progress --> awaiting: needs input
    awaiting --> in_progress: client resumes
    in_progress --> cancelling: cancel request
    cancelling --> cancelled

    completed --> [*]
    failed --> [*]
    cancelled --> [*]

TrajectoryMetadata — 감사 추적

이것이 ACP의 핵심 차별점입니다. 모든 메시지 파트(message part)에는 에이전트가 정확히 무엇을 했는지 보여주는 메타데이터가 들어갈 수 있습니다.

{
  "role": "agent/researcher",
  "parts": [
    {
      "content_type": "text/plain",
      "content": "The weather in San Francisco is 72F and sunny.",
      "metadata": {
        "kind": "trajectory",
        "message": "I need to check the weather for this location",
        "tool_name": "weather_api",
        "tool_input": { "location": "San Francisco, CA" },
        "tool_output": { "temperature": 72, "condition": "sunny" }
      }
    }
  ]
}

규제 산업(regulated industry)에서는 이 점이 매우 중요합니다. 모든 답변에는 어떤 도구가 호출되었고, 어떤 입력이 쓰였으며, 어떤 출력이 돌아왔는지를 증명할 수 있는 추론 사슬(chain of reasoning)이 함께 붙습니다. 더 이상 블랙박스가 아닙니다.

ACP는 출처 표기(source attribution)를 위한 CitationMetadata도 지원합니다.

{
  "kind": "citation",
  "start_index": 0,
  "end_index": 47,
  "url": "https://weather.gov/sf",
  "title": "NWS San Francisco Forecast"
}

ANP(Agent Network Protocol)

만든 곳: 오픈소스 커뮤니티(GaoWei Chang이 시작) 저장소: github.com/agent-network-protocol/AgentNetworkProtocol 문제: 서로 다른 조직의 에이전트가 중앙 권위 없이 서로를 어떻게 신뢰하는가?

ANP는 탈중앙 신원 프로토콜(decentralized identity protocol)입니다. W3C 탈중앙 식별자(Decentralized Identifiers; DID)와 종단 간 암호화(end-to-end encryption; E2EE)를 사용해 신뢰를 구축합니다. 알려진 엔드포인트를 통해 에이전트를 발견하는 A2A와 달리, ANP는 에이전트가 자기 신원을 암호학적으로 증명하게 합니다.

ANP는 세 개의 계층으로 구성됩니다.

graph TB
    subgraph Layer3["Layer 3: Application Protocol"]
        AD[Agent Description Documents]
        DISC[Discovery endpoints]
    end
    subgraph Layer2["Layer 2: Meta-Protocol"]
        NEG[AI-powered protocol negotiation]
        CODE[Dynamic code generation]
    end
    subgraph Layer1["Layer 1: Identity & Secure Communication"]
        DID["did:wba (W3C DID)"]
        HPKE[HPKE E2EE - RFC 9180]
        SIG[Signature verification]
    end

    Layer3 --> Layer2
    Layer2 --> Layer1

    style Layer1 fill:#d1fae5,stroke:#059669
    style Layer2 fill:#dbeafe,stroke:#2563eb
    style Layer3 fill:#f3e8ff,stroke:#7c3aed

DID 문서의 실제 구조

ANP는 did:wba(Web-Based Agent)라는 자체 DID 방식(method)을 사용합니다. DID 식별자 did:wba:example.com:user:alicehttps://example.com/user/alice/did.json으로 해석됩니다.

{
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/suites/jws-2020/v1",
    "https://w3id.org/security/suites/secp256k1-2019/v1"
  ],
  "id": "did:wba:example.com:user:alice",
  "verificationMethod": [
    {
      "id": "did:wba:example.com:user:alice#key-1",
      "type": "EcdsaSecp256k1VerificationKey2019",
      "controller": "did:wba:example.com:user:alice",
      "publicKeyJwk": {
        "crv": "secp256k1",
        "x": "NtngWpJUr-rlNNbs0u-Aa8e16OwSJu6UiFf0Rdo1oJ4",
        "y": "qN1jKupJlFsPFc1UkWinqljv4YE0mq_Ickwnjgasvmo",
        "kty": "EC"
      }
    },
    {
      "id": "did:wba:example.com:user:alice#key-x25519-1",
      "type": "X25519KeyAgreementKey2019",
      "controller": "did:wba:example.com:user:alice",
      "publicKeyMultibase": "z9hFgmPVfmBZwRvFEyniQDBkz9LmV7gDEqytWyGZLmDXE"
    }
  ],
  "authentication": [
    "did:wba:example.com:user:alice#key-1"
  ],
  "keyAgreement": [
    "did:wba:example.com:user:alice#key-x25519-1"
  ],
  "humanAuthorization": [
    "did:wba:example.com:user:alice#key-1"
  ],
  "service": [
    {
      "id": "did:wba:example.com:user:alice#agent-description",
      "type": "AgentDescription",
      "serviceEndpoint": "https://example.com/agents/alice/ad.json"
    }
  ]
}

주의해서 볼 점은 다음과 같습니다.

  • 키 분리(Key separation)가 강제됩니다. 서명용 키(secp256k1)는 암호화용 키(X25519)와 별도로 관리됩니다.
  • humanAuthorization은 ANP에만 있는 요소입니다. 이 키를 사용하려면 생체 인증, 비밀번호, HSM(Hardware Security Module) 같은 명시적인 사람의 승인이 필요합니다. 자금 이체처럼 위험도가 높은 작업은 이 경로를 거칩니다.
  • keyAgreement 키는 HPKE 종단 간 암호화(RFC 9180)에 사용됩니다.
  • service 섹션은 Agent Description 문서로 연결됩니다.

ANP에서 신뢰가 작동하는 방식

ANP는 신뢰 그물망(web-of-trust)이나 추천 그래프(endorsement graph)를 사용하지 않습니다. 신뢰는 양자 간(bilateral)이며, 상호작용이 일어날 때마다 매번 검증됩니다.

sequenceDiagram
    participant A as Agent A
    participant Domain as Agent A's Domain
    participant B as Agent B

    A->>B: HTTP request + DID + signature
    B->>Domain: Fetch DID document (HTTPS)
    Domain-->>B: DID document + public key
    B->>B: Verify signature with public key
    B-->>A: Issue access token
    A->>B: Subsequent requests use token
    Note over A,B: Trust = TLS domain verification<br/>+ DID signature verification<br/>+ Principle of least trust

신뢰는 다음 세 곳에서 만들어집니다.

  1. 도메인 수준 TLS가 DID 문서를 제공하는 호스트를 검증합니다.
  2. DID의 암호학적 서명이 에이전트의 신원을 검증합니다.
  3. 최소 신뢰 원칙(Principle of least trust)이 꼭 필요한 최소 권한만 부여합니다.

소문 기반 신뢰 전파(gossip-based trust propagation)나 페이지랭크(PageRank) 방식의 점수 계산은 사용하지 않습니다. 각 에이전트는 자신의 DID를 통해 직접 검증됩니다.

메타 프로토콜 협상

이것이 ANP의 가장 새로운 기능입니다. 서로 다른 생태계의 두 에이전트가 만나도, 미리 합의된 데이터 형식이 없어도 됩니다. 자연어로 협상합니다.

{
  "action": "protocolNegotiation",
  "sequenceId": 0,
  "candidateProtocols": "I can communicate using:\n1. JSON-RPC with hotel booking schema\n2. REST with OpenAPI 3.1 spec\n3. Natural language over HTTP",
  "modificationSummary": "Initial proposal",
  "status": "negotiating"
}
sequenceDiagram
    participant A as Agent A
    participant B as Agent B

    A->>B: protocolNegotiation (candidateProtocols)
    B->>A: protocolNegotiation (counter-proposal)
    A->>B: protocolNegotiation (accepted)
    Note over A,B: Agents dynamically generate code<br/>to handle the agreed format.<br/>Max 10 rounds, then timeout.

에이전트들은 최대 10라운드 동안 메시지를 주고받으며 형식에 합의한 뒤, 그 형식을 처리할 코드를 동적으로 생성합니다. 상태 값은 negotiating, rejected, accepted, timeout 네 가지입니다.

즉, 서로 한 번도 본 적 없는 두 에이전트도 미리 공유 스키마를 정의해 두지 않아도 통신 방식을 스스로 찾아낼 수 있다는 뜻입니다.

비교(수정된 관점)

MCPA2AACPANP
만든 곳AnthropicGoogle / Linux FoundationIBM / BeeAI커뮤니티
사양 형식JSON-RPCJSON-RPC / REST / gRPCOpenAPI 3.1 (REST)JSON-RPC
주 사용처에이전트-도구에이전트-에이전트에이전트-에이전트에이전트-에이전트
발견 방식도구 목록 조회/.well-known/agent-card.jsonGET /agents, /.well-known/agent.yml/.well-known/agent-descriptions, DID 서비스 엔드포인트
신원암묵적(로컬)보안 스킴(OAuth, mTLS)서버 수준E2EE가 결합된 W3C DID(did:wba)
감사 추적없음기본 수준(과제 이력)TrajectoryMetadata(도구 호출, 추론)형식상 지정되지 않음
상태 머신없음9개의 과제 상태7개의 실행 상태없음
스트리밍없음SSESSE전송 계층에 독립적
고유 기능도구 스키마Agent Card와 스킬궤적 감사 추적메타 프로토콜 협상
가장 적합한 곳도구와 데이터동적 협업규제 산업조직 간 신뢰
상태안정(Stable)안정 v1.0(Stable)A2A로 병합 중활발한 개발 중(Active development)

함께 동작하는 방식

이 프로토콜들은 서로 배타적이지 않습니다. 현실적인 기업 시스템은 여러 프로토콜을 함께 사용합니다.

graph TB
    subgraph org["Your Organization"]
        RA[Research Agent] <-->|A2A| CA[Coding Agent]
        RA -->|MCP| SS[Search Server]
        CA -->|MCP| GS[GitHub Server]
        AUDIT["All agent responses carry<br/>ACP TrajectoryMetadata"]
    end

    subgraph ext["External (DID verified via ANP)"]
        EA[External Agent]
        PA[Partner Agent]
    end

    RA <-->|ANP + A2A| EA
    CA <-->|ANP + A2A| PA

    style org fill:#f8fafc,stroke:#334155
    style ext fill:#fef2f2,stroke:#991b1b
    style AUDIT fill:#fef3c7,stroke:#d97706
  • MCP는 각 에이전트를 도구와 연결합니다.
  • A2A는 내부와 외부 에이전트 사이의 협업을 담당합니다.
  • ACP는 감사 가능성을 보장하기 위해 응답을 궤적 메타데이터(trajectory metadata)로 감쌉니다.
  • ANP는 우리가 직접 통제하지 않는 외부 에이전트의 신원 검증을 제공합니다.

직접 만들기

Step 1: 핵심 메시지 타입

모든 멀티 에이전트 시스템은 메시지 형식(message format)에서 시작합니다. 여기서는 실제 프로토콜이 사용하는 구조에 대응하는 타입을 정의합니다.

import crypto from "node:crypto";

type MessageRole = "user" | "agent";

type MessagePart =
  | { kind: "text"; text: string }
  | { kind: "data"; data: unknown; mediaType: string }
  | { kind: "file"; name: string; url: string; mediaType: string };

type TrajectoryEntry = {
  reasoning: string;
  toolName?: string;
  toolInput?: unknown;
  toolOutput?: unknown;
  timestamp: number;
};

type AgentMessage = {
  id: string;
  role: MessageRole;
  parts: MessagePart[];
  trajectory?: TrajectoryEntry[];
  replyTo?: string;
  timestamp: number;
};

function createMessage(
  role: MessageRole,
  parts: MessagePart[],
  replyTo?: string
): AgentMessage {
  return {
    id: crypto.randomUUID(),
    role,
    parts,
    replyTo,
    timestamp: Date.now(),
  };
}

function textMessage(role: MessageRole, text: string): AgentMessage {
  return createMessage(role, [{ kind: "text", text }]);
}

MessagePart는 실제 A2A와 ACP 사양처럼 멀티모달(텍스트, 구조화 데이터, 파일)입니다. TrajectoryEntry는 ACP의 TrajectoryMetadata와 마찬가지로 추론 과정의 사슬을 담아냅니다.

Step 2: A2A Agent Card와 Registry

실제 A2A 사양에 부합하는 에이전트 발견 기능을 구현합니다.

type Skill = {
  id: string;
  name: string;
  description: string;
  tags: string[];
  inputModes: string[];
  outputModes: string[];
};

type AgentCard = {
  name: string;
  description: string;
  version: string;
  url: string;
  capabilities: {
    streaming: boolean;
    pushNotifications: boolean;
  };
  defaultInputModes: string[];
  defaultOutputModes: string[];
  skills: Skill[];
};

class AgentRegistry {
  private cards: Map<string, AgentCard> = new Map();

  register(card: AgentCard) {
    this.cards.set(card.name, card);
  }

  discoverBySkillTag(tag: string): AgentCard[] {
    return [...this.cards.values()].filter((card) =>
      card.skills.some((skill) => skill.tags.includes(tag))
    );
  }

  discoverByInputMode(mimeType: string): AgentCard[] {
    return [...this.cards.values()].filter(
      (card) =>
        card.defaultInputModes.includes(mimeType) ||
        card.skills.some((skill) => skill.inputModes.includes(mimeType))
    );
  }

  resolve(name: string): AgentCard | undefined {
    return this.cards.get(name);
  }

  listAll(): AgentCard[] {
    return [...this.cards.values()];
  }
}

이 구현은 단순한 이름-역량 매핑보다 훨씬 풍부합니다. 실제 A2A 사양이 지원하는 것처럼, 스킬 태그(skill tag), 입력 MIME 타입, 이름으로 에이전트를 발견할 수 있습니다.

Step 3: A2A Task 생명주기

A2A의 전체 과제 상태 머신(task state machine)을 구현합니다.

type TaskState =
  | "submitted"
  | "working"
  | "input-required"
  | "auth-required"
  | "completed"
  | "failed"
  | "canceled"
  | "rejected";

const TERMINAL_STATES: TaskState[] = [
  "completed",
  "failed",
  "canceled",
  "rejected",
];

type TaskStatus = {
  state: TaskState;
  message?: AgentMessage;
  timestamp: number;
};

type Artifact = {
  id: string;
  name: string;
  parts: MessagePart[];
};

type Task = {
  id: string;
  contextId: string;
  status: TaskStatus;
  artifacts: Artifact[];
  history: AgentMessage[];
};

type TaskEvent =
  | { kind: "statusUpdate"; taskId: string; status: TaskStatus }
  | {
      kind: "artifactUpdate";
      taskId: string;
      artifact: Artifact;
      append: boolean;
      lastChunk: boolean;
    };

type TaskHandler = (
  task: Task,
  message: AgentMessage
) => AsyncGenerator<TaskEvent>;

class TaskManager {
  private tasks: Map<string, Task> = new Map();
  private handlers: Map<string, TaskHandler> = new Map();
  private listeners: Map<string, ((event: TaskEvent) => void)[]> = new Map();

  registerHandler(agentName: string, handler: TaskHandler) {
    this.handlers.set(agentName, handler);
  }

  subscribe(taskId: string, listener: (event: TaskEvent) => void) {
    const existing = this.listeners.get(taskId) ?? [];
    existing.push(listener);
    this.listeners.set(taskId, existing);
  }

  async sendMessage(
    agentName: string,
    message: AgentMessage,
    contextId?: string
  ): Promise<Task> {
    const handler = this.handlers.get(agentName);
    if (!handler) {
      const task = this.createTask(contextId);
      task.status = {
        state: "rejected",
        timestamp: Date.now(),
        message: textMessage("agent", `${agentName}에 대한 handler가 없습니다`),
      };
      return task;
    }

    const task = this.createTask(contextId);
    task.history.push(message);
    task.status = { state: "submitted", timestamp: Date.now() };

    this.processTask(task, handler, message).catch((err) => {
      task.status = {
        state: "failed",
        timestamp: Date.now(),
        message: textMessage("agent", String(err)),
      };
    });
    return task;
  }

  getTask(taskId: string): Task | undefined {
    return this.tasks.get(taskId);
  }

  cancelTask(taskId: string): boolean {
    const task = this.tasks.get(taskId);
    if (!task || TERMINAL_STATES.includes(task.status.state)) return false;
    task.status = { state: "canceled", timestamp: Date.now() };
    this.emit(taskId, {
      kind: "statusUpdate",
      taskId,
      status: task.status,
    });
    return true;
  }

  private createTask(contextId?: string): Task {
    const task: Task = {
      id: crypto.randomUUID(),
      contextId: contextId ?? crypto.randomUUID(),
      status: { state: "submitted", timestamp: Date.now() },
      artifacts: [],
      history: [],
    };
    this.tasks.set(task.id, task);
    return task;
  }

  private async processTask(
    task: Task,
    handler: TaskHandler,
    message: AgentMessage
  ) {
    task.status = { state: "working", timestamp: Date.now() };
    this.emit(task.id, {
      kind: "statusUpdate",
      taskId: task.id,
      status: task.status,
    });

    try {
      for await (const event of handler(task, message)) {
        if (TERMINAL_STATES.includes(task.status.state)) break;

        if (event.kind === "statusUpdate") {
          task.status = event.status;
        }
        if (event.kind === "artifactUpdate") {
          const existing = task.artifacts.find(
            (a) => a.id === event.artifact.id
          );
          if (existing && event.append) {
            existing.parts.push(...event.artifact.parts);
          } else {
            task.artifacts.push(event.artifact);
          }
        }
        this.emit(task.id, event);
      }
    } catch (err) {
      task.status = {
        state: "failed",
        timestamp: Date.now(),
        message: textMessage("agent", String(err)),
      };
      this.emit(task.id, {
        kind: "statusUpdate",
        taskId: task.id,
        status: task.status,
      });
    }
  }

  private emit(taskId: string, event: TaskEvent) {
    for (const listener of this.listeners.get(taskId) ?? []) {
      listener(event);
    }
  }
}

이 코드는 실제 A2A 과제 생명주기인 submitted, working, input-required, 그리고 종료 상태(terminal state)를 그대로 구현합니다. 핸들러는 비동기 생성기(async generator)이며, SSE 스트리밍 모델과 일치하는 상태 업데이트(status update)와 산출물 청크(artifact chunk)를 yield합니다.

Step 4: ACP 스타일 감사 추적

통신을 궤적 추적(trajectory tracking)으로 감싸 봅니다.

type AuditEntry = {
  runId: string;
  agentName: string;
  input: AgentMessage[];
  output: AgentMessage[];
  trajectory: TrajectoryEntry[];
  status: "created" | "in-progress" | "completed" | "failed" | "awaiting";
  startedAt: number;
  completedAt?: number;
  sessionId?: string;
};

class AuditableRunner {
  private log: AuditEntry[] = [];
  private handlers: Map<
    string,
    (input: AgentMessage[]) => Promise<{
      output: AgentMessage[];
      trajectory: TrajectoryEntry[];
    }>
  > = new Map();

  registerAgent(
    name: string,
    handler: (input: AgentMessage[]) => Promise<{
      output: AgentMessage[];
      trajectory: TrajectoryEntry[];
    }>
  ) {
    this.handlers.set(name, handler);
  }

  async run(
    agentName: string,
    input: AgentMessage[],
    sessionId?: string
  ): Promise<AuditEntry> {
    const entry: AuditEntry = {
      runId: crypto.randomUUID(),
      agentName,
      input: structuredClone(input),
      output: [],
      trajectory: [],
      status: "created",
      startedAt: Date.now(),
      sessionId,
    };
    this.log.push(entry);

    const handler = this.handlers.get(agentName);
    if (!handler) {
      entry.status = "failed";
      return entry;
    }

    entry.status = "in-progress";
    try {
      const result = await handler(input);
      entry.output = structuredClone(result.output);
      entry.trajectory = structuredClone(result.trajectory);
      entry.status = "completed";
      entry.completedAt = Date.now();
    } catch (err) {
      entry.status = "failed";
      entry.trajectory.push({
        reasoning: `오류: ${String(err)}`,
        timestamp: Date.now(),
      });
      entry.completedAt = Date.now();
    }
    return entry;
  }

  getFullAuditLog(): AuditEntry[] {
    return structuredClone(this.log);
  }

  getAuditLogForAgent(agentName: string): AuditEntry[] {
    return structuredClone(
      this.log.filter((e) => e.agentName === agentName)
    );
  }

  getAuditLogForSession(sessionId: string): AuditEntry[] {
    return structuredClone(
      this.log.filter((e) => e.sessionId === sessionId)
    );
  }

  getTrajectoryForRun(runId: string): TrajectoryEntry[] {
    const entry = this.log.find((e) => e.runId === runId);
    return entry ? structuredClone(entry.trajectory) : [];
  }
}

에이전트 실행이 일어날 때마다 완전한 감사 항목(audit entry)이 만들어집니다. 무엇이 입력으로 들어갔고, 무엇이 출력으로 나왔으며, 그 사이에 어떤 도구 호출과 추론 단계가 있었는지가 전부 기록됩니다. 에이전트별, 세션별, 개별 실행(run)별로 조회할 수 있습니다.

Step 5: ANP 스타일 신원 검증

DID 기반 신원과 검증을 구현합니다.

type VerificationMethod = {
  id: string;
  type: string;
  controller: string;
  publicKeyDer: string;
};

type DIDDocument = {
  id: string;
  verificationMethod: VerificationMethod[];
  authentication: string[];
  keyAgreement: string[];
  humanAuthorization: string[];
  service: { id: string; type: string; serviceEndpoint: string }[];
};

type AgentIdentity = {
  did: string;
  document: DIDDocument;
  privateKey: crypto.KeyObject;
  publicKey: crypto.KeyObject;
};

class IdentityRegistry {
  private documents: Map<string, DIDDocument> = new Map();

  publish(doc: DIDDocument) {
    this.documents.set(doc.id, doc);
  }

  resolve(did: string): DIDDocument | undefined {
    return this.documents.get(did);
  }

  verify(did: string, signature: string, payload: string): boolean {
    const doc = this.documents.get(did);
    if (!doc) return false;

    const authKeyIds = doc.authentication;
    const authKeys = doc.verificationMethod.filter((vm) =>
      authKeyIds.includes(vm.id)
    );

    for (const key of authKeys) {
      const publicKey = crypto.createPublicKey({
        key: Buffer.from(key.publicKeyDer, "base64"),
        format: "der",
        type: "spki",
      });
      const isValid = crypto.verify(
        null,
        Buffer.from(payload),
        publicKey,
        Buffer.from(signature, "hex")
      );
      if (isValid) return true;
    }
    return false;
  }

  requiresHumanAuth(did: string, operationKeyId: string): boolean {
    const doc = this.documents.get(did);
    if (!doc) return false;
    return doc.humanAuthorization.includes(operationKeyId);
  }
}

function createIdentity(domain: string, agentName: string): AgentIdentity {
  const did = `did:wba:${domain}:agent:${agentName}`;
  const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");

  const publicKeyDer = publicKey
    .export({ format: "der", type: "spki" })
    .toString("base64");

  const keyId = `${did}#key-1`;
  const encKeyId = `${did}#key-x25519-1`;

  const document: DIDDocument = {
    id: did,
    verificationMethod: [
      {
        id: keyId,
        type: "Ed25519VerificationKey2020",
        controller: did,
        publicKeyDer,
      },
      {
        id: encKeyId,
        type: "X25519KeyAgreementKey2019",
        controller: did,
        publicKeyDer,
      },
    ],
    authentication: [keyId],
    keyAgreement: [encKeyId],
    humanAuthorization: [],
    service: [
      {
        id: `${did}#agent-description`,
        type: "AgentDescription",
        serviceEndpoint: `https://${domain}/agents/${agentName}/ad.json`,
      },
    ],
  };

  return { did, document, privateKey, publicKey };
}

function signPayload(identity: AgentIdentity, payload: string): string {
  return crypto
    .sign(null, Buffer.from(payload), identity.privateKey)
    .toString("hex");
}

이 코드는 실제 ANP 신원 모델을 그대로 흉내 냅니다. 에이전트는 인증(authentication), 키 합의(key agreement), 사람 승인(human authorization) 키가 분리된 DID 문서를 가집니다. IdentityRegistry는 DID 해석을 시뮬레이션합니다. 실제 환경에서는 에이전트 도메인에 HTTP로 요청을 보내 DID 문서를 가져옵니다.

Step 6: 프로토콜 게이트웨이

이제 네 가지 프로토콜을 하나의 통합된 시스템으로 묶습니다.

graph LR
    REQ[Incoming Request] --> ANP_V{ANP: Verify DID}
    ANP_V -->|Valid| A2A_D{A2A: Discover Agent}
    ANP_V -->|Invalid| REJECT[Reject]
    A2A_D -->|Found| ACP_A[ACP: Audit Run]
    A2A_D -->|Not Found| REJECT
    ACP_A --> A2A_T[A2A: Create Task]
    A2A_T --> RESULT[Task + Audit Entry]

    style ANP_V fill:#d1fae5,stroke:#059669
    style A2A_D fill:#dbeafe,stroke:#2563eb
    style ACP_A fill:#fef3c7,stroke:#d97706
    style A2A_T fill:#dbeafe,stroke:#2563eb
class ProtocolGateway {
  private registry: AgentRegistry;
  private taskManager: TaskManager;
  private auditRunner: AuditableRunner;
  private identityRegistry: IdentityRegistry;

  constructor(
    registry: AgentRegistry,
    taskManager: TaskManager,
    auditRunner: AuditableRunner,
    identityRegistry: IdentityRegistry
  ) {
    this.registry = registry;
    this.taskManager = taskManager;
    this.auditRunner = auditRunner;
    this.identityRegistry = identityRegistry;
  }

  async delegateTask(
    fromDid: string,
    signature: string,
    targetAgent: string,
    message: AgentMessage,
    sessionId?: string
  ): Promise<{ task: Task; audit: AuditEntry } | { error: string }> {
    if (!this.identityRegistry.verify(fromDid, signature, message.id)) {
      return { error: "Identity verification failed" };
    }

    const card = this.registry.resolve(targetAgent);
    if (!card) {
      return { error: `Agent ${targetAgent} not found in registry` };
    }

    const audit = await this.auditRunner.run(
      targetAgent,
      [message],
      sessionId
    );
    const task = await this.taskManager.sendMessage(targetAgent, message);

    return { task, audit };
  }

  discoverAndDelegate(
    fromDid: string,
    signature: string,
    skillTag: string,
    message: AgentMessage
  ): Promise<{ task: Task; audit: AuditEntry } | { error: string }> {
    const candidates = this.registry.discoverBySkillTag(skillTag);
    if (candidates.length === 0) {
      return Promise.resolve({
        error: `No agents found with skill tag: ${skillTag}`,
      });
    }
    return this.delegateTask(
      fromDid,
      signature,
      candidates[0].name,
      message
    );
  }
}

게이트웨이는 한 번의 호출 안에서 네 가지 일을 합니다.

  1. ANP: DID 서명으로 호출자의 신원을 검증합니다.
  2. A2A: 대상 에이전트를 발견하고 역량을 확인합니다.
  3. ACP: 실행을 궤적이 담긴 감사 추적으로 감쌉니다.
  4. A2A: 전체 생명주기 추적이 붙은 과제(task)를 생성합니다.

Step 7: 전체 연결

async function protocolDemo() {
  const registry = new AgentRegistry();
  registry.register({
    name: "researcher",
    description: "Searches and summarizes findings",
    version: "1.0.0",
    url: "https://researcher.local/a2a/v1",
    capabilities: { streaming: true, pushNotifications: false },
    defaultInputModes: ["text/plain"],
    defaultOutputModes: ["text/plain", "application/json"],
    skills: [
      {
        id: "web-research",
        name: "Web Research",
        description: "Searches the web",
        tags: ["research", "search", "summarization"],
        inputModes: ["text/plain"],
        outputModes: ["application/json"],
      },
    ],
  });
  registry.register({
    name: "coder",
    description: "Writes code from specs",
    version: "1.0.0",
    url: "https://coder.local/a2a/v1",
    capabilities: { streaming: false, pushNotifications: false },
    defaultInputModes: ["text/plain", "application/json"],
    defaultOutputModes: ["text/plain"],
    skills: [
      {
        id: "code-gen",
        name: "Code Generation",
        description: "Generates code",
        tags: ["coding", "generation"],
        inputModes: ["text/plain", "application/json"],
        outputModes: ["text/plain"],
      },
    ],
  });

  const taskManager = new TaskManager();
  const auditRunner = new AuditableRunner();

  const researchTrajectory: TrajectoryEntry[] = [];

  taskManager.registerHandler(
    "researcher",
    async function* (task, message) {
      yield {
        kind: "statusUpdate" as const,
        taskId: task.id,
        status: { state: "working" as const, timestamp: Date.now() },
      };

      researchTrajectory.push({
        reasoning: "Searching for React 19 documentation",
        toolName: "web_search",
        toolInput: { query: "React 19 compiler features" },
        toolOutput: {
          results: ["react.dev/blog/react-19", "github.com/react/react"],
        },
        timestamp: Date.now(),
      });

      researchTrajectory.push({
        reasoning: "Extracting key findings from search results",
        toolName: "doc_analysis",
        toolInput: { url: "react.dev/blog/react-19" },
        toolOutput: {
          summary:
            "React 19 compiler auto-memoizes, no manual useMemo needed",
        },
        timestamp: Date.now(),
      });

      yield {
        kind: "artifactUpdate" as const,
        taskId: task.id,
        artifact: {
          id: crypto.randomUUID(),
          name: "research-results",
          parts: [
            {
              kind: "data" as const,
              data: {
                findings: [
                  "React 19 compiler auto-memoizes components",
                  "No more manual useMemo/useCallback needed",
                  "Compiler runs at build time, not runtime",
                ],
                sources: ["react.dev/blog/react-19"],
              },
              mediaType: "application/json",
            },
          ],
        },
        append: false,
        lastChunk: true,
      };

      yield {
        kind: "statusUpdate" as const,
        taskId: task.id,
        status: { state: "completed" as const, timestamp: Date.now() },
      };
    }
  );

  auditRunner.registerAgent("researcher", async () => ({
    output: [
      textMessage("agent", "React 19 compiler auto-memoizes components"),
    ],
    trajectory: researchTrajectory,
  }));

  const identityRegistry = new IdentityRegistry();

  const coderIdentity = createIdentity("coder.local", "coder");
  const researcherIdentity = createIdentity("researcher.local", "researcher");

  identityRegistry.publish(coderIdentity.document);
  identityRegistry.publish(researcherIdentity.document);

  const gateway = new ProtocolGateway(
    registry,
    taskManager,
    auditRunner,
    identityRegistry
  );

  console.log("=== Protocol Demo ===\n");

  console.log("1. Agent Discovery (A2A)");
  const researchAgents = registry.discoverBySkillTag("research");
  console.log(
    `   Found ${researchAgents.length} agent(s):`,
    researchAgents.map((a) => a.name)
  );

  console.log("\n2. Identity Verification (ANP)");
  const message = textMessage("user", "Research React 19 compiler features");
  const signature = signPayload(coderIdentity, message.id);
  const verified = identityRegistry.verify(
    coderIdentity.did,
    signature,
    message.id
  );
  console.log(`   Coder DID: ${coderIdentity.did}`);
  console.log(`   Signature verified: ${verified}`);

  console.log("\n3. Task Delegation (A2A + ACP + ANP)");
  const result = await gateway.delegateTask(
    coderIdentity.did,
    signature,
    "researcher",
    message,
    "session-001"
  );

  if ("error" in result) {
    console.log(`   Error: ${result.error}`);
    return;
  }

  console.log(`   Task ID: ${result.task.id}`);
  console.log(`   Task state: ${result.task.status.state}`);
  console.log(`   Artifacts: ${result.task.artifacts.length}`);

  console.log("\n4. Audit Trail (ACP)");
  console.log(`   Run ID: ${result.audit.runId}`);
  console.log(`   Status: ${result.audit.status}`);
  console.log(`   Trajectory steps: ${result.audit.trajectory.length}`);
  for (const step of result.audit.trajectory) {
    console.log(`     - ${step.reasoning}`);
    if (step.toolName) {
      console.log(`       Tool: ${step.toolName}`);
    }
  }

  console.log("\n5. Full Audit Log");
  const fullLog = auditRunner.getFullAuditLog();
  console.log(`   Total runs: ${fullLog.length}`);
  for (const entry of fullLog) {
    const duration = entry.completedAt
      ? `${entry.completedAt - entry.startedAt}ms`
      : "in-progress";
    console.log(`   ${entry.agentName}: ${entry.status} (${duration})`);
  }
}

protocolDemo().catch((err) => {
  console.error("Protocol demo failed:", err);
  process.exitCode = 1;
});

이 데모는 A2A 방식의 에이전트 발견, ANP 방식의 DID 서명 검증, A2A와 ACP, ANP를 함께 사용한 과제 위임, ACP 감사 추적 출력, 전체 감사 로그 조회를 차례로 보여줍니다. 실제 동작 가능한 전체 구현은 code/main.ts에서 확인할 수 있습니다.

무엇이 잘못되는가

프로토콜은 정상 경로(happy path)만 해결합니다. 실제 운영 환경에서는 다음과 같은 일들이 깨집니다.

스키마 드리프트(Schema drift): 에이전트 A가 Agent Card에 application/json 출력을 광고하지만, JSON 스키마가 버전이 바뀌면서 함께 달라집니다. 에이전트 B는 예전 형식을 파싱하다가 쓰레기 값을 받게 됩니다. 해결책은 스킬과 출력 스키마에 버전(version)을 명시하는 것입니다. A2A 사양이 Agent Card에 version을 둔 이유가 바로 여기에 있습니다.

상태 머신 위반(State machine violations): 에이전트 핸들러가 completed 이벤트를 낸 뒤 산출물(artifact)을 더 내보내려고 합니다. 그러나 과제는 이미 불변이 되었습니다. 코드는 조용히 업데이트를 버리거나 예외를 던지게 됩니다. 해결책은 yield 하기 전에 종료 상태인지 확인하는 것입니다. 위에서 만든 TaskManager는 종료 상태에 도달하면 break로 이를 강제합니다.

신뢰 검증 실패(Trust resolution failures): 에이전트 A가 에이전트 B의 DID를 검증하려는데, 에이전트 B의 도메인이 다운되어 있습니다. DID 문서를 가져올 수 없는 상황입니다. 이때 통과시킬 것인가(fail open), 막을 것인가(fail closed)? ANP는 최소 신뢰 원칙에 따라 막는 쪽(fail closed)을 권합니다.

궤적 로그 비대화(Trajectory bloat): ACP의 궤적 로깅은 강력하지만 비용이 큽니다. 한 번 실행할 때 도구를 200번 호출하는 복잡한 에이전트는 거대한 감사 항목을 만들어 냅니다. 해결책은 설정 가능한 상세 수준(verbosity level)으로 궤적을 기록하는 것입니다. 규제 준수를 위해 도구 이름과 입출력은 기록하되, 규제 대상이 아닌 워크로드에서는 추론 단계를 생략할 수 있습니다.

발견 시 트래픽 폭주(Discovery thundering herd): 시스템 시작 시 50개 에이전트가 동시에 GET /agents를 호출합니다. 해결책은 Agent Card를 TTL과 함께 캐싱하고, 발견 호출 간격을 엇갈리게 하거나, 폴링 대신 푸시 기반 등록을 사용하는 것입니다.

사용해보기

실제 구현체

A2A가 가장 성숙합니다. Google의 공식 사양은 Linux Foundation 아래에서 오픈소스로 공개되어 있고, Python과 TypeScript SDK가 제공됩니다. 에이전트가 동적 발견과 협업을 해야 한다면 여기서 시작합니다.

ACP는 A2A로 병합되는 중입니다. IBM의 BeeAI 프로젝트는 REST 우선 대안으로 ACP를 만들었지만, 궤적 메타데이터 개념은 A2A 생태계로 흡수되고 있습니다. 전송 계층으로 A2A를 사용하더라도 궤적 로깅과 실행(run) 생명주기 같은 ACP 패턴은 그대로 활용할 수 있습니다.

ANP가 가장 실험적입니다. 커뮤니티 저장소에는 Python SDK인 AgentConnect가 있습니다. 메타 프로토콜 협상이라는 개념은 실제로 새로운 시도입니다. 조직 간 에이전트 배포를 위해 지켜볼 가치가 있습니다.

MCP는 이미 Phase 13에서 다뤘습니다. 에이전트가 도구를 사용해야 한다면 MCP가 사실상의 표준입니다.

올바른 프로토콜 고르기

graph TD
    START{Do agents need<br/>to use tools?}
    START -->|Yes| MCP_R[Use MCP]
    START -->|No| TALK{Do agents need to<br/>talk to each other?}
    TALK -->|No| NONE[You don't need<br/>a protocol]
    TALK -->|Yes| AUDIT{Need audit trails<br/>for compliance?}
    AUDIT -->|Yes| ACP_R[A2A + ACP<br/>trajectory patterns]
    AUDIT -->|No| ORG{All agents<br/>within your org?}
    ORG -->|Yes| A2A_R[A2A<br/>Agent Cards + Tasks]
    ORG -->|No| INFRA{Shared<br/>infrastructure?}
    INFRA -->|Yes| BROKER[A2A + message broker]
    INFRA -->|No| ANP_R[ANP + A2A<br/>DID verification]

    style MCP_R fill:#d1fae5,stroke:#059669
    style A2A_R fill:#dbeafe,stroke:#2563eb
    style ACP_R fill:#fef3c7,stroke:#d97706
    style ANP_R fill:#f3e8ff,stroke:#7c3aed
    style BROKER fill:#e0e7ff,stroke:#4338ca

산출물 만들기

이 강의는 다음을 만듭니다.

  • code/main.ts -- 네 가지 프로토콜 패턴 전체 구현
  • outputs/prompt-protocol-selector.md -- 시스템에 맞는 프로토콜 선택을 돕는 프롬프트

연습문제

  1. 멀티홉 과제 위임(Multi-hop task delegation) — 어려움: 에이전트 핸들러가 다른 에이전트에게 하위 과제를 위임할 수 있도록 TaskManager를 확장하세요. 연구자(researcher)는 과제를 받은 뒤 "search"와 "summarize" 하위 과제를 두 전문 에이전트에게 위임하고, 둘 다 완료되기를 기다린 뒤 결과를 자기 산출물로 병합합니다.

  2. 스트리밍 감사 추적(Streaming audit trail) — 중간: AuditableRunner가 스트리밍 모드를 지원하도록 수정하세요. 전체 결과를 기다리는 대신, 궤적 항목(trajectory entry)이 추가될 때마다 실시간으로 AuditEntry 업데이트를 yield합니다. 감사 스냅샷을 생성하는 비동기 생성기(async generator)를 사용하세요.

  3. DID 회전(DID rotation) — 중간: IdentityRegistry에 키 회전(key rotation) 기능을 추가하세요. 에이전트는 새 키가 포함된 DID 문서를 게시하면서 previousDid 참조를 유지할 수 있어야 합니다. 검증자는 유예 기간(grace period) 동안 현재 키와 이전 키의 서명을 모두 받아들여야 합니다.

  4. 프로토콜 협상(Protocol negotiation) — 어려움: ANP의 메타 프로토콜 개념을 구현하세요. 두 에이전트가 후보 형식이 담긴 protocolNegotiation 메시지를 교환합니다. 예를 들어 "나는 JSON-RPC를 말할 수 있다"와 "나는 REST를 선호한다"를 주고받습니다. 최대 3라운드 안에 형식에 합의하거나 타임아웃됩니다. 합의된 형식은 어떤 TaskManagerAuditableRunner를 사용할지 결정합니다.

  5. 속도 제한이 있는 발견(Rate-limited discovery) — 중간: 설정 가능한 TTL로 Agent Card 조회를 캐시하고 에이전트별 초당 발견 질의를 제한하는 RateLimitedRegistry 래퍼를 추가하세요. 시작 시 서로를 발견하는 100개 에이전트의 트래픽 폭주(thundering herd) 상황을 시뮬레이션하고, 캐시 적용 전후의 차이를 측정해 보세요.

핵심 용어

용어흔한 설명실제 의미
MCP(Model Context Protocol)"AI 도구용 프로토콜"에이전트가 도구를 발견하고 사용할 수 있게 해 주는 클라이언트-서버 프로토콜입니다. 에이전트-도구 통신이지, 에이전트-에이전트 통신이 아닙니다.
A2A(Agent2Agent)"Google의 에이전트 프로토콜"Linux Foundation 아래에서 관리되는 피어 투 피어 에이전트 협업 프로토콜입니다. Agent Card로 발견하고, 9개의 상태를 가진 과제 생명주기를 정의하며, SSE 스트리밍을 지원합니다. JSON-RPC, REST, gRPC 바인딩을 모두 지원합니다.
ACP(Agent Communication Protocol)"기업용 에이전트 메시징"IBM/BeeAI가 만든 에이전트 실행(run) 단위 REST API입니다. TrajectoryMetadata를 통해 모든 응답에 추론과 도구 호출의 전체 사슬을 붙입니다. 현재는 A2A로 병합되는 중입니다.
ANP(Agent Network Protocol)"탈중앙 에이전트 신원"did:wba(DID)를 이용한 암호학적 신원, E2EE용 HPKE, 처음 만난 에이전트를 위한 AI 기반 메타 프로토콜 협상을 사용하는 커뮤니티 프로토콜입니다.
Agent Card"에이전트의 명함"/.well-known/agent-card.json에 게시되는 JSON 문서입니다. 스킬, 지원 MIME 타입, 보안 스킴, 프로토콜 바인딩을 설명합니다.
DID(Decentralized Identifier)"탈중앙 ID"에이전트 자신의 도메인에서 호스팅되는, 암호학적으로 검증 가능한 신원을 위한 W3C 표준입니다. ANP는 did:wba 방식을 사용합니다.
TrajectoryMetadata"감사 영수증"ACP가 모든 에이전트 응답에 추론 단계와 도구 호출, 그리고 입력과 출력을 함께 붙이는 메커니즘입니다.
메타 프로토콜(Meta-protocol)"에이전트들이 말하는 법을 협상함"ANP에서 에이전트가 자연어로 데이터 형식에 동적으로 합의하고, 이를 처리할 코드를 생성하는 접근입니다.
과제(Task)"작업 단위"제출부터 완료까지 작업을 추적하는 A2A의 상태가 있는 객체입니다. 종료 상태에 도달하면 더 이상 바뀌지 않습니다.

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

prompt-protocol-selector

Helps choose the right agent communication protocol (MCP, A2A, ACP, ANP) based on system requirements

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.A2A에서 Agent Card는 무엇이며 왜 중요한가요?

2.멀티 에이전트 시스템이 에이전트 간 단순 문자열 전달 대신 구조화된 통신 프로토콜을 필요로 하는 이유는 무엇인가요?

3.ACP(Agent Communication Protocol)는 A2A와 설계 목표가 어떻게 다른가요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다