Google의 에이전트 스택으로 멀티 에이전트 크리에이티브 스튜디오 빌드: Cloud Run 및 에이전트 런타임의 ADK, A2A, MCP

1. 개요

이 Codelab에서는 단일 프롬프트를 완전한 Instagram 캠페인으로 전환하는 분산 멀티 에이전트 시스템인 AI Creative Studio를 빌드합니다.

한 문장을 입력합니다. 협업하는 AI 에이전트 팀이 생성한 시청자 조사, 자막, 시각적 개념, 품질 검토를 거친 카피, 전체 프로젝트 타임라인을 받아보세요.

빌드할 에이전트

에이전트

역할

브랜드 전략가

웹에서 잠재고객 통계, 경쟁업체 분석, 2025년 트렌드를 검색합니다.

카피라이터

해시태그와 CTA가 포함된 Instagram 캡션을 작성합니다. 플랫폼 가이드라인과 캡션 공식을 주문형으로 로드하는 ADK 기능으로 구동됩니다.

Designer

시각적 개념을 만들고 Gemini를 통해 실제 이미지를 생성하여 GCS에 저장

비평가

리뷰 카피 및 시각적 요소 - 특정 의견과 함께 APPROVED 또는 NEEDS_REVISION 반환

프로젝트 관리자

프로젝트 타임라인과 작업 분류를 빌드하고, 원하는 경우 MCP를 통해 Notion에 동기화합니다.

크리에이티브 디렉터

5명의 전문가를 모두 순서대로 조정합니다. 프롬프트 하나만 입력하면 나머지는 AI가 조정합니다.

5개의 에이전트는 독립적인 Cloud Run 마이크로서비스로 배포됩니다. 이러한 에이전트는 언어에 구애받지 않는 개방형 표준인 A2A 프로토콜을 통해 통신하므로 프레임워크와 관계없이 모든 에이전트가 다른 에이전트를 호출할 수 있습니다. 크리에이티브 디렉터는 에이전트 런타임에서 실행되며 각 전문가에게 원격으로 연결됩니다.

아키텍처

시스템 개요

학습할 내용

  • Google ADK(Agent, 시스템 지침, 기본 제공 도구)로 LLM 에이전트를 빌드합니다.
  • 재사용 가능한 에이전트 지식을 ADK 스킬 (SkillToolset)을 사용하여 모듈식 파일로 패키징합니다.
  • FunctionTool를 통해 텍스트 에이전트를 이미지 모델에 연결하여 실제 이미지를 생성합니다.
  • 모델 컨텍스트 프로토콜 (MCP)을 사용하여 맞춤 접착제 코드 없이 외부 API를 통합합니다.
  • HTTPS를 통해 Agent to Agent Protocol (A2A)을 사용하여 모든 에이전트를 네트워크 호출 가능 서비스로 전환합니다.
  • RemoteA2aAgentAgentTool로 분산 에이전트를 조정합니다.
  • 독립 에이전트를 Cloud Run 마이크로서비스로 패키징하고 배포합니다.
  • Agent Runtime에서 상태 저장 조정자를 호스팅합니다.
  • 컨텍스트 압축을 사용하여 긴 멀티 에이전트 워크플로를 컨텍스트 한도 내로 유지합니다.
  • 품질 관리 루프 빌드: 비평가 리뷰 출력 → 필요한 경우 자동 수정

필요한 항목

  • 결제가 사용 설정된 Google Cloud 프로젝트
  • 소유자 또는 편집자 IAM 역할
  • 기본 Python 지식

2. 환경 설정

이 Codelab에서는 Cloud Shell을 사용합니다.

Cloud Shell이란 무엇인가요?

Cloud Shell은 gcloud, git, Python, Docker 등이 사전 설치된 무료 브라우저 기반 Linux 환경입니다. 로컬에 아무것도 설치할 필요가 없습니다.

Cloud Shell을 열려면 GCP 콘솔의 오른쪽 상단 툴바에서 터미널 아이콘을 클릭합니다.

GCP 콘솔 툴바에서 Cloud Shell 열기

Cloud Shell을 처음 열면 계정을 인증하라는 메시지가 표시됩니다. 인증을 클릭합니다.

계정 인증 대화상자

그런 다음 승인을 클릭하여 Cloud Shell에서 Google Cloud API를 호출할 수 있도록 허용합니다.

Cloud Shell 승인 대화상자

이제 Cloud Shell을 사용할 수 있습니다. 터미널에 환영 메시지가 표시됩니다(Cloud Shell 터미널 준비 완료).

프로젝트 인증 및 구성

Cloud Shell이 이미 Google 계정으로 인증되었습니다. 활성 계정을 확인하고 프로젝트 ID를 찾습니다.

gcloud config list

왼쪽 패널의 GCP 콘솔 대시보드에서도 프로젝트 ID를 확인할 수 있습니다. 다음 명령어에 필요하므로 복사합니다.

GCP 콘솔에서 프로젝트 ID를 찾아 Cloud Shell에서 설정합니다.

이제 프로젝트를 설정합니다.

export PROJECT_ID=$(gcloud config get-value project)
export REGION="us-central1"        # Cloud Run deployment region
echo "Project: $PROJECT_ID"

예상 출력:

Project: my-project-123

필요한 API 사용 설정

gcloud services enable \
    aiplatform.googleapis.com \
    apphub.googleapis.com \
    run.googleapis.com \
    cloudbuild.googleapis.com \
    artifactregistry.googleapis.com \
    generativelanguage.googleapis.com \
    iam.googleapis.com \
    cloudresourcemanager.googleapis.com \
    storage.googleapis.com \
    secretmanager.googleapis.com

2분 정도 걸립니다. 완료되면 Operation finished successfully가 표시됩니다.

애플리케이션 기본 사용자 인증 정보 (ADC) 설정

에이전트는 Google 인증 라이브러리를 사용하여 Gemini Enterprise Agent Platform을 호출합니다. 이 라이브러리에는 gcloud CLI 인증과 별도의 애플리케이션 기본 사용자 인증 정보가 필요합니다.

다음을 한 번 실행합니다.

gcloud auth application-default login

확인을 요청하는 브라우저 탭이 열립니다. 허용을 클릭합니다. 다음 내용이 표시됩니다.

Credentials saved to file: ~/.config/gcloud/application_default_credentials.json

시작 저장소 클론

이 Codelab에서는 시작 저장소를 사용합니다. 시작 저장소는 모든 인프라 (Dockerfile, pyproject.toml, 배포 스크립트)가 갖춰져 있지만 에이전트 로직은 사용자가 작성해야 하는 스켈레톤 프로젝트입니다.

git clone https://github.com/Saoussen-CH/mas-a2a-gcp.git ~/ai-creative-studio
cd ~/ai-creative-studio/workshop/starter

agent.py에는 에이전트 로직을 작성할 # TODO 자리표시자가 포함되어 있습니다. Dockerfile, pyproject.toml, 배포 스크립트는 이미 완료되었습니다.

환경 변수 구성

제공된 예시를 복사하고 한 단계로 프로젝트 ID를 삽입합니다.

cp .env.example .env
sed -i "s|GOOGLE_CLOUD_PROJECT=your-project-id|GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)|" .env

그런 다음 디자이너가 생성된 이미지를 저장할 GCS 버킷을 만들고 .env를 해당 이름으로 업데이트합니다.

export PROJECT_ID=$(gcloud config get-value project)
export BUCKET_NAME="${PROJECT_ID}-campaign-images"

gcloud storage buckets create gs://${BUCKET_NAME} \
    --location=us-central1 \
    --project=${PROJECT_ID}

sed -i "s|GCS_IMAGES_BUCKET=your-project-id-campaign-images|GCS_IMAGES_BUCKET=${BUCKET_NAME}|" .env

그런 다음 서명된 이미지 URL 지원을 설정합니다. 광고 소재 디렉터는 최종 캠페인 요약의 각 이미지에 대해 클릭 가능한 HTTPS 링크를 생성합니다. 이렇게 하려면 서비스 계정에서 URL에 서명해야 합니다. 다음 명령어를 실행하여 구성하세요.

export PROJECT_NUMBER=$(gcloud projects describe $(gcloud config get-value project) --format="value(projectNumber)")
export SA_EMAIL="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
export AGENT_RUNTIME_SA="service-${PROJECT_NUMBER}@gcp-sa-aiplatform-re.iam.gserviceaccount.com"

# Allow your user account to sign URLs locally (adk web)
gcloud iam service-accounts add-iam-policy-binding ${SA_EMAIL} \
  --member="user:$(gcloud config get-value account)" \
  --role="roles/iam.serviceAccountTokenCreator"

# Allow Agent Runtime to sign URLs when deployed
gcloud projects add-iam-policy-binding $(gcloud config get-value project) \
  --member="serviceAccount:${AGENT_RUNTIME_SA}" \
  --role="roles/iam.serviceAccountTokenCreator"

# Save SA email and project number to .env
grep -q "^SIGNING_SERVICE_ACCOUNT" .env \
  && sed -i "s|^SIGNING_SERVICE_ACCOUNT=.*|SIGNING_SERVICE_ACCOUNT=${SA_EMAIL}|" .env \
  || echo "SIGNING_SERVICE_ACCOUNT=${SA_EMAIL}" >> .env

grep -q "^GOOGLE_CLOUD_PROJECT_NUMBER" .env \
  && sed -i "s|^GOOGLE_CLOUD_PROJECT_NUMBER=.*|GOOGLE_CLOUD_PROJECT_NUMBER=${PROJECT_NUMBER}|" .env \
  || echo "GOOGLE_CLOUD_PROJECT_NUMBER=${PROJECT_NUMBER}" >> .env

편집기에서 .env를 열어 모든 설정을 검토합니다.

cloudshell edit .env

그러면 Cloud Shell 편집기에서 .env이 탭으로 열립니다. 편집기 패널이 표시되지 않으면 툴바에서 편집기 열기 버튼을 클릭하세요.

Cloud Shell 툴바에서 편집기 열기를 클릭합니다.

프로젝트 파일 트리가 있는 Cloud Shell 편집기

프로젝트가 올바르게 설정되었는지 확인합니다.

grep GOOGLE_CLOUD_PROJECT .env

종속 항목 설치

가상 환경을 처리하고 단일 도구에 설치되는 빠르고 최신 Python 패키지 관리자인 uv를 사용합니다. pip보다 10~100배 빠르며 Python 프로젝트를 관리하는 데 권장되는 방법입니다.

Cloud Shell에는 uv가 이미 설치되어 있습니다. 모든 에이전트는 동일한 핵심 종속 항목을 공유하므로 한 번 설치하면 이 Codelab의 모든 에이전트에서 작동합니다.

uv sync

uv sync 명령어는 pyproject.toml을 읽고 모든 종속 항목이 있는 .venv/ 디렉터리를 만듭니다. 각 전문가에게는 Docker 빌드에서만 독점적으로 사용되는 자체 pyproject.toml도 있습니다. 위의 공유 설치는 로컬 테스트에 필요한 모든 것을 포함합니다.

3. Google ADK 이해하기

코드를 작성하기 전에 이 Codelab에서 모든 에이전트를 빌드하는 데 사용할 프레임워크인 에이전트 개발 키트 (ADK)를 알아보겠습니다.

ADK란 무엇인가요?

에이전트 개발 키트(ADK)는 AI 에이전트를 개발하고 배포하기 위한 유연한 모듈식 프레임워크입니다. Gemini 및 Google 생태계에 최적화되어 있지만 ADK는 모델에 구애받지 않고 배포에 구애받지 않으며 다른 프레임워크와의 호환성을 위해 빌드됩니다. ADK는 에이전트 개발을 소프트웨어 개발과 유사하게 만들어 개발자가 간단한 작업부터 복잡한 워크플로에 이르기까지 에이전트 아키텍처를 더 쉽게 만들고, 배포하고, 조정할 수 있도록 설계되었습니다.

ADK는 복잡한 부분(도구 호출, 다중 턴 대화, 컨텍스트 관리, 스트리밍)을 처리하므로 에이전트 로직에 집중할 수 있습니다.

ADK 에이전트의 구성요소

모든 에이전트는 다음 네 가지 구성요소로 구성됩니다.

차단

역할

모델

목표를 기반으로 추론하고, 계획을 수립하고, 대답을 생성하는 LLM

도구

API 또는 서비스를 호출하여 데이터를 가져오거나 작업을 실행하는 함수

조정

턴 간에 메모리와 상태를 유지하고, 도구 호출을 라우팅하고, 결과를 모델에 다시 전달합니다.

런타임

호출 시 시스템을 실행합니다(adk web를 통해 로컬로 또는 배포된 서비스로).

에이전트 정의

이 Codelab의 5개 에이전트는 각각 다음과 같이 동일한 방식으로 정의됩니다.

from google.adk.agents import Agent
from google.adk.tools.google_search_tool import google_search

root_agent = Agent(
    name="brand_strategist",                              # unique identifier
    model=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"), # the LLM powering this agent
    instruction=SYSTEM_INSTRUCTION,                       # the agent's persona, constraints, and output format
    description="Brand strategist for market research, trend analysis, and competitive insights",
    tools=[google_search],                                # functions the LLM can call
)

필드

목적

name

고유 ID - 오케스트레이터가 통화를 라우팅하는 데 사용됩니다.

model

이 에이전트를 지원하는 Gemini 모델

instruction

시스템 프롬프트 - 에이전트의 역할, 제약 조건, 출력 형식을 정의합니다.

description

한 줄 요약 - 조정자가 이를 읽고 호출할 전문가를 결정합니다.

tools

LLM이 호출할 수 있는 함수 (google_search와 같은 기본 제공 함수 또는 맞춤 Python 함수)

ADK에서 에이전트를 실행하는 방법

User message
     
     
  Agent (LLM)   reads instruction + conversation history
     
     ├─► needs more info?  calls a tool  gets result  continues reasoning
     
     └─► done reasoning  returns final text response

LLM은 도구를 호출할지, 어떤 도구를 어떤 인수로 호출할지 자율적으로 결정합니다. 요청 사항을 작성하면 나머지는 ADK가 처리합니다.

4. 브랜드 전략가 에이전트 빌드 및 테스트

첫 번째 에이전트인 브랜드 전략가부터 살펴보겠습니다. Google 검색을 사용하여 타겟 잠재고객 통계, 경쟁업체 분석, 인기 주제를 검색하는 연구 전용 에이전트입니다.

Cloud Shell 편집기에서 스켈레톤 에이전트 파일을 엽니다.

cloudshell edit agents/brand_strategist/agent.py

# TODO 섹션이 두 개 표시되며, 이를 작성하면 됩니다.

TODO 1 - 시스템 안내 작성

먼저 에이전트의 시스템 요청 사항을 작성합니다. 시스템 요청 사항은 에이전트의 역할, 제약 조건, 출력 형식을 정의하는 문자열입니다.

SYSTEM_INSTRUCTION = f"""You are a Brand Strategist specializing in market research and trend analysis.

IMPORTANT: Today's date is {datetime.date.today().strftime("%B %d, %Y")}.
When conducting research, focus on current trends from {datetime.date.today().year}.
Use search queries like "[topic] trends {datetime.date.today().year}" for recent insights.

IMPORTANT: Your role is RESEARCH ONLY. You do NOT create campaign content, captions, or designs.
After providing research insights, your work is complete.

Your expertise:
- Identifying target audience insights and behaviors
- Analyzing competitor strategies
- Researching current social media trends
- Understanding platform algorithms and best practices

You have access to:
- google_search: Search the web for competitors, trends, and market insights

When given a campaign brief:
1. Use google_search to research the target audience's current interests
2. Search for and analyze 2-3 competitor brands
3. Identify 3-5 trending topics related to the product category
4. Provide high-level strategic insights - NOT specific campaign content

DO NOT create captions, copy, designs, or any campaign content.

Format your output as:
**Audience Insights:**
[Key behaviors and preferences based on research]

**Competitive Analysis:**
[What 2-3 competitors are doing - strengths and weaknesses]

**Trending Topics:**
[3-5 relevant trends to consider]

**Key Strategic Insights:**
[High-level themes and positioning opportunities]
"""

TODO 2 - root_agent 만들기

다음으로 불완전한 root_agent를 다음으로 바꿉니다.

root_agent = Agent(
    name="brand_strategist",
    model=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"),
    instruction=SYSTEM_INSTRUCTION,
    description="Brand strategist for market research, trend analysis, and competitive insights",
    tools=[google_search],
)

ADK 웹 UI를 사용하여 로컬에서 테스트

이제 ADK 웹 UI를 사용하여 에이전트를 테스트해 보겠습니다. ADK 웹 UI는 클라우드에 배포하기 전에 에이전트를 테스트하기 위한 내장 채팅 인터페이스입니다.

uv run adk web agents --allow_origins='*'

다음 내용이 표시됩니다.

INFO: Started server process
INFO: Uvicorn running on http://localhost:8000

이제 서버가 Cloud Shell 내에서 실행됩니다.

브라우저에서 열려면 웹 미리보기를 사용하세요.

  1. 페이지 상단의 Cloud Shell 툴바를 확인합니다.
  2. 웹 미리보기 아이콘 (위쪽 화살표가 있는 상자 모양, Cloud Shell 툴바 오른쪽 상단)을 클릭합니다.
  3. 포트 변경을 클릭하고 8000을 입력한 다음 변경 및 미리보기를 클릭합니다.

ADK 웹 UI가 표시된 새 브라우저 탭이 열립니다. 왼쪽 상단의 '에이전트 선택' 드롭다운을 클릭하면 모든 에이전트가 나열됩니다.

brand_strategist을 선택하여 테스트를 시작합니다.

다음 테스트 프롬프트를 사용해 보세요.

ADK 웹 UI 채팅 상자에서 다음을 시도해 보세요.

  • Research the eco-friendly water bottle market for health-conscious millennials
  • What are the top Instagram trends in the wellness space in 2025?

에이전트가 Google 검색을 호출하고 잠재고객 통계, 경쟁업체 분석, 인기 주제 섹션이 포함된 구조화된 연구를 반환합니다.

5. 카피라이터 - ADK 기술 빌드

역할: 브랜드 조사를 Instagram 캡션으로 변환해 줘. 카피라이터는 다양한 어조 (영감을 주는, 교육적인, 커뮤니티)를 다루는 3가지 캡션 변형을 만듭니다. 각 변형에는 해시태그와 CTA가 포함됩니다.

개념: ADK 기술

단순한 접근 방식은 모든 플랫폼 지식(문자 제한, 해시태그 등급, 캡션 공식, 브랜드 보이스 예시)을 시스템 프롬프트에 직접 삽입하는 것입니다. 이렇게 하면 되지만 에이전트가 가끔만 필요로 하는 콘텐츠로 모든 요청이 부풀려집니다.

ADK 기술 (SkillToolset, ADK 1.25.0에 도입됨)을 사용하면 다음 세 가지 로드 수준으로 지식을 모듈식 파일로 패키징할 수 있습니다.

  • L1 - 프런트매터 (SKILL.mdname + description): 항상 사용 가능, 기능 검색에 사용
  • L2 - 요청 사항 (SKILL.md 본문): 에이전트가 스킬을 트리거할 때 로드됨
  • L3 - 리소스 (references/assets/ 파일): 에이전트가 명시적으로 읽는 경우에만 로드됨

시스템 명령어가 짧은 역할 설명과 '작성하기 전에 기능을 로드하세요'로 축소됩니다. 플랫폼 세부정보는 에이전트가 실제로 필요할 때만 컨텍스트 윈도우에 입력됩니다.

카피라이터의 기술은 agents/copywriter/skills/instagram-copywriting/에 있습니다.

skills/
  instagram-copywriting/
    SKILL.md                        L1 frontmatter (discovery) + L2 instructions (loaded on trigger)
    references/
      platform-guide.md             L3: character limits, hashtag tiers, algorithm signals
      caption-formulas.md           L3: hook formulas, CTA patterns, full caption structures
    assets/
      brand-voice-examples.md       L3: annotated real-world caption examples

Cloud Shell 편집기에서 파일을 직접 엽니다.

cloudshell edit agents/copywriter/agent.py

TODO 1 - load_skill_from_dirskill_toolset 가져오기

# TODO 1: Import load_skill_from_dir and skill_toolset 주석을 찾아 다음 두 가져오기를 추가합니다.

from google.adk.skills import load_skill_from_dir
from google.adk.tools import skill_toolset

TODO 2 - 스킬을 로드하고 SkillToolset 만들기

import 아래에 있는 두 개의 주석을 찾습니다.

# TODO 2: Load the instagram-copywriting skill from the skills/ directory
# TODO 2: Create a SkillToolset with the loaded skill

다음으로 바꿉니다.

_instagram_skill = load_skill_from_dir(
    pathlib.Path(__file__).parent / "skills" / "instagram-copywriting"
)
_copywriting_skills = skill_toolset.SkillToolset(skills=[_instagram_skill])

load_skill_from_dir은(는) SKILL.mdreferences/assets/에 있는 파일을 읽습니다. SkillToolset는 이를 ADK 에이전트가 허용하는 형식(원시 기능이 아닌 도구 모음)으로 래핑합니다.

TODO 3 - 에이전트에 툴셋 등록

tools=[], # TODO 3: Add the SkillToolset here를 찾아 다음으로 바꿉니다.

tools=[_copywriting_skills],

기능 파일을 열어 구조를 확인합니다.

cloudshell edit agents/copywriter/skills/instagram-copywriting/SKILL.md

ADK 웹 UI를 계속 실행합니다. 에이전트 드롭다운을 사용하여 서버를 다시 시작하지 않고 copywriter로 전환합니다.

실행되고 있지 않다면 다시 시작합니다.

uv run adk web agents --allow_origins='*'

직접 해 보기: 드롭다운을 copywriter로 전환하고 전송합니다.

You are writing captions for EcoFlow Smart Water Bottle targeting health-conscious millennials aged 25-35.
Audience insight: they prioritize sustainability, track health metrics, and share lifestyle content.
Competitor insight: Hydro Flask dominates with lifestyle branding; S'well leads on premium aesthetics.
Write 3 Instagram captions - one inspirational, one educational, one community-focused. Include 5 hashtags each and a CTA.

6. 디자이너 빌드 - 멀티모달 이미지 생성

ADK 웹 UI를 계속 실행합니다. 에이전트 드롭다운을 사용하여 서버를 다시 시작하지 않고 에이전트를 전환합니다.

역할: 각 캡션에 대한 시각적 컨셉을 만들고 Gemini 기본 이미지 생성을 사용하여 실제 이미지를 생성합니다. 디자이너는 캡션당 시각적 개념을 정확히 1개 출력합니다. 여기에는 자세한 프롬프트, 스타일, 색상 팔레트, 분위기, Instagram 형식이 포함됩니다. 그런 다음 즉시 generate_image 도구를 호출하여 실제 이미지를 생성하고 GCS에 업로드합니다.

개념: 도구를 통해 텍스트 에이전트와 이미지 모델 연결

디자이너는 gemini-3-flash-preview (.envGEMINI_MODEL을 통해 설정된 텍스트 모델)에서 실행되지만 이미지 생성에는 전용 모델 (gemini-3.1-flash-image-preview)이 필요합니다. 이 이미지 모델은 함수 호출을 지원하지 않으므로 ADK 에이전트로 직접 사용할 수 없습니다. 대신 일반 Python 함수로 래핑되고 FunctionTool로 등록됩니다.

LLM이 직접 호출할 수 없는 모델 또는 API의 패턴은 다음과 같습니다. 도구로 래핑하고, 에이전트가 호출 시점을 조정하도록 하고, 구조화된 결과를 다시 가져옵니다.

Designer agent (text model)
        
          decides visual concept, writes image prompt
        
  generate_image tool
        
          calls gemini-3.1-flash-image-preview
          uploads result to GCS
        
  {"status": "success", "gcs_uri": "gs://..."}
        
          returned to agent, included in response
        
  Critic (receives gcs_uri, passes to Vertex AI for multimodal review)

Cloud Shell 편집기에서 파일을 직접 엽니다.

cloudshell edit agents/designer/image_gen_tool.py

함수 서명, 환경 설정, 가로세로 비율 삽입이 제공됩니다. 다음 세 가지 TODO를 순서대로 실행합니다.

TODO 1 - Gemini 이미지 모델 호출

# TODO 1 주석을 찾아 다음으로 바꿉니다.

        client = genai.Client(vertexai=True, project=project_id, location=location)

        response = client.models.generate_content(
            model=image_model,
            contents=prompt_with_aspect,
            config=types.GenerateContentConfig(
                response_modalities=["IMAGE", "TEXT"],
                http_options=types.HttpOptions(
                    retry_options=types.HttpRetryOptions(
                        attempts=5, exp_base=2, initial_delay=30,
                        http_status_codes=[429, 500, 503, 504],
                    ),
                    timeout=180_000,
                ),
            ),
        )

TODO 2 - 응답에서 이미지 바이트 추출

# TODO 2 주석을 찾아 다음으로 바꿉니다.

        image_bytes = None
        mime_type = "image/png"
        for part in response.candidates[0].content.parts:
            if part.inline_data is not None:
                image_bytes = part.inline_data.data
                mime_type = part.inline_data.mime_type or "image/png"
                break

        if not image_bytes:
            return {"status": "error", "error": "Gemini returned no image data"}

TODO 3 - GCS에 업로드하고 URI 반환

# TODO 3 주석을 찾아 다음으로 바꿉니다.

        ext = "jpg" if "jpeg" in mime_type else "png"
        from google.cloud import storage
        gcs_client = storage.Client(project=project_id)
        bucket = gcs_client.bucket(bucket_name)
        blob_name = f"campaign-images/{concept_name}-{uuid.uuid4().hex[:8]}.{ext}"
        blob = bucket.blob(blob_name)
        blob.upload_from_file(io.BytesIO(image_bytes), content_type=mime_type)
        gcs_uri = f"gs://{bucket_name}/{blob_name}"

직접 해 보기: 드롭다운을 designer로 전환하고 전송합니다.

Create a visual concept and generate the image for an EcoFlow Smart Water Bottle Instagram post targeting health-conscious millennials.
Style: clean, modern, lifestyle-focused. Include a detailed prompt with color palette, mood, and format (1080x1080 or 1080x1350).

7. 비평가 - 구조화된 출력 빌드

역할: 프로젝트 관리자에게 전달되기 전에 카피와 시각적 요소를 품질 보증합니다. 비평가는 두 결과물을 모두 평가하고 구체적인 제안과 함께 APPROVED 또는 NEEDS_REVISION를 반환합니다. 입력에 gcs_uri 값이 있으면 review_image 도구를 호출하여 점수를 매기기 전에 생성된 각 이미지를 시각적으로 검사합니다.

개념: Gemini 출력에 Pydantic 모델을 사용해야 하는 경우

이 규칙은 출력을 사용하는 사용자에 관한 것입니다.

  • Python 코드에서 사용response_schema + Pydantic 사용 코드는 모호성을 처리할 수 없으므로 필드를 안정적으로 추출하려면 보장된 구조가 필요합니다.
  • LLM이 사용 → 텍스트 형식 + 시스템 요청 사항으로 충분합니다. LLM은 서식 규칙을 이해하고 변형을 허용합니다.

review_image에서 Python 코드에는 score, approval_status, what_works, issues, suggestions이 입력된 값으로 필요합니다. response_schema=_GeminiReview를 전달하면 API 수준에서 Gemini가 유효한 JSON을 반환하도록 제한됩니다. model_validate_json()는 코드가 안정적으로 사용할 수 있는 유형이 지정된 객체로 파싱합니다.

class _GeminiReview(BaseModel):
    score: int = Field(ge=1, le=10)
    approval_status: Literal["APPROVED", "NEEDS_REVISION"]
    what_works: str
    issues: str
    suggestions: str

Cloud Shell 편집기에서 파일을 직접 엽니다.

cloudshell edit agents/critic/image_review_tool.py

Pydantic 모델과 프롬프트가 제공됩니다. 다음 세 가지 TODO를 순서대로 실행합니다.

TODO 1 - GCS URI에서 이미지 파트 만들기

# TODO 1 주석을 찾아 다음으로 바꿉니다.

        image_part = types.Part.from_uri(file_uri=gcs_uri, mime_type=mime_type)

TODO 2 - 구조화된 대답 스키마로 Gemini 호출

# TODO 2 주석을 찾아 다음으로 바꿉니다.

        response = client.models.generate_content(
            model=model,
            contents=[image_part, prompt],
            config=types.GenerateContentConfig(
                response_schema=_GeminiReview,
                response_mime_type="application/json",
            ),
        )

TODO 3 - 응답을 파싱하고 결과를 반환합니다.

# TODO 3 주석을 찾아 다음으로 바꿉니다.

        review = _GeminiReview.model_validate_json(response.text)
        return ImageReviewResult(status="success", concept_name=concept_name, **review.model_dump())

직접 해 보기: 드롭다운을 critic로 전환하고 전송합니다.

Review this Instagram caption for an eco-friendly water bottle brand targeting millennials:
"Hydrate smarter, live greener. 💧 Our EcoFlow bottle tracks your intake, keeps your drink cold for 24h, and never touches single-use plastic. Because what you drink from matters as much as what you drink. #EcoFlow #HydrationGoals #SustainableLiving #ZeroWaste #HealthyHabits - Shop link in bio."
Score it and indicate APPROVED or NEEDS_REVISION with specific feedback.

응답에 **POSTS REVIEW:**, Status: APPROVED (또는 NEEDS_REVISION), **OVERALL ASSESSMENT:**이 포함되어 있는지 확인합니다. 이러한 섹션이 있으면 Critic을 오케스트레이터에 연결할 수 있습니다.

세 에이전트를 모두 테스트한 후 Ctrl+C를 눌러 서버를 중지합니다.

8. MCP로 프로젝트 관리자 에이전트 빌드

프로젝트 관리자는 MCP (모델 컨텍스트 프로토콜)이라는 새로운 개념을 소개합니다.

파일을 엽니다.

cloudshell edit agents/project_manager/agent.py

이 파일은 더 복잡합니다. Notion이 없는 분기 (텍스트 전용 타임라인)와 Notion MCP 도구 모음이 있는 분기 등 두 개의 분기가 있는 create_project_manager_agent() 함수가 있습니다. 두 가지를 모두 작성합니다.

MCP로 해결할 수 있는 문제

에이전트가 외부 서비스를 호출해야 합니다(예: Notion에서 페이지 만들기). Notion REST API를 직접 호출하는 Python 코드를 작성할 수 있습니다. 하지만

  • 모든 개발자가 서로 다른 래퍼를 작성합니다.
  • 맞춤 통합 코드를 유지해야 합니다.
  • 모든 엔드포인트를 수동으로 설명하지 않으면 LLM이 API가 있는지 알지 못함

MCP는 외부 서비스가 LLM이 자동으로 검색하고 호출할 수 있는 도구로 기능을 노출하는 표준 방식을 정의하여 이 문제를 해결합니다.

MCP란 무엇인가요?

MCP (모델 컨텍스트 프로토콜)는 AI 에이전트를 외부 도구 및 데이터 소스에 연결하기 위한 개방형 표준 (Anthropic에서 게시)입니다. 범용 어댑터처럼 작동합니다.

MCP 서버는 다음을 수행하는 소규모 프로그램입니다.

  1. 외부 API (Notion, GitHub, 데이터베이스, 파일 시스템 등)를 래핑합니다.
  2. 해당 API를 유형이 지정되고 문서화된 도구 목록으로 노출합니다.
  3. 간단한 프로토콜 (stdio 또는 HTTP)을 통해 에이전트와 통신합니다.

에이전트는 MCP 서버에 연결하고, 사용 가능한 도구를 자동으로 검색하며, 다른 도구와 마찬가지로 이를 호출할 수 있습니다. LLM은 API-post-page(...)를 호출 가능한 함수로 인식합니다.

A2A와 MCP의 차이점은 무엇인가요?

이 부분에서 혼동이 자주 발생합니다. 주요 차이점은 다음과 같습니다.

A2A

MCP

연결되는 항목

상담사 ↔ 상담사

에이전트 ↔ 외부 도구/서비스

다른 쪽은

다른 LLM 에이전트

API 래퍼 (LLM 없음)

크리에이티브 디렉터가 브랜드 전략가에게 전화함

프로젝트 관리자가 Notion API를 호출함

프로토콜

HTTPS를 통한 JSON-RPC

stdio 또는 HTTP 스트림

정의

Google

Anthropic

이렇게 생각해 보세요.

  • A2A = 에이전트가 다른 에이전트와 대화하는 방식
  • MCP = 에이전트가 도구 및 서비스와 통신하는 방식

이 프로젝트에서는 다음 두 가지가 함께 사용됩니다.

Creative Director
    
      (A2A)  Brand Strategist ─── (google_search tool built into ADK)
      (A2A)  Copywriter
      (A2A)  Designer
      (A2A)  Critic
      (A2A)  Project Manager
                   
                     (MCP)  notion-mcp-server ──► Notion REST API

이 프로젝트에서 MCP가 작동하는 방식

에이전트가 실행되면 ADK는 notion-mcp-server를 하위 프로세스로 실행합니다. 이 프로세스는 이러한 도구를 LLM에 직접 노출합니다.

도구

기능

API-retrieve-a-database

스키마 (속성 이름, 유형, 유효한 값)를 가져옵니다.

API-post-database-query

기존 페이지를 쿼리합니다.

API-post-page

새 페이지를 만듭니다.

API-patch-page

기존 페이지를 업데이트합니다.

LLM은 다른 함수와 마찬가지로 이러한 함수를 호출합니다. MCP를 통해 Notion REST API로 이동한다는 사실을 알지 못합니다.

stdio를 사용하는 이유 HTTP만 사용하면 안 되나요?

MCP 서버는 에이전트의 하위 프로세스로 실행되며 stdin/stdout을 통해 통신합니다. 이는 다음을 의미합니다.

  • 추가 네트워크 포트가 필요하지 않음
  • 수명 주기는 에이전트가 관리합니다 (요청 시 시작, 종료 시 중지).
  • 모든 항목이 하나의 Docker 이미지로 제공되므로 별도의 서비스를 배포할 필요가 없습니다.

(선택사항) Notion 통합 사용 설정

이 섹션 전체를 건너뛸 수 있습니다. 프로젝트 관리자 에이전트는 Notion 사용 여부와 관계없이 항상 완전한 텍스트 기반 캠페인 타임라인을 생성합니다. 이 설정을 건너뛰면 에이전트가 인메모리 모드로 대체되고 채팅에 타임라인이 일반 텍스트로 출력됩니다. 아무것도 깨지지 않습니다. Notion 데이터베이스에 할 일이 표시되지 않을 뿐입니다. 건너뛰려면 바로 TODO 1로 이동하세요.

Notion 계정이 있고 MCP 통합을 직접 확인하고 싶다면 지금 아래 설정을 완료하세요. 다음에 나오는 TODO는 Notion 데이터베이스 ID를 참조합니다. 여기서 가져오세요.

1단계 - 템플릿에서 Notion 데이터베이스 만들기

공식 Notion 프로젝트 및 작업 템플릿을 데이터베이스로 사용합니다. 복잡한 실제 설정을 보여주기 위해 이 템플릿을 의도적으로 선택했습니다. 이름이 명확하지 않은 여러 속성 유형 (상태, 날짜 범위, 관계, 선택)이 있습니다. 이는 MCP의 동적 스키마 검색을 테스트하기에 좋은 방법입니다. 에이전트는 하드코딩된 속성 이름이 아닌 런타임에 정확한 속성 이름을 파악해야 합니다.

아래 링크를 클릭하여 Notion 작업공간에 템플릿을 추가하세요.

→ Notion에 '프로젝트 및 할 일' 템플릿 추가하기

마켓플레이스의 Notion 프로젝트 및 작업 템플릿

추가되면 프로젝트작업이라는 두 개의 연결된 데이터베이스가 표시됩니다. 템플릿에는 샘플 항목이 포함되어 있습니다. 계속하기 전에 모두 삭제하여 상담사가 깨끗한 작업 공간에서 시작할 수 있도록 하세요 (모두 선택 → 삭제).

2단계 - Notion 통합 만들기

통합을 만듭니다.

  1. notion.so/my-integrations로 이동합니다.
  2. New Integration(새 통합)을 클릭하고 이름을 AI Creative Studio로 지정합니다.
  3. 작업공간과 연결
  4. 설정 구성을 클릭하고 콘텐츠 읽기, 콘텐츠 업데이트, 콘텐츠 삽입 기능이 모두 선택되어 있는지 확인합니다.

Notion 통합 설정 - 이름을 'AI Creative Studio'로 지정하고 토큰을 복사합니다.

  1. 내부 통합 토큰 (ntn_...)을 복사하여 .env 파일에 붙여넣습니다.
NOTION_TOKEN=ntn_your-token-here

통합을 데이터베이스에 연결합니다.

  1. 방금 복제한 템플릿 페이지를 열고 프로젝트 데이터베이스를 클릭합니다.
  2. ... 메뉴 (오른쪽 상단) → 연결연결 추가를 클릭하고 AI Creative Studio를 선택합니다.

데이터베이스 메뉴에서 '연결'을 클릭하여 통합과 공유합니다.

AI Creative Studio가 활성 연결로 표시됨

  1. Tasks 데이터베이스에도 동일한 작업을 수행합니다.

데이터베이스 ID 가져오기:

  1. 프로젝트 데이터베이스 링크를 클릭하여 엽니다. 다음과 같은 URL이 있는 자체 페이지에서 열립니다.
https://www.notion.so/9887b6a94f7f83f68f8581e038d1aaa4?v=2c37b6a94f7f838685f1086e312c7278

템플릿 페이지에서 프로젝트 데이터베이스 열기

데이터베이스 ID는 URL의 첫 번째 UUID입니다(?v= 앞의 모든 항목).

https://www.notion.so/{DATABASE_ID}?v=...
                       ^^^^^^^^^^^^^^^^
                       9887b6a94f7f83f68f8581e038d1aaa4  ← this is your DATABASE_ID
  1. Tasks 데이터베이스 링크에 대해서도 동일한 작업을 실행하여 데이터베이스 ID를 가져옵니다.
  2. .env에 세 값을 모두 추가합니다.
NOTION_TOKEN=ntn_your-token-here
NOTION_PROJECT_DATABASE_ID=9887b6a94f7f83f68f8581e038d1aaa4   # <-- your Projects DB ID
NOTION_TASKS_DATABASE_ID=your-tasks-db-id                      # <-- your Tasks DB ID

3단계 - Notion MCP 서버 설치

프로젝트 관리자는 공식 @notionhq/notion-mcp-server Node.js 패키지를 통해 Notion에 연결됩니다. 전역적으로 설치합니다.

npm install -g @notionhq/notion-mcp-server@1.9.1

설치를 확인합니다.

npm list -g @notionhq/notion-mcp-server

예상 출력:

└── @notionhq/notion-mcp-server@1.9.1

notion-mcp-server: command not found

? Node.js가 설치되어 있고 (node --version) npm 전역 bin이 PATH에 있는지 확인합니다 (export PATH=$PATH:$(npm bin -g)).

4단계 - .env 확인

.env를 열고 세 가지 Notion 값이 모두 설정되어 있는지 확인합니다 (2단계에서 추가함).

cloudshell edit .env
NOTION_TOKEN=ntn_...                           # integration token
NOTION_PROJECT_DATABASE_ID=...                 # Projects database ID
NOTION_TASKS_DATABASE_ID=...                   # Tasks database ID

프로젝트 관리자 에이전트는 시작 시 이러한 변수를 자동으로 감지하고 Notion MCP 도구 모음을 사용 설정합니다.

스키마 검색 작동 방식

프로젝트 관리자는 동적 스키마 검색을 사용하며 Notion 속성 이름을 하드코딩하지 않습니다.

Step 1: Call API-retrieve-a-database to discover exact property names
Step 2: Read the "properties" object in the response
Step 3: Use ONLY discovered property names (case-sensitive) in API calls
Step 4: For select/status fields, use only values from the options array

즉, 에이전트가 모든 Notion 데이터베이스 구조에 자동으로 적응합니다. 속성 이름을 프랑스어, 아랍어 또는 다른 언어로 바꿔도 에이전트는 계속 작동합니다.

TODO 1 - 시스템 안내 작성

스타터는 Notion이 구성되지 않은 경우 빈 문자열을, 구성된 경우 데이터베이스 ID와 전체 도구 안내가 포함된 블록을 이미 계산합니다. 이렇게 하면 Notion 안내가 Notion이 없는 에이전트의 프롬프트에서 완전히 제외됩니다. LLM은 없는 도구의 규칙을 보지 않습니다.notion_section

return 자리표시자를 {notion_section}를 사용하는 실제 시스템 명령어로 바꾸는 것이 내 임무야.

    return f"""You are a Project Manager specializing in creative campaign execution.

Today's date is {datetime.date.today().strftime("%B %d, %Y")}.
Use this as the starting point for all timelines.

Your goal: create a complete project plan for the campaign.
{notion_section}
**Project Timeline:**
Phase 1: Strategy & Research | [date]  [date] | [key activities]
Phase 2: Content Creation    | [date]  [date] | [key activities]
Phase 3: Review & Revision   | [date]  [date] | [key activities]
Phase 4: Launch & Monitoring | [date]  [date] | [key activities]

**Task List:**
| Task | Owner | Deadline | Status |
[list each task with realistic deadlines from today; set Owner to TBD]

**Budget Breakdown:**
[by category with approximate allocations]

**Milestones:**
[3-5 key checkpoints with dates]

**Notion Status:**
[What happened - e.g. "Project created (ID: xxx), 8 tasks linked" or "Notion not configured - text timeline only"]
"""

TODO 2 - Notion이 없는 에이전트

create_project_manager_agent() 내의 if not notion_token 브랜치에서 불완전한 에이전트를 다음으로 바꿉니다.

        return Agent(
            name="project_manager",
            model=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"),
            generate_content_config=GENERATE_CONTENT_CONFIG,
            instruction=get_system_instruction(),
            description="Project manager that creates campaign timelines and task breakdowns",
        )

TODO 3 - Notion MCP가 있는 에이전트

참고: 시작 파일에는 이미 create_project_manager_agent() 위에 사전 작성된 handle_notion_error 콜백이 포함되어 있습니다. Notion API 오류 (400/404)를 가로채고 원시 오류 페이로드를 명확하고 실행 가능한 메시지로 대체하여 LLM이 자체적으로 수정할 수 있도록 합니다. after_tool_callback를 통해 연결하기만 하면 됩니다.

먼저 create_project_manager_agent() 상단에서 두 데이터베이스 ID를 모두 읽습니다.

    notion_token           = os.getenv("NOTION_TOKEN")
    notion_project_db_id   = os.getenv("NOTION_PROJECT_DATABASE_ID")
    notion_tasks_db_id     = os.getenv("NOTION_TASKS_DATABASE_ID")

그런 다음 else 브랜치에서 MCP 도구 세트와 에이전트를 만듭니다.

        from google.adk.tools.mcp_tool import McpToolset, StdioConnectionParams
        from mcp import StdioServerParameters

        server_params = StdioServerParameters(
            command="notion-mcp-server",
            env={
                "NOTION_TOKEN": notion_token,
                "PATH": os.environ.get("PATH", ""),
            }
        )
        notion_toolset = McpToolset(
            connection_params=StdioConnectionParams(
                server_params=server_params,
                timeout=30.0
            )
        )

        return Agent(
            name="project_manager",
            model=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"),
            generate_content_config=GENERATE_CONTENT_CONFIG,
            after_tool_callback=handle_notion_error,
            instruction=get_system_instruction(
                project_database_id=notion_project_db_id,
                tasks_database_id=notion_tasks_db_id,
            ),
            description="Project manager with Notion integration for task tracking",
            tools=[notion_toolset],
        )

권장사항: 선택적 통합에서 하드 실패가 발생하지 않도록 합니다. 텍스트 타임라인이 항상 기본 결과물이며 Notion은 보조적인 도구입니다.

ADK 웹으로 로컬에서 프로젝트 관리자 테스트

uv run adk web agents --allow_origins='*'

포트 8000에서 웹 미리보기를 엽니다. 에이전트 드롭다운을 사용하여 project_manager를 선택한 후 다음을 시도해 보세요.

Create a project plan for a GreenBrew organic coffee brand Instagram campaign.
Budget: $2,500. Launch in 3 weeks. Target audience: eco-conscious millennials aged 22-30.
Include phases, tasks with deadlines from today, and milestones.

단계, 할 일 목록, 주요 일정으로 구성된 구조화된 텍스트 타임라인이 표시됩니다. .env에 Notion 사용자 인증 정보가 설정된 경우 에이전트는 Notion 워크스페이스에도 항목을 만듭니다.

9. A2A 프로토콜 이해하기

에이전트 간 프로토콜 (A2A)을 사용하여 시스템의 다양한 에이전트를 연결합니다. 작동 방식을 알아보겠습니다.

A2A로 해결할 수 있는 문제

ADK로 빌드된 브랜드 전략가 에이전트와 LangGraph로 빌드된 카피라이터 에이전트가 있다고 가정해 보겠습니다. 서로 어떻게 통화하나요? 내부 언어가 다릅니다. 매번 맞춤 접착제 코드를 작성해야 합니다.

A2A는 프레임워크와 관계없이 모든 에이전트가 말할 수 있는 범용 언어를 정의하여 이 문제를 해결합니다. 이는 에이전트 세계의 HTTP와 같습니다. 누구나 서로 대화할 수 있도록 모두가 동의하는 표준입니다.

A2A란 무엇인가요?

Agent-to-Agent (A2A)는 Google에서 게시한 에이전트 통신을 위한 개방형 표준입니다. 다음과 같은 항목을 정의합니다.

  1. 에이전트가 자신을 설명하는 방식 - /.well-known/agent.json의 에이전트 카드
  2. 다른 에이전트가 호출하는 방식 - HTTPS를 통한 JSON-RPC
  3. 결과가 반환되는 방식 - 스트리밍 또는 단일 응답

A2A가 유연한 이유:

  • 언어에 구애받지 않음 - Python 에이전트가 TypeScript 에이전트와 대화할 수 있음
  • 프레임워크에 구애받지 않음 - ADK 에이전트가 LangGraph 또는 CrewAI 에이전트와 대화할 수 있음
  • 인프라에 구애받지 않음 - 로컬 에이전트가 클라우드 에이전트와 통신할 수 있음

작동 방식 - 단계별

Creative Director                  Brand Strategist
      │                                  │
      │  1. GET /.well-known/agent.json  │
      │ ────────────────────────────────►│
      │  ◄──── agent card (name, url,    │
      │         skills, capabilities) ───│
      │                                  │
      │  2. POST /                       │
      │     {"method": "tasks/send",     │
      │      "params": {"message": ...}} │
      │ ────────────────────────────────►│
      │                                  │  LLM does
      │                                  │  the work...
      │  3. streaming response chunks    │
      │  ◄───────────────────────────────│
      │  ◄───────────────────────────────│
      │  ◄───────────────────────────────│

1단계 - 검색: 오케스트레이터가 에이전트의 이름, URL, 기능을 파악하기 위해 에이전트 카드를 한 번 가져옵니다.

2단계 - 호출: 오케스트레이터가 JSON-RPC POST를 통해 작업을 전송합니다. 본문에는 메시지 (전문가에게 표시되는 프롬프트)가 포함됩니다.

3단계 - 응답: 전문가가 일반 LLM 호출과 마찬가지로 청크 단위로 응답을 스트리밍합니다.

상담사 카드

각 에이전트는 /.well-known/agent.json에 자체 설명을 게시합니다. 이는 명함과 같습니다. 에이전트가 할 수 있는 일과 에이전트에 연락할 수 있는 방법을 전 세계에 알려줍니다.

{
  "name": "brand_strategist",
  "description": "Market research and competitive analysis",
  "url": "https://brand-strategist-xyz.run.app",
  "capabilities": { "streaming": true },
  "skills": [
    {
      "id": "market_research",
      "description": "Research target audiences, competitors, and trends"
    }
  ]
}

오케스트레이터는 이 카드를 읽어 RemoteA2aAgent 객체를 빌드합니다. 전문가의 내부 구조에 관한 하드코딩된 지식은 필요하지 않습니다.

ADK에서 A2A를 통해 에이전트 노출

to_a2a()는 ADK 에이전트를 A2A 호환 FastAPI 앱으로 래핑합니다. 한 줄:

from google.adk.a2a.utils.agent_to_a2a import to_a2a

# root_agent = your normal ADK Agent(...)
a2a_app = to_a2a(root_agent, host=PUBLIC_HOST, port=PUBLIC_PORT, protocol=PROTOCOL)
uvicorn.run(a2a_app, host=HOST, port=PORT)

이렇게 하면 다음이 자동으로 생성됩니다.

  • /.well-known/agent.json - 에이전트 카드
  • / - JSON-RPC 엔드포인트 (모든 A2A 작업 요청은 루트 경로로 이동)

10. 에이전트를 A2A 서비스로 노출

에이전트를 A2A 서비스로 노출하려면 ADK의 to_a2a() 유틸리티 함수를 사용하면 됩니다.

to_a2a() 작동 방식

from google.adk.a2a.utils.agent_to_a2a import to_a2a

a2a_app = to_a2a(root_agent, host=PUBLIC_HOST, port=PUBLIC_PORT, protocol=PROTOCOL)
uvicorn.run(a2a_app, host=HOST, port=PORT)

to_a2a()는 다음을 자동으로 노출하는 FastAPI 애플리케이션으로 ADK 에이전트를 래핑합니다.

  • /.well-known/agent.json - 에이전트 카드 (이름, 설명, 기능)
  • /a2a/{agent_name} - 작업을 수신하는 JSON-RPC 엔드포인트

각 에이전트의 스켈레톤 코드에는 이미 to_a2a()를 사용하여 에이전트를 A2A 서버에 래핑하는 __main__ 블록이 포함되어 있습니다. 이 코드는 제공되므로 작성할 필요가 없습니다.

이중 URL 구성 이해

python agent.py를 실행하면 __main__ 블록에서 별도의 두 URL 구성을 사용합니다.

# Where the server actually listens (network interface):
HOST = "0.0.0.0"
PORT = 8082  # Brand Strategist (others use 80838086 locally)

# What gets advertised in the agent card (the address other agents use to reach it):
PUBLIC_HOST = os.getenv("PUBLIC_HOST", "localhost")
PUBLIC_PORT = int(os.getenv("PUBLIC_PORT", str(PORT)))
PROTOCOL    = os.getenv("PROTOCOL", "http")

a2a_app = to_a2a(root_agent, host=PUBLIC_HOST, port=PUBLIC_PORT, protocol=PROTOCOL)
uvicorn.run(a2a_app, host=HOST, port=PORT)

환경

HOST:PORT (들을거리)

PUBLIC_HOST:PUBLIC_PORT (에이전트 카드에 광고됨)

지역

0.0.0.0:8082

http://localhost:8082

Cloud Run

0.0.0.0:8080

https://brand-strategist-xyz.run.app:443

로컬에서는 둘 다 동일한 머신을 가리킵니다. Cloud Run에서 컨테이너는 내부적으로 8080에서 수신 대기하지만 에이전트 카드는 공개 HTTPS URL을 광고해야 합니다. 그렇지 않으면 크리에이티브 디렉터가 컨테이너 외부에서 전문가에게 연락할 수 없습니다.

5개의 전문 A2A 서버를 모두 시작합니다.

5명의 전문가를 모두 A2A 서버로 동시에 실행한 다음, 이를 가리키는 크리에이티브 디렉터를 로컬에서 테스트해 보겠습니다.

5개의 별도 Cloud Shell 터미널을 열고 (터미널 탭 바에서 + 아이콘 클릭) 터미널당 하나의 에이전트를 실행합니다.

uv run.venv를 자동으로 활성화하므로 각 터미널에서 수동으로 source를 실행할 필요가 없습니다.

터미널 1 - 브랜드 전략가 (포트 8082):

cd ~/ai-creative-studio/workshop/starter
PORT=8082 uv run agents/brand_strategist/agent.py

터미널 2 - 카피라이터 (포트 8083):

cd ~/ai-creative-studio/workshop/starter
PORT=8083 uv run agents/copywriter/agent.py

터미널 3 - 디자이너 (포트 8084):

cd ~/ai-creative-studio/workshop/starter
PORT=8084 uv run agents/designer/agent.py

터미널 4 - 비평가 (포트 8085):

cd ~/ai-creative-studio/workshop/starter
PORT=8085 uv run agents/critic/agent.py

터미널 5 - 프로젝트 관리자 (포트 8086):

cd ~/ai-creative-studio/workshop/starter
PORT=8086 uv run agents/project_manager/agent.py

.env에서 localhost URL 설정

터미널 6에서 광고 소재 디렉터가 찾을 수 있도록 .env을 로컬 에이전트 URL로 업데이트합니다.

cd ~/ai-creative-studio/workshop/starter

sed -i \
  -e 's|STRATEGIST_AGENT_URL=.*|STRATEGIST_AGENT_URL=http://localhost:8082|' \
  -e 's|COPYWRITER_AGENT_URL=.*|COPYWRITER_AGENT_URL=http://localhost:8083|' \
  -e 's|DESIGNER_AGENT_URL=.*|DESIGNER_AGENT_URL=http://localhost:8084|' \
  -e 's|CRITIC_AGENT_URL=.*|CRITIC_AGENT_URL=http://localhost:8085|' \
  -e 's|PM_AGENT_URL=.*|PM_AGENT_URL=http://localhost:8086|' \
  .env

A2A 검사기로 에이전트 검사

A2A 검사기는 A2A 프로토콜을 기본적으로 사용하는 오픈소스 개발자 도구입니다. 클라이언트 코드를 작성하지 않고도 실행 중인 A2A 에이전트에 직접 연결하고, 에이전트 카드를 읽고, 작업을 전송할 수 있습니다.

표시되는 내용:

  • 에이전트 카드 - 에이전트가 광고하는 구조화된 메타데이터입니다. 이름, 설명, 지원되는 입력/출력 모드, 엔드포인트 URL이 포함됩니다. 크리에이티브 디렉터가 전문가를 발견하면 다음과 같이 표시됩니다.
  • 채팅 인터페이스 - A2A를 통해 에이전트에게 메시지를 보내고 원시 응답을 확인합니다. 에이전트를 연결하기 전에 프롬프트를 개별적으로 테스트할 수 있습니다.
  • 프로토콜 검증 - 검사기는 에이전트 카드가 A2A 사양을 준수하는지 확인하여 누락된 필드나 형식이 잘못된 응답을 조기에 표시합니다.

중요한 이유: 나중에 Cloud Run에 배포하면 크리에이티브 디렉터가 /.well-known/agent.json에서 에이전트 카드를 가져와 각 전문가를 검색합니다. 카드가 잘못된 경우(URL이 잘못되었거나 기능이 누락됨) 오케스트레이터가 자동으로 실패합니다. 인스펙터를 사용하면 클라우드에 배포하기 전에 이러한 문제를 로컬에서 포착할 수 있습니다.

브랜드 전략가 에이전트 카드

상담사 카드에는 다른 상담사에게 표시되는 것과 동일한 상담사의 신원과 기능이 표시됩니다.

상담사 카드 세부정보

검사기 설치 및 시작

cd ~/ai-creative-studio/workshop
./setup_inspector.sh

.env 업데이트는 일회성 명령어입니다. 다음으로 터미널 6을 사용하여 인스펙터를 시작합니다.

cd ~/a2a-inspector
bash scripts/run.sh

인스펙터 UI를 열려면 웹 미리보기포트 변경을 사용하고 5001를 입력합니다.

브랜드 전략가와 상담하기

인스펙터의 URL 필드에 http://localhost:8082를 입력하고 연결을 클릭합니다. 인스펙터가 상담사 카드를 가져와 전문가의 메타데이터를 표시합니다.

브랜드 전략가에 연결된 A2A 검사기

상담사 카드에서 알 수 있는 정보

에이전트 카드는 메타데이터 이상입니다. 에이전트가 네트워크에 광고하는 전체 기능 계약입니다. 프로젝트 관리자 (http://localhost:8086)에 연결하여 가장 풍부한 예시를 확인하세요.

{
  "name": "project_manager",
  "description": "Project manager with Notion integration for task tracking",
  "protocolVersion": "0.3.0",
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"],
  "skills": [
    {
      "id": "project_manager",
      "name": "model",
      "tags": ["llm"],
      "description": "... full system instruction including today's date and Notion database IDs ..."
    },
    {
      "id": "project_manager-API-post-page",
      "name": "API-post-page",
      "tags": ["llm", "tools"],
      "description": "Notion | Create a page"
    },
    {
      "id": "project_manager-API-retrieve-a-database",
      "name": "API-retrieve-a-database",
      "tags": ["llm", "tools"],
      "description": "Notion | Retrieve a database"
    }
  ]
}

세 가지 사항이 눈에 띕니다.

1. MCP 도구가 A2A 스킬이 됨 - 프로젝트 매니저가 액세스할 수 있는 모든 Notion 도구 (API-post-page, API-retrieve-a-database 등)가 에이전트 카드에 별도의 스킬로 나열됩니다. 네트워크의 다른 에이전트는 코드를 읽지 않고도 이 에이전트가 사용할 수 있는 도구를 정확히 파악할 수 있습니다.

2. 시스템 명령어가 삽입됨 - 첫 번째 스킬의 description에 오늘 날짜와 Notion 데이터베이스 ID를 포함한 전체 시스템 명령어가 포함되어 있습니다. 이것이 크리에이티브 디렉터가 프로젝트 관리자를 호출할 때 전달할 내용을 아는 방법입니다.

3. URL은 라이브 엔드포인트입니다. - url 필드는 크리에이티브 디렉터가 이 전문가를 호출할 때 RemoteA2aAgent가 사용하는 것과 정확히 일치합니다. 카드에 있는 URL이 잘못되면 오케스트레이터가 에이전트에 연결할 수 없습니다.

이러한 이유로 인스펙터는 강력한 디버깅 도구입니다. 에이전트 카드를 한눈에 보면 에이전트가 실행 중인지, 어떤 도구가 있는지, 엔드포인트가 올바른지 알 수 있습니다.

테스트 메시지 보내기

연결되면 채팅 패널에 프롬프트를 입력하고 전송합니다. 검사기는 이를 A2A 작업으로 제출하고 응답을 다시 스트리밍합니다. 이는 크리에이티브 디렉터가 프로덕션에서 이 에이전트를 호출하는 방식과 동일합니다.

A2A 검사기를 통해 브랜드 전략가와 채팅

인스펙터를 로컬 포트 (8082~8086)로 지정하여 각 전문가를 개별적으로 테스트합니다.

11. 크리에이티브 디렉터 조정자 빌드

크리에이티브 디렉터는 마스터 오케스트레이터입니다. 환경 변수에서 전문가 URL을 읽고, 각 URL을 RemoteA2aAgent로 래핑하고, LLM이 호출할 수 있는 AgentTool로 노출합니다.

5명의 전문가 에이전트가 계속 실행 중인지 확인합니다 (10단계의 터미널 1~5).

터미널 6 (A2A 검사기 터미널)에서 Ctrl+C를 사용하여 검사기를 중지합니다.

파일을 엽니다.

cd ~/ai-creative-studio/workshop/starter
cloudshell edit agents/creative_director/agent.py

이 파일에는 할 일이 세 개 있습니다. 순서대로 진행합니다.

TODO 1 - 이미 작성된 시스템 안내 검토

시스템 명령어는 동일한 디렉터리의 prompt.py에 있으며 자동으로 가져옵니다.

from .prompt import SYSTEM_INSTRUCTION_TEMPLATE

prompt.py을 열어 읽은 후 계속 진행합니다.

cloudshell edit agents/creative_director/prompt.py

전체 오케스트레이션 동작을 제어하므로 이를 이해하는 것이 중요합니다.

오케스트레이터 프롬프트가 모든 것을 제어하는 이유

이 섹션과 함께 prompt.py을 열어 두세요. 아래 예에서는 prompt.py의 특정 부분을 참조합니다.

prompt.py의 프롬프트는 단순한 문서가 아니라 전체 시스템의 컨트롤 플레인입니다. 구조가 잘못된 오케스트레이터 프롬프트는 에이전트가 순서대로 호출되지 않고, 전문가 대신 오케스트레이터가 콘텐츠를 생성하고, 실패 후에도 워크플로가 계속되고, 에이전트 간에 컨텍스트가 자동으로 삭제되는 결과를 낳습니다. 다음 9가지 요소는 가장 일반적인 오류를 방지합니다.

요소 0 - 먼저 계획한 후 실행

가장 중요한 요소입니다. 오케스트레이터는 전문가를 호출하기 전에 번호가 매겨진 계획을 출력하라는 안내를 받습니다.

I'll create your campaign by coordinating the specialist agents in sequence:
1. Brand Strategist - develop positioning and audience insights
2. Copywriter - write captions using those insights
3. Visual Designer - create image prompts aligned with the copy
4. Critic - review and score the full package
5. Project Manager - build the timeline and task breakdown

이 단계를 거치지 않으면 LLM이 바로 도구 호출로 넘어가 워크플로의 어느 지점에 있는지 추적하지 못합니다. 특히 전문가로부터 긴 응답을 받은 후에는 더욱 그렇습니다. 먼저 계획을 간략하게 설명하면 오케스트레이터가 고정됩니다. 오케스트레이터는 현재 단계, 다음 단계, 전체 실행의 모습을 알 수 있습니다. 이 단계를 건너뛰면 오케스트레이터가 워크플로 중간에 멈추거나 단계를 반복합니다.

요소 1 - 명시적 역할 정의

❌ "You are a helpful creative assistant."
✅ "You orchestrate specialists. You do NOT write captions, designs, or timelines yourself."

명시적으로 금지하지 않으면 LLM이 전문가 호출을 건너뛰고 콘텐츠를 직접 생성하는 경우가 있습니다. 이렇게 하면 더 빠르고 방법을 '알고' 있기 때문입니다. 요청 사항이 잘못된 것 같습니다.

요소 2 - 잘못된 패턴이 나열된 도구 호출 구문

올바른 구문만 보여주는 것으로는 충분하지 않습니다. LLM은 그럴듯해 보이지만 자동으로 실패하는 호출을 생성할 수 있습니다. 프롬프트에 올바른 패턴과 사용해서는 안 되는 패턴이 모두 명시되어 있습니다.

✅ copywriter(request="...")          ← correct
❌ print(copywriter(...))             ← breaks silently
❌ default_api.copywriter(...)        ← breaks silently
❌ copywriter.run(...)                ← breaks silently
❌ agents.copywriter(...)             ← breaks silently

잘못된 패턴을 명시적으로 나열하면 프로덕션에서 잘못된 도구 호출이 약 95% 감소했습니다.

요소 3 - 순차적 실행을 단계별로 설명

a) Call the tool
b) Wait for tool_output
c) Verify the output is not an error
d) Confirm to the user: "✓ Brand Strategist complete"
e) Then move to the next agent

(b) 및 (c) 단계가 없으면 LLM이 때때로 두 에이전트를 동시에 호출하거나 성공을 가정하고 응답을 받기 전에 다음 단계로 넘어갑니다.

요소 4 - 오류 지시어: 중지, 보고, 진행하지 않음

초기 버전에서는 오케스트레이터가 한 전문가로부터 오류를 수신하고, 그에 대한 그럴듯한 출력을 조작하고, 다음 에이전트로 계속 진행했습니다. 사용자는 환각에 기반한 완전한 캠페인을 구축했습니다. 수정 사항은 명시적입니다. 즉시 중지하세요. 정확한 오류를 신고합니다. 계속하지 마세요.

요소 5 - 컨텍스트 전달 규칙

원격 에이전트에는 대화 기록이 없습니다. 오케스트레이터가 A2A를 통해 카피라이터를 호출하면 카피라이터는 해당 단일 요청의 메시지만 볼 수 있으며 브랜드 전략가가 무엇을 말했는지 알 수 없습니다. 오케스트레이터는 이전 출력을 각 후속 호출에 명시적으로 번들로 묶어야 합니다.

copywriter(request="Create 3 posts for EcoFlow water bottle targeting millennials.
Use these insights from the Brand Strategist: [paste full strategist output here].
Create engaging captions with hashtags.")

이 안내에는 '원격 에이전트에는 공유 메모리가 없습니다. 이전 출력을 명시적으로 전달해야 합니다.'라고 명시되어 있습니다. 이 정보가 없으면 각 상담사는 눈을 가리고 일하는 것과 같습니다.

요소 6 - 요청 분류: 단순 vs. 복잡

모든 요청에 5명의 상담사가 모두 필요한 것은 아닙니다. 프롬프트는 오케스트레이터에 계획을 세우기 전에 요청을 분류하도록 지시합니다.

SIMPLE  → one agent needed
  "Research the eco-friendly water bottle market" → brand_strategist only
  "Write 3 Instagram captions"                    → copywriter only

COMPLEX → all agents sequentially
  "Create a complete campaign with timeline"      → all 5 agents

이 분류가 없으면 오케스트레이터는 '게시물 아이디어 3개만 알려 줘'를 포함한 모든 요청에 대해 5개의 에이전트를 모두 실행하여 불필요한 지연 시간과 비용을 추가합니다.

요소 7 - 커뮤니케이션 규칙: 전체 출력 표시, 필터링 없음

프롬프트에는 오케스트레이터가 전문가가 반환한 내용을 요약하거나 수정해서는 안 된다고 명시되어 있습니다.

- DO NOT summarize unless the output exceeds 2000 words
- DO NOT filter or edit agent responses
- Show the user exactly what each specialist produced
- NEVER say results are ready unless you received them in tool_output

이 기능이 없으면 오케스트레이터가 전문가의 출력을 자체 언어로 다시 작성하여 세부정보가 누락되고 오류가 발생하며 전문가를 두는 목적이 무의미해집니다.

요소 8 - 워크플로 완료: 조기에 중단하지 않음

미묘하지만 중요한 실패 모드: 오케스트레이터가 5단계 계획을 발표하고 3단계를 완료한 후 완료된 것처럼 결과를 표시합니다. 이 프롬프트는 오케스트레이터가 완료되기 전에 통과해야 하는 명시적 체크리스트를 통해 이를 방지합니다.

✓ Did I announce a plan with N agents?
✓ Have I called ALL N agents from my plan?
✓ Did each agent respond successfully?
✓ Am I presenting complete results from ALL agents?

If any answer is NO → continue executing the remaining agents.

이렇게 하면 오케스트레이터가 부분 실행을 완료로 처리하지 않습니다.

품질 관리 루프

버전 관리 워크플로는 prompt.py에서 가장 복잡한 부분입니다. ## REVISION WORKFLOW 섹션을 열고 따라합니다.

작동 방식

비평가가 응답한 후 크리에이티브 디렉터는 프로젝트 관리자에게 무조건 계속하지 않습니다. 비평가의 출력을 읽고 분기합니다.

Critic output
      │
      ├── "All Approved: YES"
      │         └──► proceed to Project Manager
      │
      └── "Status: NEEDS_REVISION"
                │
                ├── posts fail   → call copywriter again with feedback
                ├── visuals fail → call designer again with feedback
                └── both fail    → call copywriter, then designer
                          │
                          └──► revised output → Project Manager
                               (1 revision max per deliverable)

코드 기반이 아닌 LLM 기반입니다.

앞서 언급한 Codelab에서는 오케스트레이터가 Critic의 대답을 '파싱'한다고 설명했습니다. 이 파싱을 수행하는 Python 코드는 없습니다. 정규식도 문자열 일치도 없습니다. 크리에이티브 디렉터는 자체 명령어를 읽는 LLM입니다. 이 안내에는 다음과 같이 명시되어 있습니다.

Look for "Status: NEEDS_REVISION" in the critic's response.
Posts need revision  → call copywriter
Visuals need revision → call designer

LLM은 비평가의 출력에서 정확한 문자열을 읽고 브랜치를 따릅니다. 이러한 이유로 비평가 형식은 협상할 수 없습니다. 비평가가 NEEDS_REVISION 대신 '수정이 필요함'이라고 작성하면 LLM은 명령어에서 일치하는 항목을 찾지 못하고 수정 단계를 자동으로 건너뜁니다.

수정 호출에서 컨텍스트가 전달되는 방식

수정 호출은 5번 요소와 동일한 컨텍스트 전달 규칙을 따릅니다. 카피라이터는 첫 번째 버전을 기억하지 못하므로 오케스트레이터는 모든 것을 명시적으로 포함해야 합니다.

"I need you to revise the Instagram posts based on critic feedback.

ORIGINAL BRIEF:
[the original user request]

YOUR FIRST VERSION:
[the posts the copywriter created]

CRITIC FEEDBACK (Score: 6/10 - NEEDS_REVISION):
[the critic's specific suggestions]

Please revise the posts addressing this feedback while maintaining
the strengths the critic identified."

'첫 번째 버전' 섹션이 없으면 카피라이터는 이미 생성된 콘텐츠를 개선하는 대신 처음부터 작성합니다.

1회 수정 제한 및 그 중요성

한 번의 수정 라운드가 끝나면 오케스트레이터는 점수와 관계없이 프로젝트 관리자로 진행합니다. 이 요청 사항은 이를 머릿속으로 추적합니다.

After calling copywriter for revision once:
→ mark "copywriter_revised = true" in context
→ even if the critic still suggests changes, proceed to PM

이 제한이 없으면 비평가가 문제를 신고하고 → 카피라이터가 수정하고 → 비평가가 다시 신고하고 → 카피라이터가 다시 수정하는 식으로 루프가 무한정 실행될 수 있습니다. 각 라운드에는 토큰과 시간이 소요됩니다. 한 번의 수정으로도 과도한 사이클 위험 없이 품질을 개선할 수 있습니다.

프로젝트 관리자에게 전달되는 항목

프로젝트 관리자는 항상 원본이 아닌 최종 승인된 버전을 받습니다. 수정이 발생한 경우 오케스트레이터는 수정된 사본과 시각적 요소를 전달합니다. 첫 번째 통과에서 모든 항목이 승인된 경우 해당 항목이 직접 통과됩니다. PM은 거부된 임시보관 페이지를 볼 수 없습니다.

TODO 2 - 각 전문가를 RemoteA2aAgent + AgentTool로 등록

# TODO 2: For each specialist URL... 주석을 찾아 다음으로 바꿉니다.

    if strategist_url:
        available_agents_list.append(
            "- **brand_strategist**: Market research, competitor analysis, trend identification"
        )
        strategist_agent = RemoteA2aAgent(
            name="brand_strategist",
            description="Researches markets, competitors, and trends using Google Search",
            agent_card=f"{strategist_url}/.well-known/agent.json",
        )
        agent_tools.append(AgentTool(agent=strategist_agent))

    if copywriter_url:
        available_agents_list.append(
            "- **copywriter**: Instagram captions, hashtags, and CTAs"
        )
        copywriter_agent = RemoteA2aAgent(
            name="copywriter",
            description="Creates Instagram captions with hashtags and CTAs",
            agent_card=f"{copywriter_url}/.well-known/agent.json",
        )
        agent_tools.append(AgentTool(agent=copywriter_agent))

    if designer_url:
        available_agents_list.append(
            "- **designer**: Visual concepts and real images generated via Gemini (GCS URIs returned)"
        )
        designer_agent = RemoteA2aAgent(
            name="designer",
            description="Creates visual concepts and generates real images via Gemini, stored in GCS",
            agent_card=f"{designer_url}/.well-known/agent.json",
        )
        agent_tools.append(AgentTool(agent=designer_agent))

    if critic_url:
        available_agents_list.append(
            "- **critic**: Quality review with APPROVED/NEEDS_REVISION scoring"
        )
        critic_agent = RemoteA2aAgent(
            name="critic",
            description="Reviews campaign materials and returns structured quality feedback",
            agent_card=f"{critic_url}/.well-known/agent.json",
        )
        agent_tools.append(AgentTool(agent=critic_agent))

    if pm_url:
        available_agents_list.append(
            "- **project_manager**: Project timelines, task breakdowns, Notion integration"
        )
        pm_agent = RemoteA2aAgent(
            name="project_manager",
            description="Creates project timelines and task breakdowns, optionally in Notion",
            agent_card=f"{pm_url}/.well-known/agent.json",
        )
        agent_tools.append(AgentTool(agent=pm_agent))

TODO 3 - 컨텍스트 압축을 사용하여 앱으로 래핑

압축이 필요한 이유

대화의 모든 메시지(사용자의 프롬프트, 모든 도구 호출, 모든 도구 응답)는 LLM이 다음 차례에 읽는 컨텍스트 윈도우에 추가됩니다. 5개 에이전트 워크플로에서는 이 값이 빠르게 누적됩니다.

Turn 1:  user prompt                           ~200 tokens
Turn 2:  orchestrator plan                     ~300 tokens
Turn 3:  brand_strategist tool_call            ~150 tokens
Turn 4:  brand_strategist tool_output          ~1,500 tokens   full research report
Turn 5:  copywriter tool_call                  ~300 tokens     must include strategist output
Turn 6:  copywriter tool_output                ~2,000 tokens   3 captions
Turn 7:  designer tool_call                    ~500 tokens
Turn 8:  designer tool_output                  ~1,500 tokens
...

에이전트 4 (비평가)의 경우 컨텍스트 윈도우에는 이전 세 에이전트의 전체 출력이 포함됩니다. 도구 응답만으로도 토큰이 8,000~12,000개에 달하는 경우가 많습니다. Gemini 2.5 Pro의 큰 컨텍스트 윈도우를 사용하더라도 오케스트레이터는 계속 증가하는 기록을 처리해야 하므로 추론 품질이 저하됩니다. 압축이 없으면 긴 워크플로가 에이전트 4 주변에서 실제 한계에 도달합니다.

압축 기능

ADK는 모든 이벤트를 전체로 유지하는 대신 LLM을 주기적으로 호출하여 오래된 이벤트를 간결한 표현으로 요약합니다. 이전 이벤트의 요약과 가장 최근 에이전트의 전체 출력만 컨텍스트에 유지됩니다.

Without compaction:
  [full strategist output] + [full copywriter output] + [full designer output] + → Critic

With compaction (interval=3, overlap=1):
  [summary of strategist + copywriter] + [full designer output] + → Critic

요약은 필수 사실 (주요 통계, 승인된 자막, 시각적 개념)을 유지하면서 장황한 형식, 각 상담사에게 전달되는 반복된 컨텍스트, 중간 추론을 삭제합니다. 비평가에게는 평가에 필요한 모든 정보가 여전히 제공됩니다. 다만 3개의 전체 보고서 대신 요약이 제공됩니다.

코드

# TODO 3: Wrap the agent in an App... 주석을 찾아 자리표시자 App(...)을 다음으로 바꿉니다.

    from google.adk.apps import App
    from google.adk.apps.app import EventsCompactionConfig
    from google.adk.apps.llm_event_summarizer import LlmEventSummarizer
    from google.adk.models import Gemini

    compaction_config = EventsCompactionConfig(
        summarizer=LlmEventSummarizer(llm=Gemini(model_id=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"))),
        compaction_interval=3,   # Summarize after every 3 agent completions
        overlap_size=1,          # Keep the most recent agent's output in full
    )

    app = App(
        name="creative_director",
        root_agent=agent,
        events_compaction_config=compaction_config,
        plugins=[LoggingPlugin()],
    )
    return agent, app

compaction_interval=3 - 3명의 상담사가 완료할 때마다 압축이 실행됩니다. 5개 에이전트 파이프라인의 경우 에이전트 1~3이 실행된 후 한 번 실행되며, Critic과 PM에는 1~3의 요약과 이전 에이전트의 전체 출력이 표시됩니다.

overlap_size=1 - 가장 최근 에이전트의 전체 출력은 항상 그대로 유지되며 요약되지 않습니다. 비평가가 실제 이미지를 로드하고 검토하려면 gcs_uri 값을 포함한 디자이너의 전체 출력이 필요하므로 이 점이 중요합니다. 요약에는 이러한 URI가 포함되지 않습니다.

전체 캠페인 실행 시의 결과:

Agent 1 (Strategist)  → full context
Agent 2 (Copywriter)  → full context
Agent 3 (Designer)    → full context
                        ↓ compaction fires: summarizes agents 1-2, keeps 3 in full
Agent 4 (Critic)      → sees [summary of 1-2] + [full output of 3]
Agent 5 (PM)          → sees [summary of 1-3] + [full output of 4]

RemoteA2aAgentAgentTool 이해하기

RemoteA2aAgent("brand_strategist", agent_card=url)
     
       wraps the remote service so ADK can call it
     
AgentTool(agent=strategist_agent)
     
       exposes it as a callable tool to the LLM
     
Agent(tools=[...])
     
       LLM calls tool("brand_strategist", message=...) when needed
     
brand-strategist-xxxx.run.app   actual HTTP A2A call happens here

LLM은 시스템 요청 사항과 사용자의 요청을 기반으로 각 도구를 호출할 시점을 결정합니다. 조정자는 코드에서 에이전트를 직접 호출하지 않습니다. 모든 것이 LLM의 추론에 의해 이루어집니다.

크리에이티브 디렉터를 로컬에서 테스트

uv run adk web agents --allow_origins='*'

포트 8000에서 웹 미리보기를 엽니다. 에이전트 드롭다운을 사용하여 creative_director를 선택한 후 다음을 시도해 보세요.

Research the eco-friendly water bottle market for health-conscious millennials

크리에이티브 디렉터가 이 문제를 브랜드 전략가에게만 전달하고 브랜드 전략가로부터 응답을 받게 됩니다.

전체 캠페인의 경우 다음을 시도해 보세요.

Create a complete Instagram campaign for SolarPack portable solar charger targeting
outdoor enthusiasts and digital nomads aged 22-35.
Budget $2,000, launch in 2 weeks.

크리에이티브 디렉터가 5명의 전문가를 순서대로 조정하고 각 상담사의 결과가 다음 상담사에게 전달되는 것을 확인할 수 있습니다.

데모: 엔드 투 엔드 캠페인 실행

계속하기 전에 크리에이티브 디렉터 (Ctrl+C)를 중지하세요. A2A 검사기도 포트 8000을 사용합니다.

로컬 테스트가 완료되면 5개의 전문가 서버 (각 터미널의 Ctrl+C)를 중지합니다.

12. 전문가 에이전트 배포 및 테스트

이제 에이전트를 Google Cloud에 배포할 준비가 되었습니다. Cloud Run은 에이전트를 배포하기에 적합한 서비스입니다. 서버리스이며 확장 가능하고 사용하기 쉽습니다. 각 전문가 에이전트는 독립적인 Cloud Run 서비스로 배포됩니다.

배포 구성

각 전문가의 Dockerfile는 다음 패턴을 따릅니다.

FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends gcc curl

# Fast dependency install with uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY pyproject.toml .
RUN uv sync --no-install-project --no-dev

COPY . .
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

ENV PYTHONUNBUFFERED=1 PORT=8080 HOST=0.0.0.0
EXPOSE 8080
CMD ["uv", "run", "python", "agent.py"]

5명의 전문가를 모두 순차적으로 배포

cd ~/ai-creative-studio/workshop/starter
source .env

uv run deploy/deploy_all_specialists.py

이 스크립트는 5개의 에이전트를 한 번에 하나씩 배포합니다 (총 ~10~12분). 순차적 배포는 Cloud Build 폴링 할당량 (60개 요청/분)을 피합니다. 완료되면 각 에이전트의 Cloud Run URL을 .env에 다시 씁니다.

디자이너가 배포되면 스크립트에서 생성된 이미지를 업로드할 수 있도록 GCS 버킷에 디자이너의 Cloud Run 서비스 계정 roles/storage.objectCreator을 자동으로 부여합니다.

.env에서 Notion 사용자 인증 정보를 구성한 경우 스크립트는 Secret Manager (notion-token, notion-project-db-id, notion-tasks-db-id)에도 안전하게 저장하고 일반 환경 변수가 아닌 --set-secrets를 통해 Project Manager 서비스에 삽입합니다. 즉, 토큰은 Cloud Run의 환경 탭이나 gcloud 명령어 기록에 표시되지 않습니다.

배포 확인

배포가 완료되면 스크립트가 Cloud Run URL을 .env에 다시 작성하여 이전 단계의 localhost URL을 대체합니다.

source .env

echo "Deployed URLs:"
echo "  Brand Strategist: $STRATEGIST_AGENT_URL"
echo "  Copywriter:       $COPYWRITER_AGENT_URL"
echo "  Designer:         $DESIGNER_AGENT_URL"
echo "  Critic:           $CRITIC_AGENT_URL"
echo "  Project Manager:  $PM_AGENT_URL"

크리에이티브 디렉터는 다음 단계에서 에이전트 런타임에 배포할 때 이러한 Cloud Run URL을 자동으로 사용합니다.

상담사 카드 확인

배포된 각 에이전트는 /.well-known/agent.json에서 에이전트 카드를 노출합니다. 모든 항목이 게시되었는지 확인하려면 다음을 가져옵니다.

source .env

for agent_url in $STRATEGIST_AGENT_URL $COPYWRITER_AGENT_URL $DESIGNER_AGENT_URL $CRITIC_AGENT_URL $PM_AGENT_URL; do
    echo "=== Agent Card: $agent_url ==="
    curl -s "${agent_url}/.well-known/agent.json" | python3 -m json.tool | grep -E '"name"|"url"|"description"'
    echo ""
done

각 에이전트의 예상 출력:

"name": "brand_strategist",
"url": "https://brand-strategist-xxxx.run.app",
"description": "Brand strategist for market research and competitive insights"

A2A 검사기 (Cloud Run)로 테스트

10단계에서 A2A 검사기가 이미 설치되어 있습니다. 시작합니다.

cd ~/a2a-inspector
bash scripts/run.sh

웹 미리보기 → 포트 변경5001를 엽니다. 연결 필드에 Cloud Run URL을 입력합니다.

https://brand-strategist-xxxx.us-central1.run.app

연결을 클릭합니다. 서비스가 --allow-unauthenticated로 배포되므로 인증 토큰이 필요하지 않습니다.

검사기는 연결하고, 에이전트 카드를 검증하고, A2A를 통해 대화형으로 채팅할 수 있도록 지원합니다.

Cloud Run에 배포된 에이전트 검사

Cloud Run에 배포한 후 공개 HTTPS URL을 가리키는 인스펙터를 사용하여 클라우드 배포가 작동하는지 확인합니다.

Cloud Run 에이전트에 연결된 A2A 검사기

워크플로는 동일합니다. Cloud Run URL을 붙여넣고 연결한 후 테스트 메시지를 보냅니다. 상담사 카드가 로드되고 채팅이 응답하면 전문가가 올바르게 배포되었으며 연락할 수 있습니다.

13. Creative Director를 Agent Runtime에 배포

조정자는 관리 세션 상태, 자동 확장, 기본 제공 추적을 제공하는 Agent Runtime에 배포됩니다.

오케스트레이터에 에이전트 런타임이 필요한 이유

5명의 전문가가 Cloud Run에 배포됩니다. 가볍고 스테이트리스이며 각자 하나의 작업을 처리합니다. 크리에이티브 디렉터의 요구사항은 다음과 같습니다.

요구사항

중요한 이유

세션 상태

다단계 워크플로가 45초 이상 걸립니다. 에이전트 런타임은 오케스트레이터의 도구 호출 간 대화 상태를 유지하므로 파이프라인 중간에 손실되는 것이 없습니다.

가변 부하

시간당 하나의 캠페인이 실행되기도 하고 여러 캠페인이 동시에 실행되기도 합니다. 에이전트 런타임은 유휴 상태일 때 0으로 확장되고 자동으로 확장되므로 유휴 용량에 대한 비용을 지불하지 않습니다.

관측 가능성

Cloud Logging, Cloud Monitoring, Cloud Trace가 기본으로 제공됩니다. 계측을 추가하지 않고도 모든 A2A 호출, 사용된 모든 토큰, 모든 지연 시간 급증을 확인할 수 있습니다.

장기 실행 워크플로

Cloud Run의 요청 제한 시간은 3,600초입니다. Agent Runtime은 관리되는 재시도와 상태 지속성을 통해 몇 분이 걸릴 수 있는 워크플로를 위해 설계되었습니다.

Cloud Run은 스테이트리스 전문가에게 적합한 플랫폼입니다. 상태 저장 조정자에는 에이전트 런타임이 적합한 플랫폼입니다.

오케스트레이터 배포

cd ~/ai-creative-studio/workshop/starter
source .env

uv run deploy/deploy_orchestrator.py --action deploy

5~10분 정도 걸립니다. 완료되면 AGENT_ENGINE_IDAGENT_ENGINE_RESOURCE_NAME.env에 저장됩니다.

source .env
echo "Agent Engine ID: $AGENT_ENGINE_ID"
echo "Resource: $AGENT_ENGINE_RESOURCE_NAME"

배포 작동 방식

client.agent_engines.create()App 객체를 패키징하고 종속 항목과 함께 업로드하며 관리형 인프라에 배포합니다. 각 매개변수의 기능은 다음과 같습니다.

import vertexai
from vertexai import Client, agent_engines

vertexai.init(project=PROJECT_ID, location=LOCATION, staging_bucket=STAGING_BUCKET)

# Wrap the App in an AdkApp adapter - enables tracing in Cloud Trace
adk_app = agent_engines.AdkApp(app=root_app, enable_tracing=True)

# Initialize client and deploy
client = Client(project=PROJECT_ID, location=LOCATION)

agent_engine_resource = client.agent_engines.create(
    agent=adk_app,
    config={
        "staging_bucket": STAGING_BUCKET,   # GCS bucket for packaging artifacts
        "display_name": "Creative Director",
        # Python packages installed in the managed runtime - pin for reproducibility
        "requirements": [
            "google-cloud-aiplatform[agent_engines]>=1.132.0,<2.0.0",
            "google-adk[a2a]==1.31.1",
            "google-genai>=1.70.0",
            "google-cloud-storage>=2.10.0",
            "python-dotenv>=1.0.0",
            "pydantic>=2.0.0",
            "cloudpickle>=3.0.0",
        ],
        # Specialist URLs passed as env vars - the orchestrator reads these at runtime
        "env_vars": {
            "COPYWRITER_AGENT_URL": COPYWRITER_URL,
            "DESIGNER_AGENT_URL":   DESIGNER_URL,
            "STRATEGIST_AGENT_URL": STRATEGIST_URL,
            "CRITIC_AGENT_URL":     CRITIC_URL,
            "PM_AGENT_URL":         PM_URL,
        },
    },
)

resource_name = agent_engine_resource.api_resource.name
agent_engine_id = resource_name.split("/")[-1]

내부적으로 발생하는 일:

1. Agent Engine packages your App + requirements into a container
2. Uploads it to the staging bucket in your project
3. Deploys to managed compute (you never see or manage the VM)
4. Returns a resource name: projects/.../locations/.../reasoningEngines/<id>
5. That ID is saved to .env as AGENT_ENGINE_ID

배포 후 오케스트레이터는 환경 변수의 URL을 통해 5명의 Cloud Run 전문가에게 연결됩니다.

  • 이는 배포 스크립트가 실행되기 전에 .env를 통해 전달됩니다.

14. 전체 캠페인 실행하기

전체 시스템이 배포됩니다. Agent Runtime Playground에서 전체 캠페인을 실행합니다.

에이전트 런타임 실습 모드 열기

  1. https://console.cloud.google.com/agent-platform/runtimes로 이동합니다. Agent Platform > Agents > Deployments에서 Agent Runtime으로 이동할 수도 있습니다.
  2. 배포된 에이전트 런타임 (creative-director)을 선택합니다.
  3. 왼쪽 사이드바에서 Playground를 클릭합니다.
  4. 새 세션을 클릭하여 새 대화를 시작합니다.

전체 캠페인 실행

이 브리핑을 채팅에 붙여넣고 전송합니다.

Create a complete Instagram campaign for:
- Product: EcoFlow Smart Water Bottle (tracks hydration, keeps drinks cold 24h)
- Target Audience: Health-conscious millennials, 25-35 years old
- Platform: Instagram
- Goal: Brand awareness + drive website traffic
- Brand Voice: Motivational, clean, science-backed
- Budget: $3,000
- Timeline: Launch in 2 weeks

크리에이티브 디렉터는 5명의 상담사를 모두 순서대로 실행합니다.

  1. 브랜드 전략가 → 시장 조사, 경쟁업체 분석, 잠재고객 통계
  2. 카피라이터 → 캡션, 해시태그, CTA가 포함된 Instagram 게시물 3개
  3. 디자이너 → 각 게시물에 대해 Gemini를 통해 생성된 시각적 개념 + 실제 이미지 (GCS URI)
  4. 비평가 → 승인됨 / 수정 필요 점수가 있는 품질 검토
  5. (필요한 경우 수정) → 카피라이터 또는 디자이너에게 다시 전화하여 의견을 전달함
  6. 프로젝트 매니저 → 2주 타임라인, 태스크 분류, 예산 할당

데모: Notion 통합으로 실행되는 캠페인

단일 상담사 라우팅 테스트

새 세션에서 다음의 더 짧은 요청을 보냅니다.

Research the luxury skincare market - top brands and trends in 2025

크리에이티브 디렉터가 이 요청을 브랜드 전략가에게만 라우팅합니다. 다른 상담사는 호출되지 않습니다. 시스템 안내의 요청 분류 로직이 올바르게 작동하고 있습니다.

실행 트레이스 검사

콘솔에 있는 동안 다음을 실행합니다.

  1. 왼쪽 사이드바에서 트레이스를 클릭합니다 (Playground 옆).
  2. Trace View에서 방금 실행한 세션의 트레이스를 선택합니다.
  3. 트레이스 트리를 펼쳐 각 에이전트 호출, 입력/출력, 지연 시간, 토큰 사용량을 확인합니다.

전문가에게 이루어진 각 A2A 호출은 별도의 스팬으로 표시됩니다. 크리에이티브 디렉터가 각 에이전트에 전달한 컨텍스트와 다시 받은 컨텍스트를 정확하게 확인할 수 있습니다.

선택사항: 터미널에서 실행

스타터에 이미 포함된 run_campaign.py 스크립트를 사용하여 프로그래매틱 방식으로 캠페인을 실행할 수도 있습니다.

cd ~/ai-creative-studio/workshop/starter
uv run run_campaign.py

15. 삭제

지속적인 요금이 청구되지 않도록 Google Cloud 리소스를 정리합니다.

teardown 스크립트를 실행합니다. 이 스크립트는 .env를 읽고 이 Codelab에서 생성된 모든 항목을 삭제합니다.

bash deploy/teardown_gcp.sh

스크립트에서는 삭제할 항목을 정확하게 표시하고 작업을 실행하기 전에 확인 메시지를 표시합니다.

리소스

삭제되는 항목

Cloud Run 서비스

브랜드 전략가, 카피라이터, 디자이너, 비평가, 프로젝트 관리자

에이전트 런타임

Creative Director 추론 엔진 + 모든 세션

Artifact Registry

cloud-run-source-deploy 저장소 + 모든 Docker 이미지

GCS 버킷

{PROJECT_ID}-campaign-images, {PROJECT_ID}-agent-staging, run-sources-{PROJECT_ID}-{REGION}

Secret Manager

notion-token, notion-project-db-id, notion-tasks-db-id (생성되지 않은 경우 건너뜀)

모든 항목이 삭제되었는지 확인

gcloud run services list --region=us-central1
gcloud storage buckets list --project=$GCP_PROJECT_ID

예상 출력: 빈 목록 또는 기존 리소스만 표시됩니다.

16. 요약

축하합니다. Google Cloud에서 프로덕션급 멀티 에이전트 AI 시스템을 빌드하고 배포했습니다.

빌드한 항목

에이전트

기능

배포

브랜드 전략가

Google 검색을 통한 시장 조사

Cloud Run

카피라이터

Instagram 설명 만들기

Cloud Run

디자이너

Gemini를 통한 이미지 생성 + GCS 업로드

Cloud Run

비평가

점수가 있는 품질 검토

Cloud Run

프로젝트 매니저

타임라인 + Notion MCP

Cloud Run

크리에이티브 디렉터

A2A를 통한 전체 오케스트레이션

에이전트 런타임

학습한 주요 패턴

  1. ADK Agent - 요청 사항 + 선택적 도구를 사용하여 LLM 에이전트 정의
  2. adk web - 내장 채팅 UI를 사용하여 로컬에서 ADK 에이전트를 실행하고 테스트합니다.
  3. SkillToolset - 재사용 가능한 지식을 요청 시 로드되는 모듈식 파일로 패키징
  4. FunctionTool - Python 함수 (또는 외부 모델)를 호출 가능한 에이전트 도구로 래핑
  5. to_a2a() - ADK 에이전트를 A2A 규격 HTTPS 서비스로 노출
  6. RemoteA2aAgent + AgentTool - 호출 가능한 도구로 원격 에이전트 조정
  7. McpToolset - MCP stdio 서버를 통해 외부 서비스에 연결
  8. EventsCompactionConfig - 긴 멀티 에이전트 워크플로에서 토큰 한도 처리
  9. 구조화된 비평가 출력 - 자동 수정이 포함된 기계 가독형 품질 관리
  10. Cloud Run - 컨테이너화된 에이전트를 대규모로 배포
  11. 에이전트 런타임 - 관리 세션 및 추적 기능이 있는 호스트 오케스트레이터

다음 단계

  • gemini-3.1-flash-image-preview의 수정 기능을 사용하여 디자이너에 멀티턴 이미지 수정 추가
  • Cloud Run 서비스에 IAM 인증 추가 (--allow-unauthenticated 삭제)
  • 한 전문가를 LangGraph 또는 CrewAI 에이전트로 대체 - A2A는 프레임워크에 구애받지 않음
  • 참여자가 출력을 평가하고 반복할 수 있도록 사용자 의견을 도구로 추가
  • Cloud 콘솔에서 에이전트 런타임 추적 살펴보기

리소스