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

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

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

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

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

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

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

הגדרה ותהליכים

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

menuSuggestionFlow := genkit.DefineFlow(g, "menuSuggestionFlow",
    func(ctx context.Context, theme string) (string, error) {
        resp, err := genkit.Generate(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() כמו זו מוסיפה פונקציונליות מסוימת: הפעולה הזו מאפשרת להריץ את התהליך מ-Genkit CLI ומממשק המשתמש למפתחים, והיא דרישה לכמה מהתכונות של Genkit, כולל פריסה וניראות (observability) (בהמשך מתוארות הנושאים האלה).

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

אחד היתרונות החשובים ביותר של תהליכי 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.GenerateData(), אבל משתמשת בפלט המובנה כדי לעצב מחרוזת פשוטה שחוזרת על עצמה. שימו לב איך אנחנו מעבירים את MenuItem כפרמטר מסוג. הפעולה הזו מקבילה להעברת האפשרות WithOutputType() ולקבלת ערך מהסוג הזה בתגובה.

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.

ממשק המשתמש למפתחים מסתמך על כך שאפליקציית Go תמשיך לפעול, גם אם הלוגיקה הושלמה. אם זו רק ההתחלה, ו-Genkit לא נכלל באפליקציה רחבה יותר, צריך להוסיף את select {} כשורה האחרונה של main() כדי למנוע את השבתת האפליקציה כדי שתוכלו לבדוק אותה בממשק המשתמש.

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

genkit start -- go run .

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

צילום מסך של דוח הזרימה

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

פריסה של תהליכי עבודה

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

שרת net/http

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

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() היא פונקציית עזר אופציונלית שמפעילה את השרת ומנהלת את מחזור החיים שלו, כולל תיעוד של אותות הפרעות כדי להקל על הפיתוח המקומי. אבל אתם יכולים להשתמש בשיטה משלכם.

כדי למלא את כל התהליכים שהוגדרו ב-codebase, אפשר להשתמש ב-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))

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

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

frameworks אחרות של שרתים

תוכלו גם להשתמש ב-frameworks אחרות של שרת כדי לפרוס את התהליכים. לדוגמה, אפשר להשתמש ב-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.