Gemini 2.5 및 LangGraph로 ReAct 에이전트 처음부터 빌드

LangGraph는 상태ful LLM 애플리케이션을 빌드하기 위한 프레임워크이므로 ReAct (추론 및 작동) 에이전트를 구성하는 데 적합합니다.

ReAct 에이전트는 LLM 추론과 작업 실행을 결합합니다. 반복적으로 생각하고, 도구를 사용하고, 관찰을 바탕으로 행동하여 사용자 목표를 달성하고 접근 방식을 동적으로 조정합니다. 'ReAct: 언어 모델에서 추론과 작동의 시너지' (2023)에서 도입된 이 패턴은 경직된 워크플로를 통해 인간과 같이 유연한 문제 해결을 반영하려고 합니다.

LangGraph는 사전 빌드된 ReAct 에이전트 (create_react_agent)를 제공하지만 ReAct 구현에 대한 더 많은 제어 및 맞춤설정이 필요한 경우에 적합합니다.

LangGraph는 다음 세 가지 주요 구성요소를 사용하여 에이전트를 그래프로 모델링합니다.

  • State: 애플리케이션의 현재 스냅샷을 나타내는 공유 데이터 구조 (일반적으로 TypedDict 또는 Pydantic BaseModel)입니다.
  • Nodes: 상담사의 로직을 인코딩합니다. 현재 상태를 입력으로 수신하고, 계산 또는 부작용을 실행하고, 업데이트된 상태(예: LLM 호출 또는 도구 호출)를 반환합니다.
  • Edges: 현재 State를 기반으로 실행할 다음 Node를 정의하여 조건부 로직과 고정 전환을 허용합니다.

아직 API 키가 없는 경우 Google AI Studio에서 무료로 가져올 수 있습니다.

pip install langgraph langchain-google-genai geopy requests

환경 변수 GEMINI_API_KEY에 API 키를 설정합니다.

import os

# Read your API key from the environment variable or set it manually
api_key = os.getenv("GEMINI_API_KEY")

LangGraph를 사용하여 ReAct 에이전트를 구현하는 방법을 더 잘 이해하려면 실용적인 예를 살펴보겠습니다. 도구를 사용하여 지정된 위치의 현재 날씨를 찾는 것을 목표로 하는 간단한 에이전트를 만들어 보겠습니다.

이 날씨 상담사의 경우 State는 진행 중인 대화 기록 (메시지 목록으로)과 상태 관리를 더 잘 설명하기 위해 수행된 단계 수를 나타내는 카운터를 유지해야 합니다.

LangGraph는 상태의 메시지 목록을 업데이트하는 편리한 도우미 add_messages를 제공합니다. 리듀서로 작동합니다. 즉, 현재 목록과 새 메시지를 가져와 결합된 목록을 반환합니다. 메시지 ID별로 업데이트를 스마트하게 처리하며 새 고유 메시지의 경우 기본적으로 '추가 전용' 동작으로 설정됩니다.

from typing import Annotated,Sequence, TypedDict

from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages # helper function to add messages to the state


class AgentState(TypedDict):
    """The state of the agent."""
    messages: Annotated[Sequence[BaseMessage], add_messages]
    number_of_steps: int

다음으로 날씨 도구를 정의합니다.

from langchain_core.tools import tool
from geopy.geocoders import Nominatim
from pydantic import BaseModel, Field
import requests

geolocator = Nominatim(user_agent="weather-app")

class SearchInput(BaseModel):
    location:str = Field(description="The city and state, e.g., San Francisco")
    date:str = Field(description="the forecasting date for when to get the weather format (yyyy-mm-dd)")

@tool("get_weather_forecast", args_schema=SearchInput, return_direct=True)
def get_weather_forecast(location: str, date: str):
    """Retrieves the weather using Open-Meteo API for a given location (city) and a date (yyyy-mm-dd). Returns a list dictionary with the time and temperature for each hour."""
    location = geolocator.geocode(location)
    if location:
        try:
            response = requests.get(f"https://api.open-meteo.com/v1/forecast?latitude={location.latitude}&longitude={location.longitude}&hourly=temperature_2m&start_date={date}&end_date={date}")
            data = response.json()
            return {time: temp for time, temp in zip(data["hourly"]["time"], data["hourly"]["temperature_2m"])}
        except Exception as e:
            return {"error": str(e)}
    else:
        return {"error": "Location not found"}

tools = [get_weather_forecast]

그런 다음 모델을 초기화하고 도구를 모델에 바인딩합니다.

from datetime import datetime
from langchain_google_genai import ChatGoogleGenerativeAI

# Create LLM class
llm = ChatGoogleGenerativeAI(
    model= "gemini-2.5-pro-preview-05-06",
    temperature=1.0,
    max_retries=2,
    google_api_key=api_key,
)

# Bind tools to the model
model = llm.bind_tools([get_weather_forecast])

# Test the model with tools
res=model.invoke(f"What is the weather in Berlin on {datetime.today()}?")

print(res)

에이전트를 실행하기 전에 마지막으로 노드와 에지를 정의해야 합니다. 이 예시에는 노드 2개와 에지 1개가 있습니다. - 도구 메서드를 실행하는 call_tool 노드 LangGraph에는 이를 위한 사전 빌드된 노드인 ToolNode가 있습니다. - model_with_tools를 사용하여 모델을 호출하는 call_model 노드 - 도구 또는 모델을 호출할지 결정하는 should_continue 에지

노드 및 에지의 수는 고정되지 않습니다. 그래프에 원하는 만큼 노드와 에지를 추가할 수 있습니다. 예를 들어 도구나 모델을 호출하기 전에 모델 출력을 확인하는 구조화된 출력 추가 노드 또는 자체 확인/반사 노드를 추가할 수 있습니다.

from langchain_core.messages import ToolMessage
from langchain_core.runnables import RunnableConfig

tools_by_name = {tool.name: tool for tool in tools}

# Define our tool node
def call_tool(state: AgentState):
    outputs = []
    # Iterate over the tool calls in the last message
    for tool_call in state["messages"][-1].tool_calls:
        # Get the tool by name
        tool_result = tools_by_name[tool_call["name"]].invoke(tool_call["args"])
        outputs.append(
            ToolMessage(
                content=tool_result,
                name=tool_call["name"],
                tool_call_id=tool_call["id"],
            )
        )
    return {"messages": outputs}

def call_model(
    state: AgentState,
    config: RunnableConfig,
):
    # Invoke the model with the system prompt and the messages
    response = model.invoke(state["messages"], config)
    # We return a list, because this will get added to the existing messages state using the add_messages reducer
    return {"messages": [response]}


# Define the conditional edge that determines whether to continue or not
def should_continue(state: AgentState):
    messages = state["messages"]
    # If the last message is not a tool call, then we finish
    if not messages[-1].tool_calls:
        return "end"
    # default to continue
    return "continue"

이제 에이전트를 빌드하는 데 필요한 모든 구성요소가 준비되었습니다. 함께 살펴보겠습니다.

from langgraph.graph import StateGraph, END

# Define a new graph with our state
workflow = StateGraph(AgentState)

# 1. Add our nodes 
workflow.add_node("llm", call_model)
workflow.add_node("tools",  call_tool)
# 2. Set the entrypoint as `agent`, this is the first node called
workflow.set_entry_point("llm")
# 3. Add a conditional edge after the `llm` node is called.
workflow.add_conditional_edges(
    # Edge is used after the `llm` node is called.
    "llm",
    # The function that will determine which node is called next.
    should_continue,
    # Mapping for where to go next, keys are strings from the function return, and the values are other nodes.
    # END is a special node marking that the graph is finish.
    {
        # If `tools`, then we call the tool node.
        "continue": "tools",
        # Otherwise we finish.
        "end": END,
    },
)
# 4. Add a normal edge after `tools` is called, `llm` node is called next.
workflow.add_edge("tools", "llm")

# Now we can compile and visualize our graph
graph = workflow.compile()

draw_mermaid_png 메서드를 사용하여 그래프를 시각화할 수 있습니다.

from IPython.display import Image, display

display(Image(graph.get_graph().draw_mermaid_png()))

png

이제 에이전트를 실행해 보겠습니다.

from datetime import datetime
# Create our initial message dictionary
inputs = {"messages": [("user", f"What is the weather in Berlin on {datetime.today()}?")]}

# call our graph with streaming to see the steps
for state in graph.stream(inputs, stream_mode="values"):
    last_message = state["messages"][-1]
    last_message.pretty_print()

이제 대화를 계속하여 다른 도시의 날씨를 묻거나 비교해 보세요.

state["messages"].append(("user", "Would it be in Munich warmer?"))

for state in graph.stream(state, stream_mode="values"):
    last_message = state["messages"][-1]
    last_message.pretty_print()