0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Android開発30日間マスターシリーズ - Day23: OpenAI API / Anthropic Claude APIとの連携 - Android アプリからのLLM呼び出し

Posted at

はじめに

いよいよ、これまでの知識を総動員して、LLM(大規模言語モデル)をAndroidアプリから実際に呼び出す方法を学びます。今回は、代表的なモデルであるOpenAIのGPTAnthropicの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連携の知識を基に、より実践的なリアルタイムチャットインターフェースストリーミングレスポンスの実装について解説します。お楽しみに!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?