【ハンズオン】LangChain + Goを使用した生成AIアプリケーションをつくってみよう！【LangChain/Go/RAG/Gemini】

## はじめに

どうも、こんにちは！LLMについては特段詳しくないフツーのバックエンドエンジニアの[@オーガ](https://twitter.com/o_ga09)といいます。

今回はそんな僕が、LLMを使用した生成AIアプリケーションをつくるというハンズオン的な感じの記事にしておこうと思っています。よろしければ、最後までお付き合いください！

## 対象読者

- Geminiさわってみたい方
- Goさわってみたい方
- Gopherくんが好きな方

## 目次

- [なぜ、Go + Geminiなのか？](#なぜgo--geminiなのか)
- [GoでLLMってプロダクトレベルで使われているの？Pythonじゃないの？](#goでllmってプロダクトレベルで使われているのpythonじゃないの)
- [つくるもの](#つくるもの)
- [技術構成](#技術構成)
- [LangChainでなにができる？](#langchainでなにができる)
- [ベクトル検索とは？](#ベクトル検索とは)
- [【ハンズオン】LangChainで生成AIアプリを作成する](#ハンズオンlangchainで生成aiアプリを作成する)
    - [1.環境構築](#1-環境構築)
    - [2.GoでHelloWorld!](#2-goでhelloworld)
    - [3.ベクトル検索なしのチャットボット作成](#3-ベクトル検索なしのチャットボット作成)
    - [4.ベクトル検索ありのチャットボット作成](#4-ベクトル検索ありのチャットボット作成)
    - [5.チャットボットAPI作成](#5-チャットボットapi作成)
- [まとめ](#まとめ)
- [さいごに](#さいごに)
- [参考](#参考)

サンプルリポジトリ

https://github.com/o-ga09/langchain-go

## TL;DR

- LangChainやりたいなら、普通にPythonかTypescriptでやれ！

## なぜ、Go + Geminiなのか？

多くの場合、Python + OpenAIが主流と思いますが、そこは他のいろんな方がやれていて擦られすぎているので記事を書いてもつまらないだろうということで今回はこの選択にしました。

簡単にいうと、逆張りオタクなのです(笑)

## GoでLLMってプロダクトレベルで使われているの？Pythonじゃないの？

https://go.dev/blog/survey2024-h1-results

公式のGo Blogに四半期のGoユーザーへのアンケート結果は以下のようになっています。
一部の開発者には使われていますが、エコシステムの不十分さは課題のようです。

>Reasons for using Go with generative AI systems
To help us understand what benefits developers hope to derive from using Go in their AI/ML services, we asked developers why they feel Go is a good choice for this domain. A clear majority (61%) of respondents mentioned one or more of Go’s core principles or features, such as simplicity, runtime safety, concurrency, or single-binary deployments. One third of respondents cited existing familiarity with Go, including a desire to avoid introducing new languages if they can avoid it. Rounding out the most common responses were various challenges with Python (particularly for running production services) at 14%.

>翻訳
生成AIシステムでGoを使用する理由
開発者がAI/MLサービスにGoを使用することでどのようなメリットを得たいと考えているかを理解するために、開発者にGoがこの分野に適していると感じる理由を尋ねました。回答者の大多数（61%）が、シンプルさ、実行時の安全性、並行性、シングル バイナリ デプロイメントなど、Go の中核となる原則や機能を 1 つ以上挙げています。回答者の 3 分の 1 は、新しい言語の導入は避けられるなら避けたいなど、Go に慣れ親しんでいることを挙げています。最も多かった回答は、Pythonに関する様々な課題（特にプロダクションサービスの実行）で、14%でした。

>Challenges when using Go with GenAI systems
Respondents were largely unified on what currently prevents them from using Go with AI-powered services: the ecosystem is centered around Python, their favorite libraries/frameworks are all in Python, getting started documentation assumes Python familiarity, and the data scientists or researchers exploring these models are already familiar with Python.
>翻訳
GenAIシステムでGoを使用する際の課題
エコシステムがPython中心であること、お気に入りのライブラリやフレームワークがすべてPythonであること、ドキュメントを読み始めるにはPythonに精通していることが前提であること、これらのモデルを研究しているデータサイエンティストや研究者がすでにPythonに精通していることなどです。

もちろん、Python優位ではあるけども。。。

## つくるもの

LangChain + Go + RAGを使用したQ&Aチャットボット

## 技術構成

|技術要素|用途|
|---|---|
|Go|アプリケーション|
|langchaingo|langchainのgoライブラリ|
|weaviate|ベクトルストア|
|Gemini Pro API|LLM|

## LangChainでなにができる？

以下のを使用したLLMアプリケーションが、簡単に作れてしまうライブラリ

- Models：LLMの選択
    - OpenAI、Gemini etc. 色々使えます
- Indexes：外部データの利用
    - RAGやベクトルDBが使えます
- Chains：複数プロンプトで一連の処理の実行
    - 一つの結果から次のプロンプトに繋げることがでいます
- Agents：ツールの実行
    - 翻訳ツールや要約ツールをLLMアプリに組み込むことができます
- Memory：対話履歴の保持
    - ユーザーとLLMとの対話履歴を保持して使うことができます

## ベクトル検索とは？



## 【ハンズオン】LangChainで生成AIアプリを作成する

### 1. 環境構築

1-1. google AI stdioでGeminiのAPIキーを取得
    
https://aistudio.google.com/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/c38a211f-2975-cc23-2dd3-a8835fcf4733.png)

1-2. ターミナルもしくはコマンドプロンプトを開いて、Goプロジェクトの初期化をする

もし、ここでまだGoをダウンロードしていないいよーって方は、Goをダウンロードしてインストールしてください！

https://go.dev/dl/

赤枠内の自分に合うOSのものをダウンロードして、ローカルで開いてインストールすれば、OKです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/db6c9065-646f-1525-8dd6-ab3ced0332e7.png)

一応、Goを正常にインストールできたかを確認してみましょう！
例として、こんな感じで表示されればOKです！

```bash
$ go version
go version go1.22.0 darwin/arm64
```

```bash
$ mkdir langchain-go-sample 
$ cd langchain-go-sample
$ go mod init github.com/[git repo]/[任意のプロジェクト名]
$ touch main.go
```

Goでは、慣習としてモジュール名は、github.com/[git repo]/[任意のプロジェクト名]
とします。

### 2. GoでHelloWorld!

2-1. エディタを開いて、main.goを編集する

```go:main.go
package main

import　"fmt"

func main() {
    fmt.Println("Hello World!")
}
```

2-2. main.goを実行する

```bash
$ go run main.go
# 出力
Hello world!
```

### 3. ベクトル検索なしのチャットボット作成

3-1. LangChainを使ってみる！

```go:main.go
package main

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

	"github.com/tmc/langchaingo/llms"
	"github.com/tmc/langchaingo/llms/googleai"
)

func main() {
	ctx := context.Background()
    // １で取得したAPIキーを環境変数に設定すること
	apiKey := os.Getenv("GOOGLEAI_API_KEY")
	opts := googleai.WithAPIKey(apiKey)
    // LangChainのインスタンスを生成
	llm, err := googleai.New(ctx, opts)
	if err != nil {
		log.Fatal(err)
	}
    // LLMにリクエストするプロンプト
	prompt := "日本の現在の総理大臣は誰ですか？"
    // LLMにリクエストする
	completion, err := llms.GenerateFromSinglePrompt(ctx, llm, prompt)
	if err != nil {
		log.Fatal(err)
	}
 
    // 結果を表示する
	fmt.Println("===answet===")
	fmt.Println(completion)
	fmt.Println("============")
    // 結果
    // 岸田文雄
}
```

3-2. 実行する

```bash
$ export GOOGLEAI_API_KEY=xxx
```

```bash
$ go mod tidy
$ go run main.go
```

### 4. ベクトル検索ありのチャットボット作成

4-1. ベクトルDBを構築する

今回、ベクトルDBには、以下のOSSで使用できる[weaviate](https://weaviate.io/)を使用します。

:::note warn
weaviateは、ポートが`8080`でないと接続できないので注意
:::

docker compose ファイルは以下の通りです。

```yaml:compose.yml
version: '3.4'
services:
  weaviate:
    image: semitechnologies/weaviate:1.19.9
    ports:
      - 8080:8080
    environment:
      QUERY_DEFAULTS_LIMIT: 25
      AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true'
      PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
      DEFAULT_VECTORIZER_MODULE: 'none'
```

以下のコマンドでDockerコンテナが起動できれば完了です。

```bash
$ docker compose up
```

4-2. ベクトルDBにデータ構造を作成する

ベクトルDBにでたスキーマを作成します。

```go:internal/vectordb/generate.go
package vectordb

import (
	"context"
	"fmt"

	"github.com/o-ga09/langchain-go/internal/llm"
	"github.com/o-ga09/langchain-go/pkg/errors"
	"github.com/weaviate/weaviate-go-client/v4/weaviate"
	"github.com/weaviate/weaviate/entities/models"
)

func Generate(ctx context.Context) error {
	weaviateClient := weaviate.New(weaviate.Config{
		Host:   "weaviate:8080",
		Scheme: "http",
	})

	if ok, err := weaviateClient.Schema().ClassExistenceChecker().WithClassName(llm.WeaviateIndexName).Do(ctx); ok {
		return errors.ErrAlredyExists
	} else if err != nil {
		return err
	}

	if err := weaviateClient.Schema().ClassCreator().WithClass(&models.Class{
		Class:       llm.WeaviateIndexName,
		Description: "qa class",
		VectorIndexConfig: map[string]any{
			"distance": "cosine",
		},
		ModuleConfig: map[string]any{},
		Properties: []*models.Property{
			{
				Name:        llm.WeaviatePropertyTextName,
				Description: "document text",
				DataType:    []string{"text"},
			},
			{
				Name:        llm.WeaviatePropertyNameSpaceName,
				Description: "namespace",
				DataType:    []string{"text"},
			},
		},
	}).Do(ctx); err != nil {
		return err
	}
	fmt.Println("created")
	return nil
}
```

4-3. ベクトルDBにデータを追加する

つぎに、ベクトルDBにベクトル化するデータを追加します。

```go:internal/vectordb/build_rug.go
package vectordb

import (
	"context"
	"fmt"
	"os"

	"github.com/o-ga09/langchain-go/internal/llm"
	"github.com/tmc/langchaingo/documentloaders"
	"github.com/tmc/langchaingo/textsplitter"
)

func BuildRag(ctx context.Context) error {
	chain, err := llm.New()
	if err != nil {
		return err
	}

	file, err := os.Open("./tools/insert_docs_html/qa.html")
	if err != nil {
		return err
	}
	defer file.Close()

	loader := documentloaders.NewHTML(file)
	docs, err := loader.LoadAndSplit(
		ctx,
		textsplitter.RecursiveCharacter{
			Separators:    []string{"\n\n", "\n", " ", ""},
			ChunkSize:     200,
			ChunkOverlap:  100,
			LenFunc:       func(s string) int { return len(s) },
			KeepSeparator: true,
		},
	)
	if err != nil {
		return err
	}

	for _, v := range docs {
		_, err := chain.AddDocument(ctx, llm.NameSpaceHTML, v.PageContent)
		if err != nil {
			return err
		}
	}
	fmt.Println("done")
	return nil
}
```

```go:internal/llm/llm.go
package llm

import (
	"context"
	"os"

	"github.com/tmc/langchaingo/chains"
	"github.com/tmc/langchaingo/embeddings"
	"github.com/tmc/langchaingo/llms"
	"github.com/tmc/langchaingo/llms/googleai"
	"github.com/tmc/langchaingo/prompts"
	"github.com/tmc/langchaingo/schema"
	"github.com/tmc/langchaingo/vectorstores"
	"github.com/tmc/langchaingo/vectorstores/weaviate"
)

const (
	WeaviateIndexName             = "Qa"
	WeaviatePropertyTextName      = "text"
	WeaviatePropertyNameSpaceName = "namespace"
	NameSpaceHTML                 = "html"
	NameSpaceCSV                  = "csv"
)

type LLM struct {
	llm   llms.Model
	store vectorstores.VectorStore
}

func (l *LLM) AddDocument(ctx context.Context, namespace string, content string) ([]string, error) {
	return l.store.AddDocuments(ctx, []schema.Document{
		{
			PageContent: content,
		},
	}, vectorstores.WithNameSpace(namespace))
}

func (l *LLM) Answer(ctx context.Context, namespace string, question string) (string, error) {
	prompt := prompts.NewPromptTemplate(
		`## Introduction 
			あなたはカスタマーサポートです。丁寧な回答を心がけてください。
			以下のContextを使用して、日本語で質問に答えてください。Contextから答えがわからない場合は、「わかりません」と回答してください。

			## 質問
			{{.question}}

			## Context
			{{.context}}

			日本語での回答:`,
		[]string{"context", "question"},
	)

	combineChain := chains.NewStuffDocuments(chains.NewLLMChain(l.llm, prompt))

	result, err := chains.Run(
		ctx,
		chains.NewRetrievalQA(
			combineChain,
			vectorstores.ToRetriever(
				l.store,
				5,
				vectorstores.WithNameSpace(string(namespace)),
				vectorstores.WithNameSpace(string(namespace)),
			),
		),
		question,
		chains.WithModel("gemini-pro"),
	)
	if err != nil {
		return "", err
	}
	return result, nil
}

```

4-4. ベクトルDBとLLMアプリケーションを接続する

先ほどの、`internal/llm/llm.go`に、
llmのインスタンスを生成するNew関数とLLMから回答を得るAnswer関数を作成

```go:internal/llm/llm.go
package llm

import (
	"context"
	"os"

	"github.com/tmc/langchaingo/chains"
	"github.com/tmc/langchaingo/embeddings"
	"github.com/tmc/langchaingo/llms"
	"github.com/tmc/langchaingo/llms/googleai"
	"github.com/tmc/langchaingo/prompts"
	"github.com/tmc/langchaingo/schema"
	"github.com/tmc/langchaingo/vectorstores"
	"github.com/tmc/langchaingo/vectorstores/weaviate"
)

const (
	WeaviateIndexName             = "Qa"
	WeaviatePropertyTextName      = "text"
	WeaviatePropertyNameSpaceName = "namespace"
	NameSpaceHTML                 = "html"
	NameSpaceCSV                  = "csv"
)

type LLM struct {
	llm   llms.Model
	store vectorstores.VectorStore
}

func New() (*LLM, error) {
	ctx := context.Background()
	apiKey := os.Getenv("GOOGLEAI_API_KEY")
	opts := googleai.WithAPIKey(apiKey)
	llm, err := googleai.New(ctx, opts)
	if err != nil {
		return nil, err
	}
	e, err := embeddings.NewEmbedder(llm)
	if err != nil {
		return nil, err
	}
	store, err := weaviate.New(
		weaviate.WithScheme("http"),        // docker-composeの設定に合わせる
		weaviate.WithHost("localhost:8080"), // docker-composeの設定に合わせる
		weaviate.WithEmbedder(e),
		weaviate.WithIndexName(WeaviateIndexName),
		weaviate.WithTextKey(WeaviatePropertyTextName),
		weaviate.WithNameSpaceKey(WeaviatePropertyNameSpaceName),
	)
	if err != nil {
		return nil, err
	}
	return &LLM{
		llm:   llm,
		store: store,
	}, nil
}

func (l *LLM) Answer(ctx context.Context, namespace string, question string) (string, error) {
	prompt := prompts.NewPromptTemplate(
		`## Introduction 
			あなたはカスタマーサポートです。丁寧な回答を心がけてください。
			以下のContextを使用して、日本語で質問に答えてください。Contextから答えがわからない場合は、「わかりません」と回答してください。

			## 質問
			{{.question}}

			## Context
			{{.context}}

			日本語での回答:`,
		[]string{"context", "question"},
	)

	combineChain := chains.NewStuffDocuments(chains.NewLLMChain(l.llm, prompt))

	result, err := chains.Run(
		ctx,
		chains.NewRetrievalQA(
			combineChain,
			vectorstores.ToRetriever(
				l.store,
				5,
				vectorstores.WithNameSpace(string(namespace)),
				vectorstores.WithNameSpace(string(namespace)),
			),
		),
		question,
		chains.WithModel("gemini-pro"),
	)
	if err != nil {
		return "", err
	}
	return result, nil
}
```

4-5. アプリケーションを実行する

```go:main.go
package main

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

	"github.com/o-ga09/langchain-go/internal/llm"
	"github.com/o-ga09/langchain-go/internal/vectordb"
	Errpkg "github.com/o-ga09/langchain-go/pkg/errors"
)

func main() {
	ctx := context.Background()
	meta := []QA_MetaData{
		{Question: "Go 1.21で追加されたbuilt-insを教えてください", NameSpace: "html"},
	}

	if err := RunWithRAG(ctx, meta); err != nil {
		fmt.Println("failed to run with RAG", err)
	}

	fmt.Println("=====================================")

	if err := RunWithOutRAG(ctx); err != nil {
		fmt.Println("failed to run with out RAG", err)
	}
}

// RAGありver.
func RunWithRAG(ctx context.Context, meta []QA_MetaData) error {
	qaBot, err := llm.New()
	if err != nil {
		fmt.Println("failed to create LLM instance", err)
		return err
	}

	for _, v := range meta {
		result, err := qaBot.Answer(ctx, v.NameSpace, v.Question)
		if err != nil {
			fmt.Println("faied to response LLM", err)
			return err
		}

		fmt.Println("=====================================")
		fmt.Printf("kind:\n %s\n", v.NameSpace)
		fmt.Printf("question:\n %s\n", v.Question)
		fmt.Printf("result:\n %s\n", result)
		fmt.Println("=====================================")
	}
	return nil
}
```

**実行**

```bash
docker compose -f docker/compose.yml up
```

**結果**

以下を期待していましたが、ハルシネーションを起こしてしまっていたようです

```bash:期待する結果
docker-llm-app-1   | =====================================
docker-llm-app-1   | kind:
docker-llm-app-1   |  htmlquestion:
docker-llm-app-1   |  Go1.21に追加された3つのbuilt-insはなんですか？
docker-llm-app-1   | result:
docker-llm-app-1   |  Go1.21で追加された新しいbuilt-insは、min、max、clear関数です。
docker-llm-app-1   | =====================================
```

```bash:実際の結果
=====================================
kind:
 html
question:
 Go1.21に追加された3つのbuilt-insはなんですか？
result:
 Go 1.21 で追加された 3 つの組み込み関数は、min、max、Pinner です。
=====================================
```

おしい！

試しに、csvを読み込んでやってみました。

```csv:入力データ
No,名前,生年月日,血液型,SNSアカウント
1,山本彩,1993/7/14,B型,https://twitter.com/SayakaNeon
2,大谷翔平,1994/7/5,B型,https://www.instagram.com/shoheiohtani/?hl=ja
3,鎮西寿々歌,1998/11/24,AB型,https://twitter.com/suzuka_fz1124
4,よしなま,1996/1//30,不明,https://twitter.com/yosi980
5,Mハシ,1995/2/15,不明,https://twitter.com/Mhashi0801
```

```bash:結果
RAGあり
=====================================
kind:
 csv
question:
 山本彩の生年月日、SNSアカウントを教えてください
result:
 山本彩さんの生年月日は1993年7月14日です。
山本彩さんのSNSアカウントは、https://twitter.com/SayakaNeon です。
=====================================

RAGなし
===answer===
**生年月日:** 1993年7月14日

**SNSアカウント:**

* **Twitter:** @SayakaNeon
* **Instagram:** @sayakaneon_official
* **TikTok:** @sayakaneon_official
============
```

```bash:結果
RAGあり
=====================================
kind:
 csv
question:
 Mハシの生年月日、SNSアカウントを教えてください
result:
 生年月日: 1995/2/15
SNSアカウント: https://twitter.com/Mhashi0801
=====================================

RAGなし
===answer===
**生年月日:** 1991年10月23日

**SNSアカウント:**

* **Instagram:** @m_hashi_official
* **Twitter:** @m_hashi_official
* **TikTok:** @m_hashi_official
* **YouTube:** Mハシ公式チャンネル
============
```

csvで読み込んだ方は良さそう！
ちゃんと、SNSアカウントもユーザー名ではなく、入力で与えたURLできているし、生年月日もあっている。

### 5. チャットボットAPI作成

適当に、GinでREST APIサーバを実装しました！
エンドポイントは以下の通りです。すみません、リクエストする際はローカルでお願いします

|パス|メソッド|内容|
|:---|:---|:---|
|/question|POST|LLMに質問を投げる|
|/add/document|POST|ベクトルDBにデータを追加|

**リクエスト方法 - LLMに質問を投げる**

```bash
curl -X POST -d '{"question": "Go1.21で追加されたbuilt-insを教えてください。"}' localhost:8082/v1/question
```

**リクエスト方法 - LLMに質問を投げる**

```bash
curl -X POST -d '{"page_content": "名前：xxx,生年月日：xxxx/x/x,血液型：x型,SNSアカウント：https://x.com/xxx"}' localhost:8082/v1/add/document
```

## まとめ

- `langchaingo`は、非公式ライブラリあるものの簡単にLangChainを使うことができた！
- `langchaingo`の本記事で使用したバージョンも`v0.1.10`であり、参考にした記事は、リリース前のタグがつかないものを使用していたため、ちょいハマりそうだった！
- RAGとはなにかとか、Emebeddingあたりの解像度高まった！
- **普通にPythonで作った方がいいです！**
- どんどん進化していて、RDBでもベクトル検索ができるようになっています
    - RDS for PostgreSQL、TiDB Serverless、firestore
- 次は大人しくPythonでもっとしっかりしたやつ作りたい

 ## さいごに

 弊社では、エンジニア積極採用中です！
SES、請負、受託、Saleforce やりたい方はご興味を持っていただけたら幸いです。

ワクトでは、以下の Mission・Vision・Value を掲げております。

Mission : 「IT× ワクワク」で、社会の発展に貢献する
Vision : 大切な人に心から薦めたい会社であり続ける
Value :
One team for customers
熱意 × 人格 × 能力
まずやってみる
ワクトでは、もう一つ重要なものとして「 マインドマップ 」というものがあります。
個人的にですが、こちらに共感していただけた方はワクトに合うかなと思っております。

![MindMap](https://camo.qiitausercontent.com/d4bc1bad02e4881409ad684cdf3e1fa4b25b2a6c/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f333531323631362f37636461653334372d626230632d393532372d656430382d3164393463376630333337652e706e67)


https://wakuto.net/

https://www.wantedly.com/companies/wakuto0910

https://twitter.com/wakuwaku_toyou

また、本記事の内容は個人の考えであり、会社を代表するものではございません。

## 参考

langchaingo

https://github.com/tmc/langchaingo

 LLMアプリ作成方法サンプル

 https://qiita.com/t_serizawa/items/a2ced4441da714b3076f

https://zenn.dev/ryomak/articles/langchain-go-weaviate

データフレームワーク

https://docs.llamaindex.ai/en/stable/

https://github.com/run-llama/llama_index