← blog 架构与系统 · 2026-07-28

android-chatgpt-style-apps-case-study

结合豆包架构调研与两个开源项目(skydoves/ChatGPT-Android、lambiengcode/compose-chatgpt),从端云协同架构、Android 分层工程结构、SSE 流式响应、Markdown 渲染、实时语音管线到 Agent 工具调用,逐层拆解一个 ChatGPT/千问/豆包级移动端 AI 助理的设计方法论。

10 min read

结合公开资料 + 开源代码,逐层讲解如何设计一个 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)。

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

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

关键点:手机算力跑不动 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,否则代码块未闭合会崩)。
  • 代码块用 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 完全没有的实时音视频工程量。一条完整链路:

graph LR
    Mic["麦克风(AudioRecord)"] --> AEC["AEC/降噪(端侧小模型或WebRTC APM)"]
    AEC --> UP["上行音频帧(WebSocket/WebRTC)"]
    UP --> CLOUD["云端端到端语音模型"]
    CLOUD --> DOWN["下行音频帧流式"]
    DOWN --> PLAY["AudioTrack播放"]
    PLAY --> VAD["用户开口→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 的根本不同。

Sources

No external sources for this entry.

Related