Structured Output

למה טקסט חופשי לא תמיד מספיק

בכל הדוגמאות עד כה קיבלנו את תשובת ה-agent כטקסט חופשי — result.messages.at(-1)?.content. זה מצוין לצ'אט מול משתמש אנושי, אבל ברגע שקוד אחר צריך לצרוך את התשובה — לצייר כרטיס ב-UI, לשמור בבסיס נתונים, או להעביר לפונקציה אחרת — טקסט חופשי הוא מקור בעיות: הניסוח יכול להשתנות מעט בכל הרצה, ופענוח שלו עם regex או parsing ידני הוא שביר ולא אמין.

פלט מובנה (structured output) פותר את זה: במקום טקסט, ה-agent מחזיר אובייקט שתואם סכימה שהגדרתם מראש, מאומת אוטומטית — בדיוק כמו שקבלת JSON תקין מ-API הרבה יותר נוח לעבודה מאשר לפרסר תשובה בשפה טבעית.

הגדרת פלט מובנה עם responseFormat ו-zod

מעבירים ל-createAgent שדה responseFormat עם סכימת zod שמתארת את צורת התשובה הסופית הרצויה. ה-agent עדיין מריץ את הלולאה האגנטית הרגילה (tool calls וכו') כרגיל, אבל בסיום הוא מחזיר גם result.structuredResponse — אובייקט שכבר תואם את הסכימה שהגדרתם.

TypeScript
import { z } from "zod";

const WeatherReport = z.object({
  city: z.string(),
  temperatureCelsius: z.number(),
  recommendation: z.string(),
});

const agent = createAgent({
  model: "anthropic:claude-sonnet-5",
  tools: [getWeather],
  responseFormat: WeatherReport,
});

const result = await agent.invoke({
  messages: [{ role: "user", content: "מה מזג האוויר בתל אביב?" }],
});

console.log(result.structuredResponse);
// → { city: "תל אביב", temperatureCelsius: 22, recommendation: "..." }

במה זה שונה מ-tool עם zod schema

בפרק הקודם ראינו tool עם schema של zod — זו הגדרה של פעולה שהמודל יכול לבחור להפעיל תוך כדי הלולאה, ורק אם הוא מחליט שהוא זקוק לה. responseFormat הוא דבר שונה לגמרי: זו לא פעולה אופציונלית, אלא אילוץ על צורת התשובה הסופית שה-agent חייב להחזיר בסיום כל הרצה, בלי קשר לכמה tool calls היו בדרך.

שני המנגנונים חולקים את אותה שפת אימות (zod), אבל את תפקידים שונים לגמרי: tool הוא יכולת שהמודל בוחר להשתמש בה; responseFormat הוא חוזה שהתשובה הסופית חייבת לקיים.

מה קורה כשהפלט לא תואם את הסכימה

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

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

דוגמה מלאה: agent שמחזיר החלטה מובנית

בדוגמה הבאה ה-agent בודק את ההזמנות של המשתמש (עם getUserOrders מהפרק הקודם) ומחזיר החלטה מובנית — approved ו-reason — במקום פסקת טקסט חופשי שהקוד שלנו היה צריך לפענח כדי לדעת אם לאשר את הבקשה.

TypeScript
const OrderDecision = z.object({
  approved: z.boolean(),
  reason: z.string(),
});

const agent = createAgent({
  model: "anthropic:claude-sonnet-5",
  tools: [getUserOrders],
  responseFormat: OrderDecision,
  systemPrompt:
    "בדוק אם למשתמש יש הזמנה פתוחה מעל 500 ש\"ח, ואשר או דחה בקשת זיכוי בהתאם.",
});

const result = await agent.invoke(
  { messages: [{ role: "user", content: "אפשר לאשר לי זיכוי על ההזמנה האחרונה?" }] },
  { configurable: { thread_id: "chat-3", userId: "user-42" } }
);

if (result.structuredResponse.approved) {
  await issueRefund(result.structuredResponse.reason);
}