3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

生成AIに関する記事を書こう!
Qiita Engineer Festa20242024年7月17日まで開催中!

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

Last updated at Posted at 2024-07-17

はじめに

どうも、こんにちは!LLMについては特段詳しくないフツーのバックエンドエンジニアの@オーガといいます。

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

対象読者

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

目次

サンプルリポジトリ

TL;DR

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

なぜ、Go + Geminiなのか?

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

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

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

公式の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キーを取得

image.png

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

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

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

image.png

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

$ go version
go version go1.22.0 darwin/arm64
$ 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を編集する

main.go
package main

import "fmt"

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

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

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

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

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

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()
    // 1で取得した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. 実行する

$ export GOOGLEAI_API_KEY=xxx
$ go mod tidy
$ go run main.go

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

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

今回、ベクトルDBには、以下のOSSで使用できるweaviateを使用します。

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

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

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コンテナが起動できれば完了です。

$ docker compose up

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

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

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にベクトル化するデータを追加します。

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
}
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関数を作成

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. アプリケーションを実行する

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
}

実行

docker compose -f docker/compose.yml up

結果

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

期待する結果
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   | =====================================
実際の結果
=====================================
kind:
 html
question:
 Go1.21に追加された3つのbuilt-insはなんですか?
result:
 Go 1.21 で追加された 3 つの組み込み関数は、min、max、Pinner です。
=====================================

おしい!

試しに、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
結果
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
============
結果
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に質問を投げる

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

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

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

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

参考

langchaingo

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

データフレームワーク

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?