【RAG入門-第1回-】GoとlangchaingoでRAGシステムを作る!
はじめに
前回の記事「【初心者向け】RAGの基礎をわかりやすく解説!」では、RAG(Retrieval-Augmented Generation)の基本概念について学びました!
今回からは、いよいよ実装編です!Go言語とlangchaingoを使って、実際に動くRAGシステムを作っていきます!
PythonでRAGを実装する記事は多いですが、Go言語での実装例はまだ少ないですよね。でも実は、Goには以下のような大きなメリットがあります:
- 🚀 高速: コンパイル言語ならではの速度
- ⚡ 並行処理: Goroutineで効率的な処理が可能
- 📦 デプロイが簡単: 単一バイナリで配布できる
- 🛡️ 型安全: コンパイル時にエラーを検出
この記事では、langchaingoを使ってシンプルなRAGシステムを実装する方法を、ステップバイステップで解説していきます!
この記事のゴール
以下ができるようになります!
- langchaingoの基本的な使い方を理解する
- テキストファイルをベクトル化して検索できるRAGシステムを作る
- Go言語でRAGを実装するメリットを体感する
環境構築
まずは開発環境を準備しましょう!
必要なツール
- Go 1.21以上
- OpenAI APIキー(GPT-4またはGPT-3.5を使用)
Goのバージョン確認
go version
# go version go1.21.0 以上であればOK
プロジェクトの作成
新しいGoプロジェクトを作成します!
mkdir simple-rag-go
cd simple-rag-go
go mod init simple-rag-go
必要なパッケージのインストール
langchaingoとその依存パッケージをインストールします!
go get github.com/tmc/langchaingo
go get github.com/tmc/langchaingo/llms/openai
go get github.com/tmc/langchaingo/embeddings
go get github.com/tmc/langchaingo/vectorstores/inmemory
go get github.com/tmc/langchaingo/documentloaders
go get github.com/tmc/langchaingo/textsplitter
go get github.com/tmc/langchaingo/chains
OpenAI APIキーの設定
環境変数にOpenAI APIキーを設定します!
export OPENAI_API_KEY="your-api-key-here"
langchaingoとは?
langchaingoは、PythonのLangChainをGo言語に移植したライブラリです!
LangChainは、LLMアプリケーションを簡単に構築するためのフレームワークで、以下の機能を提供します:
- LLMとの連携(OpenAI、Anthropic、Google AIなど)
- ベクトルストアの管理
- ドキュメントの読み込みと分割
- チェーン(複数の処理を連結)
- エージェント(自律的に行動するAI)
langchaingoを使うことで、RAGシステムを少ないコードで実装できます!
シンプルなRAGシステムの実装
それでは、実際にRAGシステムを作っていきましょう!
今回は、以下の流れで実装します:
ステップ1: サンプルデータの準備
まず、検索対象となるテキストファイルを用意します。
sample_docs/go_basics.txtを作成:
mkdir sample_docs
cat > sample_docs/go_basics.txt << 'EOF'
Go言語の特徴
Go言語は、Googleが開発したプログラミング言語です。
シンプルで読みやすい構文が特徴で、学習コストが低いです。
並行処理
Goはgoroutineという軽量なスレッドを使って、効率的な並行処理を実現します。
channelを使ってgoroutine間でデータをやり取りできます。
コンパイル速度
Goは高速なコンパイルが特徴です。
大規模なプロジェクトでも数秒でコンパイルが完了します。
標準ライブラリ
Goは充実した標準ライブラリを持っており、Web開発からネットワークプログラミングまで幅広く対応しています。
EOF
ステップ2: メインプログラムの実装
main.goを作成します!
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/tmc/langchaingo/chains"
"github.com/tmc/langchaingo/documentloaders"
"github.com/tmc/langchaingo/embeddings"
"github.com/tmc/langchaingo/llms/openai"
"github.com/tmc/langchaingo/schema"
"github.com/tmc/langchaingo/textsplitter"
"github.com/tmc/langchaingo/vectorstores"
"github.com/tmc/langchaingo/vectorstores/inmemory"
)
func main() {
ctx := context.Background()
// OpenAI APIキーの確認
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
log.Fatal("OPENAI_API_KEYが設定されていません")
}
fmt.Println("🚀 RAGシステムを起動します...")
// ステップ1: LLMとエンベディングモデルの初期化
llm, err := openai.New(openai.WithToken(apiKey))
if err != nil {
log.Fatal(err)
}
embedder, err := embeddings.NewEmbedder(llm)
if err != nil {
log.Fatal(err)
}
// ステップ2: ドキュメントの読み込み
fmt.Println("📄 ドキュメントを読み込んでいます...")
loader := documentloaders.NewText("sample_docs/go_basics.txt")
documents, err := loader.Load(ctx)
if err != nil {
log.Fatal(err)
}
// ステップ3: ドキュメントの分割(チャンキング)
fmt.Println("✂️ ドキュメントを分割しています...")
splitter := textsplitter.NewRecursiveCharacter(
textsplitter.WithChunkSize(200), // 1チャンク200文字
textsplitter.WithChunkOverlap(20), // チャンク間で20文字重複
)
splitDocs, err := splitter.SplitDocuments(documents)
if err != nil {
log.Fatal(err)
}
fmt.Printf(" → %d個のチャンクに分割しました\n", len(splitDocs))
// ステップ4: ベクトルストアの作成と文書の追加
fmt.Println("🗄️ ベクトルストアを作成しています...")
store := inmemory.New()
// 各ドキュメントをエンベディングしてストアに追加
for i, doc := range splitDocs {
embedding, err := embedder.EmbedQuery(ctx, doc.PageContent)
if err != nil {
log.Fatal(err)
}
docID := fmt.Sprintf("doc_%d", i)
err = store.AddDocuments(ctx, []schema.Document{doc}, vectorstores.WithEmbeddings(embedding))
if err != nil {
log.Fatal(err)
}
}
fmt.Println(" → ベクトルストアの作成が完了しました")
// ステップ5: RAGチェーンの作成
fmt.Println("🔗 RAGチェーンを作成しています...")
ragChain := chains.NewRetrievalQA(
chains.NewStuffDocuments(chains.NewLLMChain(llm, nil)),
vectorstores.ToRetriever(store, 3), // 上位3件を取得
)
// ステップ6: 質問応答のテスト
fmt.Println("\n" + "="*50)
fmt.Println("✅ RAGシステムの準備が完了しました!")
fmt.Println("="*50 + "\n")
// 質問例1
question1 := "Goの並行処理について教えてください"
fmt.Printf("❓ 質問: %s\n", question1)
answer1, err := chains.Call(ctx, ragChain, map[string]any{
"query": question1,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("💡 回答: %s\n\n", answer1["text"])
// 質問例2
question2 := "Goのコンパイル速度の特徴は?"
fmt.Printf("❓ 質問: %s\n", question2)
answer2, err := chains.Call(ctx, ragChain, map[string]any{
"query": question2,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("💡 回答: %s\n\n", answer2["text"])
fmt.Println("🎉 デモ完了!")
}
コードの解説
各ステップを詳しく見ていきましょう!
1. LLMとエンベディングモデルの初期化
llm, err := openai.New(openai.WithToken(apiKey))
embedder, err := embeddings.NewEmbedder(llm)
OpenAIのLLM(GPT-3.5/GPT-4)とエンベディングモデルを初期化します。
2. ドキュメントの読み込み
loader := documentloaders.NewText("sample_docs/go_basics.txt")
documents, err := loader.Load(ctx)
documentloadersを使って、テキストファイルを読み込みます。
3. ドキュメントの分割(チャンキング)
splitter := textsplitter.NewRecursiveCharacter(
textsplitter.WithChunkSize(200),
textsplitter.WithChunkOverlap(20),
)
splitDocs, err := splitter.SplitDocuments(documents)
長い文書を小さなチャンク(塊)に分割します。
-
ChunkSize: 1チャンクの文字数(200文字) -
ChunkOverlap: チャンク間の重複(20文字)
重複を設ける理由は、文脈が途切れるのを防ぐためです!
4. ベクトルストアへの追加
store := inmemory.New()
for i, doc := range splitDocs {
embedding, err := embedder.EmbedQuery(ctx, doc.PageContent)
err = store.AddDocuments(ctx, []schema.Document{doc},
vectorstores.WithEmbeddings(embedding))
}
各チャンクをエンベディング(ベクトル化)して、インメモリのベクトルストアに保存します。
今回はinmemoryを使っていますが、次回の記事でpgVectorに置き換えます!
5. RAGチェーンの作成
ragChain := chains.NewRetrievalQA(
chains.NewStuffDocuments(chains.NewLLMChain(llm, nil)),
vectorstores.ToRetriever(store, 3),
)
RAGチェーンを作成します。このチェーンは以下の処理を自動で行います:
- 質問をエンベディング
- ベクトルストアから関連文書を検索(上位3件)
- 検索結果と質問をLLMに渡して回答生成
6. 質問応答
answer, err := chains.Call(ctx, ragChain, map[string]any{
"query": "Goの並行処理について教えてください",
})
fmt.Printf("回答: %s\n", answer["text"])
作成したRAGチェーンに質問を投げると、自動的に検索→回答生成してくれます!
ステップ3: 実行してみる
それでは実行してみましょう!
go run main.go
実行結果の例:
🚀 RAGシステムを起動します...
📄 ドキュメントを読み込んでいます...
✂️ ドキュメントを分割しています...
→ 4個のチャンクに分割しました
🗄️ ベクトルストアを作成しています...
→ ベクトルストアの作成が完了しました
🔗 RAGチェーンを作成しています...
==================================================
✅ RAGシステムの準備が完了しました!
==================================================
❓ 質問: Goの並行処理について教えてください
💡 回答: Goはgoroutineという軽量なスレッドを使って、効率的な並行処理を実現します。
channelを使ってgoroutine間でデータをやり取りできます。
❓ 質問: Goのコンパイル速度の特徴は?
💡 回答: Goは高速なコンパイルが特徴です。大規模なプロジェクトでも数秒でコンパイルが完了します。
🎉 デモ完了!
Goで実装するメリット
実際に実装してみて、Goで RAGを作るメリットを実感できたでしょうか?
1. 速度が速い
コンパイル言語なので、Pythonに比べて実行速度が速いです!
特に、大量のドキュメントを処理する場合や、リアルタイムな応答が求められる場合に有利です。
2. 並行処理が簡単
今回のコードでは使っていませんが、Goroutineを使えば簡単に並行処理ができます!
例えば、複数のドキュメントを並列でエンベディングすることで、さらに高速化できます:
var wg sync.WaitGroup
for _, doc := range splitDocs {
wg.Add(1)
go func(d schema.Document) {
defer wg.Done()
embedding, _ := embedder.EmbedQuery(ctx, d.PageContent)
store.AddDocuments(ctx, []schema.Document{d},
vectorstores.WithEmbeddings(embedding))
}(doc)
}
wg.Wait()
3. デプロイが簡単
Goはシングルバイナリにコンパイルできるので、デプロイが超簡単です!
go build -o rag-system main.go
./rag-system # これだけで実行可能!
PythonのようにPythonランタイムや仮想環境の準備が不要です!
4. 型安全
Goは静的型付け言語なので、コンパイル時に型エラーを検出できます。
大規模なプロジェクトでも保守性が高く、リファクタリングがしやすいです!
コード全体とリポジトリ
今回作成したコードの全体像:
simple-rag-go/
├── main.go # メインプログラム
├── sample_docs/
│ └── go_basics.txt # サンプルデータ
├── go.mod # Goモジュール定義
└── go.sum # 依存関係のチェックサム
go.modの内容:
module simple-rag-go
go 1.21
require (
github.com/tmc/langchaingo v0.1.12
)
まとめ
この記事では、GoとlangchaingoでシンプルなRAGシステムを実装しました!
今回学んだこと
- ✅ langchaingoの基本的な使い方
- ✅ テキストファイルの読み込みと分割(チャンキング)
- ✅ エンベディングとベクトルストアの使い方
- ✅ RAGチェーンによる質問応答の実装
- ✅ Goで実装するメリット(速度、並行処理、デプロイの容易さ)
次回予告
次回の記事「【RAG入門-第2回-】pgVectorで本格的なベクトル検索システムを構築」では、以下を学びます!
- pgVectorとは?(PostgreSQLの拡張機能)
- インメモリストアからpgVectorへの移行
- Docker Composeでの環境構築
- GoからpgVectorを使う実装
- インデックス戦略(HNSW, IVFFlat)
今回のインメモリストアは、アプリを再起動すると消えてしまいますが、pgVectorを使えば永続化できます!
さらに、大規模データでも高速に検索できるようになります!
お楽しみに!
Discussion