Содержание
- Что такое A2A и какую задачу закрывает протокол
- Архитектура A2A и роль Agent Card
- Как устроена Agent Card
- Три транспортных биндинга протокола
- Принцип работы и жизненный цикл задачи
- Три способа получать обновления по задаче
- Чем A2A отличается от MCP
- Что изменилось в версии 1.0 и где спотыкаются на практике
- Когда A2A нужен, а когда избыточен
- Практика с двумя агентами на a2a-sdk
- Заключение
- Референсные ссылки
A2A (Agent2Agent Protocol) это открытый протокол, по которому ИИ-агенты общаются друг с другом: находят подходящего партнёра, передают ему задачу и забирают результат. Он отвечает не за доступ агента к базе или внешнему API. Тема другая: как две независимые агентные системы договариваются о работе. Разные фреймворки и разные вендоры этому не мешают.
Что такое A2A и какую задачу закрывает протокол
Протокол A2A относится к классу протоколов межагентного взаимодействия и работает поверх обычного HTTP. Разработку начала Google. В июне 2025 года проект передан в Linux Foundation. К 2026 году спецификация дошла до стабильной версии 1.0, а вокруг неё собралось больше 150 организаций, среди них Microsoft, Salesforce и ServiceNow. Формально это набор из трёх слоёв: канонической модели данных, абстрактных операций и конкретных транспортных биндингов.
Задача, которую решает протокол, скучная и очень практическая. Когда в компании появляется десяток агентов от разных команд, связывать их попарно самописными интеграциями бессмысленно. Каждый новый агент умножает количество связей. A2A задаёт общий словарь, чтобы агент мог обратиться к незнакомому агенту так же, как браузер обращается к незнакомому сайту.
Ключевых принципов у протокола несколько, и они объясняют большинство его решений.
- Непрозрачность исполнения. Агенты работают друг с другом по объявленным возможностям и обмениваются только сообщениями. Внутренние промпты, память, инструменты и цепочки рассуждений наружу не отдаются.
- Повторное использование стандартов. HTTP, JSON-RPC 2.0, Server-Sent Events, OAuth 2.0. Ничего нового изобретать не пришлось.
- Асинхронность по умолчанию. Задача может выполняться минуты, часы и дни, в том числе с участием человека, и протокол это учитывает.
- Независимость от модальности. В обмене участвуют текст, файлы и структурированные данные, а не только строки.
Из этих принципов вырастает вся остальная механика, начиная с того, как агент вообще узнаёт о существовании другого агента.
Архитектура A2A и роль Agent Card
В обмене участвуют три стороны. Пользователь ставит цель. Клиентский агент (A2A Client) действует от его имени и шлёт запросы. Удалённый агент (A2A Server, Remote Agent) выставляет наружу HTTP-эндпоинт и выполняет работу. Для клиента удалённый агент остаётся чёрным ящиком: видно, что он умеет и что вернул, но не видно, как именно он это сделал.
Точка входа во всю эту конструкцию одна и называется Agent Card.
Как устроена Agent Card
Agent Card это JSON-документ, визитка агента. Рекомендованный способ публикации по спецификации это well-known адрес /.well-known/agent-card.json на домене агента, по правилам RFC 8615. Кроме well-known адреса спецификация упоминает курируемые реестры и прямую конфигурацию клиента, но стандартного API для реестров пока нет. Вот реальная карточка агента из демо к этой статье, отданная сервером на GET-запрос.
# вывод прогона, протокол A2A 1.0, a2a-sdk 1.1.2, 12.08.2026
{
"name": "BigDataSchool Course Agent",
"description": "Консультирует по курсам учебного центра BigDataSchool",
"supportedInterfaces": [
{
"url": "http://127.0.0.1:41241/a2a/jsonrpc/",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
],
"version": "1.0.0",
"capabilities": {
"streaming": true
},
"defaultInputModes": [
"text/plain"
],
"defaultOutputModes": [
"text/plain"
],
"skills": [
{
"id": "course_info",
"name": "Справка по курсу",
"description": "Отвечает, о чём курс и сколько он длится",
"tags": [
"курсы",
"обучение"
],
"examples": [
"Про что курс AGENT?",
"Сколько длится MLOPS?"
]
}
]
}
По карточке клиент понимает четыре вещи: кто этот агент, куда стучаться, что он поддерживает и что умеет. Навыки (skills) описывают конкретные умения с примерами запросов. Блок capabilities объявляет опциональные возможности вроде потоковой выдачи и пуш-уведомлений. В supportedInterfaces перечислены доступные транспорты с адресами. Карточку можно подписать: в версии 1.0 появилась структура AgentCardSignature с JWS по RFC 7515, чтобы клиент мог убедиться, что визитка не подделана по дороге.
Три транспортных биндинга протокола
Спецификация описывает операции отдельно от способа их передачи, а затем даёт три готовых биндинга: JSON-RPC 2.0 поверх HTTP, gRPC и HTTP+JSON в стиле REST. Требование функциональной эквивалентности означает, что через любой из них доступны все операции с одинаковой семантикой. Выбор транспорта это вопрос инфраструктуры, а не возможностей. Версия протокола и запрошенные расширения передаются служебными параметрами A2A-Version и A2A-Extensions, для HTTP это обычные заголовки.
ИИ-агенты для оптимизации бизнес-процессов
Код курса
AGENT
Ближайшая дата курса
26 октября, 2026
Продолжительность
24 ак.часов
Стоимость обучения
66 000
Принцип работы и жизненный цикл задачи
Разговор начинается с операции SendMessage. Клиент отправляет сообщение (Message) с ролью и списком частей (Part), а агент отвечает одним из двух способов. Если запрос простой, возвращается просто ответное сообщение. Если работа требует отслеживания, создаётся задача (Task) с собственным идентификатором и жизненным циклом.
Задача проходит через состояния, имена которых в версии 1.0 пишутся заглавными буквами: 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. Достигнув терминального состояния, задача становится неизменяемой: перезапустить её нельзя, уточнение оформляется новой задачей в том же контексте.
Связность разговора держится на двух идентификаторах. Идентификатор задачи taskId генерирует только сервер, клиент своих значений не придумывает. Идентификатор контекста contextId объединяет несколько задач и сообщений в одну логическую сессию, и именно по нему агент собирает историю переписки. Результат работы приезжает не текстом в чате, а артефактом (Artifact) с именем и собственным идентификатором, поэтому его можно запросить позже отдельно от переписки.
Помимо отправки сообщений протокол описывает ещё несколько операций. GetTask возвращает текущее состояние, ListTasks с курсорной пагинацией показывает список задач, CancelTask пытается отменить работу, SubscribeToTask подписывает на обновления. Отдельно стоят четыре операции управления конфигурациями пуш-уведомлений и GetExtendedAgentCard для расширенной карточки после аутентификации.
Три способа получать обновления по задаче
Долгие задачи создают очевидную проблему: клиент не должен висеть на открытом соединении сутки. Протокол предлагает три механизма на выбор.
- Опрос. Клиент периодически дёргает GetTask и смотрит статус. Просто и работает везде.
- Потоковая выдача. Операция SendStreamingMessage держит поток событий через Server-Sent Events и присылает обновления статуса и куски артефактов по мере готовности.
- Пуш-уведомления. Агент сам делает HTTP POST на вебхук клиента, когда происходит значимое событие. Вариант для задач, которые считаются часами.
Важная деталь. Потоковая выдача и пуш-уведомления опциональны. Клиент обязан проверить их поддержку в блоке capabilities, иначе получит ошибку.
Чем A2A отличается от MCP
Эти два протокола постоянно ставят рядом, хотя решают разные задачи. Model Context Protocol подключает агента к инструментам и данным, то есть работает вертикально, вниз. A2A связывает агента с другим агентом, то есть работает горизонтально. Подробный разбор соседнего протокола есть в Wiki-статье про Model Context Protocol. Практический взгляд на его устройство даёт материал блога что такое MCP-протокол и почему он важен для LLM.
| Критерий | MCP | A2A |
|---|---|---|
| Что соединяет | Агента с инструментом или источником данных | Агента с другим агентом |
| Единица работы | Вызов инструмента, чтение ресурса | Задача с жизненным циклом |
| Модель партнёра | Прозрачная, схема инструмента известна | Непрозрачная, известны только объявленные навыки |
| Обнаружение | Список инструментов от сервера | Agent Card по well-known адресу или из реестра |
| Длительные операции | Не основной сценарий | Состояния задачи, поток событий, пуш-уведомления |
На практике оба протокола живут в одном агенте. Через MCP он берёт инструменты. Через A2A отдаёт свои умения соседям и сам просит у них помощи.
Разработка и внедрение ML-решений
Код курса
MLOPS
Ближайшая дата курса
9 ноября, 2026
Продолжительность
24 ак.часов
Стоимость обучения
66 000
Что изменилось в версии 1.0 и где спотыкаются на практике
Версия 1.0 это не косметическое обновление, а перекладка протокола на proto-first основу. Нормативным источником стал файл спецификации в формате Protocol Buffers, а канонической сериализацией ProtoJSON. Отсюда растут остальные изменения.
- Значения перечислений переименованы. Вместо строк вида completed теперь TASK_STATE_COMPLETED, вместо user теперь ROLE_USER.
- Часть сообщения стала плоской. Обёртки TextPart, FilePart и DataPart убраны, содержимое кладётся прямо в Part через поля text, raw, url и data. Поле-дискриминатор kind удалено.
- Адрес агента переехал. Вместо одного поля url в карточке появился список supportedInterfaces из объектов AgentInterface с транспортом, версией протокола и адресом.
- Появились подписанные карточки и мультиарендность. Один эндпоинт теперь может обслуживать несколько агентов, а карточку можно проверить криптографически.
Понимание этих изменений экономит время при чтении чужих примеров. Половина туториалов в сети написана под версию 0.3 и на 1.0 просто не запустится.
Отдельно стоит сказать про грабли, на которые я наступил при подготовке демо.
- Первая. Если в AgentInterface не указать версию протокола явно, карточка объявляется как 0.3. Ручной запрос без заголовка A2A-Version тогда падает с ошибкой -32009 про неподдерживаемую версию.
- Вторая. Клиент из SDK по умолчанию ждёт ответ недолго, а локальная модель думает десятки секунд. Без своего HTTP-клиента с увеличенным таймаутом прогон падает с A2AClientTimeoutError.
- Третья про порядок событий. Объект Task обязан уйти в поток первым, иначе SDK бросает InvalidAgentResponseError. Смешивать сообщения с событиями задачи в одном потоке тоже нельзя.
Когда A2A нужен, а когда избыточен
Протокол хорошо ложится на вполне конкретные сценарии и плохо на все остальные.
- Разные владельцы. Агенты написаны разными командами или вендорами, общего кода и общей памяти у них нет.
- Длинные задачи. Работа считается минутами и часами, требует согласования с человеком или дозапроса данных.
- Специализация. Один агент собирает ответ, привлекая узких специалистов, и не хочет знать, как они устроены внутри.
- Внешняя экспозиция. Агента нужно выставить наружу как сервис, с аутентификацией и понятным контрактом.
Если же все агенты живут в одном процессе и одном репозитории, A2A добавит HTTP-слой и сериализацию там, где хватило бы вызова функции. Для оркестрации внутри одного приложения ближе графовые фреймворки, например LangGraph. Собирать агентные системы целиком учат на курсе ИИ-агенты для оптимизации бизнес-процессов.
Практика с двумя агентами на a2a-sdk
По традиции весь код используемый в статье выкладываем на наш GitHub репозиторий
Демо состоит из двух файлов. Первый, course_agent.py, поднимает удалённого агента. Он публикует карточку, обслуживает JSON-RPC биндинг и отвечает по каталогу курсов, спрашивая модель qwen2.5:7b через Ollama.
# a2a-sdk 1.1.2 (протокол A2A 1.0), Python 3.12.13, Ollama 0.32.9, модель qwen2.5:7b
"""Удалённый агент A2A: отвечает на вопросы по курсам BigDataSchool.
Публикует Agent Card по адресу /.well-known/agent-card.json и обслуживает
JSON-RPC биндинг протокола A2A на /a2a/jsonrpc/.
"""
import uvicorn
from ollama import AsyncClient
from starlette.applications import Starlette
from a2a.helpers import (
get_message_text,
new_task_from_user_message,
new_text_artifact_update_event,
new_text_status_update_event,
)
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import (
AgentCapabilities,
AgentCard,
AgentInterface,
AgentSkill,
TaskState,
)
HOST = "127.0.0.1"
PORT = 41241
MODEL = "qwen2.5:7b"
# Мини-каталог, который агент держит у себя. Клиентскому агенту он не виден:
# в A2A удалённый агент остаётся чёрным ящиком.
CATALOG = """
AGENT - ИИ-агенты для оптимизации бизнес-процессов, 2 дня, про агентные системы и LLM.
MLOPS - Разработка и внедрение ML-решений, 3 дня, про промышленный цикл ML.
KAFKA - Apache Kafka: администрирование кластера, 3 дня, про эксплуатацию брокера.
"""
SYSTEM_PROMPT = (
"Ты консультант учебного центра. Отвечай строго по каталогу ниже, "
"на русском языке, одним абзацем не длиннее 40 слов. "
"Если курса в каталоге нет, так и скажи.\nКаталог:" + CATALOG
)
# Agent Card: паспорт агента, по которому клиент понимает, что агент умеет
# и куда стучаться. В версии 1.0 адрес живёт в supported_interfaces.
AGENT_CARD = AgentCard(
name="BigDataSchool Course Agent",
description="Консультирует по курсам учебного центра BigDataSchool",
version="1.0.0",
supported_interfaces=[
AgentInterface(
protocol_binding="JSONRPC",
protocol_version="1.0",
url=f"http://{HOST}:{PORT}/a2a/jsonrpc/",
)
],
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
capabilities=AgentCapabilities(streaming=True),
skills=[
AgentSkill(
id="course_info",
name="Справка по курсу",
description="Отвечает, о чём курс и сколько он длится",
tags=["курсы", "обучение"],
examples=["Про что курс AGENT?", "Сколько длится MLOPS?"],
)
],
)
class CourseAgentExecutor(AgentExecutor):
"""Исполнитель задачи: переводит запрос A2A в вызов локальной LLM."""
def __init__(self) -> None:
self._llm = AsyncClient()
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
# Шаг 1. Заводим Task. По правилам A2A 1.0 объект Task обязан уйти
# в очередь событий первым, иначе SDK бросит InvalidAgentResponseError.
task = context.current_task or new_task_from_user_message(context.message)
await event_queue.enqueue_event(task)
# Шаг 2. Сообщаем клиенту, что взяли работу в обработку.
await event_queue.enqueue_event(
new_text_status_update_event(
task_id=task.id,
context_id=task.context_id,
state=TaskState.TASK_STATE_WORKING,
text="Смотрю каталог курсов",
)
)
# Шаг 3. Собственно работа. Что тут внутри, клиенту знать не нужно.
question = get_message_text(context.message)
response = await self._llm.chat(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": question},
],
options={"temperature": 0.1},
)
answer = response["message"]["content"].strip()
# Шаг 4. Результат работы уезжает артефактом, а не просто текстом
# в чате: артефакт привязан к задаче и его можно запросить позже.
await event_queue.enqueue_event(
new_text_artifact_update_event(
task_id=task.id,
context_id=task.context_id,
name="course_answer",
text=answer,
last_chunk=True,
)
)
# Шаг 5. Терминальное состояние закрывает жизненный цикл задачи.
await event_queue.enqueue_event(
new_text_status_update_event(
task_id=task.id,
context_id=task.context_id,
state=TaskState.TASK_STATE_COMPLETED,
text="Готово",
)
)
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
# Отмена в демо не поддержана: спецификация разрешает вернуть ошибку.
raise NotImplementedError("Отмена задачи в этом агенте не реализована")
def build_app() -> Starlette:
handler = DefaultRequestHandler(
agent_executor=CourseAgentExecutor(),
task_store=InMemoryTaskStore(),
agent_card=AGENT_CARD,
)
routes = create_agent_card_routes(AGENT_CARD)
routes += create_jsonrpc_routes(handler, rpc_url="/a2a/jsonrpc/")
return Starlette(routes=routes)
if __name__ == "__main__":
uvicorn.run(build_app(), host=HOST, port=PORT, log_level="warning")
Второй файл, client_agent.py, играет роль клиентского агента. Он скачивает карточку по well-known адресу, собирает по ней клиента и делегирует задачу, разбирая приходящий поток событий.
# a2a-sdk 1.1.2 (протокол A2A 1.0), Python 3.12.13, прогон против course_agent.py
"""Клиентский агент A2A: находит удалённого агента по Agent Card и делегирует ему задачу."""
import asyncio
import httpx
from a2a.client import A2ACardResolver, ClientConfig, create_client
from a2a.helpers import get_stream_response_text, new_text_message
from a2a.types import Role, SendMessageRequest, TaskState
AGENT_BASE_URL = "http://127.0.0.1:41241"
QUESTION = "Про что курс AGENT и сколько он длится?"
async def main() -> None:
async with httpx.AsyncClient(timeout=120) as http:
# Шаг 1. Discovery: карточка агента лежит по well-known адресу
# /.well-known/agent-card.json, скачивается обычным GET-запросом.
resolver = A2ACardResolver(httpx_client=http, base_url=AGENT_BASE_URL)
card = await resolver.get_agent_card()
print("=== Agent Card ===")
print("name:", card.name)
print("version:", card.version)
for iface in card.supported_interfaces:
print("interface:", iface.protocol_binding, iface.url)
print("streaming:", card.capabilities.streaming)
for skill in card.skills:
print("skill:", skill.id, "|", skill.name)
# Шаг 2. Клиент собирается по карточке: транспорт и адрес берутся из неё,
# руками URL метода никто не склеивает.
client = await create_client(card, ClientConfig(httpx_client=http))
# Шаг 3. Отправка сообщения. Ответом приходит поток событий: сначала Task,
# затем обновления статуса и артефакты.
request = SendMessageRequest(
message=new_text_message(QUESTION, role=Role.ROLE_USER)
)
print("\n=== Поток событий ===")
answer = ""
async for chunk in client.send_message(request):
if chunk.HasField("task"):
print(f"task: id={chunk.task.id} state={TaskState.Name(chunk.task.status.state)}")
elif chunk.HasField("status_update"):
state = TaskState.Name(chunk.status_update.status.state)
print(f"status_update: {state} | {get_stream_response_text(chunk)}")
elif chunk.HasField("artifact_update"):
answer = get_stream_response_text(chunk)
print(f"artifact_update: name={chunk.artifact_update.artifact.name}")
elif chunk.HasField("message"):
print("message:", get_stream_response_text(chunk))
print("\n=== Ответ удалённого агента ===")
print(answer)
if __name__ == "__main__":
asyncio.run(main())
Тот же агент вызывается и без SDK, голым HTTP-запросом. Это удобно, чтобы увидеть формат протокола своими глазами.
# прогон 12.08.2026, протокол A2A 1.0, заголовок версии обязателен
curl -s -X POST http://127.0.0.1:41241/a2a/jsonrpc/ \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{"jsonrpc":"2.0","id":"req-1","method":"SendMessage","params":{"message":{"role":"ROLE_USER","messageId":"msg-1","parts":[{"text":"Сколько длится курс MLOPS?"}]}}}'
Вывод клиентского агента укладывает механику протокола в четыре строки. Задача создана, перешла в работу, отдала артефакт и закрылась.
Заключение
A2A даёт агентным системам то, чего им не хватало: общий контракт на делегирование работы между независимыми агентами. Три вещи стоит запомнить. Точка входа это Agent Card по well-known адресу. Единица работы это задача с неизменяемым финальным состоянием и артефактами. Партнёр всегда остаётся чёрным ящиком. Это не ограничение, а осознанный выбор: агенты разных вендоров работают вместе, не раскрывая внутренностей.



