From your first Python agent with Gemini and the Agent Development Kit, to a production architecture with sequential agents, parallel agents, and load balancing on Vertex AI Agent Engine.
0 / 10 completed
Level 1 · Foundations
Your first agent with the ADK
Goal: install Google's Agent Development Kit (ADK), configure Gemini, and run your first agent.
The Agent Development Kit (ADK) is Google's open source, code-first framework for building agents with Gemini (or any model via LiteLLM), designed to scale directly to Vertex AI in production.
1. Install the ADK
TERMINAL
pip install google-adk
export GOOGLE_API_KEY="your-gemini-api-key"
# or, if you're using Vertex AI directly:
# export GOOGLE_GENAI_USE_VERTEXAI=true
# export GOOGLE_CLOUD_PROJECT="your-gcp-project"
2. Your first agent
agent.py
from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
agent = LlmAgent(
name="assistant",
model="gemini-2.5-flash",
instruction="Answer briefly and clearly.",
)
session_service = InMemorySessionService()
runner = Runner(agent=agent, app_name="my_app", session_service=session_service)
async def main():
session = await session_service.create_session(app_name="my_app", user_id="u1")
content = types.Content(role="user", parts=[types.Part(text="What is an AI agent, in one sentence?")])
async for event in runner.run_async(user_id="u1", session_id=session.id, new_message=content):
if event.is_final_response():
print(event.content.parts[0].text)
Unlike other SDKs, the ADK explicitly separates the Agent (the definition) from the Runner (who executes it) and the SessionService (where state lives) — this separation matters later when you want to deploy to Agent Engine.
Challenge
Run the same agent with adk web from the terminal to test it in the ADK's local development UI.
Level 2 · Personality
Instructions and model parameters
Goal: control the agent's behavior with instruction and generation config.
agent.py
from google.adk.agents import LlmAgent
from google.genai import types
agent = LlmAgent(
name="support",
model="gemini-2.5-flash",
instruction=(
"You are a technical support assistant. Reply in English, "
"briefly, with numbered steps. If you don't know something, say so."
),
generate_content_config=types.GenerateContentConfig(
temperature=0.3,
max_output_tokens=400,
),
)
The instruction field is the equivalent of a system prompt in other frameworks. You can pass a string, or a function that generates the instruction dynamically based on session state.
Challenge
Replace instruction with a function that includes the current time in the system prompt, so the agent can reason about "now".
Level 3 · Tools
Tools: plain Python functions
Goal: give the agent tools using ordinary Python functions, no decorators required.
A nice quirk of the ADK: any Python function with type hints and a docstring is already a valid tool — no @tool decorator needed, just pass it into the tools=[] list.
agent.py
def current_weather(city: str) -> dict:
"""Returns the current reported weather for a given city.
Args:
city: city name, e.g. "Guatemala City"
Returns:
A dictionary with the weather status.
"""
data = {"Guatemala City": "72°F, partly cloudy"}
return {"weather": data.get(city, "no data available")}
agent = LlmAgent(
name="weather_assistant",
model="gemini-2.5-flash",
instruction="You help look up the weather in cities.",
tools=[current_weather],
)
Challenge
Add a second function, search_product(name: str) -> dict, and confirm that Gemini picks the right tool based on the question.
Level 4 · Memory
Sessions and state
Goal: maintain context across turns using the same session_id, and understand session state (session.state).
chat_with_memory.py
session = await session_service.create_session(app_name="my_app", user_id="customer-42")
async def speak(text):
content = types.Content(role="user", parts=[types.Part(text=text)])
async for event in runner.run_async(user_id="customer-42", session_id=session.id, new_message=content):
if event.is_final_response():
return event.content.parts[0].text
print(await speak("I'm looking for a laptop for graphic design"))
print(await speak("Which of the ones you mentioned is the cheapest?"))
# Both calls share the same session.id, so history is preserved
Beyond message history, session.state is a dictionary your tools can read and write — useful for storing user preferences or intermediate results between agents (you'll use this in Level 8).
Challenge
Save the user's name in session.state the first time they mention it, and have the agent use it in later replies.
Level 5 · Structured output
output_schema with Pydantic
Goal: force a validated JSON response with output_schema.
agent.py
from pydantic import BaseModel
from google.adk.agents import LlmAgent
class SupportTicket(BaseModel):
category: str
urgency: str
summary: str
agent = LlmAgent(
name="classifier",
model="gemini-2.5-flash",
instruction="Classify the incoming support ticket.",
output_schema=SupportTicket,
output_key="classified_ticket", # saved to session.state
)
Watch out for this ADK quirk: once you set output_schema, the agent can no longer use tools in that same call — the model focuses exclusively on producing the JSON. If you need tools and structured output at once, split the work across two agents (one that researches with tools, another that structures the final result).
Challenge
Build a 2-agent flow: one with tools that solves the task, and a second with output_schema that only formats the first one's response.
Level 6 · Streaming
Real-time events with run_async
Goal: process agent events as they happen, not just the final response.
runner.run_async() already returns a stream of events — what you did in earlier levels by filtering only for is_final_response() was actually discarding useful intermediate events (tool calls, partial responses).
stream.py
async for event in runner.run_async(user_id="u1", session_id=session.id, new_message=content):
if event.get_function_calls():
for call in event.get_function_calls():
print(f"[tool] calling {call.name}({call.args})")
elif event.content and event.content.parts:
for part in event.content.parts:
if part.text:
print(part.text, end="", flush=True)
For real-time audio/voice, the ADK also exposes run_live(), designed for low-latency, bidirectional conversations.
Challenge
Wrap this event loop in an async generator and expose it over a WebSocket for a live chat.
Level 7 · Multi-agent
sub_agents: your first team of agents
Goal: compose several specialized agents using sub_agents, with dynamic delegation.
The ADK supports multi-agent natively: a root agent with sub_agents can hand off control to one of them based on the model's decision — the right sub-agent gets activated based on its description, just like a tool.
agent_team.py
from google.adk.agents import LlmAgent
sales_agent = LlmAgent(
name="sales",
model="gemini-2.5-flash",
description="Specialist in pricing and product questions.",
instruction="Answer sales questions concisely.",
)
support_agent = LlmAgent(
name="support",
model="gemini-2.5-flash",
description="Specialist in technical errors and bugs.",
instruction="Help resolve technical issues step by step.",
)
root_agent = LlmAgent(
name="front_desk",
model="gemini-2.5-flash",
instruction="Analyze each message and delegate to the right sub-agent.",
sub_agents=[sales_agent, support_agent],
)
The root_agent never resolves domain questions itself — its only job is deciding which of its sub_agents to hand the turn to.
Challenge
Add a third billing sub-agent and test a conversation where the root agent correctly transfers based on detected intent.
Level 8 · Orchestrator
SequentialAgent and ParallelAgent
Goal: use deterministic orchestration with workflow agents, instead of leaving the order up to the model.
When execution order should not depend on the model's reasoning (for example: always research → draft → review, in that exact order), the ADK offers deterministic workflow agents: SequentialAgent runs its children in fixed order, ParallelAgent runs them at the same time.
orchestrator.py
from google.adk.agents import LlmAgent, SequentialAgent, ParallelAgent
researcher = LlmAgent(
name="researcher", model="gemini-2.5-flash",
instruction="Research concise technical facts about the topic.",
output_key="research", # available at session.state["research"]
)
writer = LlmAgent(
name="writer", model="gemini-2.5-flash",
instruction="Use {research} from session.state to write a clear paragraph.",
output_key="draft",
)
editor = LlmAgent(
name="editor", model="gemini-2.5-flash",
instruction="Fix grammar and clarity in {draft} from session.state.",
)
pipeline = SequentialAgent(
name="content_pipeline",
sub_agents=[researcher, writer, editor],
)
# Each agent passes its output to the next automatically via session.state
With ParallelAgent, several sub-agents run in separate threads but share the same session.state — which is why each one must write to a different output_key, to avoid race conditions.
Challenge
Replace the SequentialAgent with a ParallelAgent running the researcher alongside a second researcher (a different source), and compare execution times.
Level 9 · Production
Evaluation and observability
Goal: evaluate the agent's quality and trace its behavior before deploying it.
TERMINAL
adk eval my_agent_package path/to/eval_set.json
# Runs the agent against defined test cases and compares
# the actual response vs. the expected one, with quality metrics
The ADK ships a built-in evaluation framework (adk eval) to run regressions before each agent release
The local development UI (adk web) shows every tool call and every reasoning step for debugging
Basic guardrails: cap max_output_tokens, validate user input before passing it to the agent, and use before_model_callback / after_model_callback to inspect or block content
Challenge
Write a before_model_callback that blocks any message containing a word from a banned list, before it reaches the model.
Level 10 · Final architecture
Vertex AI Agent Engine + load balancing
Goal: deploy the full pipeline to Vertex AI Agent Engine, with auto-scaling and distributed load.
Google recommends deploying ADK agents directly to Vertex AI Agent Engine: a fully managed runtime that handles horizontal scaling for you — you don't build the load balancer yourself, you specify it as deployment configuration.
1. Deploy the pipeline to Agent Engine
deploy.py
from vertexai import agent_engines
app = agent_engines.create(
agent_engine=pipeline, # the SequentialAgent from Level 8
requirements=["google-adk", "google-cloud-aiplatform"],
display_name="content-pipeline-prod",
min_instances=2, # never fewer than 2 active replicas (avoids cold starts)
max_instances=20, # auto-scaling ceiling for traffic spikes
)
print(app.resource_name)
2. Load balancing across regional deployments
For multi-region architectures, you put several Agent Engine deployments (one per region) behind a global Google Cloud Load Balancer, which routes each request to the region with the lowest available latency/load:
With that, you've got the full path: from a few-line LlmAgent in Level 1, to a multi-agent pipeline deployed across multiple regions, with auto-scaling and continuous quality evaluation.
Final challenge
Deploy the Level 8 pipeline to Agent Engine with min_instances=2 and measure cold-start time compared to min_instances=0.