Firebase Genkit מספק הפשטות שיעזרו לכם ליצור תהליכי יצירת נתונים שמבוססים על אחזור (RAG), וגם יישומי פלאגין שמספקים שילובים עם כלים קשורים.
מהו RAG?
יצירת תוכן באמצעות אחזור משופר היא טכניקה שמשמשת לשילוב מקורות מידע חיצוניים בתשובות של LLM. חשוב לעשות זאת כי בדרך כלל מודלים מסוג LLM מאומנים על כמות גדולה של חומר, אבל לרוב נדרש ידע ספציפי בתחום כדי להשתמש בהם באופן מעשי (לדוגמה, יכול להיות שתרצו להשתמש ב-LLM כדי לענות על שאלות של לקוחות לגבי המוצרים של החברה).
פתרון אחד הוא לשפר את המודל באמצעות נתונים ספציפיים יותר. עם זאת, הדבר יכול להיות יקר גם מבחינת עלות המחשוב וגם מבחינת המאמץ הנדרש כדי להכין נתוני אימון מתאימים.
לעומת זאת, RAG פועל על ידי שילוב מקורות נתונים חיצוניים בהנחיה בזמן שהיא מועברת למודל. לדוגמה, אפשר לדמיין שההנחיה "מה הקשר בין בארט לליזה?" עשויה להתרחב ('להשתפר') על ידי הוספת מידע רלוונטי, וכתוצאה מכך ההנחיה תהיה "הילדים של הומר ומרג' הם בארט, ליזה ומגי. מה הקשר של בארט לליסה?"
לגישה הזו יש כמה יתרונות:
- השיטה הזו יכולה להיות משתלמת יותר כי לא צריך לאמן מחדש את המודל.
- אתם יכולים לעדכן את מקור הנתונים באופן שוטף, ו-LLM יכול להשתמש במידע המעודכן באופן מיידי.
- עכשיו יש לכם אפשרות לצטט הפניות בתשובות של ה-LLM.
לעומת זאת, שימוש ב-RAG פירושו באופן טבעי הנחיות ארוכות יותר, וחלק משירותי ה-API ל-LLM גובים על כל אסימון קלט ששולחים. בסופו של דבר, עליכם להעריך את העלויות של האפליקציות שלכם.
RAG הוא תחום רחב מאוד, ויש הרבה שיטות שונות להשגת ה-RAG באיכות הטובה ביותר. במסגרת הליבה של Genkit יש שתי הפשטות עיקריות כדי לעזור לך לבצע RAG:
- מוסדי אינדקס: הוספת מסמכים לאינדקס.
- הטמעה: המרת מסמכים לייצוג בווקטור
- שירותי אחזור: אחזור מסמכים מ'אינדקס', על סמך שאילתה.
ההגדרות האלה רחבות כוונה, כי ל-Genkit אין דעה לגבי מהו 'אינדקס' או לגבי האופן שבו בדיוק המסמכים מאוחזרים ממנו. Genkit מספק רק פורמט Document
, וכל השאר מוגדר על ידי ספק ההטמעה של האחזור או של מנוע האינדקס.
אינדקסים
האינדקס אחראי על מעקב אחר המסמכים שלכם באופן שמאפשר לאחזר במהירות מסמכים רלוונטיים לפי שאילתה ספציפית. לרוב, הדרך לעשות זאת היא באמצעות מסד נתונים של וקטורים, שמוסיף את המסמכים לאינדקס באמצעות וקטורים מרובת-מימדים שנקראים הטמעות (embeddings). הטמעת טקסט (אטום) מייצגת את המושגים שמבוטאים בקטע טקסט. הם נוצרים באמצעות מודלים למטרה מיוחדת של למידת מכונה. הוספת טקסט לאינדקס באמצעות הטמעתו מאפשרת למסד נתונים וקטורי לקבץ טקסטים שקשורים מבחינה מושגית, ולאחזר מסמכים שקשורים למחרוזת טקסט חדשה (השאילתה).
לפני שאפשר לאחזר מסמכים למטרת יצירה, צריך להטמיע אותם באינדקס המסמכים. תהליך הטמעה טיפוסי כולל את הפעולות הבאות:
פיצול מסמכים גדולים למסמכים קטנים יותר, כך שרק החלקים הרלוונטיים ישמשו להוספת תוכן להנחיות – 'חלוקה לקטעים'. הצורך הזה נובע מכך שלמודלים רבים של שפה גדולה יש חלון הקשר מוגבל, ולכן לא מעשי לכלול בהנחיה מסמכים שלמים.
ב-Genkit אין ספריות מובְנות מובנות, אבל יש ספריות של קוד פתוח שתואמות ל-Genkit.
יצירת הטמעות (embeddings) לכל מקטע. בהתאם למסד הנתונים שבו אתם משתמשים, תוכלו לעשות זאת באופן מפורש באמצעות מודל ליצירת הטמעה, או להשתמש במחולל הטמעה שסופק על ידי מסד הנתונים.
מוסיפים את מקטע הטקסט ואת האינדקס שלו למסד הנתונים.
אם אתם עובדים עם מקור נתונים יציב, יכול להיות שתפעילו את תהליך הטמעת הנתונים לעיתים רחוקות או רק פעם אחת. לעומת זאת, אם אתם עובדים עם נתונים שמשתנים לעיתים קרובות, כדאי להפעיל את תהליך הטמעת הנתונים באופן רציף (לדוגמה, בטריגר של Cloud Firestore, בכל פעם שמסמך מתעדכן).
גורמים שמטמיעים
כלי הטמעה הוא פונקציה שלוקחת תוכן (טקסט, תמונות, אודיו וכו') ויוצרת וקטור מספרי שמקודד את המשמעות הסמנטית של התוכן המקורי. כפי שצוין למעלה, רכיבי הטמעה משמשים כחלק מתהליך ההוספה לאינדקס, אבל אפשר להשתמש בהם גם באופן עצמאי כדי ליצור הטמעות ללא אינדקס.
רטריבר
רטריבר הוא קונספט שכולל לוגיקה שקשורה לכל סוג של אחזור מסמכים. רוב המקרים הנפוצים של אחזור כוללים אחזור מחנויות וקטורים, אבל ב-Genkit, פונקציית אחזור יכולה להיות כל פונקציה שמחזירה נתונים.
כדי ליצור אובייקט אחזור, אפשר להשתמש באחת מההטמעות שסופקו או ליצור אובייקט משלכם.
שירותי הוספה לאינדקס, אחזור והטמעה נתמכים
Genkit מספק תמיכה במסד הנתונים המנוהל ובאחזור באמצעות מערכת הפלאגינים שלו. הפלאגינים הבאים נתמכים באופן רשמי:
- מסד נתונים של וקטורים בענן Pinecone
בנוסף, Genkit תומך במאגרי הווקטורים הבאים באמצעות תבניות קוד מוגדרות מראש, שאפשר להתאים אישית בהתאם להגדרות ולסכימה של מסד הנתונים:
- PostgreSQL עם
pgvector
התמיכה במודל ההטמעה מסופקת באמצעות יישומי הפלאגין הבאים:
פלאגין | דגמים |
---|---|
AI גנרטיבי של Google | הטמעת טקסט ב-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()
err := vertexai.Init(ctx, &vertexai.Config{})
if err != nil {
log.Fatal(err)
}
err = localvec.Init()
if err != nil {
log.Fatal(err)
}
menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(
"menuQA",
localvec.Config{
Embedder: vertexai.Embedder("text-embedding-004"),
},
)
if err != nil {
log.Fatal(err)
}
יצירת הגדרה אישית של חלוקה למקטעים
בדוגמה הזו נעשה שימוש בספרייה textsplitter
, שמספקת כלי פשוט לפיצול טקסט כדי לפצל מסמכים לקטעים שאפשר להפוך לווקטורים.
ההגדרה הבאה מגדירה את פונקציית הפילוח כך שתחזיר קטעי מסמך באורך 200 תווים, עם חפיפה בין קטעים באורך 20 תווים.
splitter := textsplitter.NewRecursiveCharacter(
textsplitter.WithChunkSize(200),
textsplitter.WithChunkOverlap(20),
)
אפשרויות נוספות לחלוקה לקטעים בספרייה הזו מפורטות במאמרי העזרה בנושא langchaingo
.
הגדרת תהליך הוספת הנתונים לאינדקס
genkit.DefineFlow(
"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 עם שלבי אחזור.
הגדרת תהליך עם אחזור
הדוגמה הבאה ממחישה איך אפשר להשתמש ברטריבר בתהליך RAG. בדומה לדוגמה של ה-indexer, בדוגמה הזו נעשה שימוש ב-Genkit's file-based vector retriever, שאסור להשתמש בו בסביבת הייצור.
ctx := context.Background()
err := vertexai.Init(ctx, &vertexai.Config{})
if err != nil {
log.Fatal(err)
}
err = localvec.Init()
if err != nil {
log.Fatal(err)
}
model := vertexai.Model("gemini-1.5-flash")
_, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
"menuQA",
localvec.Config{
Embedder: vertexai.Embedder("text-embedding-004"),
},
)
if err != nil {
log.Fatal(err)
}
genkit.DefineFlow(
"menuQA",
func(ctx context.Context, question string) (string, error) {
// Retrieve text relevant to the user's question.
docs, err := menuPdfRetriever.Retrieve(ctx, &ai.RetrieverRequest{
Document: 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 ai.GenerateText(ctx, 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 := ai.DefineRetriever(
"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{
Document: req.Document,
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
},
)