Agent2Agent Protocol
Agent2Agent Protocol은 구글이 개발하고 리눅스 재단에 기증한 공개 표준으로, 서로 다른 배경을 가진 AI 에이전트들이 원활하게 소통하고 협업할 수 있도록 만들어진 '공용어'에 가까운 개념이다.
이 글에서는 A2A 프로토콜 핵심 개념과 실제 통신 예시를 구성하여 어떻게 동작하는지 살펴본다.
핵심 컨셉
A2A 프로토콜은 에이전트간 상호 운용성(Interoperability)1 을 확보하기 위해 만들어졌다. 각자 다른 전문 분야를 가진 에이전트들이 서로 작업을 위임하고, 정보를 교환하며, 행동을 조율하는 복잡한 멀티 에이전트를 활용한 애플리케이션 환경에서 사용된다.
A2A가 해결하고자 하는 주요 문제는 다음과 같다.
- 플랫폼 종속성 탈피: 특정 프레임워크나 벤더에 종속되지 않고, 어떤 기술로 만들어졌든 상관없이 에이전트 간의 협업을 가능하게 한다.
- 복잡한 워크플로우 지원: 하나의 거대한 목표를 위해 여러 에이전트가 하위 작업을 나누어 처리하는 등 복잡하고 긴밀한 협업을 지원한다.
- 불투명성(Opaque Execution): 에이전트들은 서로의 내부 로직, 메모리, 독점 기술을 공개할 필요 없이 상호작용할 수 있다.2 이는 각 에이전트의 보안과 지적 재산권을 보호하는 핵심 원칙으로 작용한다.
- 비동기 통신 지원: 보고서 생성처럼 오랜 시간이 걸리는 작업이나 중간에 사람의 개입이 필요한 시나리오를 자연스럽게 지원한다.3
이러한 목표를 달성하기 위해 A2A는 몇 가지 핵심적인 구성 요소를 정의한다.4
- Agent Card: 에이전트의 신분증이다. ID, 기능, 통신 주소(URL), 보유 기술, 인증 요구사항 등이 담긴 JSON 문서로, 클라이언트가 특정 에이전트를 발견하고 그 에이전트와 어떻게 상호작용해야 할지 이해하도록 돕는다.
{ "a2aVersion": "0.3.0", "agentId": "travel-planner-agent", "displayName": "Travel Planner", "url": "https://example.com/a2a", "capabilities": { "streaming": true }, "authentication": { "type": "oauth2" } }
- Task: 상태를 가지는 작업 단위다. 고유 ID를 가지며, 시작부터 완료될때까지의 생명주기를 가집니다. 오랜 시간이 걸리는 작업을 추적하고, 여러 번의 소통이 오가는 상호작용을 관리하는 역할을 한다.
{ "kind": "task", "id": "task-flight-booking-456", "contextId": "ctx-travel-fghij-67890", "status": { "state": "input-required" } }
- Message: 에이전트와 클라이언트 간에 한 번 오가는 대화다. "user" 또는 "agent" 역할을 가지며, 밑에 설명할 Part를 포함한다.
{ "messageId": "msg-user-001", "role": "user", "parts": [{ "kind": "text", "text": "제주도 가는 비행기 표 좀 예약해줘." }] }
- Part: 메시지와 아티팩트 내부에 담기는 콘텐츠의 기본 단위이다.. 텍스트(TextPart), 파일(FilePart), 구조화된 데이터(DataPart) 등이 있다.
{ "kind": "text", "text": "어디서 출발하시나요?" }
- Artifact: 작업 수행 중 에이전트가 만들어내는 구체적인 결과물이다. 문서, 이미지, 차트 등이 여기에 해당하며, 에이전트가 수행한 작업의 최종 산출물을 전달하는 컨테이너 역할을 한다.
{ "artifactId": "artifact-flight-ticket-123", "name": "flight_details", "parts": [{ "kind": "data", "data": { "flight": "KE123", "seat": "15A" } }] }
- Context: 여러 Task를 논리적으로 묶어주는 id로 대화 세션 id등으로 활용할 수 있다. 요청-응답간
contextId
속성을 통해 활용한다.
MCP와의 차이점
A2A를 이야기할 때 자주 함께 언급되는 것이 Model Context Protocol(MCP)다. 둘은 경쟁 관계가 아닌, 서로를 보완하는 상호 보완적인 관계다.5
- MCP (Agent-to-Tool): 에이전트가 자신의 도구와 소통하는 방법을 정의한다.. 여기서 도구란 데이터베이스, API, 계산기처럼 명확한 입출력을 가진 기능적인 요소다.
- A2A (Agent-to-Agent): 에이전트가 자신의 동료 에이전트와 소통하는 방법을 정의한다. 각자 독립적인 생각(추론, 계획)과 상태를 가진 에이전트들이 공동의 목표를 위해 협업하는 방법을 다룬다.
카센터로 비교하면 이렇다.
- 고객 → 매니저 에이전트(A2A 통신): 고객이 "차에서 덜컹거리는 소리가 나요"라고 카센터의 매니저 에이전트에게 A2A 프로토콜로 말을 건다.
- 매니저 에이전트 → 정비사 에이전트 (A2A 통신): 매니저 에이전트는 진단을 위해 정비사 에이전트에게 작업을 위임한다. 이 역시 A2A를 통해 이루어진다.
- 정비사 에이전트 → 진단 스캐너 (MCP 통신): 정비사 에이전트는 문제 원인을 파악하기 위해 차량 진단 스캐너라는 도구를 사용한다. 이때는 MCP를 통해 스캐너 API를 호출한다.
- 정비사 → 부품 공급사 에이전트 (A2A 통신): 진단 결과 특정 부품이 필요하다는 것을 알게 된 정비사 에이전트는 부품 공급사 에이전트에게 필요한 부품을 주문한다. 이것 또한 A2A를 통한 협업이다
A2A 프로토콜의 주요 동작 (Methods)
A2A 통신은 HTTP(S) 기반의 JSON-RPC 2.0을 기본 페이로드 형식으로 사용한다.6 클라이언트에서 요청을 다음과 같은 형태로 보내는 것이 기본이다. REST나 grpc도 지원한다.
{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {method에 적합한 param}
}
클라이언트는 특정 동작을 수행하기 위해 정의된 메서드를 호출할 수 있다. 주요 메서드는 다음과 같다.7
message/send
: 에이전트에게 메시지를 보내 새로운 상호작용을 시작하거나 기존 상호작용을 이어갈 때 사용한다.message/stream
: 에이전트에게 메시지를 보내 작업을 시작하고, Server-Sent Events (SSE)를 통해 해당 작업에 대한 실시간 업데이트를 구독한다.tasks/get
: 이전에 시작된 작업의 현재 상태(상태, 아티팩트 등)를 조회한다.message/send
로 시작된 비동기 작업의 진행 상황을 폴링(polling)하는 데 사용될 수 있다.tasks/cancel
: 현재 진행 중인 작업을 취소하도록 요청한다.tasks/resubscribe
: 이전에 구독했던 SSE 스트림 연결이 끊어졌을 경우, 다시 연결하여 업데이트를 계속 받기 위해 사용한다.tasks/pushNotificationConfig/*
: 지정된 작업에 대한 푸시 알림(웹훅) 구성을 관리(set, get, list, delete)한다. 클라이언트가 오프라인 상태여도 업데이트를 받을 수 있도록 서버에 알림을 받을 주소를 등록할 때 사용된다.
A2A 통신 예시 1 - Agent Discovery, 대화 시작하기
이제 실제 통신 예시를 통해 A2A가 어떻게 동작하는지 살펴보겠다.
본 예시에서는 Agent Discovery(에이전트 발견)와 Task, Context를 중심으로 대화가 어떻게 시작되고 이어지는지 알아본다.
Agent Discovery
사람들이 처음 만나면 명함을 교환하듯, 에이전트들은 Agent Card를 통해 서로를 알아간다. 클라이언트 에이전트는 협업할 상대를 찾아야 할 때 다음과 같은 방법으로 Agent Card를 발견할 수 있다.8
- Well-Known URI: 공개적으로 알려진 에이전트의 경우,
https://{도메인}/.well-known/agent-card.json
과 같은 표준화된 주소로 Agent Card를 게시한다. - Curated Registries (중앙 레지스트리): 기업 환경에서는 에이전트 카드를 중앙에서 관리하는 레지스트리(카탈로그)를 운영할 수 있다. 클라이언트는 "이미지 생성 스킬을 가진 에이전트"와 같이 필요한 역량을 기준으로 검색하여 적합한 에이전트를 찾을 수 있다.
- Direct Configuration (직접 설정): 이미 협업할 에이전트의 정보를 알고 있을 때, 해당 에이전트의 Agent Card URL을 직접 설정하여 통신한다.
통신 예시 - Agent 확인과 대화 시작
클라이언트가 creative-agent.com
이라는 도메인을 가진 이미지 생성 에이전트와 대화하고 싶어하는 상황이다. 클라이언트는 먼저 표준 주소(.well-known/agent-card.json
)로 GET 요청을 보내 에이전트의 '명함'을 요청한다.
GET /.well-known/agent-card.json HTTP/1.1
Host: creative-agent.com
서버는 자신의 정보가 담긴 Agent Card를 JSON 형태로 응답한다. 여기서 핵심은 실제 통신에 사용될 url
필드이다.
// HTTP 200 OK
{
"a2aVersion": "0.3.0",
"agentId": "creative-cloud-agent-prod",
"displayName": "Creative Agent",
"url": "https://api.creative-agent.com/a2a/v1",
"authentication": { "type": "oauth2" },
"capabilities": {
"methods": ["message/send", "message/stream", "tasks/get", "tasks/cancel"]
}
}
클라이언트는 Agent Card에서 얻은 url (https://api.creative-agent.com/a2a/v1
)을 실제 엔드포인트로 사용하여 message/send 요청을 보낸다.
// HTTP POST -> https://api.creative-agent.com/a2a/v1
{
"jsonrpc": "2.0",
"id": "req-001",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{ "kind": "text", "text": "밤하늘을 나는 푸른 용을 그려줘." }]
}
}
}
이제 'Creative Agent'는 요청을 받아 작업을 처리하고, Task를 생성하여 응답한다.
// HTTP 200 OK
{
"jsonrpc": "2.0",
"id": "req-001",
"result": {
"kind": "task",
"id": "task-dragon-drawing-987",
"contextId": "ctx-creative-xyz-123",
"status": { "state": "completed" },
"artifacts": [
{
"artifactId": "artifact-dragon-image-456",
"parts": [
{
"kind": "file",
"file": {
"mimeType": "image/png",
"url": "https://cdn.creative-agent.com/images/dragon-456.png"
}
}
]
}
]
}
}
이 두 단계를 통해 클라이언트는 에이전트의 공개된 주소만으로 실제 통신 엔드포인트를 찾아 성공적으로 대화를 시작할 수 있다.
통신 예시 - Task 생성 없이 즉시 응답하기
모든 요청이 Task를 생성해야 하는 것은 아니다. 에이전트가 클라이언트의 요청을 상태 저장이 필요 없는 일회성 응답으로 즉시 처리할 수 있을 때는 kind: "message"
로 응답할 수 있다. 9
사용자가 에이전트에게 간단한 농담을 요청한다.
// HTTP POST -> message/send
{
"jsonrpc": "2.0",
"id": "req-joke-001",
"method": "message/send",
"params": {
"message": { "role": "user", "parts": [{ "kind": "text", "text": "농담 하나 해줘." }] }
}
}
에이전트는 이 요청을 별도의 상태 추적이 필요 없는 간단한 질의응답 으로 판단하고, Task를 생성하는 대신 Message 객체로 바로 응답한다. 농담 내용은 gemini한테 만들라고 시켰다.
// HTTP 200 OK
{
"jsonrpc": "2.0",
"id": "req-joke-001",
"result": {
"kind": "message",
"messageId": "msg-agent-joke-123",
"contextId": "ctx-conv-abcde-12345",
"role": "agent",
"parts": [
{
"kind": "text",
"text": "과학자들이 원자를 믿지 않는 이유가 뭔지 알아? 원자가 모든 걸 지어내기(make up) 때문이야!"
}
]
}
}
이 경우에는 Task를 생성하지 않는다. messageId
로 해당 응답 메시지만을 식별하며, 별도의 생명주기를 추적하지 않게 된다.
A2A 통신 예시 2 - 목적을 가진 대화
이번 예시에서는 Task의 생명주기와 상태 업데이트를 중심으로, 에이전트가 사용자와 정보를 주고받으며 복잡한 작업을 완료하는 과정을 JSON 요청/응답과 함께 살펴보겠다.
Task Status
A2A의 Task는 일 처리 과정을 나타내는 명확한 상태(Status)를 가진다.10
submitted
: 요청이 접수되었고, 실행을 대기 중인 상태working
: 작업이 활발하게 처리되고 있는 상태input-required
: 작업 진행을 위해 사용자(클라이언트)의 추가 정보가 필요하여 일시 중지된 상태auth-required
: 인증이 필요하여 대기 중인 상태completed
: 작업이 성공적으로 완료된 최종 상태canceled
: 작업이 중간에 취소된 최종 상태failed
: 오류로 인해 작업이 실패한 최종 상태rejected
: 에이전트가 작업을 수행하지 않기로 결정한 최종 상태
통신 예시 - 항공권 예약하기
사용자가 '여행 플래너' 에이전트에게 목적지만 알려주며 모호한 질문으로 항공권 예약을 요청한다.
// HTTP POST -> message/send
{
"jsonrpc": "2.0",
"id": "req-003",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{ "kind": "text", "text": "제주도 가는 비행기 표 좀 예약해줘." }]
}
}
}
에이전트는 목적지만으로는 예약을 진행할 수 없다. 따라서 Task의 상태를 input-required
로 변경하고, 사용자에게 필요한 정보를 되묻는 메시지를 보낸다.
// HTTP 200 OK
{
"jsonrpc": "2.0",
"id": "req-003",
"result": {
"kind": "task",
"id": "task-flight-booking-456",
"status": {
"state": "input-required",
"message": {
"role": "agent",
"parts": [
{
"kind": "text",
"text": "네, 도와드릴게요! 어디서 출발하시나요? 그리고 원하시는 여행 날짜를 알려주시겠어요?"
}
]
}
}
}
}
사용자가 에이전트의 질문에 답변한다. 이전 contextId