
为你的移动应用构建端侧AI助手
构建完全在用户设备上运行的对话式AI助手的架构模式。模型选择、对话管理、UI模式和生产考虑事项。
端侧AI助手是由在用户手机上本地运行的语言模型驱动的对话界面。无云API。无网络依赖。即时响应。完全隐私。
本指南涵盖从模型选择到生产部署的完整架构。
架构概览
端侧助手有四个层次:
- 模型层: llama.cpp加载并运行GGUF模型
- 对话层: 管理聊天历史、系统提示词和上下文窗口
- 界面层: 带流式token显示的聊天UI
- 状态层: 持久化对话,管理模型生命周期
聊天的模型选择
对话式AI受益于更大的模型。微调后的3B模型是推荐的起点:
| 模型大小 | 对话质量 | 多轮连贯性 | 推荐用于 |
|---|---|---|---|
| 1B | 够用 | 2-3轮 | 简单问答、FAQ机器人 |
| 3B | 良好 | 5-8轮 | 完整聊天助手 |
微调对聊天至关重要。基础3B模型会生成通用回复。微调后的3B模型用你的品牌声音说话,了解你的产品,处理你的特定用例。
对话管理
提示词模板
每个模型系列都有特定的聊天模板。对于Llama 3.2:
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
You are the RecipeHelper assistant. Help users find and modify recipes. Always include prep and cook times.<|eot_id|><|start_header_id|>user<|end_header_id|>
Quick dinner for two?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
微调时,训练框架会自动处理模板格式。在推理时,你的应用必须在发送给llama.cpp之前将对话格式化为此模板。
上下文窗口管理
移动模型通常使用2048-4096 token的上下文窗口(可配置,但更大的窗口会使用更多内存并减慢推理速度)。对话可能很快超出这个限制。
滑动窗口方法: 保留系统提示词和最近的N轮对话。当窗口满时丢弃最旧的轮次:
func buildPrompt(systemPrompt: String, turns: [Turn], maxTokens: Int) -> String {
var prompt = formatSystem(systemPrompt)
var tokenCount = countTokens(prompt)
// 从最新到最旧添加轮次,窗口满时停止
var includedTurns: [Turn] = []
for turn in turns.reversed() {
let turnTokens = countTokens(formatTurn(turn))
if tokenCount + turnTokens > maxTokens - 512 { break } // 为回复预留512
includedTurns.insert(turn, at: 0)
tokenCount += turnTokens
}
for turn in includedTurns {
prompt += formatTurn(turn)
}
return prompt
}
摘要方法: 当对话超出窗口时,将较旧的轮次总结为紧凑的上下文,并附加到系统提示词前面。这在保持token预算的同时保留了关键信息。但这需要额外的一次推理调用。
对话持久化
将对话存储在本地存储中(iOS上的Core Data,Android上的Room),这样用户可以从上次中断处继续:
- 保存每条消息(角色、内容、时间戳)
- 保存对话元数据(标题、创建日期、最后活跃时间)
- 限制存储的对话数量以防止无限增长
- 允许用户删除对话
流式UI
聊天界面应在token生成时即时显示。这创造了快速、响应迅速的助手体验。
iOS (SwiftUI)
struct ChatView: View {
@StateObject var viewModel = ChatViewModel()
var body: some View {
ScrollView {
ForEach(viewModel.messages) { message in
MessageBubble(message: message)
}
}
.onChange(of: viewModel.streamingText) { _ in
// 自动滚动到底部
}
HStack {
TextField("Message", text: $viewModel.input)
Button("Send") { viewModel.send() }
}
}
}
Android (Compose)
@Composable
fun ChatScreen(viewModel: ChatViewModel) {
val messages by viewModel.messages.collectAsState()
val streaming by viewModel.streamingText.collectAsState()
LazyColumn {
items(messages) { message ->
MessageBubble(message)
}
if (streaming.isNotEmpty()) {
item { StreamingBubble(streaming) }
}
}
Row {
TextField(value = input, onValueChange = { input = it })
Button(onClick = { viewModel.send(input) }) { Text("Send") }
}
}
Token显示节奏
llama.cpp一次生成一个token。逐个显示每个token可能会导致视觉抖动。在更新UI之前缓冲2-3个token,以实现更平滑的文本呈现:
private var tokenBuffer = StringBuilder()
private var bufferCount = 0
fun onToken(token: String) {
tokenBuffer.append(token)
bufferCount++
if (bufferCount >= 3 || token.contains("\n")) {
updateUI(tokenBuffer.toString())
tokenBuffer.clear()
bufferCount = 0
}
}
模型生命周期管理
加载和卸载
模型加载需要1-3秒,取决于模型大小和设备。卸载是即时的。管理生命周期以平衡响应速度和内存:
- 首次AI交互时加载: 不要在应用启动时加载。在用户打开聊天功能时加载。
- 活跃会话期间保持加载: 当用户在聊天中时,保持模型在内存中。
- 离开时卸载: 当用户离开聊天界面时,卸载模型以释放内存。
- 处理内存警告: 注册系统内存警告,触发时卸载模型。
加载指示器
模型首次加载时显示简短的加载状态(1-3秒)。之后响应立即开始生成。用户已经习惯了新功能的短暂加载状态。
为你的助手微调
基础模型和微调模型之间的质量差距在聊天场景中非常显著:
| 指标 | 基础3B | 微调3B |
|---|---|---|
| 主题相关回复 | 60-70% | 92-96% |
| 格式遵循 | 55-65% | 94-98% |
| 领域准确性 | 50-60% | 88-94% |
| 语调一致性 | 40-50% | 90-95% |
聊天训练数据
创建涵盖以下方面的训练样本:
- 常见问题: 用户最常问的50-100个问题
- 边缘情况: 超出范围的问题,附带优雅的重定向回复
- 多轮模式: 包含3-5 轮自然追问的对话
- 风格样本: 用你的品牌声音和首选格式的回复
500-2,000个训练对话可以产出高质量的聊天助手。Ertas等平台处理训练流程: 上传对话样本,使用LoRA微调,导出GGUF。
生产考虑事项
回复质量护栏
端侧模型可能生成偏离主题或不正确的回复。实施轻量级护栏:
- 输入验证: 检查超长输入或明显的非文本内容
- 输出监控: 在本地记录回复主题以识别质量问题
- 反馈机制: 让用户标记不良回复。使用此反馈改进训练数据
性能监控
跟踪端侧指 标:
- 首token时间(应低于300ms)
- 每秒tokens数(应保持在10以上)
- 模型加载时间
- 推理期间的内存使用
- AI交互期间的崩溃率
模型更新
通过你正常的模型交付流程推送模型改进:
- 应用启动时检查更新(联网时)
- 后台下载
- 在下次聊天会话开始时替换模型文件
- 保留旧模型作为备用,直到新模型验证通过
最终结果是一个随处可用、即时响应、每次对话零成本、用户数据完全私密的聊天助手。
Ship AI that runs on your users' devices.
Free plan with 30 credits/mo, no card required. Paid plans from $25/mo USD.
Ship AI that runs on your users' devices.
Free plan with 30 credits/mo, no card required. Paid plans from $25/mo USD.


