לדלג לתוכן

3.3 דיבוג שיטתי הרצאה

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


למה הagents נוטים ל"תיקון בטוח ושגוי" - confident wrong fixes

מודל שפה מאומן לענות בביטחון. כשהוא רואה stack trace, יש לו נטייה חזקה להכיר תבנית מוכרת ("אה, זו שגיאת null reference קלאסית") ולהציע תיקון שמתאים לתבנית - גם אם התבנית הזו לא בהכרח מה שקורה בקוד שלכם. הבעיה מחריפה כי התיקון בדרך כלל אכן פותר את התסמין המיידי: אם השגיאה היא "Cannot read property 'id' of undefined", הוספת בדיקת if (obj) לפני הגישה תעצור את הקריסה. אבל אם obj לא אמור להיות undefined במקום הזה מלכתחילה, הוספת הבדיקה רק מסתירה את הבאג האמיתי - איפשהו קודם, מישהו לא אתחל את obj, וזה ימשיך לקרות, רק בלי קריסה שמתריעה עליו.

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

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


שלב 1: הזנת הcontext המלא - feeding the full context

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

> I got the following error when I tried to cancel a subscription via the
  API. Here's the full stack trace, the request that was sent, and the
  response that came back:

  TypeError: Cannot read properties of null (reading 'planId')
      at cancelSubscription (src/services/billingService.ts:88)
      at async SubscriptionController.cancel (src/api/controllers/subscriptionController.ts:41)

  Request: DELETE /subscriptions/8231
  Body: {}
  Response: 500 Internal Server Error

ככל שנותנים יותר ראיות גולמיות - stack trace מלא (לא קטוע), קלט שהופעל, פלט שהתקבל, גרסת קוד רלוונטית - כך פחות מקום לagent "לנחש" את החסר. אם יש לוגים נוספים, סביבת ריצה ספציפית, או תנאים שרק בהם זה קורה (למשל "רק בפרודקשן, לא מקומית"), כל פרט כזה מצמצם את מרחב ההשערות.

טבלת מה כדאי לצרף לפני שמתחילים דיבוג:

מקור מידע למה חשוב
stack trace מלא מצביע בדיוק על שורת הקריסה ועל שרשרת הקריאות שהובילה אליה
הקלט המדויק שהפעיל את הבאג מאפשר שחזור, לא רק ניחוש
הפלט שהתקבל מול הפלט שציפיתם מגדיר את התסמין באופן חד-משמעי
מתי זה קורה (תמיד / לפעמים / בתנאי מסוים) רומז אם מדובר בבאג דטרמיניסטי או race condition
האם זה חדש (רגרסיה) או תמיד היה ככה אם זו רגרסיה, git log/git blame על הקובץ הרלוונטי הוא כלי ראשון

שלב 2: שחזור לפני תיקון - reproduce before fixing

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

> Before you suggest a fix, reproduce the bug: write a small script
  or test that calls cancelSubscription with the same conditions,
  and show me that it fails with the same error.

למה זה קריטי? כי שחזור מוכיח שהבנתם את התנאים הנכונים, לפני שמשקיעים בתיקון. אם הagent לא מצליח לשחזר, זה איתות חשוב - אולי חסר לו context (נתוני DB ספציפיים, state מסוים), ואולי ההשערה הראשונית על מקור הבאג פשוט שגויה.

$ npm test -- --grep "cancelSubscription with null plan"

 FAIL  src/services/__tests__/billingService.repro.test.ts
  FAIL: fails with TypeError when the subscription has no plan (12 ms)

    TypeError: Cannot read properties of null (reading 'planId')
      at cancelSubscription (src/services/billingService.ts:88)

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


שלב 3: בניית והפרכת השערות - hypothesis testing

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

> Why is subscription.plan null at this point? Check the code
  that creates and updates subscriptions, and explain to me what your
  hypothesis is - before you touch the code.

תשובה טובה נראית כך - השערה מפורשת, לא תיקון:

Hypothesis: in src/services/subscriptionService.ts:56, when a
subscription is created with a trial plan, plan_id is stored as null
until the trial ends and a real plan is assigned. In cancelSubscription
there's no check whether plan is null before accessing planId - this
was probably not an issue until now because in tests plan was always set.

How to test the hypothesis: I'll check whether the subscription that
caused the error in production (id 8231) is indeed in trial state. If
so, the hypothesis is strengthened.

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

> Check: run a query (or write a script) that checks whether subscription
  8231 is really in trial state and has no assigned plan.

אם ההשערה מתאמתת - מצוין, ממשיכים לתיקון עם ביטחון אמיתי. אם היא לא מתאמתת (למשל מתברר ש-8231 כן היה לו plan, אבל הוא נמחק בטעות בפעולה אחרת), חוזרים אחורה ובונים השערה חדשה. זה בדיוק ההבדל בין דיבוג שיטתי לניחוש: כל השערה נבדקת לפני שפועלים לפיה.


שלב 4: דרישת שורש הבעיה, לא רק פאץ' - root cause, not a patch

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

> There are two options here: (1) add a null check before accessing
  planId in cancelSubscription, or (2) ensure that a subscription in
  trial state can never reach a state where someone tries to cancel it
  without a valid plan in the first place. Give me an analysis - what
  the root cause is, and the risks of each approach.

תשובה טובה תבחין בין השניים:

Option 1 (null check) - treats the symptom. Prevents the crash, but
subscription.plan will still be invalidly null - any future code
that touches plan could fail the same way somewhere else.

Option 2 (root-cause fix) - the real problem is that createSubscription
allows creating a trial subscription without a temporary plan_id, and
there's no process that ensures the plan is filled in before the
subscription becomes active. The correct fix: enforce, at the model
layer / DB constraint level, that plan_id cannot be null for a
subscription that's no longer in trial, and handle canceling a trial
subscription as a separate, explicit case (not through the same
cancellation function as a regular subscription).

Recommendation: implement both - a DB-level constraint as a safety
net, and explicit handling of trial cancellation as the root-cause fix.

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


כשקשה לשחזר - intermittent and flaky bugs

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

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

> This bug happens roughly once every 20 requests, not every time.
  Check whether there's a correlation: does it happen more under high
  load? When there are two close requests from the same user? Look at
  last week's logs and search for a pattern.

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

> Write a test that fires two identical requests almost simultaneously
  (an intentional race condition) and see if it reproduces the bug.
$ npm test -- --grep "concurrent cancel requests"

 FAIL  src/services/__tests__/billingService.race.test.ts
  FAIL: two concurrent cancel requests cause a double charge (89 ms)

כשגם זה לא עובד - הבאג עדיין לא משוחזר בסביבת פיתוח - יש שתי דרכי המשך לגיטימיות: להוסיף תיעוד (logging) ממוקד סביב האזור החשוד ולחכות להתרחשות הבאה בפרודקשן כדי לתפוס נתונים, או לעבור לניתוח סטטי של הקוד - לקרוא את הלוגיקה בעיון ולחפש נקודות שבהן שני תהליכים יכולים לגעת באותו state בו-זמנית, גם בלי הוכחה חיה:

> I can't reproduce this locally. Go over the code of
  cancelSubscription and look for points where two concurrent calls
  could read and write the same record without proper locking - even
  without an actual reproduction, just from logical analysis.

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


דוגמה מלאה: מהשגיאה לתיקון

1. > [full context] I got a TypeError... [stack trace + request]

2. > Before a fix, reproduce: write a test that fails with the same error.
   < [test fails successfully - reproduction confirmed]

3. > Why is subscription.plan null here? Give a hypothesis before fixing.
   < [hypothesis: trial subscriptions don't fill in plan_id]

4. > Check the hypothesis against subscription 8231 in reality.
   < [confirmed: 8231 is indeed in trial state without a plan]

5. > Give me an analysis: point fix vs. root-cause fix, and a recommendation.
   < [analysis + recommendation on both layers]

6. > Implement it: DB constraint + separate handling for trial cancellation.
   Make sure the test from step 2 now passes, and that all other
   billingService tests still pass.
   < [fix implemented, reproduction test passes, no regression]

שימו לב שהבדיקה שנכתבה בשלב 2 לא נזרקת אחרי התיקון - היא נשארת בחבילת הבדיקות כ-regression test, ומבטיחה שהבאג הזה לא יחזור בשקט בעתיד.


מלכודות נפוצות

  • קבלת התיקון הראשון בלי שחזור: אם הagent מציע תיקון לפני שמישהו שחזר את הבאג, יש סיכוי גבוה שהתיקון פותר תבנית מוכרת ולא את הבעיה האמיתית שלכם. תמיד לדרוש שחזור קודם.
  • סיפוק מ"זה כבר לא קורס": קריסה שנעצרה היא לא הוכחה שהבאג תוקן - יכול להיות שהתסמין רק הוסתר. שאלו "מה קורה עכשיו במקום הקריסה - האם ההתנהגות נכונה?"
  • דילוג על אימות השערה: הagent מסביר בביטחון "הבעיה היא X" בלי שאף אחד בדק את X מול המציאות. תמיד לבקש דרך קונקרטית לאמת את ההשערה לפני שממשיכים.
  • תיקון שמכסה יותר מדי: לפעמים הagent "מתקן" על ידי עטיפת קוד רחב ב-try/catch, שמשתיק לא רק את השגיאה הספציפית אלא כל שגיאה עתידית באזור הזה. לבדוק את היקף התיקון בדיוק.
  • שכחת regression test: אחרי שהבאג תוקן, לוודא שהבדיקה ששחזרה אותו נשארת בקוד, לא נמחקת "כי היא כבר לא נכשלת".

סיכום

  • הagents AI נוטים להציע "תיקון בטוח ושגוי" - פתרון שמתאים לתבנית מוכרת ומשתיק את התסמין, בלי לפתור את שורש הבעיה.
  • מזינים לagent ראיות גולמיות ומלאות - stack trace שלם, קלט מדויק, פלט בפועל מול פלט צפוי - ולא תיאור מילולי כללי של השגיאה.
  • לפני כל תיקון, דורשים שחזור: בדיקה או סקריפט שנכשל באותה שגיאה. בלי שחזור, אין בסיס לדיבוג אמיתי.
  • עוברים לחקירה דרך השערות מפורשות שנבדקות מול המציאות לפני שפועלים לפיהן - לא מקבלים הסבר "הגיוני" בלי אימות.
  • בכל תיקון בודקים אם מדובר בפאץ' לתסמין או בטיפול בשורש הבעיה, ומבקשים מהagent ניתוח מפורש של שתי האפשרויות והסיכונים שלהן.
  • הבדיקה ששחזרה את הבאג נשארת בחבילת הבדיקות כ-regression test, ומוודאים שהתיקון לא שבר התנהגות אחרת.
  • הכלל המנחה: לא לתת לביטחון של הagent להחליף אימות. שחזור, השערה, בדיקה - בסדר הזה, בכל פעם.