הגדרת תהליכי עבודה של AI

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

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

כל שלב בתהליך העבודה הזה חייב לפעול יחד כדי שכל משימה שקשורה ל-AI תצליח.

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

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

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

הגדרה של תהליכים והפעלה שלהם

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

menuSuggestionFlow := genkit.DefineFlow(g, "menuSuggestionFlow",
    func(ctx context.Context, theme string) (string, error) {
        resp, err := genkit.GenerateData(ctx, g,
            ai.WithPrompt("Invent a menu item for a %s themed restaurant.", theme),
        )
        if err != nil {
            return "", err
        }

        return resp.Text(), nil
    })

הוספת הקוד הזה לקריאות genkit.Generate() מוסיפה פונקציונליות מסוימת: כך תוכלו להריץ את התהליך מ-CLI של Genkit ומממשק המשתמש למפתחים, וזו גם דרישה לשימוש בכמה מהתכונות של Genkit, כולל פריסה וניטור (הנושאים האלה נדונים בקטעים הבאים).

סכימות קלט ופלט

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

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

type MenuItem struct {
    Name        string `json:"name"`
    Description string `json:"description"`
}

menuSuggestionFlow := genkit.DefineFlow(g, "menuSuggestionFlow",
    func(ctx context.Context, theme string) (MenuItem, error) {
        return genkit.GenerateData[MenuItem](ctx, g,
            ai.WithPrompt("Invent a menu item for a %s themed restaurant.", theme),
        )
    })

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

type MenuItem struct {
    Name        string `json:"name"`
    Description string `json:"description"`
}

menuSuggestionMarkdownFlow := genkit.DefineFlow(g, "menuSuggestionMarkdownFlow",
    func(ctx context.Context, theme string) (string, error) {
        item, _, err := genkit.GenerateData[MenuItem](ctx, g,
            ai.WithPrompt("Invent a menu item for a %s themed restaurant.", theme),
        )
        if err != nil {
            return "", err
        }

        return fmt.Sprintf("**%s**: %s", item.Name, item.Description), nil
    })

תהליכי עבודה לשיחות

אחרי שמגדירים תהליך, אפשר להפעיל אותו מקוד Go:

item, err := menuSuggestionFlow.Run(ctx, "bistro")

הארגומנט לתהליך חייב להתאים לסכימת הקלט.

אם הגדרתם סכימה של פלט, התגובה של התהליך תהיה תואמת לה. לדוגמה, אם מגדירים את הסכימה של הפלט כ-MenuItem, הפלט של התהליך יכיל את המאפיינים שלה:

item, err := menuSuggestionFlow.Run(ctx, "bistro")
if err != nil {
    log.Fatal(err)
}

log.Println(item.DishName)
log.Println(item.Description)

תהליכי סטרימינג

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

דוגמה לתהליך עבודה שתומך בסטרימינג:

type Menu struct {
    Theme  string     `json:"theme"`
    Items  []MenuItem `json:"items"`
}

type MenuItem struct {
    Name        string `json:"name"`
    Description string `json:"description"`
}

menuSuggestionFlow := genkit.DefineStreamingFlow(g, "menuSuggestionFlow",
    func(ctx context.Context, theme string, callback core.StreamCallback[string]) (Menu, error) {
        item, _, err := genkit.GenerateData[MenuItem](ctx, g,
            ai.WithPrompt("Invent a menu item for a %s themed restaurant.", theme),
            ai.WithStreaming(func(ctx context.Context, chunk *ai.ModelResponseChunk) error {
                // Here, you could process the chunk in some way before sending it to
                // the output stream using StreamCallback. In this example, we output
                // the text of the chunk, unmodified.
                return callback(ctx, chunk.Text())
            }),
        )
        if err != nil {
            return nil, err
        }

        return Menu{
            Theme: theme,
            Items: []MenuItem{item},
        }, nil
    })

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

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

קריאה לזרמי סטרימינג

אפשר להריץ תהליכי סטרימינג כמו תהליכים רגילים באמצעות menuSuggestionFlow.Run(ctx, "bistro"), או להפעיל אותם בסטרימינג:

streamCh, err := menuSuggestionFlow.Stream(ctx, "bistro")
if err != nil {
    log.Fatal(err)
}

for result := range streamCh {
    if result.Err != nil {
        log.Fatal("Stream error: %v", result.Err)
    }
    if result.Done {
        log.Printf("Menu with %s theme:\n", result.Output.Theme)
        for item := range result.Output.Items {
            log.Println(" - %s: %s", item.Name, item.Description)
        }
    } else {
        log.Println("Stream chunk:", result.Stream)
    }
}

הרצת תהליכים משורת הפקודה

אפשר להריץ תהליכים משורת הפקודה באמצעות הכלי Genkit CLI:

genkit flow:run menuSuggestionFlow '"French"'

בתהליכי סטרימינג, אפשר להדפיס את הפלט של הסטרימינג במסוף על ידי הוספת הדגל -s:

genkit flow:run menuSuggestionFlow '"French"' -s

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

ניפוי באגים בתהליכים

אחד מהיתרונות של אנקפסולציה של לוגיקה של AI בתוך תהליך הוא שאפשר לבדוק ולפתור באגים בתהליך בנפרד מהאפליקציה באמצעות ממשק המשתמש למפתחים של Genkit.

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

genkit start -- go run .

בכרטיסייה Run בממשק המשתמש למפתחים, אפשר להריץ כל אחד מהתהליכים שהוגדרו בפרויקט:

צילום מסך של הכלי להרצת Flow

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

פריסת תהליכים

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

שרת net/http

כדי לפרוס תהליך באמצעות כל פלטפורמת אירוח של Go, כמו Cloud Run, מגדירים את התהליך באמצעות DefineFlow() ומפעילים שרת net/http עם בורר התהליכים שסופק:

import (
    "context"
    "log"
    "net/http"

    "github.com/firebase/genkit/go/genkit"
    "github.com/firebase/genkit/go/plugins/googlegenai"
    "github.com/firebase/genkit/go/plugins/server"
)

func main() {
    ctx := context.Background()

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

    menuSuggestionFlow := genkit.DefineFlow(g, "menuSuggestionFlow",
        func(ctx context.Context, theme string) (MenuItem, error) {
            // Flow implementation...
        })

    mux := http.NewServeMux()
    mux.HandleFunc("POST /menuSuggestionFlow", genkit.Handler(menuSuggestionFlow))
    log.Fatal(server.Start(ctx, "127.0.0.1:3400", mux))
}

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

כדי להציג את כל התהליכים שהוגדרו בקוד, אפשר להשתמש ב-ListFlows():

mux := http.NewServeMux()
for _, flow := range genkit.ListFlows(g) {
    mux.HandleFunc("POST /"+flow.Name(), genkit.Handler(flow))
}
log.Fatal(server.Start(ctx, "127.0.0.1:3400", mux))

אפשר לקרוא לנקודת קצה של תהליך באמצעות בקשת POST באופן הבא:

curl -X POST "http://localhost:3400/menuSuggestionFlow" \
    -H "Content-Type: application/json" -d '{"data": "banana"}'

מסגרות שרת אחרות

אפשר גם להשתמש במסגרות שרת אחרות לפריסה של תהליכים. לדוגמה, אפשר להשתמש ב-Gin עם כמה שורות קוד בלבד:

router := gin.Default()
for _, flow := range genkit.ListFlows(g) {
    router.POST("/"+flow.Name(), func(c *gin.Context) {
        genkit.Handler(flow)(c.Writer, c.Request)
    })
}
log.Fatal(router.Run(":3400"))

למידע על פריסה בפלטפורמות ספציפיות, ראו Genkit עם Cloud Run.