הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

באג רודף באג

21/05/2026

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

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

  def retry_delay
    delay = settings.dig('retry_delay')
    delay.present? ? delay.to_i : 1
  end

ואני רציתי לשנות אותה ל:

what about just

settings.dig('retry_delay')&.to_i || 1

קלוד שינה ואז כתב:

Done. Note: this has a slight behavioral difference — if retry_delay is "0", the original would return 0 (truthy string → to_i), while "0".to_i is 0 which is falsy in Ruby, so || 1 would return 1 instead. If 0 is a valid value, you'd want || 1 replaced with something like .then { |v| v || 1 }. Probably not an issue in practice, but worth knowing.

מה בעצם הוא אומר פה?

  1. שיניתי לך את הקוד כמו שביקשת.

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

  3. זה כנראה לא בעיה כי בטח הפונקציה שלך אף פעם לא מחזירה 0 ממילא אז אפשר להתעלם.

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

3.4.4 :001 > 0 || 1
 => 0
3.4.4 :002 > nil || 1
 => 1
3.4.4 :003 > !!0
 => true
3.4.4 :004 > puts "ruby" if 0
ruby
 => nil
3.4.4 :005 >

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

אינטראופ

20/05/2026

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

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

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

אבל אינטראופרביליות מורגשת הכי חזק כשהיא לא נמצאת.

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

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

כל עוד VS Code Copilot תמך בשיחות עם מודלים רק דרך המנוי של קופיילוט היה אפשר להחליף מודל באמצע השיחה והכל עבד. כשהם התחילו להוסיף ספקים כמו Ollama פתאום גילינו שאי אפשר להתחיל שיחה עם קלוד ואחרי כמה הודעות לעבור לדיפסיק. כל ספק מודלים מצפה לרשימת הודעות במבנה אחר ולא מצליח להתמודד עם רשימה שיצר ספק אחר.

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

פייברים, חיבורים ל DB וסוכנים חכמים.

19/05/2026

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

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

  1. אתר שולח מייל לנרשמים חדשים.

  2. אתר לוח מודעות מטפל במשתמש שמעלה מודעה חדשה עם תמונה. התמונה עוברת עיבוד ושינוי גודל ברקע והמודעה עולה לאתר.

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

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

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

בעולם הישן מערכות השתמשו ב Thread Pool כדי לנהל את כל התהליכים במקביל. היה לך מאגר של 5-8 תהליכונים שרצים ברקע (20 למיטיבי לכת). כל תהליך תופס משימה, מבצע אותה, משחרר וממשיך למשימה הבאה. צריכים לשלוח מייל למישהו שנרשם? מצוין תוסיף את המשימה הזאת לתור המשימות, כשיתפנה אחד הפועלים הוא ייקח את המשימה מהתור, ישלח את המייל וימשיך למשימה הבאה. קל.

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

מה עושים? זורקים את מודל התהליכונים ועוברים למודל לא כל כך חדש של מענה אסינכרוני, או למודל קצת יותר חדש של Fibers (ברובי) או Virtual Threads (ב Java). בשני המקרים אנחנו עולים בקיבולת ומטפלים בעזרת מספר קטן של תהליכונים בעשרות אלפי בקשות במקביל. התהליכון "מקפץ" בין התשובות כמו נציג שירות שמדבר אתכם בווטסאפ, וכל פעם שיש מידע חדש מאיזשהו מודל שפה מערכת ההפעלה מעבירה את המידע לאותו תהליכון שיודע לנתב את המידע חזרה לגולש.

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

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

ActiveRecord::Base.connection_pool.release_connection
chat.complete

כמה שכבות של בעיות

18/05/2026

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

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

https://github.com/ynonp/langlets-rails/blob/c846dfe220fa9e25c1f54b3b3b22aaff1a463592/app/jobs/resynctimestampsjob.rb

class ResyncTimestampsJob < ApplicationJob
  queue_as :default

  def perform(course_id, json_data)
    course = Course.find(course_id)
    medium = course.medium

    Rails.logger.info "Starting ResyncTimestampsJob for course #{course.id} (#{course.slug})"

    phrases = medium.phrases.ordered_by_timestamp.to_a

    if phrases.length != json_data.length
      raise "Phrase count mismatch: medium has #{phrases.length} phrases, JSON has #{json_data.length} entries"
    end

    # First pass: build a hash of phrase.id => new timestamp
    updates = {}
    json_data.each_with_index do |entry, index|
      phrase = phrases[index]
      new_timestamp = entry["timestamp"]

      if new_timestamp.blank?
        raise "Missing timestamp at JSON index #{index}"
      end

      updates[phrase.id] = new_timestamp
      Rails.logger.info "Mapping phrase #{phrase.id} ('#{phrase.text_l1}') => #{new_timestamp}"
    end

    # Second pass: apply all updates
    updates.each do |phrase_id, new_timestamp|
      phrase = Phrase.find(phrase_id)
      old_timestamp = phrase.timestamp
      phrase.update!(timestamp: new_timestamp)
      Rails.logger.info "Updated phrase #{phrase_id} timestamp: #{old_timestamp} => #{new_timestamp}"
    end

    # Update lesson timestamps based on first and last phrase in each lesson
    course.lessons.includes(:activities).find_each do |lesson|
      lesson_phrase_ids = lesson.activities.joins(:phrases).pluck("phrases.id").uniq
      lesson_phrases = phrases.select { |p| lesson_phrase_ids.include?(p.id) }.sort_by(&:timestamp)

      if lesson_phrases.any?
        first_timestamp = lesson_phrases.first.timestamp
        last_timestamp = lesson_phrases.last.timestamp

        lesson.update!(start_timestamp: first_timestamp, end_timestamp: last_timestamp)
        Rails.logger.info "Updated lesson #{lesson.id} timestamps: #{first_timestamp} => #{last_timestamp}"
      end
    end

    Rails.logger.info "ResyncTimestampsJob completed for course #{course.id}. Updated #{updates.count} phrases."

  rescue => e
    Rails.logger.error "ResyncTimestampsJob failed for course #{course_id}: #{e.message}"
    Rails.logger.error e.backtrace.join("\n")
    raise e
  end
end

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

  1. כל הטקסט של הוידאו נטען לזכרון בשורה 10 (הופכת את כל השורות למערך).

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

  3. אחרי זה מעדכנים את כל הטקסטים לזמן החדש שלהם.

  4. בסיום רצים על השיעורים בקורס ומעדכנים את זמני ההתחלה והסיום שלהם כדי להתאים לזמנים החדשים מהקובץ.

למה AI הסתבך עם זה? אני לא יודע, אבל אני חושד שהקוד כולל כמה תבניות לא נכונות והיה קשה לפתור את הסבך. הבעיות המרכזיות בקוד:

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

  2. הוא עושה עבודה מיותרת - טוען לזכרון את כל הטקסטים ואז בונה את המילון, כל זה מבלבל את ה AI.

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

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

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

https://github.com/ynonp/langlets-rails/blob/main/app/jobs/resynctimestampsjob.rb

class ResyncTimestampsJob < ApplicationJob
  queue_as :default

  def perform(course_id, json_data)
    course = Course.find(course_id)
    medium = course.medium

    Rails.logger.info "Starting ResyncTimestampsJob for course #{course.id} (#{course.slug})"

    phrases = medium.phrases.ordered_by_timestamp.to_a
    verify_json_data!(json_data, phrases)

    # Update all phrase timestamps in a single pass
    json_data.zip(phrases).each do |entry, phrase|
      phrase.update!(timestamp: entry["timestamp"])
    end

    # Update lesson timestamps based on first and last phrase in each lesson
    course.sync_lesson_timestamps

    Rails.logger.info "ResyncTimestampsJob completed for course #{course.id}. Updated #{phrases.length} phrases."

  rescue => e
    Rails.logger.error "ResyncTimestampsJob failed for course #{course_id}: #{e.message}"
    Rails.logger.error e.backtrace.join("\n")
    raise e
  end

  private

  # Expected json_data structure:
  #   [{ "timestamp" => "00:01:23.456", ... }, ...]
  # One entry per phrase, ordered to match medium.phrases.ordered_by_timestamp.
  def verify_json_data!(json_data, phrases)
    if phrases.length != json_data.length
      raise "Phrase count mismatch: medium has #{phrases.length} phrases, JSON has #{json_data.length} entries"
    end

    missing = json_data.each_with_index.select { |entry, _| entry["timestamp"].blank? }
    if missing.any?
      raise "Missing timestamps at JSON indices: #{missing.map(&:last).join(', ')}"
    end
  end
end

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

  def sync_lesson_timestamps
    ordered_lessons = lessons.includes(activities: { activity_phrases: :phrase }).sort_by(&:order)

    # Precompute sorted unique phrases per lesson to avoid N+1 inside the loop
    lesson_phrases_map = ordered_lessons.to_h do |lesson|
      phrases = lesson.activities.flat_map { |a| a.activity_phrases.map(&:phrase) }.uniq.sort_by(&:timestamp)
      [lesson, phrases]
    end

    ordered_lessons.each_with_index do |lesson, index|
      lesson_phrases = lesson_phrases_map[lesson]
      next if lesson_phrases.empty?

      start_ts = lesson_phrases.first.timestamp

      end_ts = if (next_lesson = ordered_lessons[index + 1])
        next_phrases = lesson_phrases_map[next_lesson]
        next_phrases.any? ? next_phrases.first.timestamp : lesson_phrases.last.timestamp
      else
        Phrase.to_string_timestamp(lesson_phrases.last.timestamp_seconds + 5)
      end

      lesson.update!(start_timestamp: start_ts, end_timestamp: end_ts)
    end
  end

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

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

אתה כנראה אוהב לחשוב יותר מאשר לכתוב

17/05/2026

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

"אבל אני אוהב לכתוב קוד", או במילים שלו:

I definitely didn't want to use AI to write code. I loved programming! That's the fun part! I'd already switched back from being a team tech lead to just being an IC, because I had been reduced to only 50% coding time from being stuck in meetings, and that was awful

קשה שלא להזדהות, אבל זה פחד שלא עומד במבחן המציאות.

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

  1. כבר לא צריך לזכור איך לכתוב לולאת for; כן צריך לזכור מה זה איטרציה טורית, מקבילית ואסינכרונית ואיזה סוג סריקה מתאים לבעיה שלנו.

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

  3. כבר לא צריך לזכור איך לטפל בשגיאות; כן צריך לבחור באיזה שגיאות לטפל, מתי לבנות מנגנון fallback ומתי לתת לזה להתרסק.

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

הקלדה וטעויות תחביר Out; חשיבה וקבלת החלטות In. אני בעד ונראה לי שגם אתם.

מ-מבין ל-בוחר

16/05/2026

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

נתחיל ברעיון הפשוט - הפרומפט הבא פתר לי את יום 10 של Advent Of Code בלי שכתבתי שורת קוד אחת. כאן יש את תיאור החידה:

https://adventofcode.com/2025/day/10

זה היה הפרומפט:

Solve
https://adventofcode.com/2025/day/10

for the demo input


  [.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
  [...#.] (0,2,3,4) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,7,2}
  [.###.#] (0,1,2,3,4) (0,3,4) (0,1,2,4,5) (1,2) {10,11,11,5,10,5}


  should yield 7

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

import itertools
import re

def parse_machine(line):
    """Parse a line into (target_pattern, list_of_button_sets)"""
    # Extract bracketed target pattern
    target_match = re.search(r'\[([^\]]+)\]', line)
    target = target_match.group(1)

    # Extract parenthesized button schematics
    buttons = []
    for m in re.finditer(r'\(([^)]+)\)', line):
        btn = tuple(int(x.strip()) for x in m.group(1).split(','))
        buttons.append(btn)

    return target, buttons


def solve_machine(target, buttons):
    n = len(target)
    goal = [1 if c == '#' else 0 for c in target]

    best = float('inf')

    # Try all subsets of buttons
    for r in range(len(buttons) + 1):
        for combo in itertools.combinations(range(len(buttons)), r):
            state = [0] * n
            for idx in combo:
                for pos in buttons[idx]:
                    if 0 <= pos < n:
                        state[pos] ^= 1
            if state == goal:
                best = min(best, r)
                break  # for this r, all combos have same size r, so first match is optimal
        if best != float('inf'):
            break

    return best if best != float('inf') else None


def main():
    total = 0
    unsolved = 0

    with open('input.txt') as f:
        for line_num, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            target, buttons = parse_machine(line)
            best = solve_machine(target, buttons)
            if best is None:
                print(f"Line {line_num}: NO SOLUTION")
                unsolved += 1
            else:
                total += best

    print(f"\nTotal minimum presses: {total}")
    if unsolved:
        print(f"Unsolved machines: {unsolved}")


if __name__ == '__main__':
    main()

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

אבל זה לא כל הסיפור. כשאני פותח את קוד הפייתון שהמודל כתב אני רואה דרך פתרון אחת - פתרון Brute Force שרץ 2 בחזקת n פעמים. פה זה התחיל לעניין. האם אפשר לפתור את זה בפחות עבודה? מה המנגנון? איך כדאי לשמור את המידע?

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

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

LightDiagram = Data.define(:state, :buttons) do
  # Convert diagram to a system of equations over GF(2) in z3 script syntax.
  # Each bit position becomes an equation: XOR of buttons affecting that bit = target state.
  # x0..xn are boolean variables (one per button), addition is XOR.
  def to_z3
    lines = []
    buttons.each_with_index do |_, i|
      lines << "(declare-const x#{i} Bool)"
    end

    state.each_with_index do |target, bit|
      relevant = buttons.each_with_index.select { |btn, _| btn.bits.include?(bit) }
      if relevant.empty?
        # No button affects this bit — impossible if target is true
        lines << "(assert false)" if target
      else
        terms = relevant.map { |_, i| "x#{i}" }.join(" ")
        lines << "(assert (= (xor #{terms}) #{target}))"
      end
    end

    lines.join("\n")
  end
end

Button = Data.define(:bits)

def parse(filename)
  diagrams = []

  File.readlines(filename).each do |line|
    line = line.strip
    next if line.empty?

    pattern = line[/\[(.*?)\]/, 1]
    state = pattern.chars.map { |c| c == '#' }

    buttons = []
    line.scan(/\((.*?)\)/) do |match|
      bits = match[0].split(",").map(&:to_i)
      buttons << Button.new(bits:)
    end

    diagrams << LightDiagram.new(state:, buttons:)
  end

  diagrams
end

class Z3
  def initialize(script)
    @script = script
  end

  # Run z3 and return the raw model output (or nil if unsatisfiable)
  def call
    full_script = "(set-option :produce-models true)\n#{@script}\n(check-sat)\n(get-model)"
    result = `echo '#{full_script}' | z3 -in`
    return nil unless result.include?("sat")
    result
  end

  # Return the minimum number of buttons that need to be pressed to satisfy all equations.
  # Uses z3's optimization to minimize the count of true variables.
  def size
    n = @script.scan(/x(\d+)/).map(&:first).map(&:to_i).max
    return 0 if n.nil?

    n += 1 # number of variables is max index + 1

    opt_script = "(set-option :produce-models true)\n#{@script}\n"

    # Create integer cost variables: 1 if button pressed, 0 otherwise
    n.times do |i|
      opt_script += "(declare-const c#{i} Int)\n"
      opt_script += "(assert (= c#{i} (ite x#{i} 1 0)))\n"
    end

    cost_vars = (0...n).map { |i| "c#{i}" }.join(" ")
    opt_script += "(minimize (+ #{cost_vars}))\n"
    opt_script += "(check-sat)\n(get-model)"

    result = `echo '#{opt_script}' | z3 -in`
    return nil unless result.include?("sat")

    # Count how many x variables are true in the optimal model
    count = 0
    result.scan(/\(define-fun (x\d+) \(\) Bool\s+(true|false)\)/) do |_, val|
      count += 1 if val == "true"
    end
    count
  end
end

diagrams = parse('input.txt')
pp diagrams.sum { |diagram| Z3.new(diagram.to_z3).size }

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

גם אם אני מבין את שני הפתרונות, רק אחד מהם אני בחרתי.

ב 2026 המציאות השתנתה. מה שמעניין הוא כבר לא "איך" כותבים את הקוד אלא "איזה" קוד כותבים. עוד נצטרך לגלות איך זה ישפיע על לימודי התכנות בעתיד.

שלושה מקומות נכונים לשמור זכרון ל AI (לאנשים שמגוונים בסוכני הקידוד)

15/05/2026

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

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

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

  2. קופיילוט שומר זכרונות באופן אוטומטי שוב לקבצים בתיקיית הבית.

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

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

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

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

המשך קריאה

מבחן ה Actually

14/05/2026

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

שימו לב למשפטים נבחרים מתגובה של מודל שפה גדול בסוכן הקידוד פאי (הכל מאותה תשובה אינסופית):

Wait, looking at the flow more carefully: Looking at checking.py more carefully: But wait - looking at the caller maigret() function Actually, let me re-read the code more carefully Actually, let me think about this differently Wait, but ... But wait, I should also add the check in Actually, looking at the code more carefully Actually the current code already handles this! Look at Let me think about this differently. Actually, the simplest and most maintainable approach Wait, but the user says Actually wait, let me re-read the user's request Actually, I think the most correct regex would be Actually, I need to be more careful Actually, let me think about this more practically

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

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

האם שפת התכנות עדיין חשובה?

13/05/2026

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

  1. שפת תכנות = אקוסיסטם. בגלל זה כולם כותבים GUI בשפות מבוססות ווב וכלי למידת מכונה בפייתון. אבל מה קורה כשאין אקוסיסטם? למה לנגגרף כתובה בפייתון ולא ב rust? למה אנחנו לא מתחילים לכתוב שרתים באליקסיר?

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

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

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

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

מה דעתכם?

היום למדתי: "חוץ מ" בגיט

12/05/2026

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

$ git add .
$ git reset api.js

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

$ git add . ':!api.js'

הגרשיים שם כי zsh (וגם bash ובעצם כמעט כולם) חושב שנקודותיים סימן קריאה זה השלמת היסטוריה. בלי גרשיים zsh ינסה להחליף את api.js בפקודה קודמת מההיסטוריה וגיט אף פעם לא יראה את הסימנים המיוחדים.

וכן זה עובד בכל הפקודות ב git שצריכות קבצים כמו add, rm, restore, log ועוד.