AI ワークフローの定義

アプリの AI 機能のコアは生成モデルのリクエストですが、ユーザー入力を取得してモデルに渡し、モデルの出力をユーザーに表示するだけというケースはほとんどありません。通常、モデル呼び出しには前処理と後処理の手順が必要です。例:

  • モデル呼び出しとともに送信するコンテキスト情報を取得する
  • ユーザーの現在のセッションの履歴の取得(チャットアプリなど)
  • 1 つのモデルを使用して、別のモデルに渡すのに適した方法でユーザー入力を再フォーマットする
  • モデルの出力をユーザーに提示する前に、その出力の「安全性」を評価する
  • 複数のモデルの出力を組み合わせる

AI 関連のタスクを成功させるには、このワークフローのすべてのステップが連携して機能する必要があります。

Genkit では、この緊密にリンクされたロジックをフローという構造を使用して表します。フローの関数は、通常の TypeScript コードを使用して作成されますが、AI 機能の開発を容易にするための追加機能が追加されています。

  • 型安全性: Zod を使用して定義された入出力スキーマ。静的型チェックとランタイム型チェックの両方を提供します。
  • デベロッパー UI との統合: デベロッパー UI を使用して、アプリケーション コードとは別にフローをデバッグします。デベロッパー UI では、フローを実行し、フローの各ステップのトレースを表示できます。
  • 簡素化されたデプロイ: Cloud Functions for Firebase またはウェブアプリをホストできる任意のプラットフォームを使用して、フローをウェブ API エンドポイントとして直接デプロイします。

他のフレームワークの同様の機能とは異なり、Genkit のフローは軽量で目立たず、アプリに特定の抽象化を強制しません。フロー内のすべてのロジックは標準の TypeScript で記述され、フロー内のコードはフロー対応である必要はありません。

フローの定義と呼び出し

最も単純な形式のフローは、関数をラップしただけのものです。次の例では、generate() を呼び出す関数をラップしています。

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

このように generate() 呼び出しをラップするだけで、いくつかの機能が追加されます。これにより、Genkit CLI とデベロッパー UI からフローを実行できます。また、デプロイとオブザーバビリティなど、Genkit のいくつかの機能の要件でもあります(これらのトピックについては後のセクションで説明します)。

入力スキーマと出力スキーマ

Genkit フローがモデル API を直接呼び出すよりも優れている点の 1 つは、入力と出力の両方の型安全性が確保される点です。フローを定義するときに、generate() 呼び出しの出力スキーマを定義する場合と同様に、Zod を使用してフローのスキーマを定義できます。ただし、generate() とは異なり、入力スキーマも指定できます。

次の例は、最後の例を改良したもので、文字列を入力として受け取り、オブジェクトを出力するフローを定義しています。

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

フローのスキーマは、フロー内の generate() 呼び出しのスキーマと一致している必要はありません(実際、フローには generate() 呼び出しが含まれていないこともあります)。次の例は、スキーマを generate() に渡しますが、構造化出力を使用して、フローが返す単純な文字列をフォーマットします。

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

通話フロー

フローを定義したら、Node.js コードから呼び出すことができます。

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

フローへの引数は、入力スキーマ(定義されている場合)に準拠している必要があります。

出力スキーマを定義した場合、フロー レスポンスはそれに準拠します。たとえば、出力スキーマを MenuItemSchema に設定すると、フロー出力にはそのプロパティが含まれます。

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

ストリーミング フロー

フローは、generate() のストリーミング インターフェースに似たインターフェースを使用してストリーミングをサポートしています。ストリーミングは、フローで大量の出力が生成される場合に便利です。生成された出力をユーザーに提示できるため、アプリの応答性が向上します。よくある例として、チャットベースの LLM インターフェースでは、生成されたレスポンスをユーザーにストリーミングすることがよくあります。

ストリーミングをサポートするフローの例を次に示します。

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,
    };
  }
);
  • streamSchema オプションは、フロー ストリームの値の型を指定します。これは、フロー全体の出力のタイプである outputSchema と同じタイプである必要はありません。
  • streamingCallback は、streamSchema で指定された型の単一のパラメータを受け入れるコールバック関数です。フロー内でデータが利用可能になったら、この関数を呼び出してデータを出力ストリームに送信します。streamingCallback は、フローの呼び出し元がストリーミング出力をリクエストした場合にのみ定義されるため、呼び出す前に定義されていることを確認する必要があります。

上記の例では、フローによってストリーミングされる値は、フロー内の generate() 呼び出しによってストリーミングされる値に直接結合されています。多くの場合、これは事実ですが、必ずしもそうである必要はありません。フローにとって有用な頻度で、コールバックを使用してストリームに値を出力できます。

通話ストリーミング フロー

ストリーミング フローも呼び出し可能ですが、プロミスではなくレスポンス オブジェクトをすぐに返します。

const response = menuSuggestionStreamingFlow('Danube');

レスポンス オブジェクトにはストリーム プロパティがあります。このプロパティを使用すると、生成されたフローのストリーミング出力を反復処理できます。

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

ストリーミング以外のフローと同様に、フロー全体の出力を取得することもできます。

const output = await response.output;

フローのストリーミング出力は、完全な出力と同じタイプではない場合があります。ストリーミング出力は streamSchema に準拠しますが、完全な出力は outputSchema に準拠します。

コマンドラインからのフローの実行

フローは、Genkit CLI ツールを使用してコマンドラインから実行できます。

genkit flow:run menuSuggestionFlow '"French"'

ストリーミング フローの場合は、-s フラグを追加してストリーミング出力をコンソールに出力できます。

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

コマンドラインからフローを実行すると、フローのテストや、アドホックに必要なタスクを実行するフローの実行に役立ちます。たとえば、ドキュメントをベクトル データベースに取り込むフローを実行できます。

フローのデバッグ

AI ロジックをフロー内にカプセル化する利点の一つは、Genkit デベロッパー UI を使用して、アプリから独立してフローをテストおよびデバッグできることです。

デベロッパー UI を起動するには、プロジェクト ディレクトリから次のコマンドを実行します。

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

デベロッパー UI の [実行] タブから、プロジェクトで定義した任意のフローを実行できます。

Flow ランナーのスクリーンショット

フローを実行したら、[トレース表示] をクリックするか、[検査] タブで、フローの呼び出しのトレースを確認できます。

トレースビューアでは、フロー全体の実行に関する詳細と、フロー内の個々のステップの詳細を確認できます。たとえば、次のフローには複数の生成リクエストが含まれています。

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

このフローを実行すると、トレース ビューアに、出力を含む各生成リクエストの詳細が表示されます。

トレース インスペクタのスクリーンショット

フローステップ

前回の例では、各 generate() 呼び出しがトレース ビューアに個別のステップとして表示されました。Genkit の基本的なアクションは、フローの個別のステップとして表示されます。

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

上記以外のコードをトレース内に含める場合は、コードを run() 呼び出しでラップします。これは、Genkit に対応していないサードパーティ ライブラリの呼び出しや、コードの重要なセクションに対して行うことができます。

たとえば、次の 2 ステップのフローがあります。最初のステップでは、指定されていないメソッドを使用してメニューを取得し、2 番目のステップでは、generate() 呼び出しのコンテキストとしてメニューを含めます。

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

取得ステップは run() 呼び出しでラップされているため、トレース ビューアにステップとして含まれます。

トレース インスペクタで明示的に定義されたステップのスクリーンショット

フローをデプロイする

フローは、ウェブ API エンドポイントとして直接デプロイして、アプリ クライアントから呼び出すことができます。デプロイについては、他のいくつかのページで詳しく説明していますが、このセクションではデプロイ オプションの概要について説明します。

Cloud Functions for Firebase

Cloud Functions for Firebase でフローをデプロイするには、firebase プラグインを使用します。フロー定義で、defineFlowonFlow に置き換え、authPolicy を含めます。

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) => {
    // ...
  }
);

詳しくは次のページをご覧ください。

Express.js

Cloud Run などの Node.js ホスティング プラットフォームを使用してフローをデプロイするには、defineFlow() を使用してフローを定義し、startFlowServer() を呼び出します。

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

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

デフォルトでは、startFlowServer はコードベースで HTTP エンドポイントとして定義されているすべてのフロー(http://localhost:3400/menuSuggestionFlow など)を提供します。POST リクエストを使用してフローを呼び出す方法は次のとおりです。

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

必要に応じて、次のように、特定のフローリストを提供するフロー サーバーをカスタマイズできます。カスタム ポートを指定することもできます(PORT 環境変数が設定されている場合は、その環境変数を使用します)。また、CORS 設定を指定することもできます。

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: '*',
  },
});

特定のプラットフォームにデプロイする方法については、Cloud Run でデプロイする任意の Node.js プラットフォームにフローをデプロイするをご覧ください。