Structured Outputs — איך גורמים למודל AI להחזיר JSON אמין
מאת צוות מדיה דיל · 09.08.2026 · AI · 9 דק׳
מודל שכותב טקסט חופשי הוא נחמד להדגמה, אבל מערכת פרודקשן צריכה JSON תקין בכל קריאה. מדריך למנגנוני Structured Outputs, Schema constraints ולמה הם לא פתרון קסם.
בדיקה ראשונית של פיצ'ר AI נראית תמיד מבטיחה: שולחים פרומפט, מבקשים "תחזיר לי JSON עם שדות X ו-Y", ומקבלים תשובה נקייה. ואז, אחרי כמה מאות קריאות בפרודקשן, מגיע היום שבו המודל מחזיר JSON עם פסיק עודף בסוף מערך, או עוטף את התשובה במשפט הסבר לפני ה-JSON, או משנה את שם השדה מ-"user_id" ל-"userId" סתם כי זה נראה לו יותר טבעי באותו הקשר. קוד שמריץ JSON.parse על הפלט קורס, וכל שרשרת האוטומציה שנשענת על המבנה הזה נופלת יחד איתו. Structured Outputs הוא שם כולל למשפחת טכניקות שנועדו לפתור בדיוק את זה: לגרום למודל להחזיר פלט שתואם סכמה מוגדרת מראש, בכל פעם, בלי חריגות.
שלוש רמות של אמינות מבנית
לא כל "Structured Output" נותן את אותה רמת ביטחון. שווה להבחין בין שלוש גישות שונות מהותית:
- Prompt-based formatting - פשוט מבקשים מהמודל "תחזיר JSON" בהוראות הטקסט. זו הגישה החלשה ביותר: היא עובדת ברוב המקרים, אבל אין שום הבטחה טכנית - המודל עדיין "מנחש" איך להיראות תקין, ובמקרי קצה (הקשר ארוך, נושא לא שגרתי, טמפרטורה גבוהה) הוא סוטה.
- JSON mode - חלק מהספקים תומכים במצב שמבטיח פלט שהוא JSON תקין תחבירית (Syntax), אבל לא מבטיח שהוא תואם לסכמה ספציפית - אין הבטחה ששדה מסוים יופיע, או שהוא יהיה מהסוג הנכון.
- Schema-constrained generation - הרמה החזקה ביותר: מעבירים לספק סכמת JSON Schema מפורשת (שדות, טיפוסים, שדות חובה, Enum-ים), והמודל מוגבל ברמת ה-Constrained Decoding - כלומר בכל שלב של יצירת הטוקן הבא, רק טוקנים שתואמים את הסכמה נשקלים בכלל. זו לא "בקשה נחמדה" אלא אילוץ טכני על תהליך ה-Sampling עצמו.
איך Constrained Decoding עובד מתחת למכסה המנוע
ברמה הבסיסית, מודל שפה מייצר טקסט טוקן אחר טוקן, כשבכל שלב הוא מחשב התפלגות הסתברות על פני כל אוצר המילים האפשרי ובוחר (או דוגם) את הבא. Constrained decoding מוסיף שכבת מסנן (mask) שמאפסת את ההסתברות של כל טוקן שלא יכול להוביל לפלט תקין לפי הסכמה - למשל, אם הסכמה מגדירה שדה enum עם שלושה ערכים אפשריים, ברגע שהמודל מתחיל לכתוב את הערך של השדה, רק טוקנים שמתחילים באחד משלושת המחרוזות המותרות נשארים בהתפלגות. התוצאה: מבחינה מבנית, פשוט אי אפשר למודל לייצר JSON לא תקין - זה לא עניין של "הוא בדרך כלל מצליח", אלא אילוץ קשיח ברמת מנגנון היצירה.
חשוב להבין את המגבלה: האילוץ הזה מבטיח תקינות מבנית (Syntax + Schema conformance), אבל לא מבטיח נכונות סמנטית. מודל יכול להחזיר JSON תקין לחלוטין עם שדה "amount" שמכיל מספר שגוי לחלוטין ביחס לשאלה שנשאלה. הסכמה שומרת על הצורה, לא על התוכן.
דוגמה לסכמה מוגבלת
{
"type": "object",
"properties": {
"intent": { "type": "string", "enum": ["refund", "complaint", "info"] },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
"extracted_amount": { "type": ["number", "null"] }
},
"required": ["intent", "confidence"],
"additionalProperties": false
}
מה קורה כשהמודל לא מצליח למלא שדה חובה
סכמה עם שדות חובה מעלה שאלה שלא תמיד ברורה מיד: מה קורה כשהמידע הדרוש לשדה פשוט לא קיים בקלט? אם משתמש שואל שאלה כללית ומבקשים מהמודל לחלץ "תאריך אירוע" כשדה חובה, אבל אין תאריך בטקסט בכלל - Constrained decoding עדיין יכריח את המודל להחזיר משהו בשדה הזה, כי הסכמה דורשת את קיומו. זה בדיוק המקום שבו עיצוב סכמה נכון קובע את איכות התוצאה: שדות שיכולים להיות ריקים לגיטימית צריכים להיות מוגדרים כ-nullable מפורשות ("type": ["string", "null"]), לא כחובה גורפת. סכמה שמכריחה ערך במקום שבו אין מידע אמיתי היא הזמנה להזיה - המודל "ממלא" משהו סביר-לכאורה רק כי הסכמה דורשת ממנו.
עיקרון דומה חל על שדה "confidence" או "reasoning" - הוספת שדה שמבקש מהמודל לדרג את הביטחון שלו בתשובה, או להסביר בקצרה איך הגיע אליה, יכולה לשמש כאיתות מוקדם לזיהוי מקרים שבהם כדאי ולידציה נוספת או הסלמה אנושית, בלי לשנות את הסכמה העסקית המרכזית.
הקשר ל-Function Calling
Structured Outputs ו-Function Calling חולקים את אותו מנגנון בסיסי - שניהם דורשים מהמודל להחזיר פלט שתואם סכמה מוגדרת. בפועל, הרבה ספקים מיישמים Structured Outputs באמצעות אותה תשתית שמיישמת קריאה לכלי: מגדירים "כלי" וירטואלי שהוא בעצם הסכמה הרצויה, "כופים" על המודל לקרוא לו, ומקבלים בחזרה את הארגומנטים כפלט המובנה. ההבדל המעשי הוא כוונתי: Function Calling נועד להפעיל פעולה בעולם האמיתי, Structured Outputs נועד רק לעצב את צורת התשובה.
ביצועים: מה המחיר בפועל של האכיפה המבנית
שווה להתייחס גם לעלות הביצועית של Constrained Decoding, כי היא לא תמיד זניחה. חישוב המסכה (mask) על אוצר המילים בכל שלב יצירת טוקן מוסיף תקורה חישובית, שיכולה להיות משמעותית יותר עבור סכמות מורכבות (הרבה שדות מקוננים, Enum-ים גדולים) לעומת סכמות פשוטות. ברוב המקרים המעשיים, התוספת הזו קטנה יחסית לזמן היצירה הכולל ולא מורגשת, אבל בעומסי פרודקשן גבוהים במיוחד, שבהם כל מילישנייה נספרת, שווה למדוד אותה בפועל ולא להניח שהיא זניחה אוטומטית. גורם נוסף שמשפיע על latency הוא עומק הקינון בסכמה - סכמה עם כמה רמות של אובייקטים מקוננים בתוך מערכים דורשת מעקב מצב (state tracking) מורכב יותר לאורך תהליך היצירה, מה שמאט מעט את קצב הפקת הטוקנים ביחס לסכמה שטוחה עם אותה כמות שדות.
למה זה לא פותר את כל הבעיה
גם עם Schema-constrained generation מלא, נשארות כמה בעיות שהאילוץ המבני לא נוגע בהן:
- הזיות בתוך שדה תקין - שם, תאריך או מספר שממציא המודל, אבל תואם טיפוס ופורמט. צריך שכבת ולידציה עסקית נפרדת, ולעיתים מודל מאמת ייעודי.
- סכמות מורכבות מדי מאטות ומבלבלות - סכמה עם עשרות שדות מקוננים, Enum-ים ארוכים ותנאים הדדיים (conditional required) יכולה להאט משמעותית את הפקת הפלט ואף לגרום למודל "להיתקע" בניסיון לספק ערך שלא ברור לו. כלל אצבע: לשמור על סכמות שטוחות ומינימליות ככל האפשר.
- לא כל ספק תומך באותה רמת אכיפה - יש הבדלים משמעותיים בין ספקים ברמת התמיכה ב-Constrained Decoding אמיתי מול JSON mode חלש יותר, וזה משפיע ישירות על כמה אפשר "לסמוך" על הפלט בלי ולידציה נוספת.
עיצוב סכמה כתהליך איטרטיבי, לא החלטה חד פעמית
סכמה טובה כמעט אף פעם לא נכונה מהנסיון הראשון. הדרך המעשית לבנות אותה היא איטרטיבית: מתחילים בסכמה מינימלית, מריצים אותה על מדגם מייצג של קלטים אמיתיים (כולל מקרי קצה - קלטים חסרים, דו-משמעיים, ארוכים במיוחד), ובודקים לא רק אם הפלט תקין מבחינה תחבירית אלא אם הוא שימושי. לעיתים קרובות מתגלה שצריך לפרק שדה אחד לשניים (למשל "כתובת" שצריך להיות רחוב, עיר ומיקוד נפרדים כדי שהמערכת שמקבלת את הפלט תוכל לעבד אותו), או להוסיף Enum במקום string חופשי כדי לצמצם שונות מיותרת. תהליך העדכון הזה צריך לרוץ תחת אותה משמעת של Evals שחלה על שאר המערכת - שינוי בסכמה הוא שינוי בהתנהגות המערכת, וצריך להיבדק כזה.
שווה גם לתעד מפורשות בקוד את הגרסה של הסכמה שכל רשומה נוצרה איתה, כי מערכות פרודקשן ארוכות טווח משנות סכמות עם הזמן, וללא תיוג ברור אי אפשר לדעת בבטחה איך לפענח רשומות ישנות שנוצרו לפי מבנה קודם - זו בעיה קלאסית של Schema versioning, לא ייחודית ל-AI אבל קריטית לא פחות כשמדובר בפלט שנוצר על ידי מודל ולא נכתב ידנית.
שכבת ולידציה - עדיין הכרחית
גם עם Structured Outputs ברמה הגבוהה ביותר, פרודקשן רציני עדיין מריץ ולידציה נוספת אחרי קבלת התשובה: בדיקת טווחי ערכים סבירים, בדיקת עקביות בין שדות (למשל תאריך סיום אחרי תאריך התחלה), ובמקרים רגישים - בקרה אנושית לפני ביצוע פעולה בלתי הפיכה. הסכמה מבטיחה שהצורה תקינה; הלוגיקה העסקית צריכה לוודא שהתוכן הגיוני. שילוב טוב הוא גם עם Guardrails שבודקים את הפלט המובנה מול כללי מדיניות עסקיים לפני שהוא ממשיך הלאה בצינור.
דוגמה מהשטח: חילוץ נתונים ממסמכים
מערכת שמחלצת פרטי חשבונית ממסמך סרוק צריכה להחזיר תמיד מבנה קבוע - מספר חשבונית, סכום, תאריך, שם ספק - בלי קשר לאיך המסמך המקורי מעוצב. בלי Structured Outputs, כל וריאציה בפורמט המסמך עלולה לגרום למודל להחזיר טקסט חופשי או מבנה שונה מעט, מה שדורש Parsing מבוסס Regex שביר. עם סכמה קשיחה, כל תשובה מגיעה במבנה זהה, גם אם ערך מסוים חסר (במקרה כזה השדה יסומן כ-null במקום שהמודל "יאלתר" משהו). זה בדיוק סוג המקרה שבו Document Intelligence נשען כמעט לחלוטין על אמינות הפלט המובנה.
Structured Outputs וזרימת סטרימינג
נקודה טכנית שכדאי להכיר: כשמפיקים תשובה בסטרימינג (טוקן אחר טוקן, מוצג למשתמש בזמן אמת), פלט JSON מובנה מציב אתגר - אי אפשר לפרסר JSON חלקי ולא תקין באמצע הפקתו. הפתרון הנפוץ הוא Parsing חלקי מיוחד (partial JSON parsing) שיודע לזהות אילו שדות כבר "נסגרו" והפכו שלמים תוך כדי הזרמה, ולעדכן את הממשק בהתאם - למשל להציג שדה שכבר הושלם גם אם שדות אחרים בסכמה עדיין בתהליך יצירה. זה שימושי בעיקר בממשקי משתמש שרוצים להראות התקדמות בזמן אמת (כמו טופס שמתמלא שדה אחר שדה), אבל דורש תשתית קליינט שתומכת בפענוח הדרגתי כזה.
שאלות נפוצות
האם Structured Outputs מבטיח שהמודל לא "יטעה" בתוכן?
לא. הוא מבטיח שהתשובה תהיה במבנה הנכון (טיפוסים, שדות חובה), אבל לא שהערכים עצמם נכונים. תמיד צריך ולידציה עסקית נוספת על התוכן.
מה ההבדל בין JSON mode לבין Schema-constrained generation?
JSON mode מבטיח תחביר JSON תקין בלבד. Schema-constrained generation אוכף גם את מבנה השדות, הטיפוסים והערכים המותרים - ברמת מנגנון היצירה עצמו.
האם כדאי להשתמש בסכמות מורכבות עם הרבה תתי-אובייקטים?
עדיף להימנע. סכמות שטוחות ופשוטות יותר נותנות תוצאות אמינות יותר ומהירות יותר. אם צריך מבנה מורכב, שקלו לפצל לכמה קריאות נפרדות.
האם צריך עדיין לטפל בשגיאות Parsing?
כן, גם אם הן נדירות בהרבה. תמיד צריך Fallback למקרה של כשל API, Timeout, או מקרה קצה שבו הספק לא עומד בהתחייבות המבנית.
האם Structured Outputs מתאים גם ליצירת תוכן חופשי כמו מאמר?
פחות. הוא מיועד למקרים שבהם צריך פלט הניתן לעיבוד מכני. לתוכן חופשי לקריאה אנושית, אילוץ מבני מיותר ולעיתים אף פוגע באיכות הכתיבה.
שכבת פלט מובנה ואמין היא תשתית קריטית בכל מערכת AI שמזינה מערכות נוספות במורד הזרם. אם אתם בונים אינטגרציה כזו ורוצים לוודא שהיא עמידה לפרודקשן, צוות מדיה דיל בונה פתרונות AI עם שכבות ולידציה מלאות - נשמח לדבר על זה בוואטסאפ.
תגיות: Structured Outputs · JSON Schema · Function Calling · Constrained Decoding · LLM Output · Validation