KI-Workflows definieren

Der Kern der KI-Funktionen Ihrer App sind Anfragen an generative Modelle. Es ist jedoch selten möglich, die Nutzereingabe einfach an das Modell weiterzuleiten und die Modellausgabe dem Nutzer anzuzeigen. Normalerweise sind Vor- und Nachverarbeitungsschritte erforderlich, die dem Modellaufruf vorausgehen und folgen. Beispiel:

  • Kontextinformationen werden abgerufen, um sie mit dem Modellaufruf zu senden.
  • Abrufen des Verlaufs der aktuellen Sitzung des Nutzers, z. B. in einer Chat-App.
  • Mit einem Modell die Nutzereingabe so umformatieren, dass sie an ein anderes Modell übergeben werden kann.
  • Die „Sicherheit“ der Ausgabe eines Modells bewerten, bevor sie dem Nutzer präsentiert wird.
  • Die Ausgabe mehrerer Modelle kombinieren.

Alle Schritte dieses Workflows müssen zusammenarbeiten, damit eine KI-bezogene Aufgabe erfolgreich abgeschlossen werden kann.

In Genkit wird diese eng verknüpfte Logik mit einem Konstrukt namens „Flow“ dargestellt. Flows werden wie Funktionen geschrieben, mit gewöhnlichem Go-Code. Sie bieten jedoch zusätzliche Funktionen, die die Entwicklung von KI-Funktionen erleichtern sollen:

  • Typsicherheit: Eingabe- und Ausgabeschemata, die sowohl statische als auch Laufzeittypprüfungen bieten.
  • Integration in die Entwickler-UI: Mit der Entwickler-UI können Sie Abläufe unabhängig von Ihrem Anwendungscode debuggen. In der Entwickler-Benutzeroberfläche können Sie Abläufe ausführen und sich für jeden Schritt des Ablaufs Traces ansehen.
  • Vereinfachte Bereitstellung: Sie können Workflows direkt als Web API-Endpunkte bereitstellen, indem Sie eine beliebige Plattform verwenden, auf der eine Webanwendung gehostet werden kann.

Die Abläufe von Genkit sind schlanker und unaufdringlicher und zwingen Ihre App nicht dazu, einer bestimmten Abstraktion zu entsprechen. Die gesamte Logik des Ablaufs ist in Standard-Go geschrieben und der Code in einem Ablauf muss nicht flussbewusst sein.

Abläufe definieren und aufrufen

In seiner einfachsten Form umschließt ein Ablauf nur eine Funktion. Im folgenden Beispiel wird eine Funktion umschlossen, die GenerateData() aufruft:

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
    })

Durch das Einfügen dieser Funktion in Ihre genkit.Generate()-Aufrufe können Sie den Ablauf sowohl über die Genkit-Befehlszeile als auch über die Entwickler-Benutzeroberfläche ausführen. Dies ist eine Voraussetzung für mehrere Genkit-Funktionen, einschließlich Bereitstellung und Beobachtbarkeit. Diese Themen werden in den folgenden Abschnitten behandelt.

Eingabe- und Ausgabeschemas

Einer der wichtigsten Vorteile von Genkit-Abläufen gegenüber dem direkten Aufruf einer Modell-API ist die Typsicherheit sowohl von Eingaben als auch von Ausgaben. Beim Definieren von Abläufen können Sie Schemas ähnlich wie das Ausgabeschema eines genkit.Generate()-Aufrufs definieren. Im Gegensatz zu genkit.Generate() können Sie jedoch auch ein Eingabeschema angeben.

Hier ist eine Verfeinerung des letzten Beispiels, bei dem ein Ablauf definiert wird, der einen String als Eingabe nimmt und ein Objekt als Ausgabe liefert:

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 im Ablauf übereinstimmen. Tatsächlich enthält ein Ablauf möglicherweise gar keine genkit.Generate()-Aufrufe. Hier ist eine Variante des Beispiels, bei der ein Schema an genkit.Generate() übergeben wird, aber die strukturierte Ausgabe verwendet wird, um einen einfachen String zu formatieren, der vom Ablauf zurückgegeben wird.

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 aus Ihrem 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 Ablaufantwort diesem. Wenn Sie beispielsweise das Ausgabeschema auf MenuItem festlegen, enthält die Ablaufausgabe die zugehörigen Eigenschaften:

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

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

Streaming-Streams

Flows unterstützen das Streaming über eine Benutzeroberfläche, die der Streamingoberfläche von genkit.Generate() ähnelt. Das Streaming ist nützlich, wenn Ihr Flow eine große Menge an Ausgabe generiert, da Sie die Ausgabe dem Nutzer präsentieren können, während sie generiert wird. Dies verbessert die wahrgenommene Reaktionsfähigkeit Ihrer App. Ein bekanntes Beispiel: Chatbasierte LLM-Benutzeroberflächen streamen ihre Antworten oft direkt 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 string-Typ in StreamCallback[string] gibt den Werttyp Ihrer Datenstreams an. Dieser muss nicht unbedingt mit dem Rückgabetyp übereinstimmen, d. h. dem Typ der gesamten Ausgabe des Ablaufs (Menu in diesem Beispiel).

In diesem Beispiel sind die vom Ablauf gestreamten Werte direkt mit den vom genkit.Generate()-Aufruf innerhalb des Ablaufs gestreamten Werten verknüpft. Das ist zwar oft der Fall, muss aber nicht so sein: Sie können Werte mit dem Rückruf so oft in den Stream ausgeben, wie es für Ihren Ablauf sinnvoll ist.

Anruf-Streamingabläufe

Streaming-Abläufe können wie nicht-streamingfähige Abläufe 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

Mit dem Genkit-Befehlszeilentool können Sie Workflows über die Befehlszeile ausführen:

genkit flow:run menuSuggestionFlow '"French"'

Bei Streaming-Abläufen können Sie die Streamingausgabe in der Console ausgeben, indem Sie das Flag -s hinzufügen:

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

Das Ausführen eines Workflows über die Befehlszeile ist nützlich, um einen Workflow zu testen oder Workflows auszuführen, die Aufgaben ausführen, die ad hoc erforderlich sind, z. B. einen Workflow, der ein Dokument in Ihre Vektordatenbank aufnimmt.

Debugging-Abläufe

Einer der Vorteile der Kapselung der KI-Logik in einem Flow besteht darin, dass Sie den Flow mithilfe der Genkit-Entwickler-UI unabhängig von Ihrer App testen und debuggen können.

Die Entwickleroberfläche setzt voraus, dass die Go-App weiter ausgeführt wird, auch wenn die Logik abgeschlossen ist. Wenn Sie gerade erst anfangen und Genkit nicht Teil einer größeren App ist, fügen Sie select {} als letzte Zeile von main() hinzu, damit die App nicht heruntergefahren wird und Sie sie in der Benutzeroberfläche prüfen können.

Führen Sie den folgenden Befehl in Ihrem Projektverzeichnis aus, um die Entwickler-Benutzeroberfläche zu starten:

genkit start -- go run .

Auf dem Tab Ausführen der Entwickleroberfläche können Sie jeden der in Ihrem Projekt definierten Abläufe ausführen:

Screenshot des Ablaufauslösers

Nachdem Sie einen Ablauf ausgeführt haben, können Sie sich einen Ablauf-Trace ansehen. Klicken Sie dazu auf Trace ansehen oder rufen Sie den Tab Prüfen auf.

Abläufe bereitstellen

Sie können Ihre Abläufe direkt als Web-API-Endpunkte bereitstellen, die Sie von Ihren App-Clients aus aufrufen können. Die Bereitstellung wird auf mehreren anderen Seiten ausführlich behandelt. In diesem Abschnitt finden Sie jedoch eine kurze Übersicht über Ihre Bereitstellungsoptionen.

net/http Server

Wenn Sie einen Workflow mit einer beliebigen Go-Hostingplattform wie Cloud Run bereitstellen möchten, definieren Sie den Workflow mit DefineFlow() und starten Sie einen net/http-Server mit dem bereitgestellten Workflow-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, einschließlich der Erfassung von Unterbrechungssignalen zur Vereinfachung der lokalen Entwicklung. Sie können jedoch auch eine eigene Methode verwenden.

Wenn Sie alle in Ihrer Codebasis definierten Aufrufabfolgen ausliefern möchten, 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))

So rufen Sie einen Ablaufendpunkt mit einer POST-Anfrage auf:

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

Andere Server-Frameworks

Sie können auch andere Server-Frameworks zum Bereitstellen Ihrer Abläufe verwenden. So können Sie 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 zum Bereitstellen auf bestimmten Plattformen finden Sie unter Genkit mit Cloud Run verwenden.