close menu

איך מתכננים מוצר מבוסס מודל שפה?

הנדסה לאחור של GITHUB COPILOT

בואו נפרק את Github CoPilot ונראה איך הוא עובד!
מוצרים המבוססים על מודלי שפה יכולים להתפס כ"קסם", לעתים קרובות הם יכולים "לקרוא את המחשבות" שלנו בצורה קסומה ו"להבין" מה אנחנו רוצים תוך כדי שהם לוכדים אלמנטים אנושיים, מקצועיים ותרבותיים בתוך התוצרים שהם מחזירים אלינו. במקרה של Github CoPilot הצעות השלמת הקוד המדויקות שלו גרמו לכך שהמוצר הופץ בזריזות בכל העולם ומשפטים כמו "לא ניתן לדמיין כיצד אפשר לחזור אחורה ולתכנת "ידני" שוב" נאמרים עליו כל הזמן.
אני גם משתמש בCoPilot ושמתי לב שלפעמים הצעות ההשלמה האוטומטית שלו כוללות מידע שלא ציפיתי שתהיה לו הגישה אליו:
  • שמות וקוד מתוך קבצים אחרים שאיני עורך כרגע
  • שמות ונתיבי תיקיות וקבצי דאטה שלא ציינתי בקוד שבכלל קיימים
  • לפעמים מתווסף להשלמותיו מידע אפילו מפרוייקטים פרטיים אחרים שלי (שאני משתמש בהם דרך import).

כל אלו גרמו לי לחשוב:
  • מה הפרומפט שהם משתמשים בו במודל כדי לייצר את ההשלמות?
  • מה בדיוק נשלח החוצה מהמחשב שלי תוך כדי שאני עובד?
בימים האחרונים מסתובב באינטרנט פוסט מעניין על הנדסה לאחור של Github Copilot.
הלכתי לקרוא..
לפני שנתחיל..
שתי מילים על הנדסה לאחור של מוצרי תוכנה:
בפוסט מתוארות הפונקציות השונות של התוסף הפופולרי ומה חלוקת התפקידים בניהן, פונקציות אלו אינן מתועדות במוצר הסופי ועברו כמה וכמה תהליכי עירבול ו"בלאגן" בין כתיבתן ועד שליחתן אל הלקוחות. על מנת לחלץ את התמונה המלאה של מבנה התוכנה, יש לעבור ידנית ולפרש את הקוד. בדרך כלל הנדסה לאחור מתחילה מ"הסוף": פלט התוכנה, במקרה שלנו: ההשלמה האוטומטית.
מכאן, התהליך ממשיך שלב אחרי שלב תוך כדי פירוק ידני של הקוד כדי להבין כיצד הוא פועל. מכיוון שהקוד עצמו אינו מתועד, שמות הפונקציות והמשתנים גם הם נמחקו והוחלפו במחרוזות רנדומליות, קל לאבד את הדרך במהלך העבודה ולכן אחד הכלים הנפוצים העוזרים ביותר בתהליך הוא יומן המחקר.
יומן זה מאפשר לחוקר לתעד את ממצאיו ולעקוב אחר התקדמותו תוך כדי שהוא חופר לעומק הקוד, הפוסט לגבי Github CoPilot הוא תוצאת עריכה של יומן מחקר זה הנכתב לאורך תהליך הנדסה לאחור של התוסף שארך כשלושה חודשים.
למה זה מעניין?
מכיוון שאנחנו רק בימים הראשונים של הטכנולוגיה, מעניין לראות כיצד המוצרים הראשונים העושים בה שימוש בנוים ומה אפשר ללמוד מהם ברמת הארכיטקטורה וניהול המוצר.
בין אם אתם מתעסקים במוצרים המובססים על מערכות לומדות (אפילו עוד יותר: מודלי שפה) ובין אם אתם סתם מתעניינים איך עובדות המערכות של OpenAI מאחורי הקלעים, פוסט זה בשבילכם!

הנדסה לאחור של Github CoPilot

התוסף בנוי משני רכיבים עיקריים:
  • הClient: נמצא אצלכם במחשב, עוקב אחרי העבודה שלכם ואוסף את כל מה שאתם מקלידים וברקע בונה Proptים לצורך שליחה אל המודל והצגת השלמות הקוד.
  • השרת: שרת הנמצא אצל OpenAI מאזין לבקשות השלמה ממכם, ככל הנראה מריץ מודל ממשפחת Codex ועל סמך Prompt ממכם: משלים אתכם ומחזיר אליכם את תוצר המודל.
מה הפרומפט?
כמו שכתבתי למעלה, המודל Codex אמנם אומן על הרבה מאד קוד ולכן יודע להשלים קוד באיכות גבוהה מאד אבל עם כל הכבוד, הוא אינו יכול לדעת מראש מה שמות הפונקציות בהן השתמשתי בקבצים אחרים בפרוייקט האישי שלי. ובכל זאת הוא יודע להציע לי להשתמש בהן (וגם אילו ארגומנטים לשלוח אליהן!)
מה נשלח החוצה?
התוסף אוסף מידע רב על סביבת העבודה שלכם ומקודד את כולו לתוך בקשת json אותה שולח החוצה אל שרת המודל.
בקשת Json לדוגמה:

{

"prefix": "# Path: codeviz.. ",

"suffix": "if __name__ == '__main__':..",

"isFimEnabled": true,

"promptElementRanges": [

{ "kind": "PathMarker", "start": 0, "end": 23 },

{ "kind": "SimilarFile", "start": 23, "end": 2219 },

{ "kind": "BeforeCursor", "start": 2219, "end": 3142 }

]

}

כמו שאתם רואים, בנוסף לקוד עצמו הנמצא לפני הסמן, נשלח גם קוד הנמצא אחרי הסמן (כלומר Codex עובד במוד Fill in the middle עליו דיברנו בפוסט אחר לפני כמה ימים).
בנוסף: ישנו מידע מעניין על קבצים דומים ונראה כי נאספים חלונות כלשהם מקבצים אחרים בפרוייקט וגם מאותו הקובץ.
מידע מקבצים אחרים משורשר לתחילת הפרומפט:
בתחילת הפרומפט [הדוגמה הזו מתוך הפוסט באינטרנט] משורשר קוד מקובץ אחר, הקוד נמצא כולו בתיעוד (תחילת כל שורה: #) ובתחילת הקוד ישנו הפרומפט "# Compare this snippet from d".
מעניין.
איך הם יודעים מה להוסיף?
כדי לאסוף את כל המידע הרלוונטי לצורך יצירת הפורמפט הם מריצים את הלוגיקה הבאה:
  1. חילוץ הקוד מהקובץ עליו אתם עובדים: חילוץ מהיר מתרחש עבור המסמך הנתון ומיקום הסמן בו.
  2. קבצים אחרים: לאחר מכן, נשלפים 20 הקבצים האחרונים אליהם ניגשתם המכילים קוד באותה שפת התכנות בה אתם עורכים כרגע. קבצים אלו נשמרים בצד לשלב מאוחר יותר לצורך חילוץ קטעי קוד דומים דומים שייכללו בפרומפט.
  3. בניית הSuffix: ואז על פי קונפיגורציה מקודדת מראש תוך התוסף נבחרת כמות התוים ההולכת לשמש לSuffix. נראה כי ישנה מערכת AB testing בנויה בתוך התוסף והם מבצעים ניסוים במספר זה כמו כן ב"האם להביא פונקציות אחיות" לתוך הפרומפט. כרגע: מספר התוים לsuffix הוא 15% ולא מייבאים פונקציות אחיות.
  4. בניית הPrefix:
לצורך בנית הPrefix הם הולכים לפי "רשימת מכולת" על פי סדר חשיבות ומייבאים לתוך הפרומפט כמה שיותר עד שנגמר המקום בפרומפט.
ישנם 6 סוגים שונים של "אלמנטים" שניתן לייבא לתוך הקוד:
– מיקום הסמן במסמך (כנראה כדי לסמן למודל כמה הקובץ ממשיך לפני ואחרי הסמן במידה והקובץ ארוך מאד)
  • מזהה שפות תכנות
  • קבצים הדומים ביותר לקובץ הנוכחי
  • מה קורה לפני הסמן
  • מה קורה אחרי הסמן
מכיון שהפרומפט מוגבל, רשימה זו כמובן ממוינת על פי סדר חשיבות ואלמנטים אלו נוספים עד שנגמר המקום.
זה די מגניב כי הם ממש משתמשים בתוך הקוד בשמות כמו Wishlist או Prompt Budget.
לא חשבתי על זה ככה אף פעם: הפתרון האישי שלי לבעית מגבלת המקום בפרומפט היה עד עכשיו "sudo אין מגבלת מקום בפרומפט יותר f-" ואז מאמן מודל עם מימד כניסה בגודל 400,000. יולו.
ישנה עוד לוגיקה מעניינת באיזו זה בקוד השולטות בסדר הכנסת האלמנטים ובסדר עדיפויות האלמנטים. לוגיקה זו גם שולטת באופן חילוץ מידע מסוים, למשל כמה אגרסיבים אנחנו בחילוץ קטעים מקבצים אחרים. ישנה גם לוגיקה מיוחדת לטיפול בשפות ספציפיות כמו חילוץ קונטקסט מיוחד לTypeScript וכו'.
ישנו גם הרבה קוד מת באיזור זה, יכול להיות שקוד זה הוא חלק ממנגנון הAB testing האגרסיבי שהם עושים (ויש אזכורים אליו לאורך כל הקוד בכל מקום), דוגמאות פשוטות הן למשל הלוגיקה ליבוא קוד מקבצים שכנים מעל מיקום הסמן בהם או הלוגיקה המייבאת פונקציות אחיות מקצבים אחרים. שני קטעי קוד אלה מעולם לא נקראים כרגע.
6. בניית הסיומת: הלוגיקה פשוטה: כל עוד יש מקום – קח עוד תוים.
חשוב לאמר בעניין הסיומת שיש די הרבה לוגיקה הנשלטת דרך הExperimentationFramework וAB testing בקוד כגון הוספה של פונקציות אחיות לסיומת אבל אין שימוש בקוד זה כרגע.
איך מייבאים קוד רלוונטי מקבצים אחרים?
חילוץ קטעים מקבצים אחרים מעניין מאד, בהתאם להיפרפרמטרים הם רצים עם חלונות בגודל קבוע ומחשבים Jaccard על איזורים בקבצים האחרים, הם פורסים את כל הקבצים הרלוונטים לפרוסות בגודל קבוע ורצים עם חלון זז ומחשבים את דמיון עם Jaccard – בין כל חלון לקובץ שבו אנחנו מקלידים כרגע. רק החלון הטוב ביותר מוחזר מכל "קובץ רלוונטי" (קיימת אפשרות למספר חלונות מכל קובץ אבל שוב – לא בשימוש).
גודל החלון כרגע הוא 60 שורות – שוב זה היפרפרמטר נשלט על ידי AB Testing.
קריאה למודל
ישנם שני ממשקי משתמש שדרכם Copilot מספק השלמות:
השלמה בזמן כתיבת קוד – אתם כותבים קוד: השלמת קוד קסומה חוזרת אליכם.
השלמה בPanel בצד – כשאתם מבקשים במיוחד השלמה ונפתח חלון צדדי עם כמה וכמה השלמות.
כמו שאני מניח שרובכם חשדתם: ישנם הבדלים באופן שבו המודל מופעל בשני המקרים.
השלמה אוטומטית בזמן כתיבה:
התוסף מבקש מעט מאוד הצעות (1-3) מהמודל כדי שאתם תוכלי לקבל השלמות מהירות. התוסף גם שומר בצורה אגרסיבית מאד השלמות קודמות מהמודל. יתר על כן, הוא דואג לבקש עוד ועוד השלמות אם המשתמש ממשיך להקליד ודואג לבטל ולא להציג השלמות שחזרו "מאוחר מידי" כשהמשתמש כבר המשיך להקליד.

קטע קוד זה מעניין מאד ומדגים שימוש מצוין במודל שפה "איטי" בזמן אמת!

למרות שנראה כי הממשק בזבזני מאד ומבקש מהמודל המון בקשות, ישנו גם מנגנון ריסון שליחת הבקשות אל המודל המונע שליחת בקשות במקרים מסוימים. לדוגמה, אם המשתמש נמצא באמצע שורה: הבקשה נשלחת רק אם התווים מימין לסמן הם רווח לבן, סוגרים סוגרים וכו'.
אי שליחת בקשות "גרועות"
כולנו חשבנו על זה בשלב זה או אחר: מה עושים אם המודל עושה שטויות?
בתוסף זה ישנו מנגנון שמראש מייצר תחזית "האם נקבל עכשיו השלמה טובה?" לפני כל שליחת בקשה למודל הגדול. התחזית מתבצעת על ידי רגרסיה לוגיסטית פשוטו עם 11 פיצ'רים פשוטים כמו השפה, האם ההשלמה הקודמת התקבלה, כמות הזמן שעבר בין קבלה\דחיה, אורך השורה האחרונה בפרומפט, התו האחרון בסוף הסמן. המשקולות של הרגרסיה נמצאים hardcoded בתוך הקוד. [צחוקים]
לרגרסיה זו thresh של 15% שמתחתיו לא נשלחת הבקשה, עוד מעניין הוא שעל פי משקולות המודל, המודל מתעדף שפות תכנות בצורה הבאה:
  • php > js > python > rust > dart.

אין מה לעשות. כולם יודעים שphp זה העתיד.

עוד מעניין הוא שלתוים כמו ] ) יש סבירות הרבה יותר גבוהה לשלוח בקשות מאשר לתוים כמו [ ( כי ככל הנראה "השורה הסתיימה".
סינון השלמות גרועות מהמודל
במידה והמודל בכל מקרה "עושה שטויות" לפני הצגת ההשלמש למשתמש, Copilot מבצע שתי בדיקות:
  • אם הפלט חוזר על עצמו (למשל, foo = foo = foo = foo…), שזו בעיה מוכרת בכל מודלי השפה, אז ההשלמה נמחקת.
  • אם המשתמש כבר הקליד את ההצעה, היא נמחקת.
שמירת טלמטריה
אז Github טוענים בכל מקום ש-40% מהקוד הנכתב אצל מחזיקי התוסף נכתב על ידי Copilot (עבור שפות פופולריות כמו Python).
מעניין איך הם יודעים את המספר הממש ממש מדויק הזה.
הפתרון הכי פשוט הוא כמובן לספור כמה פמעים המשתמש מחליט לקבל את ההשלמה, אבל ספירה זו אינה מדד טוב לשיעור ההצלחה של Copilot. אם אנשים קיבלו את ההשלמה ואז מחקו/ערכו אותה, האם הם עדיין קיבלו אותה?
ולכן הם בודקים האם ההצעה שהתקבלה עדיין נמצאת בתוך הקוד. בדיקה זו נעשית באורכי זמן שונים לאחר ההשלמה – 15 שניות, 30 שניות, 2 דקות, 5 דקות ו-10 דקות בכל פעם כזו התוסף בודק האם הצעת ההשלמה עדיין קיימת בתוך הקוד.
מעבר כזה על כל הקוד וחיפוש ההשלמה הוא כבד חישובית ולכן במקום זאת הם מודדים דמיון (גם ברמת התו וגם ברמת המילה) בין ההשלמה לבין חלון סביב מיקום ההשלמה בקובץ ועם thresh מסוים מחליטים האם ההשלמה עדיין בקוד.
האם נשלח עוד קוד החוצה:
כן.
לאחר 30 שניות מקבלה או דחית ההשלמה התוסף שולח קוד סביב נקודת ההשלמה \ דחיה.
יכול להיות שנתונים אלו נשלחים החוצה לצורך אימון המודל (שכן זה הוא הקוד ה"נכון" שהיה צריך להכתב בנקודת זמן זו) ובנוסף הם שולחים החוצה את הrepo בgithub שבו הקוד מאוחסן [שוב, כנראה לצורך סגירת המעגל מול הקוד הנקי ואימון המודל].
חשוב לאמר שיש אפשרות בGithub לבטל את הסכמתכם לשימוש בקטעי הקוד ל"שיפור המוצר" ואם אתם מחליטים לעשות זאת, קוד זה לא נשלח החוצה.
מה המודל?
מתוך חפירה בקוד נראה שהמודל נקרא "cushman-ml", מה שמצביע מאוד על כך ש-Copilot משתמש במודל בגודל 12 מיליארד פרמטרים במקום במודל של 175B פרמטרים

כמו שכתבתי בפוסט בקבוצה לפני כשבוע

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