Firebase Genkit מספק הפשטות שיעזרו לכם ליצור תהליכי יצירת נתונים שמבוססים על אחזור (RAG), וגם יישומי פלאגין שמספקים שילובים עם כלים קשורים.
מה זה RAG?
יצירה של שיטת אחזור מבוססת-אחזור היא שיטה לשילוב למקורות מידע בתשובות של LLM. חשוב להיות מסוגלים לעשות כי בעוד שמודלים גדולים של שפה מאומנים בדרך כלל לפי לרוב, שימוש מעשי ומעשי במודלים גדולים של שפה (LLM) צריך ידע בתחום מסוים למשל, אולי כדאי להשתמש ב-LLM כדי לענות על שאלות על במוצרים של החברה).
פתרון אחד הוא לשפר את המודל באמצעות נתונים ספציפיים יותר. עם זאת, הדבר יכול להיות יקר גם מבחינת עלות המחשוב וגם מבחינת המאמץ הנדרש כדי להכין נתוני אימון מתאימים.
לעומת זאת, RAG פועל על ידי שילוב מקורות נתונים חיצוניים בהנחיה משך הזמן שעבר למודל. לדוגמה, אפשר לדמיין שההנחיה "מה הקשר בין בארט לליזה?" עשויה להתרחב ('להשתפר') על ידי הוספת מידע רלוונטי, וכתוצאה מכך ההנחיה תהיה "הילדים של הומר ומרג' הם בארט, ליזה ומגי. מהי הקשר של בארט to Lisa? "
לגישה הזו יש כמה יתרונות:
- האפשרות הזו יכולה להיות משתלמת יותר, כי אין צורך לאמן מחדש את המודל.
- אתם יכולים לעדכן את מקור הנתונים באופן שוטף, ו-LLM יכול להשתמש במידע המעודכן באופן מיידי.
- עכשיו יש לכם אפשרות לצטט מקורות בתשובות של ה-LLM.
לעומת זאת, שימוש ב-RAG פירושו באופן טבעי הנחיות ארוכות יותר, וממשק API כלשהו של LLM השירותים יחויבו על כל אסימון קלט שאתם שולחים. בסופו של דבר, עליכם להעריך את העלויות של האפליקציות שלכם.
ניתוח RAG הוא תחום רחב מאוד, ויש הרבה שיטות שונות להשגת ניתוח RAG באיכות הטובה ביותר. מסגרת הליבה של Genkit מציעה שתי הפשטות ראשיות שיעזרו לכם לבצע RAG:
- מוסדי אינדקס: הוספת מסמכים לאינדקס.
- הטמעה: המרת מסמכים לייצוג בווקטור
- שירותי אחזור: אחזור מסמכים מ'אינדקס', על סמך שאילתה.
ההגדרות האלה רחבות בכוונה, כי ל-Genkit אין דעה לגבי
מה היה "אינדקס" או איך בדיוק המסמכים מאוחזרים ממנו. Genkit מספק רק פורמט Document
, וכל השאר מוגדר על ידי ספק ההטמעה של האחזור או של מנוע האינדקס.
מוספים לאינדקס
האינדקס אחראי על מעקב אחר המסמכים שלכם באופן שמאפשר לאחזר במהירות מסמכים רלוונטיים לפי שאילתה ספציפית. לרוב, הדרך לעשות זאת היא באמצעות מסד נתונים של וקטורים, שמוסיף את המסמכים לאינדקס באמצעות וקטורים מרובת-מימדים שנקראים הטמעות (embeddings). הטמעת טקסט מייצגת (באופן אטום) את המושגים שמתבטאים בקטע טקסט. הטמעות כאלה נוצרות באמצעות מודלים של למידת מכונה למטרות מיוחדות. הוספת טקסט לאינדקס באמצעות הטמעתו מאפשרת למסד נתונים וקטורי לקבץ טקסטים שקשורים מבחינה מושגית, ולאחזר מסמכים שקשורים למחרוזת טקסט חדשה (השאילתה).
לפני שתוכל לאחזר מסמכים למטרת יצירה, עליך להטמיע אותם באינדקס המסמך. תהליך בדרך כלל של הטמעת נתונים הבאים:
פיצול מסמכים גדולים למסמכים קטנים יותר, כך שרק החלקים הרלוונטיים ישמשו להוספת תוכן להנחיות – 'חלוקה לקטעים'. הצורך הזה נובע מכך שלמודלים רבים של שפה גדולה יש חלון הקשר מוגבל, ולכן לא מעשי לכלול בהנחיה מסמכים שלמים.
Genkit לא מספק ספריות חלוקה מובנות, עם זאת, יש ספריות מקור זמינות שתואמות ל-Genkit.
יצירת הטמעות (embeddings) לכל מקטע. בהתאם למסד הנתונים שבו אתם משתמשים, אפשר לעשות את זה במפורש באמצעות מודל ליצירת הטמעה, או יכול להשתמש במחולל ההטמעה שמסד הנתונים מספק.
מוסיפים למסד הנתונים את מקטע הטקסט ואת האינדקס שלו.
אם העבודה שלך עובדת, יכול להיות שהפעלת את תהליך הטמעת הנתונים בתדירות נמוכה או פעם אחת בלבד במקור נתונים יציב. לעומת זאת, אם אתם עובדים עם נתונים שמשתנים לעיתים קרובות, כדאי להפעיל את תהליך הטמעת הנתונים באופן רציף (לדוגמה, בטריגר של Cloud Firestore, בכל פעם שמסמך מתעדכן).
הטמעות
כלי הטמעה הוא פונקציה שלוקחת תוכן (טקסט, תמונות, אודיו וכו') ויוצרת וקטור מספרי שמקודד את המשמעות הסמנטית של התוכן המקורי. כפי שצוין למעלה, נעשה שימוש במטמיעים כחלק מתהליך ההוספה לאינדקס. עם זאת, ניתן להשתמש בהם גם באופן עצמאי כדי ליצור הטמעות ללא אינדקס.
אחזור נתונים
רטריבר הוא עיקרון שכולל לוגיקה שקשורה לכל סוג של מסמך באחזור. מקרי האחזור הפופולריים ביותר כוללים בדרך כלל אחזור מ- אבל ב-Genkit, רטריבר יכול להיות כל פונקציה שמחזירה נתונים.
כדי ליצור אובייקט אחזור, אפשר להשתמש באחת מההטמעות שסופקו או ליצור אובייקט משלכם.
שירותי הוספה לאינדקס, אחזור והטמעה נתמכים
Genkit מספק תמיכה באינדקסים וברטריבר דרך מערכת הפלאגין שלו. הפלאגינים הבאים נתמכים באופן רשמי:
- מסד נתונים של וקטורים בענן של Pinecone
בנוסף, Genkit תומך במאגרי הווקטורים הבאים באמצעות תבניות קוד, שאפשר להתאים אישית להגדרות של מסד הנתונים Schema:
- PostgreSQL עם
pgvector
התמיכה בהטמעת מודלים ניתנת באמצעות הפלאגינים הבאים:
פלאגין | דגמים |
---|---|
Google Generative AI | הטמעת טקסט שממית |
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 = vertexai.Init(ctx, g, &vertexai.Config{})
if err != nil {
log.Fatal(err)
}
err = localvec.Init()
if err != nil {
log.Fatal(err)
}
menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(
g,
"menuQA",
localvec.Config{
Embedder: vertexai.Embedder(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. מוצא חן בעיניי את דוגמת האינדקס, נשתמש בדוגמה הזו בווקטור רטריבר מבוסס-קובץ של Genkit, שלא כדאי להשתמש בה בסביבת ייצור.
ctx := context.Background()
g, err := genkit.Init(ctx)
if err != nil {
log.Fatal(err)
}
err = vertexai.Init(ctx, g, &vertexai.Config{})
if err != nil {
log.Fatal(err)
}
err = localvec.Init()
if err != nil {
log.Fatal(err)
}
model := vertexai.Model(g, "gemini-1.5-flash")
_, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
g,
"menuQA",
localvec.Config{
Embedder: vertexai.Embedder(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
},
)