はじめに
どうも、こんにちは!LLMについては特段詳しくないフツーのバックエンドエンジニアの@オーガといいます。
今回はそんな僕が、LLMを使用した生成AIアプリケーションをつくるというハンズオン的な感じの記事にしておこうと思っています。よろしければ、最後までお付き合いください!
対象読者
- Geminiさわってみたい方
- Goさわってみたい方
- Gopherくんが好きな方
目次
- なぜ、Go + Geminiなのか?
- GoでLLMってプロダクトレベルで使われているの?Pythonじゃないの?
- つくるもの
- 技術構成
- LangChainでなにができる?
- ベクトル検索とは?
- 【ハンズオン】LangChainで生成AIアプリを作成する
- まとめ
- さいごに
- 参考
サンプルリポジトリ
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キーを取得
1-2. ターミナルもしくはコマンドプロンプトを開いて、Goプロジェクトの初期化をする
もし、ここでまだGoをダウンロードしていないいよーって方は、Goをダウンロードしてインストールしてください!
赤枠内の自分に合うOSのものをダウンロードして、ローカルで開いてインストールすれば、OKです!
一応、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を編集する
package main
import "fmt"
func main() {
fmt.Println("Hello World!")
}
2-2. main.goを実行する
$ go run main.go
# 出力
Hello world!
3. ベクトル検索なしのチャットボット作成
3-1. LangChainを使ってみる!
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 ファイルは以下の通りです。
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にでたスキーマを作成します。
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にベクトル化するデータを追加します。
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
}
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関数を作成
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. アプリケーションを実行する
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
熱意 × 人格 × 能力
まずやってみる
ワクトでは、もう一つ重要なものとして「 マインドマップ 」というものがあります。
個人的にですが、こちらに共感していただけた方はワクトに合うかなと思っております。
また、本記事の内容は個人の考えであり、会社を代表するものではございません。
参考
langchaingo
LLMアプリ作成方法サンプル
データフレームワーク