15. Session/Memory/Artifact [4] - DatabaseSessionService 활용Google ADK + PostgreSQL 정리(2/2)
Google ADK 날씨 에이전트 코드 상세 정리합니다. 이번에는 코드를 단순히 한 줄씩 번역하는 데 그치지 않고, 왜 이 코드를 작성했는지, Python이 어떻게 실행하는지, Google ADK가 어디에 관여하는지, PostgreSQL에 저장한 사용자 상태와 어떻게 연결되는지까지 정리합니다.
특히 이번 코드에서 가장 중요한 개념은 세 가지입니다.
-
도구(Tool): 날씨 데이터를 조회하고 결과를 반환하는 Python 함수
-
상태(State): 사용자가 마지막으로 물어본 도시를 기억하는 정보
-
에이전트(Agent): 사용자의 질문을 해석하고 적절한 도구를 선택하는 AI
1. 코드 전체 구조 먼저 보기

여기서 꼭 구분해야 할 점이 있습니다. WEATHER_DB는 Python 코드 안에 있는 날씨 데이터이고, PostgreSQL은 ADK가 관리하는 세션과 사용자 상태를 저장하는 곳입니다. 날씨 정보 자체를 PostgreSQL에서 읽는 구조는 아닙니다.
2. 첫 번째 부분: 필요한 모듈 가져오기
import os
from typing import Optional
from google.adk import Agent
from google.adk.tools import ToolContext
Python에서 import는 다른 모듈이나 패키지에 정의된 기능을 현재 코드에서 사용할 수 있도록 가져오는 문법입니다.
2-1. import os
os는 Python이 운영체제와 상호작용할 때 사용하는 표준 라이브러리입니다.
이번 코드에서는 다음 부분에서 사용합니다.
model=os.getenv("GOOGLE_MODEL", "gemini-3.1-flash-lite")
여기서 os.getenv()는 운영체제 환경변수의 값을 읽는 함수입니다.
예를 들어 PowerShell에서 다음과 같이 환경변수를 설정했다고 가정해 보겠습니다.
$env:GOOGLE_MODEL = "사용할_모델명"
그러면 Python 코드의 os.getenv("GOOGLE_MODEL", ...)는 해당 환경변수의 값을 가져옵니다.
반대로 GOOGLE_MODEL이라는 환경변수가 설정되어 있지 않으면 두 번째 인수인 "gemini-3.1-flash-lite"를 기본값으로 사용합니다.
정리하면 다음과 같습니다.( 상황/ 반환되는 값 )
| GOOGLE_MODEL 환경변수 있음 | 환경변수에 지정된 모델명 |
| GOOGLE_MODEL 환경변수 없음 | gemini-3.1-flash-lite |
이렇게 작성하면 모델명을 코드에 고정하지 않고 실행 환경에 따라 바꿀 수 있습니다.
2-2. from typing import Optional
Optional은 어떤 변수가 지정된 자료형의 값일 수도 있고 None일 수도 있음을 표현하는 타입 힌트입니다.
예를 들어 다음 코드를 보겠습니다.
city: Optional[str] = None
이것은 다음과 같은 의미입니다.
-
city에 "서울" 같은 문자열을 넣을 수 있다.
-
아직 도시명이 지정되지 않았다면 None을 넣을 수도 있다.
여기서 None은 문자열 "None"이 아니라, 값이 없음을 나타내는 Python의 특별한 값입니다.
Optional[str]은 다음과 같은 표현과 같은 의미입니다.
str | None
이 표기는 Python 3.10 이상에서 사용할 수 있습니다. 현재 실습 환경인 Python 3.13에서는 둘 다 사용할 수 있습니다.
중요한 점은 Optional이 값이 없어도 된다는 것을 표현하는 타입 힌트라는 사실입니다. 이 표기만으로 함수가 자동으로 도시명을 찾아 주는 것은 아닙니다. 실제 동작은 함수 내부의 코드가 결정합니다.
2-3. from google.adk import Agent
이 코드는 Google ADK에서 Agent 클래스를 가져옵니다.
Agent는 우리가 작성한 Python 함수를 AI 에이전트의 도구로 등록하고, 모델과 지시문을 연결하는 데 사용됩니다.
일반적인 Python 함수는 호출한 코드가 실행 방법을 직접 결정합니다.
get_today_weather("서울")
반면 ADK 에이전트에서는 사용자가 자연어로 질문하면 모델이 질문을 해석한 후 등록된 도구 중 적절한 것을 선택할 수 있습니다.
예를 들어:
-
“서울 오늘 날씨 어때?” → get_today_weather
-
“서울 내일 날씨 알려줘” → get_tomorrow_weather
-
“내일은?” → 저장된 상태를 이용하는 get_tomorrow_weather
다만 어떤 도구가 호출되는지는 모델의 판단과 도구 설명, 지시문 등에 영향을 받습니다. 자연어 질문이 특정 함수 호출로 자동 변환되는 규칙이 Python에 직접 작성되어 있는 것은 아닙니다.
2-4. from google.adk.tools import ToolContext
이 줄은 이번 실습에서 특히 중요합니다.
from google.adk.tools import ToolContext
ToolContext는 ADK가 도구 함수를 실행할 때 제공하는 문맥 정보입니다. 도구가 실행되는 과정에서 상태를 읽거나 변경하는 데 활용할 수 있습니다.
예를 들어:
tool_context.state[USER_LAST_CITY_KEY] = city
이 코드는 ToolContext를 통해 ADK가 관리하는 상태를 변경합니다.
이 상태는 단순한 Python 지역변수와 다릅니다. ADK의 세션 및 상태 관리 체계를 통해 관리되며, 이번 실습에서는 DatabaseSessionService를 이용해 PostgreSQL에 영속적으로 저장되도록 구성했습니다.
쉽게 비유하면 다음과 같습니다.
-
city: 이번 함수에 전달된 도시명
-
tool_context: 도구가 현재 대화의 상태에 접근할 수 있도록 해 주는 통로
-
tool_context.state: 해당 문맥에서 사용할 수 있는 상태 데이터
ToolContext는 도구 함수가 직접 생성하는 것이 아닙니다. ADK가 도구 실행 시 제공하도록 설계된 객체입니다.

3. 두 번째 부분: 날씨 데이터 만들기
이제 실제로 날씨 정보를 보관하는 부분입니다.
WEATHER_DB는 Python의 딕셔너리(dictionary) 입니다. 딕셔너리는 데이터를 키(key): 값(value) 형태로 저장하는 자료구조입니다.
가장 단순한 예부터 살펴보겠습니다.
person = { "name": "김철수", "age": 30}
여기서는 "name"이 키이고 "김철수"가 값입니다. "age"가 키이고 30이 값입니다.
이번 날씨 데이터는 딕셔너리 안에 또 다른 딕셔너리가 들어 있는 구조입니다. 이를 중첩 딕셔너리라고 부릅니다.

예를 들어 다음 코드를 실행하면:
WEATHER_DB["서울"]
결과는 다음과 같은 딕셔너리입니다.
{ "today": "맑음, 22도", "tomorrow": "비, 18도"}
여기서 한 단계 더 들어가면:
WEATHER_DB["서울"]["today"]
결과는 다음과 같습니다.
맑음, 22도
이것이 가능한 이유는 첫 번째 ["서울"]이 서울에 해당하는 내부 딕셔너리를 찾고, 두 번째 ["today"]가 그 안에서 오늘 날씨 값을 찾기 때문입니다.
3-2. USER_LAST_CITY_KEY는 무엇인가요?
USER_LAST_CITY_KEY = "user:last_city"
이 변수는 사용자가 마지막으로 질문한 도시를 저장할 상태의 키 이름입니다.
변수 이름은 대문자와 밑줄로 작성되어 있습니다. Python에서는 이런 형식을 상수처럼 사용하려는 의도를 나타내는 관례로 흔히 사용합니다. 다만 Python이 실제로 값을 변경하지 못하도록 막아 주는 것은 아닙니다.
여기서 중요한 것은 오른쪽의 문자열입니다.
"user:last_city"
이 문자열은 도시명이 아닙니다. ADK에게 “이 상태는 사용자 범위로 관리하는 last_city 항목이다”라고 알려 주는 상태 키입니다.
ADK의 상태 접두어에는 특별한 의미가 있습니다.( 상태 / 키의미 )
| last_city | 현재 세션 범위의 상태 |
| user:last_city | 사용자 범위의 상태 |
| app:last_city | 앱 범위의 상태 |
이번에는 user:를 사용했기 때문에 같은 앱과 사용자 범위에서 상태를 공유할 수 있도록 설계했습니다. DB에 저장될 때는 ADK가 접두어를 해석하여 user_states의 JSON 상태에 last_city라는 항목으로 저장합니다.
따라서 DB에 다음처럼 보이는 것이 정상입니다.
{"last_city": "서울"}
여기서 last_city는 JSON 안의 키이고, "서울"은 실제 값입니다.
4. 세 번째 부분: 오늘 날씨 도구
코드를 다시 보겠습니다.
def get_today_weather( city: str, tool_context: ToolContext,) -> str: # 마지막으로 질문한 도시 저장 tool_context.state[USER_LAST_CITY_KEY] = city info = WEATHER_DB.get(city) if info: return f"{city}의 오늘 날씨는 {info['today']}입니다." return f"죄송해요. {city}의 날씨 정보가 없어요..."
이 함수는 두 가지 일을 합니다.
-
사용자가 질문한 도시를 상태에 저장합니다.
-
해당 도시의 오늘 날씨를 찾아 결과를 반환합니다.
4-1. def는 함수를 정의합니다
def get_today_weather(
def는 Python에서 함수를 정의할 때 사용하는 키워드입니다.
get_today_weather는 우리가 정한 함수 이름입니다. 함수 이름은 호출할 때 어떤 작업을 수행하는지 이해하기 쉽게 작성하는 것이 좋습니다.
이 함수는 “오늘 날씨를 가져온다”는 의미를 담고 있습니다.
4-2. city: str은 무엇인가요?
city: str,
city는 함수에 전달되는 매개변수(parameter)입니다. str은 문자열을 의미하는 타입 힌트입니다.
예를 들어 ADK가 이 도구를 서울에 대해 호출한다고 가정하면, 함수는 개념적으로 다음과 같은 입력을 받습니다.
city = "서울"
이처럼 함수를 호출할 때 전달하는 실제 값은 인수(argument)라고 합니다.
-
city: 매개변수 이름
-
"서울": 실제 전달되는 인수 값
타입 힌트는 코드를 이해하고 도구의 입력 형식을 정의하는 데 도움을 줍니다. 다만 Python에서 city: str이라고 적었다고 해서 모든 잘못된 입력을 자동으로 차단하는 것은 아닙니다.
4-3. tool_context: ToolContext는 왜 필요한가요?
tool_context: ToolContext,
이 매개변수는 ADK가 도구 실행 시 제공하는 문맥 객체를 받습니다.
일반적인 함수라면 도시명만 받아도 날씨 데이터를 찾을 수 있습니다.
get_today_weather("서울")
그러나 이번 함수는 날씨 조회뿐 아니라 ADK 상태도 변경해야 합니다. 그래서 ToolContext가 필요합니다.
여기서 함수 매개변수 이름을 tool_context로 명확히 작성하고 ToolContext 타입으로 선언한 것은 ADK의 도구 문맥을 사용하는 의도를 나타냅니다. 이 객체를 임의로 만들어 전달하는 방식이 아니라 ADK 도구 실행 체계에서 제공받는 방식입니다.
4-4. 마지막 도시를 저장하는 핵심 코드
tool_context.state[USER_LAST_CITY_KEY] = city
이 한 줄을 잘 이해해야 합니다.
오른쪽부터 읽으면:
city
이번 도구에 전달된 도시명입니다. 예를 들어 "서울"입니다.
USER_LAST_CITY_KEY
상태의 키 이름인 "user:last_city"입니다.
tool_context.state[USER_LAST_CITY_KEY]
ADK가 관리하는 상태에서 해당 키에 해당하는 항목을 가리킵니다.
그리고 =는 오른쪽 값을 왼쪽 상태 항목에 대입한다는 의미입니다.
따라서 이 줄은 개념적으로 다음과 같은 동작을 합니다.
사용자 상태의 마지막 도시 항목 ← 서울
ADK가 사용자 범위 상태를 처리하고, 이번 실습의 DatabaseSessionService가 그 상태를 PostgreSQL에 영속적으로 저장할 수 있도록 합니다.
중요: 이 코드가 실행되었다는 사실과 DB에 값이 최종적으로 저장되었다는 사실은 구분해야 합니다. 실제 DB 반영은 ADK의 이벤트 및 상태 저장 처리에 따라 이루어지므로 SQL 조회로 검증하는 것이 좋습니다.
4-5. WEATHER_DB.get(city)는 무엇인가요?
info = WEATHER_DB.get(city)
딕셔너리의 .get() 메서드는 지정한 키가 존재하면 그에 대응하는 값을 반환합니다.
예를 들어:
WEATHER_DB.get("서울")
서울에 해당하는 내부 딕셔너리를 반환합니다.
반면 다음과 같이 지원하지 않는 도시를 요청하면:
WEATHER_DB.get("인천")
현재 WEATHER_DB에는 인천이 없으므로 기본적으로 None을 반환합니다.
일반적인 딕셔너리 접근인 WEATHER_DB[city]와 비교하면 차이가 있습니다.
-
WEATHER_DB[city]: 키가 없으면 KeyError가 발생할 수 있음
-
WEATHER_DB.get(city): 키가 없으면 기본적으로 None을 반환
이번처럼 지원하지 않는 도시를 부드럽게 처리하려는 경우 .get()이 유용합니다.
4-6. if info:는 무엇을 검사하나요?
if info:
이 조건은 info가 참으로 평가되는 값인지 확인합니다.
-
서울처럼 존재하는 도시라면 info에 내부 딕셔너리가 들어 있습니다.
-
없는 도시라면 info는 None입니다.
존재하는 도시의 내부 딕셔너리는 비어 있지 않으므로 조건이 참이 됩니다.
4-7. return과 f-string
return f"{city}의 오늘 날씨는 {info['today']}입니다."
return은 함수의 결과를 호출한 쪽으로 반환하고, 해당 함수 실행을 끝냅니다.
여기서 f"..."는 f-string입니다. 문자열 안에 {}를 넣어 변수나 표현식의 값을 삽입할 수 있습니다.
예를 들어:
city = "서울"info = {"today": "맑음, 22도"}
라면:
f"{city}의 오늘 날씨는 {info['today']}입니다."
는 다음 문자열이 됩니다.
서울의 오늘 날씨는 맑음, 22도입니다.
{info['today']}는 info 딕셔너리의 "today" 값을 꺼내 문자열에 넣습니다.
마지막으로 지원하지 않는 도시라면:
return f"죄송해요. {city}의 날씨 정보가 없어요..."
가 실행되어 오류 대신 안내 문구를 반환합니다.
다만 현재 함수는 날씨 데이터를 찾기 전에 상태를 먼저 저장합니다. 따라서 "인천"처럼 지원하지 않는 도시를 입력하면 그 도시가 마지막 도시 상태에 저장될 수 있습니다. 실제 서비스로 확장할 때는 지원하는 도시인지 먼저 검증한 후 상태를 저장하도록 순서를 바꾸는 것이 더 안전합니다.
5. 네 번째 부분: 내일 날씨 도구
이번 함수가 전체 실습에서 가장 중요한 부분입니다. 오늘 날씨 함수와 달리, 사용자가 도시명을 생략할 수 있기 때문입니다.
def get_tomorrow_weather( city: Optional[str] = None, tool_context: ToolContext = None,) -> str:
이 함수는 다음 두 가지 입력 상황을 모두 처리하려고 만들어졌습니다.
-
사용자가 “부산 내일 날씨 알려줘”라고 도시명을 명시하는 경우
-
사용자가 앞선 대화에 이어 “내일은?”이라고 도시명을 생략하는 경우
5-1. city: Optional[str] = None
city: Optional[str] = None
여기에는 세 가지 개념이 들어 있습니다.
| city | 도시명을 전달받는 매개변수 |
| Optional[str] | 문자열 또는 None이 올 수 있음 |
| = None | 호출할 때 생략하면 기본적으로 None을 사용 |
예를 들어 도시를 명시하면:
get_tomorrow_weather(city="부산")
개념적으로 city에는 "부산"이 들어갑니다.
반대로 도시를 생략하면:
get_tomorrow_weather()
city는 기본값인 None이 됩니다.
물론 실제 ADK에서는 모델이 도구 호출을 결정하고, ADK가 해당 함수를 실행합니다. 위 코드는 함수가 받는 입력의 의미를 이해하기 위한 Python 수준의 예시입니다.
5-2. tool_context: ToolContext = None은 괜찮을까요?
현재 코드에서는 다음과 같이 작성되어 있습니다.
tool_context: ToolContext = None
이것은 tool_context를 생략하면 None이 기본값이 되도록 허용한다는 뜻입니다.
하지만 함수 안에서는 다음과 같이 사용합니다.
tool_context.state
만약 tool_context가 실제로 None이라면 .state에 접근할 수 없어 오류가 발생합니다.
따라서 상태를 읽어야 하는 이 함수에서는 tool_context를 필수 매개변수로 선언하는 편이 더 명확합니다. 다음처럼 바꾸는 것을 권합니다.
def get_tomorrow_weather( tool_context: ToolContext, city: Optional[str] = None,) -> str:
Python에서는 기본값이 없는 필수 매개변수 뒤에 기본값이 있는 매개변수를 배치할 수 있습니다. ADK는 ToolContext를 도구 실행 과정에서 제공하므로, 이 매개변수를 필수로 선언하면 잘못된 호출 상황도 더 명확하게 구분할 수 있습니다.
5-3. 첫 번째 방어 코드: 상태 키가 도시명으로 들어오는 문제
if city == USER_LAST_CITY_KEY: city = None
이 두 줄은 앞서 겪었던 실제 문제를 해결하기 위해 추가했습니다.
원래 의도는 다음과 같습니다.
city = "서울"
또는 도시명을 생략해서:
city = None
그런데 모델이 도구를 잘못 호출하면 다음과 같이 전달할 수 있습니다.
city = "user:last_city"
이것은 도시명이 아니라 상태 키입니다.
따라서:
if city == USER_LAST_CITY_KEY:
는 city가 상태 키 문자열과 같은지 비교합니다.
비교 결과가 참이면:
city = None
으로 바꿉니다.
이제 아래 코드가 도시명을 생략한 상황과 동일하게 처리할 수 있게 됩니다.
왜 이 방어 코드가 중요한가요?
앞서 확인했던 문제는 user:last_city라는 문자열이 실제 도시명인 것처럼 취급되어 DB의 "서울" 값을 덮어쓰는 것이었습니다. 이 코드는 그런 잘못된 입력을 도시명으로 사용하지 않도록 막습니다.
5-4. 두 번째 핵심: 도시명이 없으면 상태에서 읽기
target_city = ( city or tool_context.state.get(USER_LAST_CITY_KEY))
이 코드는 Python의 or 연산자를 활용합니다.
or는 왼쪽 값이 참으로 평가되면 왼쪽 값을 반환하고, 그렇지 않으면 오른쪽 값을 반환합니다.
경우 A: 사용자가 부산을 지정한 경우
city = "부산"
그러면:
target_city = city or tool_context.state.get(USER_LAST_CITY_KEY)
에서 city가 비어 있지 않으므로 "부산"이 선택됩니다.
결과:
target_city = "부산"
경우 B: 사용자가 “내일은?”이라고 질문한 경우
city = None
이 경우 왼쪽 값이 없으므로 오른쪽 표현식이 실행됩니다.
tool_context.state.get(USER_LAST_CITY_KEY)
이것은 ADK의 상태에서 마지막 도시를 읽는 코드입니다.
예를 들어 사용자 상태가 다음과 같다면:
{"last_city": "서울"}
결과적으로:
target_city = "서울"
이 됩니다.
이 한 줄이 바로 이전 질문의 도시를 기억해서 후속 질문에 사용하는 핵심 로직입니다.
다만 여기서 or가 데이터베이스를 직접 조회하는 것은 아닙니다. tool_context.state라는 ADK 상태 인터페이스를 통해 값을 요청하는 것이고, ADK의 세션·상태 관리 계층이 그 값을 제공합니다.
5-5. 세 번째: 상태에 도시가 없으면 질문하기
if not target_city: return "어느 도시의 날씨를 알고 싶으신가요?"
not은 조건의 참·거짓을 반대로 만듭니다.
예를 들어:
-
target_city = "서울"이면 not target_city는 거짓입니다.
-
target_city = None이면 not target_city는 참입니다.
따라서 저장된 도시도 없고 사용자가 도시를 지정하지도 않았다면, 함수는 안내 문구를 반환하고 종료합니다.
이것은 잘못된 데이터를 억지로 사용하거나 임의로 도시를 추측하지 않도록 하는 안전장치입니다.
5-6. 네 번째: 실제 지원하는 도시만 저장하기
if city in WEATHER_DB: tool_context.state[USER_LAST_CITY_KEY] = city
이 코드는 사용자가 새 도시를 명시한 경우에만 상태를 변경합니다.
먼저:
city in WEATHER_DB
는 해당 도시가 WEATHER_DB의 키로 존재하는지 검사합니다.
예를 들어:
"부산" in WEATHER_DB
는 참입니다.
반면:
"인천" in WEATHER_DB
는 거짓입니다.
따라서 "부산"을 입력하면 마지막 도시 상태를 부산으로 변경하지만, 지원하지 않는 "인천"은 이 코드에서 상태를 변경하지 않습니다.
그리고 city가 None인 경우에도 딕셔너리의 키로 존재하지 않으므로 저장하지 않습니다. 이렇게 하면 사용자가 “내일은?”이라고 물었을 때 기존의 서울 상태를 불필요하게 덮어쓰지 않습니다.
5-7. 다섯 번째: 내일 날씨 조회
info = WEATHER_DB.get(target_city)
앞서 설명한 .get() 메서드를 사용해 최종 대상 도시의 날씨를 조회합니다.
도시가 서울이면 info는 다음과 같습니다.
{ "today": "맑음, 22도", "tomorrow": "비, 18도"}
그다음:
if info: return ( f"{target_city}의 내일 날씨는" f"{info['tomorrow']}입니다." )
두 개의 f-string이 괄호 안에서 이어집니다. Python에서는 문자열 리터럴을 이렇게 나란히 작성하면 하나의 문자열로 연결됩니다.
결과:
서울의 내일 날씨는비, 18도입니다.
현재 코드에는 "날씨는" 뒤에 공백이 없으므로 출력 결과가 조금 어색합니다. 다음과 같이 공백을 넣으면 더 자연스럽습니다.
return ( f"{target_city}의 내일 날씨는 " f"{info['tomorrow']}입니다.")
그러면 최종 결과는 다음과 같습니다.
서울의 내일 날씨는 비, 18도입니다.
마지막으로 지원하지 않는 도시라면:
return f"죄송해요. {target_city}의 내일 날씨 정보가 없어요..."
가 실행됩니다.
내일 날씨 함수의 전체 실행 흐름

6. 다섯 번째 부분: 날씨 에이전트 만들기
이제 작성한 두 함수를 AI 에이전트에 연결합니다.
weather_agent = Agent(
이 코드는 Agent 클래스를 이용해 에이전트 객체를 생성하고, 그 객체를 weather_agent라는 변수에 저장합니다.
여기서 구분할 것이 있습니다.
-
Agent: 에이전트를 구성하기 위한 클래스
-
Agent(...): 설정값을 전달해 에이전트 객체를 생성하는 표현
-
weather_agent: 생성된 에이전트 객체를 가리키는 변수
쉽게 비유하면 Agent가 설계도라면, weather_agent는 그 설계도에 따라 만든 실제 에이전트 객체입니다.
6-2. model=...
model=os.getenv("GOOGLE_MODEL", "gemini-3.1-flash-lite"),
에이전트가 사용할 언어 모델을 지정합니다.
앞에서 설명했듯이 GOOGLE_MODEL 환경변수가 있으면 그 값을 사용하고, 없다면 "gemini-3.1-flash-lite"를 기본값으로 사용합니다.
언어 모델은 다음과 같은 일을 담당합니다.
-
사용자의 자연어 질문 해석
-
오늘 날씨인지 내일 날씨인지 판단
-
적절한 도구 선택
-
도구 결과를 바탕으로 사용자에게 답변 생성
반면 실제 날씨 데이터 조회와 상태 저장은 우리가 작성한 Python 도구가 담당합니다.
따라서 모델이 직접 PostgreSQL을 조회하거나 날씨 데이터 딕셔너리를 직접 수정하는 구조는 아닙니다. 모델은 ADK의 도구 호출 기능을 통해 Python 함수의 실행을 요청합니다.
6-3. name="weather_agent"
name="weather_agent",
이것은 에이전트의 이름입니다.
이름은 에이전트를 구분하고 관리할 때 사용됩니다. weather_agent라는 이름은 날씨 에이전트라는 역할을 표현합니다.
여기서 혼동하기 쉬운 이름 세 개를 비교하겠습니다.
| weather_agent | Python 코드에서 사용하는 에이전트 객체의 변수명 |
| name="weather_agent" | ADK 에이전트 객체의 이름 |
| root_agent | ADK가 프로젝트의 진입 에이전트를 찾을 때 사용하는 변수명 |
이들은 서로 관련되어 있지만 같은 개념은 아닙니다.
6-4. instruction은 에이전트의 행동 지침
instruction="""당신은 날씨 정보를 알려주는 AI 에이전트입니다....""",
instruction은 모델에 어떤 역할을 수행하고 어떤 규칙을 따를지 알려 주는 지시문입니다.
이번 코드에서는 크게 세 가지를 지시합니다.
-
오늘 날씨를 물으면 get_today_weather를 사용합니다.
-
내일 날씨를 물으면 get_tomorrow_weather를 사용합니다.
-
도시명을 생략하면 상태를 활용하고, user:last_city를 도시명으로 넘기지 않습니다.
여기서 중요한 점은 지시문은 모델의 행동을 유도하는 것이지, Python의 강제 조건문과 동일하지 않다는 사실입니다.
예를 들어 다음 지시문을 넣었다고 합시다.
"user:last_city"는 도시명이 아닙니다.
모델이 이를 따르도록 유도할 수는 있지만, 모든 상황에서 반드시 지켜진다고 보장되지는 않습니다. 그래서 앞서 함수 안에도 다음 방어 코드를 넣었습니다.
if city == USER_LAST_CITY_KEY: city = None
역할을 구분하면 이해하기 쉽습니다.
-
instruction: 모델이 올바른 도구 호출을 하도록 유도
-
Python 함수의 조건문: 잘못된 입력이 들어왔을 때 실제로 처리 방식을 통제
또한 현재 지시문은 번호가 1, 2, 4, 5로 되어 있습니다. 실행 오류를 일으키지는 않지만, 읽기 쉽게 1, 2, 3, 4로 정리하는 것을 권합니다.
6-5. tools=[...]는 무엇인가요?
tools=[ get_today_weather, get_tomorrow_weather],
이 설정은 에이전트가 사용할 수 있는 도구를 등록합니다.
여기서는 두 개의 Python 함수를 도구로 제공합니다.
-
get_today_weather
-
get_tomorrow_weather
주의할 점은 함수 이름 뒤에 괄호를 붙이지 않았다는 것입니다.
tools=[get_today_weather, get_tomorrow_weather]
이렇게 함수 자체를 전달하면 ADK가 도구로 등록할 수 있습니다.
반면:
tools=[get_today_weather(), get_tomorrow_weather()]
처럼 작성하면 함수를 등록하는 대신, 에이전트를 만드는 시점에 함수를 즉시 실행하려는 표현이 됩니다. 이 코드에서는 의도한 동작이 아닙니다.
즉, tools는 “지금 실행할 함수 목록”이 아니라 “에이전트가 필요할 때 호출할 수 있도록 제공하는 도구 목록”입니다.
7. 전체 대화가 실제로 실행되는 순서
지금까지 각 코드 부분을 살펴봤으니, 실제로 사용자가 질문했을 때 어떤 일이 벌어지는지 연결해 보겠습니다.
시나리오 A: 처음으로 서울 오늘 날씨를 묻는 경우
사용자:
서울 오늘 날씨 어때?
-
ADK가 사용자 메시지를 에이전트에 전달합니다.
-
언어 모델이 질문을 해석하고 get_today_weather 도구를 선택합니다.
-
도구에 city="서울"과 tool_context가 전달됩니다.
-
tool_context.state[USER_LAST_CITY_KEY] = city가 실행되어 마지막 도시 상태를 변경합니다.
-
WEATHER_DB.get("서울")로 서울의 날씨 데이터를 가져옵니다.
-
함수가 오늘 날씨 결과를 반환합니다.
-
모델이 도구 결과를 바탕으로 사용자에게 답변합니다.
-
ADK의 DB 기반 상태 저장 처리를 통해 사용자 상태가 PostgreSQL에 영속적으로 반영됩니다.
시나리오 B: 이어서 “내일은?”이라고 묻는 경우
사용자:
내일은?
-
모델이 앞선 질문과 연결된 내일 날씨 질문으로 해석합니다.
-
get_tomorrow_weather를 호출합니다. 도시명이 생략되어 city가 None일 수 있습니다.
-
함수는 tool_context.state.get(USER_LAST_CITY_KEY)로 저장된 마지막 도시를 확인합니다.
-
상태에 "서울"이 있으면 target_city는 "서울"이 됩니다.
-
WEATHER_DB.get("서울")로 내일 날씨 데이터를 찾습니다.
-
함수가 서울의 내일 날씨를 반환하고, 모델이 사용자에게 답합니다.
여기서 한 가지 더 생각해 볼 점이 있습니다. 새 대화에서는 앞선 대화의 자연어 문맥이 없을 수 있습니다. 하지만 같은 앱과 사용자에 대해 user: 범위 상태가 DB에 저장되어 있고, 에이전트가 그 상태를 활용하도록 도구를 호출하면 이전 대화에서 저장된 도시를 다시 사용할 수 있습니다.
이것이 이번 실습에서 단순한 챗봇과 영속적인 상태를 사용하는 에이전트의 차이를 확인한 부분입니다.
8. 마지막으로 꼭 알아둘 점: root_agent
제공해 주신 코드의 마지막은 다음과 같습니다.
weather_agent = Agent( ...)
그런데 ADK 웹에서 이 에이전트를 찾아 실행하려면 프로젝트의 agent.py에 다음 줄도 있어야 합니다.
root_agent = weather_agent
이 줄은 새로운 에이전트를 만드는 것이 아닙니다. 이미 생성한 weather_agent 객체를 root_agent라는 이름으로 연결하는 것입니다.
ADK가 root_agent를 진입점으로 찾아 사용하도록 해 주는 역할입니다.
전체 연결을 정리하면 다음과 같습니다.
agent.py
│
├── WEATHER_DB
│
├── USER_LAST_CITY_KEY
│
├── get_today_weather()
│
├── get_tomorrow_weather()
│
├── weather_agent = Agent(...)
│ ├── model
│ ├── instruction
│ └── tools
│
└── root_agent = weather_agent
9. 이번 코드에서 꼭 기억해야 할 핵심 7가지( 핵심 개념 / 이번 코드에서 하는 일 )
| WEATHER_DB | Python 메모리 안에 테스트용 날씨 데이터를 보관 |
| Optional[str] = None | 도시명이 생략될 수 있음을 표현 |
| ToolContext | 도구가 ADK의 상태를 읽고 변경하도록 지원 |
| tool_context.state[...] = city | 마지막 도시를 상태에 기록 |
| tool_context.state.get(...) | 저장된 마지막 도시를 읽음 |
| instruction | 모델이 적절한 도구를 선택하도록 행동 지침 제공 |
| tools=[...] | Python 함수를 에이전트가 호출할 수 있는 도구로 등록 |
특히 다음 두 줄은 함께 기억하시면 좋습니다.
tool_context.state[USER_LAST_CITY_KEY] = city
기억하기 — 상태에 값을 기록합니다.
tool_context.state.get(USER_LAST_CITY_KEY)
꺼내기 — 상태에서 값을 읽습니다.
그리고 PostgreSQL은 이 상태를 프로그램이 종료되어도 보존할 수 있도록 지원합니다. Python 함수가 상태를 변경하는 것, ADK가 상태를 관리하는 것, PostgreSQL이 데이터를 영속적으로 보관하는 것은 서로 연결되어 있지만 각각 다른 역할입니다.
마지막으로 이번 실습에서 배운 가장 중요한 설계 원칙은 이것입니다.
AI 모델의 지시문만 믿지 말고, 중요한 데이터 처리 규칙은 Python 코드에서도 검증해야 합니다. user:last_city를 도시명으로 잘못 전달했던 문제를 함수 내부에서 방어하도록 수정한 것이 바로 좋은 예입니다.