Der Kern der KI-Features Ihrer Anwendung sind Anfragen für generative Modelle. Es kommt jedoch selten vor, dass Sie einfach eine Nutzereingabe an das Modell übergeben und die Modellausgabe dem Nutzer zurückgeben können. Normalerweise müssen mit dem Modellaufruf Vor- und Nachverarbeitungsschritte durchgeführt werden. Beispiel:
- Kontextinformationen abrufen, die mit dem Modellaufruf gesendet werden sollen.
- Verlauf der aktuellen Sitzung des Nutzers abrufen, z. B. in einer Chat-App
- Verwendung eines Modells, um die Nutzereingabe so neu zu formatieren, dass sie an ein anderes Modell übergeben werden kann.
- Bewertung der „Sicherheit“ der Modellausgabe, bevor sie dem Nutzer präsentiert wird.
- Kombinieren der Ausgabe mehrerer Modelle.
Alle Schritte dieses Workflows müssen zusammenarbeiten, damit jede KI-bezogene Aufgabe erfolgreich ist.
In Genkit stellen Sie diese eng miteinander verknüpfte Logik mithilfe einer Konstruktion dar, die als Ablauf bezeichnet wird. Abläufe werden wie Funktionen mit normalem Go-Code geschrieben, fügen jedoch zusätzliche Funktionen hinzu, die die Entwicklung von KI-Features erleichtern:
- Typsicherheit: Eingabe- und Ausgabeschemas, die sowohl statische als auch Laufzeittypen prüfen können.
- Integration in die Entwickler-UI: Mit dieser Funktion können Sie Abläufe unabhängig vom Anwendungscode debuggen. In der Entwickler-UI können Sie Abläufe ausführen und Traces für jeden Schritt des Ablaufs ansehen.
- Vereinfachte Bereitstellung: Stellen Sie Abläufe direkt als Web-API-Endpunkte über jede Plattform bereit, auf der eine Webanwendung gehostet werden kann.
Die Abläufe von Genkit sind schlank und unaufdringlich und erzwingen keine Anwendung auf eine bestimmte Abstraktion. Die gesamte Ablauflogik wird in Standard-Go geschrieben und der Code innerhalb eines Ablaufs muss nicht den Ablauf unterstützen.
Datenflüsse definieren und aufrufen
In seiner einfachsten Form umschließt ein Ablauf einfach eine Funktion. Im folgenden Beispiel wird eine Funktion eingebunden, die Generate()
aufruft:
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
})
Wenn Sie Ihre genkit.Generate()
-Aufrufe einfach so zusammenfassen, fügen Sie einige Funktionen hinzu: Auf diese Weise können Sie den Ablauf über die Genkit-Befehlszeile und die Entwickler-UI ausführen. Dies ist eine Voraussetzung für mehrere Genkit-Features, einschließlich Bereitstellung und Beobachtbarkeit (diese Themen werden in späteren Abschnitten erläutert).
Eingabe- und Ausgabeschemas
Einer der wichtigsten Vorteile von Genkit-Abläufen gegenüber dem direkten Aufrufen einer Modell-API ist die Typsicherheit von Eingaben und Ausgaben. Beim Definieren von Abläufen können Sie Schemas genauso definieren, wie Sie das Ausgabeschema eines genkit.Generate()
-Aufrufs definieren. Im Gegensatz zu genkit.Generate()
können Sie jedoch auch ein Eingabeschema angeben.
Hier sehen Sie eine Optimierung des letzten Beispiels, in dem ein Ablauf definiert wird, der einen String als Eingabe verwendet und ein Objekt ausgibt:
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),
)
})
Das Schema eines Ablaufs muss nicht unbedingt mit dem Schema der genkit.Generate()
-Aufrufe innerhalb des Ablaufs übereinstimmen (ein Ablauf enthält möglicherweise nicht einmal genkit.Generate()
-Aufrufe). Hier ist eine Variante des Beispiels, bei dem genkit.GenerateData()
aufgerufen wird. Dabei wird die strukturierte Ausgabe jedoch verwendet, um einen einfachen String zu formatieren, den der Ablauf zurückgibt. Wie MenuItem
als Typparameter übergeben wird, entspricht dies der Übergabe der Option WithOutputType()
und dem Abrufen eines Werts dieses Typs als Antwort.
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
})
Anrufabläufe
Nachdem Sie einen Ablauf definiert haben, können Sie ihn über Ihren Go-Code aufrufen:
item, err := menuSuggestionFlow.Run(ctx, "bistro")
Das Argument für den Ablauf muss dem Eingabeschema entsprechen.
Wenn Sie ein Ausgabeschema definiert haben, entspricht die Flussantwort diesem Schema. Wenn Sie beispielsweise das Ausgabeschema auf MenuItem
festlegen, enthält die Flussausgabe ihre Attribute:
item, err := menuSuggestionFlow.Run(ctx, "bistro")
if err != nil {
log.Fatal(err)
}
log.Println(item.DishName)
log.Println(item.Description)
Streaming-Abläufe
Abläufe unterstützen Streaming über eine Schnittstelle, die der Streamingschnittstelle von genkit.Generate()
ähnelt. Streaming ist nützlich, wenn Ihr Ablauf eine große Menge an Ausgabe generiert, da Sie die Ausgabe dem Nutzer beim Generieren präsentieren können, wodurch die wahrgenommene Reaktionsfähigkeit Ihrer Anwendung verbessert wird. Ein bekanntes Beispiel streamen chatbasierte LLM-Schnittstellen häufig ihre Antworten an den Nutzer, während sie generiert werden.
Hier ein Beispiel für einen Ablauf, der Streaming unterstützt:
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
})
Der Typ string
in StreamCallback[string]
gibt den Typ der Werte an, die Ihr Fluss streamt. Dieser muss nicht unbedingt derselbe Typ sein wie der Rückgabetyp, bei dem es sich um den Typ der vollständigen Ausgabe des Ablaufs handelt (in diesem Beispiel Menu
).
In diesem Beispiel sind die vom Ablauf gestreamten Werte direkt mit den Werten gekoppelt, die vom genkit.Generate()
-Aufruf innerhalb des Ablaufs gestreamt werden.
Dies ist zwar häufig der Fall, muss es aber nicht sein: Sie können mit dem Callback Werte an den Stream ausgeben, so oft, wie dies für Ihren Ablauf sinnvoll ist.
Streaming-Abläufe aufrufen
Streaming-Datenflüsse können wie Nicht-Streaming-Datenflüsse mit menuSuggestionFlow.Run(ctx, "bistro")
ausgeführt oder gestreamt werden:
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)
}
}
Abläufe über die Befehlszeile ausführen
Sie können Abläufe über die Befehlszeile mit dem Genkit CLI-Tool ausführen:
genkit flow:run menuSuggestionFlow '"French"'
Bei Streaming-Abläufen können Sie die Streamingausgabe in der Konsole ausgeben, indem Sie das Flag -s
hinzufügen:
genkit flow:run menuSuggestionFlow '"French"' -s
Das Ausführen eines Ablaufs über die Befehlszeile ist nützlich, um ihn zu testen oder Abläufe auszuführen, die Aufgaben auf Ad-hoc-Basis ausführen, z. B. um einen Ablauf auszuführen, der ein Dokument in Ihre Vektordatenbank aufnimmt.
Debugging-Abläufe
Einer der Vorteile der Kapselung von KI-Logik in einem Ablauf besteht darin, dass Sie den Ablauf unabhängig von Ihrer App mithilfe der Genkit-Entwickler-UI testen und debuggen können.
Die Entwickler-UI setzt voraus, dass die Go-Anwendung weiterhin ausgeführt wird, auch wenn die Logik abgeschlossen ist. Wenn Sie gerade erst anfangen und Genkit nicht Teil einer umfassenderen Anwendung ist, fügen Sie select {}
als letzte Zeile von main()
hinzu, um zu verhindern, dass die Anwendung heruntergefahren wird und Sie sie in der UI überprüfen können.
Führen Sie den folgenden Befehl in Ihrem Projektverzeichnis aus, um die Entwickler-UI zu starten:
genkit start -- go run .
Auf dem Tab Ausführen der Entwickler-UI können Sie alle in Ihrem Projekt definierten Abläufe ausführen:
Nachdem Sie einen Ablauf ausgeführt haben, können Sie ein Trace des Ablaufaufrufs überprüfen. Klicken Sie dazu entweder auf Trace ansehen oder sehen Sie sich den Tab Prüfen an.
Abläufe bereitstellen
Sie können Ihre Abläufe direkt als Web-API-Endpunkte bereitstellen, die Sie dann über Ihre Anwendungsclients aufrufen können. Die Bereitstellung wird auf mehreren Seiten ausführlich erläutert. Dieser Abschnitt gibt jedoch einen kurzen Überblick über Ihre Bereitstellungsoptionen.
net/http
Server
Wenn Sie einen Ablauf mit einer Go-Hostingplattform wie Cloud Run bereitstellen möchten, definieren Sie den Ablauf mit DefineFlow()
und starten Sie einen net/http
-Server mit dem bereitgestellten Ablauf-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()
ist eine optionale Hilfsfunktion, die den Server startet und seinen Lebenszyklus verwaltet. Dazu gehört auch das Erfassen von Unterbrechungssignalen, um die lokale Entwicklung zu erleichtern. Sie können aber auch Ihre eigene Methode verwenden.
Um alle in Ihrer Codebasis definierten Abläufe bereitzustellen, können Sie ListFlows()
verwenden:
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))
Sie können einen Ablaufendpunkt mit einer POST-Anfrage so aufrufen:
curl -X POST "http://localhost:3400/menuSuggestionFlow" \
-H "Content-Type: application/json" -d '{"data": "banana"}'
Andere Server-Frameworks
Sie können zum Bereitstellen Ihrer Abläufe auch andere Server-Frameworks verwenden. Sie können beispielsweise Gin mit nur wenigen Zeilen verwenden:
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"))
Informationen zur Bereitstellung auf bestimmten Plattformen finden Sie unter Genkit mit Cloud Run.