יום 2 - פלט מובנה מסוכן
הצד החזק של ספריית Pydantic AI הוא המילה הראשונה בשם הספריה - pydantic. פידנטיק היא ספריית הגדרת טיפוסים והסוכן שלנו היום ישתמש בפידנטיק כדי לוודא שהוא מקבל מהמודל מידע בדיוק בצורה לה הוא מצפה.
טיפים קצרים וחדשות למתכנתים
הצד החזק של ספריית Pydantic AI הוא המילה הראשונה בשם הספריה - pydantic. פידנטיק היא ספריית הגדרת טיפוסים והסוכן שלנו היום ישתמש בפידנטיק כדי לוודא שהוא מקבל מהמודל מידע בדיוק בצורה לה הוא מצפה.
ספריית Pydantic AI היא ספריית פיתוח סוכנים בפייתון אגנוסטית למודל. מבחינה רעיונית היא לוקחת את הפשטות מ OpenAI Agents SDK ואת הגמישות והפתיחות להתחבר לכל הספקים מ LangGraph. ב 12 הימים הקרובים אפרסם פה 12 מדריכים קצרים לבניית סוכנים עם הספריה.
מעבר להנאה בעבודה עם Pydantic AI נרצה לשים לב לרעיונות הבסיסיים בפיתוח סוכנים: הפעלת כלים, קבלת פלט מובנה, לולאת הסוכן, דפי הוראות, פרמטרים ספציפיים למודל ועוד. כל דוגמה מציגה רעיון או שיטת עבודה שיעזרו גם אם תכתבו את הסוכן שלכם בספריה אחרת.
הסוכנים לכל הדוגמאות כבר עלו לגיטהאב בריפו:
https://github.com/ynonp/pydanticai-demos
כך שתוכלו להוריד את הפרויקט ולהריץ עם המדריכים.
גיירמו ראוך (מייסד vercel) פרסם בטוויטר קריאה לקרוא את הקוד. אני מתרגם:
אם אתם לא קוראים את הקוד אחד הבאים נכון:
מודלים עושים טעויות של מתחילים ובוחרים ארכיטקטורה לא נכונה. המודל הכי טוב בעולם הרגע הוסיף לי המתנה של 700ms לקוד בלי שום סיבה וכשהערתי על זה הוא ענה שאני צודק.
מעניינת הרשימה שלו אבל לדעתי מפספסת את הנקודה. הדוגמה בסוף היא המפתח.
בנינו מכונה אדירה שיודעת לתרגם טקסט בשפה טבעית לקוד וכמעט שכחנו ששפה טבעית היא לא קוד. היא לא מתארת את כל מה שצריך לעשות בכל המצבים. אנשים שונים יקראו טקסט בשפה טבעית ויבינו ממנו דברים אחרים. לא משנה איזה מכונה תיקח היא לא תוכל להחליט תמיד את ההחלטה הנכונה כי אין החלטה אחת נכונה.
קריאת הקוד היא החוזה בינינו לבין המכונה. אנחנו לא צריכים לוודא שהמכונה כותבת נכון לולאת for אנחנו צריכים לוודא שמכל המקומות בהם אפשר לכתוב לולאת for כדי להגיע לתוצאה שרצינו היא בחרה את המקום שגם אנחנו היינו בוחרים. גיירמו טוען שזה זמני, שעוד מעט המודלים כבר לא יעשו כאלה טעויות, שנצטרך לקרוא את הקוד פחות ופחות. אני חושב שמי שאחראי על מערכת צריך להכיר אותה ולהבין איך היא עובדת והדרך היחידה לעשות את זה היא לקרוא את הקוד.
ואולי יותר חשוב צריך גם לדעת איך לקרוא את הקוד. יום יבוא וההחלטות של המודלים יראו לנו כל כך הגיוניות שיהיה קשה לחשוב על אלטרנטיבה. זה לא המצב היום. כרגע גם בעבודה עם המודלים הכי טובים אם סיימתם יום עבודה ולא הערתם לסוכן על החלטות מהותיות שגויות שהוא קיבל סביר להניח שפספסתם את אותן החלטות מהותיות ושגויות.
לפני כמה ימים הצגתי כאן דוגמה שמראה איך לשבור ריפו באמצעות ערבוב בין merge ל rebase. הפוסט כלל את המשפט "נניח שבשלב מסוים main התקדם ואני רציתי בפיצ'ר שלי לקבל שינויים שנכנסו ב main", בעקבותיו עלתה השאלה "למה לא לעשות rebase בשלב הזה?" התשובה קשורה לתוכן הענפים וכמה כאב ראש כרוך בשינוי. נפרט:
ל JavaScript יש טיפוס נתונים שנקרא number לייצוג מספרים:
> typeof(12)
'number'
משתנה מסוג number הוא בגודל 64 ביט אבל JavaScript עצמה לא משתמשת בכל 64 הביטים כדי לייצג מספרים שלמים. הערכים שמותר לייצג כמספרים הם הטווח:
> Number.MIN_SAFE_INTEGER
-9007199254740991
> Number.MAX_SAFE_INTEGER
9007199254740991
ל JavaScript יש גם טיפוס נתונים בשם bigint שיודע לייצג מספרים הרבה יותר גדולים ואנחנו יוצרים ערך מסוג bigint בעזרת הסיומת n בסוף מספר:
> typeof(12n)
'bigint'
הגודל של bigint מוגבל רק בכמות הזכרון שיש לכם על המחשב. מה קורה כשיוצאים מחוץ לטווח הבטוח? שימו לב:
> Number.MAX_SAFE_INTEGER + 1
9007199254740992
> Number.MAX_SAFE_INTEGER + 2
9007199254740992
> Number.MAX_SAFE_INTEGER + 3
9007199254740994
> Number.MAX_SAFE_INTEGER + 4
9007199254740996
> 9007199254740992 == 9007199254740993
true
נו זה היה כיף. במספרים גדולים זה כמובן לא היה קורה:
> 9007199254740992n == 9007199254740993n
false
אז למה לא להשתמש ב bigint כל הזמן? בגלל JSON כמובן:
> JSON.stringify(9007199254740992)
'9007199254740992'
> JSON.stringify(9007199254740992n)
Uncaught TypeError: Do not know how to serialize a BigInt
at JSON.stringify (<anonymous>)
בשביל ש JavaScript יהפוך מספר גדול ל JSON הוא יצטרך לכתוב ערך שאף אחד לא יוכל לקרוא. מה עושים? הופכים למחרוזת וזוכרים להמיר ל BigInt בחזרה:
BigInt(JSON.parse(JSON.stringify(90071992547409932n.toString())))
90071992547409932n
נ.ב. הסיבה שאנחנו צריכים לדבר על זה היא שאנשים עדיין שולחים לפעמים id בתור מספר במקום בתור מחרוזת וכך כשיש יותר מדי אוביקטים במערכת המזהים מתחילים להישבר. מספרים גדולים זה אחלה לחישובים אבל רוב הזמן יהיה יותר קל לדאוג תמיד לשמור את המזהים שלנו בתור מחרוזות ולא לדאוג לגודל.
תמיד חלמתם לראות את Chrome Devtools MCP בגרסת ה CLI? אם כן ורסל התנדבו לסגור את הפער ופיתחו כלי שליטה בדפדפן משורת הפקודה בקוד פתוח, זהו Vercel Agent Browser. מתקינים כאן:
דה מרקר פרסמו כתבה גדולה על המצב של התעשייה בעקבות מהפכת ה AI וחלק משמעותי מוקדש שם לפיתוח. אני רוצה להתיחס ל-5 ציטוטים שעלו משם בעיקר כדי להוסיף צבע וקונטקסט כי הסיפור הרבה פחות ברור ממה שהוצג.
לאחרונה ראסט הכריזו על מדיניות LLM שסוף סוף עוזבת את התבנית הקבועה של "אין לנו בעיה עם LLM" או "אין כניסה לסוכנים". אנחנו נוטים לחשוב ש"מדיניות LLM" זו בעיה של פרויקטי קוד פתוח שמקבלים קוד מן הגורן ומן היקב, אבל האמת שרוב חברות התוכנה שראיתי רק ירוויחו מהגדרת מדיניות ברורה לשימוש ב LLM מעבר ל"תבזבזו כמה שיותר טוקנים אבל לא מעבר לתקציב".
אלה הדברים המרכזיים שמדיניות LLM צריכה להגדיר:
גם אם החלטתם שסוכני קידוד הם כלי עבודה שאי אפשר בלעדיו ו 100% מהקוד צריך להיות מיוצר על ידי סוכנים יש עוד ערימה של דברים שצוותי פיתוח מייצרים: אימיילים, טיקטים בג'ירה, דיווחי באגים, תשובות למשתמשים, סיכומי פגישות, תיעוד, מדריכים למפתחים חדשים. אם LLM יכול להכניס לבד דיווחי באגים אז יהיו דיווחי באגים מיותרים. אם LLM הולך לכתוב טקסט של טיקטים אז טיקטים יהיו לא עקביים ואולי לא מדויקים. כלל אצבע במדיניות של ראסט אומר למשל:
Showing LLM output to another human without solicitation is likely banned
ברור שאי אפשר לאכוף את זה וזו לא המטרה. הסיפור כאן הוא תיאום ציפיות והוגנות. אני הייתי מוסיף שאם אתה רוצה להדביק פלט של LLM לשיחה עדיף שתדביק את הפרומפט שכתבת לו. לכולם יש גישה לקלוד ויכולים לייצר בעצמם את ההשלמה.
לא משנה מי הסוכן שכותב את הקוד יש בעיות והטיות מובנות לכל הכלים האלה: הם כותבים קוד מתגונן, הם לא מוחקים, הם כותבים בדיקות ספציפיות מדי, הם כותבים טלאים שגורמים לדברים לעבוד גם כשהם לא נכונים, הם כותבים קוד משוכפל בכמה מקומות ורוב הזמן מתקשים למצוא את המקום הנכון לכתוב את המימושים שלהם.
אפשר להחליט שאחרי כל יצירת קוד עם סוכן קידוד מישהו אנושי נכנס לקוד ומתקן. זה לוקח זמן ודורש מיומנות כלומר במקום שפיצ'ר יהיה באוויר אחרי שעה ש LLM ישב עליו הוא יצטרך יומיים של מפתח אנושי. אפשר להחליט שאנחנו דוחפים כל מה ש LLM יצר אם זה לא ממש שגוי ומשקיעים זמן קבוע פעם בשבוע בניקוי הקוד כולל קוד ישן. אפשר להחליט שאנחנו מתקנים כשמזהים באגים תוך כדי תנועה. אפשר להחליט שאנחנו רצים הכי מהר שאפשר עד שהמכונה נתקעת ואז עוצרים לנקות או מתחילים פרויקט חדש. מה שלא תחליטו כדאי שכל האנשים בארגון יהיו מיושרים על ההחלטה. אם מנהל הפיתוח בטוח שכל קוד שנכנס למערכת עבר Review אנושי ואיטרציות והוא עומד בסטנדרטים הכי גבוהים של פיתוח אבל בצד השני יש מפתחת שבטוחה שהדבר הכי חשוב זה מהירות ואסור לבזבז זמן על קריאת הקוד מתישהו יהיה פיצוץ.
אם חבר מבקש שתעברו על קוד שהוא כתב זה בסדר להשתמש ב AI? אם צריך לתקן בעיה בגיט זה בסדר לתת לקלוד לסדר את זה? מה לגבי ניתוח פלט של בדיקות ותיקון בדיקות שנכשלו? אל תתנו לכל מפתח להחליט לבד רמת המיומנות שלו או שלה או לפי כמה זמן יש להם עכשיו. בואו ניישר קו - אצלנו לפני שקוד נכנס לפרודקשן הוא עובר Review של בודק ה AI, או אצלנו לפני שגרסה עולה לפרודקשן מפתח אנושי חייב לשבת יומיים ולקרוא את כל הקוד שהשתנה מאז הגרסה האחרונה, או אצלנו מפתח אנושי חייב לעבור על הפלט של בודק ה AI. מה שתחליטו זה בסדר רק תראו שאתם שלמים עם ההשלכות.
למרות שמודלי שפה לא חותמים על הטקסט שהם מייצרים לבני אדם במיוחד בארץ קל מאוד להבין כשאנחנו מדברים עם מודל שפה בשונה משיחה עם בן אדם אמיתי. קל לנו מאוד לזהות פלט של מודל שפה ואפילו קוד של סוכן קידוד במיוחד על מערכת גדולה. המכונות האלה אולי מייצרות טקסט דומה לזה שבני אדם מייצרים אבל הן לא בני אדם. חלק חשוב מהיכולת לעבוד עם מכונות אלה בצורה יעילה הוא חלוקת התפקידים שתתאים ליכולות, למגבלות ולאחריות של כל אחד.
ריבייס מאפשר לייצר היסטוריית גיט לינארית באמצעות יצירה מחדש של הקומיטים שבוצעו לפיצ'ר מסוים על גבי קומיט חדש יותר. זה עובד ממש טוב כשאני בונה פיצ'ר ובזמן הזה הענף הראשי התקדם. אני רוצה לשלב את הפיצ'ר במערכת אבל ההתקדמות של הענף הראשי היא לא הסיפור, ולכן אני עושה rebase מהענף שפיתחתי על הקומיט החדש ביותר בענף הראשי, מה שגורם לגיט "לבנות מחדש" את פיתוח הפיצ'ר מתוך הקומיט החדש ביותר. אחרי ריבייס אפשר למזג ובהיסטוריית גיט זה נראה כאילו חיכיתי לסיום הפיצ'רים של הענף הראשי לפני שפיתחתי את הפיצ'ר שלי, כלומר הפיצ'ר שלי נבנה על גבי הפיצ'רים מהענף הראשי.
עכשיו בואו נשבור את זה.
נניח שבשלב מסוים main התקדם ואני רציתי בפיצ'ר שלי לקבל שינויים שנכנסו ב main אז ביצעתי merge מ main ל feature1. המיזוג לא עבר חלק ופתרתי קונפליקטים אבל זה בסדר כי ממילא הייתי צריך לפתור את הקונפליקטים האלה לפני שאני יכול לשלב. בינתיים main ממשיך להתקדם וכעבור זמן אני מגיע לריפו במצב הבא:
* 45cfa5d (main) main: 3 commits ahead
* 51c18b4 main: 2 commits ahead
* dfad935 main: 1 commit ahead
| * fa37287 (HEAD -> merge-feature1) Merge feature1 into old main (RESOLVED CONFLICT)
|/|
| * 2392145 (feature1) feature1: add HTML report part 2
| * 6b24764 feature1: add HTML report part 1
* | 9961b60 main: added some changes
|/
* d4d0727 fixed stuff
* 9155d8f initial commit
מה יקרה אם עכשיו אני מנסה להפעיל את אותו טריק ולבצע ריבייס מהענף הממוזג ל main החדש? שימו לב:
(merge-feature1) $ git rebase main
Auto-merging main.py
CONFLICT (content): Merge conflict in main.py
error: could not apply 6b24764... feature1: add HTML report part 1
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Recorded preimage for 'main.py'
Could not apply 6b24764... # feature1: add HTML report part 1
קיבלתי קונפליקט על קומיט 6b24764, כלומר קונפליקט שכבר פתרתי ב Merge Commit. היה לי רעיון טוב להקדים תרופה למכה ולמזג את השינויים מ main לפני שאני מסיים לעבוד על הבראנץ' אבל בסוף זו היתה עבודה מיותרת כי ריבייס מתעלם מהקונפליקטים שפתרתי.
מה עושים? קודם כל כדאי לזכור שקשה להזיז merge commit ולכן עדיף לחשוב לפני שאתם מתחילים merge איפה אתם רוצים שה merge commit שלכם ישב. אם בכל זאת תבקשו מאיזה קלוד להתעקש הוא ימזג חזרה את main העדכני לתוך merge-feature1, ימחק את main ואז יבנה קומיט חדש עם git write-tree שיש לו רגל אחת על אותו ענף צד ורגל שניה איפה שהיה פעם main.
כשאתם נותנים ל AI לכתוב Shell Scripts שמחפשים בקבצים הוא אוהב להשתמש ב grep. זה עובד בשמות פשוטים ויכול לבלבל כשהשמות מכילים תווים מיוחדים של ביטויים רגולארים.
לדוגמה נתון קובץ טקסט עם השורות הבאות:
line1
line2
line3
line4
line.txt
בשביל לבדוק אם קיימת בו השורה line3 נוכל לכתוב:
$ grep line3 demo.txt
ונקבל את השורה.
בשביל לבדוק אם קיימת בו השורה line.txt מפתה לכתוב גם:
$ grep line.txt demo.txt
וזה גם עובד ומדפיס את line.txt - אבל זה בטעות. הנקודה ב line.txt היא תו של ביטויים רגולאריים ובעצם מתאימה לכל תו. לדוגמה אם הקובץ שלנו מכיל:
line-txt
line_txt
line2txt
אותה פקודת grep תחזיר את כל השורות:
grep line.txt demo.txt
line.txt
line-txt
line_txt
line2txt
מה עושים? נזכרים בטיפ בכותרת. פקודת grep -F או פקודת fgrep מחפשות טקסט פשוט ולא ביטוי רגולארי לכן כשאנחנו מחפשים טקסט רגיל נעדיף לכתוב:
$ fgrep line.txt demo.txt
line.txt
ונקבל רק את השורה עם הנקודה ולא את השורות שמתאימות לביטוי הרגולארי.
נ.ב. grep עצמו מבלבל כי הוא מתעלם באופן אוטומטי מתווים מסוימים של ביטויים רגולאריים במיוחד פלוס, כלומר אם בקובץ יש לי את השורות:
2+4
2229
פקודת grep תחזיר ממש את הפלוס ותתעלם מהפלוס במשמעותו המיוחדת של "תו נוסף" שאנחנו מכירים מביטויים רגולאריים:
$ grep '2+' demo.txt
2+4
אבל זו רק הטעיה כי אם מוסיפים לה \ לפני הפלוס היא חוזרת להתיחס אליו כמו תו של ביטויים רגולאריים:
$ grep '2\+' demo.txt
2+4
2229