이번에는 단순히 load_artifact()와 list_artifacts()를 추가하는 수준을 넘어, GCS에 축적된 과거 보고서를 검색하고, 읽고, 요약하고, 서로 비교하는 통합형 Weather Intelligence Agent로 만드는 것이 좋겠습니다.
이번 단계는 지금까지 공부한 ADK의 여러 기능을 하나로 연결하는 상당히 좋은 종합 실습입니다.
| 사용자 요청 | 에이전트가 수행할 작업 |
| 서울 날씨 보고서를 만들어 줘 | 새 보고서 생성 및 GCS 저장 |
| 지난번 서울 보고서를 보여줘 | GCS에서 과거 보고서 검색 및 조회 |
| 지금까지 서울 보고서 목록을 보여줘 | Artifact 목록 조회 |
| 서울 보고서 중 가장 최근 것을 읽어줘 | 최신 보고서 선택 및 읽기 |
| 서울의 지난 3일 보고서를 요약해 줘 | 여러 Artifact 조회 및 종합 요약 |
| 서울과 부산의 날씨를 비교해 줘 | 두 도시의 과거 보고서 조회 및 비교 |
| 서울의 기온 변화 추이를 분석해 줘 | 여러 보고서에서 기온을 추출하고 분석 |
| 지난 보고서와 오늘 보고서를 비교해 줘 | 과거 버전 또는 별도 날짜 보고서 비교 |
다만 한 가지 중요한 설계 원칙이 있습니다.
파일을 찾고 읽는 일은 Python Tool이 담당하고, 내용을 이해하고 요약·비교하는 일은 Gemini가 담당하도록 분리하겠습니다.
이렇게 하면 LLM이 존재하지 않는 과거 데이터를 만들어 내는 문제를 줄이고, 실제 GCS에 저장된 데이터에 근거하여 답변하게 할 수 있습니다.
1. 전체 시스템 설계
기존 WeatherBot에 기능을 계속 덧붙이는 방식보다, 기능별 Tool을 분리하는 방식이 좋습니다.
Weather Intelligence Agent 2.0

이번에는 다음 네 가지 Tool을 만들겠습니다.
| Tool | 핵심 메서드 | 역할 |
| generate_weather_report | save_artifact() | 새 보고서 생성 |
| list_weather_reports | list_artifacts() | 보고서 목록 조회 |
| read_weather_report | load_artifact() | 특정 보고서 읽기 |
| read_multiple_reports | list_artifacts() + load_artifact() | 여러 보고서 읽기 |
여기에 Gemini가 요약·비교·분석을 수행하도록 하겠습니다.
2. 가장 중요한 설계 변경: Session Scope → User Scope
코드를 작성하기 전에 반드시 짚고 넘어갈 부분이 있습니다.
지난번에 만든 코드는 다음과 같이 저장했습니다.
filename = f"report_{safe_city}_{date_string}.txt"
이 방식은 기본적으로 Session Scope입니다.
즉, 같은 세션에서는 과거 보고서를 다시 읽을 수 있지만, 새로운 세션으로 넘어가면 기존 세션의 보고서를 list_artifacts()로 찾을 수 없습니다.
그런데 우리가 만들려는 서비스는 다릅니다.
오늘 생성한 서울 보고서를 내일 새로운 대화에서 조회하고, 일주일 뒤에도 비교할 수 있어야 합니다.
따라서 이번에는 다음과 같이 변경하겠습니다.
filename = f"user:report_{safe_city}_{date_string}.txt"
앞에 user:를 붙이는 것이 핵심입니다.
| 구분 | 기존 방식 | 변경 방식 |
| 파일명 | report_서울_2026-11-01.txt | user:report_서울_2026-11-01.txt |
| 저장 범위 | Session Scope | User Scope |
| 동일 세션 조회 | 가능 | 가능 |
| 다른 세션 조회 | 불가 | 가능 |
| ADK Web 재시작 후 조회 | 기존 세션에 종속 | 동일 사용자라면 가능 |
ADK의 GCS ArtifactService는 user: 접두어를 인식하여 사용자 단위로 파일을 관리합니다. list_artifacts()는 현재 세션의 파일과 해당 사용자의 User Scope 파일을 함께 조회합니다.
이번에 적용할 저장 구조
GCS ArtifactService
│
└── App: WeatherBot
│
└── User: 사용자 ID
│
├── report_서울_2026-10-30.txt
├── report_서울_2026-10-31.txt
├── report_서울_2026-11-01.txt
├── report_부산_2026-11-01.txt
└── report_대전_2026-11-01.txt
위는 논리적인 사용자별 저장 구조입니다. 실제 GCS 객체 이름은 ADK가 관리합니다.
기존에 저장했던 보고서는 어떻게 될까요?
기존 파일은 삭제되거나 변경되지 않습니다.
-
기존 세션을 그대로 사용하면 기존 보고서도 조회할 수 있습니다.
-
새 세션에서는 기존 Session Scope 파일을 자동으로 모두 검색할 수 없습니다.
-
새 코드로 생성하는 보고서부터는 User Scope로 저장되므로 세션을 바꿔도 조회할 수 있습니다.
이 부분은 기존 데이터와 새 데이터의 차이이므로 꼭 기억해 주세요.

3. 전체 코드
기존 작업 폴더를 그대로 사용합니다.
C:\Dev\book_agentic_ai\adk_
기존 프로젝트 구조가 다음과 같다고 가정합니다.
adk_
├── .env
├── venv
└── weather_agent
├── __init__.py
└── agent.py
agent.py 전체를 아래 코드로 교체하면 됩니다.
이 코드는 기존 보고서 형식과 UTF-8 TXT 저장 방식을 유지하면서, 새 보고서를 User Scope로 저장하도록 확장한 것입니다.
또한 기존 코드에서 사용하던 ToolContext, types.Part.from_bytes(), Agent, root_agent 구조를 그대로 이어갑니다.
4. 코드 상세 해설
이제 코드를 단순히 복사해서 사용하는 것이 아니라, 왜 이렇게 설계했는지 이해해 보겠습니다.
특히 이번에는 기존 실습에서 새롭게 등장한 부분에 집중하겠습니다.
4-1. 공통 유틸리티 함수
이번 코드에서는 공통 기능을 별도의 함수로 분리했습니다.
def safe_city_name(city: str) -> str:
return re.sub(
r"[^a-zA-Z0-9가-힣_-]",
"_",
city.strip()
)
이 함수는 파일명에 사용할 도시 이름을 정리합니다.
예를 들어 다음과 같습니다.
"서울 특별시"
↓
"서울_특별시"
이런 함수를 별도로 만드는 이유는 무엇일까요?
보고서를 생성할 때도 사용하고, 과거 보고서를 검색할 때도 동일한 규칙을 사용하기 때문입니다.
즉, 파일을 저장할 때 사용한 규칙과 파일을 검색할 때 사용한 규칙이 반드시 일치해야 합니다.
다음 함수도 중요합니다.
def is_weather_report(filename: str) -> bool:
name = filename.removeprefix("user:")
return (
name.startswith("report_")
and name.endswith(".txt")
)
이 함수는 Artifact 목록에서 날씨 보고서만 골라내는 역할을 합니다.
왜 removeprefix("user:")를 사용할까요?
이번부터 파일명이 다음과 같이 저장되기 때문입니다.
user:report_서울_2026-11-01.txt
실제 파일 이름을 비교할 때는 user:를 제거하고 다음과 같이 확인합니다.
report_서울_2026-11-01.txt
여기서 removeprefix()는 Python 3.9부터 사용할 수 있는 문자열 메서드입니다.
4-2. extract_artifact_text()
이번 코드에서 상당히 중요한 함수입니다.
def extract_artifact_text(artifact) -> Optional[str]:
if artifact is None:
return None
if artifact.inline_data is not None:
data = artifact.inline_data.data
if data:
return data.decode("utf-8")
if artifact.text:
return artifact.text
return None
지난번에는 보고서를 저장하는 것까지만 했습니다.
이제는 반대로 저장된 파일을 읽어야 합니다.
load_artifact()의 반환값은 일반적인 Python 문자열이 아닙니다.
google.genai.types.Part 객체입니다.
이 객체 안에 파일의 데이터가 들어 있습니다.
이번 실습에서는 types.Part.from_bytes()를 이용하여 저장했으므로, 다시 읽으면 기본적으로 다음 구조로 접근합니다.
artifact.inline_data.data
여기서 data는 bytes입니다.
따라서 다음과 같이 문자열로 복원합니다.
data.decode("utf-8")
전체 과정은 다음과 같습니다.

즉, 저장할 때의 encode()와 읽을 때의 decode()는 한 쌍입니다.
4-3. extract_weather_info()
def extract_weather_info(report_text: str) -> dict:
이 함수는 보고서 전체 내용에서 실제 날씨 수치만 추출합니다.
예를 들어 다음 보고서가 있다고 합시다.
대상 도시 : 서울
생성 시간 : 2026-11-01 14:30
- 기온 : 21°C
- 날씨 : 맑음
- 습도 : 60%
이것을 다음과 같은 Python 딕셔너리로 바꿉니다.
{
"city": "서울",
"created_at": "2026-11-01 14:30",
"temperature": "21",
"condition": "맑음",
"humidity": "60"
}
여기서 사용한 것이 정규표현식입니다.
r"기온\s*:\s*(-?\d+(?:\.\d+)?)\s*°C"
이 표현은 다음과 같은 기온을 추출할 수 있습니다.
-
21°C
-
-3°C
-
21.5°C
왜 이렇게 만들었을까요?
나중에 여러 보고서를 비교할 때 LLM에 원문만 전달하는 것보다 구조화된 데이터도 함께 전달하면 분석하기가 더 수월하기 때문입니다.
특히 기온 변화 추이를 분석할 때 유용합니다.
4-4. get_report_filenames()
이제 list_artifacts()가 등장합니다.
async def get_report_filenames(
tool_context: ToolContext
) -> list:
filenames = await tool_context.list_artifacts()
return [
name
for name in filenames
if is_weather_report(name)
]
이 함수가 하는 일은 다음과 같습니다.
① 현재 사용자가 접근할 수 있는 Artifact 목록을 가져옵니다.
filenames = await tool_context.list_artifacts()
② 전체 파일 중 날씨 보고서만 골라냅니다.
if is_weather_report(name)
예를 들어 Artifact 목록이 다음과 같다면,
[
"user:report_서울_2026-10-30.txt",
"user:report_서울_2026-10-31.txt",
"user:report_부산_2026-11-01.txt",
"user:contract.pdf",
"user:summary.xlsx"
]
결과는 다음과 같습니다.
[
"user:report_서울_2026-10-30.txt",
"user:report_서울_2026-10-31.txt",
"user:report_부산_2026-11-01.txt"
]
이것이 list_artifacts()의 핵심입니다.
파일의 실제 내용을 읽기 전에 어떤 파일이 있는지 먼저 확인하는 것입니다.
4-5. load_report_data()
async def load_report_data(
tool_context: ToolContext,
filename: str,
version: Optional[int] = None
) -> Optional[dict]:
이번 코드의 핵심 조회 함수입니다.
실제로 GCS에서 파일을 불러오는 부분은 다음입니다.
artifact = await tool_context.load_artifact(
filename=filename,
version=version
)
이것은 지난번 save_artifact()의 반대 방향입니다.
| 저장 | 조회 |
| save_artifact() | load_artifact() |
| Python 데이터 → GCS | GCS → Python 데이터 |
| 버전 번호 반환 | 지정 버전 또는 최신 버전 반환 |
여기서 version=None이면 최신 버전을 불러옵니다.
예를 들어 다음과 같습니다.
artifact = await tool_context.load_artifact(
filename="user:report_서울_2026-11-01.txt"
)
특정 버전을 읽으려면 다음과 같이 합니다.
artifact = await tool_context.load_artifact(
filename="user:report_서울_2026-11-01.txt",
version=0
)
이것은 동일 파일명으로 여러 번 저장했을 때 유용합니다.
다만 list_artifacts()는 파일명 목록을 반환할 뿐, 각 파일의 버전 목록을 반환하지 않습니다. 버전 목록까지 자동으로 열람하려면 별도의 ArtifactService list_versions() 기능을 연결해야 합니다.
그 다음은 앞에서 설명한 extract_artifact_text()를 이용하여 실제 보고서 내용을 추출합니다.
최종 반환값은 다음과 같은 딕셔너리입니다.
{
"filename": "user:report_서울_2026-11-01.txt",
"version": None,
"date": "2026-11-01",
"mime_type": "text/plain",
"weather_info": {
"city": "서울",
"temperature": "21",
"condition": "맑음",
"humidity": "60"
},
"content": "실제 보고서 전체 내용..."
}
이제 단순히 파일을 읽는 것을 넘어, 분석에 필요한 정보까지 함께 확보하게 됩니다.
4-6. get_city_reports()
이 함수는 이번 프로젝트에서 가장 중요한 검색 엔진 역할을 합니다.
async def get_city_reports(
tool_context: ToolContext,
city: str,
limit: int = 5,
start_date: str = "",
end_date: str = ""
) -> list:
사용자가 서울의 과거 보고서를 요청하면 다음 과정을 수행합니다.
1단계: 도시 이름 정리
safe_city = safe_city_name(city)
서울을 파일명 규칙에 맞게 정리합니다.
2단계: 전체 보고서 파일명 조회
filenames = await get_report_filenames(tool_context)
3단계: 도시별로 필터링
if not name.startswith(f"report_{safe_city}_"):
continue
예를 들어 다음 목록이 있다면,
user:report_서울_2026-10-30.txt
user:report_서울_2026-10-31.txt
user:report_부산_2026-10-31.txt
user:report_대전_2026-11-01.txt
서울을 요청했으므로 다음 두 개만 선택합니다.
user:report_서울_2026-10-30.txt
user:report_서울_2026-10-31.txt
4단계: 날짜 조건 적용
if start_date and report_date < start_date:
continue
if end_date and report_date > end_date:
continue
예를 들어 다음과 같이 요청하면,
시작일: 2026-10-30
종료일: 2026-11-01
해당 기간의 보고서만 선택합니다.
5단계: 최신순 정렬
matched.sort(
key=get_report_date,
reverse=True
)
reverse=True이므로 최신 날짜가 먼저 나옵니다.
6단계: 필요한 파일만 실제로 읽기
selected = matched[:limit]
그리고 다음 부분에서 각각의 Artifact를 읽습니다.
for filename in selected:
report = await load_report_data(
tool_context=tool_context,
filename=filename
)
if report:
reports.append(report)
이것이 중요합니다.
list_artifacts()는 파일의 이름만 알려줍니다.
실제 내용을 가져오는 것은 load_artifact()입니다.
따라서 다음 두 단계가 구분됩니다.
list_artifacts()
↓
어떤 파일이 있는지 확인
↓
load_artifact()
↓
실제 파일 내용 읽기
이 구조를 정확히 이해하면 ArtifactService를 활용하는 다른 에이전트도 쉽게 만들 수 있습니다.
4-7. Tool 1: generate_weather_report()
이 함수는 지난번에 만든 기능을 거의 그대로 유지했습니다.
달라진 핵심은 딱 한 가지입니다.
filename = (
f"user:report_{safe_city}_{date_string}.txt"
)
이제 보고서는 사용자 단위로 저장됩니다.
예를 들어 서울 보고서를 생성하면 다음과 같은 파일명이 만들어집니다.
user:report_서울_2026-11-01.txt
그리고 기존과 동일하게 다음을 실행합니다.
version_id = await tool_context.save_artifact(
filename=filename,
artifact=weather_artifact
)
이제 새로운 세션에서도 같은 사용자의 보고서를 조회할 수 있습니다.
참고로 같은 날짜에 같은 도시의 보고서를 다시 생성하면 파일명은 동일하므로 새로운 Artifact 버전이 만들어집니다.
예를 들어 다음과 같습니다.
| 저장 횟수 | 파일명 | 버전 |
| 첫 번째 | user:report_서울_2026-11-01.txt | 0 |
| 두 번째 | user:report_서울_2026-11-01.txt | 1 |
| 세 번째 | user:report_서울_2026-11-01.txt | 2 |
즉, 파일명과 버전이라는 두 가지 개념을 동시에 활용하는 것입니다.
4-8. Tool 2: list_weather_reports()
이제 실제로 사용자가 보고서 목록을 조회할 수 있도록 하는 Tool입니다.
async def list_weather_reports(
tool_context: ToolContext,
city: str = "",
limit: int = 20
):
예를 들어 다음과 같이 호출할 수 있습니다.
서울 보고서 목록을 보여줘.
그러면 Tool은 다음 작업을 수행합니다.
filenames = await get_report_filenames(tool_context)
그리고 서울만 필터링합니다.
if city:
safe_city = safe_city_name(city)
filenames = [
name
for name in filenames
if name.removeprefix("user:").startswith(
f"report_{safe_city}_"
)
]
결과는 다음과 같은 형태입니다.
{
"status": "success",
"count": 2,
"reports": [
{
"filename": "user:report_서울_2026-11-01.txt",
"date": "2026-11-01",
"scope": "USER"
},
{
"filename": "user:report_서울_2026-10-31.txt",
"date": "2026-10-31",
"scope": "USER"
}
]
}
여기서 scope는 우리가 직접 구분한 값입니다.
-
USER: user: 접두어가 있는 파일
-
SESSION: 일반 파일명으로 저장한 파일
따라서 기존 보고서와 신규 보고서를 구분하는 데도 도움이 됩니다.
4-9. Tool 3: read_weather_report()
이것이 사용자가 가장 자주 사용할 조회 기능입니다.
async def read_weather_report(
filename: str,
tool_context: ToolContext,
version: Optional[int] = None
):
예를 들어 사용자가 다음과 같이 요청합니다.
지난번에 생성한 서울 날씨 보고서를 다시 보여줘.
에이전트는 먼저 보고서 목록을 확인합니다.
그 후 적절한 파일명을 선택합니다.
user:report_서울_2026-10-31.txt
그리고 이 Tool을 호출합니다.
report = await load_report_data(
tool_context=tool_context,
filename=filename,
version=version
)
이때 version을 지정하지 않으면 최신 버전을 가져옵니다.
또 한 가지 중요한 부분이 있습니다.
available = await get_report_filenames(tool_context)
if filename not in available:
return {
"status": "not_found",
...
}
왜 먼저 목록을 확인할까요?
LLM이 임의로 만들어 낸 파일명이나 존재하지 않는 파일명을 읽으려고 할 수 있기 때문입니다.
실제로 현재 사용자가 접근할 수 있는 Artifact 목록에 존재하는 파일인지 먼저 확인하도록 한 것입니다.
이것은 일종의 접근 범위 검증이기도 합니다.
4-10. Tool 4: read_multiple_reports()
이제부터가 본격적인 분석 기능입니다.
async def read_multiple_reports(
city: str,
tool_context: ToolContext,
limit: int = 5,
start_date: str = "",
end_date: str = ""
):
이 Tool은 특정 도시의 보고서를 여러 개 가져옵니다.
예를 들어 다음과 같이 요청할 수 있습니다.
서울의 최근 5개 날씨 보고서를 읽어줘.
그러면 내부적으로 다음과 같이 실행됩니다.
reports = await get_city_reports(
tool_context=tool_context,
city=city,
limit=limit,
start_date=start_date,
end_date=end_date
)
그리고 각 보고서의 실제 내용까지 반환합니다.
예를 들어 다음과 같은 구조입니다.
{
"status": "success",
"city": "서울",
"count": 3,
"reports": [
{
"date": "2026-11-01",
"weather_info": {
"city": "서울",
"temperature": "21",
"condition": "맑음",
"humidity": "60"
},
"content": "실제 보고서 내용..."
},
{
"date": "2026-10-31",
"weather_info": {
"city": "서울",
"temperature": "18",
"condition": "비",
"humidity": "75"
},
"content": "실제 보고서 내용..."
},
{
"date": "2026-10-30",
"weather_info": {
"city": "서울",
"temperature": "23",
"condition": "구름 조금",
"humidity": "55"
},
"content": "실제 보고서 내용..."
}
]
}
이 데이터가 Gemini에게 전달됩니다.
이제 Gemini는 실제 보고서 내용을 바탕으로 다음과 같이 분석할 수 있습니다.
-
최근 3개 보고서의 날씨 변화
-
기온의 최고·최저
-
기온 변화 폭
-
습도 변화
-
날씨 상태의 변화
여기서 중요한 것은 Python은 데이터를 수집하고, Gemini는 그 데이터를 해석한다는 점입니다.
4-11. Tool 5: compare_weather_reports()
이번에는 두 도시를 비교하는 Tool입니다.
async def compare_weather_reports(
city_a: str,
city_b: str,
tool_context: ToolContext,
limit_per_city: int = 3
):
예를 들어 사용자가 다음과 같이 요청합니다.
최근 서울과 부산의 날씨를 비교해 줘.
그러면 다음 두 작업을 수행합니다.
reports_a = await get_city_reports(
tool_context=tool_context,
city=city_a,
limit=limit_per_city
)
reports_b = await get_city_reports(
tool_context=tool_context,
city=city_b,
limit=limit_per_city
)
서울과 부산의 보고서를 각각 가져오는 것입니다.
그리고 결과를 하나의 구조로 묶습니다.
return {
"status": "success",
"comparison": {
city_a: reports_a,
city_b: reports_b
}
}
이제 Gemini가 두 도시의 데이터를 비교합니다.
예를 들어 다음과 같은 결과를 작성할 수 있습니다.
| 비교 항목 | 서울 | 부산 |
| 평균 기온 | 21°C | 24°C |
| 평균 습도 | 63% | 70% |
| 맑음 보고서 | 2회 | 1회 |
| 비 보고서 | 1회 | 2회 |
물론 이 표의 수치는 설명을 위한 예시입니다. 실제 답변에서는 GCS에서 읽어온 보고서 데이터만 사용해야 합니다.
한 가지 유의사항: 두 도시의 보고서가 서로 다른 날짜라면 같은 날짜의 날씨를 직접 비교한 것처럼 설명하면 안 됩니다. 그래서 Agent 지침에 날짜 차이를 명확히 설명하도록 넣었습니다.
4-12. Agent 정의
마지막으로 Agent입니다.
weather_agent = Agent(
name="WeatherIntelligenceAgent",
model=os.getenv(
"GOOGLE_MODEL",
"gemini-3.5-flash"
),
tools=[
generate_weather_report,
list_weather_reports,
read_weather_report,
read_multiple_reports,
compare_weather_reports
],
instruction="..."
)
여기서 가장 중요한 변화는 Tool이 다섯 개로 늘어났다는 점입니다.
| Tool | 담당 역할 |
| generate_weather_report | 생성 |
| list_weather_reports | 목록 확인 |
| read_weather_report | 단일 파일 읽기 |
| read_multiple_reports | 여러 파일 읽기 |
| compare_weather_reports | 두 도시 데이터 수집 |
Gemini는 사용자 요청을 분석하여 이 중 적절한 Tool을 선택합니다.
예를 들어 다음과 같습니다.

이것이 바로 여러 Tool을 가진 LLM Agent의 기본적인 동작 방식입니다.
5. 실행 방법
이제 실제 환경에서 실행해 보겠습니다.
기존에 사용하던 환경을 그대로 이용하면 됩니다.
① PowerShell 실행
cd C:\Dev\book_agentic_ai\adk_
② 가상환경 활성화
.\venv\Scripts\Activate.ps1
③ 환경변수 설정
$env:GCS_BUCKET_NAME = "agent-project_512212-staging"
필요하다면 모델도 지정합니다.
$env:GOOGLE_MODEL = "gemini-3.5-flash"
기존에 정상 작동했던 모델 ID를 사용하면 됩니다.
④ ADK Web 실행
adk web --port 8080 --artifact_service_uri="gs://agent-project_512212-staging"
브라우저에서 다음 주소로 접속합니다.
6. 실제 테스트 시나리오
이제 기능별로 테스트하겠습니다.
단순히 한 번 실행하고 끝내지 말고, 아래 순서대로 진행하면 다섯 가지 Tool이 어떻게 협력하는지 이해하기 좋습니다.
테스트 1. 새로운 보고서 생성
다음과 같이 입력합니다.
서울 날씨 보고서를 만들어 줘.
예상 결과:
서울 날씨 보고서가 생성되었습니다.
기온: 21°C
날씨: 맑음
습도: 60%
GCS 저장 정보
파일명: user:report_서울_2026-11-01.txt
버전: 0
저장소: agent-project_512212-staging
실제 수치는 실행할 때마다 달라집니다.
이때 PowerShell 로그에서 다음을 확인합니다.
[*] 보고서 생성 시작: 서울
[*] GCS 저장 성공:
user:report_서울_2026-11-01.txt, Version: 0
테스트 2. 다른 도시 보고서 생성
부산 날씨 보고서를 만들어 줘.
대전 날씨 보고서를 만들어 줘.
이렇게 하면 GCS에 세 개의 도시 보고서가 저장됩니다.
예상되는 논리적 Artifact 목록
user:report_서울_2026-11-01.txt
user:report_부산_2026-11-01.txt
user:report_대전_2026-11-01.txt
테스트 3. 전체 보고서 목록 조회
지금까지 GCS에 저장된 날씨 보고서 목록을 모두 보여줘.
이때 호출되는 Tool은 다음입니다.
list_weather_reports()
예상 결과:
| 도시 | 날짜 | 저장 범위 |
| 서울 | 2026-11-01 | USER |
| 부산 | 2026-11-01 | USER |
| 대전 | 2026-11-01 | USER |
테스트 4. 서울 보고서만 조회
서울 날씨 보고서 목록을 보여줘.
이번에는 다음 Tool을 사용합니다.
list_weather_reports(city="서울")
서울 보고서만 필터링되어 반환됩니다.
테스트 5. 지난번 서울 보고서 다시 읽기
이제 핵심 테스트입니다.
지난번에 생성한 서울 날씨 보고서를 다시 보여줘.
실행 순서는 다음과 같습니다.
과거 보고서 조회 흐름

여기서 가장 중요한 것은 Gemini가 과거 날씨를 기억하는 것이 아니라는 사실입니다.
실제로 GCS에서 파일을 읽어온 다음 그 내용을 바탕으로 답변하는 것입니다.
테스트 6. 과거 보고서 여러 개 요약
서울의 최근 5개 날씨 보고서를 읽고
주요 내용을 요약해 줘.
이때 다음 Tool이 실행됩니다.
read_multiple_reports( city="서울", limit=5)
이 Tool은 최대 5개의 보고서를 실제로 읽어 Gemini에게 전달합니다.
예상 답변 형식은 다음과 같습니다.
서울 날씨 보고서 분석
분석 대상: 최근 5개 보고서
조회된 보고서: 5개
1. 기온 변화
- 최고 기온: 25°C
- 최저 기온: 16°C
2. 날씨 변화
- 맑음: 2회
- 비: 2회
- 구름 조금: 1회
3. 습도 변화
- 평균 습도: 68%
4. 종합 의견
조회된 보고서에서는 기온 변화와
날씨 상태의 변화가 관찰됩니다.
※ 실습용 Mock 데이터에 대한 분석입니다.
이 수치는 형식 예시이며, 실제 결과는 저장된 보고서에 따라 달라집니다.
주의: 현재 코드의 파일명은 도시와 날짜만 포함합니다. 따라서 같은 날짜에 같은 도시의 보고서를 여러 번 저장하면 별도 파일이 아니라 하나의 파일에 여러 버전이 생성됩니다. list_artifacts()만으로는 이 여러 버전을 각각 다른 보고서처럼 나열하지 않습니다. 이번 다중 보고서 분석은 서로 다른 날짜에 생성된 파일들을 대상으로 합니다.
테스트 7. 서울과 부산 비교
서울과 부산의 최근 날씨 보고서를 비교해 줘.
기온과 습도, 날씨 상태를 중심으로 분석해 줘.
이때 다음 Tool이 실행됩니다.
compare_weather_reports(
city_a="서울",
city_b="부산",
limit_per_city=3
)
Gemini가 실제 조회된 데이터를 바탕으로 비교합니다.
테스트 8. ADK Web 재시작 후 조회
이번에는 저장소의 영속성을 확인합니다.
-
ADK Web을 종료합니다.
-
동일한 명령어로 다시 실행합니다.
-
기존과 동일한 ADK 사용자 ID를 선택합니다.
-
새로운 대화 세션에서 다음을 입력합니다.
이전에 저장했던 서울 날씨 보고서를 찾아서 보여줘.
기존 보고서가 조회된다면 User Scope를 이용한 세션 간 접근이 정상적으로 작동한 것입니다.
단, app_name과 user_id가 같아야 합니다. user: 접두어는 모든 사용자에게 공개하는 기능이 아니라 같은 애플리케이션의 동일 사용자에게 파일을 공유하는 기능입니다.
7. 이번 실습에서 알아야 할 중요한 한계와 개선점
지금 코드로 생성·조회·요약·비교까지 실습할 수 있습니다. 다만 실제 서비스로 발전시키려면 몇 가지 사항을 알고 있어야 합니다.
① 현재 코드로 모든 과거 GCS 객체를 검색하는 것은 아니다
list_artifacts()는 GCS 버킷 전체를 검색하는 명령이 아닙니다.
현재 실행 컨텍스트에서 접근 가능한 세션 파일과 사용자 파일을 조회합니다.
따라서 다음은 구분해야 합니다.
-
동일 사용자의 User Scope 파일: 여러 세션에서 조회 가능
-
현재 세션의 Session Scope 파일: 현재 세션에서 조회 가능
-
다른 사용자의 User Scope 파일: 조회 불가
-
다른 세션에 속한 과거 Session Scope 파일: 자동 조회 불가
특히 지난번에 만든 기존 보고서는 Session Scope였기 때문에 새 세션에서 자동으로 찾아지지 않을 수 있습니다.
이전 보고서까지 통합하려면 기존 세션에서 User Scope로 다시 저장하는 마이그레이션 작업이나 별도의 인덱스 관리가 필요합니다.
② 현재 버전은 파일명 기준의 최신 보고서 조회다
현재는 다음과 같은 파일명을 사용합니다.
user:report_서울_2026-11-01.txt
같은 날짜에 세 번 저장하면 버전 0, 1, 2가 만들어집니다.
하지만 목록에는 파일명이 하나만 표시됩니다.
따라서 버전별 보고서를 비교하는 기능을 추가하려면 다음 API를 연결하는 것이 좋습니다.
artifact_service.list_versions(...)
이것은 ToolContext의 기본 list_artifacts()와는 별개입니다.
③ 여러 보고서를 분석할 때는 데이터 양을 제한해야 한다
현재 코드에는 다음 설정이 있습니다.
MAX_REPORTS = 10
이것은 한 번에 너무 많은 파일을 읽어 Gemini에게 전달하는 것을 방지합니다.
실무에서는 다음과 같이 발전시키면 좋습니다.
-
보고서가 10개 이하라면 직접 읽기
-
보고서가 수백 개라면 날짜와 도시 기준으로 검색
-
수천 개라면 별도 메타데이터 DB를 활용
-
장기간 분석은 DB에 저장된 구조화 데이터로 계산하고 필요한 원문만 GCS에서 읽기
④ Mock 데이터라는 한계
현재 기온과 습도는 임의로 생성합니다.
따라서 이 데이터를 이용해 통계 분석과 Tool 연동을 학습할 수는 있지만, 실제 날씨 예측이나 기상 추세를 판단하는 용도로 사용하면 안 됩니다.
실제 서비스로 발전시킬 때는 기상청 API나 Open-Meteo 등의 실측·예보 데이터를 연결하면 됩니다.
8. 다음 단계: Weather Intelligence Agent 3.0으로 발전시키기
이번 실습을 여기서 끝내기보다는 한 단계 더 발전시키는 것을 추천합니다.
지금까지 구현한 것은 다음과 같습니다.
-
보고서 생성
-
GCS 저장
-
과거 파일 목록 조회
-
과거 파일 읽기
-
다중 보고서 조회
-
도시 간 비교
-
Gemini 기반 요약과 해석
다음 단계에서는 데이터 관리 방식을 조금 더 전문적으로 바꿀 수 있습니다.
Weather Intelligence Agent 3.0 제안

① GCS Artifact 저장소
원본 보고서 TXT, PDF, JSON 보관
② PostgreSQL 메타데이터 DB
도시, 날짜, 기온, 습도, 날씨, 파일명, 버전 관리
③ 분석 및 검색 엔진
도시별 검색, 기간 검색, 기온 추이, 통계 계산
④ Gemini 분석 Agent
자연어 질문, 요약, 비교, 해석, 보고서 작성
⑤ 분석 결과 보고서 생성
PDF, Excel, TXT 등을 다시 GCS에 저장
이 구조가 되면 사용자가 다음과 같이 질문할 수 있습니다.
-
지난 한 달간 서울의 평균 기온은?
-
서울과 부산의 월별 평균 기온 차이는?
-
올해 가장 더웠던 날은?
-
최근 7일간 습도 변화는?
-
특정 날짜의 보고서를 PDF로 만들어 줘.
-
지난달과 이번 달의 날씨를 비교한 Excel 보고서를 만들어 줘.
이제 단순한 날씨 에이전트가 아니라 데이터를 축적하고 검색하며 분석하고, 다시 문서로 만들어 내는 AI Agent가 되는 것입니다.
개인적으로는 이 구조가 앞으로 공부할 Manager Agent, Research Agent, Document Agent의 설계에도 큰 도움이 될 것이라고 생각합니다.
이번 코드를 먼저 실행합니다. 특히 user: 접두어를 적용한 뒤 새로운 세션에서도 과거 보고서가 조회되는지 확인하는 것이 이번 실습의 가장 중요한 검증입니다.
참고 자료: