ToCode
1.43K subscribers
3.95K links
טיפים קצרים למתכנתים מאת ינון פרק
Download Telegram
אנחנו עוד נצטרך את הערך שלו. אחרי שהמשתמש מאשר את הגישה דורקיפר שולחת את המשתמש חזרה לסוכן ה AI עם פרמטר בשם code.

החלפת קוד הגישה בטוקן

סוכן ה AI קיבל קוד גישה אבל עדיין אי אפשר לבצע פעולות במערכת עם קוד זה, ל OAuth יש עוד שלב אחד - החלפת קוד הגישה בטוקן. בשביל ההחלפה סוכן ה AI פונה לנתיב:

POST /oauth/token {grant_type, code, code_verifier, client_id}


ומצרף את הקוד שקיבל. דורקיפר בצד השרת מבצעת:

1. מוצאת את השורה הרלוונטית מ oauth_access_grants בה עמודת token שווה לקוד שהתקבל כפרמטר.
2. מחשבת Hash של code_verifier ומוודאת שהוא תואם ל code_challenge ששמור בטבלה.
3. במידה והכל תקין מחזירה Access Token ו Refresh Token. ה Access Token תקף לשעתיים וה Refresh Token לחודש. בעזרת ה Refresh Token אפשר לקבל Access Tokens נוספים בהמשך.

ביצוע בקשות מול ה MCP

עכשיו יש לסוכן ה AI אסימון גישה והוא יכול להתחיל לבצע פעולות בשם המשתמש. פניה ראשונה ל /mcp מגיעה לקובץ:

https://github.com/ynonp/langlets-rails/blob/main/app/controllers/mcp_controller.rb

שם אנחנו פוגשים את הפונקציה handle שמגדירה את הכלים הזמינים ומפעילה את הכלי המתאים:

  def handle
server = MCP::Server.new(
name: "langlets",
version: "1.0.0",
tools: [ Tools::GetVocabulary, Tools::GetCourses ],
server_context: { user: @current_user, scopes: @token_scopes, base_url: request.base_url }
)
transport = MCP::Server::Transports::StreamableHTTPTransport.new(
server, stateless: true, enable_json_response: true
)

status, transport_headers, body = transport.handle_request(request)
transport_headers.each { |key, value| response.set_header(key, value) }
render body: body.join, status: status, content_type: transport_headers["Content-Type"] || "application/json"
end


הכלים עצמם מוגדרים בקבצים בתיקיה:

https://github.com/ynonp/langlets-rails/tree/main/app/mcp/tools

ובתוך כל כלי המשתנה server_context[:user] מחזיק את המשתמש המחובר.

אומנם לנגלטס כתוב ב Ruby אבל העקרונות וזרימת המידע שתוארה כאן יעבדו בכל מערכת שתרצו לחבר לסוכני AI או אפילו באופן כללי יותר למערכות צד שלישי באמצעות OAuth.
📌 בעידן ה AI באמת אין דבר כזה חודשים?

לקוח אמר לי את זה השבוע ואני חייב להודות שגם אני שמתי לב לשינוי: כל רעיון שיש לנו אפשר להעלות לאוויר דמו של הפיצ'ר תוך שעה-שעתיים.

אפילו אם מדובר בפיצ'ר גדול ועם כל האתגר של ביצועים ומעבר לפרודקשן, אם הפידבק חיובי תוך כמה שעות או ימים אנחנו עם זה באוויר.

השבוע חבר שכתב פיצ'ר במערכת. במקור המפתח שבנה את הפיצ'ר ישב על זה חודשים, אולי חצי שנה. החבר העלה גרסה חדשה מאפס וטובה יותר של הפיצ'ר תוך יומיים. הזמנים האלה התקצרו.

כשבוריס צ'רני מדבר על Coding Is Solved אני לא חושב שהכוונה שלא יהיו אנשי תוכנה, אלא שהחלק הספציפי בעבודה של אנשי תוכנה של לקחת רעיון ולהמיר אותו לקוד התקצר משמעותית. השאלה היא כבר לא האם אפשר לבנות את זה או כמה זמן זה ייקח אלא איך אני רוצה שזה יהיה בנוי. זה שינוי גדול.

מצד שני עדיין יש מנגנונים שייקח לנו הרבה זמן לבנות, אולי אפילו חודשים. לדוגמה:

1. פיצ'ר שאנחנו לא בטוחים איך יראה, הרבה פעמים אני מזהה שאני מעלה דמו ללקוח, מקבל משוב, מעלה עוד דמו וכך ממשיכים להתלבט ולנסות עד שמגיעים למשהו שעובד מספיק טוב. דיאלוג כזה יכול לקחת שבועות ואפילו חודשים.
2. פיצ'ר שדורש תיאום עם לקוחות או עם בעלי עניין אחרים. אם צריך מיגרציה של מידע בין מערכות וכדי להריץ את המיגרציה אני צריך ששני צוותים יפסיקו את העבודה השוטפת על המערכות עד שאני מסיים להעביר את הנתונים התיאום הזה יכול לקחת זמן.

ככל שהזמנים לכתיבת קוד מתקצרים אנחנו לומדים לראות את החלקים האחרים של העבודה שלנו ואת החשיבות שלהם: תקשורת, קבלת החלטות מתוך הבנה טכנית מעמיקה, היכרות עם הארגון, יכולת לצפות את העתיד, הבנת המשתמשים. האם מפתחים הופכים יותר לאנשי פרודקט? או שאנשי פרודקט הופכים יותר דומים למפתחים? עדיין מוקדם לדעת. מה שבטוח הוא שהיכולת לדחוף מהר מוצרים עובדים ללקוחות מביאה שינוי עצום לתעשייה.
👍1
📌 בסוף גם קלוד הסכים - המלחמה היא על המעטפת

היתה לי שיחה מוצלחת עם קלוד על האסטרטגיה של אנטרופיק, איפה התחרות ואיפה הם מנסים למנוע אותה. אם יש לכם כח אפשר לקרוא כאן:

https://claude.ai/share/87cbabb1-608f-4375-b37c-b6f659b9bba4

אלה הנקודות המרכזיות שאני העליתי:

1. אנטרופיק באופן עקבי משתמשת בקלוד קוד בתור חומה. יש קושי מכוון לעבור מקלוד קוד לכלים אחרים כי במעבר אנחנו מאבדים את הסקילים, פלאגינים, שרתי MCP שהתקנו, היסטוריית שיחות.
2. גם MCP שהוא לכאורה סטנדרט פתוח היה עובד הרבה יותר טוב בתור הרחבה של תקן Open API. הבחירה להמציא משהו חדש ולאלץ את כל התעשייה להתיישר לא תרמה לתחרות.
3. כשאנטרופיק משתמשים בקלוד קוד בתור חומה ומקשים עלינו לעבור לכלים מתחרים הם אומרים משהו על איכות המודל ועל איפה התחרות האמיתית. אם המודל היה כל כך טוב הם היו שמחים לאפשר לנו לנסות כלים אחרים ומודלים אחרים. במציאות הנוכחית גם אנטרופיק מבינים שהייתרון של אופוס או פייבל על GPT או אפילו דיפסיק לא מספיק בולט ולא מספיק משכנע.

בסוף השיחה קלוד הסביר את זה מאוד יפה:

1. המודלים מדלגים אחד מעל השני בכל גרסה. גם אם היום יש לך מודל ממש טוב אחרי כמה שבועות מתחרה יגיע עם מודל טוב יותר. היתרון ברמת ה Harness הוא הדרך ההגיונית להתחרות כשאתה מבין את מחזור החיים של המודל.
2. הרבה מהיכולות ממומשות ברמת ה Harness ולא ברמת המודל. קלוד קוד הוא חלק מהמוצר שלנו.
3. אף אחד לא טוען שהיתרון היחסי של אנטרופיק הוא ברמת המודל. המוצר שלנו מורכב מכל החלקים של האקוסיסטם.
🔥2
📌 ניסוי Mojo: פייתון רק על טורבו

מוג'ו היא שפת התכנות החדשה של קריס לאטנר, היוצר של swift ושל LLVM. היא שפה מקומפלת עם טיפוסים חזקים ותחביר שמאוד מזכיר את פייתון. יש לה גם ממשק חיבור מובנה לפייתון. מטרתה המוצהרת היא להיות שפת התכנות לאפליקציות AI מאחר והיא גם מאפשרת חיבור מובנה לכל האקוסיסטם של פייתון אבל גם מספקת ביצועים של שפה מקומפלת.

אני לא מומחה ביישומי AI אבל גם בלי לכתוב יישומי AI קל להתחיל לכתוב ב Mojo ולהאיץ יישומי פייתון כמעט בלי מאמץ. בואו נראה איך זה עובד, אם זה באמת כל כך פשוט וכמה ביצועים נקבל ממעבר ל Mojo.

הניסוי: ספירת מספרים ראשוניים בפייתון

תוכנית הניסוי שבחרתי לא מסובכת. זה הקוד:

import time

LIMIT = 1_000_000


def is_prime(n: int) -> bool:
if n < 2:
return False
if n < 4:
return True
if n % 2 == 0:
return False
d = 3
while d * d <= n:
if n % d == 0:
return False
d += 2
return True


def main():
start = time.perf_counter()
count = sum(is_prime(n) for n in range(1, LIMIT + 1))
elapsed = time.perf_counter() - start
print(f"Primes between 1 and {LIMIT:,}: {count:,}")
print(f"Took {elapsed:.3f} s in pure Python")


if __name__ == "__main__":
main()


התוכנית סופרת כמה מספרים ראשוניים יש עד מיליון ובודקת כמה זמן זה לקח. האלגוריתם לא יעיל בכוונה וריצה על המחשב שלי הסתיימה אחרי שניה וקצת:

ynonp@ynons-MacBook-Air ~/tmp/blog/mojodemo (main) $ uv run main_python.py
Primes between 1 and 1,000,000: 78,498
Took 1.341 s in pure Python


איך זה נראה ב Mojo

לפני שנלך לשלב את מוג'ו עם פייתון בואו נראה את אותו הקוד כתוכנית Mojo רגילה. את מוג'ו התקנתי עם uv לפי ההוראות מהאתר שלהם עם הפקודה:

uv add mojo --prerelease allow


ומיד לאחר מכן התקנתי את ה Agent Skill המתאים עם הפקודה:

npx skills add modular/skills


ביקשתי מקלוד לתרגם את הקוד ל mojo וזו התוצאה:

from std.time import perf_counter_ns

comptime LIMIT = 1_000_000


def is_prime(n: Int) -> Bool:
if n < 2:
return False
if n < 4:
return True
if n % 2 == 0:
return False
var d = 3
while d * d <= n:
if n % d == 0:
return False
d += 2
return True


def main():
var start = perf_counter_ns()
var count = 0
for n in range(1, LIMIT + 1):
if is_prime(n):
count += 1
var elapsed = Float64(perf_counter_ns() - start) / 1e9
print(t"Primes between 1 and {LIMIT}: {count}")
print(t"Took {elapsed} s in pure Mojo")


ההבדל בפונקציה is_prime כמעט ולא מורגש - טיפוסי המשתנים ב mojo כתובים עם אות ראשונה גדולה והוספנו var בהצהרה על משתנים. פונקציית ה main של mojo פחות יפה מזו של פייתון, אין שם את ה sum וה List Comprehension ולפני מחרוזת עם משתנים הם רושמים t במקום f. לא משהו לקחת ללב במיוחד כשממילא יש סוכני AI שכותבים את זה.

זמני הריצה לעומת זאת זה סיפור אחר לגמרי:

Primes between 1 and 1000000: 78498
Took 0.02463 s in pure Mojo


במקום קצת יותר משניה אנחנו על 0.02 שניות. אבל שוב זה לא ממש כוחות כי אי אפשר להשוות שפת Interpreter כמו פייתון לשפה מקומפלת.

כח העל של מוג'ו: חיבור עם פייתון

אז איפה מתחיל הכיף? כח העל של Mojo הוא החיבור המובנה לפייתון. אפשר לכתוב קוד ב Mojo ולטעון אותו מפייתון, אפשר לטעון קוד פייתון מתוך קוד מוג'ו. בדוגמה של המספרים הראשוניים הניסוי השלישי להיום יהיה לקחת רק את הפונקציה שבודקת אם מספר הוא ראשוני ולכתוב אותה ב Mojo, תוך שאני שומר על הפונקציה main בפייתון, עם כל הרכבת הרשימות וה sum שאהבתי.

בשביל זה עליי לפצל את הקוד לשני קבצים. הקובץ main_mojo.py מכיל את פונקציית ה main:

import time

import mojo.importer # noqa: F401 (enables importing .mojo modules)
from primes import is_prime

LIMIT = 1_000_000


def main():
start = time.perf_counter()
count = sum(is_prime(n) for n in range(1, LIMIT + 1))
elapsed = time.perf_counter() - start
print(f"Primes between 1 and {LIMIT:,}: {count:,}")
print(f"Took {elapsed:.3f} s with Mojo is_prime")


if __name__ == "__main__":
main()


קובץ זה מייבא את הפונקציה is_prime מהמודול primes - ופה ההפתעה הראשונה, במקום שיהיה לי קובץ primes.py יצרתי קובץ primes.mojo. זה תוכנו:
from std.os import abort
from std.python import PythonObject
from std.python.bindings import PythonModuleBuilder


@export
def PyInit_primes() abi("C") -> PythonObject:
try:
var m = PythonModuleBuilder("primes")
m.def_function[is_prime]("is_prime")
return m.finalize()
except e:
abort(String("failed to create module: ", e))


def is_prime(n_obj: PythonObject) raises -> PythonObject:
var n = Int(py=n_obj)
if n < 2:
return PythonObject(False)
if n < 4:
return PythonObject(True)
if n % 2 == 0:
return PythonObject(False)
var d = 3
while d * d <= n:
if n % d == 0:
return PythonObject(False)
d += 2
return PythonObject(True)


יש פה את הפונקציה is_prime שאנחנו זוכרים מקודם אבל הפעם החתימה שלה שונה וגם המימוש. הפונקציה נקראת מתוך קוד פייתון ולכן הפרמטר שלה הוא מסוג PythonObject. היא גם עלולה לזרוק Exception וב Mojo עלינו להצהיר במפורש כשפונקציה עלולה לזרוק Exception, ובמקום להחזיר בוליאני היא מחזירה PythonObject כי אותו ערך יחזור לפייתון.

הקוד עצמו לוקח את המספר הפייתונאי והופך אותו למספר Int של Mojo, וגם בכל return הקוד עוטף את הערך הבוליאני של Mojo ב PythonObject כדי שפייתון תוכל להשתמש בערך.

החלק השני של הקוד הוא הגדרת המודול. ב Mojo עליי לאתחל את המודול ולשמור בו את כל הפונקציות שאני צריך. שוב קצת כאב ראש אבל לא משהו יותר מדי מאתגר, במיוחד כשממילא קלוד כותב את הקוד.

התוצאה כצפוי יושבת באמצע, יותר איטית ממוג'ו לבד אבל עדיין הרבה יותר מהירה מפייתון לבד:

ynonp@ynons-MacBook-Air ~/tmp/blog/mojodemo (main) $ uv run main_mojo.py
Primes between 1 and 1,000,000: 78,498
Took 0.071 s with Mojo is_prime


סך הכל אם כבר יש לכם יישומי פייתון ואתם מחפשים דרך קלה לשפר ביצועים באמצעות קומפילציה של פונקציות בודדות או חלקים מהתוכנית מוג'ו כבר יכולה לתת לכם כיוון טוב לשיפור. אם אתם יודעים פייתון ומחפשים שפת תכנות מעניינת ללמוד אז Mojo נראית מלהיבה, ואם אתם רוצים לבנות קוד שיבצע פעולות וקטוריות על GPU אז Mojo בכלל נראית כמו אופציה טובה בזכות השילוב של כל האקוסיסטם של פייתון יחד עם ביצועים של שפה מקומפלת.

העליתי לגיטהאב את הקוד של שלושת התוכניות עם כל קבצי הגדרת הפרויקט. מוזמנים להסתכל ולנסות להריץ אצלכם זה הקישור:

https://github.com/ynonp/mojodemo
🔥2
📌 השלכות של פיתוח עם AI על פרויקטים

בפרויקט אחד מצאתי שני API Clients שונים שמחברים לאותו ספק צד-שלישי. ביצירת הפיצול סוכן ה AI כתב הערה ארוכה שמסבירה למה חלק מהפונקציות צריכות לעבור דרך מנגנון אחד ופונקציות אחרות דרך מנגנון שני. משם אף סוכן AI לא העלה בדעתו לבדוק מחדש ולאחד את המנגנונים.

בפרויקט אחר מצאתי שני קבצי CSS סותרים - העיצוב החדש והעיצוב הישן. שניהם הופעלו על העמוד אבל רק העיצוב החדש הופיע והוא דרס את כל הכללים של העיצוב הישן.

במקום אחר אותה פונקציה הופיעה כמה פעמים בשינויים קלים. רוב הזמן זה לא הפריע לאף אחד כי כל חלק בקוד קרא למימוש הספציפי שהתאים לו. עד שמישהו שינה את הפורמט ו-5 מקומות לא קשורים נשברו.

בדוגמה רביעית קיבלתי פונקציה עם הערה שלא תאמה את מה שמבוצע בפונקציה. קלוד שינה את הפונקציה אחרי הכתיבה בלי לעדכן את ההערה. המשך שינויים לפעמים בוצעו לפי קוד הפונקציה ולפעמים לפי ההערה, מה שרק הרחיב את אי התאימות.

בפרויקט חמישי ה AI פיתח מאפס מנגנון שקורא HTML ומוציא ממנו את החלקים החשובים בטקסט במקום להשתמש בספריה קיימת כמו readability של מוזילה או trafilatura.

פרגמנטציה בקוד המיוצר על ידי AI היא עדיין בעיה מרכזית בכל פרויקט. הסוכנים לא עושים מספיק בשביל לאחד מנגנונים או אפילו בשביל לאתר מנגנונים קיימים במערכת לפני שרצים לממש מחדש. שימו לב בעת קריאת קוד שנוצר על ידי AI לזהות כאלה פיצולים, לצמצם אותם וגם להכניס לקבצי AGENTS.md הסבר על המנגנון הקיים, דרישה להשתמש בו ודרישה שכשמשנים מנגנון לא להשאיר את השיטה הישנה.
👍2
📌 איך בכל זאת התחברתי לשרת ה MCP של ספוטיפיי

יש שני סוגים של שרתי MCP, שרתי MCP בתקשורת stdio שרצים אצלכם על המחשב ושרתי MCP שרצים על מכונה מרוחקת. אנטרופיק החליטו לבלבל את התעשייה עם סטנדרט חדש ושמות חדשים אבל האמת היא שמאחורי השמות מסתתרים אותם רכיבים שאנחנו מכירים:

1. שרת MCP מסוג stdio הוא בסך הכל כלי משורת הפקודה או סקריפט.
2. שרת MCP מסוג http הוא בסך הכל API.

במילים אחרות, אם הורדתם שרת MCP מסוג stdio וחיברתם אותו לקלוד או ל VS Code אתם ממש לא חייבים לעבוד איתו רק דרך קלוד או VS Code ואפשר לעבוד איתו רגיל משורת הפקודה עם כלי תרגום כמו mcp2cli. זה באמת בסך הכל סקריפט שמקבל פרמטרים בצורה קצת יותר מסורבלת.

מה שקצת מבלבל אנשים הוא ההזדהות - לקוחות MCP מנהלים בשבילנו את ההזדהות ולכן כשקלוד או VS Code מתחברים לשרת MCP הם שולחים אותנו לאינטרנט כדי להזדהות מול השירות שמתחברים אליו, לוקחים אליהם את הטוקן שחזר ושומרים אותו כדי להפעיל את הכלים. הפעלת כלים בשרת MCP בענן היא בסך הכל קריאת API.

לפעמים שרתי MCP מנסים להרחיק אותנו וליצור מגבלות מלאכותיות. לדוגמה שרת ה MCP של ספוטיפיי עובד מצוין מתוך קלוד אבל כשניסיתי להתחבר אליו מקוד או מ mcp2cli הוא זרק אותי ואמר שהוא מוכן להתחבר רק ללקוחות מורשים. מעבר לקונקטור בקלוד או ChatGPT גם לא מצאתי אזכור רשמי לאותו שרת MCP של ספוטיפיי. מה עושים? קצת מחקר:

1. שאלתי את קלוד מה הכתובת של שרת ה MCP של ספוטיפיי אליה הוא מחובר. את זה לא היתה לו בעיה לספר לי.
2. שמתי לב שכשאני מתחבר לשרת ה MCP של ספוטיפיי הדפדפן מראה כתובת ארוכה שכוללת משתנים כמו redirect url ו client id. רשמתי אותם בצד.
3. שמתי לב שאחרי אישור הגישה לספוטיפיי הדפדפן שלי עובר בבקשת POST לקלוד שלוקח את הקוד שהתקבל ומשתמש בו כדי לקבל אסימון גישה, כל זה מאחורי הקלעים. אז חסמתי את הגישה ל claude באמצעות קובץ /etc/hosts כדי שלא יקבל את האסימון ולקחתי את הכתובת הזאת אליי, גם אותה רשמתי בצד.

עכשיו כבר היה לי את כל מה שהייתי צריך כדי להתחזות לקלוד ולייצר אסימון גישה ל MCP של ספוטיפיי. ביקשתי מקלוד לכתוב סקריפט פייתון שמייצר פרמטרים למסך ההרשאות של ספוטיפיי ומחכה שאני אדביק את כתובת החזרה כדי לקחת משם את הפרמטרים של התשובה, מחבר את הכל לאסימון ואז מחפש קצת מוזיקה בספרדית. זאת התוצאה:

import asyncio
import os
from urllib.parse import parse_qs, parse_qsl, urlencode, urlparse, urlunparse

from dotenv import load_dotenv
from fastmcp.client.auth import OAuth
from pydantic import AnyHttpUrl
from pydantic_ai.mcp import MCPToolset


MCP_URL = "https://mcp-gateway-external-pilot.spotify.net/mcp"
REDIRECT_URI = "https://claude.ai/api/mcp/auth_callback"


class ConsoleOAuth(OAuth):
def __init__(self, client_id: str) -> None:
super().__init__(
mcp_url=MCP_URL,
client_id=client_id,
callback_port=1,
callback_timeout=600,
)
redirect_uris = [AnyHttpUrl(REDIRECT_URI)]
self.context.client_metadata.redirect_uris = redirect_uris
if self._static_client_info is not None:
self._static_client_info.redirect_uris = redirect_uris

async def redirect_handler(self, authorization_url: str) -> None:
parsed = urlparse(authorization_url)
query = dict(parse_qsl(parsed.query))
query["show_dialog"] = "true"
print(urlunparse(parsed._replace(query=urlencode(query))))

async def callback_handler(self) -> tuple[str, str | None]:
callback_url = await asyncio.to_thread(input, "Paste callback URL: ")
query = parse_qs(urlparse(callback_url).query)
code = query.get("code", [None])[0]
state = query.get("state", [None])[0]
if not code:
raise RuntimeError("Callback URL does not contain an OAuth code")
return code, state


async def main() -> None:
load_dotenv()
client_id = "a0894f3786c34e64b0fa0e77f8ea83db"
toolset = MCPToolset(
MCP_URL,
auth=ConsoleOAuth(client_id),
init_timeout=600,
)

async with toolset:
tools = await toolset.list_tools()
search = next(tool for tool in tools if tool.name == "search")
field = "query" if "query" in search.inputSchema["properties"] else "prompt"
        result = await toolset.client.call_tool(
"search",
{field: "nice Spanish music", "language": "en"},
)

print(result.model_dump_json(indent=2, exclude_none=True))


if __name__ == "__main__":
asyncio.run(main())


שרת MCP הוא בסך הכל API. הוא כבר נותן לכם את כל מה שצריך בשביל לבנות אוטומציות וחיבורים למערכת מתוך קוד. ה AI הוא רק בונוס.
📌 קלוד קוד ותכנות הגנתי

אחת הנטיות המובנות של מודלי שפה ביצירת קוד היא ליצור קוד מתגונן, כלומר קוד שבודק מספר אפשרויות לפני שמתייאש. קוד מתגונן היא תבנית טובה כשהקלט מגיע ממקור חיצוני שאין לנו שליטה עליו אבל הוא מיותר ומבלבל כשהמידע מגיע ממקום שאנחנו יצרנו. שימו לב לדוגמה הבאה בפייתון, קיצונית בכוונה:

data = {"x": 10}
if "x" in data:
print(data["x"])


ברור שה if לא משנה בכלום את ריצת התוכנית אבל הוא מפריע לקרוא את הקוד ומעוות את הכוונה. האם כותבי הקוד התכוונו שאולי בהמשך המילון הזה יגיע מבחוץ? האם הקוד שמדפיס את x צריך לטפל בעוד מילונים? אנחנו לא יודעים.

באופן כללי נרצה להשתמש בתכנות הגנתי כשיש ממה להתגונן ונרצה להיות ברורים לגבי המידע שיש לנו כשאנחנו יודעים בדיוק איזה מידע אמור להגיע לפונקציה. סוכני AI די גרועים בזה.

ניקח דוגמה יותר מעניינת מפרויקט maigret. בקובץ report.py של הפרויקט אני מוצא שתי פונקציות: get_plaintext_report ו generate_report_context. הפונקציה generate_report_context מייצרת מילון עם נתונים והפונקציה get_plaintext_report לוקחת את הנתונים ובונה מהם דוח.

בקוד יש באג - אם מחפשים משתמשים ששמם מכיל נקודה ואז רווח הדוח נשבר אחרי הרווח, כלומר משתמש כמו Dr. Evil יגרום בדוח להצגת הטקסט:

Search by username Dr.
Evil returned 0 accounts.
Extended info extracted from 0 accounts.


בגלל שבתוך המילון ש generate_report_context מייצרת יש בלוק של טקסט בו כל שורה מופרדת בנקודה ואז רווח ואז פונקציית יצירת הדוח מפעילה:

output = (context['brief'] + " ").replace('. ', '.\n')


כדי להדפיס את הדוח וזה גורם לשבירת השורה באמצע.

הפיתרון קל ואפילו קלוד יודע מה לעשות כאן, צריך לשנות את generate_report_context כדי שתחזיר את השורות המקוריות של הדוח ואז לא נצטרך לשבור לשורות לפי סימן הנקודה. הבעיה שכשאני מבקש מקלוד לעשות את השינוי הוא מוסיף מפתח brief_lines וכותב את הפקודה ההגנתית:

if "brief_lines" in context:
output = "\n".join(context['brief_lines'])
else:
output = (context['brief'] + " ").replace('. ', '.\n')


זה מיותר כיוון שאנחנו יוצרים את המילון וברור שאם יש בו את brief יהיה בו גם את המפתח החדש brief_lines.

הניסיון הראשון שלי לתקן את זה היה עם התוספת הבאה לקובץ CLAUDE.md:

Project Philosophy

- When we replace a mechanism in code don't keep the old one for record or backward compatibility
- Implement minimalistic solutions
- Consider what data is actually in a dictionary and use the correct key, don't cover too much ground


זה לא עבד.

ניסיון שני היה להחליף את ה dict בחתימת הפונקציה ב TypedDict ולרשום ממש את כל סוגי המפתחות שיש בו. גם זה לא עבד.

בניסיון השלישי הוספתי את הטקסט הבא ל CLAUDE.md ונראה שזה עובד:

Dictionary Defensive Programming

- When you read a key from a dict/object we construct ourselves, access it directly never .get() with a fallback. Fallbacks and defensive access are only for data crossing a trust boundary we don't control: user input, network responses, parsed files


וכן זה מאוד ספציפי למילונים. אבל אולי פה הקסם.

בכל מקרה השורה התחתונה כאן מעבר לתכנות הדפנסיבי היא הגישה. בעבודה עם AI אין טעם רק לנסות לפתור בעיה בקוד אלא צריך להבין למה הבעיה נוצרה והכי טוב אם מצליחים לעדכן את הפרויקט כדי שהבעיה לא תיווצר במקרים דומים בעתיד.
🏆1
📌 הדרך פנימה

הבן שלי בנה בעזרת AI משחק סיימון. כזה עם כפתורים שמגריל רצף של צבעים וצלילים וצריך ללחוץ על הכפתורים ולחזור על הרצף. כיף.

הוא הציע לי לשחק ואני בתמורה הצעתי שנסתכל על הקוד. ואז התחלתי באתגרים - בוא נשנה את הצבעים, בוא נוסיף עוד כפתורים, בוא נקצר או נאריך את הרצף, בוא נמדוד זמן.

בקונטקסט לימודי כל אתגר הוא עוד דרך פנימה לתוך הקוד. עוד הצצה. כל אתגר הוא הזדמנות להבין לא רק איך כותבים קוד אלא גם מה זה קוד טוב ולמה זה חשוב. אם צריך לשנות דברים ב-4 מקומות שונים רק בשביל להוסיף כפתורים זה מלמד אותנו משהו על מנגנון יצירת הכפתורים, ופותח דלת לשאלה הבאה ל AI - "תגיד אפשר לשכתב את הקוד של הכפתורים כדי שאוכל יותר בקלות להוסיף כפתור?"

רק לעתים נדירות ובמערכות מאוד קטנות הדרך פנימה לקוד עוברת בקריאה מלמעלה למטה של כל הקבצים. רוב הזמן, אפילו הרבה לפני ה AI, הדרך לתוך קוד היתה אותם אתגרים או שינויים קטנים. וכן דווקא מימוש שלהם ידנית עוזר לנו לראות מה קל ומה קשה לשנות, בין אם מדובר על מפתח אנושי או AI.
👍3🔥1
📌 סיכום וובינר: גיט וסוכני קידוד

בוובינר השבוע דיברנו על גיט מנקודת המבט של עבודה עם סוכני קידוד. ראינו שתי תבניות עבודה פופולריות עם סוכנים, Spec Driven Development ו Step By Step Prompts ואיך גיט משתלב בכל אחת. בואו ניזכר מה היה שם.

גיט ו Spec Driven Development

שיטת Spec Driven Development אומרת שאנחנו בונים פרומפט אחד ארוך שנקרא Spec וממנו יוצרים את הקוד באמצעות סוכן. הפרומפטים נשמרים ונוכל לקרוא אותם בעתיד או כדי להיזכר מה המטרה של הקוד שנוצר או כדי לייצר מחדש את הקוד אם נרצה לראות פתרונות נוספים לאותן בעיות, למשל עם מודל אחר או עם שינויים קלים בפרומפט.

בעבודת SDD אני מתחיל בכתיבת הפרומפט. בוובינר שמרנו את הפרומפטים בקובץ טקסט בתיקייה בשם spec בתוך הפרויקט. אחרי שהיה לנו פרומפט מוכן הכנסנו אותו לגיט:

$ git add spec/
$ git commit -m 'SPEC: new design'


ואז נתנו לסוכן לייצר את הקוד. אם הסוכן לא עשה עבודה מספיק טובה מחקנו את כל הקוד שהוא יצר:

$ git restore . && git clean -f -d


שיפרנו את קובץ הפרומפט, הכנסנו לגיט את הקובץ המתוקן:

$ git add spec/
$ git commit --amend


ואז הפעלנו שוב את הסוכן כדי לייצר את הקוד. בסוף כשהגענו לקוד שאנחנו מרוצים ממנו הכנסנו אותו עם הודעת קומיט מתאימה:

$ git add .
$ git commit -m 'IMPL: new design'


בסופו של דבר הריפו שלנו כלל זוגות של קומיטים, קומיט אחד של spec ואחריו קומיט של מימוש.

באחד המימושים נתקלנו במכשול נפוץ - מתוך יצירת הקוד זיהינו שעלינו לעדכן משהו בסיסי יותר במערכת או קוד במקום אחר כדי שהמימוש של הפיצ'ר הנוכחי יעבוד טוב יותר. בשביל זה שמנו רגע "בצד" את הקומיט של ה spec באמצעות יצירת ענף בשבילו:

$ git branch new-design


פקודת git branch יצרה ענף חדש אבל השאירה אותי על main. אחרי זה לקחנו "צעד אחורה" וביטלנו את הקומיט של ה spec עם:

$ git reset --hard HEAD~


ואז יצרנו קובץ spec חדש ואת המימוש בשביל הפיצ'ר שהיה חסר, אחרי הקומיט החזרנו את הקומיט מהענף באמצעות ריבייס:

$ git switch new-design
$ git rebase main
$ git switch main
$ git merge new-design
$ git branch -d new-design


וכך קיבלנו:

<commit hash> SPEC: new design
<commit hash> IMPL: bugfix in webapp
<commit hash> SPEC: bugfix in webapp
<commit hash> IMPL: app start
<commit hash> SPEC: app start


ראינו שבעבודה בשיטת Spec Driven Design אנחנו יכולים תמיד לחזור לקומיט של SPEC, להדביק את קובץ הספק בסוכן קידוד ולקבל את הקוד שמתאים למימוש שלו. בגלל שזה תהליך שאפשר לחזור עליו אנחנו יכולים להתמקד בשיפור הפרומפט וגם בשיפור הפרויקט כדי שהסוכן יכתוב קוד טוב יותר.

פיתוח Step By Step

שיטת עבודה שניה שראינו היתה Step By Step Prompting. כאן אין לי פרומפט אחד גדול כי כשאני בונה את הפיצ'ר אני עדיין לא יודע איך הוא יראה. שיטת Step By Step מתאימה למקרים שאני מנסה דברים ולא יודע מה יהיה מבנה הקוד בסיום.

לשיטה זו אני אוהב לפתוח ענף חדש כשמתחיל עבודה על הפיצ'ר וכל התקדמות תהיה קומיט באותו ענף, כלומר יהיה לי:

- prompt -> generate code -> commit
- prompt -> generate code -> commit
- prompt -> generate code -> commit
- prompt -> generate code -> commit
- prompt -> generate code -> commit
- prompt -> generate code -> commit
- prompt -> generate code -> commit


מבחינת גיט זה אומר שאנחנו מתחילים עבודה עם:

$ git switch -c new-design


ואז מייצרים קוד דרך פרומפט ומיד עושים קומיט. הודעות הקומיט בעבודה מסוג זה פחות מושקעות כי אני אפילו לא יודע אם הן יישארו בסוף, אני רק בניסויים. לפעמים ארצה לבטל עבודה של הסוכן או אפילו לקחת כמה צעדים אחורה, בשביל זה יש את reset:

$ git reset --hard HEAD~3


המספר 3 אומר כמה צעדים אחורה ללכת, ואפשר גם לכתוב מזהה קומיט.

בסוף התהליך יש לנו קוד שאנחנו רוצים להכניס למערכת אבל הקומיטים גרועים, לא השקעתי בהודעות קומיט כמו שצריך ואולי קומיט ישן מכניס קוד שיימחק על ידי קומיט חדש יותר. הראיתי שתי טכניקות למיזוג הקוד חזרה לענף הראשי. הראשונה נקראת merge squash והיא אומרת שבעצם כל הענף הזה זה פיצ'ר אחד ולא צריך את ההפרדה לקומיטים, החלוקה לקומיטים היתה רק לצורך פיתוח כדי שיהיה לי קל לזוז אחורה וקדימה בקוד ש AI מייצר. בשביל שילוב merge squash אני עובר לענף הראשי, כותב:

$ git merge --squash new-design
1👍1
פקודה זו לוקחת את כל הקבצים מענף new-design, מעתיקה אותם לענף main ומכניסה ל Staging בלי לקחת את הקומיטים. עכשיו נשאר לי רק לכתוב git commit ולנסח הודעת קומיט אחת שמתאימה לכל הפיצ'ר וכך אני זורק את כל היסטוריית הקומיטים של הענף ונשאר רק עם הגרסה החדשה ביותר שלו והודעת קומיט טובה. פקודת branch -d מוחקת את הענף ואפשר להמשיך בחיים:

$ git commit
$ git branch -d new-design


אפשרות שניה אם הפיתוח הוא גדול אולי אני כן רוצה לחלק לקומיטים אבל לפחות לסדר את הודעות הקומיט ואולי לצמצם חלק מהקומיטים. פקודת git rebase -i מציגה לי את כל הקומיטים בענף ומאפשרת לשנות טקסט של הודעות קומיט לא מושקעות או למחוק קומיטים שאין בהם צורך:

$ git switch new-design
$ git rebase -i main


בסיום התהליך תוכלו להפוך את הענף המבולגן עם 20 קומיטים לענף מסודר עם 3 קומיטים שמייצגים באמת נקודות מפתח שאתם רוצים לשמור בהיסטוריה המלאה.

נ.ב. התוכנית המקורית היתה לדבר גם על git worktree בוובינר אבל לא הספקתי. במקום זה ביקשתי מה AI שיכין פוסט מסודר עליהם בסגנון של טוקוד. זה מה שיצא לדעתי זה נחמד חוץ מזה שצריך להוריד את המסמך כי בצפיה אונליין סדר המילים משובש בגלל העברית:

https://chatgpt.com/share/6a59b2cf-ddcc-83eb-b176-49db4d3ecf37
👍1