יצירת מודלים עם שיפור אחזור (RAG)

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

מהו RAG?

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

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

לעומת זאת, RAG משלב מקורות נתונים חיצוניים בהנחיה בזמן שהיא מועברת למודל. לדוגמה, אפשר לדמיין שההנחיה "מה הקשר בין בארט לליזה?" עשויה להתרחב ('להשתפר') על ידי הוספת מידע רלוונטי, וכתוצאה מכך ההנחיה תהיה "הילדים של הומר ומרג' הם בארט, ליזה ומגי. What is Bart's relationship to Lisa?‎

לגישה הזו יש כמה יתרונות:

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

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

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

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

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

הוספה לאינדקס

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

כדי לאחזר מסמכים לצורך יצירה, צריך להטמיע אותם במדד המסמכים. תהליך הטמעה טיפוסי כולל את הפעולות הבאות:

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

    Genkit לא מספק ספריות מובנות לחלוקה לקטעים, אבל יש ספריות קוד פתוח שתואמות ל-Genkit.

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

  3. מוסיפים את מקטע הטקסט ואת האינדקס שלו למסד הנתונים.

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

גורמים שמטמיעים

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

אחזור נתונים

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

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

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

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

  • מסד נתונים של וקטורים בענן של Pinecone

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

התמיכה בהטמעת מודלים ניתנת באמצעות הפלאגינים הבאים:

פלאגין דגמים
Google Generative AI הטמעת טקסט ב-Gecko
Google Vertex AI הטמעת טקסט ב-Gecko

הגדרת תהליך RAG

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

התקנת יחסי תלות

בדוגמה הזו נשתמש בספרייה textsplitter מ-langchaingo ובספריית הניתוח של קובצי PDF‏ ledongthuc/pdf:

go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf

הגדרת מנהל אינדקס

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

הוא משתמש באחזור הדמיון של וקטורים שמבוסס על קבצים מקומיים, ש-Genkit מספק מראש לצורך בדיקה ופיתוח אב טיפוס פשוט (אין להשתמש בו בסביבת הייצור)

יצירת הכלי להוספה לאינדקס

// Import Genkit's file-based vector retriever, (Don't use in production.)
import "github.com/firebase/genkit/go/plugins/localvec"

// Vertex AI provides the text-embedding-004 embedder model.
import "github.com/firebase/genkit/go/plugins/vertexai"
ctx := context.Background()

g, err := genkit.Init(ctx)
if err != nil {
	log.Fatal(err)
}

err = (&googlegenai.VertexAI{}).Init(ctx, g)
if err != nil {
	log.Fatal(err)
}
err = localvec.Init()
if err != nil {
	log.Fatal(err)
}

menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(
	g,
	"menuQA",
	localvec.Config{
		Embedder: googlegenai.VertexAIEmbedder(g, "text-embedding-004"),
	},
)
if err != nil {
	log.Fatal(err)
}

יצירת הגדרה אישית של חלוקה למקטעים

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

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

splitter := textsplitter.NewRecursiveCharacter(
	textsplitter.WithChunkSize(200),
	textsplitter.WithChunkOverlap(20),
)

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

הגדרת תהליך הוספת הנתונים לאינדקס

genkit.DefineFlow(
	g,
	"indexMenu",
	func(ctx context.Context, path string) (any, error) {
		// Extract plain text from the PDF. Wrap the logic in Run so it
		// appears as a step in your traces.
		pdfText, err := genkit.Run(ctx, "extract", func() (string, error) {
			return readPDF(path)
		})
		if err != nil {
			return nil, err
		}

		// Split the text into chunks. Wrap the logic in Run so it
		// appears as a step in your traces.
		docs, err := genkit.Run(ctx, "chunk", func() ([]*ai.Document, error) {
			chunks, err := splitter.SplitText(pdfText)
			if err != nil {
				return nil, err
			}

			var docs []*ai.Document
			for _, chunk := range chunks {
				docs = append(docs, ai.DocumentFromText(chunk, nil))
			}
			return docs, nil
		})
		if err != nil {
			return nil, err
		}

		// Add chunks to the index.
		err = ai.Index(ctx, menuPDFIndexer, ai.WithIndexerDocs(docs...))
		return nil, err
	},
)
// Helper function to extract plain text from a PDF. Excerpted from
// https://github.com/ledongthuc/pdf
func readPDF(path string) (string, error) {
	f, r, err := pdf.Open(path)
	if f != nil {
		defer f.Close()
	}
	if err != nil {
		return "", err
	}

	reader, err := r.GetPlainText()
	if err != nil {
		return "", err
	}

	bytes, err := io.ReadAll(reader)
	if err != nil {
		return "", err
	}
	return string(bytes), nil
}

הרצת תהליך הוספת הנתונים לאינדקס

genkit flow:run indexMenu "'menu.pdf'"

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

הגדרת תהליך עם אחזור

בדוגמה הבאה מוסבר איך משתמשים ב-retriever בתהליך RAG. בדומה לדוגמה של ה-indexer, בדוגמה הזו נעשה שימוש באחזור וקטורים מבוסס-קובץ של Genkit, שאסור להשתמש בו בסביבת ייצור.

	ctx := context.Background()

	g, err := genkit.Init(ctx)
	if err != nil {
		log.Fatal(err)
	}

	err = (&googlegenai.VertexAI{}).Init(ctx, g)
	if err != nil {
		log.Fatal(err)
	}
	err = localvec.Init()
	if err != nil {
		log.Fatal(err)
	}

	model := googlegenai.VertexAIModel(g, "gemini-1.5-flash")

	_, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
		g,
		"menuQA",
		localvec.Config{
			Embedder: googlegenai.VertexAIEmbedder(g, "text-embedding-004"),
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	genkit.DefineFlow(
		g,
		"menuQA",
		func(ctx context.Context, question string) (string, error) {
			// Retrieve text relevant to the user's question.
			docs, err := menuPdfRetriever.Retrieve(ctx, &ai.RetrieverRequest{
				Query: ai.DocumentFromText(question, nil),
			})
			if err != nil {
				return "", err
			}

			// Construct a system message containing the menu excerpts you just
			// retrieved.
			menuInfo := ai.NewSystemTextMessage("Here's the menu context:")
			for _, doc := range docs.Documents {
				menuInfo.Content = append(menuInfo.Content, doc.Content...)
			}

			// Call Generate, including the menu information in your prompt.
			return genkit.GenerateText(ctx, g,
				ai.WithModel(model),
				ai.WithMessages(
					ai.NewSystemTextMessage(`
You are acting as a helpful AI assistant that can answer questions about the
food available on the menu at Genkit Grub Pub.
Use only the context provided to answer the question. If you don't know, do not
make up an answer. Do not add or change items on the menu.`),
					menuInfo,
					ai.NewUserTextMessage(question)))
		})

כתיבת מפתחות ומאגרי אחזור משלכם

אפשר גם ליצור מאגר משלכם. האפשרות הזו שימושית אם המסמכים שלכם מנוהלים במאגר מסמכים שלא נתמך ב-Genkit (למשל: MySQL,‏ Google Drive וכו'). ב-Genkit SDK יש שיטות גמישות שמאפשרות לספק קוד מותאם אישית לאחזור מסמכים.

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

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

type CustomMenuRetrieverOptions struct {
	K          int
	PreRerankK int
}
advancedMenuRetriever := genkit.DefineRetriever(
	g,
	"custom",
	"advancedMenuRetriever",
	func(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) {
		// Handle options passed using our custom type.
		opts, _ := req.Options.(CustomMenuRetrieverOptions)
		// Set fields to default values when either the field was undefined
		// or when req.Options is not a CustomMenuRetrieverOptions.
		if opts.K == 0 {
			opts.K = 3
		}
		if opts.PreRerankK == 0 {
			opts.PreRerankK = 10
		}

		// Call the retriever as in the simple case.
		response, err := menuPDFRetriever.Retrieve(ctx, &ai.RetrieverRequest{
			Query:   req.Query,
			Options: localvec.RetrieverOptions{K: opts.PreRerankK},
		})
		if err != nil {
			return nil, err
		}

		// Re-rank the returned documents using your custom function.
		rerankedDocs := rerank(response.Documents)
		response.Documents = rerankedDocs[:opts.K]

		return response, nil
	},
)