AI ワークフローの定義

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

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

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

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

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

Genkit のフローは軽量で邪魔にならず、アプリに特定の抽象化に適合するよう強制しません。フローのロジックはすべて標準の Go で記述されており、フロー内のコードはフローを念頭に置く必要はありません。

フローの定義と呼び出し

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

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

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

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

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.Generate() に渡す例のバリエーションです。この例では、構造化出力を使用して、フローから返される単純な文字列をフォーマットします。

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

StreamCallback[string]string 型は、フロー ストリームの値の型を指定します。これは、戻り値の型(この例では Menu)と同じ型である必要はありません。これは、フロー全体の出力の型です。

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

ストリーミング フローの呼び出し

ストリーミング フローは、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 デベロッパー UI を使用して、アプリから独立してフローをテストおよびデバッグできることです。

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

genkit start -- go run .

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

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

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

フローをデプロイする

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

net/http サーバー

Cloud Run などの Go ホスティング プラットフォームを使用してフローをデプロイするには、DefineFlow() を使用してフローを定義し、提供されたフロー ハンドラを使用して net/http サーバーを起動します。

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() は、サーバーを起動してライフサイクルを管理するオプションのヘルパー関数です。ローカル開発を容易にするために割り込みシグナルをキャプチャすることもできますが、独自の方法を使用することもできます。

コードベースで定義されたすべてのフローを処理するには、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))

次のように POST リクエストを使用してフロー エンドポイントを呼び出すことができます。

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

その他のサーバー フレームワーク

他のサーバー フレームワークを使用してフローをデプロイすることもできます。たとえば、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"))

特定のプラットフォームへのデプロイについては、Cloud Run を使用した Genkit をご覧ください。