深入 Android 端侧 AI 推理的远程配置与动态实验体系:从推理参数热更新到 A/B 分流的工程架构

去年在做端侧 AI 推理框架的工程化落地时,团队遇到了一个两难问题:模型已经随 APK 发版部署,但推理参数(阈值、采样策略、Prompt 模板)需要频繁调优。每次改动都要走发版流程,迭代周期被拉长到两周以上。

这促使我们搭建了一套端侧远程配置与实验体系。

端侧推理为什么需要远程配置

模型文件是静态的,但推理行为是动态的。 一个端侧推理链路里,模型权重只是其中一个变量。真正决定输出质量的,还有大量可调参数:

  • 温度系数(Temperature)控制生成随机性
  • Top-K / Top-P 采样阈值过滤低概率 token
  • Prompt 模板中 system prompt 的措辞调整
  • 后处理阶段的过滤阈值、分类置信度门槛

这些参数对产品体验的影响不亚于模型本身。一个 Top-P 值从 0.9 调到 0.85,就可能让对话风格从发散变严谨。但代码里直接写死了这些值,改一次等于一次发版。

迭代速度之外,实验验证是另一个刚需。以 Prompt 优化为例,三段不同的 system prompt 哪个更符合用户预期?内部评测样本有限,不够客观,必须在线上做分流验证。

配置中心的轻量化设计

服务端复用公司现有的配置中心(Apollo / Nacos 均可),端侧聚焦 拉取、缓存、注入 三个环节的封装。

核心数据模型:

data class InferenceConfig(
    val version: Long,
    val modelParams: ModelParams,
    val promptTemplates: Map<String, String>,
    val postProcess: PostProcessParams
)

data class ModelParams(
    val temperature: Float = 0.7f,
    val topP: Float = 0.9f,
    val topK: Int = 50,
    val maxTokens: Int = 2048
)

端侧启动时从配置中心拉取,本地用 MMKV 做持久化缓存,保证离线可用。核心机制是 版本号驱动更新——每次配置变更携带递增的 version 字段,端侧对比后决定是否覆盖本地缓存。

class ConfigManager(private val kv: MMKV) {
    
    fun syncConfig(remote: InferenceConfig) {
        val localVersion = kv.decodeLong(KEY_VERSION, 0L)
        if (remote.version <= localVersion) return
        
        kv.encode(KEY_VERSION, remote.version)
        kv.encode(KEY_CONFIG, Json.encodeToString(remote))
        notifyListeners(remote) // 通知推理引擎热更新
    }
    
    fun getConfig(): InferenceConfig {
        val cached = kv.decodeString(KEY_CONFIG) ?: DEFAULT_CONFIG
        return Json.decodeFromString(cached)
    }
}

notifyListeners 是热更新的关键。推理引擎持有 ConfigManager 的引用,配置变更后直接更新内存中的参数对象,不需要重启进程。

冷启动时,本地缓存的配置先于网络请求返回,保证首帧推理不等待网络。这套机制决定了 本地缓存就是兜底策略——默认配置要确保在最差情况下也能正常运行。

A/B 实验分流:端侧的特殊性

服务端的 A/B 实验依赖实时分流——请求打到服务端,根据用户 ID 哈希分配实验组,返回对应配置。端侧做不到这一点:模型在本地运行,没有请求可以作为分流的锚点。

我们的方案是 预分流 + 本地决策。配置中心下发的不再是一份配置,而是多份实验配置的集合:

data class ExperimentConfig(
    val baseConfig: InferenceConfig,           // 对照组
    val experiments: List<ExperimentGroup>     // 实验组
)

data class ExperimentGroup(
    val id: String,
    val trafficPercent: Int,    // 流量比例,0-100
    val config: InferenceConfig
)

端侧在首次加载配置时,根据用户 ID 哈希值和流量分配算法,确定该用户属于哪个实验组,后续会话中保持不变。

fun assignGroup(userId: String, groups: List<ExperimentGroup>): InferenceConfig {
    val hash = userId.hashCode().toLong() and 0x7FFFFFFF
    val bucket = (hash % 100).toInt()
    
    var accumulated = 0
    for (group in groups) {
        accumulated += group.trafficPercent
        if (bucket < accumulated) return group.config
    }
    return baseConfig // 兜底到对照组
}

哈希取模做分桶是端侧实验的经典方案,优势是 无网络依赖、确定性分流——同一个用户每次计算出的 bucket 一致,不会出现实验组来回跳变的情况。

这里踩过一个坑:最初用 kotlin.random.Random 做分流,结果每次冷启动用户被分到不同组,实验数据完全没法看。确定性是端侧实验的第一原则。

三端一致性的难题

端侧 AI 推理通常覆盖 Android、iOS,甚至还有 PC 端。配置下发时,跨端哈希一致性 是隐藏的大坑。

Java/Kotlin 的 String.hashCode() 算法与 Swift 的 hashValue 不同,更不用说 C++ 了。直接用语言自带哈希会导致同一个用户在不同端被分到不同实验组。

解法是统一哈希算法:

// 所有端统一使用同一哈希实现
object ConsistentHash {
    fun hash(input: String): Int {
        var h = 0
        for (c in input) {
            h = 31 * h + c.code
        }
        return h
    }
}

这套算法简单到可以手写在任何语言里,Android、iOS、后端保持一致毫无压力。

Prompt 模板实验是另一个高频场景。Prompt 本身是文本,改动频率远高于模型参数。我们把它做成 KV 结构下发,端侧按 key 索引:

{
  "prompt_templates": {
    "chat_default": "你是一个有帮助的助手...",
    "chat_exp_v2": "你是一个专业的技术顾问,回答时请...",
    "summary_short": "用一句话总结以下内容:"
  }
}

实验组引用不同的 key,模板内容可以独立迭代,不需要改客户端代码。

安全兜底与回滚

远程配置扛不住的一个风险:错误配置直接打挂端侧推理。比如 maxTokens 被误设为 0,或者 prompt 模板超出上下文窗口。

防护分三层:

第一层,类型校验。 端侧反序列化配置时做字段合法性检查,不合理的值丢弃并 fallback 到默认值。

fun validate(config: ModelParams): ModelParams {
    return config.copy(
        temperature = config.temperature.coerceIn(0f, 2f),
        topP = config.topP.coerceIn(0f, 1f),
        topK = config.topK.coerceIn(1, 200),
        maxTokens = config.maxTokens.coerceIn(1, 4096)
    )
}

第二层,本地兜底。 配置拉取失败或校验不通过,立刻回退到本地缓存的上一份有效配置。本地也没有,用 APK 内置的默认配置。

第三层,远程熔断。 配置平台侧设置灰度窗口,先推 5% 流量观察 crash 率和推理失败率,确认无异常后再全量。端侧上报配置版本号和错误码,平台侧出问题时一键回滚到上一个版本。

三层兜底跑了大半年,只出过一次问题——prompt 模板超出 token 限制触发了熔断,五分钟内回滚。

关键取舍

不建议把模型文件本身纳入远程配置体系。 一个量化模型动辄几百 MB,增量更新复杂度远超参数配置。模型的 A/B 实验更适合用多模型本地共存、配置指定加载哪个文件的方式,而不是热更新下载。

也不建议做过于复杂的端侧实验编排。 多层正交实验、动态流量调整这些,放在端侧实现成本和风险都太高。端侧实验保持”分配一次、会话内不变”的简单模型即可,复杂的正交实验交给服务端请求链路。