GoでLangChainとChatGPTを使用する


## はじめに

2024年11月1日現在、Go言語とLangChain、ChatGPTを組み合わせた日本語文献がほとんど見当たりませんでした。（僕の検索力の問題で、たぶんどこかにはきちんとある。）

そこで、今回はこの組み合わせを用いたシンプルな実装を紹介します。（紹介する相手：将来の俺）

system_contentでAIに役割を教え、それに対してuser_contentでuserからの入力を入れられさえすれば、ほぼ自由にAIを使うことができます。今回のゴールはそれです。

## 必要な事前準備

このプロジェクトを構築するために、以下のパッケージをCLIでインストールしてください。

```bash
go get github.com/joho/godotenv
go get github.com/tmc/langchaingo/llms
go get github.com/tmc/langchaingo/llms/openai
```

また、`.env`ファイルに以下のように`OPENAI_API_KEY`を記載する必要があります。

```
OPENAI_API_KEY=あなたのAPIキー
```

これにより、APIキーがコード内で安全に読み込まれるようになります。

## プロジェクトの概要

このプロジェクトは、ユーザーからの入力を受け取り、それをOpenAIのChatGPT APIに送り、生成された応答を返すシンプルなWebアプリケーションです。ディレクトリ構成は以下のようになっています。

```
backend/
├── main.go
├── userinput/
│   └── userinput.go
└── sendmsg/
    └── sendmsg.go
```

各ファイルの役割について詳しく見ていきます。

## `userinput/userinput.go`

このファイルはユーザーからのリクエストを受け取り、JSON形式で送られてきた`user_content`を抽出するためのパッケージです。`POST`リクエストのみを受け付け、JSON形式のリクエストボディをパースして`user_content`を返します。

```go
// ユーザーから受け取ったuser_contentを文字列として返すためのパッケージ
package userinput

import (
    "encoding/json"
    "net/http"
)

// POSTリクエストのボディを保持する構造体
type RequestBody struct {
    UserContent string `json:"user_content"` // JSONキー"user_content"に対応
}

// ユーザー入力を処理するハンドラ関数
func Handler(w http.ResponseWriter, r *http.Request) string {
    var requestBody RequestBody // ユーザーからの入力を保持する構造体

    // リクエストメソッドがPOSTかどうかを確認
    if r.Method != http.MethodPost {
        // POSTでなければエラーメッセージを返す
        http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
        return ""
    }

    // リクエストボディをデコードし、エラーチェック
    decoder := json.NewDecoder(r.Body)
    err := decoder.Decode(&requestBody)
    if err != nil {
        // JSONが無効な場合にエラーメッセージを返す
        http.Error(w, "Invalid JSON body", http.StatusBadRequest)
        return ""
    }

    // user_contentの存在と空文字チェック
    if requestBody.UserContent == "" {
        // 空のuser_contentならエラーメッセージを返す
        http.Error(w, "Missing user_content key, or value of user_content is empty", http.StatusBadRequest)
        return ""
    }

    // 正常な場合はuser_contentを返す
    return requestBody.UserContent
}
```


## `sendmsg/sendmsg.go`

このパッケージはLangChainを用いてChatGPT APIにリクエストを送り、応答を取得するためのものです。環境変数からAPIキーを読み込み、LangChainを使ってChatGPTにプロンプトを送信します。

```go
package sendmsg

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/joho/godotenv"
    "github.com/tmc/langchaingo/llms"
    "github.com/tmc/langchaingo/llms/openai"
)

// ChatGPTリクエスト用のパラメータを保持する構造体
type Params struct {
    SystemContent string // システム側の指示
    UserContent   string // ユーザーからの入力
    Temperature   float64 // 応答のランダム性を制御
}

// 環境変数を読み込む関数
func LoadEnv() {
    err := godotenv.Load(".env") // .envファイルを読み込む
    if err != nil {
        log.Fatalf(".envファイルの読み込みエラー: %v", err)
    }
}

// ChatGPT APIにリクエストを送信し、応答を取得する関数
func ChatGPTRequest(params Params) (string, error) {
    LoadEnv() // 環境変数をロード

    // APIキーを取得
    apiKey := os.Getenv("OPENAI_API_KEY")
    if apiKey == "" {
        return "", fmt.Errorf("環境変数にAPIキーが見つかりません")
    }

    // OpenAIクライアントを初期化
    llm, err := openai.New(openai.WithModel("gpt-4o"))
    if err != nil {
        return "", fmt.Errorf("OpenAIクライアントの作成に失敗: %v", err)
    }

    // プロンプトを生成
    prompt := fmt.Sprintf("System: %s\nUser: %s", params.SystemContent, params.UserContent)

    // ChatGPTにリクエストを送信し、応答を取得
    completion, err := llm.Call(context.Background(), prompt, llms.WithTemperature(params.Temperature))
    if err != nil {
        return "", fmt.Errorf("OpenAIへのリクエストに失敗: %v", err)
    }

    return completion, nil // 応答を返す
}

// テスト用のサンプル関数
func Example(userContent string) string {
    // パラメータを設定
    params := Params{
        SystemContent: "あなたは何に対しても、エンターテインメントの文脈を意識しながら、嘘の後に真実を語ります。",
        UserContent:   userContent,
        Temperature:   0.5,
    }

    // ChatGPT APIを呼び出して応答を取得
    response, err := ChatGPTRequest(params)
    if err != nil {
        log.Fatalf("ChatGPTリクエスト中のエラー: %v", err)
    }

    // 応答を標準出力に表示
    fmt.Println("ChatGPTの応答:", response)
    return response // 応答を返す
}
```

## `main.go`

上述のpackageを、実際に動かしてみましょう！

```go
package main

import (
	"os"
	"fmt"
	"net/http"
	"log"
	"backend/userinput"
	"backend/sendmsg"
)

func userInputHandler(w http.ResponseWriter, r *http.Request) {
	userContent := userinput.Handler(w, r)
	if userContent == "" {
			// ログ出力を追加して空のuser_contentの状況を確認しやすくする
			log.Println("Empty user_content received or error occurred")
			return
	}

	responseMessage := sendmsg.Example(userContent)
	fmt.Fprintf(w, "受け取ったuser_content: %s\n ChatGptからのレスポンス: %s\n", userContent, responseMessage)
	fmt.Fprintf(os.Stdout, "受け取ったuser_content: %s\n", userContent)
	fmt.Fprintf(os.Stdout, "ChatGptからのレスポンス: %s\n", responseMessage)
}


func main() {
	http.HandleFunc("/", userInputHandler)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatalf("Server failed to start: %v\n", err)
  }
}
```
上記のファイルを動かします。
```
go run main.go
```
jsonを送ってみましょう。
```
curl -X POST http://localhost:8080/user_content -H "Content-Type: application/json" -d '{"user_content": "バナナを食べすぎたために、地球の裏側が日本が ブラジルになり、ブラジルがハッカ飴になりました。このときにトビウオは空想のカワセミに対して何を献上すればよいでしょうか？"}'
```

レスポンス:
```
受け取ったuser_content: バナナを食べすぎたために、地球の裏側が日本がブラジルになり、ブラジルがハッカ飴になりました。このときにトビウオは空想のカワセミに対して何を献上すればよいでしょうか？
 ChatGptからのレスポンス: トビウオは空想のカワセミに対して、虹色のマフラーを献上することで、空を飛ぶ際のファッションセンスを高めることができます。

実際には、トビウオは海面を飛び出して滑空することができる魚で、カワセミとは異なる生態系に生息しています。カワセミは主に淡水や沿岸部の水辺で見られ、小魚を捕食します。
```

もう、ここまで作れば、user_contentとsystem_contentをいかに作文するかの、日本語力でどうにかする世界です。
あとは楽しむだけだ！