2983 words
15 minutes
Android移动端chatgpt类应用案例分析

结合公开资料 + 开源代码,逐层讲解如何设计一个 ChatGPT / 千问 / 豆包级的 Android 移动端 AI 助理应用。

一、先看真实案例(联网检索结果)#

检索并拉取了三份可参照的一手资料:

案例性质参照价值
豆包手机 App 技术架构调研(2aran.com)对字节豆包的七层架构猜想,基于火山引擎/Seed 团队公开披露看清”LLM 原生 App”与传统 App 的本质差异
skydoves/ChatGPT-Android(GitHub,1.4k+ star)开源,Jetpack Compose + Hilt + Stream Chat SDK + 模块化多模块工程工程结构、DI、UI、会话编排的最佳实践
lambiengcode/compose-chatgpt(GitHub)开源,极简,直连 OpenAI SSE 流式流式响应的最小可运行实现(可直接抄)

下面把这些案例的精髓提炼成一套设计方法论。

二、本质认知:LLM 原生 App ≠ 传统 App#

豆包架构调研的核心结论(也是设计的总纲):

传统移动 App 后端 =「确定性请求/响应 + CRUD 数据库」; LLM 原生 App 后端 =「概率性 token 流 + GPU 推理集群 + KV Cache 当一等公民调度」。

落到 Android 客户端,这意味着 两个”传统 App 没有的专属难点”

  1. 流式渲染:token 一个个到达,要边到边渲染 Markdown/代码块/表格,还要处理中断、重生成、滚动跟随。
  2. 实时音频管线:本地采集 → 回声消除/降噪 → 上行 → 服务端端到端语音模型 → 下行音频流式播放,且要支持随时打断(用户一开口就截停 AI)。

豆包实时语音用「端到端联合建模」(语音理解+生成一体),把传统 ASR→LLM→TTS 三段拼接压成一个模型,换来 250ms/300ms 级延迟下降和自然打断感。这是”LLM 原生”对”拼接式语音助手”的结构性替换。你若做语音,要尽早决定走级联(易实现、延迟高)还是端到端(难、需自研/对接端到端模型)。

成本结构也倒挂:传统 App 单次请求成本趋近于零;LLM App 每次对话都按 token 烧钱。所以客户端设计要考虑:本地缓存历史、压缩上下文、按任务难度路由到不同档位模型(简单闲聊走 Lite,复杂推理走 Pro)。

三、整体架构:端云协同全景#

┌─────────────────────────── 手机端 (Android) ───────────────────────────┐
│ UI层 (Jetpack Compose) │
│ ├─ 会话列表 / 聊天流 (流式Markdown渲染) │
│ ├─ 语音通话页 (全双工音频) │
│ └─ 设置/账号/模型选择 │
│ ViewModel层 (MVVM/MVI, StateFlow) │
│ ├─ ChatViewModel ── 持有会话状态、接流式Flow │
│ └─ VoiceViewModel ── 音频管线状态机 │
│ Domain/Repository层 (单一数据源) │
│ ├─ ChatRepository ── 历史(Room) + 远端(SSE) 合流 │
│ ├─ StreamingClient ── SSE/WebSocket → Flow<String> │
│ ├─ VoiceEngine ── AudioRecord/Track + AEC/VAD │
│ └─ MemoryStore ── 上下文压缩/记忆持久化 │
│ Data层 │
│ ├─ Room (本地会话历史, off-line可读) │
│ ├─ DataStore (偏好/密钥) │
│ └─ Retrofit+OkHttp (SSE流) / WebSocket (语音) │
│ 端侧小模型(可选) │
│ └─ 唤醒词 / VAD / 降噪 (ONNX/MediaPipe) │
└──────────────────────────────────────────────────────────────────────┘
│ SSE (文本流) │ WebSocket/WebRTC (音频流)
▼ ▼
┌─────────────────────────── 云端 ─────────────────────────────────────┐
│ 接入/网关层 长连接 + 按token限流计费 + 就近接入 │
│ 编排/Agent层 Orchestrator: 模型推理 ↔ 工具调用(搜索/代码/多模态) ↔ 回填 │
│ 推理服务层 Continuous Batching / PagedAttention / Prefix Cache / PD分离 │
│ 模型层 路由 → Pro(难)/Lite(常规)/Mini(轻)/实时语音/视觉 │
│ 记忆/数据层 会话记忆 + RAG检索 + 多模态对象存储 │
│ 基础设施层 GPU集群 + veCCL互联 + 分布式KVCache(EIC) + 潮汐弹性调度 │
└──────────────────────────────────────────────────────────────────────┘

关键点:手机算力跑不动 Pro 级大模型,主对话必在云端;端侧只跑唤醒词/VAD/轻量降噪这种小模型。这是”端云协同”,不是”纯端侧”。

四、Android 客户端分层设计(可直接落地的工程结构)#

参照 skydoves/ChatGPT-Android 的多模块工程,这是目前社区公认的最佳实践:

app/ ← 启动、导航、组装
core-model/ ← 纯数据模型 (GPTMessage, GPTChatRequest/Response)
core-network/ ← Retrofit接口 + Interceptor + DI
core-data/ ← Repository (单一数据源)
core-designsystem/ ← 主题、组件 (LoadingIndicator, Background)
core-navigation/ ← 导航图
core-preferences/ ← DataStore
feature-chat/ ← 聊天功能 (ViewModel + Compose UI)
feature-login/ ← 登录功能

为什么这样切:每个 feature-* 只依赖 core-*,feature 之间互不可见;core-model 是纯 Kotlin(无 Android 依赖),可单测;core-network 封装所有 HTTP 细节,换协议只动这一层。

技术栈选型(社区主流,与案例一致):

  • UI: Jetpack Compose(声明式,天然适合”状态→UI”的流式更新)
  • 架构: MVVM + 单向数据流(或 MVI,大团队更稳)
  • DI: Hilt(Dagger 编译时安全)
  • 网络: OkHttp + Retrofit(@Streaming 做 SSE)
  • 本地: Room(历史)、DataStore(偏好)
  • 异步: Kotlin Coroutines + Flow(流式的天然载体)
  • 启动: AppStartup(比 ContentProvider 更轻)

五、关键技术点逐个拆解(附真实代码)#

1. 流式响应(SSE)——这是 LLM App 的”主干道”#

豆包调研原话:“传统 App 里流式是边角料,这里是主干道。” 文本走 SSE,语音走 WebSocket/WebRTC。

lambiengcode 的实现是最小可运行范本,三个要点:

(a) Retrofit 接口加 @Streaming,返回 ResponseBody 而非反序列化对象:

interface OpenAIApi {
@POST("v1/chat/completions")
@Streaming
fun textCompletionsTurboWithStream(@Body body: JsonObject): Call<ResponseBody>
}

为什么要 @Streaming + ResponseBody?默认 Retrofit 会把整个响应体读进内存再反序列化,那就失去了”边到边吐字”的意义。@Streaming 让你能拿到原始字节流,自己逐行读。

(b) OkHttp 加 Bearer 头(密钥注入):

skydoves 的做法是用 Interceptor,而不是硬编码:

class GPTInterceptor @Inject constructor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer ${BuildConfig.GPT_API_KEY}")
.build()
return chain.proceed(request)
}
}

并在 OkHttpClient 上设长超时(SSE 是长连接,默认 10s 超时会断流):

OkHttpClient.Builder()
.addInterceptor(GPTInterceptor())
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) // 流式必须放宽
.writeTimeout(15, TimeUnit.SECONDS)
.build()

(c) 把字节流转成 Kotlin Flow<String>——这是整个流式架构的灵魂。callbackFlow 包裹同步的逐行读取:

override fun textCompletionsWithStream(params: TextCompletionsParam): Flow<String> =
callbackFlow {
withContext(Dispatchers.IO) {
val response = openAIApi.textCompletionsTurboWithStream(params.toJson()).execute()
if (response.isSuccessful) {
val reader = response.body()!!.byteStream().bufferedReader()
while (true) {
val line = reader.readLine() ?: continue
when {
line == "data: [DONE]" -> close()
line.startsWith("data:") -> {
val token = parseDelta(line) // 解析 content 字段
if (token.isNotEmpty()) trySend(token)
}
}
}
} else { trySend("Failure: ${response.errorBody()?.string()}"); close() }
}
}

ViewModel 里收集这个 Flow,逐 token 更新 UI 状态:

class ChatViewModel(private val repo: ChatRepository) : ViewModel() {
private val _streamingText = MutableStateFlow("")
val streamingText = _streamingText.asStateFlow()
fun send(prompt: String) = viewModelScope.launch {
_streamingText.value = ""
repo.textCompletionsWithStream(prompt).collect { token ->
_streamingText.value += token // 每来一个 token,UI 自动重组
}
}
}

Compose 端只订阅 streamingText,就实现了”字一个个蹦出来”的效果。这就是为什么 Compose + Flow 是 LLM 客户端的黄金组合。

豆包调研还点出一个网关层细节:断线重连 + 偏移续传。弱网下 SSE 会断,好的客户端会带 Last-Event-ID 续传。开源案例都没做,但生产级必做。

2. 流式 Markdown 渲染——“LLM 专属难点一”#

token 是半个词、半个代码块地到的,不能等整段生成完再渲染。要点:

  • 用支持增量解析的 Markdown 库(如 compose-markdown / Markwon),把已到达的部分文本整体重渲染(而非增量 patch,否则代码块未闭合会崩)。
  • 代码块用 LazyColumnAnnotatedString,边到边高亮。
  • 滚动跟随:流式输出时自动滚到底部,但用户手动上滑后应停止跟随(否则强抢焦点很烦)。用一个 userScrolled 标志位。
  • 中断/重生成:发 cancel() 给 SSE 的 Call,UI 回退到上一条用户消息。

3. 会话管理与持久化——“上下文是新的数据库”#

豆包调研把 L2 记忆层定义为”会话记忆 + RAG 检索 + 多模态对象存储”。客户端侧落地:

  • 本地历史:Room 存所有会话与消息,offline 可读、可回看。
  • 上下文注入:每次请求把最近 N 条消息(或压缩后的摘要)塞进 messages 数组。skydoves 的 GPTChatRequest 就是 List<GPTMessage>(role: system/user/assistant)。
  • 上下文压缩(对应压缩阈值触发的摘要思路):消息超 token 阈值时,本地用小模型把旧消息摘要,或服务端做。客户端要能处理”带摘要续聊”。
  • 多端同步(可选):会话历史可跨设备同步——但这属于增量,不是 MVP 必需。

4. 实时语音管线——“LLM 专属难点二”#

这是传统 App 完全没有的实时音视频工程量。一条完整链路:

麦克风(AudioRecord) → AEC/降噪(端侧小模型或WebRTC APM) →
上行音频帧(WebSocket/WebRTC) → 云端端到端语音模型 →
下行音频帧流式 → AudioTrack播放 → [用户开口→VAD检测→中断下行+截停AI]

关键决策点:

  • 打断:必须有 VAD(人声检测)。用户一发声,立刻 cancel() 下行播放 + 发中断信号给服务端。豆包把打断响应延迟压到约 300ms。
  • 全双工:用 WebSocket 或 WebRTC,不是 HTTP。HTTP 请求-响应模型做不了双向同时流。
  • 端侧降噪/AEC:Android 有 AcousticEchoCanceler 系统级 API,复杂场景上 ONNX 小模型。
  • 如果不自研端到端语音模型,可走级联(ASR→LLM→TTS),易实现但延迟叠加、情绪丢失。豆包选端到端是为了砍掉这三段。

5. 工具调用 / Agent——“控制流从代码迁到模型”#

豆包 2.0 自披露具备工具调用、Search Agent、长链路任务能力。客户端落地:

  • 请求里带 tools 定义(搜索、代码执行、绘图等)。
  • 服务端返回 tool_call 事件(也是流式 SSE),客户端展示”正在搜索…”的中间态气泡。
  • 工具结果回填后,继续流式输出最终答案。
  • UI 要为”中间态”设计专门的组件(loading chip、引用来源卡片),这是 Agent App 区别于纯聊天的视觉差异。

6. 安全、密钥、限流#

  • 密钥绝不硬编码:skydoves 用 secrets.properties + Secrets Gradle Plugin,编译期注入 BuildConfig.GPT_API_KEY。生产级更推荐走自建网关,客户端只持短期 token(类似 Bearer 24h TTL 思路),不持厂商 API Key。
  • 越权/风控:服务端按 token 限流计费,客户端做”正在生成中”防连点。
  • 隐私:语音/图片上传要明确授权;本地 Room 加密(SQLCipher)。

六、与”不发版改行为”的工程含义#

豆包调研点出一个反直觉点:LLM App 加功能 ≠ 写代码发版。改 system prompt、加一个工具、换一档模型,就能改行为,部分免发版。

这对客户端设计的影响:把 system prompt、工具清单、模型路由表做成可远程下发的配置(类似 Firebase Remote Config / 自建配置中心),而不是写死在 APK 里。这样产品/运营能独立调优,无需走客户端发版流程。

七、从 MVP 到生产的演进路径#

  1. MVP:Compose + MVVM + Retrofit SSE 直连厂商 API(抄 lambiengcode),单模型、纯文本、本地 Room 历史。能跑、能流式。
  2. 模块化:拆成 core-* / feature-* 多模块(抄 skydoves),加 Hilt、加设计系统、加设置页。
  3. 自建网关:客户端不再直连厂商,改连自建网关(密钥不外泄 + 统一限流计费 + 多模型路由)。这步开始接近生产级。
  4. 多模态/语音:加图片上传、文档解析;加 WebSocket 实时语音管线。
  5. Agent:加工具调用 UI、联网搜索中间态、长链路任务编排。

每一步都能在前面三个案例里找到对照实现,不需要从零摸索。

八、一句话总结#

表面是个聊天/语音助手壳,骨子里是一套**“以流式 token 为中心、端云协同、上下文为一等公民”的系统。客户端用 Compose + Flow 吃下流式,用模块化 + 单一数据源**控住复杂度;后端的物理重心从”数据”彻底挪到”算力与上下文”——这是它和上一代超级 App 的根本不同。

Android移动端chatgpt类应用案例分析
https://sgjki547.top/posts/android移动端chatgpt类应用案例分析/
Author
SGJki
Published at
2026-07-28
License
CC BY-NC-SA 4.0