AI Sparkup

복잡한 AI 세상을 읽는 힘

LangGraph와 Gemini 2.5 Pro로 구현하는 ReAct 에이전트

인공지능 애플리케이션은 단순한 챗봇에서 복잡한 추론과 계획, 그리고 현실 세계와 상호작용할 수 있는 (반)자율 시스템으로 진화하고 있습니다. 이러한 시스템을 우리는 ‘에이전트’라고 부릅니다.

AI 에이전트는 애플리케이션의 제어 흐름을 결정하기 위해 LLM(대규모 언어 모델)을 사용하는 시스템입니다.

에이전트는 단순한 이론적 개념이 아닌 실제로 다양한 분야에서 배포되고 있으며, 점점 더 복잡하고 장기적인 작업을 수행하고 있습니다. 이 블로그 포스트에서는 Google의 Gemini 2.5 Pro 또는 Gemini 2.0 Flash와 LangGraph를 활용하여 처음부터 ReAct 에이전트를 만드는 방법을 알아보겠습니다.

ReAct 에이전트란?

ReACT(Reasoning and Acting) 에이전트는 LLM의 추론 능력과 행동 실행을 결합하여, 문제를 단계적으로 생각하고, 도구를 사용하며, 관찰한 결과를 바탕으로 사용자의 목표를 달성할 수 있는 AI 시스템입니다.

ReAct 패턴은 2023년 “ReAct: Synergizing Reasoning and Acting in Language Models” 논문에서 처음 소개되었습니다. 이는 미리 정의된 워크플로우를 구현하는 대신, 인간이 복잡한 작업을 계획하고 해결하는 방식에서 영감을 받았습니다. ReAct 에이전트는 새로운 정보나 이전 단계의 결과에 따라 자신의 행동을 동적으로 조정하기 위해 LLM의 추론 능력에 의존합니다.

ReACT 에이전트는 복잡한 작업을 관리 가능한 추론 단계로 분해하고 외부 도구를 활용하는 능력 덕분에 큰 주목을 받았습니다.

ReAct 에이전트의 원리

ReAct 에이전트는 다음과 같은 단계로 작동합니다:

  1. 사용자의 쿼리를 입력으로 받음
  2. 쿼리에 대해 추론하고 행동을 결정
  3. 사용 가능한 도구를 사용하여 선택한 행동 실행
  4. 행동의 결과 관찰
  5. 최종 답변을 제공할 수 있을 때까지 2-4단계 반복

초기 ReAct 에이전트

첫 세대의 ReAct 에이전트는 “생각, 행동, 관찰” 단계의 체인을 생성하기 위한 간단하지만 효과적인 프롬프팅 기법을 사용했습니다:

  • “생각(Thought)” 구성 요소는 다음 행동을 계획하거나 최종 답변을 알고 있다고 결정합니다
  • “행동(Action)”은 검색 엔진이나 계산기와 같은 외부 리소스와 상호 작용합니다
  • “관찰(Observation)”은 행동의 결과를 추론 과정에 통합합니다

다음은 초기 ReAct 에이전트의 흐름을 보여주는 의사 코드 예시입니다:

User: Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?
 
Thought: I need to find out who Olivia Wilde's boyfriend is and then calculate his age raised to the 0.23 power.
Action: [search("Olivia Wilde boyfriend")]
Observation: Olivia Wilde started dating Harry Styles after ending her years-long engagement to Jason Sudeikis — see their relationship timeline.
 
Thought: I need to find out Harry Styles' age.
Action: [search("Harry Styles age")]
Observation: 29 years
 
Thought: I need to calculate 29 raised to the 0.23 power.
Action: [calculator(29^0.23)]
Observation: Answer: 2.169459462491557
 
Thought: I now know the final answer.
Final Answer: Harry Styles, Olivia Wilde's boyfriend, is 29 years old and his age raised to the 0.23 power is 2.169459462491557.

현대의 ReAct 에이전트

ReAct 에이전트가 소개된 이후 LLM의 기능은 발전해왔습니다. 가장 중요한 개선점 중 하나는 함수 호출(function calling)입니다. 함수 호출을 통해 LLM을 구조화된 방식으로 외부 도구에 연결할 수 있으며, 이는 원시 텍스트를 파싱하는 것보다 더 신뢰할 수 있고 오류와 환각을 줄일 수 있습니다.

다음은 함수 호출을 사용한 ReAct 에이전트의 흐름을 보여주는 의사 코드 예시입니다:

User: Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?
 
Assistant: FunctionCall(name="search", args={"query": "Olivia Wilde boyfriend"})
User: FunctionResponse(result="Olivia Wilde started dating Harry Styles after ending her years-long engagement to Jason Sudeikis — see their relationship timeline.")
 
Assistant: FunctionCall(name="search", args={"query": "Harry Styles age"})
User: FunctionResponse(result="29 years")
 
Assistant: FunctionCall(name="calculator", args={"expression": "29^0.23"})
User: FunctionResponse(result="2.169459462491557")
 
Assistant: Harry Styles, Olivia Wilde's boyfriend, is 29 years old. His age raised to the 0.23 power is 2.169459462491557.

전통적인 ReAct 에이전트 vs 현대의 ReAct 에이전트(함수 호출)

측면전통적인 ReAct 에이전트현대의 ReAct 에이전트(함수 호출)
행동텍스트 기반 설명, 시스템에 의해 파싱됨구조화된 형식의 직접적인 함수 호출
효율성파싱 오류로 인해 낮음파싱 오버헤드가 감소하여 높음
신뢰성오류와 환각에 더 취약함더 신뢰할 수 있고 정확한 도구 실행
LLM 요구사항모든 LLM에서 작동함수 호출을 지원하는 LLM이 필요
구현주로 신중한 프롬프트 엔지니어링을 통해종종 LangGraph와 같은 SDK와 프레임워크에 의해 용이해짐

LangGraph로 처음부터 ReAct 에이전트 만들기

이제 ReAct 에이전트가 어떻게 작동하는지에 대한 기본적인 이해를 갖게 되었습니다. 이제 처음부터 우리만의 에이전트를 만들어 보겠습니다. LangGraph와 Gemini 2.5 Pro를 사용할 것입니다. LangGraph는 제어 가능한 에이전트를 구축하기 위한 프레임워크입니다. LangGraph에는 이미 미리 구축된 ReAct 에이전트 create_react_agent가 있지만, 때로는 더 많은 제어와 커스터마이징이 필요할 수 있습니다. 또한, 기본 개념을 이해하고 처음부터 ReAct 에이전트를 구축하는 방법을 아는 것이 좋습니다.

LangGraph는 에이전트를 그래프로 모델링합니다. 세 가지 핵심 구성 요소를 사용하여 에이전트의 동작을 정의합니다:

  • State: 애플리케이션의 현재 스냅샷을 나타내는 공유 데이터 구조입니다. 일반적으로 모든 노드에서 공유되는 TypedDict 또는 Pydantic BaseModel입니다.
  • Nodes: 에이전트의 로직을 인코딩합니다. 현재 State를 입력으로 받아 계산이나 부작용을 수행하고 업데이트된 State를 반환합니다(예: LLM 호출, 도구 호출 등).
  • Edges: 현재 State를 기반으로 다음에 실행할 Node를 결정합니다. 조건부 분기나 고정 전환일 수 있습니다.

먼저 필요한 패키지를 설치하고 API 키를 설정합니다. API 키가 없다면 Google AI Studio에서 무료로 얻을 수 있습니다.

%pip install langgraph langchain-google-genai geopy requests
import os 
 
# 환경 변수에서 API 키를 읽거나 수동으로 설정합니다
api_key = os.getenv("GEMINI_API_KEY","xxx")

가장 기본적인 ReAct 에이전트를 만들어 보겠습니다. 이 에이전트는 주어진 위치에 대한 날씨를 검색하는 모의 도구 하나를 사용합니다. 이를 위해 그래프 상태에 메시지 목록을 대화 기록으로 저장해야 합니다. add_messages헬퍼 함수를 사용하여 상태에 메시지를 추가할 것입니다. add_messages 함수는 두 메시지 목록을 병합하고, ID로 기존 메시지를 업데이트하며, 새 메시지가 기존 메시지와 동일한 ID를 가지지 않는 한 상태가 “추가 전용”임을 보장하는 리듀서입니다. 설명을 위해 상태에 단계 수도 저장합니다.

참고: 상태에 메시지 목록을 저장하는 것이 매우 일반적이기 때문에, 메시지를 쉽게 사용할 수 있도록 MessagesState라는 미리 구축된 상태가 있습니다.

from typing import Annotated,Sequence, TypedDict
 
from langchain_core.messages import BaseMessage 
from langgraph.graph.message import add_messages # 상태에 메시지를 추가하기 위한 헬퍼 함수
 
 
class AgentState(TypedDict):
    """에이전트의 상태."""
    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):
    """Open-Meteo API를 사용하여 주어진 위치(도시)와 날짜(yyyy-mm-dd)에 대한 날씨를 검색합니다. 각 시간에 대한 시간과 온도를 포함하는 딕셔너리 목록을 반환합니다."""
    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 langchain_google_genai import ChatGoogleGenerativeAI
  
# LLM 클래스 생성 
llm = ChatGoogleGenerativeAI(
    model= "gemini-2.5-pro-exp-03-25", # "gemini-2.0-flash"로 대체 가능
    temperature=1.0,
    max_tokens=None,
    timeout=None,
    max_retries=2,
    google_api_key=api_key,
)
 
# 도구를 모델에 바인딩
model = llm.bind_tools([get_weather_forecast])
 
# 도구가 있는 모델 테스트
model.invoke("What is the weather in Berlin on 12th of March 2025?")

에이전트를 실행하기 전 마지막 단계는 노드와 에지를 정의하는 것입니다. 우리 예제에서는 두 개의 노드와 하나의 에지가 있습니다.

  • 도구 메서드를 실행하는 call_tool 노드. LangGraph에는 이를 위한 미리 구축된 노드인 ToolNode가 있습니다.
  • model_with_tools를 사용하여 모델을 호출하는 call_model 노드.
  • 도구나 모델을 호출할지 결정하는 should_continue 에지.

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

import json
from langchain_core.messages import ToolMessage, SystemMessage
from langchain_core.runnables import RunnableConfig
 
tools_by_name = {tool.name: tool for tool in tools}
 
# 이것은 'prompt' 매개변수로 create_react_agent를 커스터마이징하는 것과 유사하지만 더 유연합니다
# system_prompt = SystemMessage(
#     "You are a helpful assistant that use tools to access and retrieve information from a weather API. Today is 2025-03-04. Help the user with their questions. Use the history to answer the question."
# )
 
# 도구 노드 정의
def call_tool(state: AgentState):
    outputs = []
    # 마지막 메시지의 도구 호출을 반복합니다
    for tool_call in state["messages"][-1].tool_calls:
        # 이름으로 도구를 가져옵니다
        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,
):
    # 시스템 프롬프트와 메시지로 모델을 호출합니다
    response = model.invoke(state["messages"], config)
    # 리스트를 반환합니다. 이는 add_messages 리듀서를 사용하여 기존 메시지 상태에 추가됩니다
    return {"messages": [response]}
 
 
# 계속할지 여부를 결정하는 조건부 에지를 정의합니다
def should_continue(state: AgentState):
    messages = state["messages"]
    # 마지막 메시지가 도구 호출이 아니면 종료합니다
    if not messages[-1].tool_calls:
        return "end"
    # 기본적으로 계속합니다
    return "continue"

이제 에이전트를 구축하기 위한 모든 구성 요소가 준비되었습니다. 이들을 모두 합쳐봅시다.

from langgraph.graph import StateGraph, END
 
# 상태를 가진 새 그래프 정의
workflow = StateGraph(AgentState)
 
# 1. 노드 추가 
workflow.add_node("llm", call_model)
workflow.add_node("tools",  call_tool)
# 2. `agent`를 엔트리포인트로 설정, 이는 처음 호출되는 노드입니다
workflow.set_entry_point("llm")
# 3. `llm` 노드가 호출된 후 조건부 에지를 추가합니다.
workflow.add_conditional_edges(
    # 에지는 `llm` 노드가 호출된 후 사용됩니다.
    "llm",
    # 다음에 호출될 노드를 결정하는 함수입니다.
    should_continue,
    # 다음으로 이동할 위치에 대한 매핑, 키는 함수의 반환 문자열이고 값은 다른 노드입니다.
    # END는 그래프가 완료됨을 표시하는 특수 노드입니다.
    {
        # `tools`인 경우, 도구 노드를 호출합니다.
        "continue": "tools",
        # 그렇지 않으면 완료합니다.
        "end": END,
    },
)
# 4. `tools`가 호출된 후 일반 에지를 추가합니다. 다음으로 `llm` 노드가 호출됩니다.
workflow.add_edge("tools", "llm")
 
# 이제 그래프를 컴파일하고 시각화할 수 있습니다
graph = workflow.compile()

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

from IPython.display import Image, display
 
display(Image(graph.get_graph().draw_mermaid_png()))

이제 에이전트를 실행해 봅시다.

# 초기 메시지 딕셔너리 생성
inputs = {"messages": [("user", "How is the weather in Berlin on 12th of March 2025?")]}
 
# 단계를 보기 위해 스트리밍으로 그래프 호출
 
for state in graph.stream(inputs, stream_mode="values"):
    last_message = state["messages"][-1]
    last_message.pretty_print()

결과:

================================ Human Message =================================

How is the weather in Berlin on 12th of March 2025?
================================== Ai Message ==================================
Tool Calls:
    get_weather_forecast (162a0f9f-352f-4b06-b6be-e071d6eee35b)
    Call ID: 162a0f9f-352f-4b06-b6be-e071d6eee35b
    Args:
    date: 2025-03-12
    location: Berlin
================================= Tool Message =================================
Name: get_weather_forecast

{'2025-03-12T00:00': 4.5, '2025-03-12T01:00': 4.5, '2025-03-12T02:00': 4.8, '2025-03-12T03:00': 5.2, '2025-03-12T04:00': 5.2, '2025-03-12T05:00': 5.1, '2025-03-12T06:00': 5.0, '2025-03-12T07:00': 5.1, '2025-03-12T08:00': 5.6, '2025-03-12T09:00': 6.2, '2025-03-12T10:00': 7.0, '2025-03-12T11:00': 7.3, '2025-03-12T12:00': 7.5, '2025-03-12T13:00': 7.8, '2025-03-12T14:00': 8.2, '2025-03-12T15:00': 8.1, '2025-03-12T16:00': 7.9, '2025-03-12T17:00': 7.6, '2025-03-12T18:00': 7.1, '2025-03-12T19:00': 6.9, '2025-03-12T20:00': 5.5, '2025-03-12T21:00': 6.0, '2025-03-12T22:00': 5.0, '2025-03-12T23:00': 4.7}
================================== Ai Message ==================================

OK. Here is the weather forecast for Berlin on March 12th, 2025:

The temperature will range from 4.5°C in the early morning to a high of 8.2°C in the afternoon. Temperatures will start around 4.5°C at midnight, rise through the morning to reach the peak in the early afternoon (around 2 PM), and then cool down again in the evening, dropping to around 4.7°C by 11 PM.

이제 대화를 계속하고 예를 들어 다른 도시의 날씨를 물어보거나 비교할 수 있습니다.

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()

결과:

================================ Human Message =================================
    
Would it be in Munich warmer?
================================== Ai Message ==================================
Tool Calls:
    get_weather_forecast (1736dcae-c173-4b90-8c07-3751b6fd848d)
    Call ID: 1736dcae-c173-4b90-8c07-3751b6fd848d
    Args:
    date: 2025-03-12
    location: Munich
================================= Tool Message =================================
Name: get_weather_forecast

{'2025-03-12T00:00': 5.3, '2025-03-12T01:00': 5.0, '2025-03-12T02:00': 4.7, '2025-03-12T03:00': 3.8, '2025-03-12T04:00': 3.5, '2025-03-12T05:00': 3.4, '2025-03-12T06:00': 3.5, '2025-03-12T07:00': 4.5, '2025-03-12T08:00': 4.9, '2025-03-12T09:00': 5.7, '2025-03-12T10:00': 6.6, '2025-03-12T11:00': 8.0, '2025-03-12T12:00': 9.2, '2025-03-12T13:00': 10.1, '2025-03-12T14:00': 10.6, '2025-03-12T15:00': 9.9, '2025-03-12T16:00': 9.6, '2025-03-12T17:00': 9.3, '2025-03-12T18:00': 8.9, '2025-03-12T19:00': 8.2, '2025-03-12T20:00': 7.5, '2025-03-12T21:00': 6.9, '2025-03-12T22:00': 6.3, '2025-03-12T23:00': 5.6}
================================== Ai Message ==================================

Yes, it looks like Munich will be warmer than Berlin on March 12th, 2025, especially during the afternoon.

*   **Munich:** The temperature is expected to reach a high of about 10.6°C around 2 PM. The lowest temperature will be around 3.4°C in the early morning.
*   **Berlin:** The high is predicted to be 8.2°C, also around 2 PM, with a low of 4.5°C.

So, while Munich might start a bit colder in the very early morning, its daytime high will be noticeably warmer than Berlin's.

결론

LangGraph와 Gemini 2.5 Pro 또는 Gemini 2.0 Flash를 사용하여 ReAct 에이전트를 구축하면 개발자가 제어 흐름을 커스터마이징할 수 있는 유연성과 복잡한 작업을 실행할 수 있는 신뢰성을 갖춘 실제 상용화 가능한 에이전트를 구축할 수 있습니다.

AI 에이전트 발전의 의미

이러한 기술의 발전은 단순한 기술적 진보 이상의 의미가 있습니다. AI 에이전트는 더 이상 단순히 질문에 대답하는 것이 아닌, 복잡한 문제를 해결하고 다양한 도구를 활용하여 사용자를 위한 작업을 수행할 수 있는 시스템으로 진화하고 있습니다.

ReAct 패턴의 핵심 아이디어는 추론(Reasoning)과 행동(Acting)의 결합입니다. 이는 단순히 주어진 입력에 대해 출력을 생성하는 것이 아니라, 지속적으로 상황을 평가하고, 행동하며, 그 결과를 관찰하고, 다시 평가하는 순환 구조를 통해 LLM이 복잡한 문제 해결 능력을 발휘할 수 있게 합니다.

특히 LangGraph와 같은 도구가 등장하면서 이러한 에이전트의 개발이 더욱 용이해졌습니다. 그래프 구조를 통해 AI 시스템의 흐름을 명확하게 설계하고 제어할 수 있게 되었으며, 이는 더 복잡하고 신뢰성 있는 AI 응용 프로그램의 개발로 이어지고 있습니다.

Gemini와 같은 최신 LLM과 결합된 ReAct 에이전트는 복잡한 정보 검색, 분석, 의사 결정 지원과 같은 다양한 분야에서 활용될 수 있으며, 이는 기술적 혁신을 넘어 우리가 AI와 상호작용하는 방식 자체를 변화시키고 있습니다.

앞으로의 전망

ReAct 에이전트 기술은 지속적으로 발전하고 있으며, 앞으로의 발전 방향은 다음과 같을 것으로 예상됩니다:

  1. 다중 에이전트 시스템: 서로 다른 전문성을 가진 여러 에이전트가 협력하여 더 복잡한 작업을 해결하는 시스템
  2. 장기 기억과 학습: 이전 상호작용에서 학습하고 경험을 축적하는 능력을 갖춘 에이전트
  3. 도메인 특화 에이전트: 특정 분야(의료, 법률, 금융 등)에 특화된 지식과 도구를 갖춘 에이전트
  4. 자율성 향상: 최소한의 인간 개입으로 복잡한 워크플로우를 수행할 수 있는 능력

이러한 발전은 AI가 단순한 도구에서 벗어나 인간의 생산성과 창의성을 증폭시키는 능동적인 협력자로 진화하는 과정을 보여줍니다. LangGraph와 Gemini와 같은 최신 기술을 활용한 ReAct 에이전트는 이러한 미래를 향한 중요한 발걸음이라고 할 수 있습니다.

참고자료:


Awsom GPT에서 더 알아보기

구독을 신청하면 최신 게시물을 이메일로 받아볼 수 있습니다.

Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다