Tool Use — כלים לסוכנים
סוכן טוב הוא רק כמו הכלים שנתת לו. איך מעצבים כלים שהמודל מבין, מפעיל נכון, ולא שובר. המדריך המעשי.
במדריך פלטים מובנים ו-Tool Use ראינו את המכניקה הבסיסית של function calling. כאן נעמיק בעיצוב — איך לבנות כלים שסוכן AI באמת יצליח להשתמש בהם, שזו האמנות שמפרידה בין סוכן שעובד לסוכן שמסתבך.
הגדרת כלי היא פרומפט, לא תיעוד API
זו ההבנה שמשנה הכי הרבה, והיא הפוכה לאינטואיציה של מי שבא מפיתוח.
המודל לא רואה את המימוש שלך. הוא רואה שם, תיאור, ורשימת פרמטרים עם התיאורים שלהם — וזה כל המידע שעל בסיסו הוא מחליט האם לקרוא לכלי, מתי, ובאילו ערכים. כלומר שלושת אלה הם פרומפט, ונכתבים לפי אותם כללים.
ורוב האנשים כותבים אותם כמו תיעוד טכני:
# ✗ תיאור שנכתב למפתח
{
"name": "get_data",
"description": "Retrieves data from the database",
"parameters": {"id": {"type": "string"}}
}
# ✓ תיאור שנכתב למי שמחליט
{
"name": "get_customer_by_phone",
"description": (
"מחזיר את פרטי הלקוח לפי מספר טלפון. "
"השתמש כשצריך לדעת מסלול, היסטוריית הזמנות "
"או פרטי קשר. אם הלקוח לא נמצא — מחזיר null, "
"וזה אומר שזה לקוח חדש."
),
"parameters": {
"phone": {
"type": "string",
"description": "מספר טלפון ישראלי, ספרות בלבד, "
"מתחיל ב-0. לדוגמה: 0501234567"
}
}
}
ההבדל אינו אורך אלא שהשני עונה על שלוש שאלות שהמודל באמת שואל: מתי להשתמש בזה, באיזה פורמט הערך, ומה אומרת תשובה ריקה. השלישית היא זו שהכי חסרה, וזו שגורמת לסוכן להמציא כשהכלי לא מצא כלום.
תן את רשימת הכלים שלך לאדם שלא מכיר את המערכת ותשאל מתי הוא היה קורא לכל אחד. אם הוא מתלבט — המודל יתלבט יותר.
כלי אחד = החלטה אחת
הפיתוי הטבעי הוא לאחד: כלי אחד עם פרמטר action שמקבל "create", "update" או "delete". בקוד זה נקי; במודל זה מכפיל את מספר הטעויות.
הסיבה פשוטה — עכשיו המודל צריך לקבל שתי החלטות במקום אחת: איזה כלי, ואז איזו פעולה. וכשהכל תחת תיאור אחד, אין מקום לכתוב מתי כל פעולה מתאימה.
# ✗ החלטה כפולה
manage_order(action, order_id, ...)
# ✓ החלטה אחת לכל אחד
get_order(order_id)
cancel_order(order_id, reason)
update_order_address(order_id, address)
יש לזה גם יתרון נלווה שמתגלה מאוחר: אפשר לתת הרשאות שונות לכלים שונים. get_order יכול לרוץ חופשי, cancel_order יכול לדרוש אישור. עם כלי מאוחד, ההפרדה הזאת בלתי אפשרית בלי לקרוא את הפרמטרים.
השם הוא חצי מהתיאור
לפני שהמודל מגיע לתיאור הוא רואה את השם, ובמקרים רבים זה כל מה שהוא באמת שוקל. שם מדויק חוסך שתי שורות הסבר; שם עמום גורם לכך שאף הסבר לא יעזור.
שלושה כללים פשוטים: פועל ואחריו אובייקט — cancel_subscription ולא subscription_handler; ספציפי ולא כללי — search_customers ולא lookup, שיכול להיות כל דבר; ועקבי לרוחב הרשימה — אם אחד נקרא get_order, השני לא ייקרא fetch_invoice. חוסר עקביות בשמות גורם למודל לחפש הבדל משמעותי במקום שבו אין כזה.
ושם באנגלית גם כשהמערכת בעברית. מזהי הכלים והפרמטרים הם חלק מהמבנה, והמודלים ראו מיליוני חתימות פונקציות באנגלית ומעט מאוד בעברית. המבנה באנגלית, הערכים והתיאורים בעברית — זה מה שעובד בפועל.
הפרמטרים הם איפה שזה נשבר
המודל טועה בערכים הרבה יותר מאשר בבחירת הכלי, ורוב הטעויות האלה ניתנות למניעה בתיאור.
- פורמט מפורש, עם דוגמה. "תאריך" לא מספיק.
"YYYY-MM-DD, לדוגמה 2026-03-09"— כן. בלי זה תקבל שלושה פורמטים שונים באותו שבוע. - יחידות. סכום בשקלים או באגורות? משך בדקות או בשניות? זה מקור לבאגים שקטים ויקרים.
- רשימה סגורה במקום טקסט חופשי. אם יש חמש קטגוריות אפשריות —
enum, לאstring. המודל ימציא קטגוריה שנשמעת סבירה, וברשימה סגורה הוא פשוט לא יכול. - מה קורה כשמשמיטים. "אם לא צוין — מחזיר את 30 הימים האחרונים". בלי זה המודל ימלא ערך שנראה לו הגיוני.
- מינימום פרמטרים. כל פרמטר אופציונלי הוא עוד הזדמנות לטעות. אם אפשר לגזור ערך בקוד — תגזור בקוד.
ובעברית יש כלל מעשי שחוסך הרבה: הנרמול שייך לכלי, לא למודל. אל תבקש שיעביר טלפון בפורמט מסוים — תקבל מה שיש, ותנרמל בתוך המימוש. אותו דבר לתאריכים ולשמות. המודל גרוע בעקביות פורמט וטוב בהבנת כוונה; תן לו את מה שהוא טוב בו.
מה שהכלי מחזיר חשוב כמו מה שהוא עושה
חצי מהבעיות בסוכנים נובעות לא מהקריאה אלא מהתשובה. שלושה כללים:
- תמציתי. כלי שמחזיר מאה שדות דוחף את כולם להקשר, מדלל את תשומת הלב ועולה כסף. תחזיר את מה שצריך להחלטה הבאה. אם המודל יצטרך עוד — יקרא שוב.
- מובנה ועקבי. אותו מבנה בהצלחה ובכישלון. מודל שמקבל לפעמים אובייקט ולפעמים מחרוזת שגיאה מתבלבל.
- אומר מה לעשות הלאה. וזה החלק שכמעט אף אחד לא עושה. תשובה ריקה שאומרת רק
[]לא מנחה;{"found": false, "hint": "לא נמצא לקוח עם הטלפון הזה. אפשר לבקש ממנו לאמת את המספר."}כן.
def get_customer_by_phone(phone: str) -> dict:
norm = normalize_phone(phone) # הנרמול כאן
if not norm:
return {"ok": False,
"error": "מספר לא תקין",
"hint": "בקש מהמשתמש מספר בן 10 ספרות"}
row = db.find(norm)
if not row:
return {"ok": True, "found": False,
"hint": "לקוח חדש — אפשר להציע הרשמה"}
return {"ok": True, "found": True,
"customer": {"name": row.name,
"plan": row.plan,
"since": row.since}}
שים לב שאין כאן חריגה שנזרקת החוצה. שגיאה היא תשובה, לא קריסה — וזה הסעיף הבא.
כמה תוצאות להחזיר
השאלה הזו חוזרת בכל כלי חיפוש, והתשובה האינטואיטיבית — "הכל, שהמודל יבחר" — היא הטעות היקרה ביותר בסעיף הזה. חיפוש שמחזיר ארבעים לקוחות דוחף ארבעים רשומות להקשר, והמודל קורא את כולן בכל צעד שאחרי.
הדפוס שעובד הוא להחזיר מעט, ולומר כמה יש:
return {"ok": True,
"total": 47,
"showing": 5,
"results": rows[:5],
"hint": "יש עוד 47 תוצאות. צמצם לפי עיר "
"או תאריך כדי לקבל רשימה ממוקדת."}
המודל מקבל מספיק כדי להחליט, ויודע שיש עוד. בלי שדה total הוא מניח שחמש התוצאות הן כל מה שקיים, ועונה למשתמש תשובה שגויה בביטחון מלא.
ובעברית יש כאן שיקול נוסף: טקסט עברי נשבר ליותר טוקנים מאותו טקסט באנגלית. רשימת תוצאות עם תיאורים ארוכים בעברית תופסת בהקשר יותר ממה שנראה לעין, ולכן חיתוך שדות — להחזיר שם ומזהה ולא את כל הרשומה — משתלם כאן יותר מאשר בכלי אנגלי מקביל.
שגיאות: אל תפיל את הלולאה
כלי שזורק חריגה שלא נתפסה עוצר את הסוכן באמצע. המשתמש רואה כלום, ומה שכבר בוצע נשאר באוויר.
הדפוס הנכון: לתפוס הכל, ולהחזיר את השגיאה כתוצאת כלי. המודל יקבל אותה, יבין מה קרה, ויחליט — לנסות אחרת, לשאול את המשתמש, או לוותר על הצעד.
וההודעה עצמה היא גם פרומפט. יש הבדל גדול בין שתי אלה:
✗ "Error 400: Bad Request"
המודל לא יודע מה לעשות עם זה,
ולרוב ינסה שוב בדיוק אותו דבר.
✓ "התאריך שנשלח אינו תקין. נדרש
פורמט YYYY-MM-DD. קיבלתי: 9/3/26"
→ המודל מתקן ומנסה שוב, נכון.
ושתי הגבלות שחייבות להיות בקוד ולא בהנחיה: תקרה לניסיונות חוזרים — אחרת סוכן שנתקע מנסה לנצח ושורף תקציב; וזיהוי לולאה — אם אותה קריאה עם אותם פרמטרים חוזרת פעמיים ברצף, זה סימן שהמודל תקוע ועדיף לעצור ולשאול את המשתמש.
הכלל שעולה כסף אם מפספסים אותו
סוכנים מנסים שוב. זה קורה כשקריאה נכשלת בזמן, כשהתשובה לא הגיעה, וכשהמודל פשוט לא בטוח שהצעד בוצע.
המשמעות: כל כלי שמשנה מצב חייב להיות בטוח לקריאה כפולה. לשלוח מייל, לחייב, ליצור הזמנה, לפתוח פנייה — בכל אחד מאלה, קריאה שנייה חייבת לא לעשות את זה שוב.
def create_order(customer_id, items, request_id):
# request_id נוצר על ידך, לא על ידי המודל
existing = db.orders.find_by_request(request_id)
if existing:
return {"ok": True, "order_id": existing.id,
"note": "ההזמנה כבר נוצרה"}
order = db.orders.create(customer_id, items,
request_id=request_id)
return {"ok": True, "order_id": order.id}
המזהה נוצר בקוד שלך ולא על ידי המודל — הוא נגזר מהצעד בתהליך, לא ממה שהמודל החליט לשלוח. מזהה שהמודל מייצר הוא מזהה שהוא יכול לייצר אחרת בניסיון השני, וזה מבטל את כל המנגנון.
וכשאי אפשר להפוך פעולה לבטוחה — היא הולכת לסעיף הבא.
אישור אנושי: לאילו כלים
לא כל כלי צריך אישור, ואישור על הכל הופך את הסוכן לחסר ערך. הקו הפשוט: אישור לכל מה שלא ניתן לביטול או שרואים אותו מבחוץ.
- דורש אישור: תשלום או זיכוי, מחיקה, שליחת הודעה ללקוח, שינוי הרשאות, כל דבר שעולה כסף.
- לא דורש: קריאה, חיפוש, חישוב, יצירת טיוטה, כל מה שלא עוזב את המערכת.
ומה שקובע אם האישור שווה משהו הוא איך הוא מוצג. "הסוכן מבקש להפעיל את send_email — לאשר?" הוא כפתור שאנשים לוחצים עליו בלי לקרוא אחרי הפעם החמישית. מה שצריך להופיע הוא התוכן עצמו: למי, מה כתוב, וכמה זה יעלה. אישור שאפשר לקרוא בשתי שניות הוא אישור שבאמת נקרא.
וכשיש הרבה אישורים — עדיף לקבץ. סוכן שמציג בבת אחת את שלוש הפעולות שהוא עומד לעשות מקבל תשומת לב אמיתית; אחד ששואל שלוש פעמים מקבל שלוש לחיצות אוטומטיות.
ומה קורה כשהמשתמש מסרב
זה החלק שנשכח כמעט תמיד. בונים מסך אישור, מטפלים במקרה שלחצו "אשר", ושוכחים שהכפתור השני קיים גם הוא.
סירוב שמיושם כשגיאה טכנית גורם למודל לנסות שוב — לפעמים עם ניסוח מעט שונה, שיעבור הפעם כי המשתמש עייף. סירוב שפשוט עוצר את הסוכן משאיר את המשתמש בלי תשובה ובלי מושג מה כן בוצע.
הנכון הוא להחזיר את הסירוב כתוצאת כלי רגילה, ולציין במפורש שהפעולה לא בוצעה ושאין לנסות אותה שוב ללא הנחיה חדשה. ואם הממשק מאפשר למשתמש לכתוב למה — להעביר גם את זה. "לא, המחיר שגוי" הוא מידע שמאפשר למודל לתקן; סירוב בלי הסבר מאפשר לו רק לוותר.
כמה כלים זה יותר מדי
אין מספר קסם, ויש תופעה ברורה: ככל שרשימת הכלים ארוכה יותר, כך הבחירה מדויקת פחות. עם חמישה כלים מובחנים היטב, הדיוק גבוה. עם שלושים שחלקם חופפים, המודל מתחיל לבחור את הדומה במקום את הנכון.
ויש לזה גם מחיר ישיר: הגדרות הכלים נשלחות בכל קריאה. שלושים כלים מפורטים הם חלק משמעותי מההקשר, בכל צעד.
שלוש דרכים לטפל:
- לאחד כלים שחופפים. אם שניים עושים כמעט אותו דבר, המודל יתלבט ביניהם לנצח. או שתמזג או שתחדד את התיאורים כך שהגבול ברור.
- לטעון לפי שלב. לא כל הכלים רלוונטיים תמיד. סוכן בשלב בירור צריך כלי קריאה; רק כשהגיע לביצוע צריך את השאר.
- ניתוב בשתי רמות. מודל ראשון מחליט על תחום, ואז נטענים רק הכלים של אותו תחום. מוסיף קריאה וחוסך הרבה בלבול.
כשיש כמה צעדים
סוכן שמבצע שרשרת מייצר שתי בעיות שלא קיימות בקריאה בודדת.
הראשונה: הצטברות. טעות בצעד השני נכנסת להקשר, והצעד השלישי מסתמך עליה כעובדה. שרשרת של חמישה צעדים מסוכנת הרבה יותר מפי חמישה מצעד אחד. שרשראות קצרות עם נקודת עצירה באמצע בטוחות בהרבה מארוכות ברצף.
והשנייה: תכנון מוקדם מדי. מודל שמתבקש לתכנן חמישה צעדים מראש מתכנן לפי מה שהוא מניח שיחזור, ואז נצמד לתוכנית גם כשהמציאות שונה. עדיף צעד-אחר-צעד: לבצע אחד, לראות מה חזר, ולהחליט על הבא.
ושתי אופטימיזציות שכן שוות:
- קריאות במקביל כשאין תלות ביניהן — לבדוק מלאי ולשלוף פרטי לקוח בו-זמנית. חוסך המתנה אמיתית.
- מצב מחוץ למודל. מה כבר בוצע נשמר בטבלה ולא בתמליל, כפי שמפורט בזיכרון של סוכנים. זה מה שמונע ביצוע כפול אחרי ניתוק.
כלים שלוקחים זמן
רוב הדוגמאות מניחות שכלי חוזר מיד. בפועל יש כלים שלוקחים ארבעים שניות — הפקת דוח, עיבוד קובץ, קריאה למערכת חיצונית איטית — וזה שובר את הלולאה בשתי דרכים.
הראשונה היא מגבלת זמן. כלי בלי timeout בקוד יכול לתלות סוכן שלם עד שהמשתמש סוגר את החלון. תקרת זמן מפורשת, שמחזירה תוצאת שגיאה רגילה כשהיא נחצית, הופכת תקיעה לאירוע שהמודל יכול לטפל בו.
והשנייה היא שהמשתמש לא רואה כלום. ארבעים שניות של שקט נקראות כתקלה, גם כשהכל עובד.
הפתרון לשניהם זהה: לפצל לשני כלים. הראשון מתחיל את העבודה ומחזיר מזהה מיד; השני בודק מה מצבה. הסוכן מדווח למשתמש שההפקה התחילה, ממשיך לדברים אחרים, ובודק שוב בהמשך. זה גם מה שהופך ניתוק באמצע לבלתי מזיק — העבודה ממשיכה בשרת, והמזהה נשמר מחוץ לתמליל.
כלים הם משטח התקיפה
כאן ההבדל בין סוכן שקורא לסוכן שפועל נעשה משמעותי, ושתי הנקודות הבאות אינן המלצות אלא תנאים.
הרשאות נקבעות בקוד, לא בפרומפט. "אל תמחק לקוחות" בהנחיית המערכת היא בקשה, ואפשר לשכנע מודל לעקוף בקשה. אם כלי מסוכן קיים ברשימה — הוא ניתן לקריאה. הדרך היחידה למנוע היא שהמימוש יבדוק הרשאה, לפי מי המשתמש, לפני שהוא עושה משהו.
וכל טקסט שנכנס להקשר יכול להכיל הוראות. מסמך שהמשתמש העלה, דף שנשלף, מייל שנכנס, ואפילו תוצאה של כלי אחר — כולם יכולים להכיל טקסט שמנסה להשפיע על הצעד הבא. זה נקרא prompt injection, וההגנה היא אותה הגנה: המודל מציע, הקוד מחליט.
שלוש פעולות מעשיות: להגביל היקף — כלי שמחפש לקוחות מחפש רק בלקוחות של המשתמש הנוכחי; להגביל קצב — סוכן בלולאה לא אמור לשלוח מאה מיילים; ולתעד כל קריאה עם הפרמטרים, כי בלי זה אין דרך לדעת מה קרה. הרחבה: אבטחת סוכנים.
כשהכלים לא שלך
יותר ויותר כלים מגיעים מוכנים דרך MCP — פרוטוקול שמאפשר לחבר שרת כלים חיצוני לסוכן. זה חוסך מימוש, ומוסיף שני שיקולים.
התיאורים אינם שלך. מה שכתוב בהגדרת הכלי הוא מה שהמודל יקרא, ואם הוא מנוסח גרוע — הדיוק ייפגע ואין לך שליטה. שווה לבדוק את התיאורים לפני שמחברים שרת שלם.
וזה מרחיב את משטח התקיפה. שרת כלים חיצוני הוא קוד שרץ ומחזיר טקסט שנכנס להקשר שלך. אותו כלל מהסעיף הקודם תקף במלואו, ובתוספת: לחבר רק שרתים שאתה סומך על מי שמפעיל אותם.
למדוד את הכלים בנפרד
"הסוכן עובד" לא מספיק. שלושה מדדים שמפרידים בין בעיות שונות:
- דיוק בחירת הכלי. בהינתן מצב, האם נבחר הכלי הנכון. כאן תיאורים חלשים מתגלים.
- דיוק הפרמטרים. נבחר הכלי הנכון — האם הערכים תקינים. כאן תיאורי פרמטרים מתגלים.
- שיעור כשל בקריאות. כמה קריאות נכשלו, ומה היה אחרי — האם המודל התאושש או נתקע.
והדרך לבנות את זה: עשרים עד שלושים מצבים אמיתיים עם הקריאה הנכונה מסומנת. אותה גישה שמפורטת בEvals, ובלעדיה כל שינוי בתיאור כלי הוא ניחוש.
תיאור כלי הוא קוד
הנקודה האחרונה במדידה היא גם התרבותית שבהן: שינוי מילה בתיאור כלי הוא שינוי התנהגות של המערכת, בדיוק כמו שינוי בתנאי if.
בפועל התיאורים נערכים כאילו הם הערות. מישהו מחדד ניסוח, ושבוע אחרי מגלים שהסוכן הפסיק לקרוא לכלי מסוים במצב שבו קרא לו קודם. בלי מדידה אין דרך לקשר בין השניים, כי השינוי לא הופיע באף בדיקה.
שלושה הרגלים שמונעים את זה: לשמור את התיאורים בגרסת מקור ולא בממשק ניהול שאין לו היסטוריה; להריץ את מערך המצבים אחרי כל עריכת תיאור, גם כשנראה שזו רק הבהרה; ולשנות דבר אחד בכל פעם — שני תיאורים שנערכו יחד מייצרים תוצאה שאי אפשר לייחס לאף אחד מהם.
מתי לא צריך כלים בכלל
- כשהתהליך ידוע מראש. אם תמיד מבצעים א׳ ואז ב׳ ואז ג׳ — זו אוטומציה רגילה. אל תיתן למודל להחליט על משהו שכבר הוחלט.
- כשיש תשובה אחת נכונה. חישוב, שליפה לפי מזהה, בדיקת תנאי. קוד עושה את זה בוודאות.
- כשכל כלי דורש אישור. אז אין סוכן — יש ממשק מסובך. עדיף טופס.
- לפני שיש הערכות. סוכן עם כלים בלי דרך למדוד אם הוא בוחר נכון הוא מערכת שלא תוכל לשפר.
טעויות שחוזרות
- תיאור שנכתב למפתח. המודל צריך לדעת מתי, לא איך.
- כלי אחד עם פרמטר action. מכפיל את מספר ההחלטות ומונע הפרדת הרשאות.
- טקסט חופשי במקום רשימה סגורה. הזמנה להמצאת ערכים.
- כלי שמחזיר הכל. מדלל את ההקשר ועולה בכל צעד.
- חריגה שלא נתפסה. עוצרת את הסוכן באמצע, עם צעדים שכבר בוצעו.
- הודעת שגיאה טכנית. המודל ינסה שוב בדיוק אותו דבר.
- בלי מפתח ייחודיות. הדרך שבה סוכן מחייב פעמיים.
- כלי איטי בלי תקרת זמן. תולה את הסוכן עד שהמשתמש מוותר.
- סירוב שמטופל כשגיאה. המודל ינסה שוב, והפעם ינוסח יפה יותר.
- עריכת תיאור בלי הרצת בדיקות. שינוי התנהגות שאיש לא שם לב אליו.
- הרשאות בהנחיה במקום בקוד. בקשה שאפשר לשכנע מודל לעקוף.