סוכן ReAct מאפס באמצעות Gemini 2.5 ו-LangGraph

LangGraph הוא מסגרת ליצירת אפליקציות LLM עם מצב (stateful), ולכן הוא בחירה טובה ליצירת סוכנים של ReAct (Reasoning and Acting).

סוכני ReAct משלבים בין החשיבה של LLM לבין ביצוע הפעולות. הם חושבים באופן איטרטיבי, משתמשים בכלים ומבצעים פעולות על סמך התצפיות שלהם כדי להשיג את יעדי המשתמשים, תוך התאמה דינמית של הגישה שלהם. התבנית הזו הוצגה במאמר ReAct: Synergizing Reasoning and Acting in Language Models (2023), ומטרתה לשקף פתרון בעיות גמיש דמוי-אנושי במקום תהליכי עבודה נוקשים.

LangGraph מציע סוכן ReAct מובנה (create_react_agent), אבל הוא שימושי במיוחד כשצריך יותר שליטה והתאמה אישית בהטמעות של ReAct.

ב-LangGraph, המודלים של הסוכנויות הם גרפים שמבוססים על שלושה רכיבים מרכזיים:

  • State: מבנה נתונים משותף (בדרך כלל TypedDict או Pydantic BaseModel) שמייצג את קובץ snapshot הנוכחי של האפליקציה.
  • Nodes: קידוד הלוגיקה של הסוכנים. הם מקבלים את המצב הנוכחי כקלט, מבצעים חישוב או השפעה משנית כלשהי ומחזירים מצב מעודכן, כמו קריאות ל-LLM או קריאות לכלים.
  • Edges: הגדרת ה-Node הבא שיתבצע על סמך ה-State הנוכחי, עם אפשרות לשימוש בלוגיקה מותנית ובמעבר קבוע.

אם עדיין אין לכם מפתח API, תוכלו לקבל מפתח בחינם ב-Google AI Studio.

pip install langgraph langchain-google-genai geopy requests

מגדירים את מפתח ה-API במשתנה הסביבה GEMINI_API_KEY.

import os

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

כדי להבין טוב יותר איך מטמיעים סוכן ReAct באמצעות LangGraph, נבחן דוגמה מעשית. תלמדו ליצור סוכן פשוט שמטרתו להשתמש בכלי כדי למצוא את מזג האוויר הנוכחי במיקום מסוים.

כדי להמחיש את ניהול המצבים, ב-State של סוכן מזג האוויר הזה צריך לשמור את היסטוריית השיחות המתמשכת (כרשימה של הודעות) וספירה של מספר השלבים שבוצעו.

ב-LangGraph יש פונקציית עזר נוחה, add_messages, לעדכון רשימות ההודעות במדינה. הוא פועל כמקטין, כלומר הוא לוקח את הרשימה הנוכחית ואת ההודעות החדשות, ואז מחזיר רשימה משולבת. המערכת מטפלת בצורה חכמה בעדכונים לפי מזהה ההודעה, והיא מוגדרת כברירת מחדל ל'הוספה בלבד' להודעות חדשות וייחודיות.

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",
    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)

השלב האחרון לפני שאפשר להריץ את הסוכן הוא להגדיר את הצמתים והקצוות. בדוגמה הזו יש שני צמתים וגבול אחד. - צומת call_tool שמפעיל את שיטת הכלי. ב-LangGraph יש צומת מובנה מראש בשם ToolNode. - צומת call_model שמשתמש ב-model_with_tools כדי לקרוא למודל. - 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()