はじめに
いよいよ、これまでの知識を総動員して、LLM(大規模言語モデル)をAndroidアプリから実際に呼び出す方法を学びます。今回は、代表的なモデルであるOpenAIのGPTとAnthropicのClaudeを例に、そのAPI連携の実装方法を解説します。
1. OpenAIとAnthropicのAPIの比較
特徴 | OpenAI API (GPT-4, GPT-3.5) | Anthropic Claude API (Claude 3.5) |
---|---|---|
APIエンドポイント | https://api.openai.com/v1/chat/completions |
https://api.anthropic.com/v1/messages |
認証方法 | Authorization: Bearer YOUR_API_KEY |
x-api-key: YOUR_API_KEY anthropic-version: 2023-06-01
|
リクエスト形式 |
messages 配列 |
messages 配列 |
レスポンス形式 |
choices 配列内のmessage
|
content 配列内のtext
|
ストリーミング | サポート(stream: true ) |
サポート(stream: true ) |
必須パラメータ |
model , messages
|
model , messages , max_tokens
|
両者のAPIは非常に似ており、messages
という会話履歴の配列を送信して応答を受け取るという、基本的な設計思想は共通しています。ただし、リクエストヘッダーやレスポンスの構造に若干の違いがあります。
2. 環境準備とセキュリティ考慮事項
APIキーの安全な管理
APIキーは機密情報です。以下の方法で安全に管理しましょう:
1. local.propertiesを使用(開発環境):
# local.properties(Gitに含めない)
OPENAI_API_KEY=sk-your-openai-key
ANTHROPIC_API_KEY=your-anthropic-key
2. build.gradleでの読み込み:
android {
defaultConfig {
// local.propertiesから読み込み
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
buildConfigField "String", "OPENAI_API_KEY", "\"${properties.getProperty('OPENAI_API_KEY')}\""
buildConfigField "String", "ANTHROPIC_API_KEY", "\"${properties.getProperty('ANTHROPIC_API_KEY')}\""
}
}
3. Retrofit2を使ったAPI連携の実装
ステップ1: 依存関係の追加
// build.gradle (Module: app)
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
}
ステップ2: データモデルの定義
共通のMessageクラス:
data class Message(
val role: String, // "user", "assistant", "system"
val content: String
)
OpenAI向けモデル:
// リクエストボディ
data class OpenAIRequest(
val model: String,
val messages: List<Message>,
val max_tokens: Int = 1000,
val temperature: Double = 0.7,
val stream: Boolean = false
)
// レスポンスボディ
data class OpenAIResponse(
val id: String,
val choices: List<OpenAIChoice>,
val usage: Usage
)
data class OpenAIChoice(
val message: Message,
val finish_reason: String
)
data class Usage(
val prompt_tokens: Int,
val completion_tokens: Int,
val total_tokens: Int
)
Anthropic向けモデル:
// リクエストボディ
data class AnthropicRequest(
val model: String,
val messages: List<Message>,
val max_tokens: Int,
val temperature: Double = 0.7,
val stream: Boolean = false
)
// レスポンスボディ
data class AnthropicResponse(
val id: String,
val type: String,
val content: List<ContentBlock>,
val usage: AnthropicUsage
)
data class ContentBlock(
val type: String, // "text"
val text: String
)
data class AnthropicUsage(
val input_tokens: Int,
val output_tokens: Int
)
ステップ3: APIサービスインターフェースの定義
OpenAIのサービス:
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface OpenAIService {
@POST("v1/chat/completions")
suspend fun createChatCompletion(
@Body request: OpenAIRequest
): Response<OpenAIResponse>
}
Anthropicのサービス:
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface AnthropicService {
@POST("v1/messages")
suspend fun createMessage(
@Body request: AnthropicRequest
): Response<AnthropicResponse>
}
4. OkHttp InterceptorによるAPIキーとヘッダーの管理
class ApiKeyInterceptor(
private val apiKey: String,
private val apiType: ApiType
) : Interceptor {
enum class ApiType { OPENAI, ANTHROPIC }
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
.addHeader("Content-Type", "application/json")
when (apiType) {
ApiType.OPENAI -> {
requestBuilder.addHeader("Authorization", "Bearer $apiKey")
}
ApiType.ANTHROPIC -> {
requestBuilder
.addHeader("x-api-key", apiKey)
.addHeader("anthropic-version", "2023-06-01")
}
}
return chain.proceed(requestBuilder.build())
}
}
Retrofitインスタンスの作成
object ApiClient {
private fun createOkHttpClient(apiKey: String, apiType: ApiKeyInterceptor.ApiType): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(ApiKeyInterceptor(apiKey, apiType))
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
val openAIService: OpenAIService by lazy {
Retrofit.Builder()
.baseUrl("https://api.openai.com/")
.client(createOkHttpClient(BuildConfig.OPENAI_API_KEY, ApiKeyInterceptor.ApiType.OPENAI))
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(OpenAIService::class.java)
}
val anthropicService: AnthropicService by lazy {
Retrofit.Builder()
.baseUrl("https://api.anthropic.com/")
.client(createOkHttpClient(BuildConfig.ANTHROPIC_API_KEY, ApiKeyInterceptor.ApiType.ANTHROPIC))
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(AnthropicService::class.java)
}
}
5. Repository層の実装
sealed class LLMResult<T> {
data class Success<T>(val data: T) : LLMResult<T>()
data class Error<T>(val message: String, val exception: Throwable? = null) : LLMResult<T>()
data class Loading<T>(val isLoading: Boolean = true) : LLMResult<T>()
}
class ChatRepository {
suspend fun sendOpenAIMessage(messages: List<Message>): LLMResult<String> {
return try {
val request = OpenAIRequest(
model = "gpt-3.5-turbo",
messages = messages,
max_tokens = 1000
)
val response = ApiClient.openAIService.createChatCompletion(request)
if (response.isSuccessful) {
val responseBody = response.body()
val content = responseBody?.choices?.firstOrNull()?.message?.content
if (content != null) {
LLMResult.Success(content)
} else {
LLMResult.Error("レスポンスが空でした")
}
} else {
LLMResult.Error("APIエラー: ${response.code()} ${response.message()}")
}
} catch (e: Exception) {
LLMResult.Error("ネットワークエラー: ${e.message}", e)
}
}
suspend fun sendAnthropicMessage(messages: List<Message>): LLMResult<String> {
return try {
val request = AnthropicRequest(
model = "claude-3-5-sonnet-20241022",
messages = messages,
max_tokens = 1000
)
val response = ApiClient.anthropicService.createMessage(request)
if (response.isSuccessful) {
val responseBody = response.body()
val content = responseBody?.content?.firstOrNull()?.text
if (content != null) {
LLMResult.Success(content)
} else {
LLMResult.Error("レスポンスが空でした")
}
} else {
LLMResult.Error("APIエラー: ${response.code()} ${response.message()}")
}
} catch (e: Exception) {
LLMResult.Error("ネットワークエラー: ${e.message}", e)
}
}
}
6. ViewModel層での実装例
class ChatViewModel : ViewModel() {
private val repository = ChatRepository()
private val _messages = MutableLiveData<List<Message>>(emptyList())
val messages: LiveData<List<Message>> = _messages
private val _isLoading = MutableLiveData<Boolean>(false)
val isLoading: LiveData<Boolean> = _isLoading
private val _error = MutableLiveData<String?>()
val error: LiveData<String?> = _error
fun sendMessage(userMessage: String, useOpenAI: Boolean = true) {
viewModelScope.launch {
_isLoading.value = true
_error.value = null
// ユーザーメッセージを追加
val currentMessages = _messages.value?.toMutableList() ?: mutableListOf()
currentMessages.add(Message("user", userMessage))
_messages.value = currentMessages
// APIを呼び出し
val result = if (useOpenAI) {
repository.sendOpenAIMessage(currentMessages)
} else {
repository.sendAnthropicMessage(currentMessages)
}
when (result) {
is LLMResult.Success -> {
currentMessages.add(Message("assistant", result.data))
_messages.value = currentMessages
}
is LLMResult.Error -> {
_error.value = result.message
}
is LLMResult.Loading -> {
// 既に_isLoadingで管理済み
}
}
_isLoading.value = false
}
}
}
7. 使用上の注意とベストプラクティス
エラーハンドリング
- ネットワークエラー: タイムアウト、接続エラーの適切な処理
- APIエラー: レート制限、認証エラー、サーバーエラーの処理
- レスポンス検証: 空のレスポンスや不正な形式への対応
パフォーマンス最適化
- 適切なタイムアウト設定: 接続とリードタイムアウトの設定
- リクエスト制限: 同時リクエスト数の制限
- キャッシュ戦略: 適切なキャッシュポリシーの実装
セキュリティ
- APIキーの保護: 本番環境では環境変数やSecureな設定管理を使用
- 証明書ピニング: 本番環境での通信セキュリティ強化
- データ検証: 入力値の検証とサニタイズ
まとめ
-
OpenAI APIとAnthropic APIは、いずれも
messages
ベースの会話形式のAPIを採用しており、基本構造は似ていますが、認証方法やレスポンス形式に違いがあります - Retrofit2とデータクラスを使うことで、型安全なAPI連携を実現できます
- OkHttpのInterceptorを使用することで、APIキーなどの共通ヘッダーを安全かつ効率的に管理できます
- 適切なエラーハンドリングとセキュリティ対策を実装することで、本番環境でも安定したLLM連携が可能になります
次回は、今回学んだAPI連携の知識を基に、より実践的なリアルタイムチャットインターフェースとストリーミングレスポンスの実装について解説します。お楽しみに!