ADK 및 CloudSQL을 사용한 지속적인 AI 에이전트 빌드

1. 소개

이 실습에서는 기본 상태 비저장 챗봇을 넘어 친절한 바리스타 역할을 하는 Gemini 기반 AI 에이전트인 Smart Cafe Concierge를 만듭니다. 세션 상태에서 추적된 커피 주문을 가져오고, 사용자 범위 상태에서 장기적인 식단 선호도를 기억하고, 모든 것을 Cloud SQL PostgreSQL 데이터베이스에 유지합니다. 결국 애플리케이션을 다시 시작하고 완전히 새로운 대화를 시작한 후에도 상담사는 사용자가 유당 불내증이 있다는 사실을 기억합니다.

빌드할 시스템 아키텍처는 다음과 같습니다.

a98bbd65ddedd29c.jpeg

기본 요건

  • 평가판 결제 계정이 있는 Google Cloud 계정
  • Python에 대한 기본적인 지식
  • ADK, AI 에이전트 또는 Cloud SQL에 대한 사전 경험이 필요하지 않습니다.

학습할 내용

  • 맞춤 도구를 사용하여 Google의 에이전트 개발 키트 (ADK)로 AI 에이전트 만들기
  • ToolContext를 통해 세션 상태를 읽고 쓰는 도구 정의
  • 세션 범위 상태와 사용자 범위 상태 구분 (user: 접두사)
  • Cloud SQL PostgreSQL 인스턴스를 프로비저닝하고 Cloud Shell에서 연결
  • adk web 명령어를 사용할 때 기본값인 로컬 스토리지에서 전용 데이터베이스가 있는 영구 스토리지인 DatabaseSessionService로 이전
  • 에이전트 메모리가 애플리케이션 재시작과 별도의 대화 세션 간에 유지되는지 확인합니다.

필요한 항목

  • 작동하는 컴퓨터와 안정적인 인터넷 연결
  • Google Cloud 콘솔에 액세스하기 위한 브라우저(예: Chrome)
  • 호기심과 배우고자 하는 열의

2. 환경 설정

이 단계에서는 Cloud Shell 환경을 준비하고 Google Cloud 프로젝트를 구성합니다.

Cloud Shell 열기

브라우저에서 Cloud Shell을 엽니다. Cloud Shell은 이 Codelab에 필요한 모든 도구가 사전 구성된 환경을 제공합니다. 메시지가 표시되면 승인을 클릭합니다.

인터페이스는 다음과 유사하게 표시됩니다.

86307fac5da2f077.png

이것이 기본 인터페이스가 됩니다. 상단에는 IDE가 있고 하단에는 터미널이 있습니다.

작업 디렉터리 설정하기

작업 디렉터리를 만듭니다. 이 Codelab에서 작성하는 모든 코드는 참조 저장소와 별도로 여기에 있습니다.

# Create your working directory
mkdir -p ~/build-agent-adk-cloudsql

# Change cloudshell workspace and working directory into previously created dir
cloudshell workspace ~/build-agent-adk-cloudsql && cd ~/build-agent-adk-cloudsql

터미널을 생성하려면 보기 -> 터미널을 찾습니다.

ccc3214812750f1c.png

Google Cloud 프로젝트 및 초기 환경 변수 설정

프로젝트 설정 스크립트를 작업 디렉터리에 다운로드합니다.

curl -sL https://raw.githubusercontent.com/alphinside/cloud-trial-project-setup/main/setup_verify_trial_project.sh -o setup_verify_trial_project.sh

스크립트를 실행합니다. 무료 체험 결제 계정을 확인하고, 새 프로젝트를 만들거나 기존 프로젝트를 검증하고, 현재 디렉터리의 .env 파일에 프로젝트 ID를 저장하고, 터미널에서 활성 프로젝트를 설정합니다.

bash setup_verify_trial_project.sh && source .env

이 명령어를 실행하면 추천 프로젝트 ID 이름이 표시됩니다. Enter 키를 눌러 계속할 수 있습니다.

54b615cd15f2a535.png

잠시 기다린 후 콘솔에 다음 출력이 표시되면 다음 단계로 진행할 수 있습니다. e576b4c13d595156.png

실행된 스크립트는 다음 단계를 수행합니다.

  1. 활성 상태의 무료 체험 결제 계정이 있는지 확인
  2. .env에 기존 프로젝트가 있는지 확인합니다 (있는 경우).
  3. 새 프로젝트를 만들거나 기존 프로젝트를 재사용합니다.
  4. 무료 체험 결제 계정을 프로젝트에 연결
  5. 프로젝트 ID를 .env에 저장
  6. 프로젝트를 활성 gcloud 프로젝트로 설정

Cloud Shell 터미널 프롬프트의 작업 디렉터리 옆에 있는 노란색 텍스트를 확인하여 프로젝트가 올바르게 설정되었는지 확인합니다. 프로젝트 ID가 표시되어야 합니다.

9e11ee21cd23405f.png

필요한 API 사용 설정

이 Codelab에 필요한 Google Cloud API를 사용 설정합니다.

gcloud services enable \
  aiplatform.googleapis.com \
  sqladmin.googleapis.com \
  compute.googleapis.com
  • Vertex AI API (aiplatform.googleapis.com): 에이전트가 Vertex AI를 통해 Gemini 모델을 사용합니다.
  • Cloud SQL Admin API (sqladmin.googleapis.com) - 영구 스토리지를 위해 PostgreSQL 인스턴스를 프로비저닝하고 관리합니다.
  • Compute Engine API (compute.googleapis.com) - Cloud SQL 인스턴스를 만드는 데 필요합니다.

Gemini 및 Cloud 제품 리전 구성

계속하기 전에 상호작용하는 제품에 필요한 위치/지역 구성도 설정해 보겠습니다. .env 파일에 다음 구성을 추가합니다.

# This is for our Gemini endpoint
echo "GOOGLE_CLOUD_LOCATION=global" >> .env

# This is for our other Cloud products
echo "REGION=us-central1" >> .env

source .env

다음 단계로 계속 진행해 보겠습니다.

3. Cloud SQL 설정

이 단계에서는 Cloud SQL PostgreSQL 인스턴스를 프로비저닝하고 에이전트를 인메모리에서 데이터베이스 지원 스토리지로 전환합니다. 인스턴스를 만드는 데 몇 분 정도 걸리므로 먼저 인스턴스를 만들고 인스턴스가 만들어지는 동안 다음 주제에 대해 계속 논의할 수 있습니다.

인스턴스 생성 시작

데이터베이스 비밀번호를 .env 파일에 추가하고 다시 로드합니다. 비밀번호로 cafe-agent-pwd-2025를 사용합니다.

echo "DB_PASSWORD=cafe-agent-pwd-2025" >> .env
source .env

이 명령어를 실행하여 Cloud SQL PostgreSQL 인스턴스를 만듭니다. 완료하는 데 몇 분 정도 걸리므로 실행 상태로 두고 다음 섹션으로 계속 진행하세요.

gcloud sql instances create cafe-concierge-db \
  --database-version=POSTGRES_17 \
  --edition=ENTERPRISE \
  --region=${REGION} \
  --availability-type=ZONAL \
  --project=${GOOGLE_CLOUD_PROJECT} \
  --tier=db-f1-micro \
  --root-password=${DB_PASSWORD} \
  --quiet &

위 명령어에 관한 몇 가지 참고사항은 다음과 같습니다.

  • db-f1-micro는 가장 작고 저렴한 Cloud SQL 등급으로, 이 Codelab에 적합합니다.
  • --root-password는 기본 postgres 사용자의 비밀번호를 설정합니다.
  • 명령어의 접미사 &는 백그라운드에서 명령어를 실행하므로 계속 작업할 수 있습니다.

이 프로세스는 백그라운드에서 실행되지만 콘솔 출력은 현재 터미널에 가끔 표시됩니다. 더 집중할 수 있도록 Cloud Shell에서 새 터미널 탭을 엽니다 (+ 아이콘 클릭).

b01e3fbd89f17332.png

작업 디렉터리로 다시 이동하여 이전 설정 스크립트를 사용하여 프로젝트를 활성화합니다.

cd ~/build-agent-adk-cloudsql
bash setup_verify_trial_project.sh && source .env

다음 섹션으로 계속 진행하겠습니다.

4. 카페 컨시어지 에이전트 빌드

이 단계에서는 ADK 에이전트의 프로젝트 구조를 만들고 메뉴 도구를 사용하여 기본 카페 컨시어지를 정의합니다.

Python 프로젝트 초기화

이 Codelab에서는 가상 환경과 종속 항목을 하나의 도구로 처리하는 빠른 Python 패키지 관리자인 uv를 사용합니다. 이 도구는 Cloud Shell에 사전 설치되어 있습니다.

Python 프로젝트를 초기화하고 ADK를 종속 항목으로 추가합니다.

uv init
uv add google-adk==1.25.0 asyncpg

uv initpyproject.toml 및 가상 환경을 만듭니다. uv add는 종속 항목을 설치하고 pyproject.toml에 기록합니다.

에이전트 프로젝트 구조 초기화

ADK는 특정 폴더 레이아웃을 예상합니다. 에이전트 디렉터리 내에 __init__.py, agent.py, .env가 포함된 에이전트 이름을 딴 디렉터리

ADK에는 이를 빠르게 설정하는 데 도움이 되는 명령어가 내장되어 있습니다. 다음 명령어를 실행하세요.

uv run adk create cafe_concierge \
    --model gemini-2.5-flash \
    --project ${GOOGLE_CLOUD_PROJECT} \
    --region ${GOOGLE_CLOUD_LOCATION}

이 명령어는 gemini-2.5-flash을 브레인으로 사용하여 에이전트 구조를 만듭니다. 이제 디렉터리가 다음과 같이 표시됩니다.

build-agent-adk-cloudsql/
├── cafe_concierge/
│   ├── __init__.py
│   ├── agent.py
│   └── .env
├── pyproject.toml
├── .env      
├── .venv/
└── ...

에이전트 작성

Cloud Shell 편집기에서 cafe_concierge/agent.py 열기

cloudshell edit cafe_concierge/agent.py

다음 코드로 파일을 덮어씁니다.

# cafe_concierge/agent.py
from google.adk.agents import LlmAgent
from google.adk.tools import ToolContext

CAFE_MENU = {
    "espresso": {
        "price": 3.50,
        "description": "Rich and bold single shot",
        "tags": ["vegan", "dairy-free", "gluten-free"],
    },
    "latte": {
        "price": 5.00,
        "description": "Espresso with steamed milk",
        "tags": ["gluten-free"],
    },
    "oat milk latte": {
        "price": 5.50,
        "description": "Espresso with steamed oat milk",
        "tags": ["vegan", "dairy-free", "gluten-free"],
    },
    "cappuccino": {
        "price": 4.50,
        "description": "Espresso with equal parts steamed milk and foam",
        "tags": ["gluten-free"],
    },
    "cold brew": {
        "price": 4.00,
        "description": "Slow-steeped for 12 hours, served over ice",
        "tags": ["vegan", "dairy-free", "gluten-free"],
    },
    "matcha latte": {
        "price": 5.50,
        "description": "Ceremonial grade matcha with steamed milk",
        "tags": ["gluten-free"],
    },
    "croissant": {
        "price": 3.00,
        "description": "Buttery, flaky French pastry",
        "tags": [],
    },
    "banana bread": {
        "price": 3.50,
        "description": "Homemade with walnuts",
        "tags": ["vegan"],
    },
}


def get_menu() -> dict:
    """Returns the full cafe menu with prices, descriptions, and dietary tags.

    Use this tool when the customer asks what's available, wants to see
    the menu, or asks about specific items.
    """
    return CAFE_MENU


root_agent = LlmAgent(
    name="cafe_concierge",
    model="gemini-2.5-flash",
    instruction="""You are a friendly and knowledgeable barista at "The Cloud Cafe".

Your job:
- Help customers browse the menu and answer questions about items.
- Take coffee and food orders.
- Remember and respect dietary preferences.

Be conversational, warm, and concise. If a customer mentions a dietary
restriction, acknowledge it and suggest suitable options from the menu.
""",
    tools=[get_menu],
)

이 코드는 get_menu()라는 도구 하나가 있는 기본 에이전트를 정의합니다. 에이전트는 메뉴에 관한 질문에 답변할 수 있지만 아직 주문을 추적하거나 환경설정을 기억할 수는 없습니다.

에이전트가 실행되는지 확인

작업 디렉터리에서 ADK 개발 UI를 시작합니다.

cd ~/build-agent-adk-cloudsql
uv run adk web

Cloud Shell의 웹 미리보기 기능을 사용하여 터미널에 표시된 URL (일반적으로 http://localhost:8000)을 엽니다. 왼쪽 상단의 에이전트 드롭다운에서 cafe_concierge를 선택합니다.

채팅 표시줄에 다음 텍스트를 입력하고 에이전트가 메뉴 항목과 가격으로 응답하는지 확인합니다.

What's on the menu?

376ee6b189657e7a.png

계속하기 전에 Ctrl+C를 사용하여 개발 UI를 중지합니다.

5. 상태 저장 주문 관리 추가

에이전트가 메뉴를 표시할 수는 있지만 주문을 받거나 환경설정을 기억할 수는 없습니다. 이 단계에서는 ADK의 상태 시스템을 사용하여 대화 내에서 주문을 추적하고 여러 대화에서 식단 선호도를 저장하는 네 가지 도구를 추가합니다.

세션 이벤트 및 상태 이해하기

모든 ADK 대화는 Session 객체 내에 있습니다. 세션은 이벤트상태라는 두 가지 항목을 추적합니다. 이 차이점을 이해하는 것은 올바른 방식으로 올바른 사항을 기억하는 에이전트를 빌드하는 데 중요합니다.

이벤트는 대화에서 발생하는 모든 일의 시간순 로그입니다. 모든 사용자 메시지, 모든 에이전트 응답, 모든 도구 호출 및 반환 값은 각각 Event로 기록되고 세션의 events 목록에 추가됩니다. 이벤트는 변경할 수 없습니다. 기록된 후에는 변경되지 않습니다. 이벤트를 대화의 전체 스크립트라고 생각하면 됩니다.

상태는 대화 중에 에이전트가 읽고 쓰는 키-값 스크래치패드입니다. 이벤트와 달리 상태는 변경 가능합니다. 대화가 진행됨에 따라 값이 변경됩니다. 상태는 에이전트가 조치를 취하는 데 필요한 구조화된 데이터를 저장하는 곳입니다(현재 주문, 고객의 선호도, 누적 합계). 상태를 상담사가 스크립트 옆에 보관하는 메모라고 생각하세요.

두 용어의 관계는 다음과 같습니다.

cd9871699451867d.png

도구는 ToolContext를 통해 상태를 읽고 씁니다. ToolContext는 ADK가 매개변수로 선언된 도구 함수에 자동으로 삽입하는 객체입니다. 직접 만들지 않습니다. tool_context.state를 통해 도구는 세션의 상태 스크래치패드를 읽고 쓸 수 있습니다. ADK는 함수 서명을 검사합니다. ToolContext 유형의 매개변수는 삽입되고 다른 모든 매개변수는 대화를 기반으로 LLM에 의해 채워집니다.

도구가 tool_context.state에 쓰면 ADK는 이벤트 내에서 해당 변경사항을 state_delta로 기록합니다. 그런 다음 SessionService가 세션의 현재 상태에 델타를 적용합니다. 즉, 상태 변경은 항상 상태 변경을 유발한 이벤트로 추적할 수 있습니다. callback_context와 같은 다른 형태의 컨텍스트도 마찬가지입니다.

상태 접두사 이해하기

상태 키는 접두사를 사용하여 범위를 제어합니다.

프리픽스

범위

다시 시작 후에도 유지되나요? (DB 포함)

(없음)

현재 세션에만

user:

이 사용자의 모든 세션

app:

모든 세션, 모든 사용자

temp:

현재 호출만

아니요

이 Codelab에서는 세션 범위 데이터 (현재 주문 - 이 대화에만 관련됨)의 접두사가 없는 키와 사용자 범위 데이터 (식단 선호도 - 이 사용자의 모든 대화에 관련됨)의 user: 키라는 두 가지 접두사를 사용합니다.

스테이트풀 도구 추가

Cloud Shell 편집기에서 cafe_concierge/agent.py를 엽니다.

cloudshell edit cafe_concierge/agent.py

그런 다음 root_agent 정의 에 다음 네 가지 함수를 추가합니다.

# cafe_concierge/agent.py (add below get_menu, above root_agent)

def place_order(tool_context: ToolContext, items: list[str]) -> dict:
    """Places an order for the specified menu items.

    Use this tool when the customer confirms they want to order something.

    Args:
        tool_context: Provided automatically by ADK.
        items: A list of menu item names the customer wants to order.
    """
    valid_items = []
    invalid_items = []
    total = 0.0

    for item in items:
        item_lower = item.lower()
        if item_lower in CAFE_MENU:
            valid_items.append(item_lower)
            total += CAFE_MENU[item_lower]["price"]
        else:
            invalid_items.append(item)

    if not valid_items:
        return {"error": f"None of these items are on our menu: {invalid_items}"}

    order = {"items": valid_items, "total": round(total, 2)}
    tool_context.state["current_order"] = order

    result = {"order": order}
    if invalid_items:
        result["warning"] = f"These items are not on our menu: {invalid_items}"
    return result


def get_order_summary(tool_context: ToolContext) -> dict:
    """Returns the current order summary for this session.

    Use this tool when the customer asks about their current order,
    wants to review what they ordered, or asks for the total.

    Args:
        tool_context: Provided automatically by ADK.
    """
    order = tool_context.state.get("current_order")
    if order:
        return {"order": order}
    return {"message": "No order has been placed yet in this session."}


def set_dietary_preference(tool_context: ToolContext, preference: str) -> dict:
    """Saves a dietary preference that persists across all conversations.

    Use this tool when the customer mentions a dietary restriction or
    preference (e.g., "I'm vegan", "I'm lactose intolerant",
    "I have a nut allergy").

    Args:
        tool_context: Provided automatically by ADK.
        preference: The dietary preference to save (e.g., "vegan",
            "lactose intolerant", "nut allergy").
    """
    existing = tool_context.state.get("user:dietary_preferences", [])
    if not isinstance(existing, list):
        existing = []

    preference_lower = preference.lower().strip()
    if preference_lower not in existing:
        existing.append(preference_lower)

    tool_context.state["user:dietary_preferences"] = existing
    return {
        "saved": preference_lower,
        "all_preferences": existing,
    }


def get_dietary_preferences(tool_context: ToolContext) -> dict:
    """Retrieves the customer's saved dietary preferences.

    Use this tool when you need to check the customer's dietary
    restrictions before making recommendations.

    Args:
        tool_context: Provided automatically by ADK.
    """
    preferences = tool_context.state.get("user:dietary_preferences", [])
    if preferences:
        return {"preferences": preferences}
    return {"message": "No dietary preferences saved yet."}

다음 두 가지 사항을 참고하세요.

  1. place_orderget_order_summary는 접두사가 없는 키 (current_order)를 사용합니다. 이 상태는 현재 세션에 연결되어 있으며 새 대화는 빈 주문으로 시작됩니다.
  2. set_dietary_preferenceget_dietary_preferencesuser: 접두사 (user:dietary_preferences)를 사용합니다. 이 상태는 동일한 사용자의 모든 세션에서 공유됩니다.

새 도구와 안내로 에이전트 업데이트

파일 하단의 기존 root_agent 정의를 다음으로 바꿉니다.

# cafe_concierge/agent.py (replace the existing root_agent)

root_agent = LlmAgent(
    name="cafe_concierge",
    model="gemini-2.5-flash",
    instruction="""You are a friendly and knowledgeable barista at "The Cloud Cafe".

Your job:
- Help customers browse the menu and answer questions about items.
- Take coffee and food orders.
- Remember and respect dietary preferences.

The customer's saved dietary preferences are: {user:dietary_preferences?}

IMPORTANT RULES:
- When a customer mentions a dietary restriction, ALWAYS save it using the
  set_dietary_preference tool before doing anything else.
- Before recommending items, check the customer's dietary preferences. If they
  have preferences saved, only recommend items compatible with those
  restrictions. Check the menu item tags to determine compatibility.
- When placing an order, confirm the items and total with the customer.

Be conversational, warm, and concise.
""",
    tools=[
        get_menu,
        place_order,
        get_order_summary,
        set_dietary_preference,
        get_dietary_preferences,
    ],
)

이 안내에서는 상태 삽입 템플릿 {user:dietary_preferences?}를 사용하여 이 고객의 저장된 환경설정을 프롬프트에 직접 삽입합니다.

전체 파일 확인하기

이제 cafe_concierge/agent.py에 다음이 포함됩니다.

  • CAFE_MENU 사전
  • 5가지 도구 기능: get_menu, place_order, get_order_summary, set_dietary_preference, get_dietary_preferences
  • 다섯 가지 도구가 모두 포함된 root_agent 정의

6. ADK 개발 UI로 에이전트 테스트

이 단계에서는 에이전트를 실행하고 모든 상태 저장 기능(주문, 환경설정 추적, 교차 세션 메모리(동일한 프로세스 내))을 연습합니다. 또한 이벤트 및 상태 패널을 검사하여 ADK가 내부적으로 대화를 추적하는 방법을 확인합니다.

개발 UI 시작

cd ~/build-agent-adk-cloudsql
uv run adk web

포트 8000에서 웹 미리보기를 열고 드롭다운에서 cafe_concierge를 선택합니다.

대화 1: 주문하고 환경설정 지정하기

다음 프롬프트를 순서대로 사용해 보세요.

What's on the menu?
I'm lactose intolerant
What would you recommend?
I'll have an oat milk latte and a banana bread
What's my order?

세션 이벤트 검사

이벤트가 모두 캡처되어 웹 UI에 표시됩니다. 채팅 상자에는 프롬프트와 대답뿐만 아니라 tool_calltool_response도 표시됩니다.

9051b46978c8017b.png

이벤트 목록이 순서대로 표시됩니다. 각 이벤트에는 작성자 (이벤트를 생성한 사용자)와 유형 (이벤트가 나타내는 상호작용의 종류)이 있습니다.

저자

유형

의미

user

message

채팅에 입력한 메시지

cafe_concierge

message

상담사의 텍스트 응답

cafe_concierge

tool_call

에이전트가 도구를 호출하기로 결정함 (함수 이름 + 인수를 표시함)

cafe_concierge

tool_response

도구 호출의 반환 값

tool_call 이벤트 중 하나(예: set_dietary_preference 호출)를 클릭합니다. 다음과 같이 표시됩니다.

  • 함수 이름: set_dietary_preference
  • Arguments(인수): {"preference": "lactose intolerant"}

이제 바로 아래에 있는 해당 tool_response 이벤트를 클릭합니다. 반환 값이 표시됩니다.

  • 대답: {"saved": "lactose intolerant", "all_preferences": ["lactose intolerant"]}

b528f4efd6a9f337.png

tool_response 이벤트 내에서 state_delta 필드를 찾습니다. 이 도구 호출의 결과로 변경된 상태를 정확하게 보여줍니다.

state_delta: {"user:dietary_preferences": ["lactose intolerant"]}

모든 상태 변경은 특정 이벤트로 추적할 수 있습니다. 이러한 방식으로 ADK는 상태 스크래치패드가 대화 기록과 동기화된 상태를 유지하도록 합니다.

세션 상태 검사

상태 탭을 클릭합니다. 전체 기록을 보여주는 이벤트 로그와 달리 상태 탭에는 상담사가 현재 알고 있는 내용의 스냅샷, 즉 모든 상태 키의 현재 값이 표시됩니다.

5e06fb54f3f0d8d6.png

다음과 같이 두 개의 항목이 표시됩니다.

  • current_order{"items": ["oat milk latte", "banana bread"], "total": 9.0}
  • user:dietary_preferences["lactose intolerant"]

키 이름의 차이점을 확인하세요.

  • current_order에는 접두사가 없습니다. 세션 범위입니다. 이 대화에만 존재하며 세션이 종료되면 사라집니다.
  • user:dietary_preferences에는 user: 접두사가 있습니다. 사용자 범위입니다. 이 사용자의 모든 세션에서 공유됩니다.

이 구분은 코드에서 보이지 않지만 (둘 다 tool_context.state 사용) 데이터가 도달하는 범위를 제어합니다. 다음 테스트에서 이 내용을 확인할 수 있습니다.

대화 2: 교차 세션 사용자 상태 확인

개발 UI에서 새 세션 버튼을 클릭하여 새 대화를 시작합니다. 이렇게 하면 동일한 사용자에 대한 새 세션이 생성됩니다.

57408cfae5f041ac.png

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

What do you recommend for me?

새 세션에서 상태 탭을 확인합니다. user:dietary_preferences 키는 유지되지만 current_order는 사라집니다. 이 상태는 이전 세션에 연결되어 있었기 때문입니다.

764eb3885251307d.png

7. 로컬 스토리지 제한 준수

에이전트는 세션 간에 환경설정을 기억하지만 로컬 저장소가 있는 동안에만 기억합니다. 이 단계에서는 로컬 스토리지의 근본적인 제한사항을 보여줍니다.

에이전트 다시 시작

이전 단계의 끝에서 개발 UI를 중지했습니다. 이제 로컬 스토리지를 삭제하고 다시 시작하여 상태가 없는 서버리스 환경을 시뮬레이션합니다.

cd ~/build-agent-adk-cloudsql
rm -f cafe_concierge/.adk/session.db
uv run adk web

이제 포트 8000에서 웹 미리보기를 열고 cafe_concierge를 선택합니다.

테스트 선호도 리콜

유형:

Do you remember my dietary preferences?

상담사가 기억하지 못합니다. 선호하는 음식, 주문 내역 등 모든 정보가 사라집니다.

82a5e05434cafe83.png

서버리스 환경을 사용할 때 일반적으로 발생하는 로컬 스토리지를 삭제하면 모든 항목이 완전히 삭제되었습니다. session.db은 모든 상태를 프로세스 메모리에 저장합니다. 삭제하면 모든 데이터가 삭제됩니다.

해결 방법: 이 튜토리얼에서 모든 세션 데이터를 Cloud SQL 데이터베이스의 PostgreSQL에 저장하는 DatabaseSessionService를 지정합니다. 에이전트 코드와 도구는 정확히 동일하게 유지되며 스토리지 백엔드만 변경됩니다.

계속하기 전에 Ctrl+C를 사용하여 개발자 UI를 중지합니다.

8. 데이터베이스 설정 다시 확인

이 시점에서 데이터베이스 인스턴스 생성이 이미 완료되어야 합니다. 다음 명령어를 실행하여 확인해 보겠습니다.

gcloud sql instances describe cafe-concierge-db --format="value(state)"

다음과 같은 출력이 표시되면 완료로 표시합니다.

RUNNABLE

데이터베이스 만들기

에이전트의 세션 데이터를 위한 전용 데이터베이스를 만듭니다.

gcloud sql databases create agent_db --instance=cafe-concierge-db

Cloud SQL 인증 프록시 시작

Cloud SQL 인증 프록시는 IP 주소를 허용 목록에 추가하지 않고도 Cloud Shell에서 Cloud SQL 인스턴스로의 안전한 인증된 연결을 제공합니다. Cloud Shell에 이미 사전 설치되어 있습니다.

cloud-sql-proxy ${GOOGLE_CLOUD_PROJECT}:${REGION}:cafe-concierge-db --port 5432 &

명령어의 & 접미사는 프록시가 백그라운드에서 실행되도록 합니다. 아래와 같이 프록시가 준비되었음을 확인하는 출력이 표시됩니다.

[your-project-id:your-region:cafe-concierge-db] Listening on 127.0.0.1:5432
The proxy has started successfully and is ready for new connections!

연결 확인하기

프록시를 통해 데이터베이스에 연결할 수 있는지 테스트합니다.

psql "host=127.0.0.1 port=5432 dbname=agent_db user=postgres password=$DB_PASSWORD" -c "SELECT 'Connection ok' AS status;"

다음과 같이 표시됩니다.

      status
---------------------
 Connection ok
(1 row)

9. 세션 간 영구 메모리 확인

이 단계에서는 cafe_concierge/.adk/session_db (로컬 데이터베이스)가 삭제되고 대화 세션에 걸쳐 있는지 확인하여 에이전트의 메모리가 재설정 후에도 유지됨을 증명합니다.

에이전트 시작

Cloud SQL 인증 프록시가 계속 실행 중인지 확인합니다 (작업으로 확인). 그렇지 않은 경우 다시 시작합니다.

if ss -tlnp | grep -q ':5432 '; then
  echo "Cloud SQL Auth Proxy is already running."
else
  cloud-sql-proxy ${GOOGLE_CLOUD_PROJECT}:${REGION}:cafe-concierge-db --port 5432 &
fi

그런 다음 데이터베이스를 세션 서비스로 지정하여 ADK 개발 UI를 시작합니다.

uv run adk web --session_service_uri postgresql+asyncpg://postgres:${DB_PASSWORD}@127.0.0.1:5432/agent_db

포트 8000에서 웹 미리보기를 열고 cafe_concierge를 선택합니다.

테스트 1: 주문하고 환경설정 지정하기

첫 번째 세션에서 다음 프롬프트를 실행합니다.

Show me the menu
I'm vegan
What can I eat?
I'll have a cold brew and banana bread

테스트 2: 다시 시작 후 생존

Ctrl+C로 개발 UI를 중지하고 로컬 session.db가 삭제되었는지 확인합니다.

rm -f cafe_concierge/.adk/session.db

그런 다음 개발 UI 서버를 다시 실행합니다.

uv run adk web --session_service_uri postgresql+asyncpg://postgres:${DB_PASSWORD}@127.0.0.1:5432/agent_db

포트 8000에서 웹 미리보기를 열고 cafe_concierge를 선택한 후 새 세션을 시작합니다. 그런 다음

What are my dietary preferences?

에이전트는 저장된 환경설정인 비건으로 응답합니다. 이제 데이터가 로컬 저장소가 아닌 PostgreSQL에 저장되므로 다시 시작해도 데이터가 유지됩니다. user: 상태가 이 사용자의 모든 새 세션으로 이전되므로 새 세션을 만드는 경우에도 마찬가지입니다.

9c139bf89becb748.png

데이터베이스 직접 검사

Cloud Shell에서 새 터미널 탭을 열고 데이터베이스를 쿼리하여 저장된 데이터를 확인합니다.

psql "host=127.0.0.1 port=5432 dbname=agent_db user=postgres password=$DB_PASSWORD" -c "\dt"

ADK에서 세션, 이벤트, 상태를 저장하기 위해 자동으로 생성한 테이블이 다음 예와 같이 표시됩니다.

                List of relations
 Schema |         Name          | Type  |  Owner   
--------+-----------------------+-------+----------
 public | adk_internal_metadata | table | postgres
 public | app_states            | table | postgres
 public | events                | table | postgres
 public | sessions              | table | postgres
 public | user_states           | table | postgres
(5 rows)

상태 동작 요약

상태 키

프리픽스

범위

세션 간에 공유되나요?

current_order

(없음)

세션

아니요

user:dietary_preferences

user:

사용자

10. 축하합니다 / 정리

축하합니다. ADK와 Cloud SQL을 사용하여 지속적이고 스테이트풀(Stateful)한 AI 에이전트를 성공적으로 빌드했습니다.

학습한 내용

  • 세션 상태를 읽고 쓰는 맞춤 도구를 사용하여 ADK 에이전트를 만드는 방법
  • 세션 범위 상태 (접두사 없음)와 사용자 범위 상태 (user: 접두사)의 차이점
  • 기본 adk local session.db이 개발에만 적합한 이유: 삭제 시 모든 데이터가 손실되며 (삭제가 쉽고 백업이 없음) 스테이트리스인 서버리스 배포에 적합하지 않음
  • Cloud SQL PostgreSQL 인스턴스를 프로비저닝하고 Cloud SQL 인증 프록시로 연결하는 방법
  • 최소한의 코드 변경으로 CloudSQL의 PostgreSQL을 사용하여 DatabaseSessionService에 연결하는 방법 - 동일한 도구, 동일한 에이전트, 다른 백엔드
  • 별도의 대화 세션에서 사용자 범위 상태가 유지되는 방식

정리

Google Cloud 계정에 비용이 청구되지 않도록 이 Codelab에서 만든 리소스를 정리합니다.

삭제하는 가장 쉬운 방법은 프로젝트를 삭제하는 것입니다. 이렇게 하면 프로젝트와 연결된 모든 리소스가 삭제됩니다.

gcloud projects delete ${GOOGLE_CLOUD_PROJECT}

옵션 2: 개별 리소스 삭제

프로젝트는 유지하고 이 Codelab에서 만든 리소스만 삭제하려면 다음 단계를 따르세요.

gcloud sql instances delete cafe-concierge-db --quiet