Creare un agente ReAct da zero con Gemini 2.5 e LangGraph

LangGraph è un framework per la creazione di applicazioni LLM stateful, il che lo rende una buona scelta per la creazione di agenti ReAct (Reasoning and Acting).

Gli agenti ReAct combinano il ragionamento LLM con l'esecuzione delle azioni. Pensano, utilizzano strumenti e agiscono in modo iterativo in base alle osservazioni per raggiungere gli obiettivi degli utenti, adattando dinamicamente il loro approccio. Introdotto in "ReAct: Synergizing Reasoning and Acting in Language Models" (2023), questo pattern cerca di rispecchiare la risoluzione dei problemi flessibile e simile a quella umana rispetto a flussi di lavoro rigidi.

Sebbene LangGraph offra un agente ReAct predefinito (create_react_agent), è particolarmente utile quando hai bisogno di maggiore controllo e personalizzazione per le implementazioni di ReAct.

I modelli LangGraph rappresentano gli agenti come grafici utilizzando tre componenti chiave:

  • State: struttura dati condivisa (in genere TypedDict o Pydantic BaseModel) che rappresenta lo snapshot attuale dell'applicazione.
  • Nodes: codifica la logica dei tuoi agenti. Ricevono lo stato attuale come input, eseguono alcuni calcoli o effetti collaterali e restituiscono uno stato aggiornato, ad esempio chiamate LLM o chiamate di strumenti.
  • Edges: definisci il successivo Node da eseguire in base all'State corrente, consentendo la logica condizionale e le transizioni fisse.

Se non hai ancora una chiave API, puoi ottenerne una senza costi su Google AI Studio.

pip install langgraph langchain-google-genai geopy requests

Imposta la chiave API nella variabile di ambiente GEMINI_API_KEY.

import os

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

Per capire meglio come implementare un agente ReAct utilizzando LangGraph, vediamo un esempio pratico. Creerai un agente semplice il cui scopo è utilizzare uno strumento per trovare il meteo attuale per una località specificata.

Per questo agente meteo, il suo State dovrà mantenere la cronologia della conversazione in corso (come elenco di messaggi) e un contatore del numero di passaggi eseguiti per illustrare ulteriormente la gestione dello stato.

LangGraph fornisce un pratico helper, add_messages, per aggiornare gli elenchi di messaggi nello stato. Funziona come un riduttore, ovvero prende l'elenco attuale e i nuovi messaggi, quindi restituisce un elenco combinato. Gestisce in modo intelligente gli aggiornamenti in base all'ID messaggio e utilizza per impostazione predefinita un comportamento di "aggiunta esclusiva" per i nuovi messaggi unici.

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

Poi definisci lo strumento meteo.

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]

Poi inizializza il modello e associa gli strumenti al modello.

from datetime import datetime
from langchain_google_genai import ChatGoogleGenerativeAI

# Create LLM class
llm = ChatGoogleGenerativeAI(
    model= "gemini-2.5-pro",
    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)

L'ultimo passaggio prima di poter eseguire l'agente è definire i nodi e gli archi. In questo esempio, hai due nodi e un arco. - Nodo call_tool che esegue il metodo dello strumento. LangGraph ha un nodo predefinito per questo scopo chiamato ToolNode. - Nodo call_model che utilizza model_with_tools per chiamare il modello. - should_continue edge che decide se chiamare lo strumento o il modello.

Il numero di nodi e archi non è fisso. Puoi aggiungere tutti i nodi e gli archi che vuoi al grafico. Ad esempio, puoi aggiungere un nodo per aggiungere un output strutturato o un nodo di autoverifica/riflessione per controllare l'output del modello prima di chiamare lo strumento o il modello.

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"

Ora hai tutti i componenti per creare il tuo agente. Mettiamoli insieme.

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

Puoi visualizzare il grafico utilizzando il metodo draw_mermaid_png.

from IPython.display import Image, display

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

png

Ora eseguiamo l'agente.

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

Ora puoi continuare la conversazione e, ad esempio, chiedere le previsioni del tempo in un'altra città o chiedere di confrontarle.

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