🤖 이 글은 Google **ADK(Agent Development Kit)**로 주식 분석 멀티 에이전트 Financial Advisor를 만들며 익힌 핵심 개념 — ADK Web · Tools & Sub-agents · Agent 아키텍처 · State · Artifacts — 을 정리한 기록입니다.
**Google ADK(Agent Development Kit)**는 구글이 만든 오픈소스·코드 우선(code-first) 에이전트 개발 프레임워크다. 별도의 UI 프레임워크(Streamlit 등) 없이도 개발용 웹 UI·API 서버·세션·메모리·아티팩트 관리가 기본으로 제공되어 에이전트 개발을 "소프트웨어 개발"처럼 다룰 수 있게 해준다.
💡 여러 언어 지원 — ADK는 Python, TypeScript, Go, Java를 공식 지원하며, Kotlin·Android는 샘플 형태로 제공된다. 구글이 자사 제품에 사용하는 에이전트들도 ADK 기반으로 알려져 있다.
💡 모델 선택은 자유롭게 — ADK는 Gemini에 최적화돼 있지만 모델에 종속되지 않는다.
LiteLlm래퍼를 쓰면 OpenAI·Anthropic 등 다른 LLM도 그대로 붙일 수 있다.
# financial_advisor/agent.py
# LiteLlm: Gemini 외에도 여러 LLM으로 작업할 수 있게 해주는 패키지
from google.adk.models.lite_llm import LiteLlm
MODEL = LiteLlm("openai/gpt-4o")root 폴더에 원하는 이름의 폴더(에이전트 별칭)를 만들고 내부에 __init__.py , agent.py 라는 이름으로 파일을 생성한다.
반드시 위와 같은 형식의 파일이어야 한다.
financial-analyst/ # 프로젝트 루트 (이 위치에서 adk web 실행)
└── financial_advisor/ # 에이전트 패키지 (별칭)
├── __init__.py # 패키지 진입점
└── agent.py # root_agent 변수 필수⚠️
agent.py에는 반드시root_agent****라는 변수가 있어야 하고,__init__.py는 그 모듈을 import 해 패키지의 진입점 역할을 해야 한다. ADK는 패키지에 들어와__init__.py→root_agent순으로 에이전트를 찾는다.
# financial_advisor/__init__.py
# __init__.py가 있으면 그 폴더는 파이썬 패키지가 되고, 이 파일이 패키지의 진입점이 된다.
# ADK가 financial_advisor 패키지로 들어오면 __init__.py를 찾고,
# 이를 통해 root_agent 변수가 있는 agent 모듈을 찾는다.
from . import agent가상환경 활성화 후 에이전트가 있는 디렉토리가 아니라 그보다 상위 경로에서 adk web 명령어를 실행하면 ADK Web Server가 뜬다.
접속하면 에이전트와 대화하는 UI가 완성되어 있고, 심지어 파일 업로드와 세션 메모리도 자동으로 구축되어 있다.
또한 토큰을 얼마나 사용했는지, 어떤 도구와 에이전트가 실행되었는지 도식으로 보여주어 DX를 크게 향상시킨다.
⚠️ 주의 — 위 기능들은 어디까지나 개발자를 위한 것이다. 디버깅 정보·API 키 등 불필요하거나 민감한 정보가 노출되므로, 실제 상용 서비스에는 그대로 사용할 수 없다.



ADK Web Server 주소에 /docs를 추가하면 API Server로 연결되어 Swagger 문서가 표시된다. 거기엔 에이전트 리스트를 불러오는 API, 에이전트를 실행하는 API 등 내가 만든 에이전트를 구동하는 데 필요한 모든 API가 정의되어 있다.
💡
adk web은 내부적으로 FastAPI 서버를 띄운다. 그래서/docs(Swagger UI)가 자동 제공되며, 같은 API를 그대로 호출해 UI 없이도 에이전트를 프로그램적으로 실행할 수 있다. 배포 단계에서는adk api_server로 API 서버만 따로 띄우기도 한다.

💡 Tool이란? 에이전트가 호출할 수 있는 함수다. ADK에서는 일반 파이썬 함수를
tools리스트에 넘기기만 하면, 함수의 이름·시그니처·docstring을 읽고 LLM이 언제 어떤 인자로 호출할지 스스로 판단한다. 그래서 docstring을 명확히 작성하는 것이 중요하다.
agent = Agent(
name="WeatherAgent",
instruction="You help the user with weather related questions.",
model=MODEL,
tools=[get_weather],
)
이러한 기능들이 모두 기본으로 구현되어 있다. 뿐만 아니라 LLM이 API로 보낸 request, response 등도 모두 확인할 수 있다.
현재 프로젝트(financial_advisor)에선 yfinance로 주가·재무 데이터를 가져오는 함수를 그대로 tool로 등록해서 반환값을 구조화된 dict로 받는다.
# financial_advisor/sub_agents/data_analyst.py
import yfinance as yf
def get_company_info(ticker: str) -> str:
"""
주어진 종목 티커의 기본 회사 정보(이름·산업·섹터)를 조회한다.
Args:
ticker (str): 주식 티커 심볼 (예: 'AAPL')
Returns:
dict: ticker, success, company_name, industry, sector
"""
stock = yf.Ticker(ticker)
info = stock.info
return {
"ticker": ticker,
"success": True,
"company_name": info.get("longName", "NA"),
"industry": info.get("industry", "NA"),
"sector": info.get("sector", "NA"),
}
✅ 비용 팁(소스 주석) — 뉴스 검색용
news_analyst는 Firecrawl로 웹 검색 tool을 구현할 수도 있지만yfinance의get_newsAPI로 대체할 수 있다. 별도 API 키 없이 종목 뉴스를 가져올 수 있어 비용을 아끼는 더 좋은 방법이다. (공식 문서 참고)
💡 ADK에서 여러 에이전트를 조합하는 방법은 두 가지다. **Sub-agent(위임)**는 대화의 통제권을 넘기고, **AgentTool(도구화)**은 통제권을 유지한 채 tool 호출과 같이 다른 에이전트를 함수처럼 호출한다.
Google ADK에서의 sub_agent는 OpenAI SDK에서의 handoff와 동일한 역할을 한다.
즉, 에이전트 간 전환이 이루어지면 대화의 통제권이 넘어간다. 이를 원하지 않으면 에이전트를 tool로 바꾸면 된다.
from google.adk.tools.agent_tool import AgentTool
agent = Agent(
name="WeatherAgent",
instruction="You help the user with weather related questions.",
model=MODEL,
tools=[get_weather, convert_units, AgentTool(agent=geo_agent)],
)이렇게 하면 다른 에이전트들은 메인 에이전트의 tool로서 사용되고, 사용자는 메인 에이전트하고만 대화하면 된다.
참고로 OpenAI SDK와 달리 다시 부모 에이전트로 거슬러 올라가는(transfer-to-parent) 기능도 구현할 수 있다. (disallow_transfer_to_parent 플래그로 제어)
| 구분 | Sub-agent (sub_agents=[...]) | AgentTool (tools=[AgentTool(...)]) |
|---|---|---|
| 제어권 | 대상 에이전트로 이전(transfer) | 호출한 에이전트가 유지 |
| 비유 | OpenAI SDK의 handoff | 다른 에이전트를 함수처럼 호출 |
| 사용자 대화 상대 | 전환된 에이전트 | 항상 메인 에이전트 |
각 분석가 에이전트(sub-agent)는 아래처럼 LlmAgent로 정의한다. 이때 description****이 매우 중요하다.
# financial_advisor/sub_agents/data_analyst.py
from google.adk.agents import LlmAgent
data_analyst = LlmAgent(
name="DataAnalyst",
model=MODEL,
# root_agent가 "이 에이전트가 언제 필요한지" 판단할 수 있도록 역할/기준을 기술
description="Gathers and analyzes basic stock market data using multiple focused tools",
instruction="You are a Data Analyst who gathers stock information ...",
tools=[get_company_info, get_stock_price, get_financial_metrics],
output_key="data_analyst_result", # 결과를 state에 저장
)💡 **
description**은 라우팅의 핵심 — 메인 에이전트(또는 부모)는 각 sub-agent·AgentTool의description을 읽고 "지금 이 에이전트가 필요한가"를 판단한다. 그래서 sub-agent에는 자신이 무엇을 하는지 명확한description을 꼭 적어야 한다. (주석: "root_agent가 볼 수 있도록, 이 에이전트가 언제 필요한지 판단할 수 있도록 역할/기준을 기술")
본 프로젝트의 메인 에이전트 FinancialAdvisor는 세 분석가 에이전트를 AgentTool로 묶어, 사용자는 메인 에이전트하고만 대화하도록 설계했다.
# financial_advisor/agent.py
from google.adk.agents import Agent
from google.adk.tools.agent_tool import AgentTool # agent를 tool로 변환
from financial_advisor.prompt import PROMPT
from financial_advisor.sub_agents import data_analyst, financial_analyst, news_analyst
# Google ADK에서 Agent는 LlmAgent의 별칭(alias)
financial_advisor = Agent(
name="FinancialAdvisor",
instruction=PROMPT,
model=MODEL,
tools=[
AgentTool(agent=financial_analyst),
AgentTool(agent=news_analyst),
AgentTool(agent=data_analyst),
save_advice_report, # 일반 함수 tool도 함께 등록 가능
],
# OpenAI SDK의 Handoff와 같은 역할을 하는 것이 sub_agents (통제권 이전)
# sub_agents=[geo_agent],
)
# agent.py에는 반드시 root_agent 변수가 있어야 한다
root_agent = financial_advisor✅ Tip — ADK에서 tool은 단순
string을 반환하기보다 아래처럼 결과를 구조화된 dict로 돌려주는 것을 권장한다.success플래그나 명확한 키가 있으면 LLM이 결과를 더 정확히 해석하고 다음 행동을 결정한다.
return {
"ticker": ticker,
"success": True,
"income_statement": stock.income_stmt.to_json(),
}💡 State란? 에이전트와 tool이 데이터를 저장·공유하는 공간이다. Streamlit의 캐시나 OpenAI SDK의 Session이 하던 역할을 대체한다. state에 값을 저장하는 방법은 ① tool 안에서 직접 저장과 ②
output_key설정 두 가지다.
state는 agent가 데이터를 넣을 수 있는 공간이다.
에이전트와 내 코드(tool)에서 state에 접근해 데이터를 저장·수정할 수 있다.
streamlit 캐시나 OpenAI SDK Session의 역할을 대체한다.
agent가 state에 무언가를 저장하도록 하는 방법은 2가지가 있다.
아래는 2번 방법이다.
news_analyst = Agent(
name="NewsAnalyst",
model=MODEL,
...
output_key="news_analyst_result",
)이번엔 첫번째 방법인, tool에서 state를 설정하는 방법이다.
💡
ToolContext— tool 함수의 첫 매개변수로tool_context: ToolContext를 선언하면 ADK가 실행 시 자동으로 주입해준다. 이를 통해tool_context.state로 세션 상태에 접근하고tool_context.save_artifact()같은 기능도 쓸 수 있다. 다른 매개변수와 달리 이 인자는 LLM이 채우지 않고 런타임이 주입한다.
일반적인 tool에서 context를 받도록 할 수 있으며, 이 context를 통해 state에 접근할 수 있다. 그렇다면 context와 state의 차이점은 무엇일까?
💡 context vs state — 공식 문서 기준 정리
state(session.state****): 세션에 저장되는 데이터 그 자체다. 딕셔너리처럼 동작하며tool_context.state["key"]형태로 읽고 쓴다. 키에 접두사를 붙여 범위를 지정할 수 있다 — 접두사 없음(현재 세션),user:(같은 사용자의 모든 세션 공유),app:(앱 전체 공유),temp:(현재 호출에서만 쓰는 비영속 값).
context(ToolContext****): tool이 실행되는 순간 ADK가 주입하는 객체이며, state는 그 객체가 제공하는 기능 중 하나일 뿐이다.ToolContext는CallbackContext의 확장판으로, state 접근 외에도 아티팩트 관리(save_artifact·load_artifact·list_artifacts), 메모리 검색(search_memory), 인증 처리(request_credential), 제어 흐름 신호(actions),function_call_id같은 실행 메타데이터까지 함께 제공한다.즉, **state는 "무엇을 저장하는가(데이터)", context는 "그 데이터를 포함해 실행 환경 전반에 접근하는 통로(객체)"**다. 관계로 보면 context ⊃ state인 셈이다.
참고로 tool의 일반 매개변수들은 LLM이 읽고 알맞은 값을 자동으로 채워 넣지만, tool_context 인자만큼은 LLM이 아니라 런타임이 주입한다는 점이 다르다.
from google.adk.tools import ToolContext
# tool_context: 런타임이 주입한다 (LLM이 채우지 않음)
# summary: main Agent(financial advisor)의 요약본 — 에이전트가 자동으로 채워 넣는다
def save_advice_report(tool_context: ToolContext, summary: str):
state = tool_context.state # context를 통해 state에 접근
data_analyst_result = state.get("data_analyst_result")
print(data_analyst_result)
return {
"success": True
}💡 Artifact란? 이름과 버전이 매겨진 바이너리 데이터(이미지·오디오·영상·문서 등)를 다루는 메커니즘이다. 에이전트와 tool이 텍스트가 아닌 데이터를 생성·저장하고 버전별로 다시 읽을 수 있게 해준다. 예) 분석 보고서를 마크다운 파일로 저장.
아티팩트는 이름이 붙고 버전 관리가 되는 바이너리 데이터를 처리하는 데 쓰인다. 쉽게 말하면, 아티팩트는 agent와 tool이 텍스트가 아닌 데이터(오디오·이미지·영상 등)를 다룰 수 있게 해주며, agent가 그 데이터를 어딘가에 저장하고 버전별로 다시 읽을 수 있게 해준다.
예를 들어 보고서 문서 생성 등을 가능하게 해주는 기능이라고 볼 수 있다.
Artifact를 만드는 방법은 다음과 같다.
async def save_advice_report(tool_context: ToolContext, summary: str, ticker: str):
state = tool_context.state
...
# 파일명 지정
filename = f"{ticker}_investment_advice.md"
# Artifacts 생성
# types는 미디어 컨텐츠 타입을 뜻함.
artifact = types.Part(
inline_data=types.Blob(
mime_type="text/markdown",
data=report.encode("utf-8"),
)
)
await tool_context.save_artifact(filename, artifact)⚠️
save_artifact를 사용하려면Runner에 **ArtifactService**가 설정되어 있어야 한다. (adk web은 기본 InMemory 아티팩트 서비스를 자동 구성한다.) 또한 위types.Part(inline_data=types.Blob(...))대신 편의 메서드types.Part.from_bytes(data=..., mime_type=...)로도 동일하게 만들 수 있다.
전체 흐름을 다시 보면, 메인 에이전트는 세명의 분석가(AgentTool)를 호출해 결과를 각자의 output_key로 state에 쌓고, 마지막에 save_advice_report tool이 그 state 값들을 모아 하나의 보고서로 합친 뒤 **Artifact({ticker}_investment_advice.md)**로 저장한다.
# financial_advisor/agent.py — state를 읽어 Artifact로 저장하는 tool
from google.adk.tools import ToolContext
from google.genai import types # google genai: Google AI API와 상호작용하는 클라이언트 라이브러리
async def save_advice_report(tool_context: ToolContext, summary: str, ticker: str):
state = tool_context.state
report = f"""
# Executive Summary and Advice:
{summary}
## Data Analyst Report:
{state.get("data_analyst_result")}
## Financial Analyst Report:
{state.get("financial_analyst_result")}
## News Analyst Report:
{state.get("news_analyst_result")}
"""
state["report"] = report
artifact = types.Part(
inline_data=types.Blob(
mime_type="text/markdown",
data=report.encode("utf-8"),
)
)
await tool_context.save_artifact(f"{ticker}_investment_advice.md", artifact)
return {"success": True}📝 Financial Advisor 프로젝트를 만들며 익힌 ADK 핵심 개념을 한눈에.
| 개념 | 핵심 요약 |
|---|---|
| ADK Web | adk web으로 FastAPI 기반 개발용 UI·API 서버(/docs)를 자동 제공. 상용 배포용은 아님 |
| Tool | 일반 함수를 tools에 등록. docstring으로 LLM이 호출 시점·인자를 판단. 반환은 구조화된 dict 권장 |
| Sub-agent | sub_agents로 등록 시 통제권 이전(transfer). OpenAI SDK의 handoff와 동일 |
| AgentTool | 다른 에이전트를 함수처럼 호출. 통제권은 메인 에이전트가 유지 |
| State | 에이전트·tool이 공유하는 저장소. output_key(자동 저장) 또는 ToolContext.state(수동)로 기록 |
| Artifacts | 이름·버전이 매겨진 바이너리 데이터. tool_context.save_artifact()로 보고서·파일 저장 |
✅ 이 글의 모든 개념과 API(예:
Agent=LlmAgent별칭,sub_agentsvsAgentTool,output_key,ToolContext,save_artifact)는 ADK 공식 문서(adk.dev) 기준으로 검증했습니다.