إنشاء وكيل ReAct من الصفر باستخدام Gemini 2.5 وLangGraph

LangGraph هو إطار عمل لإنشاء تطبيقات معالجة لغوية كبيرة ذات حالة، ما يجعله خيارًا جيدًا لإنشاء وكلاء ReAct (الاستدلال والتنفيذ).

تجمع عملاء ReAct بين الاستدلال بالاستناد إلى النماذج اللغوية الكبيرة وتنفيذ الإجراءات. ويستخدمون أدوات ويطبّقون الملاحظات لتحقيق أهداف المستخدمين، مع تعديل نهجهم بشكل ديناميكي. تم تقديم هذا النمط في "ReAct: Synergizing Reasoning and Acting in Language Models" (2023)، ويحاول أن يحاكي طريقة حلّ المشاكل المرنة التي يتّبعها البشر بدلاً من سير العمل الصارمة.

على الرغم من أنّ LangGraph يوفّر وكيل ReAct مُعدّ مسبقًا (create_react_agent)، إلا أنّه يُبرز ميزاته عندما تحتاج إلى مزيد من التحكّم والتخصيص لعمليات تنفيذ ReAct.

يصوّر LangGraph موظّفي الدعم على شكل رسوم بيانية باستخدام ثلاثة مكوّنات رئيسية:

  • State: بنية البيانات المشتركة (عادةً TypedDict أو Pydantic BaseModel) التي تمثّل اللقطة الحالية للتطبيق
  • Nodes: ترميز منطق موظّفي الدعم وتتلقّى هذه الوظائف الحالة الحالية كإدخال، وتُجري بعض العمليات الحسابية أو التأثيرات الجانبية، وتُرجع حالة معدَّلة، مثل طلبات LLM أو طلبات الأدوات.
  • Edges: حدِّد Node التالي الذي سيتم تنفيذه استنادًا إلى State الحالي، مع السماح بالمنطق الشَرطي والعمليات الانتقالية الثابتة.

إذا لم يكن لديك مفتاح واجهة برمجة التطبيقات حتى الآن، يمكنك الحصول على مفتاح مجانًا في Google AI Studio.

pip install langgraph langchain-google-genai geopy requests

اضبط مفتاح واجهة برمجة التطبيقات في متغيّر البيئة 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-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)

الخطوة الأخيرة قبل أن تتمكّن من تشغيل موظّف الدّعم هي تحديد العقد والحواف. في هذا المثال، لديك عقدتَان وجانب واحد. - عقدة 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()