תרגול זה ידריך אתכם בבניית סוכן RAG (Retrieval-Augmented Generation) מלא וישים באמצעות פייתון. סוכן RAG הוא מודל שפה גדול (LLM) שמסוגל לחפש מידע חיצוני (למשל, מסמכי PDF) ולהשתמש בו כ"בסיס ידע" לפני שהוא עונה על שאלות.
במקום לענות רק על סמך נתוני האימון שלו, סוכן ה-RAG שלנו יבצע תהליך דו-שלבי:
- Retrieval (שליפה): הפיכת מסמכים (PDF) לחלקים קטנים (Chunks) וייצוגם כווקטורים (Embeddings).
- Generation (יצירה): שליפת הווקטורים הרלוונטיים ביותר לשאלת המשתמש, ושימוש בהם כקונטקסט (Prompt) למודל ה-LLM (נשתמש ב-GPT-4o).
זהו תרגול מעשי שישלב ספריות מפתח בעולם הבינה המלאכותית: PyPDF2, Sentence Transformers, PyTorch, ו-OpenAI.
בסדנה זו נשתמש בשני סוגי מודלים:
- מודל Embedding – להמרת טקסט לוקטורים מספריים, לצורך חיפוש סמנטי. נשתמש במודל Embedding מקומי, אותו נוריד למחשב.
- המודל השני הוא מודל Chat, ובו נשתמש ב-LLM מרוחק על שרתי Azure.
הערה: כאן נבנה את כל הקוד בתוך קובץ אחד בשם rag_agent.py (או כל שם אחר שתבחרו), תוך הוספת מקטעים באופן מצטבר.
הכנה – הקמת הסביבה הוירטואלית
לפני שמתחילים לכתוב קוד, חשוב להקים סביבה וירטואלית מבודדת (venv). סביבה זו מבטיחה שכל הספריות שנתקין (כמו PyTorch ו-OpenAI) יישארו צמודות לפרויקט זה בלבד ולא יתנגשו עם ספריות אחרות במערכת ההפעלה שלכם.
-
יצירת סביבה וירטואלית (
.venv)
- ודאו שאתם נמצאים בתיקיית הפרויקט הריקה. הריצו את הפקודה הבאה ליצירת התיקייה
.venv(קיצור של virtual environment):python3 -m venv .venv - הסבר: הפקודה מורה למפרש הפייתון (Python 3) ליצור סביבה וירטואלית בתיקייה מקומית בשם
.venv.
- ודאו שאתם נמצאים בתיקיית הפרויקט הריקה. הריצו את הפקודה הבאה ליצירת התיקייה
-
הפעלת הסביבה הווירטואלית (Activation)
- כדי "להיכנס" לסביבה כך שפקודות
pip installיפעלו בתוכה, הריצו את הפקודה המתאימה למערכת ההפעלה שלכם: - עבור Mac / Linux:
source .venv/bin/activate - עבור Windows (Command Prompt):
.venv\Scripts\activate.bat - אימות: ודאו ששם הסביבה מופיע בסוגריים מרובעים בשורת הפקודה שלכם, לדוגמה:
(.venv).
- כדי "להיכנס" לסביבה כך שפקודות
1. התקנה, הכנה והגדרות סביבה
בשלב זה נתקין את כל הספריות הנדרשות ונכין את קובצי התצורה של הפרויקט.
-
התקנת ספריות
- להתקנת כל הספריות הדרושות לפרויקט, הריצו את השורה הבאה ב Terminal:
pip install PyPDF2 sentence-transformers transformers torch openai dotenv colorama
- להתקנת כל הספריות הדרושות לפרויקט, הריצו את השורה הבאה ב Terminal:
- הכנת קבצים:
- צרו קובץ חדש בשם
rag_agent.py. - צרו תיקייה בשם
assetsבתוך תיקיית הפרויקט. - מקמו בתוכה קובץ PDF לבחירתכם, המכיל טקסט בנושא מסויים, וישמש כבסיס הידע לסוכן. לא מוצאים? הורידו מכאן את התסריט המלא של הסרט Lion King.
- צרו קובץ חדש בשם
- הכנת מודלי השפה (שיחה ו-Embedding):
- הורידו משרתי החברה את מודל ה Embedding בשם
multilingual-e5-baseושמרו אותו תחת תיקייה חדשה –modelsבפרויקט שלכם. - צרו קובץ בשם
.envבתיקייה הראשית של הפרויקט והגדירו בו את מפתח ה-API שלכם בפורמט:
OPENAI_API_KEY=<YOUR_API_KEY_HERE>
- הורידו משרתי החברה את מודל ה Embedding בשם
-
אימפורטים והגדרות גלובליות
- העתיקו והדביקו את קטע הקוד הבא לראש הקובץ
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 – הפיכת מסמכים לטקסט, חלוקתם ליחידות קטנות, והמרתן לווקטורים.
-
פונקציית
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.
- הוסיפו את פונקציית פירוק ה-PDF לסוף הקובץ (אין צורך להתעמק בקוד זה, הוא אינו קשור ישירות למהות המעבדה):
-
פונקציית
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) - הסבר: פונקציה זו בונה למעשה את בסיס הידע של הסוכן. היא עושה זאת במספר שלבים:
- טוענת את שברי הטקסט מתוך קובץ
knowledge_base.txt - מורידה (אם לא קיים) ומאתחלת את מודל Sentence Transformer (מודל Embedding פופולרי)
- ממירה את הטקסט לווקטורים מספריים. אחסון הווקטורים כ-PyTorch Tensor מאפשר חישובים מהירים מאוד.
בסיום פעולה זאת, נקבל שלושה מערכים בעלי גודל זהה:
kb_contentהמכיל את שברי (Chunks) הטקסט מתוך הקובץ המקוריkb_embeddingsהמכיל את הוקטורים שה LLM חישב לכל אחד מה Chunks, לפי הסדרkb_embeddings_tensorהמכיל את הוקטורים, בפורמט PyTorch Tensor מהיר ויעיל חישובית
- טוענת את שברי הטקסט מתוך קובץ
- קראו היטב את פונקציית יצירת הווקטורים ואת ההערות בגוף הפונקציה, והוסיפו אותה לסוף הקובץ:
3. רכיבי ה-LLM: ניהול מודל וקונטקסט שיחה
הכנה לשימוש ב-OpenAI API וניהול תפקידי השיחה (System, User, Assistant).
-
פונקציות
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 – חישוב וקטור השאלה והשוואתו לכל הווקטורים בבסיס הידע.
-
פונקציית
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, והיא עושה את הפעולות הבאות:
- לוקחת את שאלת המשתמש והופכת אותה לווקטור
- משווה אותה לכל הווקטורים של ה-Chunks (חישוב Cosine Similarity)
- מחזירה את הטקסט של שלושת ה-Chunks (ברירת המחדל) שהיו הכי קרובים לשאלה
- קראו היטב והוסיפו את פונקציית החיפוש הווקטורי:
5. פונקציית ה-Generation והשיחה (Chat Loop)
שילוב הקונטקסט שנשלף לתוך הפרומפט הסופי ושליחתו למודל ה-LLM.
-
פונקציית `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, ושומרת את התשובה בהיסטוריית השיחה.
- קראו היטב והוסיפו את הפונקציה המרכזית שמנהלת את השיחה:
-
לולאת השיחה והרצה ראשית
- קראו היטב והוסיפו את הפונקציה המנהלת את לולאת הקלט/פלט ואת בלוק ההרצה הראשי (שימו לב שנו את נתיב קובץ ה 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. העזרו בסבלנות.
- קראו היטב והוסיפו את הפונקציה המנהלת את לולאת הקלט/פלט ואת בלוק ההרצה הראשי (שימו לב שנו את נתיב קובץ ה PDF לטעינה בהתאם):
לאחר הרצת סוכן ה-RAG, אתם צפויים לראות בטרמינל שלושה סוגי פלט מרכזיים המדגימים את תהליך ה-RAG במלואו:
- שלב האתחול: בתחילה תראו הודעות המאשרות את טעינת בסיס הידע, הורדת מודל ה-Embedding (אם לא עבדתם עם מודל לוקאלי) ויצירת הווקטורים.
- שליפת הקונטקסט (Retrieval): בכל שאלה שתשאלו, תופיע הדפסה ירוקה גדולה תחת הכותרת
--- Context Pulled from Documents (RAG) ---. זוהי ההוכחה שהסוכן חישב את הדמיון הווקטורי, שלף את ה-Chunks הרלוונטיים ביותר מה-PDF, וצירף אותם ל-Prompt. - תשובת המודל (Generation): לאחר מכן תופיע תגובת ה-LLM תחת <<< RAG Agent Response. התשובה הזו תהיה מבוססת באופן בלעדי על הקונטקסט הירוק שסופק לו, וזהו הדבר החשוב ביותר שלמדנו כאן: אנו שולטים במקור המידע של המודל.
למדנו לבנות ארכיטקטורת AI מודרנית המשלבת עיבוד שפה טבעית (NLP) עם יכולות מתקדמות של LLMs, ובכך הפכנו את המודל ל"מומחה" במידע הספציפי שהענקנו לו.