📌 חמש דוגמאות ל Type Hints מעניינים בפייתון
כתיבת Type Hints היא אחד הרעיונות שמתחילים פשוט ואז מסתבכים כשפוגשים קוד אמיתי. זאת הסיבה שלקח לפייתון כמה שנים עד שהגיעו לתחביר Type Hints שעובד ומשמח את רוב המפתחים. בואו נראה 5 מקרים מאתגרים ואת הפתרון שלהם במערכת הטיפוסים של פייתון.
✏ העברת פרמטרים As-Is
בואו נתחיל ונכתוב דקורייטור. בפייתון דקורייטור זו דרך להוסיף התנהגות לפונקציה, לדוגמה הקוד הבא יגרום לפייתון להפעיל מחדש את הפונקציה אם היא נכשלת:
הבעיה היא שהפעלת הדקורטור מוחקת את ה Type Hint של הפונקציה. דקורטור גורם להחלפת הפונקציה בגרסה שהדקורטור מחזיר, ו wrapper היא פונקציה גנרית שיכולה לקבל כל פרמטר.
בשביל שזה יעבוד wrapper צריכה להגדיר את אותו Type Hint כמו הפונקציה
הטיפוס ParamSpec נועד בדיוק למקרים האלה. טיפוס זה נכנס בפייתון 3.10 ומייצג אוסף פרמטרים כללי ש"תופס" פרמטרים של פונקציה קיימת. בואו נראה את הפתרון המתוקן:
עכשיו ברור לפייתון מה החתימה של הפונקציה wrapper וחתימה זו תלווה את הפונקציה גם בחזרה מהדקורטור, כך שקוד כזה ייפול בבדיקת טיפוסים:
✏ בדיקת קיום פונקציה בלי לחייב ירושה
נתונה פונקציה שמקבלת רשימה של דברים שלכולם יש מאפיין או מתודה משותפת. בשפה מקומפלת קלאסית היינו דורשים שכל הדברים "ירשו" ממחלקת בסיס מסוימת שתגדיר את המאפיין המשותף. פייתון מעולם לא חייבה זאת, כלומר הקוד הבא הוא פייתון תקין וטבעי לגמרי:
אבל מה עושים כשרוצים להוסיף כאן Type Hints? לא נרצה להגדיר בצורה מפורשת בפונקציה שהיא מקבלת רק דברים מסוג ItemA או ItemB כי מחר מישהו יוסיף קלאס חדש עם מאפיין price. גם לא נרצה לשנות את ההגדרה של ItemA או ItemB ולהתחיל להגדיר עבורם היררכיית ירושה חדשה. אנחנו רוצים פשוט להגדיר שהפונקציה מקבלת רשימה של דברים שיש להם מאפיין price.
בדיוק בשביל זה נולד בפייתון 3.8 המושג Protocol. פרוטוקול מתאר מבנה של קלאס, בלי לשנות את הקוד של אותו הקלאס ומאפשר כתיבת פונקציות גנריות שמקבלות דברים שמתאימים למבנה. הדוגמה שלנו תיראה כך:
עכשיו אם ננסה להעביר משהו שאין לו price בתור אחד הפריטים mypy מיד יזהה את הטעות.
✏ ערכי החזר מסוגים שונים
קביעת הטיפוס של ערכי החזר מאפשרת לבודק הטיפוסים של פייתון להסיק מסקנות נכונות על אותם דברים שחוזרים מפונקציות. נתחיל עם פונקציה פשוטה שרק מחזירה את מה שהיא קיבלה:
בלי בדיקות טיפוסים קל לראות איך אנחנו משתמשים בפונקציה הזאת לא נכון, לדוגמה:
הטיפוס של פונקציית הזהות מעניין כי היא צריכה להחזיר בדיוק את מה שהיא קיבלה. הדרך בפייתון לחבר בין כמה טיפוסים על אותה הגדרה נקראת TypeVar וזה נראה כך:
כתיבת Type Hints היא אחד הרעיונות שמתחילים פשוט ואז מסתבכים כשפוגשים קוד אמיתי. זאת הסיבה שלקח לפייתון כמה שנים עד שהגיעו לתחביר Type Hints שעובד ומשמח את רוב המפתחים. בואו נראה 5 מקרים מאתגרים ואת הפתרון שלהם במערכת הטיפוסים של פייתון.
✏ העברת פרמטרים As-Is
בואו נתחיל ונכתוב דקורייטור. בפייתון דקורייטור זו דרך להוסיף התנהגות לפונקציה, לדוגמה הקוד הבא יגרום לפייתון להפעיל מחדש את הפונקציה אם היא נכשלת:
def retry_on_failure(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception:
print("Retrying...")
return func(*args, **kwargs)
return wrapper
@retry_on_failure
def say_my_name(name: str):
print(name)
say_my_name('demo')
הבעיה היא שהפעלת הדקורטור מוחקת את ה Type Hint של הפונקציה. דקורטור גורם להחלפת הפונקציה בגרסה שהדקורטור מחזיר, ו wrapper היא פונקציה גנרית שיכולה לקבל כל פרמטר.
בשביל שזה יעבוד wrapper צריכה להגדיר את אותו Type Hint כמו הפונקציה
say_my_name, אבל הדקורטור הוא גנרי, איך יודעים מה הפרמטרים שהוא צפוי לקבל?הטיפוס ParamSpec נועד בדיוק למקרים האלה. טיפוס זה נכנס בפייתון 3.10 ומייצג אוסף פרמטרים כללי ש"תופס" פרמטרים של פונקציה קיימת. בואו נראה את הפתרון המתוקן:
from typing import Callable, TypeVar, ParamSpec
P = ParamSpec('P')
T = TypeVar('T')
# ParamSpec captures the exact arguments (*args, **kwargs) of the wrapped function.
def retry_on_failure(func: Callable[P, T]) -> Callable[P, T]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
try:
return func(*args, **kwargs)
except Exception:
print("Retrying...")
return func(*args, **kwargs)
return wrapper
עכשיו ברור לפייתון מה החתימה של הפונקציה wrapper וחתימה זו תלווה את הפונקציה גם בחזרה מהדקורטור, כך שקוד כזה ייפול בבדיקת טיפוסים:
@retry_on_failure
def say_my_name(name: str):
print(name)
say_my_name(10)
✏ בדיקת קיום פונקציה בלי לחייב ירושה
נתונה פונקציה שמקבלת רשימה של דברים שלכולם יש מאפיין או מתודה משותפת. בשפה מקומפלת קלאסית היינו דורשים שכל הדברים "ירשו" ממחלקת בסיס מסוימת שתגדיר את המאפיין המשותף. פייתון מעולם לא חייבה זאת, כלומר הקוד הבא הוא פייתון תקין וטבעי לגמרי:
class ItemA:
def __init__(self):
self.price = 10
class ItemB:
def __init__(self):
self.price = 5
def total_price(items):
return sum(item.price for item in items) * 1.18
print(total_price([ItemB(), ItemA(), ItemA(), ItemA()]))
אבל מה עושים כשרוצים להוסיף כאן Type Hints? לא נרצה להגדיר בצורה מפורשת בפונקציה שהיא מקבלת רק דברים מסוג ItemA או ItemB כי מחר מישהו יוסיף קלאס חדש עם מאפיין price. גם לא נרצה לשנות את ההגדרה של ItemA או ItemB ולהתחיל להגדיר עבורם היררכיית ירושה חדשה. אנחנו רוצים פשוט להגדיר שהפונקציה מקבלת רשימה של דברים שיש להם מאפיין price.
בדיוק בשביל זה נולד בפייתון 3.8 המושג Protocol. פרוטוקול מתאר מבנה של קלאס, בלי לשנות את הקוד של אותו הקלאס ומאפשר כתיבת פונקציות גנריות שמקבלות דברים שמתאימים למבנה. הדוגמה שלנו תיראה כך:
from typing import Protocol
class ItemA:
def __init__(self):
self.price = 10
class ItemB:
def __init__(self):
self.price = 5
class HasPrice(Protocol):
@property
def price(self) -> float:
...
def total_price(items: list[HasPrice]):
return sum(item.price for item in items) * 1.18
print(total_price([ItemB(), ItemA(), ItemA(), ItemA()]))
עכשיו אם ננסה להעביר משהו שאין לו price בתור אחד הפריטים mypy מיד יזהה את הטעות.
✏ ערכי החזר מסוגים שונים
קביעת הטיפוס של ערכי החזר מאפשרת לבודק הטיפוסים של פייתון להסיק מסקנות נכונות על אותם דברים שחוזרים מפונקציות. נתחיל עם פונקציה פשוטה שרק מחזירה את מה שהיא קיבלה:
def same(x):
return x
בלי בדיקות טיפוסים קל לראות איך אנחנו משתמשים בפונקציה הזאת לא נכון, לדוגמה:
x = same("hello")
print(x + 5)הטיפוס של פונקציית הזהות מעניין כי היא צריכה להחזיר בדיוק את מה שהיא קיבלה. הדרך בפייתון לחבר בין כמה טיפוסים על אותה הגדרה נקראת TypeVar וזה נראה כך:
G = TypeVar("G")
def same(x: G) -> G:
return x
x = same("hello")
print(x + 5)עכשיו בודק הטיפוסים של פייתון כבר ידע להגיד ש x שחוזר מ
אבל TypeVar לבד לא מספיק, ופה דברים מתחילים להסתבך. ראינו את ParamSpec שמאפשר להבין איזה פרמטרים עוברים בשביל לחבר אותם לפונקציות שאנחנו מחזירים. אבל אם מה שאנחנו מחזירים זה tuple אז נצטרך מנגנון אחר. שימו לב לקוד הבא:
הקוד מנסה להדפיס את האורך של הדבר הראשון שחוזר מהפונקציה, שהוא מספר, ולכן נכשל. הבעיה היא שבודק הטיפוסים לא יכול לראות את זה.
את args אי אפשר להגדיר בתור רשימה גנרית או tuple גנרי. אנחנו רוצים להיות יותר מדויקים ולהגיד שאנחנו מחזירים בדיוק את מה שקיבלנו. לאיבר הראשון אין אורך אבל לשני ולשלישי כבר יש.
המבנה TypeVarTuple נועד בדיוק בשביל המקרים האלה. הפתרון נראה כך והוא מחייב שינוי חתימה:
משתנה TypeVarTuple מגדיר את סוגי המשתנים בתוך Tuple על בסיס הפרמטרים שפונקציה קיבלה. המגבלה שלו היא שחייבים להשתמש בו על פרמטרים עם כוכבית, ולכן הייתי צריך לשנות את חתימת הפונקציה ולהפוך את המשתנה היחיד ל
עכשיו בודק הטיפוסים מזהה את הבעיה בהדפסת האורך של
✏ שרשור מתודות על אוביקט בירושה
נמשיך לדוגמה הבאה ונכתוב מחשבון:
איזה כיף שאפשר להמשיך את ה add ולשרשר עוד קריאות! זה קורה בגלל שאני מחזיר את self. בשביל להוסיף טיפוסים אני יכול לסמן את הפונקציה בתור כזו שמחזירה Calc וזה יעבוד:
אבל אז אני אסתבך בירושה. שימו לב לקוד הבא:
הקריאה ל add הפכה את המשתנה מ AdvancedCalc ל Calc עבור בודק הטיפוסים כיוון שזה מה ש add מחזירה. גישה טובה יותר תגיד לפייתון שהפונקציה add מחזירה את אותו טיפוס של self - אם התחלנו עם Calc זה מה שנחזיר, ואם התחלנו עם AdvancedCalc נחזיר את AdvancedCalc. הקוד משתמש בטיפוס בשם Self, עוד חידוש של פייתון 3.11:
✏ טיפוסים רקורסיביים
טיפוס רקורסיבי הוא טיפוס שאחד השדות בו מכיל משהו מאותו הטיפוס. דוגמה קלאסית היא JSON. הגישה הקלאסית להגדרת טיפוסים כאלה השתמשה במרכאות כדי לדחות את שערוך הטיפוס בתוך החלק הרקורסיבי כלומר:
גרסאות חדשות יותר של פייתון מאפשרות לוותר על המרכאות באמצעות הגדרת Type Alias:
פייתון עשתה התקדמות מרשימה בשנים האחרונות באפשרות הגדרת הטיפוסים והיום אפשר להשתמש ב Type Hints כדי להגדיר טיפוסים בהרבה מאוד מקרים. נכון, אין לפייתון אין הדינמיות של טייפסקריפט, אבל לא כל שפת טיפוסים צריכה להיות מסוגלת להריץ Doom.
same("hello") הוא מטיפוס מחרוזת ולכן אי אפשר לחבר לו 5.אבל TypeVar לבד לא מספיק, ופה דברים מתחילים להסתבך. ראינו את ParamSpec שמאפשר להבין איזה פרמטרים עוברים בשביל לחבר אותם לפונקציות שאנחנו מחזירים. אבל אם מה שאנחנו מחזירים זה tuple אז נצטרך מנגנון אחר. שימו לב לקוד הבא:
T = TypeVar("T")
def add_item(item: T, args):
return (item, *args)
v = add_item(4, [1, 'a', 'b'])
print(len(v[0]))הקוד מנסה להדפיס את האורך של הדבר הראשון שחוזר מהפונקציה, שהוא מספר, ולכן נכשל. הבעיה היא שבודק הטיפוסים לא יכול לראות את זה.
את args אי אפשר להגדיר בתור רשימה גנרית או tuple גנרי. אנחנו רוצים להיות יותר מדויקים ולהגיד שאנחנו מחזירים בדיוק את מה שקיבלנו. לאיבר הראשון אין אורך אבל לשני ולשלישי כבר יש.
המבנה TypeVarTuple נועד בדיוק בשביל המקרים האלה. הפתרון נראה כך והוא מחייב שינוי חתימה:
from typing import TypeVarTuple, TypeVar, Any
Ts = TypeVarTuple("Ts")
T = TypeVar("T")
def add_item(item: T, *args: *Ts) -> tuple[T, *Ts]:
return (item, *args)
v = add_item(4, 1, 'a', 'b')
print(len(v[0]))
משתנה TypeVarTuple מגדיר את סוגי המשתנים בתוך Tuple על בסיס הפרמטרים שפונקציה קיבלה. המגבלה שלו היא שחייבים להשתמש בו על פרמטרים עם כוכבית, ולכן הייתי צריך לשנות את חתימת הפונקציה ולהפוך את המשתנה היחיד ל
*args.עכשיו בודק הטיפוסים מזהה את הבעיה בהדפסת האורך של
v[0] אבל יתן לי להדפיס את האורך של v[2].✏ שרשור מתודות על אוביקט בירושה
נמשיך לדוגמה הבאה ונכתוב מחשבון:
class Calc:
def __init__(self):
self.value = 0
def add(self, n: int):
self.value += n
return self
c = Calc()
c.add(10).add(20).add(30)
print(c.value)
איזה כיף שאפשר להמשיך את ה add ולשרשר עוד קריאות! זה קורה בגלל שאני מחזיר את self. בשביל להוסיף טיפוסים אני יכול לסמן את הפונקציה בתור כזו שמחזירה Calc וזה יעבוד:
def add(self, n: int) -> Calc:
self.value += n
return self
אבל אז אני אסתבך בירושה. שימו לב לקוד הבא:
class Calc:
def __init__(self):
self.value = 0
def add(self, n: int) -> Calc:
self.value += n
return self
class AdvancedCalc(Calc):
def sub(self, n: int) -> AdvancedCalc:
self.value -= n
return self
c = AdvancedCalc()
c.add(10).sub(15).add(30)
print(c.value)
הקריאה ל add הפכה את המשתנה מ AdvancedCalc ל Calc עבור בודק הטיפוסים כיוון שזה מה ש add מחזירה. גישה טובה יותר תגיד לפייתון שהפונקציה add מחזירה את אותו טיפוס של self - אם התחלנו עם Calc זה מה שנחזיר, ואם התחלנו עם AdvancedCalc נחזיר את AdvancedCalc. הקוד משתמש בטיפוס בשם Self, עוד חידוש של פייתון 3.11:
from typing import Self
class Calc:
def __init__(self):
self.value = 0
def add(self, n: int) -> Self:
self.value += n
return self
class AdvancedCalc(Calc):
def sub(self, n: int) -> Self:
self.value -= n
return self
c = AdvancedCalc()
c.add(10).sub(15).add(30)
print(c.value)
✏ טיפוסים רקורסיביים
טיפוס רקורסיבי הוא טיפוס שאחד השדות בו מכיל משהו מאותו הטיפוס. דוגמה קלאסית היא JSON. הגישה הקלאסית להגדרת טיפוסים כאלה השתמשה במרכאות כדי לדחות את שערוך הטיפוס בתוך החלק הרקורסיבי כלומר:
JsonValue = Union[int, float, str, bool, None, Dict[str, 'JsonValue'], List['JsonValue']]
def parse_api_response(payload: JsonValue):
pass
גרסאות חדשות יותר של פייתון מאפשרות לוותר על המרכאות באמצעות הגדרת Type Alias:
type JsonValue = Union[int, float, str, bool, None, Dict[str, JsonValue], List[JsonValue]]
פייתון עשתה התקדמות מרשימה בשנים האחרונות באפשרות הגדרת הטיפוסים והיום אפשר להשתמש ב Type Hints כדי להגדיר טיפוסים בהרבה מאוד מקרים. נכון, אין לפייתון אין הדינמיות של טייפסקריפט, אבל לא כל שפת טיפוסים צריכה להיות מסוגלת להריץ Doom.
❤1
📌 למי אכפת מ MCP Composability
את הדוגמה הבא להרכבת MCP ראיתי בפוסט של אנטרופיק, ששמו לב לבעיה הזאת הרבה לפניי: "הי סוכן הורד לי בבקשה את סיכום הפגישה מהדרייב ותעלה אותו לג'ירה". משימה שנראית סופר פשוטה שהופכת למסובכת רק בגלל התשתית.
כשסוכן ינסה לבצע אותה בעזרת תשתית ה MCP המוכרת היום הסוכן יצטרך לעשות:
1. הפעלת כלי - קח מידע מ Google Drive
2. הפעלת כלי - כתיבה ל Jira
הבעיה שבאמצע המידע יושב בחלון הקונטקסט של הסוכן ואין לנו דרך לוודא שהמידע שנשלח לכלי השני הוא בדיוק הטקסט שחזר מהכלי הראשון. אנטרופיק טוענים שעבור פגישה של שעתיים יש סיכוי לבזבז פה 50,000 טוקנים, רק על העברת קובץ ממקום למקום.
דוגמה יותר קלה הראיתי אתמול בוובינר כשנתתי לסוכן שרת MCP שמחזיר פוסטים מהבלוג ושאלתי אותו כמה שורות יש בפוסט מסוים. גם כאן משימה סופר פשוטה למחשבים רגילים הפכה למבלבלת עבור סוכנים חכמים. הסוכן היה צריך לטעון את כל הטקסט של הפוסט ואז לספור את השורות, ושוב גם זה מעמיס על חלון הקונטקסט ופגיע לטעויות.
פתרון אחד לבעיה מגיע מהכיוון של אנטרופיק ונשמע כמו הצעה של AI: הם מציעים לאפשר לסוכן לכתוב ולהריץ קוד TypeScript שיוכל להפעיל "פקודות" מתוך שרתי MCP. זאת הדוגמה שלהם:
היתרון בגישה זו היא שהיא עובדת, אם נוכל לקרוא ל MCP כמו לקוד נוכל להרכיב את התוצאות וכך נפתור את הבעיה. החסרון הוא שאנחנו בונים אבסטרקציה על אבסטרקציה במקום למחוק ולפשט את המערכת.
פתרון שאני יותר אוהב הוא פשוט לוותר על MCP ולכתוב כלים פשוטים לשורת הפקודה. אם היו כלים לשורת הפקודה לדבר עם gdrive ו salesforce הסוכן היה יכול לכתוב:
או בדוגמה של ספירת השורות בפוסט הסוכן יכל היה לכתוב:
סוכנים אוהבים לכתוב קוד וככל שנאפשר להם להשתמש בקוד במקום לעבד בעצמם פלטים של MCP נוכל להשתמש בצורה יעילה יותר בחלון הקונטקסט ולקבל תוצאות מדויקות יותר.
באותו נושא שווה להזכיר את הפרויקט:
https://github.com/philschmid/mcp-cli
שמאפשר להפעיל שרתי MCP מתוך תוכניות CLI.
את הדוגמה הבא להרכבת MCP ראיתי בפוסט של אנטרופיק, ששמו לב לבעיה הזאת הרבה לפניי: "הי סוכן הורד לי בבקשה את סיכום הפגישה מהדרייב ותעלה אותו לג'ירה". משימה שנראית סופר פשוטה שהופכת למסובכת רק בגלל התשתית.
כשסוכן ינסה לבצע אותה בעזרת תשתית ה MCP המוכרת היום הסוכן יצטרך לעשות:
1. הפעלת כלי - קח מידע מ Google Drive
2. הפעלת כלי - כתיבה ל Jira
הבעיה שבאמצע המידע יושב בחלון הקונטקסט של הסוכן ואין לנו דרך לוודא שהמידע שנשלח לכלי השני הוא בדיוק הטקסט שחזר מהכלי הראשון. אנטרופיק טוענים שעבור פגישה של שעתיים יש סיכוי לבזבז פה 50,000 טוקנים, רק על העברת קובץ ממקום למקום.
דוגמה יותר קלה הראיתי אתמול בוובינר כשנתתי לסוכן שרת MCP שמחזיר פוסטים מהבלוג ושאלתי אותו כמה שורות יש בפוסט מסוים. גם כאן משימה סופר פשוטה למחשבים רגילים הפכה למבלבלת עבור סוכנים חכמים. הסוכן היה צריך לטעון את כל הטקסט של הפוסט ואז לספור את השורות, ושוב גם זה מעמיס על חלון הקונטקסט ופגיע לטעויות.
פתרון אחד לבעיה מגיע מהכיוון של אנטרופיק ונשמע כמו הצעה של AI: הם מציעים לאפשר לסוכן לכתוב ולהריץ קוד TypeScript שיוכל להפעיל "פקודות" מתוך שרתי MCP. זאת הדוגמה שלהם:
// Read transcript from Google Docs and add to Salesforce prospect
import * as gdrive from './servers/google-drive';
import * as salesforce from './servers/salesforce';
const transcript = (await gdrive.getDocument({ documentId: 'abc123' })).content;
await salesforce.updateRecord({
objectType: 'SalesMeeting',
recordId: '00Q5f000001abcXYZ',
data: { Notes: transcript }
});
היתרון בגישה זו היא שהיא עובדת, אם נוכל לקרוא ל MCP כמו לקוד נוכל להרכיב את התוצאות וכך נפתור את הבעיה. החסרון הוא שאנחנו בונים אבסטרקציה על אבסטרקציה במקום למחוק ולפשט את המערכת.
פתרון שאני יותר אוהב הוא פשוט לוותר על MCP ולכתוב כלים פשוטים לשורת הפקודה. אם היו כלים לשורת הפקודה לדבר עם gdrive ו salesforce הסוכן היה יכול לכתוב:
gdrive --get-document abc123 | salesforce --update-record --record-id 00Q5f00001abcXYZ
או בדוגמה של ספירת השורות בפוסט הסוכן יכל היה לכתוב:
get-post post-title | wc -l
סוכנים אוהבים לכתוב קוד וככל שנאפשר להם להשתמש בקוד במקום לעבד בעצמם פלטים של MCP נוכל להשתמש בצורה יעילה יותר בחלון הקונטקסט ולקבל תוצאות מדויקות יותר.
באותו נושא שווה להזכיר את הפרויקט:
https://github.com/philschmid/mcp-cli
שמאפשר להפעיל שרתי MCP מתוך תוכניות CLI.
GitHub
GitHub - philschmid/mcp-cli: Lighweight CLI to interact with MCP servers
Lighweight CLI to interact with MCP servers. Contribute to philschmid/mcp-cli development by creating an account on GitHub.
❤1
📌 מה למדתי על Code Review מהסוכנים של Qt
החברים ב Qt שחררו מסמך סקיל שמלמד את קלוד קוד איך לעשות Code Review לקוד Qt. אפשר לקרוא כאן ואני ממליץ:
https://github.com/TheQtCompanyRnD/agent-skills/blob/main/skills/qt-cpp-review/SKILL.md
הנקודה הראשונה הקריטית שהמסמך מזכיר לנו בעבודה עם סוכני קידוד היא החשיבות של הפרומפט. מי שרוצה לקבל Code Review ברמה גבוהה לא יכול לבקש "תעשה לי Code Review" או אפילו "תעשה לי Code Review ותחפש בעיות ביצועים". הסוכן צריך פרומפט שישים אותו בנקודת התחלה מדויקת לגבי מה אתם מחפשים.
דף ההוראות של Qt מתחיל בזיהוי על מה בכלל אנחנו מחפשים לעשות Code Review, כאשר יש הוראות שונות אם רוצים לעשות Code Review על קומיט מסוים או Code Review על המערכת כולה. ממשיכים בבקשה לניתוח סטטי של הקוד ואז מגיע החלק המעניין - הגדרת 6 סוכני Review, כל אחד עם התפקיד שלו.
יש סוכנים שאחראיים על חיפוש בעיות נפוצות בקלאס מסוים או אוסף קלאסים, למשל הסוכן הראשון מחפש רק בעיות ב QAbstractItemModel. סוכן שני מחפש בכל המערכת בעיות זכרון, ושימו לב להגדרה שלו שכוללת ממש רשימה של בעיות זכרון נפוצות ב Qt:
גם אם במבט ראשון רשימה כזאת נראית כתופסת יותר מדי מקום ב Context Window, מודלים חזקים היום יודעים להתמודד עם זה ואפילו ירוויחו מהפרומפט המאוד ספציפי.
סוכן שלישי ממשיך עם בעיות רוחביות ודואג לחפש דברים שקשורים ל Thread Safety, סוכן רביעי מתמקד בשמות, סגנון ובעיות בשפה, לדוגמה:
סוכן חמישי מוקדש לטיפול בשגיאות והשישי לביצועים ואיכות כללית של הקוד עם הוראות כמו:
הביקורת שיש לי על רשימה כזאת היא לא בהכרח על הרשימה אלא על האנשים שישתמשו בה. רשימה מדויקת של הערות מעודדת את המודל למצוא את הבעיות ברשימה, גם אם מדובר ב False Positives, ולכן מפתחים שישתמשו ב Code Review מבוסס סוכנים עם רשימה ספציפית צריכים לשים לב:
1. לקרוא בצורה ביקורתית את הנקודות שהמודל מחזיר, להבין מה אמיתי ומה False Positive.
2. לקרוא בצורה ביקורתית את הקוד עצמו כדי למצוא נקודות שלא נמצאות ברשימה או שהמודל פספס.
3. לעדכן את הרשימה מדי פעם ככל שמגלים נקודות שלא עלו ב Code Reviews או נקודות שמעודדות False Positives. מוחקים מה שיצר טעויות ומוסיפים את מה שלא עלה.
אני גם לא בטוח ש Skill זה המנגנון הכי טוב לבנות דפי הנחיות כאלה. מצד אחד טוב שאפשר להתקין Skill-ים בקלות וכל הסוכנים אימצו תמיכה במנגנון. מצד שני אולי יהיה יותר פשוט ונכון להשתמש בקובץ Prompt ייעודי שיושב בתוך הפרויקט ומפתחים יודעים לקרוא אותו ולעדכן אותו.
החברים ב Qt שחררו מסמך סקיל שמלמד את קלוד קוד איך לעשות Code Review לקוד Qt. אפשר לקרוא כאן ואני ממליץ:
https://github.com/TheQtCompanyRnD/agent-skills/blob/main/skills/qt-cpp-review/SKILL.md
הנקודה הראשונה הקריטית שהמסמך מזכיר לנו בעבודה עם סוכני קידוד היא החשיבות של הפרומפט. מי שרוצה לקבל Code Review ברמה גבוהה לא יכול לבקש "תעשה לי Code Review" או אפילו "תעשה לי Code Review ותחפש בעיות ביצועים". הסוכן צריך פרומפט שישים אותו בנקודת התחלה מדויקת לגבי מה אתם מחפשים.
דף ההוראות של Qt מתחיל בזיהוי על מה בכלל אנחנו מחפשים לעשות Code Review, כאשר יש הוראות שונות אם רוצים לעשות Code Review על קומיט מסוים או Code Review על המערכת כולה. ממשיכים בבקשה לניתוח סטטי של הקוד ואז מגיע החלק המעניין - הגדרת 6 סוכני Review, כל אחד עם התפקיד שלו.
יש סוכנים שאחראיים על חיפוש בעיות נפוצות בקלאס מסוים או אוסף קלאסים, למשל הסוכן הראשון מחפש רק בעיות ב QAbstractItemModel. סוכן שני מחפש בכל המערכת בעיות זכרון, ושימו לב להגדרה שלו שכוללת ממש רשימה של בעיות זכרון נפוצות ב Qt:
Check for:
Structs/classes with raw pointers where new is visible and no corresponding delete/deleteLater/smart-pointer wrapping exists (Rule of Five violation)
Missing deleteLater() on QNetworkReply in finished handlers
Q_ASSERT wrapping side-effectful expressions (compiled out in release builds — the side effect disappears)
Q_ASSERT as the sole null guard (crashes in release)
Polymorphic QObject subclasses missing Q_DISABLE_COPY_MOVE
Polymorphic classes missing virtual destructor
QTimer/QObject created with new but no parent and no other lifecycle management (scope, smart pointer, explicit delete)
QObject::connect() called with potentially null sender/receiver outside a null guard (runtime warning)
m_recentlyAccessed-style tracking lists that maintain pointers to objects that may be deleted elsewhere (dangling)
Unbounded container growth (append without cap or trim)
Destructor not cleaning up owned children recursively
Abstract interfaces with no implementations beyond one class (YAGNI violation — codebase scope only)
References: references/qt-review-checklist.md § Ownership & Lifecycle, § Polymorphic Classes, § RAII Classes
גם אם במבט ראשון רשימה כזאת נראית כתופסת יותר מדי מקום ב Context Window, מודלים חזקים היום יודעים להתמודד עם זה ואפילו ירוויחו מהפרומפט המאוד ספציפי.
סוכן שלישי ממשיך עם בעיות רוחביות ודואג לחפש דברים שקשורים ל Thread Safety, סוכן רביעי מתמקד בשמות, סגנון ובעיות בשפה, לדוגמה:
Missing const on methods that don't modify state
סוכן חמישי מוקדש לטיפול בשגיאות והשישי לביצועים ואיכות כללית של הקוד עם הוראות כמו:
Expensive operation before cheap early-exit check (wasted allocation)
Missing re-entrancy guard on methods that emit signals which could trigger re-entry
הביקורת שיש לי על רשימה כזאת היא לא בהכרח על הרשימה אלא על האנשים שישתמשו בה. רשימה מדויקת של הערות מעודדת את המודל למצוא את הבעיות ברשימה, גם אם מדובר ב False Positives, ולכן מפתחים שישתמשו ב Code Review מבוסס סוכנים עם רשימה ספציפית צריכים לשים לב:
1. לקרוא בצורה ביקורתית את הנקודות שהמודל מחזיר, להבין מה אמיתי ומה False Positive.
2. לקרוא בצורה ביקורתית את הקוד עצמו כדי למצוא נקודות שלא נמצאות ברשימה או שהמודל פספס.
3. לעדכן את הרשימה מדי פעם ככל שמגלים נקודות שלא עלו ב Code Reviews או נקודות שמעודדות False Positives. מוחקים מה שיצר טעויות ומוסיפים את מה שלא עלה.
אני גם לא בטוח ש Skill זה המנגנון הכי טוב לבנות דפי הנחיות כאלה. מצד אחד טוב שאפשר להתקין Skill-ים בקלות וכל הסוכנים אימצו תמיכה במנגנון. מצד שני אולי יהיה יותר פשוט ונכון להשתמש בקובץ Prompt ייעודי שיושב בתוך הפרויקט ומפתחים יודעים לקרוא אותו ולעדכן אותו.
GitHub
agent-skills/skills/qt-cpp-review/SKILL.md at main · TheQtCompanyRnD/agent-skills
Official Qt AI engineering skills for Claude Code, Codex, Copilot, Gemini,and other AI coding tools - TheQtCompanyRnD/agent-skills
📌 למה בכלל לקרוא את הקוד
"למה לקרוא את הקוד שסוכן כותב" ו"איך לקרוא את הקוד שסוכן כותב" אלה שתי שאלות שונות. הרבה פעמים אנשים שלא יודעים לענות על השאלה השנייה משכנעים את עצמם שהראשונה לא חשובה.
מריו זכנר כתב סוכן קידוד בשם pi. מריו מבין דבר או שניים בסוכני קידוד ובמודלי שפה וההמלצה שלו היא חד משמעית:
בעברית אומר מריו דבר פשוט - כולם טועים, גם בני אדם וגם סוכני קידוד. אבל סוכני קידוד טועים הרבה יותר מהר. אנשים שלא קוראים את הקוד יצטרכו לקרוא אותו כשכבר יהיה מאוחר מדי, ובנקודה הזאת הם כבר לא ידעו איפה להתחיל.
סטיב יגה הגה את 8 השלבים של עבודה עם AI וכתב כבר שני כלים שתפקידם לסנכרן צבא של סוכני קידוד שעובדים במקביל. סטיב לא קורא את הקוד מתוך עקרון אבל גם הוא מדבר על איכות הקוד. בפוסט טיפים לעבודה עם סוכני קידוד הוא כותב:
סטיב יגה לא קורא את הקוד ולכן מעיד שהסוכן תמיד ימצא בעיות ואפילו בעיות שמפתיעות אותו. כמה בעיות עוד נשארו בקוד אחרי סבב ה Review של הסוכן? מספיק. האם יום אחד הכלי יהפוך לכזה בלאגן שסטיב כבר לא יוכל לתחזק אותו ויצטרך לבנות מחדש? ברור. באותו פוסט הוא מצהיר:
ברור שאם נתת לסוכן קידוד לבנות את המערכת בנקודה מסוימת די מוקדם בבנייה כבר לא תוכל לתקן את זה. סטיב מדבר על שנה-שנתיים אורך חיים של מוצרי תוכנה בעולם החדש. ובמהלך כל אותו זמן בלי לקרוא את הקוד אי אפשר לדעת כמה המוצר הזה מאובטח או יציב.
אין דבר יותר דחוף מבנייה מחדש של כל עולם הכשרת המתכנתים. המפתחים של העתיד יגיעו לקורס תכנות אחרי שהם כבר בנו פרויקטים עם AI, השתכנעו ב"למה לקרוא את הקוד" ועכשיו צריכים להבין את ה"איך". זה לא יהיה קל.
בינתיים בואו נשאיר בראש את ה"למה חשוב לקרוא את הקוד". גם אם כרגע ה"איך" עדיין נראה מאתגר.
"למה לקרוא את הקוד שסוכן כותב" ו"איך לקרוא את הקוד שסוכן כותב" אלה שתי שאלות שונות. הרבה פעמים אנשים שלא יודעים לענות על השאלה השנייה משכנעים את עצמם שהראשונה לא חשובה.
מריו זכנר כתב סוכן קידוד בשם pi. מריו מבין דבר או שניים בסוכני קידוד ובמודלי שפה וההמלצה שלו היא חד משמעית:
The problem with agents is that they make errors. Which is fine, humans also make errors
There's a much more important difference between clanker and human. A human is a bottleneck. A human cannot shit out 20,000 lines of code in a few hours. Even if the human creates such booboos at high frequency, there's only so many booboos the human can introduce in a codebase per day.
You have removed yourself from the loop, so you don't even know that all the innocent booboos have formed a monster of a codebase. You only feel the pain when it's too late.
בעברית אומר מריו דבר פשוט - כולם טועים, גם בני אדם וגם סוכני קידוד. אבל סוכני קידוד טועים הרבה יותר מהר. אנשים שלא קוראים את הקוד יצטרכו לקרוא אותו כשכבר יהיה מאוחר מדי, ובנקודה הזאת הם כבר לא ידעו איפה להתחיל.
סטיב יגה הגה את 8 השלבים של עבודה עם AI וכתב כבר שני כלים שתפקידם לסנכרן צבא של סוכני קידוד שעובדים במקביל. סטיב לא קורא את הקוד מתוך עקרון אבל גם הוא מדבר על איכות הקוד. בפוסט טיפים לעבודה עם סוכני קידוד הוא כותב:
Spend 40% of your time on code health, or else you’ll wind up spending >60%
Basically the agent will always find problems, often shocking ones, e.g. where you discover you have two or even three completely redundant systems (databases, logging, telemetry, whatever) that need consolidating.
סטיב יגה לא קורא את הקוד ולכן מעיד שהסוכן תמיד ימצא בעיות ואפילו בעיות שמפתיעות אותו. כמה בעיות עוד נשארו בקוד אחרי סבב ה Review של הסוכן? מספיק. האם יום אחד הכלי יהפוך לכזה בלאגן שסטיב כבר לא יוכל לתחזק אותו ויצטרך לבנות מחדש? ברור. באותו פוסט הוא מצהיר:
We are entering a surprising new phase of software development, in which rewriting things is often easier (and smarter) than trying to fix them.
ברור שאם נתת לסוכן קידוד לבנות את המערכת בנקודה מסוימת די מוקדם בבנייה כבר לא תוכל לתקן את זה. סטיב מדבר על שנה-שנתיים אורך חיים של מוצרי תוכנה בעולם החדש. ובמהלך כל אותו זמן בלי לקרוא את הקוד אי אפשר לדעת כמה המוצר הזה מאובטח או יציב.
אין דבר יותר דחוף מבנייה מחדש של כל עולם הכשרת המתכנתים. המפתחים של העתיד יגיעו לקורס תכנות אחרי שהם כבר בנו פרויקטים עם AI, השתכנעו ב"למה לקרוא את הקוד" ועכשיו צריכים להבין את ה"איך". זה לא יהיה קל.
בינתיים בואו נשאיר בראש את ה"למה חשוב לקרוא את הקוד". גם אם כרגע ה"איך" עדיין נראה מאתגר.
❤1
📌 חמש מיומנויות קריטיות למפתחים בעידן ה AI
כן המפתחים המובילים בתעשייה כבר משתמשים ב AI כדי לכתוב מעל 90% מהקוד שנכנס למוצר שלהם.
שמעתי גם על צוותים שמתעקשים לא לאפשר למפתחים לכתוב קוד ידנית במטרה לזהות Best Practices בעבודה עם AI, כלומר במקום לכתוב לבד תקרא מסודר את הפלט של ה AI, תבין למה הוא לא יצר את הקוד שרצית ותתקן את הפרומפט, קוד במקום אחר, קבצי תיעוד או מה שלא יהיה כדי לקבל את הקוד שצריך.
ולמרות שאף אחד לא יודע איך יראה המקצוע עוד שנתיים, יש כמה מיומנויות שבטוח נצטרך וכדאי לפתח כבר מאתמול:
הראשונה היא המיומנות הטכנית, היכרות עם הכלים. סוכני קידוד יכולים לעבוד מהענן, משורת הפקודה, מאפליקציית Desktop או מתוך VS Code. צריך לדעת איך לפנות אליהם, מה עושה כל כפתור ומה הפעולות המרכזיות שאפשר לעשות איתם בכל סביבה. המפתחים של העתיד ירגישו בנוח לדבר עם AI בכל כלי בו הוא נמצא.
מיומנות שניה היא ניהול קונטקסט (של ה AI) וכתיבת פרומפטים. ההבנה שכל דבר הוא פרומפט ושאם אני לא מקבל תשובה מספיק טובה מהמודל אני יכול לנסח פרומפט אחר ולשנות את הקלט כדי לשפר את התוצאות. סוכן קידוד הוא כמו טייס אוטומטי לקוד. כתיבת קונטקסט ופרומפט זה הגדרת היעד לטייס האוטומטי.
מיומנות שלישית היא הגדרת חיבורים ותהליכי עבודה: איך הסוכן יודע שהקוד שהוא כתב נכון או עובד? איך הקוד שקיים אצלנו במערכת משפיע על הקוד החדש שנוצר? ואיך אנחנו שומרים על איכות כשקוד נכתב יותר מהר ממה שאנחנו מצליחים להבין? בשביל לשפר את קצב הפיתוח נרצה להריץ סוכנים במקביל וסוכנים בענן, נרצה לבנות מערכות שמפתחות את עצמן ומזהות בעיות. בשביל לשמור על איכות הקוד נרצה לייצר צווארי בקבוק ומנגנונים שיאפשרו למפתחים לכוון את הסוכנים למימושים שאנחנו יודעים שהם נכונים. זאת כבר חשיבה מערכתית על תהליך הפיתוח וניתוח ה Outputs של התהליך כדי לשפר אותו.
מלבד שלושת המיומנויות האלה שהן ייחודיות ל AI אני מזהה עוד שתי מיומנויות שתמיד היו חשובות ומקבלות חיזוק משמעותי בעידן החדש:
הראשונה היא המיומנות ההנדסית, להבין מה אני בונה, איך החלקים במערכת עובדים, לראות מקרי קצה, שגיאות, להבין איך קוד גורם למערכות לעבוד. היכולת לקרוא ולהבין מהר.
מיומנות שניה היא ניהול קונטקסט, אבל הפעם שלנו, מה שנקרא Context Switching. מפתחים כבר היום עובדים במקביל עם מספר סוכני קידוד והמספרים האלה רק יגדלו. אנחנו קופצים ממשימה למשימה, מסוכן לסוכן, מפיצ'ר לפיצ'ר. אי אפשר לדמיין יותר מפתח שיושב שבועיים בחדר סגור לתכנן פיצ'ר ועוד שלושה חודשים לכתוב את הקוד. העתיד מורכב ממקביליות ומביצוע המון משימות יחד.
תחום GenAI נמצא בפיתוח וכל הזמן יש מוצרים חדשים. המטרה שלנו היא לא להכיר כל כלי שיוצא אלא להתמקד במיומנויות ולחפש את הכלים שיאפשרו לנו לכתוב קוד מהר יותר בלי להתפשר על איכות המוצר.
כן המפתחים המובילים בתעשייה כבר משתמשים ב AI כדי לכתוב מעל 90% מהקוד שנכנס למוצר שלהם.
שמעתי גם על צוותים שמתעקשים לא לאפשר למפתחים לכתוב קוד ידנית במטרה לזהות Best Practices בעבודה עם AI, כלומר במקום לכתוב לבד תקרא מסודר את הפלט של ה AI, תבין למה הוא לא יצר את הקוד שרצית ותתקן את הפרומפט, קוד במקום אחר, קבצי תיעוד או מה שלא יהיה כדי לקבל את הקוד שצריך.
ולמרות שאף אחד לא יודע איך יראה המקצוע עוד שנתיים, יש כמה מיומנויות שבטוח נצטרך וכדאי לפתח כבר מאתמול:
הראשונה היא המיומנות הטכנית, היכרות עם הכלים. סוכני קידוד יכולים לעבוד מהענן, משורת הפקודה, מאפליקציית Desktop או מתוך VS Code. צריך לדעת איך לפנות אליהם, מה עושה כל כפתור ומה הפעולות המרכזיות שאפשר לעשות איתם בכל סביבה. המפתחים של העתיד ירגישו בנוח לדבר עם AI בכל כלי בו הוא נמצא.
מיומנות שניה היא ניהול קונטקסט (של ה AI) וכתיבת פרומפטים. ההבנה שכל דבר הוא פרומפט ושאם אני לא מקבל תשובה מספיק טובה מהמודל אני יכול לנסח פרומפט אחר ולשנות את הקלט כדי לשפר את התוצאות. סוכן קידוד הוא כמו טייס אוטומטי לקוד. כתיבת קונטקסט ופרומפט זה הגדרת היעד לטייס האוטומטי.
מיומנות שלישית היא הגדרת חיבורים ותהליכי עבודה: איך הסוכן יודע שהקוד שהוא כתב נכון או עובד? איך הקוד שקיים אצלנו במערכת משפיע על הקוד החדש שנוצר? ואיך אנחנו שומרים על איכות כשקוד נכתב יותר מהר ממה שאנחנו מצליחים להבין? בשביל לשפר את קצב הפיתוח נרצה להריץ סוכנים במקביל וסוכנים בענן, נרצה לבנות מערכות שמפתחות את עצמן ומזהות בעיות. בשביל לשמור על איכות הקוד נרצה לייצר צווארי בקבוק ומנגנונים שיאפשרו למפתחים לכוון את הסוכנים למימושים שאנחנו יודעים שהם נכונים. זאת כבר חשיבה מערכתית על תהליך הפיתוח וניתוח ה Outputs של התהליך כדי לשפר אותו.
מלבד שלושת המיומנויות האלה שהן ייחודיות ל AI אני מזהה עוד שתי מיומנויות שתמיד היו חשובות ומקבלות חיזוק משמעותי בעידן החדש:
הראשונה היא המיומנות ההנדסית, להבין מה אני בונה, איך החלקים במערכת עובדים, לראות מקרי קצה, שגיאות, להבין איך קוד גורם למערכות לעבוד. היכולת לקרוא ולהבין מהר.
מיומנות שניה היא ניהול קונטקסט, אבל הפעם שלנו, מה שנקרא Context Switching. מפתחים כבר היום עובדים במקביל עם מספר סוכני קידוד והמספרים האלה רק יגדלו. אנחנו קופצים ממשימה למשימה, מסוכן לסוכן, מפיצ'ר לפיצ'ר. אי אפשר לדמיין יותר מפתח שיושב שבועיים בחדר סגור לתכנן פיצ'ר ועוד שלושה חודשים לכתוב את הקוד. העתיד מורכב ממקביליות ומביצוע המון משימות יחד.
תחום GenAI נמצא בפיתוח וכל הזמן יש מוצרים חדשים. המטרה שלנו היא לא להכיר כל כלי שיוצא אלא להתמקד במיומנויות ולחפש את הכלים שיאפשרו לנו לכתוב קוד מהר יותר בלי להתפשר על איכות המוצר.
ToCode
📌 חמש מיומנויות קריטיות למפתחים בעידן ה AI כן המפתחים המובילים בתעשייה כבר משתמשים ב AI כדי לכתוב מעל 90% מהקוד שנכנס למוצר שלהם. שמעתי גם על צוותים שמתעקשים לא לאפשר למפתחים לכתוב קוד ידנית במטרה לזהות Best Practices בעבודה עם AI, כלומר במקום לכתוב לבד תקרא…
ונ.ב. סופר קריטי לפוסט הזה ששכחתי בזמן הכתיבה - קריאה ביקורתית
לא לקבל קוד ש AI כותב בלי שאני מבין את זה כאילו זה קוד שאני כתבתי
לא לקבל קוד ש AI כותב בלי שאני מבין את זה כאילו זה קוד שאני כתבתי
📌 מתכנת עם מברג בכיס
בטיול בשוק בתאילנד הבת שלי רצתה לקנות חרב אור. היתה רק בעיה אחת עם החרב: כל פעם שהדלקת את האור היא גם עשתה רעש נוראי. הילדה לא רצתה את הרעש ואני בטח לא רציתי את הרעש. כששאלתי את המוכר אם יש גרסה של החרב בלי הרעש הוא ענה שלא אבל לא לדאוג הוא תכף מסדר, ובאמת הוא הוציא מהכיס מברג, פירק את החרב, חתך את החוט של הרמקול, הרכיב חזרה ושלום על ישראל, יש לנו חרב אור שרק עושה אור ולא מרעישה.
עוד לפני אותו טיול פגשתי הרבה מפתחים שהחזיקו מברג וירטואלי בכיס כדי לפתור כל בעיה. המערכת מכוערת? אין בעיה, נכנסים לקוד, מוסיפים כמה קבצי CSS, לא שוברים שום דבר ומחברים חזרה. יומיים עבודה ויש לך עיצוב חדש. השרת איטי? אין בעיה, נכנסים לקוד שמתקשר עם ה DB, משפרים את השאילתות, מוסיפים אינדקסים והפקק משתחרר. מתכנתי המברג הם אנשים סופר מקצועיים, הם יודעים להכנס למערכת בזהירות, לחזק בדיוק את הבורג שצריך ולעזוב כשדברים במצב קצת יותר טוב ממה שהיה כשהתחילו.
השנים הקרובות לא יאירו פנים למתכנתי המברג.
הבעיה של מתכנתי המברג היא לא ידע מקצועי. אנחנו מדברים על אנשים שיכולים להכנס ולתקן כל סוג של מערכת, קוראים קוד בכל שפה, מזהים בעיות לפני כולם ורואים את הקשרים העדינים בין המערכות שאנשים רגילים מפספסים. מתכנתי המברג יודעים מה הם עושים ואיזה חוט בדיוק צריך לנתק.
הבעיה שלהם היא הגישה.
עד AI היו הרבה בעיות שהיה כדאי לפתור עם חיזוק בורג קטן. היום AI מחזק את כל הברגים. אבל יותר מזה, ה AI הופך תיקונים מסובכים למהירים - בתנאי שאנחנו יודעים מה בדיוק צריך לתקן.
מתכנת שיפוצניק יכול לזהות מעקף שיסדר 90% מהבעיות. עד לא מזמן הייתי חותם על כזה פתרון בלי לחשוב פעמיים, בטח כשזה בא במקום לשנות עשרות קבצי קוד. היום? ברגע שאני מבין את הבעיה ואיך הקוד אמור להיראות אני שולח את קלוד לשנות את כל המערכת כדי להתאים לגישה החדשה והנכונה ומתרחק ממעקפים. כל מעקף שאני שם רק יעודד את הסוכן לייצר עוד עשרות פתרונות עקומים.
הגיע הזמן להיפרד מהמברג הקטן. בשביל התיקונים של העתיד נצטרך לאמץ חשיבה מערכתית.
בטיול בשוק בתאילנד הבת שלי רצתה לקנות חרב אור. היתה רק בעיה אחת עם החרב: כל פעם שהדלקת את האור היא גם עשתה רעש נוראי. הילדה לא רצתה את הרעש ואני בטח לא רציתי את הרעש. כששאלתי את המוכר אם יש גרסה של החרב בלי הרעש הוא ענה שלא אבל לא לדאוג הוא תכף מסדר, ובאמת הוא הוציא מהכיס מברג, פירק את החרב, חתך את החוט של הרמקול, הרכיב חזרה ושלום על ישראל, יש לנו חרב אור שרק עושה אור ולא מרעישה.
עוד לפני אותו טיול פגשתי הרבה מפתחים שהחזיקו מברג וירטואלי בכיס כדי לפתור כל בעיה. המערכת מכוערת? אין בעיה, נכנסים לקוד, מוסיפים כמה קבצי CSS, לא שוברים שום דבר ומחברים חזרה. יומיים עבודה ויש לך עיצוב חדש. השרת איטי? אין בעיה, נכנסים לקוד שמתקשר עם ה DB, משפרים את השאילתות, מוסיפים אינדקסים והפקק משתחרר. מתכנתי המברג הם אנשים סופר מקצועיים, הם יודעים להכנס למערכת בזהירות, לחזק בדיוק את הבורג שצריך ולעזוב כשדברים במצב קצת יותר טוב ממה שהיה כשהתחילו.
השנים הקרובות לא יאירו פנים למתכנתי המברג.
הבעיה של מתכנתי המברג היא לא ידע מקצועי. אנחנו מדברים על אנשים שיכולים להכנס ולתקן כל סוג של מערכת, קוראים קוד בכל שפה, מזהים בעיות לפני כולם ורואים את הקשרים העדינים בין המערכות שאנשים רגילים מפספסים. מתכנתי המברג יודעים מה הם עושים ואיזה חוט בדיוק צריך לנתק.
הבעיה שלהם היא הגישה.
עד AI היו הרבה בעיות שהיה כדאי לפתור עם חיזוק בורג קטן. היום AI מחזק את כל הברגים. אבל יותר מזה, ה AI הופך תיקונים מסובכים למהירים - בתנאי שאנחנו יודעים מה בדיוק צריך לתקן.
מתכנת שיפוצניק יכול לזהות מעקף שיסדר 90% מהבעיות. עד לא מזמן הייתי חותם על כזה פתרון בלי לחשוב פעמיים, בטח כשזה בא במקום לשנות עשרות קבצי קוד. היום? ברגע שאני מבין את הבעיה ואיך הקוד אמור להיראות אני שולח את קלוד לשנות את כל המערכת כדי להתאים לגישה החדשה והנכונה ומתרחק ממעקפים. כל מעקף שאני שם רק יעודד את הסוכן לייצר עוד עשרות פתרונות עקומים.
הגיע הזמן להיפרד מהמברג הקטן. בשביל התיקונים של העתיד נצטרך לאמץ חשיבה מערכתית.
👍3
📌 קריאה מודרכת בלולאת הסוכן של פאי
פאי הוא סוכן קידוד מינימליסטי, מה שאומר שהוא כולל מעט מאוד פיצ'רים ובשביל לקבל ממנו תוצאות טובות עלינו לבנות לבד הרחבות. עוד זה אומר שאפשר להכנס די בקלות לקוד שלו ולהבין מה קורה שם, שזה נפלא למי שרוצה להבין איך דברים עובדים.
הריפו של פאי נמצא כאן:
https://github.com/badlogic/pi-mono/
ובשביל להבין איך עובדת לולאת סוכן נסתכל על הקובץ:
https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent-loop.ts
ונחפש את הפונקציה runLoop. זה המימוש כמו שהוא - סופר קריא ומתועד:
אלה החלקים המרכזיים:
1. הסוכן מאפשר למשתמשים להקליד הודעות בזמן שהמודל משלים טקסט. אלה ה pendingMessages ואנחנו מקבלים אותן מהפונקציה
2. לולאת הסוכן מורכבת מלולאה כפולה, הלולאה החיצונית מטפלת בהודעות נוספות שאנחנו כותבים אחרי שהסוכן מתחיל לעבוד. הלולאה הפנימית אחראית על מענה לפרומפט.
3. הלולאה הפנימית מעניינת - למה צריך לענות לפרומפט בלולאה? למה לא לשלוח את ההודעה למודל, לקבל השלמה וזהו? התשובה היא מנגנון בסיסי של סוכני קידוד שנקרא הפעלת כלים.
4. נשים לב שכמעט בכל שלב בלולאה, וזה נכון באופן כללי לקוד של פאי, יש לנו קריאות ל emit עם מזהה אירוע. אלה "נקודות התחברות". בעבודה עם פאי נוכל לכתוב תוספים שיופעלו בכל נקודת התחברות שנרצה.
פאי הוא סוכן קידוד מינימליסטי, מה שאומר שהוא כולל מעט מאוד פיצ'רים ובשביל לקבל ממנו תוצאות טובות עלינו לבנות לבד הרחבות. עוד זה אומר שאפשר להכנס די בקלות לקוד שלו ולהבין מה קורה שם, שזה נפלא למי שרוצה להבין איך דברים עובדים.
הריפו של פאי נמצא כאן:
https://github.com/badlogic/pi-mono/
ובשביל להבין איך עובדת לולאת סוכן נסתכל על הקובץ:
https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent-loop.ts
ונחפש את הפונקציה runLoop. זה המימוש כמו שהוא - סופר קריא ומתועד:
async function runLoop(
currentContext: AgentContext,
newMessages: AgentMessage[],
config: AgentLoopConfig,
signal: AbortSignal | undefined,
emit: AgentEventSink,
streamFn?: StreamFn,
): Promise<void> {
let firstTurn = true;
// Check for steering messages at start (user may have typed while waiting)
let pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || [];
// Outer loop: continues when queued follow-up messages arrive after agent would stop
while (true) {
let hasMoreToolCalls = true;
// Inner loop: process tool calls and steering messages
while (hasMoreToolCalls || pendingMessages.length > 0) {
if (!firstTurn) {
await emit({ type: "turn_start" });
} else {
firstTurn = false;
}
// Process pending messages (inject before next assistant response)
if (pendingMessages.length > 0) {
for (const message of pendingMessages) {
await emit({ type: "message_start", message });
await emit({ type: "message_end", message });
currentContext.messages.push(message);
newMessages.push(message);
}
pendingMessages = [];
}
// Stream assistant response
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
newMessages.push(message);
if (message.stopReason === "error" || message.stopReason === "aborted") {
await emit({ type: "turn_end", message, toolResults: [] });
await emit({ type: "agent_end", messages: newMessages });
return;
}
// Check for tool calls
const toolCalls = message.content.filter((c) => c.type === "toolCall");
const toolResults: ToolResultMessage[] = [];
hasMoreToolCalls = false;
if (toolCalls.length > 0) {
const executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit);
toolResults.push(...executedToolBatch.messages);
hasMoreToolCalls = !executedToolBatch.terminate;
for (const result of toolResults) {
currentContext.messages.push(result);
newMessages.push(result);
}
}
await emit({ type: "turn_end", message, toolResults });
if (
await config.shouldStopAfterTurn?.({
message,
toolResults,
context: currentContext,
newMessages,
})
) {
await emit({ type: "agent_end", messages: newMessages });
return;
}
pendingMessages = (await config.getSteeringMessages?.()) || [];
}
// Agent would stop here. Check for follow-up messages.
const followUpMessages = (await config.getFollowUpMessages?.()) || [];
if (followUpMessages.length > 0) {
// Set as pending so inner loop processes them
pendingMessages = followUpMessages;
continue;
}
// No more messages, exit
break;
}
await emit({ type: "agent_end", messages: newMessages });
}
אלה החלקים המרכזיים:
1. הסוכן מאפשר למשתמשים להקליד הודעות בזמן שהמודל משלים טקסט. אלה ה pendingMessages ואנחנו מקבלים אותן מהפונקציה
getSteeringMessages כבר בתחילת הפונקציה.2. לולאת הסוכן מורכבת מלולאה כפולה, הלולאה החיצונית מטפלת בהודעות נוספות שאנחנו כותבים אחרי שהסוכן מתחיל לעבוד. הלולאה הפנימית אחראית על מענה לפרומפט.
3. הלולאה הפנימית מעניינת - למה צריך לענות לפרומפט בלולאה? למה לא לשלוח את ההודעה למודל, לקבל השלמה וזהו? התשובה היא מנגנון בסיסי של סוכני קידוד שנקרא הפעלת כלים.
4. נשים לב שכמעט בכל שלב בלולאה, וזה נכון באופן כללי לקוד של פאי, יש לנו קריאות ל emit עם מזהה אירוע. אלה "נקודות התחברות". בעבודה עם פאי נוכל לכתוב תוספים שיופעלו בכל נקודת התחברות שנרצה.
GitHub
GitHub - earendil-works/pi: AI agent toolkit: coding agent CLI, unified LLM API, TUI & web UI libraries, Slack bot, vLLM pods
AI agent toolkit: coding agent CLI, unified LLM API, TUI & web UI libraries, Slack bot, vLLM pods - earendil-works/pi
5. אם יש הודעות ניווט ממתינות נוסיף אותן לרשימת ההודעות של השיחה.
עכשיו מגיעה השורה הכי חשובה של הפונקציה:
שורה זו פונה למודל ומבקשת את ההודעה הבאה. הקונטקסט כולל את כל ההודעות בשיחה. אם המודל החזיר שגיאה אנחנו מסיימים כאן את הפונקציה, אם לא אנחנו מפעילים עוד שורה חשובה:
מודלי שפה רבים יודעים לעבוד עם כלים. עבודה עם כלים אומרת שהמודל מחזיר לסוכן אוביקט שאומר איזה כלי צריך להפעיל. כלי הוא פונקציה שסוכן הקידוד מגדיר והמודל צריך את התוצאה שלה כדי לבצע משימה. כשתשובת המודל כוללת בקשות להפעלת כלים הסוכן תופס את הבקשות ומפעיל את הכלים ברשימה. הסוכן יוסיף את תוצאות הפעלת הכלים לקונטקסט ובאיטרציה הבאה של הלולאה המודל כבר יקבל את ההודעות יחד עם תוצאות הפעלת הכלים כדי שאפשר יהיה להתקדם.
אחרי ששלחנו הודעה למודל, קיבלנו תשובה, הפעלנו כלים והדבקנו את התשובות שלהם על הקונטקסט אנחנו מגיעים לנקודה שנקראת
לולאת הסוכן היא הלב של סוכני קידוד והיא מטפלת בכל הודעה שאנחנו שולחים. בזכות פאי ראינו איך הלולאה הזו בנויה ומה היא מאפשרת. הלולאה שקראנו נקראת לולאת React שזה קיצור של Reason ו Act, וזה אומר שהקוד שולח הודעה למודל שפה, נותן לו הזדמנות להריץ כלים, שולח את התוצאה של הכלים שוב למודל וממשיך בלולאה עד שאין יותר קלים להפעיל.
✏ שאלות למיטיבי לכת
קראו את הקוד ונסו לחשוב:
1. באיזה מצבים נרצה לעצור את לולאת הסוכן כשהמודל רוצה להמשיך ולהפעיל עוד כלים?
2. איזה כלים עשויים לגרום לסיום הלולאה?
3. האם לולאה זו מתאימה לכל מצבי העבודה שאנחנו מכירים עם סוכנים חכמים? מה קורה במצב תכנון? במצב טייס אוטומטי? במצב Ask?
עכשיו מגיעה השורה הכי חשובה של הפונקציה:
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
שורה זו פונה למודל ומבקשת את ההודעה הבאה. הקונטקסט כולל את כל ההודעות בשיחה. אם המודל החזיר שגיאה אנחנו מסיימים כאן את הפונקציה, אם לא אנחנו מפעילים עוד שורה חשובה:
const toolCalls = message.content.filter((c) => c.type === "toolCall");
מודלי שפה רבים יודעים לעבוד עם כלים. עבודה עם כלים אומרת שהמודל מחזיר לסוכן אוביקט שאומר איזה כלי צריך להפעיל. כלי הוא פונקציה שסוכן הקידוד מגדיר והמודל צריך את התוצאה שלה כדי לבצע משימה. כשתשובת המודל כוללת בקשות להפעלת כלים הסוכן תופס את הבקשות ומפעיל את הכלים ברשימה. הסוכן יוסיף את תוצאות הפעלת הכלים לקונטקסט ובאיטרציה הבאה של הלולאה המודל כבר יקבל את ההודעות יחד עם תוצאות הפעלת הכלים כדי שאפשר יהיה להתקדם.
אחרי ששלחנו הודעה למודל, קיבלנו תשובה, הפעלנו כלים והדבקנו את התשובות שלהם על הקונטקסט אנחנו מגיעים לנקודה שנקראת
turn_end. פאי יאפשר לנו לכתוב תוספים שיתפסו את הנקודה הזאת ויעצרו את הלולאה כאן, ואם אין בקשה מיוחדת לעצירה אנחנו ממשיכים לאיטרציה הבאה. לולאת הפעלת הכלים תיעצר כשהמודל לא יבקש להפעיל שוב כלים, כשהסוכן יחליט שהפעלנו מספיק כלים וצריך לעצור או כשכל הכלים מחזירים ערך עצירה.לולאת הסוכן היא הלב של סוכני קידוד והיא מטפלת בכל הודעה שאנחנו שולחים. בזכות פאי ראינו איך הלולאה הזו בנויה ומה היא מאפשרת. הלולאה שקראנו נקראת לולאת React שזה קיצור של Reason ו Act, וזה אומר שהקוד שולח הודעה למודל שפה, נותן לו הזדמנות להריץ כלים, שולח את התוצאה של הכלים שוב למודל וממשיך בלולאה עד שאין יותר קלים להפעיל.
✏ שאלות למיטיבי לכת
קראו את הקוד ונסו לחשוב:
1. באיזה מצבים נרצה לעצור את לולאת הסוכן כשהמודל רוצה להמשיך ולהפעיל עוד כלים?
2. איזה כלים עשויים לגרום לסיום הלולאה?
3. האם לולאה זו מתאימה לכל מצבי העבודה שאנחנו מכירים עם סוכנים חכמים? מה קורה במצב תכנון? במצב טייס אוטומטי? במצב Ask?
📌 איטרציות מהירות, פיצ'רים איטיים
כולם אומרים שקוד הפך זול אפילו חינם. אבל קוד הוא חינם כמו שכלבלב הוא בחינם. אחרי שהכנסת אותו אתה תקוע איתו.
ההזדמנות ב AI היא לא לייצר יותר קוד,
אנחנו לא צריכים יותר קוד, ברוב המערכות ממילא יש יותר מדי.
ההזדמנות היא לייצר את הקוד הנכון.
יותר איטרציות,
יותר מחיקות,
יותר שינויים ענקיים,
יותר מיגרציות,
פחות היקשרות רגשית למנגנונים.
כש AI כותב לי שהוא כתב את הקוד שישבור הכי פחות דברים אני מבין שיש בעיה ב System Prompt. המטרה היא לא לשמור על הקיים אלא לכתוב ולזרוק, לכתוב ולזרוק. איטרציות מהירים, פיצ'רים איטיים.
כולם אומרים שקוד הפך זול אפילו חינם. אבל קוד הוא חינם כמו שכלבלב הוא בחינם. אחרי שהכנסת אותו אתה תקוע איתו.
ההזדמנות ב AI היא לא לייצר יותר קוד,
אנחנו לא צריכים יותר קוד, ברוב המערכות ממילא יש יותר מדי.
ההזדמנות היא לייצר את הקוד הנכון.
יותר איטרציות,
יותר מחיקות,
יותר שינויים ענקיים,
יותר מיגרציות,
פחות היקשרות רגשית למנגנונים.
כש AI כותב לי שהוא כתב את הקוד שישבור הכי פחות דברים אני מבין שיש בעיה ב System Prompt. המטרה היא לא לשמור על הקיים אלא לכתוב ולזרוק, לכתוב ולזרוק. איטרציות מהירים, פיצ'רים איטיים.
📌 הוק לקלוד עבור קומיט אחרי כל שינוי
אתמול בוובינר הראיתי איך להשתמש בגיט בעבודה עם סוכני קידוד ואחת ההמלצות החשובות היתה לא לפחד מקומיטים. כל פעם שיש שינוי קוד עושים קומיט ואחרי זה אם מתחרטים אפשר להשתמש ב
חלק מהמשתתפים העירו שאפשר לעשות את הקומיטים האלה אוטומטית ואז לא שוכחים. הלכתי לחקור את הרעיון ושמחתי לראות שזה עבד די בקלות. אם אתם בקלוד קוד אפשר ליצור קובץ
ואז הסקריפט
קלוד מפעיל את הסקריפט עם JSON של מידע על השיחה האחרונה. מה שמעניין אותנו מתוך ה JSON הזה הוא ההודעה של המשתמש, כי אני אוהב שהפרומפט שלי הופך להודעת הקומיט (אם צריך הודעה יותר מתוחכמת אפשר להשתמש ב
סך הכל רעיון מעניין. שימו לב שאפשר להפעיל את ה hook או מקומית לפרויקט הנוכחי או באופן גלובאלי אם רושמים אותו ב settings של קלוד בתיקיית הבית. אני מתכנן להתחיל עם פרויקט אחד ולראות איך זה יעבוד לאורך זמן.
אתמול בוובינר הראיתי איך להשתמש בגיט בעבודה עם סוכני קידוד ואחת ההמלצות החשובות היתה לא לפחד מקומיטים. כל פעם שיש שינוי קוד עושים קומיט ואחרי זה אם מתחרטים אפשר להשתמש ב
git reset --hard כדי לחזור אחורה.חלק מהמשתתפים העירו שאפשר לעשות את הקומיטים האלה אוטומטית ואז לא שוכחים. הלכתי לחקור את הרעיון ושמחתי לראות שזה עבד די בקלות. אם אתם בקלוד קוד אפשר ליצור קובץ
.claude/settings.json בתיקיית הפרויקט עם התוכן הבא:{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/auto-commit.sh",
"async": true,
"timeout": 30
}
]
}
]
}
}ואז הסקריפט
auto-commit.sh ירוץ כל פעם שקלוד מסיים לכתוב תשובה. את הסקריפט עצמו נתתי לקלוד לכתוב והוא נראה כך:#!/usr/bin/env bash
# Stop hook: auto-commit Claude's changes using the last user prompt
# as the commit message.
set -uo pipefail
INPUT=$(cat)
read -r STOP_HOOK_ACTIVE TRANSCRIPT_PATH PROJECT_DIR < <(
printf '%s' "$INPUT" | jq -r '[.stop_hook_active // false, .transcript_path // "", .cwd // ""] | @tsv'
)
[ "$STOP_HOOK_ACTIVE" = "true" ] && exit 0
[ -n "$PROJECT_DIR" ] && cd "$PROJECT_DIR" 2>/dev/null || exit 0
git rev-parse --git-dir >/dev/null 2>&1 || exit 0
git add -A 2>/dev/null || exit 0
git diff --cached --quiet && exit 0
# Last user text message from this turn's transcript.
# Skips tool_result entries and slash-commands (which start with '<').
USER_PROMPT=""
if [ -f "$TRANSCRIPT_PATH" ]; then
USER_PROMPT=$(jq -r '
select(.type == "user") | .message.content
| if type == "string" then .
elif type == "array" then (map(select(.type == "text")) | .[0].text // "")
else "" end
| select(. != "" and (startswith("<") | not))
' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1)
fi
# Use the user prompt as the commit message; first line as subject (capped at
# 72 chars), remaining lines as the body.
if [ -n "$USER_PROMPT" ]; then
SUBJECT=$(printf '%s' "$USER_PROMPT" | head -1 | cut -c1-72)
BODY=$(printf '%s' "$USER_PROMPT" | tail -n +2)
else
SUBJECT="checkpoint: $(date +%H:%M)"
BODY=""
fi
if [ -n "$BODY" ]; then
git commit --no-verify -m "$SUBJECT" -m "$BODY" >/dev/null 2>&1 || true
else
git commit --no-verify -m "$SUBJECT" >/dev/null 2>&1 || true
fi
exit 0
קלוד מפעיל את הסקריפט עם JSON של מידע על השיחה האחרונה. מה שמעניין אותנו מתוך ה JSON הזה הוא ההודעה של המשתמש, כי אני אוהב שהפרומפט שלי הופך להודעת הקומיט (אם צריך הודעה יותר מתוחכמת אפשר להשתמש ב
git commit --amend כדי לתקן). ליתר בטחון קלוד הוסיף --no-verify לפקודת הקומיט כדי לדלג על קומיט הוקס. אני חושב שזה הוגן וממילא לא משתמש בקומיט הוקס. יש גם בדיקה בהתחלה שמדלגת על כל הסקריפט אם אנחנו לא בתוך ריפו.סך הכל רעיון מעניין. שימו לב שאפשר להפעיל את ה hook או מקומית לפרויקט הנוכחי או באופן גלובאלי אם רושמים אותו ב settings של קלוד בתיקיית הבית. אני מתכנן להתחיל עם פרויקט אחד ולראות איך זה יעבוד לאורך זמן.