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 abrufen, die mit dem Modellaufruf gesendet werden
  • 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
  • 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 mit regulärem TypeScript-Code geschrieben, bieten aber zusätzliche Funktionen, die die Entwicklung von KI-Funktionen erleichtern sollen:

  • Typsicherheit: Mit Zod definierte Eingabe- und Ausgabeschemata, die sowohl statische als auch Laufzeittypprüfungen bieten
  • Integration in die Entwickleroberfläche: Mit der Entwickleroberfläche 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.
  • Einfache Bereitstellung: Sie können Abläufe direkt als Web-API-Endpunkte mit Cloud Functions for Firebase oder einer anderen Plattform bereitstellen, die eine Webanwendung hosten kann.

Im Gegensatz zu ähnlichen Funktionen in anderen Frameworks sind die Abläufe von Genkit schlanker und unaufdringlicher und zwingen Ihre App nicht dazu, einer bestimmten Abstraktion zu entsprechen. Die gesamte Logik des Ablaufs ist in Standard-TypeScript 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 generate() aufruft:

export const menuSuggestionFlow = ai.defineFlow(
  {
    name: 'menuSuggestionFlow',
  },
  async (restaurantTheme) => {
    const { text } = await ai.generate({
      model: gemini15Flash,
      prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`,
    });
    return text;
  }
);

Durch das Einfügen dieser Funktion in Ihre generate()-Aufrufe können Sie einige Funktionen hinzufügen: So können Sie den Ablauf über die Genkit-Befehlszeile und die Entwickler-UI 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. Wenn Sie Abläufe definieren, können Sie mit Zod Schemas für sie definieren, ähnlich wie Sie das Ausgabeschema eines generate()-Aufrufs definieren. Im Gegensatz zu 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:

const MenuItemSchema = z.object({
  dishname: z.string(),
  description: z.string(),
});

export const menuSuggestionFlowWithSchema = ai.defineFlow(
  {
    name: 'menuSuggestionFlow',
    inputSchema: z.string(),
    outputSchema: MenuItemSchema,
  },
  async (restaurantTheme) => {
    const { output } = await ai.generate({
      model: gemini15Flash,
      prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`,
      output: { schema: MenuItemSchema },
    });
    if (output == null) {
      throw new Error("Response doesn't satisfy schema.");
    }
    return output;
  }
);

Das Schema eines Ablaufs muss nicht unbedingt mit dem Schema der generate()-Aufrufe im Ablauf übereinstimmen. Tatsächlich enthält ein Ablauf möglicherweise gar keine generate()-Aufrufe. Hier ist eine Variante des Beispiels, bei der ein Schema an generate() übergeben wird, aber die strukturierte Ausgabe verwendet wird, um einen einfachen String zu formatieren, der vom Ablauf zurückgegeben wird.

export const menuSuggestionFlowMarkdown = ai.defineFlow(
  {
    name: 'menuSuggestionFlow',
    inputSchema: z.string(),
    outputSchema: z.string(),
  },
  async (restaurantTheme) => {
    const { output } = await ai.generate({
      model: gemini15Flash,
      prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`,
      output: { schema: MenuItemSchema },
    });
    if (output == null) {
      throw new Error("Response doesn't satisfy schema.");
    }
    return `**${output.dishname}**: ${output.description}`;
  }
);

Anrufabläufe

Nachdem Sie einen Ablauf definiert haben, können Sie ihn über Ihren Node.js-Code aufrufen:

const { text } = await menuSuggestionFlow('bistro');

Das Argument für den Ablauf muss dem Eingabeschema entsprechen, falls Sie eines definiert haben.

Wenn Sie ein Ausgabeschema definiert haben, entspricht die Ablaufantwort diesem. Wenn Sie beispielsweise das Ausgabeschema auf MenuItemSchema festlegen, enthält die Ablaufausgabe die zugehörigen Eigenschaften:

const { dishname, description } =
  await menuSuggestionFlowWithSchema('bistro');

Streaming-Streams

Flows unterstützen das Streaming über eine Oberfläche, die der Streamingoberfläche von 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:

export const menuSuggestionStreamingFlow = ai.defineStreamingFlow(
  {
    name: 'menuSuggestionFlow',
    inputSchema: z.string(),
    streamSchema: z.string(),
    outputSchema: z.object({ theme: z.string(), menuItem: z.string() }),
  },
  async (restaurantTheme, streamingCallback) => {
    const response = await ai.generateStream({
      model: gemini15Flash,
      prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`,
    });

    if (streamingCallback) {
      for await (const chunk of response.stream) {
        // Here, you could process the chunk in some way before sending it to
        // the output stream via streamingCallback(). In this example, we output
        // the text of the chunk, unmodified.
        streamingCallback(chunk.text);
      }
    }

    return {
      theme: restaurantTheme,
      menuItem: (await response.response).text,
    };
  }
);
  • Mit der Option streamSchema geben Sie den Werttyp für Ihre Ablaufstreams an. Dieser muss nicht unbedingt mit dem Typ von outputSchema übereinstimmen, dem Typ der vollständigen Ausgabe des Flows.
  • streamingCallback ist eine Callback-Funktion, die einen einzelnen Parameter vom Typ streamSchema annimmt. Wenn Daten in Ihrem Fluss verfügbar sind, senden Sie sie durch Aufrufen dieser Funktion an den Ausgabestream. Hinweis: streamingCallback ist nur definiert, wenn der Aufrufer deines Flows eine Streamingausgabe angefordert hat. Du musst also prüfen, ob sie definiert ist, bevor du sie aufrufst.

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

Anruf-Streamingabläufe

Streaming-Abläufe können auch aufgerufen werden, geben aber sofort ein Antwortobjekt zurück, anstatt ein Versprechen:

const response = menuSuggestionStreamingFlow('Danube');

Das Antwortobjekt hat eine Stream-Eigenschaft, mit der Sie die Streamingausgabe des Ablaufs iterieren können, während sie generiert wird:

for await (const chunk of response.stream) {
  console.log('chunk', chunk);
}

Sie können auch die vollständige Ausgabe des Ablaufs abrufen, wie bei einem nicht gestreamten Ablauf:

const output = await response.output;

Die Streamingausgabe eines Flows hat möglicherweise nicht denselben Typ wie die vollständige Ausgabe. Die Streamingausgabe entspricht streamSchema, während die vollständige Ausgabe outputSchema entspricht.

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.

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

genkit start -- tsx --watch src/your-code.ts

Auf dem Tab Ausführen der Entwickler-UI 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.

In der Trace-Ansicht sehen Sie Details zur Ausführung des gesamten Ablaufs sowie Details zu den einzelnen Schritten innerhalb des Ablaufs. Betrachten Sie beispielsweise den folgenden Ablauf, der mehrere Generierungsanfragen enthält:

const PrixFixeMenuSchema = z.object({
  starter: z.string(),
  soup: z.string(),
  main: z.string(),
  dessert: z.string(),
});

export const complexMenuSuggestionFlow = ai.defineFlow(
  {
    name: 'complexMenuSuggestionFlow',
    inputSchema: z.string(),
    outputSchema: PrixFixeMenuSchema,
  },
  async (theme: string): Promise<z.infer<typeof PrixFixeMenuSchema>> => {
    const chat = ai.chat({ model: gemini15Flash });
    await chat.send('What makes a good prix fixe menu?');
    await chat.send(
      'What are some ingredients, seasonings, and cooking techniques that ' +
        `would work for a ${theme} themed menu?`
    );
    const { output } = await chat.send({
      prompt:
        `Based on our discussion, invent a prix fixe menu for a ${theme} ` +
        'themed restaurant.',
      output: {
        schema: PrixFixeMenuSchema,
      },
    });
    if (!output) {
      throw new Error('No data generated.');
    }
    return output;
  }
);

Wenn Sie diesen Ablauf ausführen, werden in der Ablaufanzeige Details zu jeder Generierungsanfrage einschließlich der Ausgabe angezeigt:

Screenshot des Trace-Inspectors

Ablaufschritte

Im letzten Beispiel haben Sie gesehen, dass jeder generate()-Aufruf als separater Schritt in der Trace-Ansicht angezeigt wurde. Jede der grundlegenden Aktionen von Genkit wird als separater Schritt eines Ablaufs angezeigt:

  • generate()
  • Chat.send()
  • embed()
  • index()
  • retrieve()

Wenn Sie in Ihren Traces Code einfügen möchten, der sich von dem oben genannten unterscheidet, können Sie den Code in einen run()-Aufruf einschließen. Dies kann für Aufrufe von Drittanbieterbibliotheken erfolgen, die nicht Genkit-kompatibel sind, oder für kritische Codeabschnitte.

Hier ist beispielsweise ein Ablauf mit zwei Schritten: Im ersten Schritt wird ein Menü mit einer nicht näher bezeichneten Methode abgerufen und im zweiten Schritt wird das Menü als Kontext für einen generate()-Aufruf verwendet.

export const menuQuestionFlow = ai.defineFlow(
  {
    name: 'menuQuestionFlow',
    inputSchema: z.string(),
    outputSchema: z.string(),
  },
  async (input: string): Promise<string> => {
    const menu = await run('retrieve-daily-menu', async (): Promise<string> => {
      // Retrieve today's menu. (This could be a database access or simply
      // fetching the menu from your website.)

      // ...

      return menu;
    });
    const { text } = await ai.generate({
      model: gemini15Flash,
      system: "Help the user answer questions about today's menu.",
      prompt: input,
      docs: [{ content: [{ text: menu }] }],
    });
    return text;
  }
);

Da der Abrufschritt in einen run()-Aufruf verpackt ist, wird er in der Trace-Ansicht als Schritt aufgeführt:

Screenshot eines explizit definierten Schritts im Trace-Inspector

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 erhalten Sie jedoch eine kurze Übersicht über Ihre Bereitstellungsoptionen.

Cloud Functions für Firebase

Wenn Sie Workflows mit Cloud Functions für Firebase bereitstellen möchten, verwenden Sie das firebase-Plug-in. Ersetzen Sie in Ihren Ablaufdefinitionen defineFlow durch onFlow und fügen Sie eine authPolicy hinzu.

import { firebaseAuth } from '@genkit-ai/firebase/auth';
import { onFlow } from '@genkit-ai/firebase/functions';

export const menuSuggestion = onFlow(
  ai,
  {
    name: 'menuSuggestionFlow',
    authPolicy: firebaseAuth((user) => {
      if (!user.email_verified) {
        throw new Error('Verified email required to run flow');
      }
    }),
  },
  async (restaurantTheme) => {
    // ...
  }
);

Weitere Informationen finden Sie auf den folgenden Seiten:

Express.js

Wenn Sie Workflows mit einer beliebigen Node.js-Hostingplattform wie Cloud Run bereitstellen möchten, definieren Sie Ihre Workflows mit defineFlow() und rufen Sie dann startFlowServer() auf:

export const menuSuggestionFlow = ai.defineFlow(
  {
    name: 'menuSuggestionFlow',
  },
  async (restaurantTheme) => {
    // ...
  }
);

ai.startFlowServer({
  flows: [menuSuggestionFlow],
});

Standardmäßig werden über startFlowServer alle in Ihrer Codebasis definierten Workflows als HTTP-Endpunkte bereitgestellt (z. B. http://localhost:3400/menuSuggestionFlow). Sie können einen Workflow mit einer POST-Anfrage aufrufen:

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

Bei Bedarf können Sie den Flows-Server so anpassen, dass eine bestimmte Liste von Flows bereitgestellt wird, wie unten gezeigt. Sie können auch einen benutzerdefinierten Port angeben (falls festgelegt, wird die Umgebungsvariable PORT verwendet) oder CORS-Einstellungen angeben.

export const flowA = ai.defineFlow({ name: 'flowA' }, async (subject) => {
  // ...
});

export const flowB = ai.defineFlow({ name: 'flowB' }, async (subject) => {
  // ...
});

ai.startFlowServer({
  flows: [flowB],
  port: 4567,
  cors: {
    origin: '*',
  },
});

Informationen zur Bereitstellung auf bestimmten Plattformen finden Sie unter Mit Cloud Run bereitstellen und Auf einer beliebigen Node.js-Plattform bereitstellen.