结合公开资料 + 开源代码,逐层讲解如何设计一个 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 没有的专属难点”:
- 流式渲染:token 一个个到达,要边到边渲染 Markdown/代码块/表格,还要处理中断、重生成、滚动跟随。
- 实时音频管线:本地采集 → 回声消除/降噪 → 上行 → 服务端端到端语音模型 → 下行音频流式播放,且要支持随时打断(用户一开口就截停 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 + DIcore-data/ ← Repository (单一数据源)core-designsystem/ ← 主题、组件 (LoadingIndicator, Background)core-navigation/ ← 导航图core-preferences/ ← DataStorefeature-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,否则代码块未闭合会崩)。 - 代码块用
LazyColumn或AnnotatedString,边到边高亮。 - 滚动跟随:流式输出时自动滚到底部,但用户手动上滑后应停止跟随(否则强抢焦点很烦)。用一个
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 到生产的演进路径
- MVP:Compose + MVVM + Retrofit SSE 直连厂商 API(抄 lambiengcode),单模型、纯文本、本地 Room 历史。能跑、能流式。
- 模块化:拆成 core-* / feature-* 多模块(抄 skydoves),加 Hilt、加设计系统、加设置页。
- 自建网关:客户端不再直连厂商,改连自建网关(密钥不外泄 + 统一限流计费 + 多模型路由)。这步开始接近生产级。
- 多模态/语音:加图片上传、文档解析;加 WebSocket 实时语音管线。
- Agent:加工具调用 UI、联网搜索中间态、长链路任务编排。
每一步都能在前面三个案例里找到对照实现,不需要从零摸索。
八、一句话总结
表面是个聊天/语音助手壳,骨子里是一套**“以流式 token 为中心、端云协同、上下文为一等公民”的系统。客户端用 Compose + Flow 吃下流式,用模块化 + 单一数据源**控住复杂度;后端的物理重心从”数据”彻底挪到”算力与上下文”——这是它和上一代超级 App 的根本不同。