深入 Android 端云协同 AI 推理架构:从模型路由策略到离线降级的混合智能调度全链路

在做端侧 AI 功能时,我们踩过一个典型的坑:智能抠图在 Wi-Fi 下一切正常,QA 在地铁上测试却直接卡死,整个页面 ANR。排查下来发现,路由逻辑只判断了”是否联网”,没考虑网络质量和模型复杂度——一个 200MB 的模型在弱网下光上传图片就耗了 30 秒。

核心矛盾在于:决策粒度太粗。路由策略不能是简单的 if-else,得有一个感知上下文、动态权衡的多维度决策引擎。

路由决策引擎的四个维度

路由引擎就是一个评分函数,输入当前上下文特征,输出推理目标(本地 / 云端 / 混合)。我设计的引擎考虑四个维度:

路由分数 = W1 × 模型匹配度 + W2 × 网络质量 + W3 × 延迟预算 + W4 × 成本约束

每个维度归一化到 0-1,权重根据业务场景可调。

模型匹配度反映端侧模型的覆盖能力。比如端侧有一个 50MB 的通用分类模型,能处理 80% 的常见场景,但遇到细粒度识别时需要云端大模型兜底。匹配度的计算不是二元的——可以直接用端侧模型的输出置信度作为信号:置信度低于阈值(如 0.7)时,自动触发云端复核。

data class RoutingContext(
    val inputSizeBytes: Long,
    val taskType: TaskType,
    val latencyBudgetMs: Long,
    val modelConfidence: Float = 0f
)

fun computeRoute(ctx: RoutingContext, networkScore: Float): InferenceTarget {
    val matchScore = if (ctx.modelConfidence >= 0.7f) 1.0f else ctx.modelConfidence / 0.7f

    val composite = matchScore * 0.4f + networkScore * 0.3f +
                   (1f - ctx.latencyBudgetMs / 5000f).coerceIn(0f, 1f) * 0.3f

    return when {
        composite > 0.7f -> InferenceTarget.LOCAL
        composite > 0.3f -> InferenceTarget.CLOUD_FALLBACK
        else -> InferenceTarget.CLOUD_ONLY
    }
}

权重 0.4/0.3/0.3 不是拍脑门定的——我们压测了三周,在不同网络条件下跑了 5000 次推理,用 P95 延迟和成功率反推出的最优配比。实际落地时可以做成可配置项,通过远程参数下发动态调整。

网络感知:不止是判断有网没网

大部分 App 判断网络只靠一个 isConnected(),端云协同场景下这远远不够。需要的是网络质量曲线,而不是一个 bool 值。

在 Android 上拿到网络质量数据有三个途径:

// 1. ConnectivityManager 提供带宽估算
val cm = context.getSystemService(ConnectivityManager::class.java)
val caps = cm.getNetworkCapabilities(cm.activeNetwork)
val downStream = caps?.linkDownstreamBandwidthKbps ?: 0  // 估算下行带宽
val upStream = caps?.linkUpstreamBandwidthKbps ?: 0      // 估算上行带宽

// 2. 主动探测:小数据包 RTT
suspend fun probeRtt(): Long {
    val start = SystemClock.elapsedRealtime()
    val response = httpClient.head("https://api.example.com/ping")
    return SystemClock.elapsedRealtime() - start
}

// 3. 历史滑动窗口统计
class NetworkHistory(val windowMs: Long = 60_000) {
    private val samples = LinkedList<Pair<Long, Float>>()

    fun record(quality: Float) {
        samples.add(SystemClock.elapsedRealtime() to quality)
        // 清理过期数据
        while (samples.isNotEmpty() &&
               samples.first.first < SystemClock.elapsedRealtime() - windowMs) {
            samples.removeFirst()
        }
    }
    fun avg(): Float = if (samples.isEmpty()) 0f
        else samples.map { it.second }.average().toFloat()
}

三点数据交叉验证后,输出一个 0-1 的网络质量分。分数低于 0.3 时,即使模型匹配度很高,路由引擎也不会往云端发请求——直接走本地推理或降级策略。

实际踩过的坑:linkDownstreamBandwidthKbps 在部分国产 ROM 上返回 0,需要 fallback 到主动探测。WLAN 切换到 5G 时,系统回调有 1-2 秒延迟,这期间发出去的请求大概率失败,所以切换事件触发时要立刻冻结路由决策队列。

端侧降级与离线容灾

网络总有不可用的时候。离线场景的处理不是”没有了云端怎么办”,而是**“端侧能力的边界在哪里,如何让用户感知不到降级”**。

我们设计了三级端侧能力模型:

级别能力范围典型耗时适用场景
L1 全能力端侧大模型满血推理200-500msWi-Fi 离线、高质量端侧
L2 中等能力量化模型 / 简化推理路径50-150ms移动网络不稳定
L3 基础能力规则引擎 + 缓存兜底<10ms完全离线、低端机

关键在于 L2 和 L3 之间的切换要做到用户无感。以智能修图为例,L2 走量化模型还能出合理的结果,L3 直接用预设滤镜和模板。用户看到的是”修图完成”,只是效果有差异。我们对效果差异做了 A/B 测试:90% 的用户分辨不出 L2 和 L1 的差异,60% 分辨不出 L3 和 L2 的差异。

class DegradationManager(
    private val deviceCap: DeviceCapability,
    private val networkState: StateFlow<NetworkQuality>
) {
    fun currentLevel(): DegradationLevel {
        if (networkState.value.score > 0.5f && deviceCap.isHighEnd) {
            return DegradationLevel.L1
        }
        if (deviceCap.ramMb >= 4096 && networkState.value.score > 0.2f) {
            return DegradationLevel.L2
        }
        return DegradationLevel.L3
    }

    fun selectExecutor(task: InferenceTask): ModelExecutor = when (currentLevel()) {
        DegradationLevel.L1 -> LocalLargeModel.smallDispatcher()
        DegradationLevel.L2 -> QuantizedModel // 8bit 量化,模型体积压缩 75%
        DegradationLevel.L3 -> RuleEngineCache
    }
}

量化模型的加载时间也需要注意。L1 的模型常驻内存,L2 的量化模型按需加载,首次加载有 200-400ms 的冷启动成本。我们用一个简单的预热策略:当网络质量连续 3 次采样低于 0.5 时,后台预加载量化模型,不占用主线程。

请求队列与并发调度

请求队列管理是端云协同中另一个容易踩的坑。同一时刻可能有多个推理请求:用户快速切换滤镜、滑动浏览 AI 生成的内容、后台预计算下一帧。这些请求的优先级不同,统一走 FIFO 队列会拖慢交互响应。

sealed class InferencePriority : Comparable<InferencePriority> {
    data object CRITICAL : InferencePriority()    // 用户当前手势响应
    data class HIGH(val deadlineMs: Long) : InferencePriority()  // 可见区域
    data class MEDIUM(val page: String) : InferencePriority()    // 预加载
    data object LOW : InferencePriority()         // 后台计算

    private val ordinal: Int get() = when (this) {
        CRITICAL -> 0; is HIGH -> 1; is MEDIUM -> 2; LOW -> 3
    }
    override fun compareTo(other: InferencePriority) =
        ordinal.compareTo(other.ordinal)
}

val requestQueue = PriorityQueue<InferenceRequest>()

suspend fun schedule(request: InferenceRequest) {
    requestQueue.add(request)
    // CRITICAL 请求取消同类型的排队低优请求,避免重复推理
    if (request.priority == InferencePriority.CRITICAL) {
        requestQueue.removeAll { it.taskId == request.taskId &&
                                it.priority > InferencePriority.CRITICAL }
    }
}

CRITICAL 优先级还有一个去重逻辑:用户快速滑动时,同一个任务可能堆积多次请求,只保留最新的那条,避免无谓的 GPU 资源消耗。

几个决策的取舍

回头看整个架构,关键取舍有三处。

路由决策放客户端还是服务端? 我选了客户端。服务端虽然全局信息更全,但多了一次网络往返,延迟上不划算。客户端用 60 秒滑动窗口的历史数据做本地判断,足够覆盖大部分场景。代价是客户端逻辑稍重——增加约 200KB 的包体积,可以接受。

模型匹配度评估的成本。 用端侧模型推理置信度作为路由信号,意味着每次请求前都多跑一次推理。我们在高通 8 Gen 2 上测了下,这个额外开销约 15-30ms,比一次失败云端请求的损耗小一个数量级,完全可以接受。

降级效果的用户感知。 技术上能做到 L3 无缝切换,但产品侧要决定 L3 的结果是否标记”离线模式”。我们当时的结论是不标记——标记反而强化了用户对”效果打折”的感知,不标记的情况下投诉率反而更低。这是一个反直觉的发现。

如果你在做类似的端云协同方案,建议先花时间把网络质量评估做扎实——它是整个路由引擎的底座,其他策略再精巧,底座不准全白搭。量化模型的加载策略也值得前期规划,不要等到设备兼容性问题暴露了再补救。