📌 כן הקוד שלי
אנתוני מאננינג כתב פוסט ארוך ומשכנע נגד שימוש ב AI לקידוד. ממליץ לקרוא אותו כאן:
https://antman-does-software.com/i-will-never-use-ai-to-code-or-write
אנתוני משווה בין קידוד לכתיבת ספר. כמו שלא היית נותן ל AI לכתוב את הרומן הבא שלך ומבקש ממנו "רק שפר קצת את הדמות הזאת", כי בכתיבה כל מילה חשובה, כך גם בקידוד אם תתן ל AI לכתוב בשבילך את הקוד זה כבר לא יהיה הקוד שלך. כל מילה חשובה.
זה טיעון מעניין אבל לדעתי שגוי, לא משקף את החיים האמיתיים של מפתחים גם לפני AI ולא עוזר לנו לכתוב קוד טוב יותר.
בחיים האמיתיים, גם לפני AI רוב הקוד שכתבנו לא היה 100% יצירה שלנו. עבודה על מערכות קיימות, עבודה בצוות, שימוש בספריות שלא תמיד אנחנו בחרנו ואילוצי לו"ז הביאו לכך שמערכות תוכנה של העולם האמיתי הן לא בדיוק יצירות אומנות. יש בהן יופי אני לא מכחיש, אבל תמיד יש מה לשפר. לא קורה שמפתח חדש מגיע לפרויקט ואומר "וואו איזה קוד מדהים יש פה". קוד הוא לא יצירת מופת של מפתח בודד אלא תוצאה של אילוצים. מפתחים טובים יודעים איך לדחוף את מערכת האילוצים הזאת לכיוון קצת יותר בריא ולהשפיע על המערכת עצמה כדי שהקוד יגדל בצורה טבעית בצורה טובה יותר.
כשאנחנו מיישמים את אותם עקרונות על פיתוח באמצעות AI אנחנו מגלים שמפתחים טובים לא הולכים לאיבוד אלא להיפך, מוצאים יותר דרכים להביע את עצמם:
1. מפתחים טובים מגדירים תבניות בקוד אותן ה AI ישכפל.
2. מפתחים טובים מגדירים שיטות עבודה - איך עושים Deployment, מה בודקים, איך בודקים. סוכני קידוד ישתמשו בשיטות עבודה אלה באמצעות MCP ויחסכו עבודה של בני אדם.
3. מפתחים טובים מגדירים אבסטרקציות ורכיבי קוד בסיסיים, בהם סוכני הקידוד ישתמשו כדי לבנות את הדברים הבאים.
העובדה שאני יכול לבקש מסוכן קידוד לבנות לי פיצ'ר חדש במערכת, בכל פרויקט לא טריוויאלי, היא לא תוצאה של החוכמה של סוכן הקידוד אלא של מבנה טוב של המערכת. ולראיה בהרבה פרויקטים סוכני הקידוד לא מצליחים לייצר תוצאות טובות ומפתחים מבזבזים שעות בריבים עם הסוכנים.
כן הקוד שלי, המערכת שלי, המבנה שלי. אני מכיר כל שורה בו ומכתיב את הכיוון, אפילו שאת רובו לא הקלדתי בעצמי.
אנתוני מאננינג כתב פוסט ארוך ומשכנע נגד שימוש ב AI לקידוד. ממליץ לקרוא אותו כאן:
https://antman-does-software.com/i-will-never-use-ai-to-code-or-write
אנתוני משווה בין קידוד לכתיבת ספר. כמו שלא היית נותן ל AI לכתוב את הרומן הבא שלך ומבקש ממנו "רק שפר קצת את הדמות הזאת", כי בכתיבה כל מילה חשובה, כך גם בקידוד אם תתן ל AI לכתוב בשבילך את הקוד זה כבר לא יהיה הקוד שלך. כל מילה חשובה.
זה טיעון מעניין אבל לדעתי שגוי, לא משקף את החיים האמיתיים של מפתחים גם לפני AI ולא עוזר לנו לכתוב קוד טוב יותר.
בחיים האמיתיים, גם לפני AI רוב הקוד שכתבנו לא היה 100% יצירה שלנו. עבודה על מערכות קיימות, עבודה בצוות, שימוש בספריות שלא תמיד אנחנו בחרנו ואילוצי לו"ז הביאו לכך שמערכות תוכנה של העולם האמיתי הן לא בדיוק יצירות אומנות. יש בהן יופי אני לא מכחיש, אבל תמיד יש מה לשפר. לא קורה שמפתח חדש מגיע לפרויקט ואומר "וואו איזה קוד מדהים יש פה". קוד הוא לא יצירת מופת של מפתח בודד אלא תוצאה של אילוצים. מפתחים טובים יודעים איך לדחוף את מערכת האילוצים הזאת לכיוון קצת יותר בריא ולהשפיע על המערכת עצמה כדי שהקוד יגדל בצורה טבעית בצורה טובה יותר.
כשאנחנו מיישמים את אותם עקרונות על פיתוח באמצעות AI אנחנו מגלים שמפתחים טובים לא הולכים לאיבוד אלא להיפך, מוצאים יותר דרכים להביע את עצמם:
1. מפתחים טובים מגדירים תבניות בקוד אותן ה AI ישכפל.
2. מפתחים טובים מגדירים שיטות עבודה - איך עושים Deployment, מה בודקים, איך בודקים. סוכני קידוד ישתמשו בשיטות עבודה אלה באמצעות MCP ויחסכו עבודה של בני אדם.
3. מפתחים טובים מגדירים אבסטרקציות ורכיבי קוד בסיסיים, בהם סוכני הקידוד ישתמשו כדי לבנות את הדברים הבאים.
העובדה שאני יכול לבקש מסוכן קידוד לבנות לי פיצ'ר חדש במערכת, בכל פרויקט לא טריוויאלי, היא לא תוצאה של החוכמה של סוכן הקידוד אלא של מבנה טוב של המערכת. ולראיה בהרבה פרויקטים סוכני הקידוד לא מצליחים לייצר תוצאות טובות ומפתחים מבזבזים שעות בריבים עם הסוכנים.
כן הקוד שלי, המערכת שלי, המבנה שלי. אני מכיר כל שורה בו ומכתיב את הכיוון, אפילו שאת רובו לא הקלדתי בעצמי.
📌 טיפ פייתון: לא צריך לכתוב את כל הקוד ב init
כשאנחנו כותבים מודול בפייתון מאוד נוח שמשתמשים יכולים לייבא את המודול ולהפעיל פקודות ישירות על שם החבילה, כלומר נניח שיש לי מודול בשם mymodule אז משתמשים מאוד אוהבים לכתוב:
דרך אחת לכתוב את mymodule כדי שזה יעבוד היא פשוט לכתוב קובץ בשם
ועכשיו משתמשים של המודול צריכים לטעון את הקובץ הפנימי מתוך החבילה ולכתוב:
העצלנים יותר ישתמשו ב alias ויכתבו ביבוא:
אבל זה לא נראה נכון ועדיין מעצבן את המשתמשים.
כותבים מודולים יצירתיים יודעים לשים את הקוד בקובץ ה
ובתוך הקובץ
אבל כותבים חבילות יותר יצירתיים דווקא מעדיפים להשתמש בשם קובץ מסודר עבור הקודם שלהם. במצב כזה נכתוב קובץ
ושוב הכל עובד אבל עכשיו גם מסודר - משתמשים יכולים לייבא את המודול mymodule ולקרוא לפונקציה add ישירות דרך היבוא, אפילו שהפונקציה מוגדרת בקובץ פנימי
כשאנחנו כותבים מודול בפייתון מאוד נוח שמשתמשים יכולים לייבא את המודול ולהפעיל פקודות ישירות על שם החבילה, כלומר נניח שיש לי מודול בשם mymodule אז משתמשים מאוד אוהבים לכתוב:
import mymodule
print(mymodule.add(10, 20))
דרך אחת לכתוב את mymodule כדי שזה יעבוד היא פשוט לכתוב קובץ בשם
mymodule.py ובתוכו להגדיר את הפונקציה add. אבל אם אתם רוצים לכתוב פרויקט ולשתף עם חברים סיכוי טוב שתרצו לשים את המודול שלכם בתוך תיקייה מסודרת ששמה יהיה כשם הפרויקט. ופה העסק מתחיל להסתבך, כי אם אני יוצר תיקייה בשם mymodule ובתוכה קובץ בשם utils.py עם הפונקציה שלי אז יש לי מבנה תיקיות:mymodule/
__init__.py
utils.py
ועכשיו משתמשים של המודול צריכים לטעון את הקובץ הפנימי מתוך החבילה ולכתוב:
import mymodule.utils
print(mymodule.utils.add(10, 20))
העצלנים יותר ישתמשו ב alias ויכתבו ביבוא:
import mymodule.utils as mymodule
print(mymodule.add(10, 20))
אבל זה לא נראה נכון ועדיין מעצבן את המשתמשים.
כותבים מודולים יצירתיים יודעים לשים את הקוד בקובץ ה
__init__.py של החבילה, וכך לא צריכים אפילו להמציא שם לקובץ המקור האמיתי והכל מסתדר עם היבוא, כלומר יהיה לנו בצד של החבילה:mymodule/
__init__.py
ובתוך הקובץ
__init__.py תופיע הגדרת הפונקציה add, ואז משתמשים של החבילה יוכלו שוב לכתוב:import mymodule
print(mymodule.add(10, 20))
אבל כותבים חבילות יותר יצירתיים דווקא מעדיפים להשתמש בשם קובץ מסודר עבור הקודם שלהם. במצב כזה נכתוב קובץ
utils.py עם הגדרת הפונקציה add, וקובץ __init__.py שיכיל רק את פקודת היבוא והיצוא מחדש:from .utils import add
ושוב הכל עובד אבל עכשיו גם מסודר - משתמשים יכולים לייבא את המודול mymodule ולקרוא לפונקציה add ישירות דרך היבוא, אפילו שהפונקציה מוגדרת בקובץ פנימי
mymodule/utils.py בתוך החבילה.❤1👍1
📌 אם קוד נכתב ביער ואף אחד לא משתמש בו
בפרויקט בשם chatwoot (ריילס) מצאתי את הקוד הבא:
הקוד לא עובד מכמה סיבות:
1. הפונקציה
2. הפונקציה
אין אף בדיקה על הקוד ולא מפעילים אותו משום מקום אבל הוא כן מחובר ל API Endpoint לכן אדם (או סוכן קידוד) שמסתכל רק עליו עלול לקבל את הרושם שהפונקציה והקובץ קיימים. ואם אותו אדם (או יותר סביר סוכן קידוד) יגיע לכתוב עכשיו פיצ'ר חדש הוא עלול להשתמש באותם רכיבים מדומיינים. וכן זאת בעיה של סוכני קידוד הרבה יותר מאשר של בני אדם, וזו הופכת להיות בעיה של בני אדם כשאותם בני אדם מפעילים סוכני קידוד בלי להכיר את הקוד ובלי לעבור מראש על תוכנית העבודה.
עכשיו אפשר לטעון שבני אדם הם הבעיה של סוכני קידוד ואם רק ניתן לסוכני קידוד לעבוד בשקט העולם יהיה מקום טוב יותר. אמרו דבר דומה על מכוניות אוטונומיות ובכל מקרה זה לא הולך לקרות ולא יקדם אותנו.
יותר חכם לשים לב שהיום הקוד הוא הפרומפט. קוד יכול להיות שבור גם אם אף אחד לא מפעיל אותו ויכול בקלות לקלקל את התוצאות של סוכן הקידוד. בעולם שבו הקוד הוא הפרומפט, קוד נקי הוא יעד חשוב מתמיד.
בפרויקט בשם chatwoot (ריילס) מצאתי את הקוד הבא:
class Api::V1::Accounts::WorkingHoursController < Api::V1::Accounts::BaseController
before_action :check_authorization
before_action :fetch_webhook, only: [:update]
def update
@working_hour.update!(working_hour_params)
end
private
def working_hour_params
params.require(:working_hour).permit(:inbox_id, :open_hour, :open_minutes, :close_hour, :close_minutes, :closed_all_day)
end
def fetch_working_hour
@working_hour = Current.account.working_hours.find(params[:id])
end
end
הקוד לא עובד מכמה סיבות:
1. הפונקציה
fetch_webhook לא קיימת. אי אפשר להפעיל אותה לפני update.2. הפונקציה
check_authorization מחפשת קובץ בשם working_hours_policy.rb שגם לא קיים.אין אף בדיקה על הקוד ולא מפעילים אותו משום מקום אבל הוא כן מחובר ל API Endpoint לכן אדם (או סוכן קידוד) שמסתכל רק עליו עלול לקבל את הרושם שהפונקציה והקובץ קיימים. ואם אותו אדם (או יותר סביר סוכן קידוד) יגיע לכתוב עכשיו פיצ'ר חדש הוא עלול להשתמש באותם רכיבים מדומיינים. וכן זאת בעיה של סוכני קידוד הרבה יותר מאשר של בני אדם, וזו הופכת להיות בעיה של בני אדם כשאותם בני אדם מפעילים סוכני קידוד בלי להכיר את הקוד ובלי לעבור מראש על תוכנית העבודה.
עכשיו אפשר לטעון שבני אדם הם הבעיה של סוכני קידוד ואם רק ניתן לסוכני קידוד לעבוד בשקט העולם יהיה מקום טוב יותר. אמרו דבר דומה על מכוניות אוטונומיות ובכל מקרה זה לא הולך לקרות ולא יקדם אותנו.
יותר חכם לשים לב שהיום הקוד הוא הפרומפט. קוד יכול להיות שבור גם אם אף אחד לא מפעיל אותו ויכול בקלות לקלקל את התוצאות של סוכן הקידוד. בעולם שבו הקוד הוא הפרומפט, קוד נקי הוא יעד חשוב מתמיד.
📌 הצפנה ב Rails - הטוב, הרע והמכוער
לריילס יש מנגנון מובנה להצפין מידע שנשמר בבסיס הנתונים. המנגנון מגדיר שני מפתחות הצפנה וביטים של מלח כדי לבלבל את האויב, כלומר אנחנו מגדירים בקונפיגורציה:
ואז מהקוד אפשר להשתמש ב ActiveRecord::Encryption כדי להצפין או לפענח עם המפתחות האלה, או להגדיר שדות שאוטומטית יוצפנו בשמירה לבסיס הנתונים ויפוענחו בקריאה. במדריך השימוש יש להם אינסוף מידע על החלפת מפתחות, מתי להשתמש בהצפנה דטרמניסטית ומתי לא דטרמניסטית ושאר היבטים של אבטחת מידע. קל, זמין וטוב כמו שריילס יודע.
יש גם קלאס ישן ומכוער יותר להצפנה בשם MessageEncryptor. דוגמת השימוש מהתיעוד נראית כך:
זה קוד מכוער כי הוא מכריח אותי להגדיר לבד את המלח ולגזור בעצמי את המפתחות ועוד מציג את הבסיס ליצירת המפתח בצורה מפורשת בקוד. כן אפשר לכתוב הצפנה מאובטחת עם MessageEncryptor אבל זה ידרוש עבודה וידע שלא מופיעים בתיעוד.
וכשאנשים לא מספיק מקצועיים או סוכני קידוד נתקלים בכאלה בעיות הם נוטים לזרוק את החלקים הקשים ולהפוך את הקוד המכוער לקוד רע, לדוגמה קוד שקיבלתי מ ChatGPT:
לאן נעלמו הביטים האקראיים של המלח? צ'אט כנראה לא חשב שזה מספיק חשוב. פרומפט נוסף אגב החזיר לי את הקוד הזה:
הפעם הבסיס למפתח הוא
לקחים? בהחלט. כשקוד תשתית דורש מיומנות ועדינות כדי להשתמש בו נכון יהיו בני אדם וסוכני קידוד שישברו אותו.
לריילס יש מנגנון מובנה להצפין מידע שנשמר בבסיס הנתונים. המנגנון מגדיר שני מפתחות הצפנה וביטים של מלח כדי לבלבל את האויב, כלומר אנחנו מגדירים בקונפיגורציה:
Add this entry to the credentials of the target environment:
active_record_encryption:
primary_key: YehXdfzxVKpoLvKseJMJIEGs2JxerkB8
deterministic_key: uhtk2DYS80OweAPnMLtrV2FhYIXaceAy
key_derivation_salt: g7Q66StqUQDQk9SJ81sWbYZXgiRogBwS
ואז מהקוד אפשר להשתמש ב ActiveRecord::Encryption כדי להצפין או לפענח עם המפתחות האלה, או להגדיר שדות שאוטומטית יוצפנו בשמירה לבסיס הנתונים ויפוענחו בקריאה. במדריך השימוש יש להם אינסוף מידע על החלפת מפתחות, מתי להשתמש בהצפנה דטרמניסטית ומתי לא דטרמניסטית ושאר היבטים של אבטחת מידע. קל, זמין וטוב כמו שריילס יודע.
יש גם קלאס ישן ומכוער יותר להצפנה בשם MessageEncryptor. דוגמת השימוש מהתיעוד נראית כך:
len = ActiveSupport::MessageEncryptor.key_len
salt = SecureRandom.random_bytes(len)
key = ActiveSupport::KeyGenerator.new('password').generate_key(salt, len) # => "\x89\xE0\x156\xAC..."
crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
crypt.decrypt_and_verify(encrypted_data)
זה קוד מכוער כי הוא מכריח אותי להגדיר לבד את המלח ולגזור בעצמי את המפתחות ועוד מציג את הבסיס ליצירת המפתח בצורה מפורשת בקוד. כן אפשר לכתוב הצפנה מאובטחת עם MessageEncryptor אבל זה ידרוש עבודה וידע שלא מופיעים בתיעוד.
וכשאנשים לא מספיק מקצועיים או סוכני קידוד נתקלים בכאלה בעיות הם נוטים לזרוק את החלקים הקשים ולהפוך את הקוד המכוער לקוד רע, לדוגמה קוד שקיבלתי מ ChatGPT:
secret_key = Rails.application.key_generator.generate_key("my-secret", 32)
encryptor = ActiveSupport::MessageEncryptor.new(secret_key)
# Encrypt + sign
encrypted = encryptor.encrypt_and_sign("Hello world")
# Decrypt + verify
decrypted = encryptor.decrypt_and_verify(encrypted)
puts encrypted
puts decryptedלאן נעלמו הביטים האקראיים של המלח? צ'אט כנראה לא חשב שזה מספיק חשוב. פרומפט נוסף אגב החזיר לי את הקוד הזה:
secret_key_base = Rails.application.secret_key_base
# Derive a key
key_len = ActiveSupport::MessageEncryptor.key_len
salt = "my-encryption-salt"
key = ActiveSupport::KeyGenerator.new(secret_key_base).generate_key(salt, key_len)
encryptor = ActiveSupport::MessageEncryptor.new(key)
הפעם הבסיס למפתח הוא
secret_key_base שכבר נמצא בשימוש כדי להצפין את הקוקיז וכך קיבלנו מיחזור של בסיס למפתח הצפנה לשימושים שונים. רק אם אני מבקש במפורש את שיטת ההצפנה החדשה עם ActiveRecord::Encryption אני באמת מקבל אותה.לקחים? בהחלט. כשקוד תשתית דורש מיומנות ועדינות כדי להשתמש בו נכון יהיו בני אדם וסוכני קידוד שישברו אותו.
🤡1
הי חברים תגידו מה דעתכם על קוד ופוליטיקה? הייתם משתמשים בספריית קוד פתוח עם באנר של פרי פלסטין? מה לגבי סלבה אוקראינה? ולמה?
📌 באבטחת מידע לא שואלים מה כבר יכול לקרות
הרבה יותר קל לראות חולשות בקוד מאשר להבין איך לנצל את אותן חולשות כדי לעשות נזק, אבל כשאנחנו משאירים חולשות בקוד שלנו רק בגלל שאנחנו לא רואים מה כבר הנזק שהן יכולות לגרום אנחנו בסך הכל נותנים יותר מוטיבציה לאנשים שמבינים בזה.
הסיפור הבא נכנס אצלי לאוסף בעיות אבטחת מידע שקשורות ל AI והוא מדגים בדיוק את הפער בין החולשה לניצול. סיליין החליטו לתת לקלוד למיין את ה Issues שמשתמשים פותחים בעזרת קונפיגורציה כזאת:
ראיתם את הבעיה? אני בטוח שכן. לוקחים את ה title של ה Issue ושולחים אותו לסוכן שיכול להריץ פקודות Shell ולגשת לאינטרנט.
עכשיו להגנתם הסוכן רץ על מכונה של Github Actions בלי הרשאות לעשות שום דבר. ופה אני שומע בראש את השאלה שבטח גם הם חשבו "מה כבר יכול להישבר?". נו, מסתבר שגם מכונת גיטהאב אקשן בלי הרשאות יכולה לעשות נזק והתוצאה היתה גירסה מלוכלכת של cline שעלתה ל npm.
הסיפור הטכני המלא למתעניינים נמצא בלינק הזה:
https://neciudan.dev/cline-ci-got-compromised-here-is-how#what-is-openclaw-and-why-should-you-care
הלקח הוא ישן ומלווה אותנו עוד הרבה לפני ימי ה AI. באבטחת מידע לא שואלים "מה כבר יכול לקרות". סוגרים את החולשות לפני שמישהו אחר ימצא את התשובה.
הרבה יותר קל לראות חולשות בקוד מאשר להבין איך לנצל את אותן חולשות כדי לעשות נזק, אבל כשאנחנו משאירים חולשות בקוד שלנו רק בגלל שאנחנו לא רואים מה כבר הנזק שהן יכולות לגרום אנחנו בסך הכל נותנים יותר מוטיבציה לאנשים שמבינים בזה.
הסיפור הבא נכנס אצלי לאוסף בעיות אבטחת מידע שקשורות ל AI והוא מדגים בדיוק את הפער בין החולשה לניצול. סיליין החליטו לתת לקלוד למיין את ה Issues שמשתמשים פותחים בעזרת קונפיגורציה כזאת:
allowed_non_write_users: "*"
claude_args: >-
--allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch"
prompt: |
**Issue:** #${{ github.event.issue.number }}
**Title:** ${{ github.event.issue.title }}
ראיתם את הבעיה? אני בטוח שכן. לוקחים את ה title של ה Issue ושולחים אותו לסוכן שיכול להריץ פקודות Shell ולגשת לאינטרנט.
עכשיו להגנתם הסוכן רץ על מכונה של Github Actions בלי הרשאות לעשות שום דבר. ופה אני שומע בראש את השאלה שבטח גם הם חשבו "מה כבר יכול להישבר?". נו, מסתבר שגם מכונת גיטהאב אקשן בלי הרשאות יכולה לעשות נזק והתוצאה היתה גירסה מלוכלכת של cline שעלתה ל npm.
הסיפור הטכני המלא למתעניינים נמצא בלינק הזה:
https://neciudan.dev/cline-ci-got-compromised-here-is-how#what-is-openclaw-and-why-should-you-care
הלקח הוא ישן ומלווה אותנו עוד הרבה לפני ימי ה AI. באבטחת מידע לא שואלים "מה כבר יכול לקרות". סוגרים את החולשות לפני שמישהו אחר ימצא את התשובה.
Neciu Dan
How to steal npm publish tokens by opening GitHub issues
A chain of vulnerabilities and pretty clever attack strategies led to the compromise of the Cline CLI. Let me explain what happened and what you can do to protect yourself.
📌 גם גיטהאב לא בודקים זמינות לפני שמציעים שם לפרויקט
מסך יצירת ריפו חדש בגיטהאב כולל תווית מתחת לשם הפרויקט עם הצעה לשם אקראי. כל טעינה מחדש של העמוד משנה את השם. לכו נסו את זה אם אתם לא מכירים:
https://github.com/new
נשים לב שאחרי שלוחצים על השם האקראי שהוא הציע מופיעה תווית נוספת, Checking availability, ורק אחרי הבדיקה מופיעה התווית הירוקה שאומרת ששם הפרויקט זמין. ואתה רוצה לשאול, לא יכולתם לבדוק לפני? למה שתציעו לי שם אקראי אם הוא כבר תפוס?
גם בקוד שלי אני נתקל בדילמה דומה: קוד צד שרת מייצר מזהה אקראי וצריך להבין אם המזהה הזה בכלל חוקי לפני שמציע אותו ללקוח. האינסטינקט הוא לכתוב קוד "נכון", כלומר להגריל את המזהה, לוודא שהוא פנוי מול בסיס הנתונים וכך להמשיך בלולאה עד שמוצאים מזהה פנוי. ממילא לולאה כזאת לא תרוץ יותר מדי פעמים כי הסיכוי להגריל מזהה תפוס הוא מאוד נמוך.
גיטהאב אומרים לנו פה משהו מאוד חשוב לגבי קידוד - יש מצבים בהם יותר חשוב להחזיר תשובה מהר מאשר להחזיר את התשובה הנכונה. רוב האנשים שמגיעים למסך הזה ממילא לא ישתמשו בשם האקראי אלא יבחרו שם בעצמם, וגם אלה שיבחרו רוב הזמן יקבלו מזהה פנוי. הסיכוי לקבל מזהה תפוס הוא ממילא כל כך נמוך שחבל לבזבז לכולם זמן על הבדיקה הראשונה ועדיף לבדוק רק עבור מי שבאמת מתכוון להשתמש במזהה זה.
בביצועים, שלא כמו בחיים, המפתח לאושר הוא עצלות ודחיינות. מה שאפשר לא לעשות עדיף לא לעשות, ואם חייבים לעשות עדיף כמה שיותר מאוחר.
מסך יצירת ריפו חדש בגיטהאב כולל תווית מתחת לשם הפרויקט עם הצעה לשם אקראי. כל טעינה מחדש של העמוד משנה את השם. לכו נסו את זה אם אתם לא מכירים:
https://github.com/new
נשים לב שאחרי שלוחצים על השם האקראי שהוא הציע מופיעה תווית נוספת, Checking availability, ורק אחרי הבדיקה מופיעה התווית הירוקה שאומרת ששם הפרויקט זמין. ואתה רוצה לשאול, לא יכולתם לבדוק לפני? למה שתציעו לי שם אקראי אם הוא כבר תפוס?
גם בקוד שלי אני נתקל בדילמה דומה: קוד צד שרת מייצר מזהה אקראי וצריך להבין אם המזהה הזה בכלל חוקי לפני שמציע אותו ללקוח. האינסטינקט הוא לכתוב קוד "נכון", כלומר להגריל את המזהה, לוודא שהוא פנוי מול בסיס הנתונים וכך להמשיך בלולאה עד שמוצאים מזהה פנוי. ממילא לולאה כזאת לא תרוץ יותר מדי פעמים כי הסיכוי להגריל מזהה תפוס הוא מאוד נמוך.
גיטהאב אומרים לנו פה משהו מאוד חשוב לגבי קידוד - יש מצבים בהם יותר חשוב להחזיר תשובה מהר מאשר להחזיר את התשובה הנכונה. רוב האנשים שמגיעים למסך הזה ממילא לא ישתמשו בשם האקראי אלא יבחרו שם בעצמם, וגם אלה שיבחרו רוב הזמן יקבלו מזהה פנוי. הסיכוי לקבל מזהה תפוס הוא ממילא כל כך נמוך שחבל לבזבז לכולם זמן על הבדיקה הראשונה ועדיף לבדוק רק עבור מי שבאמת מתכוון להשתמש במזהה זה.
בביצועים, שלא כמו בחיים, המפתח לאושר הוא עצלות ודחיינות. מה שאפשר לא לעשות עדיף לא לעשות, ואם חייבים לעשות עדיף כמה שיותר מאוחר.
👍3
📌 טיפול בשגיאות בקוד asyncio
הקוד הבא הוא גרסה פשוטה של קטע שכתב לי סוכן קידוד ואני שמח שעצרתי לקרוא לפני שהסכמתי לקבל אותו. בואו נראה את הקוד ואז ננתח מה הסוכן עשה יפה, איפה הוא טעה ומה אפשר ללמוד על ביטולים ושגיאות ב asyncio:
הקוד מושך url מתור ואז עושה משהו עם ה url הזה, אבל מה שמעניין בו הוא כל פקודות הטיפול בשגיאות ובביטולים.
✏ ביטול לפני קבלת אלמנט
נתבונן בבלוק הראשון:
מה פתאום יש שם טיפול נפרד ב CancelledError? ולמה רק הוא מקבל את ה break? התשובה שכשמבטלים פעולה אסינכרונית הביטול יגיע לאיזשהו await, כלומר כל await בפונקציה עלול לזרוק CancelledError. אם הביטול קורה לפני שמשכנו אלמנט מהתור עלינו לצאת מהלולאה בלי להודיע על
✏ טיפול בשגיאות ללא ביטולים
הבלוק השני מטפל בשגיאות מסוג Exception. כאן צריך לזכור ש CancelledError יורש מ BaseException בפייתון ולא מ Exception ולכן לא יטופל ב except. אני מזכיר הקוד היה:
שגיאה רגילה תגיע כאן ל except ותציג את הודעת השגיאה. אחריה יופעל ה finally, המשימה תדווח בתור הושלמה ואם הופעל דגל היציאה אז נצא מהלולאה. ביטול יגיע ישר ל finally, שוב יוביל לדיווח על
מה הבעיה עם זה? קוד חיצוני שהריץ את הפונקציה וביטל יצטרך להתמודד עם תשובה לא עקבית - אם הביטול התרחש בזמן המתנה לפריט מהתור אז מפעילים break ופשוט מסיימים את הפונקציה רגיל (בתור הצלחה). אם הביטול התרחש בזמן ביצוע העבודה ה CancelledException יגרום לסיום הפונקציה וקוד חיצוני יוכל לראות שהיא התרסקה בגלל ביטול.
בעיה שניה בקוד היא הטיפול בשגיאות שאינן ביטולים - כאן הטיפול הוא בסך הכל לכתוב הודעה ללוג למרות שבעבודת רשת נכון יותר לנסות פריט כמה פעמים, לפחות עבור שגיאות זמניות. הקוד כאן לא כולל מנגנון לקיטלוג שגיאות וגם לא מנגנון לנסיונות חוזרים.
סך הכל הקוד היה עובד טוב יותר עם context manager שיטפל ב
הקוד הבא הוא גרסה פשוטה של קטע שכתב לי סוכן קידוד ואני שמח שעצרתי לקרוא לפני שהסכמתי לקבל אותו. בואו נראה את הקוד ואז ננתח מה הסוכן עשה יפה, איפה הוא טעה ומה אפשר ללמוד על ביטולים ושגיאות ב asyncio:
async def worker(..., queue, visited, ...):
while True:
try:
url, depth = await queue.get()
except asyncio.CancelledError:
break
try:
...
except Exception as e:
logger.error(f"Error processing {url}: {e}")
finally:
queue.task_done()
if stop_event.is_set():
break
הקוד מושך url מתור ואז עושה משהו עם ה url הזה, אבל מה שמעניין בו הוא כל פקודות הטיפול בשגיאות ובביטולים.
✏ ביטול לפני קבלת אלמנט
נתבונן בבלוק הראשון:
while True:
try:
url, depth = await queue.get()
except asyncio.CancelledError:
break
מה פתאום יש שם טיפול נפרד ב CancelledError? ולמה רק הוא מקבל את ה break? התשובה שכשמבטלים פעולה אסינכרונית הביטול יגיע לאיזשהו await, כלומר כל await בפונקציה עלול לזרוק CancelledError. אם הביטול קורה לפני שמשכנו אלמנט מהתור עלינו לצאת מהלולאה בלי להודיע על
task_done. בלי טיפול מיוחד זה השגיאה תגיע ל finally ו task_done ידווח גם במקרה של ביטול בזמן המתנה לאלמנט.✏ טיפול בשגיאות ללא ביטולים
הבלוק השני מטפל בשגיאות מסוג Exception. כאן צריך לזכור ש CancelledError יורש מ BaseException בפייתון ולא מ Exception ולכן לא יטופל ב except. אני מזכיר הקוד היה:
try:
...
except Exception as e:
logger.error(f"Error processing {url}: {e}")
finally:
queue.task_done()
if stop_event.is_set():
break
שגיאה רגילה תגיע כאן ל except ותציג את הודעת השגיאה. אחריה יופעל ה finally, המשימה תדווח בתור הושלמה ואם הופעל דגל היציאה אז נצא מהלולאה. ביטול יגיע ישר ל finally, שוב יוביל לדיווח על
task_done אבל אז ימשיך לפעפע למעלה ויוציא אותנו מהלולאה.מה הבעיה עם זה? קוד חיצוני שהריץ את הפונקציה וביטל יצטרך להתמודד עם תשובה לא עקבית - אם הביטול התרחש בזמן המתנה לפריט מהתור אז מפעילים break ופשוט מסיימים את הפונקציה רגיל (בתור הצלחה). אם הביטול התרחש בזמן ביצוע העבודה ה CancelledException יגרום לסיום הפונקציה וקוד חיצוני יוכל לראות שהיא התרסקה בגלל ביטול.
בעיה שניה בקוד היא הטיפול בשגיאות שאינן ביטולים - כאן הטיפול הוא בסך הכל לכתוב הודעה ללוג למרות שבעבודת רשת נכון יותר לנסות פריט כמה פעמים, לפחות עבור שגיאות זמניות. הקוד כאן לא כולל מנגנון לקיטלוג שגיאות וגם לא מנגנון לנסיונות חוזרים.
סך הכל הקוד היה עובד טוב יותר עם context manager שיטפל ב
task_done במקום מבנה ה except הכפול. לג'מיני לא היתה בעיה לכתוב את זה עם כמו שרציתי כשביקשתי הטריק היחיד היה לראות את הבעיות בקוד המקורי ולהבין איך זה צריך להיות. זה הקוד שלקחתי בסוף:from contextlib import asynccontextmanager
import asyncio
@asynccontextmanager
async def get_task(queue):
# If this is cancelled, Exception propagates and finally is NOT run (which is correct)
item = await queue.get()
try:
yield item
finally:
queue.task_done()
async def worker(queue):
# No need to catch CancelledError at all!
# When task.cancel() is called, the exception will break the loop
# automatically and cleanly cancel the task.
while True:
async with process_task(queue) as (url, depth):
... # Do work
📌 חוזרים למציאות
כשאנחנו מבינים איך דברים עובדים הרבה יותר קל לנו לראות את האילוצים ואיתם להבדיל בין הבטחות להייפ. ברור שהלוטו חייב להרוויח ולכן הסיכויים שלך לזכות הם זניחים. אם יותר אנשים היו זוכים הלוטו אולי לא היה מרוויח.
ברור שבשביל לדבר שפה בצורה שוטפת צריך זמן. כשאני רואה פרסומת שמבטיחה לי שתוך שלושה חודשים אני אדבר סינית שוטף אני בורח. למדתי מספיק שפות בשביל להכיר את הגבולות והאילוצים שלי ואני יודע מה אפשרי ומה רק חלום.
ואחרי שלוש וחצי שנים של שיחות עם מודלי שפה ולימוד עקרונות העבודה שלהם אנחנו כבר אמורים לדעת מה אפשרי ומה רק חלום גם איתם. אנחנו מבינים ש LLM הוא מכונת השלמה ושהטקסט שהוא יכתוב הוא תוצאה של הפרומפט ושל נתוני האימון שלו, בתוספת תיבול סטטיסטי. למרות הרצון "לזכות בלוטו" קל לראות למה מוצרי AI רבים לא שווים את המאמץ.
כשמישהו מבטיח סוכן קולי שיסיים בשבילי משימות אני נזכר ב OpenClaw ובטעויות שהוא עשה לאנשים. הטעויות וההזיות הן חלק בלתי נפרד ממודל שפה ולכן ברור שאני לא יכול לסמוך על הכלי שלהם.
כשמישהו מבטיח סוף לבעיות אבטחת המידע בזכות מוצר AI חדש שיקרא את הקוד שלי כדי לחפש שגיאות אני נזכר שהפתרון לבעיות אבטחה הוא לא קריאה חד פעמית של הקוד אלא תהליך פיתוח וניהול ארגוני שלם. כלי בודד שרק עובר על הקוד יכול לעזור אבל בלי התהליך המלא לעולם לא נוכל לטפל בבעיות שקורות מחוץ לסקופ של אותו כלי (למשל שינויי גרסה בפלטפורמת הריצה, בעיות קונפיגורציה או הנדסה חברתית).
כשמישהו מבטיח סוכן AI שמוצא את הסיבה לכל תקלה במערכת אני אפילו לא מנסה אותו. אני בטוח שהכוונות שלהם טובות אבל אני נזכר בכל הפעמים שמודל שפה דמיין רמזים בלוגים על בסיס נתוני האימון שלו, רמזים שרק הובילו עמוק יותר למחילת הארנב. כל עוד הבסיס של הכלי שלך הוא מודל שפה הזיות הן חלק מהסיפור.
ההבטחה לזכות בלוטו בנויה על כך שתמיד יש את הסיכוי הקטן שתזכה. דום סקרולינג זה השם שנתנו לרשתות חברתיות בהן אנשים גוררים עוד ועוד בשביל הסיכוי הקטן לקרוא משהו מעניין. על אותו משקל צריך למצוא מושג שיתאר את המרדף אחרי המודל הבא, הכלי הבא, הקסם הבא. אבל אין שום טריק. בשביל להרוויח כסף עדיף לעבוד ולא למלא לוטו. בשביל לכתוב מערכות יציבות עדיף ללמוד תכנות ולא לקוות שסוכן הקידוד הבא יצליח לייצר בדיוק את המערכת שדמיינת.
כשאנחנו מבינים איך דברים עובדים הרבה יותר קל לנו לראות את האילוצים ואיתם להבדיל בין הבטחות להייפ. ברור שהלוטו חייב להרוויח ולכן הסיכויים שלך לזכות הם זניחים. אם יותר אנשים היו זוכים הלוטו אולי לא היה מרוויח.
ברור שבשביל לדבר שפה בצורה שוטפת צריך זמן. כשאני רואה פרסומת שמבטיחה לי שתוך שלושה חודשים אני אדבר סינית שוטף אני בורח. למדתי מספיק שפות בשביל להכיר את הגבולות והאילוצים שלי ואני יודע מה אפשרי ומה רק חלום.
ואחרי שלוש וחצי שנים של שיחות עם מודלי שפה ולימוד עקרונות העבודה שלהם אנחנו כבר אמורים לדעת מה אפשרי ומה רק חלום גם איתם. אנחנו מבינים ש LLM הוא מכונת השלמה ושהטקסט שהוא יכתוב הוא תוצאה של הפרומפט ושל נתוני האימון שלו, בתוספת תיבול סטטיסטי. למרות הרצון "לזכות בלוטו" קל לראות למה מוצרי AI רבים לא שווים את המאמץ.
כשמישהו מבטיח סוכן קולי שיסיים בשבילי משימות אני נזכר ב OpenClaw ובטעויות שהוא עשה לאנשים. הטעויות וההזיות הן חלק בלתי נפרד ממודל שפה ולכן ברור שאני לא יכול לסמוך על הכלי שלהם.
כשמישהו מבטיח סוף לבעיות אבטחת המידע בזכות מוצר AI חדש שיקרא את הקוד שלי כדי לחפש שגיאות אני נזכר שהפתרון לבעיות אבטחה הוא לא קריאה חד פעמית של הקוד אלא תהליך פיתוח וניהול ארגוני שלם. כלי בודד שרק עובר על הקוד יכול לעזור אבל בלי התהליך המלא לעולם לא נוכל לטפל בבעיות שקורות מחוץ לסקופ של אותו כלי (למשל שינויי גרסה בפלטפורמת הריצה, בעיות קונפיגורציה או הנדסה חברתית).
כשמישהו מבטיח סוכן AI שמוצא את הסיבה לכל תקלה במערכת אני אפילו לא מנסה אותו. אני בטוח שהכוונות שלהם טובות אבל אני נזכר בכל הפעמים שמודל שפה דמיין רמזים בלוגים על בסיס נתוני האימון שלו, רמזים שרק הובילו עמוק יותר למחילת הארנב. כל עוד הבסיס של הכלי שלך הוא מודל שפה הזיות הן חלק מהסיפור.
ההבטחה לזכות בלוטו בנויה על כך שתמיד יש את הסיכוי הקטן שתזכה. דום סקרולינג זה השם שנתנו לרשתות חברתיות בהן אנשים גוררים עוד ועוד בשביל הסיכוי הקטן לקרוא משהו מעניין. על אותו משקל צריך למצוא מושג שיתאר את המרדף אחרי המודל הבא, הכלי הבא, הקסם הבא. אבל אין שום טריק. בשביל להרוויח כסף עדיף לעבוד ולא למלא לוטו. בשביל לכתוב מערכות יציבות עדיף ללמוד תכנות ולא לקוות שסוכן הקידוד הבא יצליח לייצר בדיוק את המערכת שדמיינת.
👍4
📌 סקירת פונקציית TypeScript שכתב סוכן קידוד
נתקלתי ב stavrobot, סוכן AI שאמור להיות גרסה מאובטחת של אופןקלו. לא התקנתי אבל מאחר והפרויקט בקוד פתוח והמפתח הדגיש שהוא עבד עם סוכן קידוד כדי לבנות את זה היה לי מעניין להסתכל בקוד. אפשר למצוא את הפרויקט בקישור הזה:
https://github.com/skorokithakis/stavrobot
בפוסט היום נתבונן בפונקציה אחת מתוך הקוד ונאתר בה מספר Anti Patterns, כלומר תבניות קוד שליליות שאני מזהה שחוזרות בצורות שונות בקוד AI. תבניות אלה יעבדו נגדנו כשנרצה להמשיך ולפתח את הפרויקט.
נתחיל בהצגת קוד הפונקציה כמו שמצאתי אותה בפרויקט:
הפונקציה מקבלת מערך של הודעות וצריכה להפוך אותו למערך של שורות כלומר כל הודעה צריכה להפוך לשורת טקסט במערך התוצאה.
✏ לולאה יוצרת במקום map
תבנית בעייתית ראשונה בקוד היא לולאת ה for. בדוגמה פשוטה שתי הלולאות האלה זהות:
ההבדל בין שתי הלולאות הוא דווקא במה שאי אפשר לעשות בלולאה השניה. לולאת map מאפשרת רק החלפה של רשימת ערכים אחת לרשימה אחרת. לולאת for היא כללית יותר ומאפשרת גם פעולות נוספות. במקרה שלנו הלולאה באמת משתמשת ביכולת של for לייצר מערך פלט באורך שונה מהקלט, כלומר מערך השורות שמוחזר עשוי לכלול יותר משורה אחת לכל הודעה - אבל קשה לנו לראות את זה מקריאה פשוטה של הקוד.
פתרון מדויק יותר ישתמש ב flatMap כדי ליצור מספר שורות מכל הודעה. הנה דוגמה פשוטה בקוד קטן להמחיש את העקרון:
נתקלתי ב stavrobot, סוכן AI שאמור להיות גרסה מאובטחת של אופןקלו. לא התקנתי אבל מאחר והפרויקט בקוד פתוח והמפתח הדגיש שהוא עבד עם סוכן קידוד כדי לבנות את זה היה לי מעניין להסתכל בקוד. אפשר למצוא את הפרויקט בקישור הזה:
https://github.com/skorokithakis/stavrobot
בפוסט היום נתבונן בפונקציה אחת מתוך הקוד ונאתר בה מספר Anti Patterns, כלומר תבניות קוד שליליות שאני מזהה שחוזרות בצורות שונות בקוד AI. תבניות אלה יעבדו נגדנו כשנרצה להמשיך ולפתח את הפרויקט.
נתחיל בהצגת קוד הפונקציה כמו שמצאתי אותה בפרויקט:
export function serializeMessagesForSummary(messages: AgentMessage[]): string {
const lines: string[] = [];
for (const message of messages) {
if (message.role === "user") {
let textContent: string;
if (typeof message.content === "string") {
textContent = message.content;
} else {
const content = Array.isArray(message.content) ? message.content : [];
textContent = content
.filter((block): block is TextContent => block.type === "text")
.map((block) => block.text)
.join("");
}
lines.push(`User: ${textContent}`);
} else if (message.role === "assistant") {
const content = Array.isArray(message.content) ? message.content : [];
const textContent = content
.filter((block): block is TextContent => block.type === "text")
.map((block) => block.text)
.join("");
if (textContent) {
lines.push(`Assistant: ${textContent}`);
}
for (const block of content) {
if (block.type === "toolCall") {
const toolCall = block as ToolCall;
const args = Object.entries(toolCall.arguments)
.map(([key, value]) => {
if (typeof value === "string") {
return `${key}=${JSON.stringify(value)}`;
}
if (typeof value === "object" && value !== null) {
return `${key}=${JSON.stringify(value)}`;
}
return `${key}=${String(value)}`;
})
.join(", ");
lines.push(`Assistant called ${toolCall.name}(${args})`);
}
}
} else if (message.role === "toolResult") {
const content = Array.isArray(message.content) ? message.content : [];
const textContent = content
.filter((block): block is TextContent => block.type === "text")
.map((block) => block.text)
.join("");
lines.push(`Tool result (${message.toolName}): ${textContent}`);
}
}
return lines.join("\n");
}הפונקציה מקבלת מערך של הודעות וצריכה להפוך אותו למערך של שורות כלומר כל הודעה צריכה להפוך לשורת טקסט במערך התוצאה.
✏ לולאה יוצרת במקום map
תבנית בעייתית ראשונה בקוד היא לולאת ה for. בדוגמה פשוטה שתי הלולאות האלה זהות:
const messages = [{text: 'one'}, {text: 'two'}, {text: 'three'}];
// 1. using for loop
const lines1 = [];
for (const message of messages) {
lines1.push(message.text);
}
// 2. using map
const lines2 = messages.map(message => message.text);ההבדל בין שתי הלולאות הוא דווקא במה שאי אפשר לעשות בלולאה השניה. לולאת map מאפשרת רק החלפה של רשימת ערכים אחת לרשימה אחרת. לולאת for היא כללית יותר ומאפשרת גם פעולות נוספות. במקרה שלנו הלולאה באמת משתמשת ביכולת של for לייצר מערך פלט באורך שונה מהקלט, כלומר מערך השורות שמוחזר עשוי לכלול יותר משורה אחת לכל הודעה - אבל קשה לנו לראות את זה מקריאה פשוטה של הקוד.
פתרון מדויק יותר ישתמש ב flatMap כדי ליצור מספר שורות מכל הודעה. הנה דוגמה פשוטה בקוד קטן להמחיש את העקרון:
const messages = [{text: 'one', times: 3}, {text: 'two'}, {text: 'three'}];
const lines1 = [];
for (const message of messages) {
const times = message.times ?? 1;
for (let i=0; i < times; i++) {
lines1.push(message.text);
}
}
console.log(lines1);
function serializeMessage(message) {
const times = message.times ?? 1;
return new Array(times).fill(message.text);
}
const lines2 = messages.flatMap(serializeMessage);
console.log(lines2);GitHub
GitHub - skorokithakis/stavrobot: An AI personal assistant with a focus on security.
An AI personal assistant with a focus on security. - skorokithakis/stavrobot
שתי הלולאות זהות ואחריהן המשתנים lines1 ו lines2 מכילים את אותו מערך. ההבדלים הם באפשרות לשימוש חוזר ובקריאות. הלולאה השנייה מבטיחה לי שאני לוקח מערך ומחזיר מערך כנראה לא באותו גודל, זה לא קל כמו map אבל עדיין אני מבין מה קרה פה. יותר מזה, הגישה השניה השאירה אותי עם פונקציה serializeMessage בה אפשר להשתמש בנפרד מהלולאה.
נשווה את זה לקטע מתוך הפונקציה המקורית:
החלק הפנימי שהופך הודעה לשורת טקסטים יופיע בעוד מקומות בפרויקט אבל אי אפשר להשתמש באותו קוד כי הוא מוחבא בתוך לולאת ה for. יותר מזה, רק קריאה מלאה של כל הבלוק תאפשר לי להבין איך הפונקציה עובדת וכך בשביל לקרוא את הקוד סוכן הקידוד צריך לבזבז יותר מקום בחלון הקונטקסט.
תבנית טובה יותר לפונקציה היתה יכולה להיות:
אנחנו רואים בפונקציה המקורית שחוסר הדיוק הוביל לכפל קוד והבלוק הזה מופיע מספר פעמים בפונקציה:
✏ טיפול לא מוסבר בכל המקרים האפשריים (ובמקרים לא אפשריים)
נקודה שניה שחוזרת המון בקוד AI היא תכנות דפנסיבי, כלומר קוד מהסוג הזה:
נשים לב - אם תוכן ההודעה הוא מסוג string אז נקח אותו, אחרת אם הוא מסוג מערך אז ניקח ממנו את כל הבלוקים שהם מסוג טקסט, ניקח את הטקסט שלהם ונאחד אותם לשורה אחת. אם התוכן הוא מכל סוג אחר נחזיר מחרוזת ריקה.
הבלוק מטפל ב-3 מקרים אבל לא מסביר לנו איזה סוגי הודעות יש ולמה שהתוכן יהיה טקסט או מערך. יותר מזה הקוד מוכן לטפל בהודעה שיש בה מספר בלוקים של טקסט, למרות שאפשר היה לעצור בבלוק הראשון כי יכול להיות מקסימום בלוק טקסט אחד בהודעה. אם יהיה בעתיד שינוי במקום אחר שיגרום לזה שהפונקציה תופעל רק עם הודעות מסוג טקסט או רק עם הודעות מסוג מערך יהיה קשה לזהות את השינוי ולמחוק את הקוד המיותר.
ספריית pi שממנה הגיע הטיפוס AgentMessage והיא שמגדירה את ההודעות השתמשה בשורה הבאה כדי למצוא תוכן טקסטואלי מהודעה:
חוץ מזה שהקוד קצר יותר הוא גם הרבה יותר מדויק. בזכות הדיוק הם לא צריכים את ה map וה join כדי לאחד את כל הבלוקים של הטקסט כי ממילא יש רק אחד.
✏ חסר מעבר ניקיון
סוכני קידוד הרבה פעמים עוצרים לפני הזמן, כשברור שהם יכולים לתקן את הבעיה עם עוד איטרציה אבל משהו בלולאה לא עבד. נתבונן בבלוק הזה:
נשווה את זה לקטע מתוך הפונקציה המקורית:
for (const message of messages) {
if (message.role === "user") {
let textContent: string;
if (typeof message.content === "string") {
textContent = message.content;
} else {
const content = Array.isArray(message.content) ? message.content : [];
textContent = content
.filter((block): block is TextContent => block.type === "text")
.map((block) => block.text)
.join("");
}
lines.push(`User: ${textContent}`);
} else if (message.role === "assistant") {החלק הפנימי שהופך הודעה לשורת טקסטים יופיע בעוד מקומות בפרויקט אבל אי אפשר להשתמש באותו קוד כי הוא מוחבא בתוך לולאת ה for. יותר מזה, רק קריאה מלאה של כל הבלוק תאפשר לי להבין איך הפונקציה עובדת וכך בשביל לקרוא את הקוד סוכן הקידוד צריך לבזבז יותר מקום בחלון הקונטקסט.
תבנית טובה יותר לפונקציה היתה יכולה להיות:
function serializeAgentMessage(message: AgentMessage): string[] {
switch(message.role) {
case "user":
return serializeUserMessage(message);
case "assistant":
return serializeAssistantMessage(message);
case "toolResult":
return serializeToolResult(message);
default:
throw new Error(`Unknown message role: ${message.role}`);
}
}
export function serializeMessagesForSummary(messages: AgentMessage[]): string {
return messages.flatMap(serializeAgentMessage).join("\n");
}אנחנו רואים בפונקציה המקורית שחוסר הדיוק הוביל לכפל קוד והבלוק הזה מופיע מספר פעמים בפונקציה:
const textContent = content
.filter((block): block is TextContent => block.type === "text")
.map((block) => block.text)
.join("");
✏ טיפול לא מוסבר בכל המקרים האפשריים (ובמקרים לא אפשריים)
נקודה שניה שחוזרת המון בקוד AI היא תכנות דפנסיבי, כלומר קוד מהסוג הזה:
if (message.role === "user") {
let textContent: string;
if (typeof message.content === "string") {
textContent = message.content;
} else {
const content = Array.isArray(message.content) ? message.content : [];
textContent = content
.filter((block): block is TextContent => block.type === "text")
.map((block) => block.text)
.join("");
}נשים לב - אם תוכן ההודעה הוא מסוג string אז נקח אותו, אחרת אם הוא מסוג מערך אז ניקח ממנו את כל הבלוקים שהם מסוג טקסט, ניקח את הטקסט שלהם ונאחד אותם לשורה אחת. אם התוכן הוא מכל סוג אחר נחזיר מחרוזת ריקה.
הבלוק מטפל ב-3 מקרים אבל לא מסביר לנו איזה סוגי הודעות יש ולמה שהתוכן יהיה טקסט או מערך. יותר מזה הקוד מוכן לטפל בהודעה שיש בה מספר בלוקים של טקסט, למרות שאפשר היה לעצור בבלוק הראשון כי יכול להיות מקסימום בלוק טקסט אחד בהודעה. אם יהיה בעתיד שינוי במקום אחר שיגרום לזה שהפונקציה תופעל רק עם הודעות מסוג טקסט או רק עם הודעות מסוג מערך יהיה קשה לזהות את השינוי ולמחוק את הקוד המיותר.
ספריית pi שממנה הגיע הטיפוס AgentMessage והיא שמגדירה את ההודעות השתמשה בשורה הבאה כדי למצוא תוכן טקסטואלי מהודעה:
const content =
typeof this.message.content === "string"
? this.message.content
: this.message.content.find((c) => c.type === "text")?.text || "";
חוץ מזה שהקוד קצר יותר הוא גם הרבה יותר מדויק. בזכות הדיוק הם לא צריכים את ה map וה join כדי לאחד את כל הבלוקים של הטקסט כי ממילא יש רק אחד.
✏ חסר מעבר ניקיון
סוכני קידוד הרבה פעמים עוצרים לפני הזמן, כשברור שהם יכולים לתקן את הבעיה עם עוד איטרציה אבל משהו בלולאה לא עבד. נתבונן בבלוק הזה:
const args = Object.entries(toolCall.arguments)
.map(([key, value]) => {
if (typeof value === "string") {
return `${key}=${JSON.stringify(value)}`;
}
if (typeof value === "object" && value !== null) {
return `${key}=${JSON.stringify(value)}`;
}
return `${key}=${String(value)}`;
})
.join(", ");
על פניו יש פה שלושה מקרים כל אחד מייצר מחרוזת שונה, אבל בעצם שני המקרים הראשונים מכילים את אותו קוד לייצור מחרוזת ולא ברור באיזה מצבים המקרה השלישי אמור לטפל מאחר וארגומנטים של tool call תמיד ניתן להציג עם JSON.stringify. סך הכל ולמיטב הבנתי הקוד יהיה שקול ל:
סוכני קידוד שניסיתי להריץ על הקוד שמחו לנקות אותו ולהגיע לגרסה הקצרה לכן ברור שהם יכולים, פשוט לא תמיד בכתיבת הקוד הם יעשו את הצעד הנוסף הזה.
✏ שורה תחתונה
סוכני קידוד הם כבר חלק בלתי נפרד מהחיים שלנו וימשיכו להיות. הטעות היא לא השימוש בסוכן קידוד אלא העדפת מהירות הכתיבה על פני האיכות. האבסורד הוא שסוכני קידוד קנו לנו זמן בכך שהם חוסכים לנו מאבקים עם תחביר ומביאים אותנו הרבה יותר מהר לגרסה ראשונה עובדת. את הזמן הזה שחסכנו עלינו לנצל כדי ללמוד, להתמקצע ולשפר את הקוד.
המטרה אף פעם לא היתה יותר קוד גרוע שנכתב מהר יותר. המטרה היא מוצרים יציבים ופשוטים יותר, וקוד שמאפשר להתמודד טוב יותר עם מערכות גדולות לאורך זמן.
const args = Object.entries(toolCall.arguments)
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
.join(", ");
סוכני קידוד שניסיתי להריץ על הקוד שמחו לנקות אותו ולהגיע לגרסה הקצרה לכן ברור שהם יכולים, פשוט לא תמיד בכתיבת הקוד הם יעשו את הצעד הנוסף הזה.
✏ שורה תחתונה
סוכני קידוד הם כבר חלק בלתי נפרד מהחיים שלנו וימשיכו להיות. הטעות היא לא השימוש בסוכן קידוד אלא העדפת מהירות הכתיבה על פני האיכות. האבסורד הוא שסוכני קידוד קנו לנו זמן בכך שהם חוסכים לנו מאבקים עם תחביר ומביאים אותנו הרבה יותר מהר לגרסה ראשונה עובדת. את הזמן הזה שחסכנו עלינו לנצל כדי ללמוד, להתמקצע ולשפר את הקוד.
המטרה אף פעם לא היתה יותר קוד גרוע שנכתב מהר יותר. המטרה היא מוצרים יציבים ופשוטים יותר, וקוד שמאפשר להתמודד טוב יותר עם מערכות גדולות לאורך זמן.
👍1😁1