複数のLLM使い分ける「ayatori」を作ってみた
はじめに
LLMをプロダクションで使う際、こんな課題に直面したことはありませんか?
- オンプレミスでLLMを動かしているが、同時接続数に限界がある
- アクセスが集中するとレスポンスが悪化したりエラーが頻発する
- かといってすべてクラウドAPIに流すとコストが跳ね上がる
- 複数のLLMプロバイダーを使い分けたいが、切り替えロジックを書くのが面倒
こうした課題を解決するために、ayatori(あやとり)というOpenAI互換のAPIゲートウェイを開発しました。
この記事では、ayatoriの開発背景、主要機能、そして実装のポイントについて紹介します。
なぜ作ったのか - オンプレLLMのキャパシティ問題
問題の本質
オンプレミスでLLM(例えばOllamaなど)を運用する場合、ハードウェアリソースによって同時に処理できるリクエスト数が制限されます。この制限を無視してすべてのリクエストをオンプレに流し込むと、以下のような問題が発生します。
- レスポンス時間の著しい増加
- メモリ不足によるクラッシュ
- タイムアウトエラーの頻発
- 最悪の場合、サービス全体の停止
理想的な解決策
この問題を解決するには、最大キャパシティをもとに自動的にリクエスト先を切り替える仕組みが必要です。具体的には以下を実現します。
- 通常時はコストの安いオンプレLLMを使用
- オンプレのキャパシティが上限に近づいたら、自動的にクラウドのLLM(Anthropic、OpenAI、Azureなど)に切り替え
- この切り替えをアプリケーション側に意識させない
ayatoriは、このユースケースを実現するために開発しました。
ayatoriでできること
コア機能
ayatoriは以下の機能を提供します:
1. タグベースのインテリジェントルーティング
タグを使ってLLMを選択できます。例えば:
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "tags:fast&cheap",
"messages": [
{"role": "user", "content": "Hello!"}
]
}'
同じタグを持つ複数のLLMがある場合、優先度が最も高いものが選択されます。
2. 使用量制限による自動切り替え
各プロバイダーに使用量の上限(キャパシティ)を設定できます。
[[providers]]
id = "ollama-local"
type = "Ollama"
priority = 1 # 最優先
model = "llama3:8b"
tags = ["fast", "cheap"]
endpoint = "http://localhost:11434"
capacity = { requests = 100, input_tokens = 50000 }
[[providers]]
id = "anthropic-claude"
type = "Anthropic"
priority = 2 # オンプレがいっぱいの時のフォールバック
model = "claude-3-5-sonnet-20241022"
tags = ["fast", "cheap"]
endpoint = "https://api.anthropic.com"
capacity = { requests = 1000, input_tokens = 500000 }
優先度1のオンプレLLMがキャパシティ上限に達すると、自動的に優先度2のクラウドLLMに切り替わります。
このリクエスト数や入力トークン数はその瞬間における使用量から判定されます。
3. OpenAI互換API
デファクトスタンダードとなっているOpenAI APIの一部機能に対応しているため、既存のツールやライブラリをそのまま使えます。エンドポイントを変更するだけでayatoriを導入できます。
4. モデル名での使用も可能
LLMの設定ファイルに載っているモデルであればモデル名からLLMを直接使用することもできます。
これにより、ayatoriのセマンティクスに対応していないクライアントからのリクエストも処理することができます。
実用シナリオ
シナリオ1: オンプレとクラウドのハイブリッド運用
通常時:
全リクエスト → Ollama(オンプレ)
高負荷時:
├─ 同時100リクエストまで → Ollama(オンプレ)
└─ それ以上 → Claude/GPT(クラウド)
コストを抑えつつ、ピーク時のパフォーマンスも確保できます。
シナリオ2: タスクごとの最適化
# 軽量タスク用
[[providers]]
id = "ollama-light"
model = "llama3:8b"
tags = ["light", "summarize"]
priority = 1
# 高度なタスク用
[[providers]]
id = "claude-advanced"
model = "claude-3-5-sonnet-20241022"
tags = ["smart", "complex"]
priority = 1
アプリケーション側はtags:light&summarizeやtags:smart&complexを指定するだけ。内部的な切り替えロジックを意識する必要がありません。
シナリオ3: マルチクラウド戦略
タグ: "fast&cheap"
├─ Priority 1: Ollama(オンプレ)
├─ Priority 2: Claude(Anthropic)
└─ Priority 3: GPT-4(OpenAI)
コストと性能のバランスを取りながら、ベンダーロックインも回避できます。
アーキテクチャのポイント
モジュール構成
ayatoriは機能ごとに小さなクレートに分割し、それを統合する設計になっています:
ayatori/
├── configuration/ # プロバイダー設定のスキーマ定義
├── llm-composer/ # クライアントプールの管理
├── llm-selector/ # ルーティングロジック
│ ├── tag_selector.rs # タグによる絞り込み
│ └── usage/selector.rs # キャパシティチェック
├── token-measure/ # トークン数の推定
└── server/ # HTTP APIサーバー
この構成により、各機能の見通しが良くなり疎結合にしやすくなります。
ルーティングの仕組み
リクエストが来ると、以下の流れで処理されます:
1. リクエスト受信 (/v1/chat/completions)
↓
2. モデル指定を解析 (tags:fast&cheap)
↓
3. タグに一致するプロバイダーを抽出
↓
4. キャパシティ超過のプロバイダーを除外
↓
5. 優先度が最も高いプロバイダーを選択
↓
6. 選択されたプロバイダーにリクエスト転送
↓
7. レスポンスをOpenAI形式で返却
使用量の追跡は、インメモリまたはRedisで行えます。複数のayatoriインスタンスを立てる場合はRedisを使うことで、分散環境でも正確なキャパシティ管理が可能です。
なぜRustか
Rustを選んだ理由は3つあります:
- 高速性 - LLMのレスポンスタイムは重要。ゲートウェイ自体のオーバーヘッドは最小限に
- 安全性 - メモリ安全性が保証され、本番運用での安定性が高い
- 個人的な好み - やはり好きな言語で書くのが一番楽しく、長続きします😊
実際に使ってみる
クイックスタート
# リポジトリをクローン
git clone https://github.com/SiLeader/ayatori.git
cd ayatori
# ビルド
cargo build --release
# サンプル設定で起動
cargo run --release -- --config sample/config.toml
設定例
シンプルな設定ファイル(config.toml):
llm_configuration = "llm_configuration.toml"
[server]
listen = "0.0.0.0:8080"
client_fallback_enabled = true
[usage_store]
type = "Local" # または "Redis" for 分散環境
[token_measure]
type = "ByteLength"
magnification_ratio = 0.3
LLMプロバイダーの設定(llm_configuration.toml):
[[providers]]
id = "ollama-local"
default = true
type = "Ollama"
priority = 1
model = "llama3:8b"
tags = ["fast", "cheap", "local"]
credential_file = "credentials/ollama.toml"
endpoint = "http://localhost:11434"
capacity = { requests = 100 }
[[providers]]
id = "anthropic-claude"
type = "Anthropic"
priority = 2
model = "claude-3-5-sonnet-20241022"
tags = ["fast", "cheap", "smart"]
credential_file = "credentials/anthropic.toml"
endpoint = "https://api.anthropic.com"
capacity = { requests = 1000, input_tokens = 500000 }
リクエストを送ってみる
# タグで指定
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "tags:fast&cheap",
"messages": [
{"role": "user", "content": "Rustの良いところを教えて"}
]
}'
レスポンスには、どのプロバイダーが使われたかが含まれます:
{
"id": "123",
"object": "chat.completion",
"choices": [...],
"usage": {...},
"ayatori_client_id": "ollama-local" ← 使用されたプロバイダー
}
今後の展望
現在、ayatoriは基本的な機能を実装していますが、今後追加を検討している機能もあります:
- より高度なルーティング: 同じ優先度内でのロードバランシング
- リトライ機構: 一時的なエラー時の自動リトライ
- メトリクス機能: Prometheusエンドポイントの提供
- ストリーミング対応: OpenAI APIのストリーミングレスポンスのサポート
- コンテナの提供: Kubernetesなどでも使用できるポータビリティの提供
ただし、現状でも実用には十分な機能を備えていると思います。実際に使ってみて、必要な機能があればぜひIssueやPRでフィードバックをいただけると嬉しいです!
まとめ
ayatoriは、複数のLLMプロバイダーを賢く使い分けるためのゲートウェイです。
- オンプレLLMのキャパシティ管理という実際の課題から生まれた
- タグベースのルーティングでアプリケーション側の変更を最小化
- 使用量制限による自動切り替えでコストとパフォーマンスを両立
- OpenAI互換APIで既存ツールとシームレスに統合
マルチクラウド環境やオンプレ・クラウドのハイブリッド運用でLLMを使っている方、コスト最適化に興味がある方は、ぜひ試してみてください。
質問やフィードバックは、GitHubのIssueやディスカッションでお待ちしています!
この記事はAIの力を借りて執筆しました。
Discussion