はじめに
昨日はOpenAIやAnthropicといった特定のLLM APIとの連携方法を学びました。しかし、異なるAPIを切り替えるたびにコードを修正するのは非効率です。今日は、前回触れた MCP(Multi-Cloud Platform) の概念をさらに深掘りし、統一されたプロトコルに基づいてLLMと通信する独自のクライアントライブラリを実装する方法を学びます。
1. なぜプロトコル準拠のクライアントが必要か?
LLMサービスは日々進化しており、新しいモデルやプロバイダーが次々と登場します。アプリが特定のLLMに直接依存していると、モデルを切り替えたり、新しいサービスを統合したりするたびに、大規模なコード修正が必要になります。
この問題を解決するのが、プロトコル(規約) です。
- プロトコル(規約)の定義: アプリとLLMの間でやり取りされるデータの形式と、通信のルールを事前に定めます。
- プロトコル準拠のクライアント: この規約に従って通信を行うクライアント(通信ライブラリ)を独自に作成します。
これにより、LLMの背後にあるプロバイダーが変更されても、アプリの主要なロジックを修正する必要がなくなります。
2. MCPプロトコルの設計(例)
ここでは、シンプルなLLM連携プロトコルを設計してみましょう。
リクエストのプロトコル
LLMへのリクエストは、以下の形式で統一します。
{
"model_id": "gemini-pro",
"messages": [
{
"role": "user",
"content": "今日の天気は?"
}
],
"stream": true
}
-
model_id
: 使用するLLMを識別するためのID(例:"gpt-4o"
,"claude-3-opus"
,"gemini-pro"
)。 -
messages
: 会話履歴。OpenAIやAnthropicの形式に合わせることで、多くのLLバイブラリと互換性を持たせます。 -
stream
: ストリーミング応答を要求するかどうか。
レスポンスのプロトコル
LLMからの応答は、ストリーミングを考慮して以下の形式で統一します。
{
"chunk_id": "...",
"status": "success",
"data": {
"role": "assistant",
"content": "こんにちは"
}
}
-
chunk_id
: ストリーミングされた応答の断片を識別するID。 -
status
: 応答の状態(例:"success"
,"error"
)。 -
data
: 実際の応答データ。
3. MCPクライアントの実装
このプロトコルに基づいて、Androidアプリから利用できる独自の通信ライブラリを作成します。Retrofit2とOkHttpを組み合わせることで、この実装は非常に簡単になります。
ステップ1: ベースとなるクライアントインターフェースの定義
BaseClient
は、どのLLMプロバイダーにも依存しない、抽象的な通信の窓口です。
interface LLMClient {
fun streamCompletion(request: ChatRequest): Flow<ChatCompletionChunk>
}
-
ChatRequest
やChatCompletionChunk
は、先ほど設計したプロトコルに準拠するデータクラスです。 -
Flow
は、ストリーミング応答を扱うのに最適なCoroutinesの機能です。
ステップ2: 各プロバイダー向けのクライアント実装
次に、LLMClient
インターフェースを実装する、各プロバイダー専用のクラスを作成します。
例:OpenAIのクライアント
class OpenAILlmClient(private val apiService: RetrofitOpenAIService) : LLMClient {
override fun streamCompletion(request: ChatRequest): Flow<ChatCompletionChunk> {
val openAiRequest = OpenAiRequestMapper.toOpenAiRequest(request)
// RetrofitのAPI呼び出し
return flow {
apiService.streamCompletion(openAiRequest).body()?.source()?.let { source ->
val reader = source.reader()
while (true) {
val line = reader.readLine() ?: break
// プロトコルに準拠した形式に変換
val chunk = OpenAiResponseMapper.toChatCompletionChunk(line)
emit(chunk) // Flowにデータを流す
}
}
}
}
}
-
Mapperクラス: 外部のAPI形式(
OpenAiRequest
)と内部のプロトコル形式(ChatRequest
)を変換するためのクラスです。これにより、両者の形式が分離され、変更の影響が小さくなります。
ステップ3: ViewModelからの利用
ViewModelは、特定のプロバイダーに依存せず、LLMClient
インターフェースを通じて通信を行います。
class ChatViewModel(private val llmClient: LLMClient) : ViewModel() {
fun sendMessage(userMessage: String) {
viewModelScope.launch {
llmClient.streamCompletion(ChatRequest(...)).collect { chunk ->
// UIをリアルタイムで更新
_chatState.value = _chatState.value.appendChunk(chunk.content)
}
}
}
}
この実装であれば、ChatViewModel
は、裏側でOpenAIが動いているのか、Anthropicが動いているのかを知る必要はありません。DI(依存性注入)を使って、実行時に適切なLLMClient
の実装を切り替えるだけで、異なるLLMを利用できるようになります。
4. まとめ
- プロトコル準拠のクライアントは、アプリを特定のLLMプロバイダーから独立させ、高い拡張性を持たせます。
- Mapperクラスを導入することで、外部APIの形式とアプリ内部のデータ形式を分離し、保守性を高めます。
-
Flow
とCoroutinesを組み合わせることで、複雑なストリーミング通信をシンプルに扱うことができます。
これらの設計パターンは、今日のLLMアプリ開発において不可欠な知識です。次回は、このクライアントを使って、実際にチャットインターフェースを構築し、ユーザーからの入力とLLMの応答を画面上でリアルタイムにやり取りできるようにします。お楽しみに!