דילוג לתוכן
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

סוכני AI ו-tool use: איך מודל קורא לפונקציות

איך tool use עובד: המודל מחזיר קריאה מובנית, תוכנית המארח מריצה את הכלי, וחוקי אישור קובעים מה מותר.

מתחילים9 דקות קריאהמאת NotHoly

מודל צ׳אט מפיק טקסט. סוכן הוא מערכת שמאפשרת למודל לעשות יותר מזה: הוא קורא בקשה, מחליט שהוא צריך מידע או פעולה מחוץ לשיחה, מבקש מתוכנית מארחת (host) לעשות את העבודה וממשיך עם התוצאה. כמעט כל סוכן בנוי על מנגנון אחד, tool use, שנקרא גם function calling.

המאמר הזה מסביר את המנגנון שלב אחר שלב, בעזרת מבני הבקשה והתשובה מתיעוד API ציבורי. הוא מראה למה המארח, ולא המודל, מחליט מה באמת רץ, למה פעולות עם תופעות לוואי דורשות החלטה אנושית, ומה הרשימה של OWASP מכנה excessive agency.

אין צורך במעבדה. סקיצה מקומית קצרה לקראת הסוף מריצה את הלולאה עם stub במקום מודל, כך שאפשר לראות את זרימת הבקרה בלי לשלוח כלום ל-API.

ממה מורכב סוכן

לסוכן יש שלושה חלקים.

המודל מקבל טקסט ומפיק טקסט. ב-tool use הוא יכול גם להפיק בקשה מובנית שאומרת "לקרוא לפונקציה הזו עם הארגומנטים האלה".

המארח הוא התוכנית שכותבים או מריצים. הוא שולח בקשות למודל, מחזיק את השיחה, קורא את הפלט ומחליט מה לעשות איתו. הקוד האמיתי של כל כלי נמצא אצל המארח.

הכלים הם פונקציות שהמארח מוכן להריץ בשם המודל: חיפוש קובץ, שאילתה למסד נתונים, משיכת דף, שליחת הודעה. לכל כלי יש שם, תיאור וסכמה של הארגומנטים.

המודל מציע והמארח מחליט. רוב שאלות האבטחה בהמשך המאמר נובעות מהחלוקה הזו.

למה הלולאה חשובה

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

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

איך קריאה לכלי עובדת

כל חילופי tool use עוברים את אותו רצף, בלי קשר ל-API.

  1. המארח שולח למודל את השיחה ורשימה של הגדרות כלים.
  2. המודל עונה בטקסט, או מחזיר קריאה מובנית לכלי ומסמן שהתשובה עוצרת בשביל כלי.
  3. המארח קורא את הקריאה, בודק אותה ומריץ את הפונקציה המתאימה אם היא קיימת והמדיניות מאפשרת.
  4. המארח מוסיף את התוצאה לשיחה ומפנה לקריאה לפי המזהה שלה.
  5. המארח שולח בחזרה את השיחה המעודכנת. המודל קורא לכלי נוסף או כותב את התשובה הסופית.

הלולאה מסתיימת כשהמודל מחזיר טקסט רגיל, כשהמארח מגיע למגבלת סבבים או כשהמארח עוצר אותה.

הגדרות כלים הן JSON Schema

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

המודל מחזיר קריאה מובנית

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

מובנה לא אומר נכון. המודל יכול לציין כלי שלא קיים, או להעביר ערך שמתאים לסכמה אבל שגוי למצב.

המארח מריץ את הכלי

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

התוצאה חוזרת

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

מבני בקשה ותשובה עם הסברים

המבנים כאן מבוססים על שני מקורות ציבוריים של tool use, שנבדקו ב-2026-11-08. מזהי המודל הם placeholders. שמות השדות מדויקים.

בקשה עם כלי אחד

json
{
  "model": "<model-id>",
  "max_tokens": 1024,
  "tools": [
    {
      "name": "get_weather",
      "description": "Get the current weather for a given location.",
      "input_schema": {
        "type": "object",
        "properties": {
          "location": {
            "type": "string",
            "description": "City and state, e.g. San Francisco, CA"
          }
        },
        "required": ["location"]
      }
    }
  ],
  "messages": [
    { "role": "user", "content": "What's the weather in San Francisco?" }
  ]
}
  • tools היא הרשימה המלאה של הפונקציות שהמודל רשאי לקרוא להן. מבחינת המודל, שום דבר מחוץ לה לא קיים.
  • name הוא מה שהמודל יחזיר, ומה שהמארח משתמש בו כדי לבחור את הפונקציה.
  • description הוא מה שהמודל קורא, ולכן הוא צריך להגיד מה הכלי עושה ומתי להשתמש בו.
  • input_schema הוא JSON Schema. השדה required אומר למודל אילו ארגומנטים הוא חייב לספק.

תשובה שמבקשת כלי

json
{
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "tool_use",
      "id": "toolu_01A09q90qw90lq917835lq9",
      "name": "get_weather",
      "input": { "location": "San Francisco, CA" }
    }
  ]
}
  • stop_reason עם הערך tool_use אומר שהמודל מחכה לתוצאה. מארח שקורא רק טקסט יפספס את הקריאה.
  • בלוק עם type בערך tool_use הוא הקריאה המובנית. חייבים להחזיר את השדה id שלו.
  • input אמור להתאים לסכמה. צריך לאמת אותו בכל מקרה, כי המודל הוא לא מנוע סכמות.

ההמשך עם התוצאה

json
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": "15 degrees Celsius, partly cloudy"
    }
  ]
}
  • tool_use_id חייב להיות שווה לשדה id של הקריאה. אי־התאמה שוברת את הצימוד.
  • כדאי לשמור על content קצר. dumps גדולים מבזבזים הקשר ויכולים לשאת טקסט מוזרק.

אותה לולאה בפורמט function calling

מקור שני משתמש בשמות אחרים לאותו רעיון. לכלי יש type בערך function, והמודל מחזיר פריט function_call שהשדה arguments שלו הוא מחרוזת JSON שהמארח צריך לפענח.

json
{
  "type": "function",
  "name": "get_weather",
  "description": "Retrieves current weather for the given location.",
  "parameters": {
    "type": "object",
    "properties": {
      "location": { "type": "string" }
    },
    "required": ["location"],
    "additionalProperties": false
  },
  "strict": true
}
json
{
  "type": "function_call",
  "id": "fc_12345xyz",
  "call_id": "call_12345xyz",
  "name": "get_weather",
  "arguments": "{\"location\":\"Paris, France\"}"
}
json
{
  "type": "function_call_output",
  "call_id": "call_12345xyz",
  "output": "{\"temperature\":\"25\",\"unit\":\"C\"}"
}

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

למה המארח מחליט מה רץ

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

מכאן נובעים שלושה תפקידים למארח. זה המקום היחיד שבו קוד רץ, כך שפונקציית מחיקה רצה רק כשהמארח מריץ אותה. זה המקום היחיד שבו אפשר לאכוף מדיניות: שורה בפרומפט שאומרת "לא לכתוב קבצים" היא בקשה, ואילו היעדר כלי כתיבה או תפקיד read-only במסד הנתונים הם בקרה. והוא שולט בסקופ, כי הוא בוחר אילו כלים מוצעים בכל סבב. הצעת פחות כלים היא הבקרה הזולה ביותר שיש.

מפרט MCP מתאר את אותה חלוקה במונחי פרוטוקול. המארח הוא אפליקציית ה-LLM, הלקוחות הם מחברים בתוכה, והשרתים חושפים כלים. לפי המפרט, מארחים חייבים לקבל הסכמה מפורשת של המשתמש לפני הפעלת כלי כלשהו, ותיאורי כלים כמו annotations צריכים להיחשב לא מהימנים אלא אם הם מגיעים משרת מהימן. דף המפרט שבדקתי ב-2026-11-08 מפנה לגרסה מתאריך 2026-07-28, ולכן צריך לקרוא את הגרסה הנוכחית לפני שבונים עליה.

למה אישור חשוב כשיש תופעות לוואי

חלק מהקריאות לכלים רק קוראות. חיפוש או שליפה לא משנים שום דבר מחוץ למארח. אחרות משנות משהו: שולחות הודעה, עורכות רשומה, מוציאות כסף או מוחקות נתונים.

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

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

Excessive agency, לפי OWASP

רשימת OWASP Top 10 for LLM Applications מונה את excessive agency בתור LLM06:2025. הערך מתאר חולשה שמאפשרת לפעולות מזיקות לרוץ בתגובה לפלט מודל לא צפוי, דו־משמעי או כזה שעבר מניפולציה, יהיה הגורם אשר יהיה. הזיות והזרקת פרומפט, ישירה או עקיפה, הן טריגרים נפוצים.

הערך מציין שלושה גורמי שורש: excessive functionality (כלים שהמשימה לא צריכה), excessive permissions (כלים שיכולים לעשות יותר ממה שהמשימה דורשת) ו-excessive autonomy (פעולות בעלות השפעה גבוהה בלי בדיקה). דרכי המיתון נובעות מהגורמים האלה. להציע רק את הכלים שהמשימה צריכה. להעדיף כלים צרים על פני shells כלליים או משיכת URL שרירותי. לאכוף גישה מינימלית במערכת שבהמשך השרשרת, למשל עם תפקידי מסד נתונים או OAuth scopes צרים. להריץ פעולות תחת ההרשאה של המשתמש עצמו. לדרוש אישור אנושי לפעולות בעלות השפעה גבוהה. לאכוף הרשאות בהמשך השרשרת, במקום לסמוך על המודל שיחליט.

לוגים, ניטור ו-rate limiting הם אמצעים להגבלת נזק. הם לא מונעים את הבעיה.

סקיצה מקומית של הלולאה

בסקיצה הזו אין provider, אין קריאת רשת ואין סודות. במקום מודל יש כאן stub קבוע. הוא מחזיר קריאה לכלי בסבב הראשון שלו וטקסט אחרי שהתוצאה מגיעה. התפקיד שלו הוא להראות איפה המארח יושב. בסביבה שלך אפשר להחליף את ה-stub בקריאת API אמיתית ולהשאיר את הלוגיקה של המארח.

שומרים את זה בשם loop_demo.py:

python
# Local sketch: no provider, no network. The "model" is a fixed stub that
# returns the same shape a provider returns for a tool call.
import json

def get_weather(location: str) -> str:
    return json.dumps({"location": location, "temp_c": 15, "sky": "partly cloudy"})

def delete_file(path: str) -> str:
    return json.dumps({"deleted": path})  # never reached in this demo

# The host decides what may run. The model only names a tool.
REGISTRY = {"get_weather": get_weather}          # delete_file is NOT registered
REQUIRES_APPROVAL = {"delete_file"}

def fake_model(messages):
    # Stub: first turn asks for weather, second turn answers with text.
    last = messages[-1]
    if last["role"] == "user" and isinstance(last["content"], str):
        return {"stop_reason": "tool_use",
                "content": [{"type": "tool_use", "id": "toolu_demo_1",
                             "name": "get_weather", "input": {"location": "Lisboa, PT"}}]}
    return {"stop_reason": "end_turn", "content": [{"type": "text", "text": "done"}]}

messages = [{"role": "user", "content": "Weather in Lisboa?"}]
for step in range(5):  # hard cap on turns
    reply = fake_model(messages)
    messages.append({"role": "assistant", "content": reply["content"]})
    if reply["stop_reason"] != "tool_use":
        print("final:", reply["content"][0]["text"])
        break
    results = []
    for block in reply["content"]:
        if block["type"] != "tool_use":
            continue
        name, args = block["name"], block["input"]
        if name not in REGISTRY:
            out, err = f"unknown tool {name}", True
        elif name in REQUIRES_APPROVAL:
            out, err = "approval required", True
        else:
            out, err = REGISTRY[name](**args), False
        print(f"host ran {name}({args}) -> {out}")
        results.append({"type": "tool_result", "tool_use_id": block["id"],
                        "content": out, "is_error": err})
    messages.append({"role": "user", "content": results})

מריצים מתיקייה שמכילה רק את הקובץ הזה:

bash
python -I loop_demo.py

הדגל -I מפעיל את Python במצב מבודד. הוא מתעלם ממשתני סביבה ומשאיר את התיקייה הנוכחית מחוץ לנתיב הייבוא, הרגל שימושי עבור קוד שלא כתבת.

על מחשב Windows 11 עם Git Bash ו-Python 3.12.10, ההרצה הדפיסה:

text
host ran get_weather({'location': 'Lisboa, PT'}) -> {"location": "Lisboa, PT", "temp_c": 15, "sky": "partly cloudy"}
final: done

ה-stub החזיר סיבת עצירה tool_use וקריאה אל get_weather. המארח מצא את השם במאגר שלו, הריץ אותו וכתב tool_result בחזרה לשיחה. ה-stub ענה אז בטקסט והלולאה הסתיימה. delete_file לא רץ, כי המארח אף פעם לא רשם אותו. כדי לראות את ענף האישור, גורמים ל-stub לבקש את delete_file ורואים שהמארח מסרב.

טעויות נפוצות

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

לכתוב תיאורים מעורפלים. "עושה דברים עם קבצים" מזמין קריאות שגויות. צריך לכתוב מה הכלי קורא או כותב ומתי לא להשתמש בו.

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

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

להשמיט מגבלת סבבים. לולאה בלי תקרה יכולה לחזור על קריאה כושלת עד שזה עולה כסף אמיתי.

לאשר סשן שלם. מאשרים קריאות ספציפיות עם הארגומנטים גלויים.

לתעד את הדברים הלא נכונים. רושמים שמות כלים, החלטות ותוצאות, בזהירות לגבי מידע אישי. פרומפטים ותשובות מלאים בלוגים יכולים להכיל נתונים שלא היו אמורים להישמר.

איך ממשיכים

לבנות לולאה עם מפתח API אמיתי וכלי אחד לקריאה בלבד, למשל פונקציה שמחזירה את השעה הנוכחית. לבדוק את גופי הבקשה והתשובה ולמצוא את הבלוק tool_use ואת tool_result שמחזירים.

אחר כך להוסיף כלי אחד שכותב לקובץ זמני בתיקייה שבבעלותך, ולשים לפניו שלב אישור. לבקש מהמודל נתיב מחוץ לתיקייה הזו ולבדוק שהמארח שלך עוצר את הקריאה.

לקרוא את הסעיפים על כלים ואבטחה במפרט MCP, ואת הערך של OWASP על excessive agency במלואו. למפות כל דרך מיתון לשורה בקוד שלך.

מקורות

נבדקו ב-2026-11-08. דפי תיעוד זזים, ולכן צריך לוודא מהי הגרסה הנוכחית לפני שמסתמכים על שם שדה.

מקורות

  1. Tool use overview, public API documentation
  2. Function calling guide, public API documentation
  3. OWASP Top 10 for LLM Applications: LLM06:2025 Excessive Agency
  4. Model Context Protocol specification

עוד בנושא סוכני AI לאבטחה

סוכני AI לאבטחה

למה לאוטמט recon ובדיקות ב-API של NotHoly

שימו בפייפליין מודל שלא מסרב, עם CVE, advisory, בסיס ידע וקריאת עמודים ציבוריים שלא עולים טוקנים. הכלים שלכם עושים את העבודה הפעילה.

ביניים2 דקות קריאה

סוכני AI לאבטחה

בניית ה-hackbot שלך, חלק 1: ארכיטקטורה

העיצוב של סוכן bug bounty קטן: LLM, אוסף כלים, זיכרון ו-guardrails, מחוברים ללולאה שרצה על המחשב שלך באישורך.

ביניים3 דקות קריאה

סוכני AI לאבטחה

בניית ה-hackbot שלך, חלק 2: קוד הסוכן

סוכן bug bounty קטן ב-Python עם קריאה לכלים על NotHoly API: הוא מציע פקודות recon, מחכה לאישורך, קורא פלט ושומר הערות.

ביניים3 דקות קריאה