📌 סקירת פונקציית 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
משתף כאן ניסוח מאוד מדויק של המפתחים ב Django
https://www.better-simple.com/django/2026/03/16/give-django-your-time-and-money/
מתוך הפוסט שלהם
This isn’t about whether you use an LLM, it’s about whether you still understand what’s being contributed.
וזה המפתח - תשתמשו בסוכן קידוד בשביל לבנות מהר יותר את הפתרונות הטובים שהייתם בונים בכל מקרה, לא בשביל לעשות ספיד ראן לטיקטים
https://www.better-simple.com/django/2026/03/16/give-django-your-time-and-money/
מתוך הפוסט שלהם
This isn’t about whether you use an LLM, it’s about whether you still understand what’s being contributed.
וזה המפתח - תשתמשו בסוכן קידוד בשביל לבנות מהר יותר את הפתרונות הטובים שהייתם בונים בכל מקרה, לא בשביל לעשות ספיד ראן לטיקטים
Better Simple
Give Django your time and money, not your tokens
The Django community wants to collaborate with you, not a facade of you.
📌 לדבר עם בן אדם
בני אדם אומרים "אני לא יודע".
בני אדם שואלים לשלומי ומתכוונים לזה.
בני אדם לא נכנסים ללולאה ובדרך כלל לא מוחקים את כל בסיס הנתונים מתוך יאוש.
בני אדם שואלים בני אדם אחרים ומשתמשים בהגיון כדי לסנן מידע נכנס.
בני אדם מוצאים פתרונות יצירתיים שאף אחד אף פעם לא חשב עליהם.
בני אדם ישאלו "מה התכוונת לעשות שם" לפני שיעזרו לי להתקדם בדרך הלא נכונה.
בני אדם לא יגידו "זה מסובך מדי אני אממש בינתיים פתרון חלקי" כשהם מקבלים משימה.
עדיין דואגים שמכונות יחליפו אתכם? כל מה שצריך זה להשקיע באותן תכונות שהופכות אתכם לבני אדם. חלק לא מבוטל מהזמן לדבר עם בן אדם זה מה שאנחנו צריכים.
בני אדם אומרים "אני לא יודע".
בני אדם שואלים לשלומי ומתכוונים לזה.
בני אדם לא נכנסים ללולאה ובדרך כלל לא מוחקים את כל בסיס הנתונים מתוך יאוש.
בני אדם שואלים בני אדם אחרים ומשתמשים בהגיון כדי לסנן מידע נכנס.
בני אדם מוצאים פתרונות יצירתיים שאף אחד אף פעם לא חשב עליהם.
בני אדם ישאלו "מה התכוונת לעשות שם" לפני שיעזרו לי להתקדם בדרך הלא נכונה.
בני אדם לא יגידו "זה מסובך מדי אני אממש בינתיים פתרון חלקי" כשהם מקבלים משימה.
עדיין דואגים שמכונות יחליפו אתכם? כל מה שצריך זה להשקיע באותן תכונות שהופכות אתכם לבני אדם. חלק לא מבוטל מהזמן לדבר עם בן אדם זה מה שאנחנו צריכים.
👍2
📌 שתי גישות לבדיקת קוד מתוזמן ב Rails
בשביל לזהות טקסט AI בקטע ספרותי משתמשים במקפים המיוחדים שלהם שרק AI אוהב לכתוב. בשביל לזהות קוד שיוצר על ידי AI אנחנו נרצה לשים לב לשימוש יתר בתבניות וערבוב עדין בין תבניות. זה נראה יפה במבט ראשון אבל משהו קטן צורם שם. הדוגמה של היום היא בדיקה בתוכנית ריילס:
הקוד מטפל בסמסים שמגיעים בחלקים כי ההודעה ארוכה ורוצה לבדוק שאחרי החלק הראשון שמנו משימה ל-2 דקות קדימה שתאסוף את החלקים. החשיבות של משימה כזו היא לצורך טיפול בהודעה חלקית, כלומר אם כל החלקים הגיעו אז החלק האחרון יאסוף את כולם ויחבר אותם להודעה אחת. אבל אם רק חלק מהחלקים הגיעו אנחנו צריכים לזהות שעבר מספיק זמן מהחלק הראשון, להניח שההמשך כבר לא יגיע ולחבר את מה שיש. פה מוגדר פרק זמן של 2 דקות המתנה אבל אפשר לשחק עם זה. בכל מקרה מה שחשוב לסיפור שלנו הוא לא הקוד האמיתי אלא הבדיקה.
הבדיקה מנסה לוודא שאחרי הפעלת הפונקציה
1. דרך אחת לזהות שמשימה מתוזמנת נשלחה היא להסתכל על הקריאות לפונקציות של המשימה - אנחנו מצפים שהפונקציה set תקרא עם פרמטר של שתי דקות המתנה ובשביל זה אנחנו דורסים חלק מהפונקציות ומוודאים הפעלה.
2. דרך שניה לזהות שמשימה מתוזמנת נשלחה היא לא לדרוס מימושים אלא להסתכל בתור המשימות המתוזמנות ולראות שיש שם משימה מתוזמנת לשתי דקות קדימה. מי שירצה לעשות את זה יגלה שיש פער זמן קטן בין המשימה המתוזמנת בתור לבין "שתי דקות קדימה" מרגע הבדיקה, כי עברו כמה מילי שניות מהרגע שיצרנו את המשימה המתוזמנת לרגע בו בדקנו אותה. בשביל זה אנחנו נוהגים להקפיא את הזמן לפני הקריאה לפונקציה.
כלומר בגישה הראשונה נכתוב את הבדיקה הזו:
ובגישה השנייה נכתוב את הבדיקה הזו:
והגישה המעורבבת? רק עוד סימן לקוד שנכתב בלי השגחה.
בשביל לזהות טקסט AI בקטע ספרותי משתמשים במקפים המיוחדים שלהם שרק AI אוהב לכתוב. בשביל לזהות קוד שיוצר על ידי AI אנחנו נרצה לשים לב לשימוש יתר בתבניות וערבוב עדין בין תבניות. זה נראה יפה במבט ראשון אבל משהו קטן צורם שם. הדוגמה של היום היא בדיקה בתוכנית ריילס:
context 'when multi-part SMS - first chunk' do
it 'stores in Redis and schedules delayed job' do
allow(Vonage::InboundSmsService).to receive(:new).and_return(double(create_message: nil))
allow(described_class).to receive(:set).and_return(described_class)
allow(described_class).to receive(:perform_later)
freeze_time
described_class.handle_concatenated_chunk(params)
expect(described_class).to have_received(:set).with(wait: 2.minutes)
end
end
הקוד מטפל בסמסים שמגיעים בחלקים כי ההודעה ארוכה ורוצה לבדוק שאחרי החלק הראשון שמנו משימה ל-2 דקות קדימה שתאסוף את החלקים. החשיבות של משימה כזו היא לצורך טיפול בהודעה חלקית, כלומר אם כל החלקים הגיעו אז החלק האחרון יאסוף את כולם ויחבר אותם להודעה אחת. אבל אם רק חלק מהחלקים הגיעו אנחנו צריכים לזהות שעבר מספיק זמן מהחלק הראשון, להניח שההמשך כבר לא יגיע ולחבר את מה שיש. פה מוגדר פרק זמן של 2 דקות המתנה אבל אפשר לשחק עם זה. בכל מקרה מה שחשוב לסיפור שלנו הוא לא הקוד האמיתי אלא הבדיקה.
הבדיקה מנסה לוודא שאחרי הפעלת הפונקציה
handle_concatenated_chunk נשלחת הודעה מתוזמנת לשתי דקות קדימה כדי לטפל בהודעות החלקיות. אבל היא עושה את זה באמצעות ערבוב של שתי תבניות:1. דרך אחת לזהות שמשימה מתוזמנת נשלחה היא להסתכל על הקריאות לפונקציות של המשימה - אנחנו מצפים שהפונקציה set תקרא עם פרמטר של שתי דקות המתנה ובשביל זה אנחנו דורסים חלק מהפונקציות ומוודאים הפעלה.
2. דרך שניה לזהות שמשימה מתוזמנת נשלחה היא לא לדרוס מימושים אלא להסתכל בתור המשימות המתוזמנות ולראות שיש שם משימה מתוזמנת לשתי דקות קדימה. מי שירצה לעשות את זה יגלה שיש פער זמן קטן בין המשימה המתוזמנת בתור לבין "שתי דקות קדימה" מרגע הבדיקה, כי עברו כמה מילי שניות מהרגע שיצרנו את המשימה המתוזמנת לרגע בו בדקנו אותה. בשביל זה אנחנו נוהגים להקפיא את הזמן לפני הקריאה לפונקציה.
כלומר בגישה הראשונה נכתוב את הבדיקה הזו:
context 'when multi-part SMS - first chunk' do
it 'stores in Redis and schedules delayed job' do
allow(Vonage::InboundSmsService).to receive(:new).and_return(double(create_message: nil))
allow(described_class).to receive(:set).and_return(described_class)
allow(described_class).to receive(:perform_later)
described_class.handle_concatenated_chunk(params)
expect(described_class).to have_received(:set).with(wait: 2.minutes)
end
end
ובגישה השנייה נכתוב את הבדיקה הזו:
context 'when multi-part SMS - first chunk' do
it 'stores in Redis and schedules delayed job' do
freeze_time
expect do
described_class.handle_concatenated_chunk(params)
end.to have_enqueued_job(described_class).at(2.minutes.from_now)
end
end
והגישה המעורבבת? רק עוד סימן לקוד שנכתב בלי השגחה.
📌 נכתב על ידי קלוד אופוס 4.6
ביקשתי מקלוד קוד לתקן בעיה לפני כמה ימים ועל דעת עצמו הוא גם עשה קומיט והוסיף להודעת הקומיט את הסיומת
סך הכל זה חמוד שהם עושים לעצמם פרסומת למרות שצריך להגיד את האמת המודל שעשה את התיקון היה חיקוי סיני בשם qwen ולא קלוד אופוס. פתיחת הודעת הקומיט בגיטהאב מראה בשדה "מחבר הקומיט" את השם שלי ושל claude ואפילו את התמונה של שנינו. הודעת הקומיט עצמה כללה הסבר מאוד שטחי על הפיצ'ר עם דגש על "מה בוצע" ולא "למה בוצע", מה שאומר שמי שיגיע אליה בעתיד יצטרך לקרוא הרבה טקסט בלי לקבל הרבה תובנות.
גם בעבודה עם גיט היכרות עם הכלים יכולה לעזור לנו להגיע לתוצאות טובות יותר וגם לתקן טעויות של סוכני קידוד ובעיקר להרגיש פחות אבודים כשסוכן קידוד עושה פעולה בשמנו. אלה פקודות גיט הכי שימושיות בעיניי בעבודה עם סוכני קידוד. לא מכירים אחת מהן? אל תתביישו לבקש מ AI החביב עליכם הסברים ודוגמאות:
✏ תיקון קומיט
פקודת
✏ ריבייס אינטרקטיבי
אם כן שמרתי כמה קומיטים ישנים או שבזמן אמת לא כתבתי הודעות קומיט מספיק טובות אני מקפיד להפעיל ריבייס אינטרקטיבי לפני שמבצע push כדי לתקן הודעות קומיט באותם קומיטים ישנים ולמחוק קומיטי ביניים שלא מוסיפים ערך לפרויקט. אחרי push אני לא אוהב לשנות קומיטים אבל בעבודה שוטפת אני עושה הרבה יותר commit מ push.
✏ פתיחת worktree
פקודת git-worktree פותחת עותק נוסף של הקבצים בפרויקט בתיקייה נפרדת, קצת כמו clone אבל בלי שצריך לעשות push ו pull כדי לסנכרן אותה עם העותק המרכזי שיש לנו על המחשב. בכל worktree אפשר לתת לסוכני קידוד להשתולל בלי שהם מפריעים אחד לשני. אם יש דוקר בפרויקט זה קסום כי אז כל סוכן יכול גם להריץ עותק שלו של כל המערכת ולהריץ בדיקות עד שהוא מצליח לבנות את הפיצ'ר.
✏ בדיקת הבדלים ושחזור
פקודות
✏ מחיקת כל העבודה של הסוכן מאז הקומיט האחרון
עוד שתי פקודות סופר שימושיות הן:
כשהסוכן התחיל להשתולל ואני מבין שהוא לא בכיוון שתי הפקודות האלה מוחקות את כל השינויים ומחזירות אותנו לקומיט האחרון. ואם אתם מגלים שגם הקומיט האחרון כבר לא רלוונטי תמיד אפשר להשתמש ב
יש לכם פקודות גיט נוספות שאתם אוהבים להשתמש עם סוכני קידוד? ספרו לי בתגובות.
ביקשתי מקלוד קוד לתקן בעיה לפני כמה ימים ועל דעת עצמו הוא גם עשה קומיט והוסיף להודעת הקומיט את הסיומת
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
סך הכל זה חמוד שהם עושים לעצמם פרסומת למרות שצריך להגיד את האמת המודל שעשה את התיקון היה חיקוי סיני בשם qwen ולא קלוד אופוס. פתיחת הודעת הקומיט בגיטהאב מראה בשדה "מחבר הקומיט" את השם שלי ושל claude ואפילו את התמונה של שנינו. הודעת הקומיט עצמה כללה הסבר מאוד שטחי על הפיצ'ר עם דגש על "מה בוצע" ולא "למה בוצע", מה שאומר שמי שיגיע אליה בעתיד יצטרך לקרוא הרבה טקסט בלי לקבל הרבה תובנות.
גם בעבודה עם גיט היכרות עם הכלים יכולה לעזור לנו להגיע לתוצאות טובות יותר וגם לתקן טעויות של סוכני קידוד ובעיקר להרגיש פחות אבודים כשסוכן קידוד עושה פעולה בשמנו. אלה פקודות גיט הכי שימושיות בעיניי בעבודה עם סוכני קידוד. לא מכירים אחת מהן? אל תתביישו לבקש מ AI החביב עליכם הסברים ודוגמאות:
✏ תיקון קומיט
פקודת
git commit --amend מתקנת את הקומיט האחרון באמצעות הוספת קבצים או שינוי הודעת הקומיט. בתהליך עבודה רגיל שלי עם סוכן קידוד אני משתמש בקומיט תיקון אחרי כל שינוי קוד שהוא עושה כלומר לפני כל הודעה נוספת שאני כותב לסוכן. בצורה כזאת אם אחרי ההודעה הסוכן החליט לזרוע הרס בכל הפרויקט תמיד אפשר לחזור צעד אחורה, גם אם כפתור הביטול ב VS Code או קרסר לא בדיוק עובד.✏ ריבייס אינטרקטיבי
אם כן שמרתי כמה קומיטים ישנים או שבזמן אמת לא כתבתי הודעות קומיט מספיק טובות אני מקפיד להפעיל ריבייס אינטרקטיבי לפני שמבצע push כדי לתקן הודעות קומיט באותם קומיטים ישנים ולמחוק קומיטי ביניים שלא מוסיפים ערך לפרויקט. אחרי push אני לא אוהב לשנות קומיטים אבל בעבודה שוטפת אני עושה הרבה יותר commit מ push.
✏ פתיחת worktree
פקודת git-worktree פותחת עותק נוסף של הקבצים בפרויקט בתיקייה נפרדת, קצת כמו clone אבל בלי שצריך לעשות push ו pull כדי לסנכרן אותה עם העותק המרכזי שיש לנו על המחשב. בכל worktree אפשר לתת לסוכני קידוד להשתולל בלי שהם מפריעים אחד לשני. אם יש דוקר בפרויקט זה קסום כי אז כל סוכן יכול גם להריץ עותק שלו של כל המערכת ולהריץ בדיקות עד שהוא מצליח לבנות את הפיצ'ר.
✏ בדיקת הבדלים ושחזור
פקודות
git log -p ו git diff מראות לנו בצורה נוחה הבדלים בין גרסאות. אם נתתם לסוכן לעבוד על ענף נפרד תוכלו להפעיל git diff feature...main כדי לראות את כל העבודה שלו. תלוי בהגדרת הפרויקט שלכם אבל הרבה פעמים אנחנו נמצא בעבודה שינויים לא רלוונטיים, למשל תיקוני סגנון. אני בדרך כלל מבקש מהסוכן לנקות את הפיצ'ר ולהשאיר רק את השינויים הרלוונטיים למה שהתבקש וגם משתמש בעצמי ב git restore כדי לבטל שינויים שנעשו בקובץ מסוים.✏ מחיקת כל העבודה של הסוכן מאז הקומיט האחרון
עוד שתי פקודות סופר שימושיות הן:
$ git restore .
$ git clean -f -d
כשהסוכן התחיל להשתולל ואני מבין שהוא לא בכיוון שתי הפקודות האלה מוחקות את כל השינויים ומחזירות אותנו לקומיט האחרון. ואם אתם מגלים שגם הקומיט האחרון כבר לא רלוונטי תמיד אפשר להשתמש ב
git reset כדי להזיז את ראש הפרויקט לקומיט ישן יותר.יש לכם פקודות גיט נוספות שאתם אוהבים להשתמש עם סוכני קידוד? ספרו לי בתגובות.
👍3
📌 ניסוי httpx - השוואה בין קוד מקבילי, טורי ואסינכרוני
חבילת httpx בפייתון היא סוג של הדור הבא של requests עם תיעוד יותר מעודכן ותמיכה בכתיבה אסינכרונית. בואו נראה איך זה עובד דרך תוכנית קצרה וגם נשווה זמנים בין גרסה טורית, מקבילית ואסינכרונית.
בשביל הדוגמה לקחתי 4 ספרים מפרויקט גטנברג ובניתי 4 פונקציות שמורידות את ארבעת הספרים, כל אחת בדרך אחרת. הקוד מתחיל ב import-ים והגדרת רשימת הספרים:
הפונקציה הראשונה פשוט מפעילה get ארבע פעמים אחת אחרי השניה:
נשים לב ש httpx מגדירה timeout ברירת מחדל של 5 שניות ועבור פרויקט גטנברג זה לא תמיד מספיק אז הגדלתי לדקה. הגדרתי גם פונקציית הורדה נפרדת כדי לחסוך כפל קוד בדוגמאות הבאות.
פונקציה שניה כבר משתמשת ב client של httpx שזה מנגנון שלהם ששומר את החיבור לאתר פתוח כדי להוריד מספר קבצים:
בגרסה השלישית רציתי לראות איך הסיפור הזה יעבוד מכמה תהליכונים במקביל כי שמעתי שכשמורידים קובץ במקביל הזמנים מתקצרים. פה כתבתי שתי גרסאות אחת עם client והשניה בלי:
ולסיום הורדה אסינכרונית. כאן לא ראיתי דרך בלי client אז הגדרתי אותו וגם בגלל שמדובר בקלאיינט אסינכרוני כבר אי אפשר היה להשתמש באותה פונקציית download שכבר כתבתי ולכן בניתי גרסה אסינכרונית שלה גם:
לסיום פונקציית main שמריצה את כל העסק וגם מודדת זמנים:
אלה התוצאות:
חבילת httpx בפייתון היא סוג של הדור הבא של requests עם תיעוד יותר מעודכן ותמיכה בכתיבה אסינכרונית. בואו נראה איך זה עובד דרך תוכנית קצרה וגם נשווה זמנים בין גרסה טורית, מקבילית ואסינכרונית.
בשביל הדוגמה לקחתי 4 ספרים מפרויקט גטנברג ובניתי 4 פונקציות שמורידות את ארבעת הספרים, כל אחת בדרך אחרת. הקוד מתחיל ב import-ים והגדרת רשימת הספרים:
import httpx
from timeit import timeit
from multiprocessing.dummy import Pool
import asyncio
book_urls = [
"https://www.gutenberg.org/cache/epub/84/pg84.txt",
"https://www.gutenberg.org/cache/epub/45304/pg45304.txt",
"https://www.gutenberg.org/cache/epub/2701/pg2701.txt",
"https://www.gutenberg.org/cache/epub/1342/pg1342.txt"
]
הפונקציה הראשונה פשוט מפעילה get ארבע פעמים אחת אחרי השניה:
def download_and_print_size(client, url):
r = client.get(url, timeout=60.0)
size = len(r.text)
print(f"Url: {url}; size: {size}")
def serial():
print("=== serial ===")
for url in book_urls:
download_and_print_size(httpx, url)
נשים לב ש httpx מגדירה timeout ברירת מחדל של 5 שניות ועבור פרויקט גטנברג זה לא תמיד מספיק אז הגדלתי לדקה. הגדרתי גם פונקציית הורדה נפרדת כדי לחסוך כפל קוד בדוגמאות הבאות.
פונקציה שניה כבר משתמשת ב client של httpx שזה מנגנון שלהם ששומר את החיבור לאתר פתוח כדי להוריד מספר קבצים:
def client_serial():
print("=== client_serial ===")
with httpx.Client() as client:
try:
for url in book_urls:
download_and_print_size(client, url)
finally:
client.close()
בגרסה השלישית רציתי לראות איך הסיפור הזה יעבוד מכמה תהליכונים במקביל כי שמעתי שכשמורידים קובץ במקביל הזמנים מתקצרים. פה כתבתי שתי גרסאות אחת עם client והשניה בלי:
def parallel():
print("=== parallel ===")
with Pool(5) as p:
p.map(lambda url: download_and_print_size(httpx, url), book_urls)
def client_parallel_threads():
print("=== client_parallel_threads ===")
with httpx.Client() as client:
try:
with Pool(5) as p:
p.map(lambda url: download_and_print_size(client, url), book_urls)
finally:
client.close()
ולסיום הורדה אסינכרונית. כאן לא ראיתי דרך בלי client אז הגדרתי אותו וגם בגלל שמדובר בקלאיינט אסינכרוני כבר אי אפשר היה להשתמש באותה פונקציית download שכבר כתבתי ולכן בניתי גרסה אסינכרונית שלה גם:
async def download_and_print_size_async(client, url):
r = await client.get(url, timeout=60.0)
size = len(r.text)
print(f"Url: {url}; size: {size}")
async def client_parallel():
print("=== client_parallel ===")
async with httpx.AsyncClient() as client:
try:
await asyncio.gather(*[download_and_print_size_async(client, url) for url in book_urls])
finally:
await client.aclose()
לסיום פונקציית main שמריצה את כל העסק וגם מודדת זמנים:
def main():
print(timeit(lambda: serial(), number=1))
print(timeit(lambda: client_serial(), number=1))
print(timeit(lambda: parallel(), number=1))
print(timeit(lambda: client_parallel_threads(), number=1))
print(timeit(lambda: asyncio.run(client_parallel()), number=1))
if __name__ == "__main__":
main()
אלה התוצאות:
=== serial ===
Url: https://www.gutenberg.org/cache/epub/84/pg84.txt; size: 446583
Url: https://www.gutenberg.org/cache/epub/45304/pg45304.txt; size: 1356681
Url: https://www.gutenberg.org/cache/epub/2701/pg2701.txt; size: 1260594
Url: https://www.gutenberg.org/cache/epub/1342/pg1342.txt; size: 763083
32.953853541985154
=== client_serial ===
Url: https://www.gutenberg.org/cache/epub/84/pg84.txt; size: 446583
Url: https://www.gutenberg.org/cache/epub/45304/pg45304.txt; size: 1356681
Url: https://www.gutenberg.org/cache/epub/2701/pg2701.txt; size: 1260594
Url: https://www.gutenberg.org/cache/epub/1342/pg1342.txt; size: 763083
12.404891041107476
=== parallel ===
Url: https://www.gutenberg.org/cache/epub/84/pg84.txt; size: 446583
❤2
Url: https://www.gutenberg.org/cache/epub/1342/pg1342.txt; size: 763083
Url: https://www.gutenberg.org/cache/epub/45304/pg45304.txt; size: 1356681
Url: https://www.gutenberg.org/cache/epub/2701/pg2701.txt; size: 1260594
6.713048083242029
=== client_parallel_threads ===
Url: https://www.gutenberg.org/cache/epub/1342/pg1342.txt; size: 763083
Url: https://www.gutenberg.org/cache/epub/84/pg84.txt; size: 446583
Url: https://www.gutenberg.org/cache/epub/45304/pg45304.txt; size: 1356681
Url: https://www.gutenberg.org/cache/epub/2701/pg2701.txt; size: 1260594
11.749367957934737
=== client_parallel ===
Url: https://www.gutenberg.org/cache/epub/84/pg84.txt; size: 446583
Url: https://www.gutenberg.org/cache/epub/45304/pg45304.txt; size: 1356681
Url: https://www.gutenberg.org/cache/epub/2701/pg2701.txt; size: 1260594
Url: https://www.gutenberg.org/cache/epub/1342/pg1342.txt; size: 763083
7.422760541085154
מופתעים? גם אני הופתעתי קצת. כלומר ברור למה הגרסה הראשונה מאוד איטית אבל אני מודה שגרסת התהליכונים בלי client הפתיעה לטובה ועקפה גם את הגרסה האסינכרונית. זה לא החזיק מעמד ובהרצה אחרת קיבלתי את התוצאה הזו:
=== serial ===
Url: https://www.gutenberg.org/cache/epub/84/pg84.txt; size: 446583
Url: https://www.gutenberg.org/cache/epub/45304/pg45304.txt; size: 1356681
Url: https://www.gutenberg.org/cache/epub/2701/pg2701.txt; size: 1260594
Url: https://www.gutenberg.org/cache/epub/1342/pg1342.txt; size: 763083
24.061222000047565
=== client_serial ===
Url: https://www.gutenberg.org/cache/epub/84/pg84.txt; size: 446583
Url: https://www.gutenberg.org/cache/epub/45304/pg45304.txt; size: 1356681
Url: https://www.gutenberg.org/cache/epub/2701/pg2701.txt; size: 1260594
Url: https://www.gutenberg.org/cache/epub/1342/pg1342.txt; size: 763083
14.5340595417656
=== parallel ===
Url: https://www.gutenberg.org/cache/epub/84/pg84.txt; size: 446583
Url: https://www.gutenberg.org/cache/epub/1342/pg1342.txt; size: 763083
Url: https://www.gutenberg.org/cache/epub/45304/pg45304.txt; size: 1356681
Url: https://www.gutenberg.org/cache/epub/2701/pg2701.txt; size: 1260594
8.315202291123569
=== client_parallel_threads ===
Url: https://www.gutenberg.org/cache/epub/1342/pg1342.txt; size: 763083
Url: https://www.gutenberg.org/cache/epub/84/pg84.txt; size: 446583
Url: https://www.gutenberg.org/cache/epub/45304/pg45304.txt; size: 1356681
Url: https://www.gutenberg.org/cache/epub/2701/pg2701.txt; size: 1260594
7.089039749931544
=== client_parallel ===
Url: https://www.gutenberg.org/cache/epub/1342/pg1342.txt; size: 763083
Url: https://www.gutenberg.org/cache/epub/84/pg84.txt; size: 446583
Url: https://www.gutenberg.org/cache/epub/45304/pg45304.txt; size: 1356681
Url: https://www.gutenberg.org/cache/epub/2701/pg2701.txt; size: 1260594
5.17865683324635
סך הכל הניסוי מוכיח מה שכבר ידענו - שעדיף להוריד קבצים במקביל מאשר בטור אבל שאין יתרון משמעותי להרצה אסינכרונית על פני שימוש בתהליכונים.
מעניין גם לשים לב לתקורה בשימוש בקוד אסינכרוני: גם הרצה עם asyncio.run, גם שימוש ב asyncio.gather בשביל להריץ דברים במקביל וגם אי אפשר היה להשתמש בקוד הסינכרוני הקיים וצריך היה לכתוב מחדש את פונקציית ההורדה. אני הרצתי על פייתון 3.14 והרצתי כמה פעמים התוצאות כל הרצה כמובן קצת שונות אבל מבחינת השוואת מהירות בין האופציות התוצאה עקבית.
📌 געגועים לספריות קוד פתוח
ספריות קוד פתוח תמיד היוו מעין קרש קפיצה מהיר לפיתוח. צריך תיבת בחירה יפה עם חיפוש ובחירה מרובה? יש קומפוננטה לזה. צריך להוריד קבצים מהאינטרנט עם אופציה להזדהות וטיפול בנסיונות חוזרים? יש ספריה לזה.
אני עדיין משתמש בספריות קוד פתוח, אבל הרבה פחות. ככל שיותר קוד נכתב על ידי סוכני קידוד הסוכן משתמש בספריות שהוא "מכיר" או "מעדיף", ומקודד מאפס את הרכיבים שהוא לא מכיר. שימוש בספריית קוד פתוח בפרויקט אומר שסוכן הקידוד צריך להתאמץ יותר בשביל לבנות את הפיצ'ר הבא או לתקן באג. ככל שהספריה פחות מתועדת או פופולרית לסוכן יהיה יותר קשה לעבוד איתה וכל שינוי ידרוש העמסה של תיעוד רלוונטי לתוך חלון הקונטקסט.
מה הפסדנו פה? אני לא בטוח. שלחתי את לאבבל לבנות לי משחק תרגול צלילים בגיטרה. זה כאן אם אתם רוצים לנסות. המעניין שם זה שאת צוואר הגיטרה לאבבל צייר לבד באמצעות SVG. בעולם הישן ציור צוואר גיטרה ב SVG היה בעיניי האופציה האחרונה. קודם הייתי מחפש ספריית קוד פתוח ובמקרה הגרוע מצייר את זה ב HTML ו CSS. הסוכן יצר קומפוננטה של גיטרה שנראית ממש אחלה ועובדת עבור המשחק לא פחות טוב מ react-fretboard או react-guitar, עם קוד ציור יפה ב SVG. אפשר לראות את הקוד שלו כאן:
https://github.com/ynonp/fret-play-learn/blob/main/src/components/GuitarFretboard.tsx
המשחק היותר משמעותי בפרידה מספריות קוד פתוח הוא השאלה "מי יתחזק את זה?". גם כאן אין לי דעה חד משמעית. מצד אחד כשהעולם משתנה ומתגלות בעיות אבטחה נוח שיש אנשים אחרים שמתחזקים את ספריית הקוד הפתוח האהובה עליי ומעלים גרסה חדשה וטובה יותר. מצד שני הרבה מהשדרוגים בספריות קוד פתוח הם תוספת של פיצ'רים שאני לא צריך ויוצא שאני מבזבז זמן על התאמות לממשק של גרסה חדשה שבכלל לא ביקשתי.
בינתיים דוגמת הקומפוננטה של הגיטרה לימדה אותי שמאוד נוח כשהסוכן בונה מאפס את הרכיב המדויק שאני צריך. כן כדאי להקפיד לבקש מהסוכן כשצריך שישים לב כשיש קומפוננטה לשימוש חוזר ויבנה אותה בצד בתור קומפוננטה נפרדת. בפיתוחים חדשים ובמצב תכנון הוא יראה את זה לבד. בתיקוני באגים הרבה יותר קשה לו לשים לב.
ספריות קוד פתוח תמיד היוו מעין קרש קפיצה מהיר לפיתוח. צריך תיבת בחירה יפה עם חיפוש ובחירה מרובה? יש קומפוננטה לזה. צריך להוריד קבצים מהאינטרנט עם אופציה להזדהות וטיפול בנסיונות חוזרים? יש ספריה לזה.
אני עדיין משתמש בספריות קוד פתוח, אבל הרבה פחות. ככל שיותר קוד נכתב על ידי סוכני קידוד הסוכן משתמש בספריות שהוא "מכיר" או "מעדיף", ומקודד מאפס את הרכיבים שהוא לא מכיר. שימוש בספריית קוד פתוח בפרויקט אומר שסוכן הקידוד צריך להתאמץ יותר בשביל לבנות את הפיצ'ר הבא או לתקן באג. ככל שהספריה פחות מתועדת או פופולרית לסוכן יהיה יותר קשה לעבוד איתה וכל שינוי ידרוש העמסה של תיעוד רלוונטי לתוך חלון הקונטקסט.
מה הפסדנו פה? אני לא בטוח. שלחתי את לאבבל לבנות לי משחק תרגול צלילים בגיטרה. זה כאן אם אתם רוצים לנסות. המעניין שם זה שאת צוואר הגיטרה לאבבל צייר לבד באמצעות SVG. בעולם הישן ציור צוואר גיטרה ב SVG היה בעיניי האופציה האחרונה. קודם הייתי מחפש ספריית קוד פתוח ובמקרה הגרוע מצייר את זה ב HTML ו CSS. הסוכן יצר קומפוננטה של גיטרה שנראית ממש אחלה ועובדת עבור המשחק לא פחות טוב מ react-fretboard או react-guitar, עם קוד ציור יפה ב SVG. אפשר לראות את הקוד שלו כאן:
https://github.com/ynonp/fret-play-learn/blob/main/src/components/GuitarFretboard.tsx
המשחק היותר משמעותי בפרידה מספריות קוד פתוח הוא השאלה "מי יתחזק את זה?". גם כאן אין לי דעה חד משמעית. מצד אחד כשהעולם משתנה ומתגלות בעיות אבטחה נוח שיש אנשים אחרים שמתחזקים את ספריית הקוד הפתוח האהובה עליי ומעלים גרסה חדשה וטובה יותר. מצד שני הרבה מהשדרוגים בספריות קוד פתוח הם תוספת של פיצ'רים שאני לא צריך ויוצא שאני מבזבז זמן על התאמות לממשק של גרסה חדשה שבכלל לא ביקשתי.
בינתיים דוגמת הקומפוננטה של הגיטרה לימדה אותי שמאוד נוח כשהסוכן בונה מאפס את הרכיב המדויק שאני צריך. כן כדאי להקפיד לבקש מהסוכן כשצריך שישים לב כשיש קומפוננטה לשימוש חוזר ויבנה אותה בצד בתור קומפוננטה נפרדת. בפיתוחים חדשים ובמצב תכנון הוא יראה את זה לבד. בתיקוני באגים הרבה יותר קשה לו לשים לב.
fret-play-learn.lovable.app
Lovable App
Lovable Generated Project
📌 מ MCP ל Skills
מרגע המצאת פרוטוקול MCP הוא התפשט כאש בשדה קוצים. כל מוצר, כל יצרן, כל API הרגיש שהוא חייב להפיץ MCP כדי שכלי AI יוכלו להתחבר לאותו המוצר. אז יש לנו MCP של ג'ירה בעזרתו המודל יכול לקרוא טיקטים, ו MCP של פיגמה בעזרתו המודל יכול לקרוא את העיצובים וגם MCP של פליירייט איתו אפשר לשלוט בדפדפן ועוד אינסוף אפשרויות. MCP הוא בסך הכל אוסף "כלים", כלומר אוסף של פקודות שהמודל יכול להפעיל. לכל פקודה יש תיאור משלה ודרך הפעלה משלה. דוגמאות לפקודות שמגיעות מ MCP יהיו "קרא קובץ מריפו בגיט", "שכפל את הריפו המרוחק", "הפעל דפדפן וגלוש לכתובת מסוימת", "החזר רשימה של האוביקטים בפריים מסוים".
חסרון אחד של MCP הוא שהרבה אנשים התקינו אותם אבל מעט אנשים כתבו אותם. זה היה יותר סיפור תרבותי מאשר מגבלה טכנולוגית כי באמת שלא מסובך לכתוב שרתי MCP אבל זה היה המצב. חסרון שני של MCP היה שכל MCP שאני מתקין "תופס מקום" בחלון הקונטקסט של המודל ולכן בתחילת כל שיחה היינו צריכים לבחור באיזה שרתי MCP אנחנו מסכימים להשתמש בשיחה זו.
מנגנון חלופי שצובר תאוצה ועוזר לפתור בעיה דומה נקרא Skills. בניגוד ל MCP, ה Skill הוא מודולרי. המודל מקבל תיאור כללי של ה Skill ומחליט לבד אם לטעון אותו או לא, כלומר טעינת ה Skill היא הפעלת כלי שהמודל יוזם. הבדל נוסף מ MCP הוא ש Skill הוא הרבה יותר חופשי - אני כותב את התיאור בקבצי Markdown ואת הקוד בקבצי סקריפט באיזה פורמט שאני בוחר. התוצאה היא שהרבה יותר קל לכתוב Skill וגם לקרוא את הקוד שלו.
דוגמה? בטח. בואו ניתן למודל שלנו אופציה לחפש ב npm חבילות הרחבה. ה Skill מורכב בסך הכל מקובץ הוראות ומסקריפט חיפוש אותם אני שם בתיקייה עם שם מיוחד בתיקיית הבית.
נפתח תיקיה חדשה
בתוך התיקייה אני יוצר קובץ בשם
Or run directly:
📚 Example Searches
✏ Evaluating Packages
When reviewing search results, consider:
1. Downloads: Higher weekly downloads indicate community trust
2. Version: Recent versions suggest active maintenance
3. Description: Clear descriptions indicate well-documented packages
4. Dependencies: Fewer dependencies mean smaller bundle size
✏ Installation
Once you've identified a suitable package:
✏ Implementation
After installation, read the package's README or documentation before using it. Look for:
⦁ Basic usage examples
⦁ Configuration options
⦁ Common patterns and best practices
מרגע המצאת פרוטוקול MCP הוא התפשט כאש בשדה קוצים. כל מוצר, כל יצרן, כל API הרגיש שהוא חייב להפיץ MCP כדי שכלי AI יוכלו להתחבר לאותו המוצר. אז יש לנו MCP של ג'ירה בעזרתו המודל יכול לקרוא טיקטים, ו MCP של פיגמה בעזרתו המודל יכול לקרוא את העיצובים וגם MCP של פליירייט איתו אפשר לשלוט בדפדפן ועוד אינסוף אפשרויות. MCP הוא בסך הכל אוסף "כלים", כלומר אוסף של פקודות שהמודל יכול להפעיל. לכל פקודה יש תיאור משלה ודרך הפעלה משלה. דוגמאות לפקודות שמגיעות מ MCP יהיו "קרא קובץ מריפו בגיט", "שכפל את הריפו המרוחק", "הפעל דפדפן וגלוש לכתובת מסוימת", "החזר רשימה של האוביקטים בפריים מסוים".
חסרון אחד של MCP הוא שהרבה אנשים התקינו אותם אבל מעט אנשים כתבו אותם. זה היה יותר סיפור תרבותי מאשר מגבלה טכנולוגית כי באמת שלא מסובך לכתוב שרתי MCP אבל זה היה המצב. חסרון שני של MCP היה שכל MCP שאני מתקין "תופס מקום" בחלון הקונטקסט של המודל ולכן בתחילת כל שיחה היינו צריכים לבחור באיזה שרתי MCP אנחנו מסכימים להשתמש בשיחה זו.
מנגנון חלופי שצובר תאוצה ועוזר לפתור בעיה דומה נקרא Skills. בניגוד ל MCP, ה Skill הוא מודולרי. המודל מקבל תיאור כללי של ה Skill ומחליט לבד אם לטעון אותו או לא, כלומר טעינת ה Skill היא הפעלת כלי שהמודל יוזם. הבדל נוסף מ MCP הוא ש Skill הוא הרבה יותר חופשי - אני כותב את התיאור בקבצי Markdown ואת הקוד בקבצי סקריפט באיזה פורמט שאני בוחר. התוצאה היא שהרבה יותר קל לכתוב Skill וגם לקרוא את הקוד שלו.
דוגמה? בטח. בואו ניתן למודל שלנו אופציה לחפש ב npm חבילות הרחבה. ה Skill מורכב בסך הכל מקובץ הוראות ומסקריפט חיפוש אותם אני שם בתיקייה עם שם מיוחד בתיקיית הבית.
נפתח תיקיה חדשה
~/.claude/skills:mkdir -p ~/.claude/skills/npm-search
בתוך התיקייה אני יוצר קובץ בשם
SKILL.md עם התוכן הבא:---
name: npm-search
description: Use this skill whenever implementing a feature or adding functionality to a JavaScript/TypeScript/Node.js project. Before writing custom code, always run npm search to find existing libraries that can solve the problem. This prevents reinventing the wheel and leverages the npm ecosystem.
---
# NPM Search Guide
## Overview
Before implementing any feature in a JavaScript/TypeScript/Node.js project, search npm for existing libraries that can solve the problem. This ensures you leverage the ecosystem rather than reinventing the wheel.
## When to Search npm
Use npm search before implementing:
- Data processing or transformation utilities
- HTTP clients or API helpers
- Date/time manipulation
- Validation libraries
- Authentication/authorization
- File handling (CSV, JSON, XML, PDF, etc.)
- Database connectors
- Testing utilities
- UI components (for frontend projects)
- Any common utility or pattern
## Running npm Search
Use the provided script to search for packages:
```bash
./scripts/npm-search.sh <search-term>
Or run directly:
npm search <search-term>
📚 Example Searches
# Looking for a date library
./scripts/npm-search.sh date format manipulate
# Looking for a validation library
./scripts/npm-search.sh schema validation zod joi
# Looking for an HTTP client
./scripts/npm-search.sh http client axios fetch
# Looking for a CSV parser
./scripts/npm-search.sh csv parse stream
✏ Evaluating Packages
When reviewing search results, consider:
1. Downloads: Higher weekly downloads indicate community trust
2. Version: Recent versions suggest active maintenance
3. Description: Clear descriptions indicate well-documented packages
4. Dependencies: Fewer dependencies mean smaller bundle size
✏ Installation
Once you've identified a suitable package:
npm install <package-name>
# or
pnpm add <package-name>
# or
yarn add <package-name>
✏ Implementation
After installation, read the package's README or documentation before using it. Look for:
⦁ Basic usage examples
⦁ Configuration options
⦁ Common patterns and best practices
נשים לב שרק החלק העליון של המארקדאון נטען כברירת מחדל לקלוד קוד. רק אם המודל יחליט לטעון את הסקיל הוא יקרא את הקובץ המלא.
לסיום אני יוצר סקריפט בשם `scripts/npm-search.sh` בתוך אותה תיקייה עם התוכן הבא:
```language-sh
❤1
#!/bin/bash
# npm-search.sh - Search npm for packages
# Usage: ./scripts/npm-search.sh <search-terms>
if [ $# -eq 0 ]; then
echo "Usage: $0 <search-terms>"
echo "Example: $0 date format manipulate"
echo " $0 csv parse stream"
exit 1
fi
SEARCH_QUERY="$*"
echo "🔍 Searching npm for: $SEARCH_QUERY"
echo "----------------------------------------"
npm search "$SEARCH_QUERY"
וזה הכל. עכשיו כשאני אבקש מקלוד קוד לממש פיצ'ר בפרויקט ווב הוא יחפש קודם ב npm search אם קיימת חבילה ואם ימצא חבילה רלוונטית הוא ישתמש בה.
סך הכל הסקיל מורכב משני הקבצים:
ynonp@ynons-MacBook-Air ~/.claude/skills/npm-search (main?) $ tree
.
├── scripts
│ └── npm-search.sh
└── SKILL.md
2 directories, 2 files
שיפור לעומת MCP? בהחלט:
1. קל יותר לכתיבה - כל הטקסטים יושבים בקובץ markdown מסודר במקום בתיאור של tool-ים. סקריפטים יכולים להיכתב בכל שפה ואפילו אפשר לשלב מספר שפות.
2. חוסך מקום בחלון הקונטקסט - הטקסטים נטענים לפי דרישה ובצורה דינמית, וכמובן אתם יכולים לטעון אותם גם בצורה יזומה באמצעות פקודת
/ ושם הסקיל במקרה שלנו /npm-search.אפשר למצוא עוד דוגמאות לסקילים בריפו של אנטרופיק כאן:
https://github.com/anthropics/skills
או במאגר הסקילים של קודקס:
https://github.com/openai/skills/tree/main/skills/.curated
אגב מבחינת ראנרים אחרים אז גם קופיילוט וגם opencode יקראו סקילז מאותה תיקיית
~/.claude/skills. קודקס יחפש אותם ב ~/.codex/skills.GitHub
GitHub - anthropics/skills: Public repository for Agent Skills
Public repository for Agent Skills. Contribute to anthropics/skills development by creating an account on GitHub.
📌 לפני חודש זה עבד
כמה תקלות שתיקנתי ללקוחות בתקופה האחרונה, כולן בקטגוריית "זה עבד לפני חודש ומאז לא נגענו"-
1. שינוי תשתית אצל ספק Third Party גרם לקוד הרגיל שלנו להפסיק לעבוד ודרש עדכון תוכנה מהצד שלנו כדי להתאים למנגנון החדש.
2. דיסק גיבוי שנגמר בו המקום גרם להשתלשלות אירועים מצערת שבסופה מערכת הפסיקה להגיב.
3. פג תוקף תעודה בחתימה עצמית שהופקה במקור לשנתיים. כן גם שנתיים עוברות בסוף, מה שהפיל את כל הקלאסטר.
4. ג'וב סינכרון בין בסיסי נתונים ש"אני בטוח שהיה שם" פשוט נעלם. הסינכרון הפסיק וממילא כולם כבר שכחו מהרפליקה עד שמישהו היה צריך דוח אנאליטיקס שיצא עם נתונים לא נכונים.
עכשיו יש סיפור כאילו סוכנים חכמים תכף הולכים לתפוס את כל מקומות העבודה של מהנדסי תוכנה ואנשי פרודקט שלא מבינים רק יכתבו פרומפטים כל היום. בכל אחד מארבעת האירועים ברשימה ה AI התחיל להיות מועיל רק אחרי שהבנתי מה מקור התקלה. בכל אחד מהאירועים אף מודל מהמודלים המובילים היום לא היה מסוגל לייצר תוכנית פעולה כדי להוריד את הסיכון להישנות תקלה כזו בפעם הבאה או לשפר את זמני התגובה שלנו כשזה קורה.
צ'אט ג'יפיטי הגדיל לעשות באחת השיחות מלאות התסכול שהיו לי איתו והציע לי להתייעץ עם איש DevOps. לך תסביר לו שאתה ה DevOps. אם יש משהו שהפער בין מהנדסי תוכנה לסוכני קידוד מלמד אותנו הוא שמהנדסי תוכנה לא הולכים לשום מקום.
נ.ב. דברים שנשברים פתאום הם תולדה של בעיה במערכת. הם לא נשברו פתאום זה תמיד היה שבור אבל עכשיו רק שמנו לב לזה כי היה פיצוץ. "איך הקוד הזה יישבר" היא עדיין השאלה הכי חשובה למפתחים על כל קוד שאנחנו כותבים.
כמה תקלות שתיקנתי ללקוחות בתקופה האחרונה, כולן בקטגוריית "זה עבד לפני חודש ומאז לא נגענו"-
1. שינוי תשתית אצל ספק Third Party גרם לקוד הרגיל שלנו להפסיק לעבוד ודרש עדכון תוכנה מהצד שלנו כדי להתאים למנגנון החדש.
2. דיסק גיבוי שנגמר בו המקום גרם להשתלשלות אירועים מצערת שבסופה מערכת הפסיקה להגיב.
3. פג תוקף תעודה בחתימה עצמית שהופקה במקור לשנתיים. כן גם שנתיים עוברות בסוף, מה שהפיל את כל הקלאסטר.
4. ג'וב סינכרון בין בסיסי נתונים ש"אני בטוח שהיה שם" פשוט נעלם. הסינכרון הפסיק וממילא כולם כבר שכחו מהרפליקה עד שמישהו היה צריך דוח אנאליטיקס שיצא עם נתונים לא נכונים.
עכשיו יש סיפור כאילו סוכנים חכמים תכף הולכים לתפוס את כל מקומות העבודה של מהנדסי תוכנה ואנשי פרודקט שלא מבינים רק יכתבו פרומפטים כל היום. בכל אחד מארבעת האירועים ברשימה ה AI התחיל להיות מועיל רק אחרי שהבנתי מה מקור התקלה. בכל אחד מהאירועים אף מודל מהמודלים המובילים היום לא היה מסוגל לייצר תוכנית פעולה כדי להוריד את הסיכון להישנות תקלה כזו בפעם הבאה או לשפר את זמני התגובה שלנו כשזה קורה.
צ'אט ג'יפיטי הגדיל לעשות באחת השיחות מלאות התסכול שהיו לי איתו והציע לי להתייעץ עם איש DevOps. לך תסביר לו שאתה ה DevOps. אם יש משהו שהפער בין מהנדסי תוכנה לסוכני קידוד מלמד אותנו הוא שמהנדסי תוכנה לא הולכים לשום מקום.
נ.ב. דברים שנשברים פתאום הם תולדה של בעיה במערכת. הם לא נשברו פתאום זה תמיד היה שבור אבל עכשיו רק שמנו לב לזה כי היה פיצוץ. "איך הקוד הזה יישבר" היא עדיין השאלה הכי חשובה למפתחים על כל קוד שאנחנו כותבים.
👍6❤2