Agentic AI/Google ADK

11. G-ADK Tool 활용[8]- PlanReActPlanner(사고 과정을 설계하는 명시적 플래너)

아톨 2026. 10. 11. 17:22

Google ADK(Agent Development Kit)와 Gemini 모델을 활용하여 Open-Meteo 지오코딩 API와 OpenWeatherMap API를 연동하고, AI 에이전트(Agent)가 도구(Tool)로 삼아 실시간 날씨를 조회할 수 있도록 구현한 파이썬 스크립트입니다.

I. 코드 요약 및 핵심 의미

  • 제목: Google ADK와 외부 날씨 API(Open-Meteo & OpenWeatherMap)를 연동한 LLM 에이전트 구현
  • 핵심 의미:
    1. 사용자의 자연어 질문(예: "서울 날씨 어때?")을 이해하고, 에이전트가 스스로 필요한 도구(get_real_current_weather)를 선택해 실행합니다.
    2. 도시 이름을 위/경도 좌표로 변환하는 지오코딩 과정(Open-Meteo)과 실제 날씨 정보를 가져오는 과정(OpenWeatherMap)을 파이프라인 형태로 연결했습니다.
    3. Google ADK의 Agent, BuiltInPlanner, PlanReActPlanner 등을 활용하여 인공지능이 논리적인 계획을 세워 도구를 호출할 수 있는 기반을 제공합니다.

II. 코드 라인별 상세 해설

1. 라이브러리 임포트 및 환경 설정

import os
import requests
from typing import Dict, Any
from dotenv import load_dotenv
from google.adk import Agent

from google.genai import types
from google.adk.planners import BuiltInPlanner

from google.adk.planners import PlanReActPlanner

# 1. 환경 설정
load_dotenv(override=True)

# --- API 키 및 설정 ---
OPEN_WEATHER_API_KEY = os.getenv("OPEN_WEATHER_API_KEY", "")
  • os, requests, typing: 시스템 환경 변수를 읽어오고, 외부 HTTP API를 호출하며, 타입 힌트를 지정하기 위한 기본 라이브러리입니다.
  • load_dotenv(override=True): .env 파일에 정의된 비밀 키(예: OPEN_WEATHER_API_KEY, GOOGLE_MODEL)를 시스템 환경 변수로 불러옵니다. override=True는 기존에 설정된 값이 있더라도 .env 파일의 값으로 덮어쓰겠다는 의미입니다.
  • OPEN_WEATHER_API_KEY: OpenWeatherMap 서비스를 이용하기 위한 인증 키를 환경 변수로부터 안전하게 가져옵니다.

2. 도시 이름으로 좌표를 찾는 함수 (get_coordinates)

def get_coordinates(location:str):
    """도시 이름을 받아 Open-Meteo Geocoding API에서 위도(latitude)와 경도(longitude)를 조회한다."""
    url = "https://geocoding-api.open-meteo.com/v1/search"
    params = {
        "name":location,
        "count":1,
        "language":"en",
        "format":"json",
    }
    response = requests.get(url, params=params, timeout=10)
    response.raise_for_status()
    data = response.json()

    if not data.get("results"):
        raise ValueError(f"도시를 찾을 수 없습니다. {location}")
    result = data["results"][0]
    return {
        "name":result["name"],
        "latitude":result["latitude"],
        "longitude":result["longitude"],
        "country":result.get("country",""),
    }
  • 역할: OpenWeatherMap의 날씨 API는 기본적으로 위도(latitude)와 경도(longitude) 값을 요구합니다. 사용자가 "서울" 같은 도시 이름을 입력했을 때, 이를 위·경도 좌표로 변환해 주는 무료 지오코딩 API(Open-Meteo)를 호출하는 함수입니다.
  • 처리 과정:
    1. 검색할 도시 이름(name)과 결과 개수(count: 1) 등을 파라미터로 담아 요청을 보냅니다.
    2. 응답이 정상(200 OK)인지 확인(response.raise_for_status())합니다.
    3. 결과 데이터에서 첫 번째 장소의 위도, 경도, 국가명 등을 추출하여 딕셔너리 형태로 반환합니다.

3. 실제 날씨를 조회하는 핵심 도구 함수 (get_real_current_weather)

def get_real_current_weather(city_name:str)->Dict[str, Any]:
    """
    Open Weather API를 사용해 '현재' 날씨 정보를 JSON으로 조회합니다.
    Args:
        city_name (str): 도시 이름.
    Returns:
        Dict[str, Any]: API에서 반환된 원본 JSON 날씨 정보
    """
    city_key = get_coordinates(city_name)
    latitude=city_key["latitude"]
    longitude=city_key["longitude"]

    params = {        
        "lat":latitude,
        "lon":longitude,        
        "appid":OPEN_WEATHER_API_KEY,
    }
    
    try:
        endpoint="https://api.openweathermap.org/data/2.5/weather"
        response = requests.get(endpoint, params=params)
        response.raise_for_status() # 오류 발생 시 예외 처리
        return response.json()
    except requests.RequestException as e:
        # LLM이 오류를 이해할 수 있도록 명확한 에러 메시지 반환
        raise ValueError(f"API 호출 실패: {e}")
  • 역할: LLM 에이전트가 직접 호출할 수 있도록 정의된 툴(Tool) 함수입니다.
  • 처리 과정:
    1. 앞서 만든 get_coordinates(city_name) 함수를 먼저 호출해 입력받은 도시의 위도와 경도를 얻습니다.
    2. OpenWeatherMap 엔드포인트([https://api.openweathermap.org/data/2.5/weather](https://api.openweathermap.org/data/2.5/weather))에 위도, 경도, API 키를 파라미터로 전달해 현재 날씨 데이터를 요청합니다.
    3. 반환된 JSON 원본 데이터를 파이썬 딕셔너리 형태로 반환합니다. 예외가 발생할 경우, 에이전트가 상황을 파악할 수 있도록 ValueError로 감싸서 전달합니다.

4. 모델 사고 설정 (ThinkingConfig)

thinking_conf = types.ThinkingConfig(include_thoughts=True, thinking_budget=1024)
  • 역할: Gemini 모델의 추론(Thinking) 능력을 활성화하고 제어하기 위한 설정 객체입니다.
  • 세부 옵션: include_thoughts=True를 통해 모델이 답을 도출하기까지의 내부 사고 과정을 함께 확인할 수 있도록 하며, thinking_budget을 통해 사고 과정에 사용할 토큰 예산을 지정합니다. (다만 아래 에이전트 선언부에서는 현재 주석 처리되어 있습니다.)

5. 에이전트 정의 및 설정 (real_weather_agent)

real_weather_agent = Agent(
    model=os.getenv("GOOGLE_MODEL", "gemini-3.5-flash"),
    name ="real_weather_agent",
    description="실제 OpenWeather API로 날씨를 알려주는 에이전트입니다.",
    # planner=BuiltInPlanner(thinking_config=thinking_conf),
    planner=PlanReActPlanner(),
    instruction="""
    사용자가 날씨를 물어보면, 반드시 Tool을 이용해 현재 날씨를 알려주세요.
    """,
    tools=[get_real_current_weather]
)
root_agent = real_weather_agent
  • model: 사용할 LLM 모델명을 환경 변수에서 가져오며, 지정되지 않은 경우 기본값으로 gemini-3.5-flash를 사용합니다.
  • planner: 에이전트가 문제를 해결할 때 사용할 계획 수립 전략을 지정합니다. 여기서는 PlanReActPlanner(Reasoning and Acting 패턴)를 사용하여 모델이 생각하고 행동(도구 호출)하는 단계를 거치도록 설정되어 있습니다.
  • instruction: 에이전트가 수행해야 할 프롬프트 지침입니다. 사용자가 날씨를 물어보면 반드시 정의된 Tool을 사용하도록 유도합니다.
  • tools: 에이전트가 스스로 판단하여 호출할 수 있는 파이썬 함수 목록을 전달합니다. 여기서는 앞서 작성한 [get_real_current_weather] 함수가 등록되어 있습니다.
  • root_agent = real_weather_agent: ADK 프레임워크가 실행될 때 진입점(Root)이 되는 메인 에이전트로 지정합니다.
 
    import os
    import requests
    from typing import Dict, Any
    from dotenv import load_dotenv
    from google.adk import Agent

    from google.genai import types
    from google.adk.planners import BuiltInPlanner

    from google.adk.planners import PlanReActPlanner

    # 1. 환경 설정
    load_dotenv(override=True)



    # --- API 키 및 설정 ---
    # OpenWeatherMap 무료 API 키를 발급
    OPEN_WEATHER_API_KEY = os.getenv("OPEN_WEATHER_API_KEY", "")

    # 2. 좌표만들기
    def get_coordinates(location:str):
        """도시 이름을 받아 Open-Meteo Geocoding API에서 위도(latitude)와 경도(longitude)를 조회한다."""
        params = {
            "name":location,
            "count":1,
            "language":"en",
            "format":"json",
        }
        response = requests.get(url, params=params, timeout=10)
        # response:  <Response [200]>
        response.raise_for_status()
        data = response.json()

        if not data.get("results"):
            raise ValueError(f"도시를 찾을 수 없습니다. {location}")
        result = data["results"][0]
        return {
            "name":result["name"],
            "latitude":result["latitude"],
            "longitude":result["longitude"],
            "country":result.get("country",""),
        }
       
    # 3. [핵심] 실제 API를 호출하는 파이썬 함수를 'Tool'로 정의
    def get_real_current_weather(city_name:str)->Dict[str, Any]:
        """
        Open Weather API를 사용해 '현재' 날씨 정보를 JSON으로 조회합니다.
        Args:
            city_name (str): 도시 이름.
        Returns:
            Dict[str, Any]: API에서 반환된 원본 JSON 날씨 정보
        """
        city_key = get_coordinates(city_name)
        latitude=city_key["latitude"]
        longitude=city_key["longitude"]

        params = {        
            "lat":latitude,
            "lon":longitude,        
            "appid":OPEN_WEATHER_API_KEY,
        }
       
        try:
            # "https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API key}"
            response = requests.get(endpoint, params=params)
            response.raise_for_status() # 오류 발생 시 예외 처리
            return response.json()
        except requests.RequestException as e:
            # LLM이 오류를 이해할 수 있도록 명확한 에러 메시지 반환
            raise ValueError(f"API 호출 실패: {e}")

    ## 추가1. [설정] ThinkingConfig 정의
    # 모델의 사고(Thinking) 기능을 활성화하고 제어하기 위한 설정 객체다.
    # include_thoughts=True로 설정하면 모델이 어떻게 답을 도출했는지 사고 과정을 포함할 수 있다.
    thinking_conf = types.ThinkingConfig(include_thoughts=True, thinking_budget=1024)


    # 4. Agent에 'tools'로 등록            
    ## 추가2.[Agent 정의] BuiltInPlanner 장착
        # [중요] BuiltInPlanner는 'Thinking'을 지원하는 모델을 사용해야 효과적이다.
        # Thinking을 지원하는 모델인지 확인이 필요하다.
    real_weather_agent = Agent(
        model=os.getenv("GOOGLE_MODEL", "gemini-3.5-flash"),
        name ="real_weather_agent",
        description="실제 OpenWeather API로 날씨를 알려주는 에이전트입니다.",
        # planner=BuiltInPlanner(thinking_config=thinking_conf),
        planner=PlanReActPlanner(),
        instruction="""
        사용자가 날씨를 물어보면, 반드시 Tool을 이용해 현재 날씨를 알려주세요.
        """,
        tools=[get_real_current_weather]
    )
    root_agent = real_weather_agent

 

[참조]

Google ADK(Agent Development Kit)에서 Planner(플래너)는 에이전트가 복잡한 작업을 처리할 때 단순히 즉흥적으로 답변하지 않고, 어떤 순서로 생각하고 행동할지(Reasoning & Acting) 체계적인 계획을 세우도록 돕는 핵심 모듈입니다. BuiltInPlanner와 PlanReAtPlanner는 이 목표를 달성하기 위해 완전히 다른 접근 방식을 취합니다. 각각의 특징과 적합한 사용 상황을 정리합니다.

1. BuiltInPlanner (모델의 네이티브 사고 활용)

  • 동작 방식: 모델 자체에 내장된 '싱킹(Thinking)' 기능을 그대로 활용합니다. 프롬프트를 별도로 조작하는 프롬프트 엔지니어링 방식이 아니라, API 요청 설정(types.ThinkingConfig)을 통해 모델 내부에 사고 예산(Token Budget)과 사고 과정 포함 여부(include_thoughts=True)를 전달합니다.
  • 특징:
    • ADK가 별도의 텍스트 파싱이나 강제적인 구조화 프롬프트를 덧붙이지 않습니다.
    • 모델 본연의 추론 능력을 사용하므로 속도가 빠르고 자연스럽습니다.
    • 사고 과정(Thoughts)이 로그에 별도로 분리되어 기록되므로 디버깅이 용이합니다.

  🐦‍🔥 언제 쓰나요?

  • 지원 모델 사용 시: Gemini 같이 네이티브 싱킹(Thinking)을 지원하는 최신 모델을 사용할 때 가장 적합합니다.
  • 가볍고 효율적인 설정이 필요할 때: 복잡한 텍스트 파싱 오버헤드 없이 모델 고유의 추론 능력을 최대한 가볍게 끌어내고 싶을 때 사용합니다.

2. PlanReActPlanner (ReAct 프롬프트 구조화 패턴)

  • 동작 방식: 전통적인 ReAct (Reason + Act) 패러다임을 프롬프트와 파서(Prompt-plus-Parser) 조합으로 구현한 방식입니다.
  • 특징:
    • 모델에게 요청을 보내기 전, 에이전트가 내부적으로 특정 태그나 구조(계획 수립, 추론, 행동, 관찰 등)를 따르도록 강제하는 시스템 지침(System Instruction)을 자동으로 앞에 덧붙입니다.
    • 모델이 응답한 텍스트를 파서가 분석하여 단계별 계획과 도구 호출을 제어합니다.
    • 모델 자체에 내장된 싱킹 기능 유무와 상관없이 어떤 모델이든 일관된 형태의 단계별 계획 루프를 강제할 수 있습니다.

 🐦‍🔥 언제 쓰나요?

  • 네이티브 싱킹이 없는 모델을 쓸 때: 모델 자체에 내장된 추론 기능이 없거나 제한적인 모델을 사용하면서도 구조화된 다단계 추론이 필요할 때 유용합니다.
  • 엄격한 단계별 가시성이 필요할 때: 에이전트가 어떤 의사결정 경로를 거쳤는지 텍스트 태그 기반의 명확한 플랜(Plan) 형태로 강제 출력받고 통제하고 싶을 때 적합합니다.

⚖️ 한눈에 비교 요약

구분 BuiltInPlanner PlanReActPlanner
작동 원리 모델 내부의 Native Thinking API 설정 활용 (ThinkingConfig) 프롬프트 주입 및 응답 텍스트 파싱 (Prompt + Parser)
모델 의존성 네이티브 싱킹을 지원하는 모델 필요 (예: 최신 Gemini) 모델 종류와 무관하게 적용 가능
추천 상황 고성능 모델을 사용하며 가볍고 자연스러운 추론을 원할 때 모델 제약 없이 엄격한 구조의 ReAct 프로세스를 강제하고 싶을 때

위 코드에서 주석 처리되어 있던 thinking_conf와 BuiltInPlanner는 전자의 방식에 해당하며, 현재 활성화되어 있는 PlanReActPlanner는 후자의 방식으로 에이전트를 구동하고 있는 것입니다.

반응형