ה-cache הוא השיחה
למה agents מרגישים טבעיים — ומה אנשים באמת מתכוונים כשהם אומרים caching בעולם החדש הזה.
עשיתי prototype לפיצ’ר Q&A שמחפש על פני ספריית תוכן. הגרסה הראשונה הייתה pipeline ישר: כל שאלה הפעילה קריאת routing כדי להבין אילו מקורות רלוונטיים, הביאה את התוכן שלהם, ואז ביקשה מהמודל לענות. כל שאלה. גם ה-follow-ups.
התיקון הוא לא יותר קוד. הוא פחות קוד עם יותר החלטות מהמודל. וברגע שאת רואה למה, המילה “agent” מפסיקה להישמע כמו שיווק.
מה ה-prototype עשה
ה-pattern הקלאסי: לקודד את השלבים, להריץ את כולם, בכל פעם.
שאלה → routing → טעינת מקורות → תשובה
זה בסדר כשכל שלב זול. זה הופך לבזבזני כשאחד השלבים הוא “לבקש ממודל שפה לקבל החלטה,” וההחלטה כמעט לא משתנה בתוך שיחה.
תחשבי על לשאול חברה שזה עתה קראה ספר: “מה אומר פרק שלוש על הנושא הזה?” ואז רגע אחר כך: “ומה הדוגמה השנייה שהשתמשו בה?”
ה-pipeline המקודד מתייחס לשאלה השנייה זהה לראשונה. מריץ את קריאת ה-routing שוב. מביא את הפרקים שוב. משלם על קריאות LLM לפני שהוא בכלל מתחיל לענות — כדי לגזור מידע routing שכבר היה לו לפני דקה.
ההסטה: לתת למודל להחליט
במקום להריץ את שלב ה-routing תמיד, חושפים אותו כ-tool שהמודל יכול לבחור להפעיל. נותנים למודל את שאלת המשתמש/ת ואת היסטוריית השיחה, ואומרים לו: “הנה tool בשם route_to_sources. תקרא לו כשאת/ה צריך/ה להוציא חומר חדש.”
המודל עכשיו מחליט לכל תור. בנושא חדש — הוא קורא ל-tool, מקבל מקורות, עונה. ב-follow-up על תוכן שכבר טעון בשיחה — הוא מדלג על ה-tool ועונה ישירות ממה שכבר רואה.
ההחלטה הזו קורית בתוך forward pass אחד דרך המודל. אין if בקוד שלנו. אנחנו לא כותבים היוריסטיקה. הבחירה היא של המודל.
אז זה מה ש-caching באמת אומר
בתוכנה רגילה, cache הוא דבר מפורש:
if (cache.has(key)) return cache.get(key);
else return await expensiveLookup();
את כותבת את ה-if. את בוחרת את ה-key. את מחליטה מה נחשב hit. ה-cache הוא מבנה נתונים שאת מתחזקת.
ב-flow של agent, ה”cache” נראה אחרת לגמרי:
- אין store חיצוני.
- אין key.
- הבדיקה היא לא קוד — היא חשיבה.
היסטוריית השיחה היא ה-cache. כל מה שהמודל טען בתורות קודמים יושב שם בתוך ה-context window שלו. כששאלה חדשה מגיעה, המודל מסתכל על השאלה, מסתכל על ההיסטוריה, ושופט: “האם יש לי כבר את מה שאני צריך, או שאני צריך להביא?” זו בדיקה סמנטית מטושטשת, לא lookup של key-value.
זו הסיבה ש-agents מרגישים שונה מ-pipelines ישנים. קבלת ההחלטות עברה מהקוד שלנו אל שלב החשיבה של המודל.
למה זה מרגיש טבעי — האנלוגיה האנושית
כשמישהו שואל אותך follow-up בשיחה, את לא חושבת:
- ללכת למדף הספרים.
- להוריד את הספר הרלוונטי.
- לחפש את התשובה.
- לחזור ולענות.
את חושבת:
- האם אני כבר זוכרת מספיק מלפני דקה כדי לענות?
- אם כן — לענות.
- אם לא — אז ללכת למדף הספרים.
הצעד הראשון — המבט על הזיכרון הפעיל שלך — הוא בעצמו בדיקת cache. הוא כמעט חינם. הטיול למדף הוא הפעולה היקרה. בני אדם בכלל לא מודעים לכך שהם עושים את הבדיקה, כי היא מיידית ומשתמשת באותו מנגנון כמו המענה עצמו.
ה-context window של ה-LLM הוא בדיוק אותו זיכרון פעיל. ה-pattern של agent נותן למודל לעשות את צעד המבט לפני שמחליט אם לעשות את הפעולה היקרה. ה-pipeline הישן דילג על המבט לגמרי — תמיד הלך למדף.
זה מה שגרם לאסימון ליפול לי — למה, בראש שלי, תמיד היה נראה ברור שה-agent צריך להיות זה שמחליט מתי להביא, לא הקוד מסביבו. כי ככה בני אדם חושבים.
ה-agent הוא התוכנה. זו ההסטה. אנחנו מפסיקות לכתוב תוכנה ש-משתמשת ב-LLM כשלב בתוך ה-control flow שלנו, ומתחילות לכתוב תוכנה שבה ה-LLM הוא ה-control flow. התפקיד שלנו עובר מ”להחליט מתי לקרוא למודל” ל”לתאר מה המודל יכול לעשות.”
וברגע שזה נוחת, התשובה ל”למה זה לא נבנה ככה מההתחלה” היא: כי חשבנו על LLMs כפונקציות יקרות, לא כשיפוט. ה-pattern של pipeline הוא מה שאת כותבת כשאת לא סומכת על המודל לקבל החלטות. ה-pattern של agent הוא מה שאת כותבת כשכן.
למה היוריסטיקה מקודדת-יד לא מספיקה
אולי תחשבי: “אי אפשר פשוט לחפש מילות סימן של follow-up כמו ‘קודם’ או ‘שוב’ ולדלג על ה-routing אז?” אפשר. זה יתפוס את המקרים הברורים.
אבל follow-ups אמיתיים לא תמיד מכריזים על עצמם:
- “מה הם מתכוונים בזה?” — אין מילת סימן, אבל ברור שזה follow-up.
- “ובפרק השני?” — מסומן רק בגלל ההשמטה.
- “למה?” — מילה אחת, תלוית-הקשר לחלוטין.
בן אדם שקורא את אלה יודע מיד אם יש לו מספיק הקשר. גם המודל — אם את נותנת לו את הבחירה. regex נופל על כל אחד מהם.
תיאורי tools הם לוגיקה של תוכנית, לא תיעוד
כאן נכנס החלק השני. התיאור שאנחנו כותבות ל-tool — המשפט בשפה טבעית שאומר למודל מה ה-tool עושה ומתי להשתמש בו — הוא לא תיעוד. הוא ה-cache policy.
כשאנחנו כותבות משהו כמו:
route_to_sources — מחפש בספריית התוכן חומר רלוונטי. קרא לזה כשהמשתמש שואל על נושא שלא כוסה בתורות קודמים. דלג על זה בשאלות הבהרה או follow-up על תוכן שכבר נמצא בשיחה.
…אנחנו ממש כותבות את ה-if-statement של ה-cache. בשפה טבעית. מוערך על ידי המודל.
שינויי ניסוח קטנים מזיזים את שיעור הקריאות באופן מדיד. תחליפי “מחפש” ל”מאתר” — התנהגות שונה. תורידי “שלא כוסה בתורות קודמים” — המודל קורא ל-tool יותר, ועולה כסף. תוסיפי “העדף לענות ישירות כשאפשר” — המודל הופך לקמצן ומדלג על routing בשאלות שבהן היה צריך לנתב.
בגלל זה את לא רוצה תיאורי tools פזורים על פני route handlers כמחרוזות inline, בדומה לאיך שאת כותבת הודעות log. את רוצה אותם במקום מובנה. זה מה ש”registry-managed” אומר.
איך “registry-managed prompts” נראה בפועל
prompt registry הוא פשוט מודול קטן — נקראי לו lib/prompts/registry.ts — שמחזיק כל prompt שהמערכת משתמשת בו. לכל prompt יש:
- שם (מזהה יציב, כמו
libraryRouterDescription). - variant אחד או יותר (כדי שתוכלי לעשות A/B test לניסוחים בלי לפצל את כל ה-route).
- שובל גרסאות בהיסטוריית ה-git.
- בסופו של דבר, תוצאות הערכה — כשאת משנה תיאור, את יכולה להריץ מחדש golden-set של שיחות מבחן ולראות איך שיעור הקריאות זז.
ה-call sites מייבאים לפי שם: resolvePromptVariant("libraryRouterDescription"). ה-route לא יודע מה הטקסט הנוכחי אומר. הוא פשוט מבקש את ה-policy.
היתרונות נצברים:
- מקור אמת אחד. אותו tool שמשמש שני routes שונים מקבל את אותו תיאור. אין drift שקט.
- בר-סקירה. PR diffs שמשנים תיאור tool עכשיו ברורים — הם חיים בקובץ ידוע. מחרוזות inline מתחבאות בגוף ה-handler ומחליקות מתחת לרדאר של ה-review.
- בר-בדיקה. את יכולה להעריך שינוי תיאור מול קורפוס של שאלות היסטוריות, למדוד איך החלטות ה-routing זזו, ולהחליט אם השינוי טוב לפני שאת משלחת.
- בר-גרסאות. כשהשיעורים נסוגים ב-prod, את יכולה לעשות
git blameלתיאור ולמצוא את השינוי.
זו אותה אינסטינקט של לא לפזר שאילתות SQL inline בקוד אפליקטיבי. SQL מתאר חוזה עם ה-database; את רוצה אותו במקום אחד שאפשר לסקור, לאופטם ולעבור עליו. תיאורי tools מתארים חוזה עם המודל. אותו הגיון.
ההסטה העמוקה
בקוד לא-agentי, אנחנו (המהנדסות) מחליטות מתי פעולות יקרות קורות ומקודדות את ההחלטה כ-control flow. בקוד agentי, אנחנו מתארות אילו פעולות זמינות ונותנות למודל להחליט מתי להפעיל אותן לפי המצב.
אנחנו ויתרנו על שליטה imperative בתמורה לשיפוט תלוי-הקשר. בגלל זה תיאורי tools הם load-bearing: הם לא תיעוד, הם לוגיקה של תוכנית. שינויי ניסוח קטנים מזיזים התנהגות באופן מדיד כי את ממש עורכת את policy ההחלטה.
לסכם
שני רעיונות לקחת:
- ה-”cache” ב-agent הוא היסטוריית השיחה עצמה, ובדיקת ה-cache היא חשיבת המודל. ה-policy חי בתוך שלב החשיבה של המודל במקום להיות כתוב כקוד.
- תיאור ה-tool הוא התוכנית. התייחסי אליו כמו ל-SQL: תני לו שם, נהלי גרסאות, סקרי בקפידה, הערכי לפני deploy. אל תחביאי אותו כמחרוזת inline.
והאינסטינקט שאומר “רגע, התוכנה לא אמורה פשוט להבין את זה לבד?” — האינסטינקט הזה בדיוק נכון. ה-agent הוא התוכנה. פשוט היינו רגילות לכתוב את זה הדרך השנייה.