شرحexplainer · 2026-10-07

الأدوات واستدعاؤها: كيف يقرّر النموذج متى ينفّذ دالة

الأداة في لانغ تشين دالة لها مُدخلات ومخرجات محدّدة يعرضها على النموذج فيقرّر بنفسه متى يستدعيها، وهذه الآلية تُسمّى استدعاء الأدوات (tool calling) لا استدعاء الدوال (function calling) الذي هو المصطلح الأقدم من OpenAI. والـ docstring وتلميحات النوع هما العقد الذي يقرأه النموذج حرفياً، لا زينة. وثلاثة أخطاء تكلّف القارئ وقتاً طويلاً: أسماء الأدوات غير snake_case، واستخدام الاسم المحجوز config أو runtime، والاعتقاد بأن return_direct تنهي الحلقة قبل أن تحمل كل أدوات الدفعة هذه العلامة.

نُشر 7 أكتوبر 2026 · قبل ساعتينconfidence 0.931 مصدرrecheck 2027-01-05
حلقة وكيل مجرّدة: النموذج يختار أدواته ويقرّر متى يتوقف، والأدوات ترجع ملاحظات تُعاد إلى النموذج في الدورة التالية.

الأداة في لانغ تشين (LangChain) ليست شيئاً سحرياً: هي دالة لها مُدخلات ومخرجات محدّدة، تُعرض على النموذج فيقرّر بنفسه إن كان يحتاجها الآن وبأي وسائط. واسم هذه الآلية استدعاء الأدوات (tool calling) — وهو ليس «استدعاء الدوال» (function calling)، وهو المصطلح الأقدم الذي ابتدعته OpenAI؛ الخلط بينهما سبب سوء فهم شائع لما يفعله النموذج فعلاً.

هذه المصطلحات ستراها في المقال:

  • الأداة (tool): دالة لها مُدخلات ومخرجات محدّدة، يمرّرها لانغ تشين إلى نموذج المحادثة.
  • استدعاء الأدوات (tool calling): أن يطلب النموذج تنفيذ أداة باسم محدّد ووسائط يختارها بنفسه.
  • استدعاء الدوال (function calling): المصطلح الأقدم من OpenAI لنفس الآلية، وهو ما ترثه وثائق ومقالات كثيرة على الويب.
  • الـ docstring: وصف الدالة الذي يقرأه النموذج ليقرّر متى يستدعيها.
  • type hints: تلميحات النوع التي منها يُبنى مُخطَّط الأداة.
  • المُخطَّط (schema): اسم الأداة ووصفها وتعريفات وسائطها.
  • الوكيل (agent): نموذج يستدعي الأدوات في حلقة حتى تكتمل المهمة.
  • عقدة الأدوات (ToolNode): العقدة الجاهزة في لانغ غراف (LangGraph) التي تنفّذ الاستدعاءات.

ما هي الأداة تقنياً؟ دالة لها مُدخلات ومخرجات محدّدة يقرّر النموذج وحده متى يستدعيها

الأداة ليست صيغة سحرية ولا عملية محجوزة على خادم بعيد: هي دالة يمرّرها لانغ تشين إلى نموذج المحادثة، فيحلّل النموذج سياق المحادثة ويقرّر بنفسه متى يستدعيها وبأي وسائط. النص الإنجليزي في الوثائق يصفها هكذا: «Under the hood, tools are callable functions with well-defined inputs and outputs that get passed to a chat model… The model decides when to invoke a tool based on the conversation context, and what input arguments to provide.»

لاحظ كلمتين مهمّتين: «بواسطة النموذج» — أي أن النموذج لا يعرف اسم دالتك ولا يسمياتها ولا يرى كودك؛ كل ما يراه اسم ووصف ومُخطَّط وسائط. و«يقرّر متى» — أي أن الاستدعاء ليس وعداً بل رغبة: قد يصدرها النموذج في أي دورة، وقد لا يصدرها أصلاً.

ما الفرق بين استدعاء الأدوات واستدعاء الدوال؟

استدعاء الأدوات (tool calling) هو التسمية المعتمدة في لانغ تشين وفي أدلة المزوّدين الحالية، واستدعاء الدوال (function calling) هو التسمية الأقدم التي جاءت من OpenAI وما زالت تسود كثيراً من التوثيقات على الويب. الاتجاه العام واضح، لكن عليك أن تعرف أمرين قبل أن تكتب عنهما وكأنهما واحد.

الأول: الوثائق نفسها تستخدم المصطلحين بالتبادل عن قصد. حرفيّاً: «You may hear the term "function calling". We use this interchangeably with "tool calling".» فإذا رأيت كلمة function calling في نشرة قديمة فلا يعني أنها تصف آلية مختلفة.

الثاني — وهو الأهم عملياً: النطاق مختلف. «الدالة» تُوحي بأن هناك دالة بايثون كتبتها أنت، وهذا يتّسق مع اصطلاح OpenAI. أما «الأداة» فأوسع: الأداة زوج من مُخطَّط ودالة قابلة للتنفيذ، وقد لا يكون التنفيذ على جهازك أصلاً. فبعض النماذج لديها أدوات مدمجة تُنفَّذ على خوادم المزوّد نفسه، وهناك أدوات تُكتشف من خادم بعيد عبر بروتوكول سياق النموذج (MCP) وتُحوَّل إلى أدوات لانغ تشين دون أن تكتب دالة واحدة. لذلك لو عمدت إلى كتابة «استدعاء الدوال» مكان «استدعاء الأدوات» في مقال، فأنت تضيّق المفهوم وتُخفي نصف ما يحدث.

كيف تُحوّل دالة عادية إلى أداة؟ زخرفة @tool

الأداة تُسجَّل بزخرفة @tool من langchain.tools، واثنان ممّا تكتبه في الدالة جزء من العقد مع النموذج لا زينة: الـ docstring وتلميحات النوع. الوثائق تقولها صراحة: «By default, the function's docstring becomes the tool's description» و«Type hints are required as they define the tool's input schema».

هذا يعني أن الـ docstring حرفياً هو النص الذي يراه النموذج ليقرّر: هل هذه الأداة مناسبة لهذا الطلب؟ فكتابة """Does stuff.""" لا تعطيه أي معلومة، وتتركه يخمّن ما تفعله الأداة:

from langchain.tools import tool

@tool
def search_database(query: str, limit: int = 10) -> str:
    """Search the customer database for records matching the query.

    Args:
        query: Search terms to look for
        limit: Maximum number of results to return
    """
    return f"Found {limit} results for '{query}'"

في هذا المثال، limit: int = 10 هو الذي يجعل limit يظهر في مُخطَّط المدخلات، والنص الإنجليزي فوقه هو ما يجعل النموذج يعرف متى يستدعي search_database. وثّق كل وسيط بجملة قصيرة، واكتب بلغة واضحة، ولا تسمِّ دالتك أو وسائطها بالعربية أمام النموذج.

تنبيه دقيق هنا: تلميحات النوع تبني مُخطَّط المُدخلات، والـ docstring كلّه يصبح وصف الأداة. إن أردت لكل مُدخل وصفاً خاصاً به، فالتلميحات وحدها لا تكفي — استخدم args_schema مع نموذج Pydantic:

from pydantic import BaseModel, Field
from typing import Literal

class WeatherInput(BaseModel):
    """Input for weather queries."""
    location: str = Field(description="City name or coordinates")
    units: Literal["celsius", "fahrenheit"] = Field(
        default="celsius",
        description="Temperature unit preference"
    )
    include_forecast: bool = Field(
        default=False,
        description="Include 5-day forecast"
    )

@tool(args_schema=WeatherInput)
def get_weather(location: str, units: str = "celsius", include_forecast: bool = False) -> str:
    """Get current weather and optional forecast."""
    temp = 22 if units == "celsius" else 72
    result = f"Current weather in {location}: {temp} degrees {units[0].upper()}"
    if include_forecast:
        result += "\nNext 5 days: Sunny"
    return result

احتفظ بقسم Args: في الـ docstring فهو عُرف بايثون مريح للقارئ البشري، لكن لا تعتمد عليه أمام النموذج؛ الاعتماد الصحيح على Field(description=...).

لماذا يجب أن يكون اسم الأداة snake_case؟

لأن الاسم يدخل ضمن المُخطَّط الذي يُرسَل إلى مزوّد النموذج، وبعض المزوّدين يرفضون الأسماء التي فيها مسافات أو رموز خاصة، فينهار الطلب عند المزوّد لا في كودك. حرفيّاً: «Prefer snake_case for tool names (e.g., web_search instead of Web Search). Some model providers have issues with or reject names containing spaces or special characters with errors.»

وهنا لقطة الدعم الحقيقية: خطأ التسمية لا يظهر في اختباراتك إن كنت تجرّب مع مزوّد واحد متسامح، ثم يظهر بعد النشر عند مزوّد آخر برسالة غامضة من واجهته. سمِّ كل أدواتك بأسماء قصيرة بلا مسافات منذ البداية.

وإن كان اسم دالتك في بايثون لا يناسب، فبإمكانك تجاوزه عند التسجيل:

from langchain.tools import tool

@tool("web_search")  # Custom name
def search(query: str) -> str:
    """Search the web for information."""
    return f"Results for: {query}"

print(search.name)  # web_search

والمعامل description= يتجاوز الوصف المُولَّد تلقائياً من الـ docstring إذا أردت صياغة موجّهة للنموذج تحديداً.

كيف يقرّر النموذج أي أداة يستدعي؟

النموذج يصدر طلباً باسم الأداة ووسائطها، ثم تُنفَّذ الأداة وتُعاد نتيجتها في رسالة ToolMessage التي يفكّر النموذج بعدها من جديد. والأدوات لا تُتاح إلا بعد ربطها بالنموذج عبر bind_tools: «In subsequent invocations, the model can choose to call any of the bound tools as needed.»

هذه هي الحلقة كاملة عند استخدام النموذج وحده، خارج الوكيل:

tools = [get_weather]
tools_by_name = {tool.name: tool for tool in tools}
model_with_tools = model.bind_tools(tools)

# Step 1: Model generates tool calls
messages = [{"role": "user", "content": "What's the weather in Boston?"}]
ai_msg = model_with_tools.invoke(messages)
messages.append(ai_msg)

# Step 2: Execute tools and collect results
for tool_call in ai_msg.tool_calls:
    # Execute the tool with the generated arguments
    tool = tools_by_name[tool_call["name"]]
    tool_result = tool.invoke(tool_call)
    messages.append(tool_result)

# Step 3: Pass results back to model for final response
final_response = model_with_tools.invoke(messages)
print(final_response.text)

كل طلب استدعاء في ai_msg.tool_calls يحمل name وargs ومعرّفاً فريداً، وعليك أن تعيد نتيجة لكل طلب ممرَّر إلى ToolMessage يحمل نفس ذلك المعرّف. إن استخدمت الوكيل بدل النموذج وحده، فكل هذه المسؤولية — التنفيذ وإعادة النتيجة — تنتقل إلى حلقة الوكيل تلقائياً.

أداة تشخيص مفيدة: النماذج الداعمة للاستدعاء تفعّل الاستدعاءات المتوازية افتراضياً، وبعض المزوّدين يسمحون بتعطيلها — ومنهم OpenAI وAnthropic — عبر parallel_tool_calls=False. عطّلها حين تتشابك المخرجات؛ عندها يصلك طلب استدعاء واحد واضح في كل دورة فتسهل قراءة سلوك النموذج.

كيف تنفّذ لانغ غراف الاستدعاءات؟ عقدة ToolNode

في لانغ غراف، تنفّذ الاستدعاءات عقدة جاهزة اسمها ToolNode، وهي تتكفّل بالتوازي ومعالجة الأخطاء وحقن الحالة تلقائياً: «ToolNode is a prebuilt node that executes tools in LangGraph workflows. It handles parallel tool execution, error handling, and state injection automatically.»

from langgraph.prebuilt import ToolNode

builder.add_node("tools", ToolNode([search, calculator]))
# ... add other nodes and edges
graph = builder.compile()

استخدمها حين تريد التحكّم الدقيق في كيفية تنفيذ أدواتك داخل رسمك، فهي اللبنة التي يقوم عليها التنفيذ في كثير من أنماط وكلاء لانغ غراف. انتبه لقيد واحد: الأدوات لا ترى إلا قيم الحالة التي مُرِّرت إلى ToolNode؛ فإذا استدعيت ToolNode يدوياً من عقدة أخرى، مرّر الحالة كاملة أو سترى messages فقط. أمّا في لانغ تشين (الوكيل نفسه) فلا تعمل ToolNode، بل تمرّ معالجة أخطاء الأدوات عبر طبقة وسيطة (middleware) — وهذا موضوع المقال التالي.

كيف تصل الأداة إلى حالة الوكيل؟ ToolRuntime

الأداة تحتاج أحياناً إلى ما هو خارج وسائطها: سجلّ الرسائل، ومعرّف المستخدم، ومخزن طويل المدى. الطريقة الوحيدة التي تعلّمها إصدارات v1 هي معامل واحد اسمه runtime من نوع ToolRuntime:

from langchain.tools import tool, ToolRuntime
from langchain.messages import HumanMessage

@tool
def get_last_user_message(runtime: ToolRuntime) -> str:
    """Get the most recent message from the user."""
    messages = runtime.state["messages"]

    # Find the last human message
    for message in reversed(messages):
        if isinstance(message, HumanMessage):
            return message.content

    return "No user messages found"

runtime.state يقرأ حالة الوكيل، وruntime.context يقرأ الإعدادات الثابتة التي تمرّرها عند الاستدعاء (مثل user_id)، وruntime.store يقرأ ويكتب في مخزن طويل المدى، وruntime.tool_call_id يعطيك معرّف الاستدعاء لتبني عليه ToolMessage. ومهمّ أن runtime مخفيّ عن النموذج: هو ليس مُدخلاً في مُخطَّط الأداة، فإذا كانت دالتك لها وسائط أخرى فالنموذج لا يرى سوى تلك.

أما runtime.execution_info فيحتاج deepagents>=0.5.0 (أو langgraph>=1.1.5).

هذه الصيغة وحيدة اليوم: الأنماط الأقدم من حقن الحالة (InjectedState وInjectedStore وget_runtime() وInjectedToolCallId) استُبدلت كلها بـToolRuntime كواجهة واحدة صريحة للحالة والسياق والمخزن وبيانات التنفيذ. إن مرّ عليك في درس قديم state: InjectedState، فاعلم أنك تقرأ كوداً سابقاً على الإصدار الحالي.

تحذير صريح: config وruntime أسماء محجوزة

الوثائق تحذّر بالنص: «The following parameter names are reserved and cannot be used as tool arguments. Using these names will cause runtime errors.» — أي أن اسم الوسيط config محجوز لتمرير RunnableConfig داخلياً، واسم runtime محجوز لمعامل ToolRuntime نفسه. تسمية وسيطك runtime تعني أنك تسدّ الباب على الحقن وتُخرج خطأ وقت التشغيل، والمعالجة أن تعطي الوسيط اسماً مختلفاً وتقرأ الحقن من ToolRuntime.

متى يتجاوز النموذج بأداة؟ return_direct وشرطه الدقيق

return_direct=True تعني: أعد مخرجات الأداة إلى المستدعي فوراً، بلا دورة نمذجة إضافية: «the agent returns the tool's output to the caller immediately, without sending it back through the model for further processing.»

from langchain.tools import tool

@tool(return_direct=True)
def fetch_order_status(order_id: str) -> str:
    """Fetch the current status of a customer order."""
    # In production, query your order management system here
    return f"Order {order_id} is shipped and will arrive in 2 days."

مرّرها إلى الوكيل كأي أداة أخرى، فيخرج مخرجاتها نصّاً نهائياً دون أن يعيد النموذج صياغته.

الشرط الذي يغفل عنه الكثيرون هو شرط الدفعة، لا الأداة الواحدة: حين يطلب النموذج عدة أدوات في خطوة واحدة، تُنفَّذ كلها أولاً، ثم «the agent routes to END only if every tool in that batch has return_direct=True». أي أن أداة واحدة بلا return_direct ضمن الدفعة تعني أن الحلقة لا تنتهي: يعود الوكيل إلى النموذج ومعه ToolMessage من كل أداة في تلك الخطوة. لا تعمّم القاعدة على «كل أداة فيها return_direct تُنهي الحلقة» — الشرط كل الأدوات في دفعة واحدة.

والعكس صحيح أيضاً: لا تضع return_direct=True على أداة تحتاج نتيجتها إلى تفكير أو تلخيص أو استدعاء أدوات أخرى، فـ«Because the model does not process the tool's output» — أي أنك ستفقد خطوة الاستدلال كلها.

كيف تسحب أدوات من خادم MCP؟ MCPAdapter وحالة نضجها

بدل كتابة دوال كل أداة بيدك، يمكنك أن تسحب أدوات خادم خارجي عبر MCPAdapter، الذي يكتشف أدوات الخادم ويحوّلها إلى أدوات لانغ تشين تمرّرها إلى create_agent كأي أداة أخرى. التثبيت يحتاج الإضافية:

pip install "langchain[mcp]"

والاستخدام كامل في كتلة غير متزامنة واحدة:

import asyncio

from langchain.agents import create_agent
from langchain.mcp import MCPAdapter

async def main():
    async with MCPAdapter("https://example.com/mcp") as adapter:
        tools = await adapter.list_tools()
        agent = create_agent(model, tools)
        return await agent.ainvoke({"messages": [{"role": "user", "content": "..."}]})

asyncio.run(main())

(model هنا هو نموذج المحادثة نفسه الذي أعددتّه في المقال السابق؛ لم يثبّت هذا المقطع اسم مزوّد بعينه لأن الوثائق تتناقض في ذلك بين الصفحات.)

احتفظ بالمحوّل مفتوحاً ما دام الوكيل يعمل؛ الأدوات نفسها تحمل الاتصال، فيبقى الوكيل صالحاً للاستخدام بعد خروج الكتلة.

وقبل أن تبني عليها، اقرأ التحذير: نطاق langchain.mcp يتطلب langchain[mcp]>=1.4.0 وهو في حالة beta، «Importing from it raises a LangChainBetaWarning once per process. The API may change.» جرّبها في مشروع جانبي، واعمل على أن تغيّر واجهتك، ولا تجعلها أساساً لنظام إنتاجي هذا الشهر.

ماذا بُني عملياً في هذا المثال؟

أداتان مسجّلتان بعقود مكتوب بعناية، حلقة استدعاء يدوية تقرأ tool_calls وتعيد ToolMessage بمعرّفاتها الصحيحة، أداة واحدة تتجاوز النموذج بشرط الدفعة كامل، وأدوات مسحوبة من خادم MCP خارجي — أربع حالات تغطّي أغلب ما ستحتاجك.

قبل أن تنتقل، راجع ثلاث نقاط: هل كل اسم أداة snake_case بلا مسافات؟ هل استخدمت config أو runtime كاسم وسيط؟ وهل لكل أداة return_direct وصف صادق؟ أغلب أخطاء الدعم في هذا الموضوع سببها واحدة منها. وفي المقال التالي تبني وكيلاً كاملاًيجمع القطع معاً، ثم بعده تصل الوكيل بقاعدة بيانات وواجهة برمجية حقيقية.


تحتاج وكيلاً يعمل لفريقك؟

نصمّم ونبني أنظمة وكلاء للشركات والأفراد فوق لانغ تشين (LangChain) ولانغ غراف (LangGraph) — من إثبات المفهوم في أسبوع إلى نظام إنتاجي يُشغَّل يومياً. ابدأ بطلبك من صفحة التواصل.

المصادر

كل ادعاء في المقال مرتبط بمصدره. الروابط تفتح في نافذة جديدة.

  1. 01
    Tools — LangChain ↗

    docs.langchain.com

    return_direct تُخرج مخرجات الأداة إلى المستدعي مباشرة بلا معالجة إضافية من النموذج، ولا ينتقل الوكيل إلى النهاية إلا إذا كانت كل الأدوات في تلك الدفعة تحمل return_direct=True.

  2. 02
    Tools — LangChain ↗

    docs.langchain.com

    أنماط الحقن القديمة InjectedState وInjectedStore وget_runtime() وInjectedToolCallId استُبدلت بـToolRuntime كواجهة واحدة صريحة للحالة والسياق والمخزن وبيانات التنفيذ.

  3. 03
    Tools — LangChain ↗

    docs.langchain.com

    الأداة في جوهرها دالة لها مُدخلات ومخرجات محدّدة يمرّرها لانغ تشين إلى نموذج المحادثة، والنموذج هو من يقرّر متى يستدعيها وبأي وسائط بناءً على سياق المحادثة.

  4. 04
    Tools — LangChain ↗

    docs.langchain.com

    الـ docstring الخاص بالدالة يصبح وصف الأداة افتراضياً، وتلميحات النوع إلزامية لأنها تعرّف مُخطَّط مُدخلات الأداة.

  5. 05
    Tools — LangChain ↗

    docs.langchain.com

    المعاملان config وruntime أسماء محجوزة لا يجوز استخدامهما كوسائط للأداة، واستخدامهما يسبب أخطاء وقت التشغيل.

  6. 06
    Tools — LangChain ↗

    docs.langchain.com

    توثيق لانغ تشين ينصح بأن تكون أسماء الأدوات بصيغة snake_case لأن بعض مزوّدات النماذج تعاني من الأسماء التي تحتوي مسافات أو رموزاً خاصة أو ترفضها.

  7. 07
    Workflows and agents — LangGraph docs ↗

    docs.langchain.com

    لانغ غراف توفّر عقدة ToolNode الجاهزة التي تنفّذ الأدوات وتتكفّل بالتوازي ومعالجة الأخطاء وحقن الحالة تلقائياً، وتكشف الأدوات داخلها قيم الحالة التي مُرِّرت إليها فقط.

  8. 08
    Model Context Protocol (MCP) — LangChain documentation ↗

    docs.langchain.com

    نطاق langchain.mcp يتطلب langchain[mcp]>=1.4.0 وهو في حالة beta، والاستيراد منه يطلق تحذير LangChainBetaWarning، وقد تتغير الواجهة.

شروح أخرى

الكل ←