מעבדת DIY – RAG Agent (מותאמת JnJ)

תוכן עניינים

תרגול זה ידריך אתכם בבניית סוכן RAG (Retrieval-Augmented Generation) מלא וישים באמצעות פייתון. סוכן RAG הוא מודל שפה גדול (LLM) שמסוגל לחפש מידע חיצוני (למשל, מסמכי PDF) ולהשתמש בו כ"בסיס ידע" לפני שהוא עונה על שאלות.

 

במקום לענות רק על סמך נתוני האימון שלו, סוכן ה-RAG שלנו יבצע תהליך דו-שלבי:
 

  1. Retrieval (שליפה): הפיכת מסמכים (PDF) לחלקים קטנים (Chunks) וייצוגם כווקטורים (Embeddings).
  2. Generation (יצירה): שליפת הווקטורים הרלוונטיים ביותר לשאלת המשתמש, ושימוש בהם כקונטקסט (Prompt) למודל ה-LLM (נשתמש ב-GPT-4o).

 

זהו תרגול מעשי שישלב ספריות מפתח בעולם הבינה המלאכותית: PyPDF2, Sentence Transformers, PyTorch, ו-OpenAI.

 

בסדנה זו נשתמש בשני סוגי מודלים:

  • מודל Embedding – להמרת טקסט לוקטורים מספריים, לצורך חיפוש סמנטי. נשתמש במודל Embedding מקומי, אותו נוריד למחשב.
  • המודל השני הוא מודל Chat, ובו נשתמש ב-LLM מרוחק על שרתי Azure.

 

הערה: כאן נבנה את כל הקוד בתוך קובץ אחד בשם rag_agent.py (או כל שם אחר שתבחרו), תוך הוספת מקטעים באופן מצטבר.


הכנה – הקמת הסביבה הוירטואלית

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

 

  1. יצירת סביבה וירטואלית (.venv)
     

    • ודאו שאתם נמצאים בתיקיית הפרויקט הריקה. הריצו את הפקודה הבאה ליצירת התיקייה .venv (קיצור של virtual environment):
      
      python3 -m venv .venv
                      
    •  

    • הסבר: הפקודה מורה למפרש הפייתון (Python 3) ליצור סביבה וירטואלית בתיקייה מקומית בשם .venv.
  2.  

  3. הפעלת הסביבה הווירטואלית (Activation)
     

    • כדי "להיכנס" לסביבה כך שפקודות pip install יפעלו בתוכה, הריצו את הפקודה המתאימה למערכת ההפעלה שלכם:
    •  

    • עבור Mac / Linux:
      
      source .venv/bin/activate
                      
    •  

    • עבור Windows (Command Prompt):
      
      .venv\Scripts\activate.bat
                      
    •  

    • אימות: ודאו ששם הסביבה מופיע בסוגריים מרובעים בשורת הפקודה שלכם, לדוגמה: (.venv).

1. התקנה, הכנה והגדרות סביבה

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

 

  1. התקנת ספריות
     

    • להתקנת כל הספריות הדרושות לפרויקט, הריצו את השורה הבאה ב Terminal:
       

      
      pip install PyPDF2 sentence-transformers transformers torch openai dotenv colorama
      
  2. הכנת קבצים:
     

    • צרו קובץ חדש בשם rag_agent.py.
    • צרו תיקייה בשם assets בתוך תיקיית הפרויקט.
    • מקמו בתוכה קובץ PDF לבחירתכם, המכיל טקסט בנושא מסויים, וישמש כבסיס הידע לסוכן. לא מוצאים? הורידו מכאן את התסריט המלא של הסרט Lion King.
  3. הכנת מודלי השפה (שיחה ו-Embedding):
     

    • הורידו משרתי החברה את מודל ה Embedding בשם multilingual-e5-base ושמרו אותו תחת תיקייה חדשה – models בפרויקט שלכם.
    • צרו קובץ בשם .env בתיקייה הראשית של הפרויקט והגדירו בו את מפתח ה-API שלכם בפורמט:
       

      
      OPENAI_API_KEY=<YOUR_API_KEY_HERE>
      
  4. אימפורטים והגדרות גלובליות
     

    • העתיקו והדביקו את קטע הקוד הבא לראש הקובץ rag_agent.py:
       

      
      import PyPDF2
      import re
      import os
      import torch
      import openai
      from sentence_transformers import SentenceTransformer, util
      from transformers import AutoModelForCausalLM, AutoTokenizer
      from colorama import Fore, init
      from dotenv import load_dotenv
      
      os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
      os.environ["PYTORCH_MPS_HIGH_WATERMARK_RATIO"] = "0.0"
      
      # colorama library - enables colored printouts
      init(autoreset=True)
      
      # 1. global vars for the knowledge base
      kb_content = []  # a list of the textual chunks
      kb_embeddings = None # the embeddings
      kb_embeddings_tensor = None # the embeddings tensor
      embedding_model = None # Language model for the embedding 
      
      chat_model = None # the chat model (we'll use OpenAI on Azure)
      chat_tokenizer = None
      device = torch.device("cpu")
      
      # 2. global var for conversation memory management (context)
      conversation = []
      
      # 3. default local path for embedding model
      EMBEDDING_MODEL_PATH = "./models/multilingual-e5-base"
      
      # 4. loading .env file as environment variables (for OPEN_API_KEY)
      load_dotenv()
      OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
      
      # sanity test for OPENAI KEY
      if not OPENAI_API_KEY:
          raise ValueError("OPENAI_API_KEY is not set in the .env file.")
      
      
    • הסבר: קטע זה מייבא את כל הספריות, מאתחל את `colorama` (להדפסה יפה של טקסטים צבעוניים), ומגדיר את המשתנים הגלובליים שיאחסנו את הנתונים המורכבים (כמו טנזור הווקטורים) לאורך חיי התוכנית.

2. פונקציות בסיס: פירוק PDF ויצירת Embeddings

זהו ליבת תהליך ה-RAG – הפיכת מסמכים לטקסט, חלוקתם ליחידות קטנות, והמרתן לווקטורים.

 

  1. פונקציית add_pdf (Chunking)
     

    • הוסיפו את פונקציית פירוק ה-PDF לסוף הקובץ (אין צורך להתעמק בקוד זה, הוא אינו קשור ישירות למהות המעבדה):
      
      def add_pdf(file_path):
          """
          Reads a PDF, extracts text, cleans it, and splits it into chunks (max 1000 chars) 
          by sentence boundaries, then saves it to knowledge_base.txt.
          """
          with open(file_path, 'rb') as pdf_file:
              pdf_reader = PyPDF2.PdfReader(pdf_file)
              num_pages = len(pdf_reader.pages)
              text = ''
              for page_num in range(num_pages):
                  page = pdf_reader.pages[page_num]
                  if page.extract_text():
                      text += page.extract_text() + " "
      
              # Normalization and cleanup
              text = re.sub(r'\s+', ' ', text).strip()
      
              # Split text into chunks by sentences
              sentences = re.split(r'(?<=[.!?]) +', text)
              chunks = []
              current_chunk = ""
              for sentence in sentences:
                  # Respecting a maximum chunk size of ~1000 characters
                  if len(current_chunk) + len(sentence) + 1 < 1000:
                      current_chunk += (sentence + " ").strip()
                  else:
                      chunks.append(current_chunk)
                      current_chunk = sentence + " "
              if current_chunk:
                  chunks.append(current_chunk)
                  
              # Save chunks to a file, each separated by two newlines
              with open("knowledge_base.txt", "w", encoding="utf-8") as kb_file: # Changed to 'w' to overwrite/start fresh
                  for chunk in chunks:
                      kb_file.write(chunk.strip() + "\n\n")
      
              print(f"PDF content split into {len(chunks)} chunks and saved to knowledge_base.txt.")
      
                      
    •  

    • הסבר: פונקציה זו קוראת PDF, מוציאה טקסט, ומבצעת Chunking - חלוקה ל"חתיכות" של עד 1000 תווים, תוך שמירה על גבולות משפט הגיוניים. התוצאה נשמרת בקובץ טקסט בשם knowledge_base.txt.
  2.  

  3. פונקציית create_embeddings
     

    • קראו היטב את פונקציית יצירת הווקטורים ואת ההערות בגוף הפונקציה, והוסיפו אותה לסוף הקובץ:
       

      
      def create_embedding():
          """
          Loads the knowledge base from a text file, 
          initializes the Sentence Transformer model,
          and generates vector embeddings for all stored text chunks.
          """
          global kb_embeddings, kb_embeddings_tensor, kb_content, embedding_model
      
          # 1. Load knowledge base content from file (if it exists)
          if os.path.exists("knowledge_base.txt"):
              with open("knowledge_base.txt", "r", encoding='utf-8') as kb_file:
                  # Read all lines as a list of strings (each line = one knowledge chunk)
                  kb_content = kb_file.readlines()
      
          print(f"Loaded {len(kb_content)} knowledge base entries.")
      
          # 2. Initialize the LOCAL embedding model using the specified model path
          print("Now generating embeddings...")
          embedding_model = SentenceTransformer(EMBEDDING_MODEL_PATH)
      
          # 3. Generate embeddings for all chunks (if there’s content)
          kb_embeddings = embedding_model.encode(kb_content) if kb_content else []
      
          # 4. Convert the resulting embeddings to a PyTorch tensor 
          #    for efficient similarity computation (cosine similarity, etc.)
          kb_embeddings_tensor = torch.tensor(kb_embeddings)
       
    •  

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

      1. טוענת את שברי הטקסט מתוך קובץ knowledge_base.txt
      2. מורידה (אם לא קיים) ומאתחלת את מודל Sentence Transformer (מודל Embedding פופולרי)
      3. ממירה את הטקסט לווקטורים מספריים. אחסון הווקטורים כ-PyTorch Tensor מאפשר חישובים מהירים מאוד.

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

      • kb_content המכיל את שברי (Chunks) הטקסט מתוך הקובץ המקורי
      • kb_embeddings המכיל את הוקטורים שה LLM חישב לכל אחד מה Chunks, לפי הסדר
      • kb_embeddings_tensor המכיל את הוקטורים, בפורמט PyTorch Tensor מהיר ויעיל חישובית

3. רכיבי ה-LLM: ניהול מודל וקונטקסט שיחה

הכנה לשימוש ב-OpenAI API וניהול תפקידי השיחה (System, User, Assistant).

 

  1. פונקציות get_llm ו-reinit_conversation
     

    • קראו היטב את שתי הפונקציות הבאות, והוסיפו אותן לסוף הקובץ:
       

      
      def get_llm():
          client = openai.AzureOpenAI(
              api_key=OPENAI_API_KEY,
              api_version="2024-12-01-preview",
              azure_endpoint="https://genaiapimna.jnj.com/openai-chat"
          )
          return client
      
      def reinit_conversation():
          """Resets the conversation history and adds the System message."""
      
          global conversation
          conversation = []
      
          # the system Prompt - the "personality" of the agent, role and instruction
          system_message = "You are a helpful assistant that is an expert at extracting the most useful information from a given text."
          conversation.append({"role": "system", "content": system_message})
      
          print(Fore.CYAN + "Conversation history reset. System prompt applied.")
                      
    •  

    • הסבר: הפונקציות האלה מכינות את המסגרת לשיחה: get_llm מחזירה את הקליינט של OpenAI, ו-reinit_conversation מנקה את היסטוריית השיחה (בדיוק כמו כפתור "New Chat") ומכניסה את ה-System Prompt שנותן למודל את ההנחיות והאישיות שלו.

4. מנוע השליפה (Retrieval)

הבסיס ל-RAG – חישוב וקטור השאלה והשוואתו לכל הווקטורים בבסיס הידע.

 

  1. פונקציית get_relevant_context
     

    • קראו היטב והוסיפו את פונקציית החיפוש הווקטורי:
      
      def get_relevant_context(user_input, kb_embeddings, kb_content, embedding_model, top_k=3):
          """
          Finds and returns the most relevant text chunks from the knowledge base
          using cosine similarity between the user input and precomputed embeddings.
          """
      
          # 1. Check if the embeddings tensor is empty (no knowledge base loaded)
          if kb_embeddings.nelement() == 0:
              return []
      
          # 2. Encode the user input into a numerical vector (embedding)
          input_embedding = embedding_model.encode([user_input])
      
          # 3. Calculate cosine similarity between the input vector and all stored embeddings
          #    Cosine similarity measures how close two vectors are in direction (1 = identical, 0 = unrelated)
          cos_scores = util.cos_sim(input_embedding, kb_embeddings)[0]
      
          # 4. Extract indices of the top-k most similar embeddings
          top_indices = torch.topk(cos_scores, k=min(top_k, len(cos_scores)))[1].tolist()
      
          # 5. Return the matching text chunks (stripped of extra spaces)
          return [kb_content[i].strip() for i in top_indices]
      
    •  

    • הסבר: פונקציה זו היא הלב של ה-RAG, והיא עושה את הפעולות הבאות:
       

      1. לוקחת את שאלת המשתמש והופכת אותה לווקטור
      2. משווה אותה לכל הווקטורים של ה-Chunks (חישוב Cosine Similarity)
      3. מחזירה את הטקסט של שלושת ה-Chunks (ברירת המחדל) שהיו הכי קרובים לשאלה

5. פונקציית ה-Generation והשיחה (Chat Loop)

שילוב הקונטקסט שנשלף לתוך הפרומפט הסופי ושליחתו למודל ה-LLM.

 

  1. פונקציית `llm_chat`
     

    • קראו היטב והוסיפו את הפונקציה המרכזית שמנהלת את השיחה:
      
      def llm_chat(user_input):
          global conversation
      
          chat_model = get_llm()
      
          relevant_context = get_relevant_context(user_input, kb_embeddings_tensor, kb_content, embedding_model)
          if relevant_context:
              context_str = "\n".join(relevant_context)
              print(Fore.GREEN + "Context Pulled:\n" + context_str[:1000] + "\n")
          else:
              context_str = ""
              print("No relevant context found.")
      
          user_input_with_context = (
              f"Use only the following context:\n{context_str}\n\n"
              f"Question: {user_input}\n"
              f"Answer only based on the context. "
              f"If no relevant info, say exactly: I do not know."
          )
      
          conversation.append({"role": "user", "content": user_input_with_context})
          
      
          first = chat_model.chat.completions.create(
              model="gpt-4o",
              messages=conversation)
          
          answer = first.choices[0].message
      
          conversation.append({"role": "assistant", "content": answer.content})
          return answer.content
      
      
    •  

    • הסבר: פונקציה זו מארגנת את כל התהליך: היא משתמשת ב-get_relevant_context לשליפה, בונה את ה-Prompt המועשר (כולל ה-Guardrails שמורים למודל לענות רק על סמך המידע שקיבל), שולחת ל-OpenAI, ושומרת את התשובה בהיסטוריית השיחה.
  2.  

  3. לולאת השיחה והרצה ראשית
     

    • קראו היטב והוסיפו את הפונקציה המנהלת את לולאת הקלט/פלט ואת בלוק ההרצה הראשי (שימו לב שנו את נתיב קובץ ה PDF לטעינה בהתאם):
       

      
      # ========== Chat Loop ==========
      def startChat():
          """
          Main chat loop that handles user interaction with the RAG system.
          It waits for user input, processes commands, and returns responses from the LLM.
          """
          # 1. Initialize or reset the conversation at the start
          reinit_conversation()
      
          # 2. Continuous loop for user questions
          while True:
              user_input = input("Ask a question about my knowledge: ")
      
              # 3. Command: reset or restart conversation
              if user_input.lower() in ["/reset", "/init", "/restart"]:
                  print("Resetting conversation...")
                  reinit_conversation()
                  continue
      
              # 4. Command: exit the chat
              if user_input.lower() in ["/bye", "/exit", "/end"]:
                  print("Exiting the chat. Goodbye!")
                  break
      
              # 5. Generate a response using the LLM with context retrieval
              response = llm_chat(user_input)
      
              # 6. Display the model's response clearly in the console
              print("llm Response:\n\n" + response + "\n")
      
      
      # ========== Main ==========
      if __name__ == "__main__":
          """
          Main entry point for running the RAG application.
          Ensures that a knowledge base exists, creates embeddings, and starts the chat.
          """
      
          # 1. Load or create the knowledge base
          if not os.path.exists("knowledge_base.txt"):
              file_path = "./assets/Lion King Script.pdf"
              add_pdf(file_path)  # Parse and append PDF content to knowledge base
      
          # 2. Generate embeddings from the knowledge base
          create_embedding()
      
          # 3. Launch the interactive chat loop
          startChat()
      
    •  

    • הרצה: כעת הקובץ מוכן להרצה! ודאו שהסביבה הווירטואלית פעילה, וכתבו בטרמינל:
      
      python rag_agent.py
                      
    • שימו לב: אם השתמשתם במודל שיחה לוקאלי - זהו מודל גדול משמעותית ממודל ה Embedding, ובשימוש הראשון בו בהרצה הנוכחית - לוקח לו זמן להטען לזיכרון ולהיות זמין ל inference. העזרו בסבלנות.

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

  1. שלב האתחול: בתחילה תראו הודעות המאשרות את טעינת בסיס הידע, הורדת מודל ה-Embedding (אם לא עבדתם עם מודל לוקאלי) ויצירת הווקטורים.
  2. שליפת הקונטקסט (Retrieval): בכל שאלה שתשאלו, תופיע הדפסה ירוקה גדולה תחת הכותרת --- Context Pulled from Documents (RAG) ---. זוהי ההוכחה שהסוכן חישב את הדמיון הווקטורי, שלף את ה-Chunks הרלוונטיים ביותר מה-PDF, וצירף אותם ל-Prompt.
  3. תשובת המודל (Generation): לאחר מכן תופיע תגובת ה-LLM תחת <<< RAG Agent Response. התשובה הזו תהיה מבוססת באופן בלעדי על הקונטקסט הירוק שסופק לו, וזהו הדבר החשוב ביותר שלמדנו כאן: אנו שולטים במקור המידע של המודל.

למדנו לבנות ארכיטקטורת AI מודרנית המשלבת עיבוד שפה טבעית (NLP) עם יכולות מתקדמות של LLMs, ובכך הפכנו את המודל ל"מומחה" במידע הספציפי שהענקנו לו.