AIのスケーリング:本番環境でGoとFirebase GenkitがPythonを凌駕する理由

AI tutorial - IT technology blog
AI tutorial - IT technology blog

午前2時のレイテンシ・スパイク

午前2時14分、私の監視ダッシュボードは単に赤いだけでなく、危機的な状況を示していました。Pythonベース of AIマイクロサービスが限界に達し、P99レイテンシが300ミリ秒から6秒以上に急上昇したのです。グローバルインタプリタロック(GIL)は、まさにその設計通りに動作していました。メモリ消費量がクラスター制限の8GBに近づく一方で、CPUのボトルネックとなっていたのです。この危機によって、一つのことが明確になりました。Pythonはデータサイエンスには素晴らしいですが、高スループットのAIゲートウェイを構築するには、Goの効率性が必要だということです。

Firebase Genkitが最近Go의 サポートを開始し、バックエンドエンジニアにとって魅力的な選択肢を提供しています。Goの型安全性と低レベルな並行処理能力を、LLMオーケストレーションのための構造化されたフレームワークと組み合わせて利用できます。私の経験では、AIオーケストレーションをGoに切り替えることで、重いPythonコンテナと比較してコールドスタート時間を最大70%削減できます。

クイックスタート:Genkitのセットアップ

次の障害が発生する前に、Go環境を立ち上げましょう。NodeベースですがGoプロジェクトをシームレスに管理できるGenkit CLIと、Gemini APIを有効にしたGoogle Cloudプロジェクトが必要です。

# Genkit CLIをインストールする
npm i -g genkit

# 新しいGoプロジェクトを作成する
mkdir genkit-go-ai && cd genkit-go-ai
go mod init genkit-go-ai

# Genkitを初期化する
genkit init

ランタイムとしてGoを選択し、プロバイダーとしてGoogle Geminiを選択します。これにより、サービスの基礎となるmain.goファイルが生成されます。ボイラープレートコードは、プラグインの初期化やモデル設定などの重い処理を担ってくれます。

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/firebase/genkit/go/ai"
	"github.com/firebase/genkit/go/plugins/googleai"
)

func main() {
	ctx := context.Background()

	// Google AIプラグインを初期化する
	if err := googleai.Init(ctx, nil); err != nil {
		log.Fatal(err)
	}

	model := googleai.Model("gemini-1.5-flash")

	resp, err := ai.Generate(ctx, model, ai.WithTextPrompt("なぜAIのスケーリングにおいて、GoはPythonよりも優れているのですか?"))
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(resp.Text())
}

genkit startを実行して、ローカルのデベロッパーUIを起動します。このプレイグラウンドでは、プロンプトのテストや実行トレースのリアルタイムな確認が可能です。複雑なプロンプトチェーンのデバッグには欠かせないツールです。

Tool Calling:LLMと内部APIの橋渡し

LLMが既存のインフラストラクチャと対話できるようになると、システムは真に有用になります。Genkitは、JSONスキーマをGoの構造体に直接マッピングすることで、Tool Calling(関数呼び出し)を簡素化します。このアプローチはGoの厳格な型付けを活用しており、実行時ではなくコンパイル時にエラーを捕捉できます。

あるフィンテッククライアント向けにサポートボットを構築した際、AIはレガシーなSQLデータベースからリアルタイムの取引ステータスを取得する必要がありました。Goでそのツールを定義する方法は以下の通りです。

type TransactionInput struct {
	ID string `json:"id"`
}

type TransactionOutput struct {
	Status string `json:"status"`
	Amount float64 `json:"amount"`
}

var getTransactionTool = ai.DefineTool(
	"getTransaction",
	"特定のトランザクションIDのステータスを取得します",
	func(ctx context.Context, input *TransactionInput) (*TransactionOutput, error) {
		// これは標準的なデータベースクエリをシミュレートしています
		return &TransactionOutput{
			Status: "完了",
			Amount: 150.50,
		}, nil
	},
)

これを統合するには、Generateリクエストにツールを渡すだけです。

resp, err := ai.Generate(ctx, model,
	ai.WithTextPrompt("トランザクションTX123のステータスを教えてください。"),
	ai.WithTools(getTransactionTool),
)

GoはPythonよりも大幅に高速にJSONのマーシャリングを処理します。高負荷なシナリオでは、これにより各ツールの実行オーバーヘッドが削減され、より小さなインスタンスでより多くの同時実行ユーザーを処理できるようになります。

RAG:大規模データの処理

本番用AIには、一般的な知識だけでなく、特定のデータへのアクセスが必要です。Genkitは、ドキュメントローダー、インデクサー、リトリーバーのための標準インターフェースを提供します。5,000個のPDFドキュメントを処理する必要がある場合、Goのゴルーチン(goroutine)を使用すれば、CPUオーバーヘッドを最小限に抑えながらデータの取り込みと埋め込みを並列で実行できます。

高性能なRAGパイプラインにおける私の典型的なワークフローは、次の3つのステップで構成されます。

  1. Indexer(インデクサー): 処理されたデータをPineconeやCloud Firestoreなどのベクトルストアに送信します。
  2. Retriever(リトリーバー): ユーザーのセマンティッククエリに基づいて、関連するドキュメントチャンクを抽出します。
  3. Flow(フロー): ロジックをGenkit Flowでラップし、可観測性を確保します。
myRetriever := ai.DefineRetriever("myDocs", "local-docs", func(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) {
	// ここをベクトルデータベースの検索ロジックに置き換えます
	return &ai.RetrieverResponse{
		Documents: []*ai.Document{
			ai.DocumentFromText("GoはGoogleで開発されたコンパイル言語です。", nil),
		},
	}, nil
})

ai.DefineFlowを使用すると、自動トレースが提供されます。本番環境でクエリが失敗した際、トレースを調査してどのドキュメントが取得されたかを正確に確認できます。この可視性は、RAGの精度を微調整するために不可欠です。

Go AIの本番運用戦略

1. コンテキストの伝播を優先する

常にcontext.Contextをすべての関数に渡してください。Genkitはトレースやリクエストのキャンセルにこれを利用します。ユーザーが切断した場合、高価なトークンの浪費を防ぐために、AIの呼び出しは即座に停止されるべきです。

2. クライアントサイドのレート制限を実装する

GeminiやOpenAIには厳格なレート制限があります。golang.org/x/time/rateパッケージを使用して、ローカルのレートリミッターを構築しましょう。これにより、一人の攻撃的なユーザーがAPIクォータを使い果たし、他のユーザーに対してサービスが停止するのを防げます。

3. トークン消費量を監視する

Genkitの呼び出しをミドルウェアでラップし、トークンの使用量をログに記録します。Gemini 1.5 Flashはコスト効率が高いですが、大量のRAGアプリケーションでは予想外の請求が発生することがあります。ユーザーごとの入力・出力トークンを追跡することが、持続可能な利益率を維持する唯一の方法です。

4. ツールのスコープを絞る

大規模言語モデルは、選択肢が多すぎると精度が低下します。コンテキストごとにツールをグループ化してください。ユーザーが「請求」ダッシュボードを操作している場合は、請求関連のツールのみを提供します。これにより、レイテンシが短縮され、モデルが誤ったツール呼び出しを生成(ハルシネーション)するのを防げます。

GoとGenkitを選択することは、単に生の速度を求めることではありません。予測可能でメンテナンスが容易なシステムを構築することです。他の開発者がPythonの依存関係管理に苦労している間に、あなたは何千ものリクエストを処理できる、静的にリンクされた単一のバイナリを出荷できます。それこそが、AI時代におけるGoの真のアドバンテージです。

Share: