点击开始动手实验


背景痛点:ChatGPT安卓SDK集成时的高频报错

把 ChatGPT 能力塞进自己的 App,听起来像“调几个接口”那么简单,真动手才发现坑比头发还多。下面这几条异常,几乎每位安卓同事都会踩一遍:

  • HTTP 429/503:官方云函数限流,高峰期直接“请稍后再试”,如果不做重试,用户只会看到一句冷冰冰的“网络错误”。
  • Token 失效 401:服务端为了安全把 JWT 有效期压到 15 min,刷新逻辑没写好,用户一出门就掉登录。
  • JSON 解析异常:模型返回字段偶尔多一层 data 嵌套,Gson 直接抛 JsonSyntaxException,Crash 率飙升。
  • 模型加载超时:弱网场景下首包响应 >10 s,SDK 内部默认 8 s 就抛超时,用户体验“秒退”。
  • WebSocket 断链 1006:切后台 30 s 被系统杀进程,回到前台没重连,用户继续说话却得不到回复。

这些报错不仅分散,还互相叠加:限流时疯狂重试又把 Token 刷爆,最后满屏都是 Toast。下面给出一条“从诊断到修复”的完整路径,全部代码基于 Kotlin Coroutine,可直接搬进生产。

技术对比:官方 SDK vs Retrofit+WebSocket 自实现

官方 SDK 的好处是“一键依赖”,但坑也藏在黑盒子里;自己搭 Retrofit+WebSocket 灵活,却要多写不少模板代码。先快速对比:

维度 官方 SDK Retrofit+WebSocket 自实现
接口更新 跟随版本发布,被动升级 自己改路径,响应快
重试策略 固定 3 次,无退避 可定制指数退避
日志观察 内部日志不可见 OkHttp 拦截器一键打印
包体积 ~3.8 MB 仅依赖 Okio/OkHttp,~1.1 MB
ProGuard 规则已内置 需手写 6 行规则
断线重连 自己写心跳,30 行代码

结论:

  • 想“最快跑通”→ 官方 SDK;
  • 想“稳定上线”→ 自实现更香,下文全部围绕自实现展开,但诊断思路两边通用。

实现方案:带指数退避的重试与统一错误处理

1. 指数退避 + 随机抖动

Kotlin 协程让“延迟”变得优雅:

suspend fun <T> retryWithBackoff(
    maxAttempts: Int = 5,
    initialDelay: Long = 300,
    factor: Double = 2.0,
    block: suspend (attempt: Int) -> T
): T {
    var currentDelay = initialDelay
    repeat(maxAttempts - 1) { attempt ->
        try {
            return block(attempt + 1)
        } catch (e: Exception) {
            if (e.isRetryable()) {
                delay(currentDelay + (0..100).random()) // 随机抖动
                currentDelay = (currentDelay * factor).toLong()
            } else throw e
        }
    }
    return block(maxAttempts) // 最后一次直接抛
}

fun Throwable.isRetryable() =
    this is HttpException && (code() == 429 || code() == 503)
  • 429/503 才重试,其余异常直接抛,避免“雪崩”。
  • 最大 5 次,退避到 4.8 s 左右,对移动端友好。

2. 统一错误处理器

把“业务码→文案”收敛到一处,Activity 只关心里面的 message

class ChatErrorHandler(private val resources: Resources) {

    fun convert(throwable: Throwable): ChatError =
        when (throwable) {
            is HttpException -> when (throwable.code()) {
                401 -> ChatError(AuthExpired, resources.getString(R.string.error_auth))
                429 -> ChatError(RateLimited, resources.getString(R.string.error_busy))
                503 -> ChatError(ServiceDown, resources.getString(R.string.error_maintenance))
                else -> ChatError(Unknown, "${throwable.code()}: ${throwable.message()}")
            }
            is JsonDataException -> ChatError(ParseError, resources.getString(R.string.error_parse))
            is SocketTimeoutException -> ChatError(Timeout, resources.getString(R.string.error_timeout))
            else -> ChatError(Unknown, throwable.localizedMessage ?: "Network error")
        }
}

data class ChatError(val type: ErrorType, val uiText: String)

在 ViewModel 里统一收集:

val errorFlow = MutableSharedFlow<ChatError>()

fun sendMessage(text: String) = viewModelScope.launch {
    runCatching { repo.chat(text) }
        .onFailure { errorFlow.emit(handler.convert(it)) }
}

Activity 只做 toast(error.uiText),再也不用写一堆 try/catch

避坑指南:ProGuard 与多线程状态

1. ProGuard 混淆规则

自实现时 OkHttp、Gson、协程都得保留:

# OkHttp
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**

# Gson
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.google.gson.** { *; }

# 协程
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler

漏写一条,Release 包就会遇到 ClassNotFoundException: kotlinx.coroutines.android.AndroidDispatcherFactory,而且只在线上炸。

2. 多线程下对话状态管理

WebSocket 回调在 IO 线程,UI 状态在 Main 线程,用 Channel 做线程间通信最安全:

private val _messages = Channel<ChatMessage>(64)
val messages: Flow<ChatMessage> = _messages.consumeAsFlow()

override fun onMessage(webSocket: WebSocket, text: String) {
    scope.launch(dispatchers.io) {
        val msg = parse(text)
        _messages.trySend(msg) // 线程安全
    }
}

千万别用 LiveData.postValue 高频刷屏,容易丢事件;Channel 背压默认挂起,更稳。

性能优化:日志拦截器与缓存策略

1. OkHttp 拦截器 + 埋点

把请求耗时、错误码直接写进埋点,方便灰度对比:

class PerfInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val start = SystemClock.elapsedRealtime()
        val request = chain.request()
        val response = chain.proceed(request)
        val cost = SystemClock.elapsedRealtime() - start
        Tracker.track("chat_api", mapOf(
            "code" to response.code,
            "cost" to cost,
            "endpoint" to request.url.encodedPath
        ))
        return response
    }
}

Release 包默认关掉 body 打印,既合规又省内存。

2. 响应缓存

模型回复对同一 prompt 往往 100 % deterministic,可做 1 min 内存缓存,减少流量:

val cache = LruCache<String, String>(50)

suspend fun chat(prompt: String): String =
    cache.get(prompt) ?: run {
        val answer = api.chat(prompt)
        cache.put(prompt, answer)
        answer
    }

注意把“用户敏感词”做 MD5 当 key,避免明文驻留内存。

互动环节:提交你的真实报错

我把最小可复现的工程模板放到了 GitHub(含本文全部代码 + 单元测试):

https://github.com/yourname/chatgpt-android-boilerplate

欢迎提 Issue 贴上你在 logcat 里遇到的奇葩报错,一起完善“ChatGPT 安卓端报错词典”。


写完这篇小结,我对“语音+文字”双通道的实时交互又起了新兴趣。顺手把同样思路搬到从0打造个人豆包实时通话AI动手实验里,官方把 ASR→LLM→TTS 整条链路封装成可拖拽模块,30 分钟就能跑通一个网页版“豆包”语音助手。我本地试了一遍,重试逻辑、错误码映射、拦截器埋点都能直接复用,基本零改造。如果你也想把“只读”的 ChatGPT 升级成“能听会说”的伙伴,不妨去实验里亲手连一连,小白也能顺利体验。

点击开始动手实验


Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐