7.5 דפוסים מתקדמים הרצאה
עד כאן בפרק בנינו את הבסיס: קריאת API, שימוש בtools, agent מלא ושרת MCP. אלה אבני הבניין. אבל כשעוברים מדוגמה למערכת ייצור אמיתית, צצות בעיות חדשות. איך נותנים למודל לענות על שאלות מתוך מסמכים שהוא מעולם לא ראה? איך חוסכים כסף כשאותו context גדול נשלח שוב ושוב? איך מקבלים פלט בפורמט שהקוד שלנו יכול לסמוך עליו? ואיך בכלל מודדים אם התשובות של המודל טובות, בקנה מידה? בהרצאה הזו נכיר ארבעה דפוסים מתקדמים שעונים בדיוק על השאלות האלה: RAG, מטמון prompts, פלט מובנה, ו-LLM כשופט. כל אחד מהם פותר בעיה נפוצה, וביחד הם ההבדל בין הדגמה למוצר.
הretrieval מוגבר - RAG¶
למודל יש ידע רק עד תאריך האימון שלו, והוא לא מכיר את המסמכים הפרטיים שלנו - מדיניות החברה, תיעוד פנימי, repository תמיכה. RAG (ראשי תיבות של Retrieval-Augmented Generation, כלומר יצירה מוגברת-אחזור) פותר את זה: במקום לצפות שהמודל "ידע", אנחנו מאחזרים את המידע הרלוונטי מתוך repository שלנו, ומדביקים אותו לבקשה כcontext.
הזרימה בשלושה שלבים:
1. Indexing (in advance): split the documents into chunks, convert each chunk into a vector (embedding), and store it
2. Retrieval (at query time): convert the question into a vector, and find the most similar chunks
3. Generation: inject the found chunks into the request, and the model answers from them
בפועל, החלק של הretrieval נעשה עם בסיס נתונים וקטורי, אבל הרעיון פשוט - נמצא את הקטעים הרלוונטיים ונשים אותם בprompt:
import anthropic
client = anthropic.Anthropic()
# assume we already retrieved the relevant chunks from a vector store
retrieved_chunks = [
"מדיניות ההחזרות: ניתן להחזיר מוצר תוך 30 יום מהרכישה.",
"החזר כספי מתבצע לאמצעי התשלום המקורי תוך 14 ימי עסקים.",
]
context = "\n\n".join(retrieved_chunks)
question = "כמה זמן יש לי להחזיר מוצר?"
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system=(
"ענה על שאלות המשתמש אך ורק על סמך ההקשר שלהלן. "
"אם התשובה לא בהקשר, אמור שאינך יודע.\n\n" + context
),
messages=[{"role": "user", "content": question}],
)
מתי RAG? כשהתשובות תלויות במידע שאינו בידע המודל, ומשתנה לעיתים קרובות מכדי לאמן עליו. הprompt "ענה רק על סמך הcontext" חשובה - היא מצמצמת hallucinations ומחייבת את המודל להיצמד למקורות.
איך בעצם עובד שלב האינדוקס? חותכים כל מסמך לקטעים קטנים (chunks) - למשל פסקה או שתיים. כל קטע עובר דרך מודל embedding שממיר טקסט לוקטור של מספרים, כך שקטעים בעלי משמעות דומה מקבלים וקטורים קרובים. את הוקטורים שומרים בבסיס נתונים וקטורי. בזמן השאלה, ממירים את השאלה לוקטור באותו אופן, ומחפשים את הקטעים שהוקטור שלהם הכי קרוב - אלה הקטעים הרלוונטיים. כך המערכת "מבינה" ששאלה על "החזרת מוצר" רלוונטית לקטע על "מדיניות החזרות", גם אם המילים לא זהות.
מטמון prompts - prompt caching¶
ב-RAG, ובכל אפליקציה עם prompt מערכת גדולה, אנחנו שולחים את אותו context גדול שוב ושוב. כל בקשה משלמת מחדש על עיבוד אותם tokens. מטמון prompts פותר את זה: מסמנים חלק מהבקשה כניתן-למטמון, וה-API שומר אותו. בבקשות הבאות, החלק המשותף מוגש מהמטמון - זול פי עשרה בערך, ומהיר יותר.
העיקרון החשוב ביותר: המטמון עובד לפי התאמת קידומת (prefix). כל שינוי בבייט אחד בקידומת מבטל את כל המטמון שאחריו. לכן שמים תוכן יציב בהתחלה (prompt מערכת קבועה, מסמכים), ותוכן משתנה (שאלת המשתמש, חותמות זמן) בסוף.
השימוש הפשוט - הוספת cache_control:
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system=[
{
"type": "text",
"text": large_document, # the large context - stable and cacheable
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": "סכם את הנקודות המרכזיות"}],
)
# check whether the cache worked
print(response.usage.cache_creation_input_tokens) # written to the cache (on the first request)
print(response.usage.cache_read_input_tokens) # read from the cache (on subsequent ones)
בבקשה הראשונה נראה ערך ב-cache_creation_input_tokens (כתיבה, קצת יותר יקר). בבקשות הבאות נראה ערך ב-cache_read_input_tokens (קריאה, זול מאוד). אם cache_read_input_tokens נשאר אפס למרות קידומת זהה - יש "מבטל שקט" בקידומת, כמו datetime.now() בprompt המערכת. מטמון חוסך עד 90% מעלות הcontext החוזר.
כמה דגשים מעשיים. סדר העיבוד הוא tools ואז system ואז messages - לכן ככל שנשים תוכן יציב מוקדם יותר, כך יותר ייכנס למטמון. יש מינימום tokens שמתחתיו כלום לא נשמר (בערך 1024 עד 4096, תלוי במודל). וברירת המחדל של המטמון היא חמש דקות; אם התעבורה שלנו "רצופה" (בקשות בפער קצר), המטמון נשאר חם מעצמו. אלה בדיוק המקרים - RAG עם מסמך גדול, צ'אט עם היסטוריה מצטברת, agent עם prompt מערכת קבועה - שבהם מטמון הופך את ההבדל בין מערכת יקרה למערכת כלכלית.
פלט מובנה - structured output¶
כשהמודל מחזיר טקסט חופשי, הקוד שלנו צריך "לנחש" איך לפרסר אותו - וזה שביר. אם אנחנו בונים מערכת שמחלצת נתונים (שם, אימייל, סכום), אנחנו צריכים פלט מובנה: להכריח את המודל להחזיר JSON שתואם בדיוק לסכמה שהגדרנו. אז הקוד יכול לסמוך על המבנה.
הדרך הנוחה בפייתון היא עם Pydantic ו-messages.parse, שמאמת את התשובה מול הסכמה אוטומטית:
from pydantic import BaseModel
import anthropic
class ContactInfo(BaseModel):
name: str
email: str
plan: str
demo_requested: bool
client = anthropic.Anthropic()
response = client.messages.parse(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{
"role": "user",
"content": "חלץ: יעל כהן (yael@co.com) רוצה מנוי Enterprise ומעוניינת בהדגמה.",
}],
output_format=ContactInfo,
)
contact = response.parsed_output # a validated ContactInfo object
print(contact.name) # "יעל כהן"
print(contact.demo_requested) # True
לחלופין, בלי Pydantic, מגדירים סכמה גולמית דרך output_config:
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": "חלץ שם ואימייל: דני לוי, dani@example.com"}],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
},
"required": ["name", "email"],
"additionalProperties": False,
},
}
},
)
הפלט המובנה מבטיח שהתשובה תהיה JSON תקין שתואם לסכמה - סוף לפרסור שביר. שימושי במיוחד בחילוץ מידע, סיווג (עם שדה enum), ובכל מקום שהפלט צריך להיכנס למערכת אחרת.
LLM כשופט - LLM-as-judge¶
בנינו מערכת שמייצרת תשובות. איך נדע אם הן טובות? לבדוק ידנית אלפי תשובות זה בלתי אפשרי. הרעיון של LLM כשופט הוא להשתמש במודל עצמו כדי להעריך פלטים - נותנים לו את הקלט, את הפלט, ורובריקה (קריטריונים), והוא נותן ציון. זה מאפשר להעריך איכות בקנה מידה גדול.
הטריק: מבקשים מהשופט להחזיר פלט מובנה (משלב שני דפוסים!) כדי שנוכל לעבד את הציונים בקוד:
from pydantic import BaseModel
import anthropic
class Judgment(BaseModel):
score: int # 1 to 5
reasoning: str # explanation for the score
is_grounded: bool # whether the answer is grounded in the source
client = anthropic.Anthropic()
def judge_answer(question, answer, source):
response = client.messages.parse(
model="claude-opus-4-8",
max_tokens=1024,
system=(
"אתה שופט מומחה. הערך את התשובה לפי הקריטריונים: "
"דיוק, רלוונטיות, והאם היא נשענת רק על המקור. "
"תן ציון 1 עד 5."
),
messages=[{
"role": "user",
"content": (
f"שאלה: {question}\n\n"
f"מקור: {source}\n\n"
f"תשובה שיש להעריך: {answer}"
),
}],
output_format=Judgment,
)
return response.parsed_output
verdict = judge_answer(
question="כמה זמן יש להחזיר מוצר?",
answer="ניתן להחזיר תוך 30 יום.",
source="מדיניות ההחזרות: 30 יום מהרכישה.",
)
print(verdict.score) # 5
print(verdict.is_grounded) # True
כמה עצות: כדאי לתת רובריקה מפורשת וברורה (קריטריונים שאפשר להעריך בנפרד), ולעיתים כדאי שהשופט יהיה מודל חזק (למשל Opus) גם אם המערכת שנבדקת רצה על מודל זול יותר. LLM כשופט שימושי לבדיקות אוטומטיות (evals), לסינון תשובות גרועות לפני שהן מגיעות למשתמש, ולהשוואת גרסאות של המערכת.
כשמריצים שופט על הרבה דוגמאות - למשל מאות תשובות במערך בדיקות - כדאי להשתמש ב-Batch API, נקודת קצה שמעבדת בקשות באופן אסינכרוני בחצי מהמחיר. שולחים את כל בקשות השיפוט בבת אחת, ממתינים שהאצווה תסתיים (בדרך כלל תוך שעה), ואוספים את התוצאות. כך אפשר להריץ מערך הערכה שלם בזול. עוד טיפ: כדי להימנע מהטיה, לא כדאי שהשופט יראה איזה גרסה של המערכת ייצרה כל תשובה - נותנים לו רק את התשובה עצמה מול הרובריקה.
חיבור הדפוסים - putting it together¶
ארבעת הדפוסים לא חיים בנפרד - הם מתחברים למערכת אחת:
| דפוס | הבעיה שהוא פותר |
|---|---|
| RAG | המודל לא מכיר את המידע הפרטי/העדכני שלנו |
| מטמון prompts | עלות גבוהה של context גדול שחוזר |
| פלט מובנה | פרסור שביר של טקסט חופשי |
| LLM כשופט | הערכת איכות בקנה מידה גדול |
מערכת RAG טיפוסית לייצור משלבת את כולם: מאחזרת קטעים רלוונטיים (RAG), שולחת אותם בcontext עם מטמון כדי לחסוך, מבקשת פלט מובנה כדי שהתשובה תיכנס למערכת, ומריצה שופט על מדגם תשובות כדי לוודא שהאיכות נשמרת. זה בדיוק המסע מהדגמה למוצר.
מתי לא להשתמש - when not to¶
דפוסים מתקדמים הם tools, לא חובה. כל אחד מוסיף מורכבות, ולכן משתמשים בו רק כשהבעיה שהוא פותר קיימת באמת:
- RAG מיותר אם כל המידע נכנס בקלות לcontext של הבקשה - אז פשוט שולחים אותו ישירות, בלי בסיס נתונים וקטורי. RAG משתלם כשהrepository גדול מכדי לשלוח כולו.
- מטמון prompts לא עוזר אם הקידומת משתנה בכל בקשה, או אם היא קצרה מהמינימום - אז משלמים על כתיבה בלי אף קריאה. הוא משתלם רק כשיש תוכן יציב וגדול שחוזר.
- פלט מובנה מגביל את המודל, ולכן לא משתמשים בו לתשובות חופשיות שמיועדות לקריאה אנושית - רק כשהפלט נכנס לקוד.
- LLM כשופט עולה כסף וזמן, ולכן מריצים אותו על מדגם או במערך בדיקות, לא על כל תשובה בזמן אמת (אלא אם הסינון קריטי).
הכלל: מתחילים פשוט - קריאת API ישירה - ומוסיפים דפוס רק כשמדידה מראה שהוא נחוץ. מורכבות מוקדמת מדי היא מלכודת נפוצה.
סיכום¶
בהרצאה הזו הכרנו ארבעה דפוסים מתקדמים שהופכים אפליקציית AI פשוטה למערכת ייצור:
- RAG (הretrieval מוגבר) - מאחזרים מידע רלוונטי מהrepository שלנו ומזריקים אותו לבקשה כcontext, במקום לצפות שהמודל "ידע". מנחים אותו לענות רק על סמך הcontext כדי לצמצם hallucinations.
- מטמון prompts - מסמנים תוכן יציב עם
cache_controlכדי לחסוך עד 90% מעלות הcontext החוזר. עובד לפי התאמת קידומת: תוכן יציב בהתחלה, משתנה בסוף. בודקים עםcache_read_input_tokens. - פלט מובנה - מכריחים את המודל להחזיר JSON תואם-סכמה, דרך
messages.parseעם Pydantic אוoutput_config. סוף לפרסור שביר; מושלם לחילוץ וסיווג. - LLM כשופט - משתמשים במודל להערכת פלטים לפי רובריקה, ומחזירים את הפסק כפלט מובנה כדי לעבד אותו בקוד. מאפשר הערכת איכות (evals) בקנה מידה.
- החיבור: מערכת ייצור אמיתית משלבת את כל הדפוסים - RAG לretrieval, מטמון לחיסכון, פלט מובנה לאמינות, ושופט לאיכות.
בזה סוגרים את הפרק. עברנו מלהיות משתמשים של tool AI לבנות אותם: מהבקשה הראשונה, דרך tools, agents ושרתי MCP, ועד הדפוסים המתקדמים של מערכות ייצור. יש לכם עכשיו את אבני הבניין לבנות אפליקציות AI אמיתיות משלכם.