• בלוג
  • דוגמת קוד RAG לקראת הוובינר בשבוע הבא

דוגמת קוד RAG לקראת הוובינר בשבוע הבא

24/01/2026

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

אחד השימושים למנגנון ה Embedding נקרא RAG או Retrieval-augmented generation. הרעיון הוא שאחרי שיצרנו וקטורי Embedding מכל הפוסטים נוכל ליצור וקטור כזה גם משאלה של משתמש ואז נוסיף את הפוסטים הרלוונטים לפרומפט בצורה אוטומטית.

בואו נראה איך זה עובד בעזרת פרויקט לדוגמה.

1. התקנה והפעלה

בקישור:

https://github.com/ynonp/embedding-demo

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

$ docker run --rm -p 5454:5432 -e POSTGRES_PASSWORD=password pgvector/pgvector:pg18-trixie

תצטרכו גם את ollama מותקן על המחשב עם מודל האמבדינג nomic-embed-text-v2-moe:latest. לאחר מכן אפשר ליצור את הנתונים בבסיס הנתונים לדוגמה עם הפקודות:

$ ./bin/rails db:migrate
$ ./bin/rails r script/scrape_data.rb
$ ./bin/rails r script/index_posts.rb

אחרי הפעלת שלושת הפקודות תקבלו בבסיס הנתונים טבלת פוסטים עם 200 פוסטים אחרונים מהבלוג פה, ולכל פוסט יהיה וקטור Embedding שמתאים לו שחושב באמצעות המודל nomic-embed-text-v2-moe:latest.

2. חישוב מרחק מפרומפט

נפתח את הקובץ app/models/post.rb ושם נמצא את הפונקציה cosine_distance_from_prompt:

  def cosine_distance_from_prompt(prompt, model: EMBED_MODEL)
    return nil unless embedding.present?

    # Calculate embedding for the prompt
    embed = RubyLLM.embed(prompt, provider: :ollama, model:, dimensions: 768)
    prompt_embedding = embed.vectors

    # Calculate cosine distance using PostgreSQL's <=> operator
    conn = self.class.connection.raw_connection
    result = conn.exec_params(
      "SELECT embedding <=> $1::vector AS distance FROM posts WHERE id = $2",
      [prompt_embedding, id]
    )

    result.first['distance'].to_f
  end

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

3.3.5 :004 > Post.find_by(slug: '2026-01-vscode-tasks').cosine_distance_from_prompt("סכנות אבטחה ב vs code")
 => 0.6270134320979381

3.3.5 :005 > Post.find_by(slug: '2026-01-basic-knowledge').cosine_distance_from_prompt("סכנות אבטחה ב vs code")
 => 0.7205912733782678

3.3.5 :006 > Post.find_by(slug: '2026-01-learn-worktree-with-ai').cosine_distance_from_prompt("סכנות אבטחה ב vs code")
 => 0.658548735705814

3.3.5 :008 > Post.find_by(slug: '2026-01-aoc2025day8').cosine_distance_from_prompt("סכנות אבטחה ב vs code")
 => 0.6936170916427378

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

3. העברת מידע נוסף לסוכן

הסוכן כתוב ב Ruby ומשתמש בספריית ruby_llm. בשביל לשלוח שאלה ל AI בספריה זו כל מה שצריך זו קריאת פונקציה אחת:

chat = Chat.create(model: "gpt-5-mini")
chat.ask "מהן סכנות האבטחה ב VS Code"

מפעילים את הפרויקט עם ./bin/dev, נכנסים ל localhost:3000/chats, לוחצים על הכפתור לפתיחת שיחה חדשה ומקלידים את הפרומפט ונוכל לראות את התשובה הבסיסית של הסוכן.

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

  def self.find_relevant_for_prompt(user_prompt, limit: 5)
    # Calculate embedding for the prompt
    embed = RubyLLM.embed(user_prompt, provider: :ollama, model: EMBED_MODEL)
    prompt_embedding = embed.vectors

    # Search database directly using vector search
    conn = connection.raw_connection
    conn.exec_params(
      "SELECT *, embedding <=> $1::vector AS distance FROM posts ORDER BY embedding <=> $1::vector LIMIT $2",
      [prompt_embedding, limit]
    )
  end

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

בקובץ app/jobs/chat_response_job.rb נוכל למצוא את הקוד הבא שאחראי על הצירוף:

  def build_content_with_relevant_posts(user_prompt)
    # Get relevant posts from Post model
    results = Post.find_relevant_for_prompt(user_prompt)

    # Build raw content block array
    raw_blocks = results.map do |row|
      distance = row['distance'].to_f
      { type: 'text', text: row['text'], meta: { origin: row['slug'], distance: distance } }
    end

    # Add user prompt as final item
    raw_blocks << { type: 'text', text: user_prompt }

    # Create RubyLLM::Content::Raw instance
    RubyLLM::Content::Raw.new(raw_blocks)
  end

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

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

4. שיפור המודל qwen3-embedding:8b

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

בשביל לשפר את הדוגמה אני מנסה מודל נוסף בשם qwen3-embedding:8b. אני מעדכן בקובץ app/models/post.rb את מודל האמבדינג, מוחק את האמבדינג שחישבתי עם המודל הקודם בעזרת הפקודה:

Post.all.each {|p| p.clear_embed! }

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

Post.first(5).each(&:calc_embed!)

הפעם אני רואה וקטורים הרבה יותר קרובים:

[["2026-01-vscode-tasks", 0.33983817338308975],
 ["2026-01-basic-knowledge", 0.5685639042518127],
 ["2026-01-git-skip-worktree", 0.5330361863293361],
 ["2026-01-minimal-is-not-buggy", 0.4484948315443078],
 ["2026-01-ai-suggestions", 0.4072950857389136]]

וגם בשאלה שלי על גיט:

3.3.5 :005 > Post.first(5).map {|p| [p.slug, p.cosine_distance_from_prompt("איך אפשר לדלג על שינויים בקובץ בגיט") ]}
  Post Load (2.6ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT 5 /*application='EmbeddingsDemo'*/
 =>
[["2026-01-vscode-tasks", 0.48373095440338176],
 ["2026-01-basic-knowledge", 0.6044550302299699],
 ["2026-01-git-skip-worktree", 0.28206969224639555],
 ["2026-01-minimal-is-not-buggy", 0.4468360452822494],
 ["2026-01-ai-suggestions", 0.46263151024514804]]

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

5. לאן ממשיכים

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

אפשר להירשם כאן כדי לקבל קישור לזום:

https://www.tocode.co.il/talking_ai

נתראה בחמישי