【Genkit】GoってLLMアプリが作れるって？？？

## はじめに

かなりお久しぶりな記事になってしまいました🙇
[@o_ga09](https://x.com/o_ga09)です。

今回は、**GoでLLMアプリケーション作成 第2弾です！！！**

以前の記事はこちら！

https://qiita.com/wakuto-o-ga/items/d06eb08d166322343f73

こちらでは、LangChainの非公式ライブラリを使用してLLMを触ってみようというものでした。
結果は、普通にPython or Node.Jsでやった方が良いという結論でした笑

そこで、今回は、GoogleのFirebaseチームからGoの公式のAIアプリケーションフレームワーク **Genkit** が登場しましたので、記事にしていきたいと思います！

## GenKitとは？

公式サイト

https://firebase.google.com/products/genkit?hl=ja

リポジトリ

https://github.com/firebase/genkit

### 何を作れるの？

<details><summary>READMEから引用</summary>

>Genkit is a versatile framework, which you can use to build many different types of 
AI applications. Common use cases include: <br>
Intelligent agents: Create agents that understand user requests and perform tasks autonomously, such as personalized travel planning or itinerary generation.<br>
Example: Compass Travel Planning App<br>
Data transformation: Convert unstructured data, like natural language, into structured formats (e.g., objects, SQL queries, tables) for integration into your app or data pipeline.<br>
Example: Add Natural Language AI Data Filters with Genkit<br>
Retrieval-augmented generation: Create apps that provide accurate and contextually relevant responses by grounding generation with your own data sources, such as chatbots or question answering systems.<br>
Example: Build AI features powered by your data<br>
</details>

READMEを意訳：

genkitは、多用途なフレームワークです。多様なAIアプリケーションを作成することが可能です。

例としては、

- AIエージェント
- データ変換パイプライン
- RAGを使用したAIアプリケーション

### 誰が使うとうれしいの？

<details><summary>READMEから引用</summary>

> Genkit is built for developers seeking to add generative AI to their apps with Node.js or Go, and can run anywhere these runtimes are supported. It's designed around a plugin architecture that can work with any generative model API or vector database, with many integrations already available.
While developed by the Firebase team, Genkit can be used independently of Firebase or Google Cloud services.


</details>

READMEを意訳：

genkitは、アプリケーション開発者のために最小の努力で既存アプリケーションに生成AIの力を組み込めるように設計されています。複数のLLMモデルを使用でき、ベクトル検索データベースも使用できます。

genkitは、firebaseチームによってOSSで開発されています。

### Genkit for go とは？

genkitをGolangで使用できるフレームワークです。開発にはGoogleのGoチームも入っているため、実質Go公式フレームワークです。

リファレンス

https://pkg.go.dev/github.com/firebase/genkit/go/genkit

## LLMアプリを作る前に・・・

LangChainでAIアプリケーションを作成する際にも必要なLLMモデルの初期化パラメータを理解しましょう！

- temperature
    - `0.0`〜`2.0`の範囲で指定する
    - `1.0`未満では、確実性の高い生成をる傾向になる
    - `1.0`以上では、創造性の高い生成する傾向にある
    - 正確に情報を生成して欲しい場合は、`1.0`未満に設定する
- topP
    - 確率分布の上位部分を切り取り、その範囲内で次のトークンをサンプリングします
    - `0.0`〜`1.0`の範囲で指定する
    - `0.0`に近いほど、確実性の高い生成をる傾向になる
    - `1.0`に近いほど、創造性の高い生成する傾向にある
- topK
    - 確率分布の上位K個のトークンのみを考慮し、その中から次のトークンをサンプリングします
    - `0`以上の整数値で指定する
    - 大きくなれば、多数の学習した単語から生成するためGPUリソースが、多く使われる可能性が高くなる。また、確率が低い、不適切な単語が選択される可能性も高まります
    - 小さくなれば、限定されたトークンの中から単語を選択することになり、より保守的で確実性の高い生成を行う確率が高まる

なので、このパラメータは用途に応じて、試行錯誤する必要があります。

上記を踏まえて、今回のサンプルアプリのモデルの初期値は、以下で行います。

**バランス重視**:
- temperature=0.7

## Genkitでできること

**NodeJs, Golang 共通**

- 処理フロー（genkitでは、フローと呼びます）の定義
- ローカルでのフローの実行
- Developper Toolを介したフローの実行
- APIを使用したPOSTリクエストでのフローの実行

## 環境準備

**Requirements**

- Go1.22以上がインストール済みであること
- Node.js20以上がインストールされていること

    ```bash
    $ go version
    go version go1.23.0 darwin/arm64
    $ node -v
    v20.12.0
    ```

1. genkit CLIをインストールする

    ```bash
    $ npm i -g genkit
    ```

2.　プロジェクトを初期化する

:::note warn
genkitは、バージョン0.9から、`genkit <option>`ではなく、`npx genkit <option>`でCLIを実行するようになりました！
:::


- ランタイムとしてGoを選択する
- LLM APIのAPIキーを環境変数に設定する



    ```bash
    $ mkdir genkit-intro && cd genkit-intro
    $ npx genkit init
    ```


    ```bash
    $ export GOOGLE_GENAI_API_KEY=<your API key>
    ```

3.　サンプルで作成されたプロジェクトを実行する

「フロー」を実行する方法は３種類あります！

- Developper Toolでの実行
- ローカル(CLI)での実行
- APIでの実行

①Developper Toolでの実行

以下のコマンドを実行するとdevelopper toolが起動して簡単にLLMアプリが実行できます

```bash
$ genkit start
```

②ローカルでの実行

初期化時に生成された`main`関数を少し修正します

```diff_go
package main

import (
        "context"
        "errors"
        "fmt"
        "log"

        // Import Genkit and the Google AI plugin
        "github.com/firebase/genkit/go/ai"
        "github.com/firebase/genkit/go/genkit"
        "github.com/firebase/genkit/go/plugins/googleai"
)

func main() {
        ctx := context.Background()

        // Initialize the Google AI plugin. When you pass nil for the
        // Config parameter, the Google AI plugin will get the API key from the
        // GOOGLE_GENAI_API_KEY environment variable, which is the recommended
        // practice.
        if err := googleai.Init(ctx, nil); err != nil {
                log.Fatal(err)
        }

        // Define a simple flow that prompts an LLM to generate menu suggestions.
+       flow := genkit.DefineFlow("menuSuggestionFlow", func(ctx context.Context, input 
-               genkit.DefineFlow("menuSuggestionFlow", func(ctx context.Context, input string) (string, error) {
                // The Google AI API provides access to several generative models. Here,
                // we specify gemini-1.5-flash.
                m := googleai.Model("gemini-1.5-flash")
                if m == nil {
                        return "", errors.New("menuSuggestionFlow: failed to find model")
                }

                // Construct a request and send it to the model API (Google AI).
                resp, err := ai.Generate(ctx, m,
                        ai.WithConfig(&ai.GenerationCommonConfig{Temperature: 1}),
                        ai.WithTextPrompt(fmt.Sprintf(`Suggest an item for the menu of a %s themed restaurant`, input)))
                if err != nil {
                        return "", err
                }

                // Handle the response from the model API. In this sample, we just
                // convert it to a string. but more complicated flows might coerce the
                // response into structured output or chain the response into another
                // LLM call.
                text := resp.Text()
                return text, nil
        })

        // Initialize Genkit and start a flow server. This call must come last,
        // after all of your plug-in configuration and flow definitions. When you
        // pass a nil configuration to Init, Genkit starts a local flow server,
        // which you can interact with using the developer UI.
+      res, err := flow.Run(ctx, "Japanese")
+      if err != nil {
+	            log.Fatal(err)
+       }
+       // 結果の表示
+       fmt.Println(res)
-      if err := genkit.Init(ctx, nil); err != nil {
-               log.Fatal(err)
-       }
}
```

`flow.Run`の第二引数にLLMへ渡したい入力を指定することで、フローを実行できます。

実行

```bash
$ go run main.go
```

③ APIでの実行

初期化時に生成された`main`関数を少し修正します

```diff_go
package main

import (
        "context"
        "errors"
        "fmt"
        "log"

        // Import Genkit and the Google AI plugin
        "github.com/firebase/genkit/go/ai"
        "github.com/firebase/genkit/go/genkit"
        "github.com/firebase/genkit/go/plugins/googleai"
)

func main() {
        ctx := context.Background()

        // Initialize the Google AI plugin. When you pass nil for the
        // Config parameter, the Google AI plugin will get the API key from the
        // GOOGLE_GENAI_API_KEY environment variable, which is the recommended
        // practice.
        if err := googleai.Init(ctx, nil); err != nil {
                log.Fatal(err)
        }

        // Define a simple flow that prompts an LLM to generate menu suggestions.
+       flow := genkit.DefineFlow("menuSuggestionFlow", func(ctx context.Context, input 
-               genkit.DefineFlow("menuSuggestionFlow", func(ctx context.Context, input string) (string, error) {
                // The Google AI API provides access to several generative models. Here,
                // we specify gemini-1.5-flash.
                m := googleai.Model("gemini-1.5-flash")
                if m == nil {
                        return "", errors.New("menuSuggestionFlow: failed to find model")
                }

                // Construct a request and send it to the model API (Google AI).
                resp, err := ai.Generate(ctx, m,
                        ai.WithConfig(&ai.GenerationCommonConfig{Temperature: 1}),
                        ai.WithTextPrompt(fmt.Sprintf(`Suggest an item for the menu of a %s themed restaurant`, input)))
                if err != nil {
                        return "", err
                }

                // Handle the response from the model API. In this sample, we just
                // convert it to a string. but more complicated flows might coerce the
                // response into structured output or chain the response into another
                // LLM call.
                text := resp.Text()
                return text, nil
        })
+   	path := []string{
+          "menuSuggestionFlow",
+	    }
+
+	    mux := genkit.NewFlowServeMux(path)
+
+	    if err := http.ListenAndServe(":8080", mux); err != nil {
+		    log.Fatal(err)
+	    }
-        // Initialize Genkit and start a flow server. This call must come last,
-        // after all of your plug-in configuration and flow definitions. When you
-        // pass a nil configuration to Init, Genkit starts a local flow server,
-        // which you can interact with using the developer UI.
-        if err := genkit.Init(ctx, nil); err != nil {
-                log.Fatal(err)
-        }
}

```

`genkit.NewFlowServeMux(path)`に、定義したフロー名の文字列配列を渡してあげることで、自動でフレームワーク側の処理でAPIのエンドポイントを作成してくれます。

実行

```bash
$ go run main.go
```

動作確認

```bash
$ curl -X POST -d '{data: "input text"}' localhost:4001/menuSuggestionFlow
```

## チャットボットを作ってみる

1. `aichat`というパッケージを作ります
    
    ```diff_shell
             .
    +        ├── aichat
             ├── basicUse
             │   ├── base_flow.go
             │   └── basic_server.go
             ├── go.mod
             ├── go.sum
             └── main.go
    ```

1. `aichat`パッケージに`chat.go`というファイルを作成します

    ```diff_shell
             .
             ├── aichat
    +        │   └── chat.go
             ├── basicUse
             │   ├── base_flow.go
             │   └── basic_server.go
             ├── go.mod
             ├── go.sum
             └── main.go
    ```

1. チャットの処理を書いていきます

    <details><summary>コード全体</summary>

    ```diff_go
    package aichat
    
    import (
        "context"
        "log"
    
        "github.com/firebase/genkit/go/ai"
        "github.com/firebase/genkit/go/genkit"
        "github.com/firebase/genkit/go/plugins/dotprompt"
        "github.com/firebase/genkit/go/plugins/googleai"
        "github.com/invopop/jsonschema"
    )
    
    type PromptInput struct {
        URL string `json:"url"`
    }
    
    func Chat(ctx context.Context) error {
        // Intialize LLM API (Gemini)
        if err := googleai.Init(ctx, nil); err != nil {
            log.Fatal(err)
        }
    
        // Select Model
        model := googleai.Model("gemini-1.5-flash")
    
        // プロンプトを定義
        prompt, err := dotprompt.Define("AI Chat Assistant",
            "First, fetch this link: {{url}}. Then, summarize the content within 20 words.",
            dotprompt.Config{
                Model: model,
                // Tools: ,
                GenerationConfig: &ai.GenerationCommonConfig{
                    Temperature: 0.7,
                },
                InputSchema:  jsonschema.Reflect(PromptInput{}),
                OutputFormat: ai.OutputFormatText,
            },
        )
        if err != nil {
            return err
        }
    
        // genkitのフローを定義
        genkit.DefineFlow("summary", func(ctx context.Context, input string) (string, error) {
            resp, err := prompt.Generate(ctx,
                &dotprompt.PromptRequest{
                    Variables: &PromptInput{
                        URL: input,
                    },
                },
                nil,
            )
            if err != nil {
                return "", err
            }
            return resp.Text(), nil
        })
        return nil
    }
    ```
    </details>

1. サーバのエンドポイントとして、チャットの「フロー」を登録します

    ```diff_go
    package basicuse
    
    import (
    	"context"
    	"log"
    	"log/slog"
    	"net/http"
    	"os"
    
    	"github.com/firebase/genkit/go/genkit"
    	"github.com/o-ga09/genkit-go/aichat"
    )
    
    func BasicServer(ctx context.Context) {
    	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{}))
    	slog.SetDefault(logger)
    
    	// genkit.NewFlowServeMuxにpathを渡すと、エンドポイントとして登録される
    	path := []string{
    +		"summary", // chat.goで定義したフロー名
    	}
    
    +	// チャットフローを呼び出す
    +	if err := aichat.Chat(ctx); err != nil {
    +		log.Fatal(err)
    +	}
    +	// gnkitでAPIサーバーにフローを登録する
    +	mux := genkit.NewFlowServeMux(path)
    
    	// APIサーバーを起動する
    	if err := http.ListenAndServe(":8080", mux); err != nil {
    		log.Fatal(err)
    	}
    }
    ```

1. APIサーバーを起動します

    ```bash
    $ go run .
    ```

### 動作確認

- Postmanでリクエスト

![スクリーンショット 2024-11-23 10.32.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/07d23f39-b83a-1cfe-854d-80124069704c.png)
    
## RAG (Retrieve Argumented Generation - 検索拡張生成)アプリを作ってみる


公式ドキュメント

さすがのGoogle様！！！ GoでRAGを実装するドキュメントを出してくれています！
ありがとうございます！！！

:::note warn

α版ですのでご注意ください！
2024.11.23時点

:::

https://firebase.google.com/docs/genkit-go/rag?_gl=1*ej2vy*_up*MQ..*_ga*MTgyODE3MjAwMy4xNzMyMzI1NjE4*_ga_CW55HF8NVT*MTczMjMyNTYxOC4xLjAuMTczMjMyNjE2My4wLjAuMA..&hl=ja

### インデクサーとは？

> インデックスは、特定のクエリに関連するドキュメントをすばやく検索できるように、ドキュメントを追跡する役割を果たします。ほとんどの場合、これはベクトル データベースを使用して実現されます。ベクトル データベースは、エンベディングと呼ばれる多次元ベクトルを使用してドキュメントのインデックスを作成します。テキスト エンベディングは、テキストの文章で表現される概念を（不透明な形で）表現したものであり、専用の ML モデルを使用して生成されます。ベクトル データベースは、そのエンベディングを使用してテキストのインデックスを作成することで、概念的に関連するテキストをクラスタ化し、新しいテキスト文字列（クエリ）に関連するドキュメントを検索できます。
生成のためにドキュメントを検索するには、ドキュメントをドキュメント インデックスに取り込む必要があります。一般的な取り込みフローは次のとおりです。
関連する部分のみがプロンプトの強化に使用されるように、大きなドキュメントを小さなドキュメントに分割します（チャンク化）。多くの LLM はコンテキスト ウィンドウが限られているため、プロンプトにドキュメント全体を含めることは現実的ではないことが、これを行う理由です。
Genkit には組み込みのチャンク ライブラリはありませんが、Genkit と互換性のあるオープンソース ライブラリがあります。
チャンクごとにエンベディングを生成します。使用しているデータベースに応じて、エンベディング生成モデルで明示的に行うことも、データベースが提供するエンベディング生成ツールを使用することもできます。
テキストチャンクとそのインデックスをデータベースに追加します。
安定したデータソースを使用している場合は、取り込みフローを頻繁に実行しないか、1 回だけ実行することをおすすめします。一方、頻繁に変更されるデータを処理する場合は、取り込みフローを継続


**つまりどういう事だって？？？**

- 検索に使用するインデックス、索引
- イメージは、普通にRDBのインデックスのと同じ
- ただ、RAGでは、**ベクトルデータベース**を使用する

### エンベデッドとは？

>エンベッダーは、コンテンツ（テキスト、画像、音声など）を受け取り、元のコンテンツの意味をエンコードする数値ベクトルを作成する関数です。前述のように、エンベッダーはインデックス作成プロセスの一部として活用されますが、インデックスなしでエンベディングを作成するために独立に使用することもできます。

**つまりどういう事だって？？？**

- コンテンツを**ベクトル数値に変換**してデータベースやファイルに保存する
- ただし、インデックス、索引は作成されないので検索に使用するにはあまり向いていない

### リトリーバーとは？

>リトリーバーは、あらゆる種類のドキュメント検索に関連するロジックをカプセル化するコンセプトです。検索の最も一般的なケースには、通常、ベクトルストアからの検索が含まれますが、Genkit では、データを返す任意の関数をリトリーバーにすることができます。
リトリーバーの作成には、提供されている実装のいずれかを使用するか、独自に作成します。

**つまりどういう事だって？？？**

- **ベクトルストアから検索**するロジック
- この結果をLLMに投げて、解答を生成する

### 参考実装

https://github.com/firebase/genkit/blob/main/go/samples/rag/main.go

ローカルで動かしてみた！

LLMへ事前に仕込んだ情報

```bash:LLMへ事前に仕込んだ情報
- Paris is the capital of France
- USA is the largest importer of coffee
- Water exists in 3 states - solid, liquid and gas
```

```bash:プロンプト
You're a helpful agent that answers the user's common questions based on the context provided.

Here is the user's query: {{query}}

Here is the context you should use: {{context}}

Please provide the best answer you can.
```

`{{query}}`と`{{context}}`にユーザーからの入力を指定する

**結果**

- 質問：What is the capital of UK?

![スクリーンショット 2024-11-27 8.12.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/353bc62e-aea3-4ee9-7777-6bc2f6db360d.png)

- 質問：What is the capital of France?

![スクリーンショット 2024-11-27 8.12.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/26d8cd51-0174-7b96-6c46-a31b33b99627.png)

ちゃんとできた！情報を渡している、フランスの首都は？と聞いたら、パリって答えてくれて、イギリスの首都は？って聞いたら、わかりませんと答えてくれました！

## まとめ

今回、Googleから2024年のGoogle IOにて発表されたgenkitを使ってみました！genkit for Goに関しては、α版なので、破壊的変更が多く、この記事が、お役に立てるかわかりませんが、概要、チュートリアルとして参考にしていただければと思います！

Pythonで実装できなくて代わりにGoで実装できるLangChainと思っていただければいいのではないでしょうか。(Node.jsで実装できるのは共通)

ですが、genkit単体でAPIサーバまで含められる点はかなりアドバンテージが大きいと思っていて、一気通貫で、素早くビジネスを加速させたい場合にかなり有用だと思いました。

最後に、genkitは、OSSで開発されていてGitHub Star ⭐️ も **779**とかなので、よければ、スターをぽちっとお願いいたします！

## 参考

https://firebase.google.com/products/genkit?hl=ja

https://github.com/firebase/genkit

https://developers.google.com/solutions/compass?hl=ja

https://firebase.google.com/docs/genkit?hl=ja