Agentic AI/Google ADK

22. Session/Memory/Artifact [11]- ADK ArtifactService 실습: 파일 생성, 저장, 조회 및 관리 구조 이해하기

아톨 2026. 10. 31. 19:27

이번 실습은 앞서 공부한 SessionService, MemoryService에 이어 ADK의 세 번째 데이터 관리 서비스인 ArtifactService를 이해하는 과정입니다.

 

이번 코드는 단순히 날씨를 조회하는 에이전트를 만드는 것이 아닙니다.

에이전트가 외부 정보를 수집하고, 그 결과를 문서 파일로 가공한 다음, ADK의 ArtifactService에 저장하는 전체 과정을 구현한 것입니다.

 

앞서 공부한 두 서비스와 비교하면 이해하기 쉽습니다.

서비스 관리  대상 핵심 역할
SessionService 대화 이벤트 사용자와 나눈 대화 관리
MemoryService 장기 기억 과거 대화에서 중요한 정보 기억
ArtifactService 파일 및 산출물 문서, 이미지, 데이터 파일 저장 및 조회

예를 들어 AI 에이전트를 업무에 적용한다고 생각해 보겠습니다.

  • SessionService: 사용자가 어떤 질문을 했는지 기록

  • MemoryService: 사용자가 어떤 업무를 담당하고 어떤 선호가 있는지 기억

  • ArtifactService: AI가 작성한 보고서, 엑셀, PDF, 이미지 등을 저장

이 세 가지를 함께 사용하면 단순한 대화형 AI를 넘어 실제 업무 결과물을 만들어 내고 관리하는 AI Agent를 구현할 수 있습니다.

먼저 이번 코드에서 반드시 짚고 넘어가야 할 부분이 있습니다.

실행 명령어의 방향은 맞습니다
(venv) PS C:\Dev\book_agentic_ai\adk_>

adk web --port 8080 --memory_service_uri="agentengine://projects/921865052026/locations/asia-northeast3/reasoningEngines/2528612861094133760"

이 명령은 MemoryService를 Vertex AI Memory Bank로 연결합니다.

하지만 ArtifactService를 Vertex AI Memory Bank에 연결하는 명령은 아닙니다.

그리고 현재 코드에 선언한 다음 객체도 주의해야 합니다.

artifact_service = InMemoryArtifactService()

이 객체는 코드에서 생성만 했을 뿐, 실제 저장 작업에 직접 사용되지는 않습니다.

실제 저장은 다음 코드가 담당합니다.

version_id = await tool_context.save_artifact(
    filename=filename,
    artifact=weather_artifact,
)

이 차이를 중심으로 하나씩 해설하겠습니다.

1. 전체 아키텍처 이해하기

이번 실습의 전체 흐름입니다. 

여기서 중요한 점은 ArtifactService가 보고서를 생성하는 것이 아니라는 사실입니다.

보고서를 만드는 것은 우리가 작성한 Python Tool이고, ArtifactService는 생성된 파일을 저장하고 다시 가져오는 역할을 합니다.

즉 다음과 같이 역할이 구분됩니다.

  • Gemini: 무엇을 해야 할지 판단

  • Python Tool: 실제 데이터 처리와 파일 생성

  • ArtifactService: 만들어진 파일의 저장 및 관리

이제 코드를 살펴보겠습니다.

2. 1단계: 라이브러리 가져오기

import datetime
import os
import random

from google.genai import types
from google.adk import Agent
from google.adk.artifacts import InMemoryArtifactService
from google.adk.tools import ToolContext

각 라이브러리가 어떤 역할을 하는지 보겠습니다.

라이브러리 역할
datetime 날짜와 시간 생성
os 환경변수에서 모델 이름 읽기
random 테스트용 임의 날씨 데이터 생성
google.genai.types 파일 데이터를 표현하는 Part, Blob 생성
Agent ADK 에이전트 정의
InMemoryArtifactService 메모리 기반 파일 저장 서비스
ToolContext Tool 실행 문맥 및 ArtifactService 접근

여기서 핵심은 다음 세 가지입니다.

2-1. google.genai.types

from google.genai import types

이번 실습에서 상당히 중요한 라이브러리입니다.

Google GenAI SDK에서 제공하는 데이터 구조를 사용하기 위해 가져옵니다.

특히 다음 두 가지를 사용합니다.

types.Part
types.Blob

이것은 일반적인 Python 문자열이나 파일 객체와는 다릅니다.

ADK와 Gemini가 이해할 수 있는 형식으로 콘텐츠를 표현하기 위한 데이터 구조입니다.

조금 뒤에 자세히 살펴보겠습니다.

2-2. InMemoryArtifactService

from google.adk.artifacts import InMemoryArtifactService

ADK가 제공하는 메모리 기반 ArtifactService입니다.

이름을 해석하면 이해하기 쉽습니다.

  • InMemory: 메모리에 저장

  • Artifact: 파일 및 산출물

  • Service: 저장 및 관리 서비스

즉, 생성된 파일을 Python 실행 환경의 메모리에서 관리하는 서비스입니다.

특징은 다음과 같습니다.

  • 별도의 데이터베이스가 필요하지 않습니다.

  • 별도의 Cloud Storage 버킷이 필요하지 않습니다.

  • 개발 및 테스트에 편리합니다.

  • 실행 환경이 종료되면 저장된 데이터가 사라질 수 있습니다.

2-3. ToolContext

from google.adk.tools import ToolContext

이것도 중요합니다.

앞서 MemoryBank 실습에서 CallbackContext를 사용했었죠.

이번에는 ToolContext입니다.

둘 다 ADK가 제공하는 실행 문맥 객체이지만 사용하는 위치가 다릅니다.

구분 CallbackContext ToolContext
사용 위치 Callback Tool
목적 에이전트 실행 과정에 접근 도구 실행 과정에 접근
사용 예 세션 종료 후 메모리 저장 파일 저장, 상태 접근
이번 실습 사용하지 않음 사용

이번에는 Python Tool 안에서 파일을 저장해야 하기 때문에 ToolContext를 사용합니다.

3. 2단계: 날씨 조회 및 보고서 생성 Tool

이제 실제 기능을 수행하는 함수를 살펴보겠습니다.

async def generate_weather_report(city: str, tool_context=None):

이번 코드의 핵심 함수입니다.

사용자가 도시 이름을 입력하면 다음 작업을 수행합니다.

  1. 도시의 날씨 데이터를 가져옵니다.

  2. 데이터를 바탕으로 보고서를 만듭니다.

  3. 보고서 내용을 파일 데이터로 변환합니다.

  4. ADK ArtifactService에 저장합니다.

  5. 저장 결과를 에이전트에게 반환합니다.

전체 흐름을 먼저 그림으로 보겠습니다.

3-1. 함수 선언과 매개변수

async def generate_weather_report(city: str, tool_context=None):

두 가지 매개변수가 있습니다.

① city: str

city: str

날씨를 조회할 도시 이름입니다.

예를 들어 사용자가 다음과 같이 질문하면,

서울 날씨를 조회하고 보고서를 만들어 줘.

Gemini가 Tool을 호출하면서 다음과 같은 값을 전달합니다.

city = "Seoul"

물론 모델이 반드시 영문으로 전달하는 것은 아니므로 실제로는 "서울"이 전달될 수도 있습니다.

② tool_context

tool_context=None

이 부분이 중요합니다.

tool_context는 ADK가 Tool 실행 시 전달하는 실행 문맥 객체입니다.

우리는 이 객체를 통해 다음과 같은 기능을 사용할 수 있습니다.

  • 현재 사용자 정보 확인

  • 현재 세션 정보 확인

  • 상태값 접근

  • 파일 저장

  • 파일 불러오기

  • 파일 목록 확인

이번 실습에서는 특히 파일 저장 기능을 사용합니다.

다만 현재 선언은 조금 수정하는 것이 좋습니다.

기존 코드:

async def generate_weather_report(city: str, tool_context=None):

권장 코드:

async def generate_weather_report(
    city: str,
    tool_context: ToolContext
):

왜 이렇게 수정할까요?

ADK는 Tool 함수의 매개변수 타입을 확인해 특별한 실행 문맥 객체를 주입할 수 있습니다.

따라서 ToolContext 타입을 명시하는 것이 좋습니다.

또한 tool_context=None이라고 선언하면 실제 실행 시 필요한 객체가 전달되지 않더라도 함수 자체는 실행을 시도할 수 있습니다.

하지만 우리는 파일 저장에 반드시 ToolContext가 필요합니다.

따라서 필수 매개변수로 선언하는 것이 더 명확합니다.

3-2. Docstring

"""
특정 도시의 날씨를 조회하고, 상세 리포트를 생성하여 파일로 저장합니다.
(실제 API 연동 대신 Mock 데이터를 사용하여 시뮬레이션합니다.)

Args:
    city: 날씨를 조회할 도시 이름
    tool_context: 런타임 컨텍스트
"""

Docstring은 함수의 목적과 매개변수를 설명하는 문서 문자열입니다.

여기서 주목할 점은 다음 내용입니다.

실제 API 연동 대신 Mock 데이터를 사용하여 시뮬레이션합니다.

즉 이번 실습은 실제 날씨 정보를 가져오는 것은 아닙니다.

실제 날씨 API를 호출하는 대신 임의의 데이터를 생성합니다.

이것은 ArtifactService의 작동을 학습하기 위한 것이므로 좋은 접근입니다.

날씨 데이터 수집과 파일 저장을 분리해서 생각할 수 있기 때문입니다.

3-3. Tool 실행 로그

print(f"[*] Tool 실행: '{city}'의 날씨 데이터를 조회합니다...")

Python의 print()를 이용해 실행 상황을 출력합니다.

예를 들어 사용자가 서울을 입력하면 다음과 같이 출력됩니다.

[*] Tool 실행: 'Seoul'의 날씨 데이터를 조회합니다...

이 메시지는 ADK Web 화면에 표시되는 에이전트의 답변이 아닙니다.

실행 중인 Python 서버의 터미널 로그에 출력됩니다.

따라서 Web UI의 대화 내용과 서버 터미널의 실행 로그를 구분해서 확인해야 합니다.

4. 3단계: 날씨 데이터 수집 (Retrieval)

temp = random.randint(10, 30)

conditions = ["Clear", "Partly Cloudy", "Rain", "Windy"]

condition = random.choice(conditions)

humidity = random.randint(40, 90)

이 부분은 날씨 정보를 수집하는 단계입니다.

다만 실제 API 호출이 아니라 Python의 random 모듈을 활용한 Mock 데이터 생성입니다.

4-1. random.randint()

temp = random.randint(10, 30)

10부터 30 사이의 정수 중 하나를 무작위로 선택합니다.

예를 들어 실행할 때마다 다음과 같이 달라질 수 있습니다.

첫 번째 실행: 18
두 번째 실행: 25
세 번째 실행: 12

여기서 중요한 점은 randint(10, 30)은 10과 30을 모두 포함한다는 것입니다.

4-2. 날씨 상태 목록

conditions = [    "Clear",    "Partly Cloudy",    "Rain",    "Windy"]

날씨 상태를 문자열 목록으로 만들어 놓았습니다.

각 항목의 의미는 다음과 같습니다.

Clear 맑음
Partly Cloudy 구름 조금
Rain 비
Windy 바람이 강함

4-3. random.choice()

condition = random.choice(conditions)

목록 중 하나를 무작위로 선택합니다.

예를 들어 다음과 같은 결과가 나올 수 있습니다.

condition = "Rain"

4-4. 습도

humidity = random.randint(40, 90)

습도도 40~90 사이에서 임의로 선택합니다.

예를 들어 다음과 같습니다.

humidity = 65

따라서 이 네 줄이 실행되면 다음과 같은 데이터가 만들어질 수 있습니다.

city = "Seoul"
temp = 23
condition = "Clear"
humidity = 65

다시 강조하지만, 이것은 실제 서울의 날씨가 아닙니다.

실제 서비스를 만들 때는 기상청 API나 Open-Meteo 등의 API로 대체해야 합니다.

5. 4단계: 보고서 텍스트 생성 (Processing)

이제 수집한 데이터를 이용해 보고서를 만듭니다.

today = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")

먼저 날짜와 시간을 생성합니다.

5-1. datetime.datetime.now()

datetime.datetime.now()

현재 컴퓨터의 날짜와 시간을 가져옵니다.

예를 들어 다음과 같은 값입니다.

2026-10-31 18:45:32

5-2. strftime()

.strftime("%Y-%m-%d %H:%M")

날짜와 시간을 원하는 형식의 문자열로 바꾸는 기능입니다.

아래는 기호와 그 의미입니다.
%Y 연도 4자리
%m 월 2자리
%d 일 2자리
%H 24시간제 시
%M 분

따라서 결과는 다음과 같습니다.

2026-10-31 18:45

참고로 datetime.now()는 PC의 로컬 시간대를 사용합니다. 실제 업무용 보고서에서는 시간대가 명확하도록 datetime.now(ZoneInfo("Asia/Seoul"))를 사용하는 것이 더 좋습니다.

5-3. 파일명 생성

filename = f"report_{city}_{datetime.date.today()}.txt"

보고서 파일 이름을 만드는 부분입니다.

예를 들어 서울을 입력하고 오늘이 2026년 10월 31일이라면 다음과 같은 파일명이 만들어집니다.

report_Seoul_2026-10-31.txt

여기서 주의할 부분이 있습니다.

같은 도시와 같은 날짜에 보고서를 여러 번 만들면 파일명이 동일합니다.

하지만 이것은 문제가 아닙니다.

ArtifactService는 동일한 파일명을 다시 저장하면 기존 파일을 덮어쓰는 대신 새로운 버전을 생성합니다.

예를 들어 다음과 같이 관리됩니다.

report_Seoul_2026-10-31.txt

Version 0
Version 1
Version 2

이것이 ArtifactService의 버전 관리 기능입니다.

Agent Development Kit (ADK)

5-4. f-string을 이용한 보고서 생성

report_content = f"""
    [WEATHER INTELLIGENCE REPORT]
    --------------------------------
    Target City : {city}
    Time        : {today}
    --------------------------------

    1. Current Status
       - Temperature : {temp}°C
       - Condition   : {condition}
       - Humidity    : {humidity}%

    2. AI Comment
       Current weather in {city} is {condition}.
       Please refer to this data for your schedule.
    --------------------------------
    Generated by ADK Agent
"""

이 부분은 Python의 f-string을 이용해 텍스트 문서를 만드는 것입니다.

f-string은 문자열 내부에 변수의 값을 삽입할 수 있는 기능입니다.

예를 들어 다음과 같습니다.

city = "Seoul"
temp = 23

report = f"City: {city}, Temperature: {temp}"

실행 결과:

City: Seoul, Temperature: 23

이번에는 여러 줄로 구성된 보고서를 만듭니다.

예를 들어 데이터가 다음과 같다면,

city = "Seoul"
today = "2026-10-31 18:45"
temp = 23
condition = "Clear"
humidity = 65

최종 보고서 내용은 다음과 같습니다.

[WEATHER INTELLIGENCE REPORT]
--------------------------------
Target City : Seoul
Time        : 2026-10-31 18:45
--------------------------------

1. Current Status
   - Temperature : 23°C
   - Condition   : Clear
   - Humidity    : 65%

2. AI Comment
   Current weather in Seoul is Clear.
   Please refer to this data for your schedule.
--------------------------------
Generated by ADK Agent

여기까지는 일반적인 Python 문자열 처리입니다.

아직 ArtifactService와는 관계가 없습니다.

중요한 것은 이제부터입니다.

우리가 만든 문자열을 실제 파일처럼 저장할 수 있는 데이터 구조로 변환해야 합니다.

6. 5단계: String을 Bytes로 변환

log_bytes = report_content.encode("utf-8")

이번 실습에서 이해해야 할 중요한 개념입니다.

왜 문자열을 그대로 저장하지 않고 Bytes로 변환할까요?

ArtifactService는 일반적인 문자열이 아니라 바이너리 데이터를 관리하기 때문입니다.

6-1. String과 Bytes의 차이

Python에서 다음 두 데이터는 서로 다른 자료형입니다.

text = "안녕하세요"

이것은 문자열입니다.

data = b"Hello"

이것은 Bytes입니다.

두 자료형의 차이를 정리하면 다음과 같습니다.

구분 String Bytes
자료형 str bytes
의미 문자 데이터 바이트 데이터
처리 문자 중심 바이너리 중심
사용 예 문장, 텍스트 이미지, PDF, 파일 데이터

컴퓨터는 실제로 데이터를 바이트 단위로 저장하고 처리합니다.

따라서 파일을 저장하거나 전송할 때는 문자열을 바이트로 변환하는 작업이 필요합니다.

6-2. encode("utf-8")

log_bytes = report_content.encode("utf-8")

문자열을 UTF-8 방식으로 인코딩합니다.

예를 들어 다음과 같습니다.

text = "Hello"
data = text.encode("utf-8")

print(type(text))
print(type(data))

결과:

<class 'str'>
<class 'bytes'>

다시 문자열로 복원하려면 다음과 같이 합니다.

restored_text = data.decode("utf-8")

즉 다음과 같은 관계입니다.

 

7. 6단계: Artifact 객체 생성 (types.Part와 types.Blob)

이제 이번 코드에서 가장 중요한 데이터 구조를 살펴보겠습니다.

weather_artifact = types.Part(
    inline_data=types.Blob(
        mime_type="text/plain",
        data=log_bytes
    )
)

처음 보면 상당히 낯설 수 있습니다.

하지만 다음과 같이 이해하면 쉽습니다.

  • Blob: 실제 파일의 바이너리 데이터와 파일 형식 정보를 담는 객체

  • Part: 해당 데이터를 ADK와 Google GenAI에서 사용하는 콘텐츠 단위로 감싸는 객체

7-1. types.Blob

types.Blob(
    mime_type="text/plain",
    data=log_bytes
)

Blob은 Binary Large Object의 약자입니다.

바이너리 데이터와 그 데이터의 MIME 타입을 함께 표현합니다.

여기서는 두 가지 정보를 전달합니다.

① mime_type

mime_type="text/plain"

파일의 종류를 지정합니다.

MIME 타입은 파일의 데이터 형식을 나타내는 표준 식별자입니다.


MIME 타입 파일 종류
text/plain 일반 텍스트
text/html HTML
application/pdf PDF
application/json JSON
image/png PNG 이미지
image/jpeg JPEG 이미지
audio/mpeg MP3 오디오
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet Excel XLSX

여기서는 TXT 파일이므로 text/plain을 사용합니다.

② data

data=log_bytes

앞에서 만든 Bytes 데이터를 전달합니다.

즉 다음과 같은 의미입니다.

이 데이터는 일반 텍스트 파일이고,
실제 파일 내용은 log_bytes에 들어 있다.

7-2. types.Part

weather_artifact = types.Part(
    inline_data=...
)

이제 Blob 객체를 Part 객체로 감쌉니다.

왜 이렇게 할까요?

ADK의 Artifact 데이터는 Google GenAI의 표준 Part 객체를 사용하기 때문입니다.

즉 ArtifactService에 저장하기 위해서는 파일 내용을 types.Part 형식으로 준비하는 것이 기본적인 방식입니다.

Agent Development Kit (ADK)+1

7-3. 더 간결하게 작성하는 방법

기존 코드도 올바른 형태입니다.

다만 다음과 같이 간결하게 표현할 수도 있습니다.

weather_artifact = types.Part.from_bytes(
    data=log_bytes,
    mime_type="text/plain"
)

두 코드는 동일한 목적을 수행합니다.

저는 두 번째 방식을 추천합니다.

코드가 짧고, 바이트 데이터를 MIME 타입과 함께 Part 객체로 만든다는 의도가 명확하기 때문입니다.

8. 7단계: 사용자, 앱, 세션 정보 확인

이제 다음 코드를 보겠습니다.

user_id = getattr(tool_context, "user_id", "unknown_user")

app_name = getattr(tool_context, "app_name", "unknown_app")

session_id = getattr(tool_context, "session_id", "default_session")

이 부분은 ArtifactService가 파일을 어떤 범위에서 관리하는지 이해하는 데 도움이 됩니다.

8-1. getattr() 함수

Python의 내장 함수입니다.

getattr(객체, "속성명", 기본값)

특정 객체에서 속성을 가져오는 함수입니다.

만약 해당 속성이 존재하지 않으면 기본값을 반환합니다.

예를 들어 다음과 같습니다.

class Person:
    name = "Steven"

person = Person()

print(getattr(person, "name", "Unknown"))
print(getattr(person, "age", 0))

결과:

Steven
0

따라서 이번 코드에서는 ToolContext에서 다음 정보를 가져오려고 합니다. (아래 속성과 의미)

user_id 현재 사용자 식별자
app_name ADK 애플리케이션 이름
session_id 현재 대화 세션 식별자

8-2. 이 정보가 왜 중요할까?

ArtifactService는 단순히 파일 이름만으로 파일을 관리하지 않습니다.

기본적으로 다음과 같은 식별 정보를 이용합니다.

app_name
    +
user_id
    +
session_id
    +
filename

예를 들어 다음과 같은 구조입니다.

weather_app
   │
   ├── user_001
   │      │
   │      ├── session_001
   │      │       └── report_Seoul.txt
   │      │
   │      └── session_002
   │              └── report_Busan.txt
   │
   └── user_002
          │
          └── session_003
                  └── report_Seoul.txt

이렇게 관리하면 서로 다른 사용자의 파일이 섞이지 않습니다.

또한 기본 파일명은 해당 세션에 종속됩니다.

다른 세션에서도 동일한 사용자의 파일에 접근하게 만들려면 GCS ArtifactService에서 user: 접두사를 사용하는 사용자 범위 저장 기능을 활용할 수 있습니다.

예를 들면 다음과 같습니다.

filename = "user:weather_report.txt"

다만 이번 실습에서는 일반 파일명을 사용하므로 세션 범위 저장입니다.

Agent Development Kit (ADK)+1

8-3. session_id는 꼭 직접 추출해야 할까?

아닙니다.

기존 코드의 주석에는 다음과 같이 되어 있습니다.

# session_id 추출 필수

이 부분은 수정하는 것이 좋겠습니다.

실제 저장할 때는 다음 메서드가 실행됩니다.

await tool_context.save_artifact(...)

이 메서드가 현재 ToolContext의 실행 문맥을 통해 사용자, 앱, 세션 정보를 사용합니다.

따라서 우리가 직접 session_id를 추출해서 저장 함수에 전달할 필요는 없습니다.

현재 코드는 로그 확인을 위한 용도로 추출하고 있는 것입니다.

즉 다음과 같이 이해하면 됩니다.

  • 저장에 필요한 세션 정보: ADK가 ToolContext를 통해 처리

  • 개발자가 출력하는 세션 정보: 디버깅과 확인용

9. 8단계: 실제 파일 저장 - save_artifact()

드디어 가장 중요한 부분입니다.

version_id = await tool_context.save_artifact(
    filename=filename,
    artifact=weather_artifact,
)

이 한 줄이 ArtifactService에 파일을 저장하는 핵심 기능입니다.

9-1. tool_context.save_artifact()

이 메서드는 현재 ADK 실행 환경에 연결된 ArtifactService에 파일을 저장합니다.

여기서 중요한 점은 ToolContext가 직접 파일을 저장하는 것이 아니라는 사실입니다.

실제 저장은 연결된 ArtifactService가 담당합니다.

구조는 다음과 같습니다.

generate_weather_report()
        │
        ▼
tool_context.save_artifact()
        │
        ▼
현재 InvocationContext
        │
        ▼
ArtifactService
        │
        ▼
파일 저장 및 버전 관리

9-2. filename

filename=filename

저장할 파일의 이름입니다.

예를 들어 다음과 같습니다.

report_Seoul_2026-10-31.txt

9-3. artifact

artifact=weather_artifact

실제로 저장할 파일 데이터입니다.

앞서 만들었던 types.Part 객체입니다.

즉 다음과 같은 정보가 들어 있습니다.

파일명:
report_Seoul_2026-10-31.txt

파일 형식:
text/plain

파일 내용:
[WEATHER INTELLIGENCE REPORT]
...

9-4. await

await tool_context.save_artifact(...)

왜 await를 사용할까요?

파일 저장 작업은 비동기 작업으로 수행되기 때문입니다.

특히 GCS와 같은 클라우드 저장소를 이용하면 네트워크를 통해 파일을 전송해야 합니다.

따라서 저장 작업이 완료될 때까지 비동기적으로 기다립니다.

9-5. version_id

version_id = await tool_context.save_artifact(...)

저장이 완료되면 저장된 파일의 버전 번호를 반환합니다.

예를 들어 다음과 같습니다.

version_id = 0

같은 파일명을 다시 저장하면 다음과 같이 됩니다.

첫 번째 저장: Version 0
두 번째 저장: Version 1
세 번째 저장: Version 2

기존 파일을 삭제하고 덮어쓰는 방식이 아니라 버전을 누적하는 방식입니다.

이것은 보고서 수정 이력이나 문서 변경 이력을 관리할 때 매우 유용합니다.

Agent Development Kit (ADK)

10. 9단계: 저장 결과를 에이전트에게 반환

return {
    "status": "success",
    "message": "날씨 로그가 저장되었습니다.",
    "file_info": {
        "filename": filename,
        "version": version_id,
        "mime_type": "text/plain"
    }
}

이 부분은 Tool 실행 결과를 반환하는 코드입니다.

여기서 구분해야 할 것이 있습니다.

파일을 저장하는 것과 저장 결과를 에이전트에게 알려주는 것은 서로 다른 작업입니다.

앞의 save_artifact()가 파일을 저장했다면, 이번 return은 저장 결과를 Gemini에게 알려주는 것입니다.

반환 데이터의 구조를 살펴보겠습니다.

10-1. status

"status": "success"

작업의 성공 여부를 나타냅니다.

10-2. message

"message": "날씨 로그가 저장되었습니다."

도구가 수행한 작업에 대한 설명입니다.

10-3. file_info

"file_info": {
    "filename": filename,
    "version": version_id,
    "mime_type": "text/plain"
}

저장된 파일의 정보를 전달합니다.

예를 들어 다음과 같은 결과가 만들어집니다.

{
    "status": "success",
    "message": "날씨 로그가 저장되었습니다.",
    "file_info": {
        "filename": "report_Seoul_2026-10-31.txt",
        "version": 0,
        "mime_type": "text/plain"
    }
}

이 결과는 Gemini에게 전달됩니다.

Gemini는 이 결과를 바탕으로 사용자에게 다음과 같이 답변할 수 있습니다.

서울의 날씨 보고서를 생성하여 저장했습니다.

파일명: report_Seoul_2026-10-31.txt
버전: 0
파일 형식: text/plain

다만 여기서 주의할 점이 있습니다.

파일이 ArtifactService에 저장되었다고 해서 사용자가 자동으로 파일을 다운로드할 수 있는 링크가 생성되는 것은 아닙니다.

현재 코드는 파일 저장 및 파일 정보를 반환하는 데까지만 구현되어 있습니다.

실제 다운로드 기능까지 구현하려면 다음과 같은 추가 작업이 필요합니다.

  • load_artifact()로 파일 불러오기

  • 파일 데이터를 웹 응답이나 다운로드 엔드포인트로 전달

  • 또는 별도의 다운로드 URL 제공

이것은 실제 업무용 에이전트를 만들 때 중요한 차이입니다.

11. 10단계: Agent 정의

이제 도구를 실제로 사용할 에이전트를 살펴보겠습니다.

weather_agent = Agent(
    name="WeatherBot",
    model=os.getenv("GOOGLE_MODEL", "gemini-3.5-flash"),
    tools=[generate_weather_report],
    instruction="""
    당신은 유능한 날씨 정보 비서입니다.
    사용자가 특정 지역의 날씨를 물어보면, 도구를 사용하여 정보를 조회하고
    반드시 결과 파일을 생성해서 제공해야 합니다.
    """
)

이 코드는 앞에서 공부했던 ADK Agent 구성 방식과 동일합니다.

다만 이번에는 날씨 보고서 생성 Tool을 연결했다는 점이 다릅니다.

11-1. name

name="WeatherBot"

에이전트 이름입니다.

11-2. model

model=os.getenv("GOOGLE_MODEL", "gemini-3.5-flash")

에이전트가 사용할 LLM입니다.

앞서 설명한 것처럼 환경변수 GOOGLE_MODEL이 설정되어 있으면 그 값을 사용하고, 그렇지 않으면 기본값을 사용합니다.

참고로 gemini-3.5-flash는 현재 사용 중인 프로젝트와 ADK 버전에서 지원되는 모델명인지 확인해야 합니다.

11-3. tools

tools=[generate_weather_report]

이 부분이 매우 중요합니다.

우리가 만든 Python 함수를 에이전트가 사용할 수 있도록 등록합니다.

이렇게 등록하면 Gemini는 사용자 질문을 분석하고, 필요하다고 판단하면 해당 함수를 호출할 수 있습니다.

예를 들어 다음과 같이 질문할 수 있습니다.

서울 날씨를 조회하고 보고서를 만들어 줘.

그러면 Gemini가 Tool을 호출할 수 있습니다.

개념적으로는 다음과 같습니다.

generate_weather_report(city="서울")

여기서 중요한 것은 Gemini가 Python 함수를 직접 실행하는 것이 아니라는 점입니다.

정확하게는 다음과 같은 구조입니다.

  1. Gemini가 Tool 호출이 필요하다고 판단합니다.

  2. 호출할 Tool의 이름과 인자를 전달합니다.

  3. ADK 런타임이 실제 Python 함수를 실행합니다.

  4. 실행 결과를 Gemini에게 전달합니다.

  5. Gemini가 최종 답변을 생성합니다.

이것이 ADK Function Tool의 기본 작동 방식입니다.

11-4. instruction

instruction="""당신은 유능한 날씨 정보 비서입니다.사용자가 특정 지역의 날씨를 물어보면, 도구를 사용하여 정보를 조회하고반드시 결과 파일을 생성해서 제공해야 합니다."""

에이전트의 행동 지침입니다.

여기서는 두 가지를 지시합니다.

  • 날씨 질문에는 Tool을 사용한다.

  • 결과 파일을 반드시 생성한다.

다만 실제 업무용으로 발전시키려면 다음 사항을 더 명확하게 지시하는 것이 좋습니다.

  • 보고서 생성 전에 도시명을 확인할 것

  • Tool 실행 결과를 확인할 것

  • 저장 실패 시 성공했다고 답변하지 않을 것

  • Mock 데이터라면 실제 관측 날씨처럼 표현하지 않을 것

이렇게 해야 사용자가 결과를 신뢰할 수 있습니다.

12. 11단계: root_agent

root_agent = weather_agent

이것은 ADK의 진입점입니다.

ADK Web은 프로젝트에서 root_agent를 찾아 실행합니다.

따라서 우리가 만든 weather_agent를 root_agent에 할당하면 됩니다.

여기까지가 전체 Python 코드입니다.

13. 현재 코드에서 수정하면 좋은 부분

이제 원래 코드를 그대로 사용하는 것보다 조금 더 좋은 형태로 정리해 보겠습니다.

제가 권장하는 수정은 다음과 같습니다.

항목 기존 코드 수정 방향
ArtifactService 객체 생성만 하고 미사용 제거
ToolContext tool_context=None tool_context: ToolContext
파일 생성 도시명 그대로 사용 파일명에 사용할 문자 정리
날짜와 시간 로컬 시간 사용 서울 시간대 명시
Part 생성 Part와 Blob 중첩 Part.from_bytes()
Mock 데이터 영문 날씨 한글 날씨
오류 처리 별도 처리 없음 저장 오류 기록 및 예외 전달
instruction 일반적인 지시 Mock 데이터 및 저장 실패 처리 명시

특히 가장 중요한 수정은 첫 번째와 두 번째입니다.

수정 1. artifact_service 객체 제거

기존 코드:

artifact_service = InMemoryArtifactService()

이 객체는 adk web을 실행할 때 자동으로 사용되지 않습니다.

왜냐하면 이 객체를 Runner에 전달하지 않았기 때문입니다.

공식 ADK 문서에서도 Python으로 직접 Runner를 구성하는 경우 artifact_service를 Runner에 전달해야 한다고 설명합니다.

Agent Development Kit (ADK)+1

반면 adk web을 사용하면 CLI가 자체적으로 ArtifactService를 구성합니다.

따라서 현재 방식에서는 불필요한 객체입니다.

수정 2. ToolContext 타입 명시

기존:

async def generate_weather_report(city: str, tool_context=None):

수정:

async def generate_weather_report(
    city: str,
    tool_context: ToolContext
):

이렇게 하면 ADK가 ToolContext를 주입해야 한다는 의도가 분명해집니다.

수정 3. 파일명 안전성

기존:

filename = f"report_{city}_{datetime.date.today()}.txt"

도시명에 공백이나 특수문자가 포함되면 파일명 관리가 불편할 수 있습니다.

예를 들어 다음과 같습니다.

서울 특별시
New York
부산/해운대

따라서 파일명에 사용할 문자를 정리하는 것이 좋습니다.

수정 4. 서울 시간대 명시

from zoneinfo import ZoneInfo
now = datetime.datetime.now(ZoneInfo("Asia/Seoul"))

이렇게 하면 실행 PC의 시간대 설정과 관계없이 서울 시간을 기준으로 보고서를 생성할 수 있습니다.

14. 권장 최종 수정 코드

원래 코드의 구조와 학습 목적은 유지하면서 불필요한 부분을 정리하고, 오류 처리와 파일명 관리 등을 보완했습니다.

실습 권장 코드

import datetime
import os
import random
import re
from zoneinfo import ZoneInfo

from google.genai import types
from google.adk import Agent
from google.adk.tools import ToolContext


# ============================================================
# 1. 날씨 보고서 생성 Tool
# ============================================================

async def generate_weather_report(
    city: str,
    tool_context: ToolContext
):
    """
    특정 도시의 Mock 날씨 데이터를 생성하고,
    보고서를 ArtifactService에 저장한다.

    Args:
        city: 날씨를 조회할 도시 이름
        tool_context: ADK가 주입하는 실행 컨텍스트
    """

    print(f"\n[*] Tool 실행: {city} 날씨 보고서 생성")

    # --------------------------------------------------------
    # 1-1. Mock 날씨 데이터 생성
    # --------------------------------------------------------

    temp = random.randint(10, 30)

    conditions = ["맑음", "구름 조금", "비", "바람 강함"]
    condition = random.choice(conditions)

    humidity = random.randint(40, 90)

    # --------------------------------------------------------
    # 1-2. 날짜 및 시간 생성
    # --------------------------------------------------------

    now = datetime.datetime.now(
        ZoneInfo("Asia/Seoul")
    )

    today = now.strftime("%Y-%m-%d %H:%M")
    date_string = now.strftime("%Y-%m-%d")

    # --------------------------------------------------------
    # 1-3. 파일명 생성
    # --------------------------------------------------------

    safe_city = re.sub(
        r"[^a-zA-Z0-9가-힣_-]",
        "_",
        city
    )

    filename = f"report_{safe_city}_{date_string}.txt"

    # --------------------------------------------------------
    # 1-4. 보고서 내용 생성
    # --------------------------------------------------------

    report_content = f"""
[WEATHER INTELLIGENCE REPORT]
--------------------------------
대상 도시 : {city}
생성 시간 : {today}
--------------------------------

1. 현재 날씨 정보

- 기온 : {temp}°C
- 날씨 : {condition}
- 습도 : {humidity}%

2. AI 분석 의견

현재 {city}의 날씨 상태는 {condition}입니다.

본 보고서는 실습용 Mock 데이터를 기반으로 생성되었습니다.
실제 기상 관측 정보가 아닙니다.

--------------------------------
Generated by ADK Weather Agent
"""

    print("[*] 보고서 텍스트 생성 완료")

    # --------------------------------------------------------
    # 1-5. String -> Bytes 변환
    # --------------------------------------------------------

    report_bytes = report_content.encode("utf-8")

    # --------------------------------------------------------
    # 1-6. Artifact 객체 생성
    # --------------------------------------------------------

    weather_artifact = types.Part.from_bytes(
        data=report_bytes,
        mime_type="text/plain"
    )

    print("[*] Artifact 객체 생성 완료")

    # --------------------------------------------------------
    # 1-7. 현재 사용자 및 세션 정보 확인
    # --------------------------------------------------------

    print(
        f"[*] App: {tool_context.app_name}, "
        f"User: {tool_context.user_id}, "
        f"Session: {tool_context.session_id}"
    )

    # --------------------------------------------------------
    # 1-8. ArtifactService에 파일 저장
    # --------------------------------------------------------

    try:

        version_id = await tool_context.save_artifact(
            filename=filename,
            artifact=weather_artifact
        )

        print(
            f"[*] 저장 완료: {filename}, "
            f"Version: {version_id}"
        )

    except Exception as e:

        print(f"[!] Artifact 저장 실패: {e}")

        raise

    # --------------------------------------------------------
    # 1-9. Tool 실행 결과 반환
    # --------------------------------------------------------

    return {
        "status": "success",
        "message": "날씨 보고서가 생성 및 저장되었습니다.",
        "file_info": {
            "filename": filename,
            "version": version_id,
            "mime_type": "text/plain"
        }
    }


# ============================================================
# 2. ADK Agent 정의
# ============================================================

weather_agent = Agent(

    name="WeatherBot",

    model=os.getenv(
        "GOOGLE_MODEL",
        "gemini-3.5-flash"
    ),

    tools=[
        generate_weather_report
    ],

    instruction="""
    당신은 유능한 날씨 정보 비서입니다.

    사용자가 특정 지역의 날씨를 요청하면
    반드시 generate_weather_report 도구를 사용하세요.

    도구 실행 결과를 확인한 후 사용자에게 보고서 생성 결과를 알려주세요.

    보고서가 성공적으로 저장되지 않았다면
    저장되었다고 답변하지 마세요.

    이 에이전트는 실습용 Mock 데이터를 사용하므로
    실제 기상 관측 정보라고 표현하지 마세요.

    답변은 모두 한글로 작성하세요.
    """

)


# ============================================================
# 3. ADK 실행 진입점
# ============================================================

root_agent = weather_agent

여기서 한 가지 더 설명하겠습니다.

위 코드에서 다음과 같이 tool_context의 정보를 출력했습니다.

tool_context.app_nametool_context.user_idtool_context.session_id

이 정보들은 디버깅할 때 유용합니다.

또한 파일명에서 도시명을 정리하기 위해 re.sub()를 사용했습니다.

safe_city = re.sub(
    r"[^a-zA-Z0-9가-힣_-]",
    "_",
    city
)

정규표현식을 이용해 파일명에 사용할 문자를 제한하는 것입니다.

예를 들어 다음과 같이 변환됩니다.

원래 도시명 파일명에 사용할 값
Seoul Seoul
서울 서울
New York New_York
부산/해운대 부산_해운대

이렇게 하면 파일명 관리가 좀 더 편리해집니다.

15. 실행 명령어: 지금 사용한 방식이 맞을까?

이제 가장 궁금해하신 실행 명령어를 살펴보겠습니다.

사용한 명령:

adk web --port 8080 --memory_service_uri="agentengine://projects/921865052026/locations/asia-northeast3/reasoningEngines/2528612861094133760"
명령 자체는 맞습니다

다만 이것이 어떤 서비스를 설정하는 명령인지 정확하게 구분해야 합니다.

현재 명령어를 분석하면 다음과 같습니다.

옵션역할
adk web ADK Web 서버 실행
--port 8080 웹 서버 포트 지정
--memory_service_uri MemoryService 연결
agentengine://... Vertex AI Memory Bank 리소스 지정

여기서 빠진 것이 하나 있습니다.

바로 다음 옵션입니다.

--artifact_service_uri

현재 ADK CLI에서는 ArtifactService를 별도로 지정하지 않으면 기본 저장 서비스를 사용합니다. 최근 ADK CLI 문서 기준으로 로컬 .adk 디렉터리에 파일을 저장하는 방식이 기본입니다. 버전과 실행 환경에 따라 기본 설정은 달라질 수 있습니다.

ADK CLI 2.13.0 documentation+1

따라서 현재 명령으로도 ArtifactService를 사용할 수 있습니다.

다만 저장 위치를 명시적으로 지정하는 편이 학습에는 좋습니다.

15-1. 방법 A: 메모리 기반 ArtifactService

adk web --port 8080 `
  --memory_service_uri="agentengine://projects/921865052026/locations/asia-northeast3/reasoningEngines/2528612861094133760" `
  --artifact_service_uri="memory://"

이렇게 실행하면 다음과 같습니다.

서비스설정저장 위치
MemoryService agentengine:// Google Cloud Memory Bank
ArtifactService memory:// Python 메모리

이 방식은 ArtifactService의 기본 개념을 실습할 때 좋습니다.

단, 서버를 종료하면 메모리 기반 Artifact 데이터는 유지되지 않습니다.

15-2. 방법 B: 로컬 파일 저장

adk web --port 8080 `
  --memory_service_uri="agentengine://projects/921865052026/locations/asia-northeast3/reasoningEngines/2528612861094133760" `
  --artifact_service_uri="file://C:/Dev/book_agentic_ai/adk_artifacts"

이 방식은 파일을 지정한 로컬 디렉터리에 저장합니다.

서버를 종료해도 파일이 유지되므로 개발 및 테스트에 유용합니다.

15-3. 방법 C: Google Cloud Storage 저장

클라우드 기반 실습 추천
adk web --port 8080 `
  --memory_service_uri="agentengine://projects/921865052026/locations/asia-northeast3/reasoningEngines/2528612861094133760" `
  --artifact_service_uri="gs://agent-project_512212-artifacts"

이렇게 하면 다음과 같은 구조가 됩니다.

서비스저장 위치
SessionService ADK 기본 설정
MemoryService Vertex AI Memory Bank
ArtifactService Google Cloud Storage

이 경우 GCS 버킷을 실제로 만들어야 하고, 인증 계정에 해당 버킷에 파일을 저장하고 조회할 수 있는 권한이 있어야 합니다.

참고로 앞서 Agent Engine 생성에 사용했던 Staging Bucket과 Artifact 저장용 Bucket은 역할이 다릅니다. 같은 버킷을 사용할 수도 있지만, 관리와 권한 분리를 위해 별도로 만드는 것을 권장합니다.

공식 ADK CLI에서 지원하는 ArtifactService URI는 gs://, memory://, file://입니다.

ADK CLI 2.12.0 documentation+1

16. 저장한 파일을 다시 불러오려면?

이 부분까지 실습하면 ArtifactService를 훨씬 정확하게 이해할 수 있습니다.

현재 코드는 파일을 저장하는 기능만 있습니다.

그런데 실제 업무에서는 저장한 파일을 다시 불러오고 확인할 수 있어야 합니다.

ADK에서는 다음 메서드를 사용합니다.

await tool_context.load_artifact()

예를 들어 다음과 같은 Tool을 추가할 수 있습니다.

async def read_weather_report(
    filename: str,
    tool_context: ToolContext
):
    """
    저장된 날씨 보고서를 불러오는 도구
    """

    artifact = await tool_context.load_artifact(
        filename=filename
    )

    if artifact is None:
        return {
            "status": "not_found",
            "message": "파일을 찾을 수 없습니다."
        }

    # Bytes -> String 복원
    report_text = artifact.inline_data.data.decode("utf-8")

    return {
        "status": "success",
        "filename": filename,
        "content": report_text
    }

여기서 앞서 배웠던 개념들이 다시 등장합니다.

코드역할
load_artifact() 저장된 파일 불러오기
artifact.inline_data 파일의 실제 데이터 영역
.data 파일의 Bytes 데이터
.decode("utf-8") Bytes를 문자열로 복원

이번에는 반대 방향입니다.

ArtifactService
      │
      ▼
load_artifact()
      │
      ▼
types.Part
      │
      ▼
inline_data.data
      │
      ▼
Bytes
      │
      ▼
decode("utf-8")
      │
      ▼
일반 Python String

추가로 저장된 파일 목록을 확인할 때는 다음 메서드를 사용합니다.

filenames = await tool_context.list_artifacts()print(filenames)

이렇게 하면 현재 사용 가능한 파일명 목록을 확인할 수 있습니다.

ADK는 파일 조회, 목록 확인, 버전 관리 등도 지원합니다.

Agent Development Kit (ADK)+1

17. 이번 실습에서 꼭 기억해야 할 핵심 개념 7가지

이번에는 다소 많은 새로운 개념이 등장했습니다.

마지막으로 핵심만 정리해 보겠습니다.

Artifact 에이전트가 생성하거나 관리하는 파일 및 바이너리 데이터
ArtifactService Artifact를 저장하고 관리하는 서비스
ToolContext Tool 실행 시 ADK가 제공하는 실행 문맥
types.Part Artifact를 표현하는 표준 콘텐츠 객체
types.Blob 실제 바이트 데이터와 MIME 타입을 담는 객체
save_artifact() 파일 저장 및 버전 생성
load_artifact() 저장된 파일 조회

그리고 세 가지 ArtifactService 구현 방식을 기억하세요.

 

18. 다음 단계로 넘어가기 전 추천하는 실습

제가 추천하는 것은 이번 날씨 보고서 생성기를 조금 발전시키는 것입니다.

단순히 파일을 저장하는 것에서 끝내지 말고, 다음 세 가지 기능을 추가해 보세요.

첫 번째: 보고서 생성

서울 날씨 보고서 생성해 줘.

→ 파일 생성 및 저장

 

두 번째: 보고서 조회

방금 생성한 서울 날씨 보고서 내용을 보여줘.

→ load_artifact() 실행

 

세 번째: 보고서 버전 관리

서울 날씨 보고서를 다시 생성해 줘.

→ 같은 파일명으로 새로운 버전 생성

 

이 세 가지가 성공하면 ArtifactService의 핵심 기능을 이해했다고 볼 수 있습니다.

그리고 다음 단계에서는 날씨 보고서를 TXT가 아니라 PDF나 DOCX로 생성해 보는 것도 좋겠습니다.

특히 지금 공부하고 있는 HWP 자동화 및 OfficeXAI와도 연결할 수 있는 기술입니다.

최종적으로 이번 실습의 의미를 한 문장으로 정리하면 다음과 같습니다.

MemoryService가 AI Agent에게 과거를 기억하게 해주는 서비스라면, ArtifactService는 AI Agent가 실제 업무 결과물을 만들고 저장하며 다시 활용할 수 있도록 해주는 서비스입니다.

이 두 가지가 SessionService와 결합되면, 대화의 맥락을 유지하면서 과거를 기억하고 실제 파일까지 만들어 내는 업무형 AI Agent의 기본 구조가 완성됩니다.

공식 학습 자료:

반응형