🚀

【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チェーンを作成します。このチェーンは以下の処理を自動で行います:

  1. 質問をエンベディング
  2. ベクトルストアから関連文書を検索(上位3件)
  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を使えば永続化できます!

さらに、大規模データでも高速に検索できるようになります!

お楽しみに!

参考リンク

NonEntropy Tech Blog

Discussion