我最近在我的 Curtain Estimator 应用中上线了一个完全在设备端运行的 AI 助手。没有 API 调用,没有云依赖,完全隐私。用户可以使用自然语言创建工作、搜索客户和管理项目——完全离线运行。
这篇文章介绍了我如何使用 llama.rn(llama.cpp 的 React Native 绑定)和 Qwen 1.7B(一个出人意料地强大的小型语言模型)来构建它。
云端 AI 的隐私问题#
大多数移动应用中的 AI 功能是这样工作的:
- 用户输入消息
- 应用发送到 OpenAI/Anthropic/Google
- 响应返回
- 账单累积
对于窗帘安装业务应用,这意味着:
- 客户姓名、地址、电话号码 → 发送给第三方
- 项目详情、报价、备注 → 存储在外部服务器上
- 合规问题(GDPR、数据驻留要求)
- 月度 API 账单随使用量增长
替代方案:直接在用户手机上运行 AI 模型。
为什么现在可行了#
一年前,这还不切实际。但三件事发生了变化:
1. 量化模型非常小 Qwen 1.7B 使用 Q4_K_M 量化只有 1.1 GB——比大多数游戏还小。下载一次后存储在应用中。
2. 移动 GPU 很快 llama.cpp 利用 Metal(iOS)和 Vulkan(Android)进行 GPU 加速。在 iPhone 14 Pro 上,我能达到约 15 tokens/秒——足以进行实时流式输出。
3. 小模型变聪明了 Qwen 1.7B 可以遵循结构化指令、解析 JSON 并进行多步推理。非常适合业务逻辑,而不是创意写作。
选择合适的模型#
在确定 Qwen3-1.7B 之前,我测试了四个小模型:
TinyLlama 1.1B(637 MB) 速度很快,但在结构化输出方面表现不佳。经常幻觉出客户 ID 或忘记必填字段。
Phi-3-mini(1.8 GB) 推理能力强,但太冗长。简单查询会生成 200 多字的回复,而我只需要 20 个字。
Gemma-2B(1.2 GB)
分类任务快速准确,但函数调用能力弱。无法可靠地输出我的工具系统所需的 <action> 标签。
Qwen3-1.7B(1.1 GB) ✅
完美平衡:可靠的结构化输出,精确遵循指令,支持使用 <think> 标签的思维链推理。
Q4_K_M 量化使用 4 位权重和 k-means 聚类——比全精度小 75%,质量损失仅约 5%。
集成:llama.rn 设置#
llama.rn 库为 React Native 封装了 llama.cpp。安装很简单:
npm install llama.rn
cd ios && pod install然后配置模型下载:
const MODEL_URL = "https://huggingface.co/unsloth/Qwen3-1.7B-GGUF/resolve/main/Qwen3-1.7B-Q4_K_M.gguf";
const MODEL_PATH = FileSystem.documentDirectory + "llama-models/Qwen3-1.7B-Q4_K_M.gguf";
const downloadModel = async () => {
const downloadResumable = FileSystem.createDownloadResumable(
MODEL_URL,
MODEL_PATH,
{},
(progress) => {
const pct = progress.totalBytesWritten / progress.totalBytesExpectedToWrite;
setDownloadProgress(pct);
}
);
await downloadResumable.downloadAsync();
};模型在首次使用时下载,带有进度追踪。WiFi 下需要 2-3 分钟,LTE 下大约 5-8 分钟。
下载后,使用 GPU 加速加载:
const ctx = await initLlama({
model: MODEL_PATH,
n_ctx: 8192, // 8K context window
n_gpu_layers: 99, // Use GPU for all layers
});n_gpu_layers: 99 至关重要——通过将计算卸载到 Metal/Vulkan 而不是 CPU,可获得约 5 倍的速度提升。
流式推理#
用户期望实时响应,而不是 10 秒的加载动画。llama.rn 支持逐 token 流式输出:
let fullResponse = "";
await llamaContext.completion(
{
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: "Create a job for John Smith" }
],
n_predict: 512,
temperature: 0.7,
top_p: 0.8,
},
(data) => {
// Called for each token
fullResponse += data.token;
setStreamingText(fullResponse);
}
);UI 随着 token 的流入实时更新。在现代手机上:
- 首个 token:约 200ms(提示处理)
- 后续 token:每个约 50-80ms
与网络绑定的 API 相比,感觉是即时的。
小模型的函数调用#
GPT-4 的函数调用使用 JSON schema。小模型在这方面表现不佳——它们会输出格式错误的 JSON 或遗漏必填字段。
相反,我使用了更简单的基于 XML 的协议:
const systemPrompt = `You are an AI assistant. When you need to take an action, output:
<action>{"type":"search_customers","query":"Smith"}</action>
Available actions:
- search_customers: {"type":"search_customers","query":"John"}
- create_job: {"type":"create_job","customer_id":5,"alias":"Living Room"}
- update_job: {"type":"update_job","job_id":"J-0042","status":"quoting"}
Rules:
1. ONE action tag per response, at the very end
2. When searching/creating → end with <action>
3. When just chatting → no action tag
`;为什么用 XML 标签而不是纯 JSON?
- 清晰的分隔符:
<action>和</action>是明确的起止标记 - 易于解析:简单正则,无需 JSON schema 验证
- 自文档化:示例展示了确切的格式
- 容错性好:即使模型添加了额外文本也能工作
当模型输出一个 action 时,我解析它:
function parseAction(text: string): Action | null {
const match = text.match(/<action>([\s\S]*?)<\/action>/);
if (!match) return null;
try {
return JSON.parse(match[1].trim());
} catch {
return null;
}
}然后执行它并将结果注入回对话:
const action = parseAction(modelResponse);
if (action) {
const result = await executeAction(action);
// Inject result as a system message
conversationHistory.push({
role: "user",
content: `[TOOL_RESULT] ${result.message}\n\n→ NEXT: Tell the user what happened.`
});
// Continue generation
await generateNextResponse();
}这实现了多步工作流。例如:
用户:“Create a job for Smith”
- 模型:
<action>{"type":"search_customers","query":"Smith"}</action> - 系统:
[TOOL_RESULT] Found: Jane Smith (id:42), Bob Smith (id:89) - 模型:“我找到了两个 Smith——Jane 和 Bob。您要哪个?”
- 用户:“Jane”
- 模型:
<action>{"type":"create_job","customer_id":42}</action> - 系统:
[TOOL_RESULT] Job J-0073 created - 模型:“完成!已为 Jane Smith 创建工作 J-0073。”
使用 <think> 标签的思维链#
小模型受益于显式的推理步骤。Qwen 支持思考模式:
const systemPrompt = `Before each response, wrap your reasoning in <think> tags:
<think>
INTENT: what does the user want?
HAVE: what data do I already have?
NEED: what's still missing?
DECISION: call tool | ask user | just respond
</think>
Then output your actual response.`;模型的内部推理:
<think>
INTENT: create a job
HAVE: customer query "Smith"
NEED: exact customer_id
DECISION: search first
</think>
<action>{"type":"search_customers","query":"Smith"}</action>我从 UI 中剥离 <think> 标签,但在开发过程中显示它们用于调试。这极大地提高了可靠性——模型会自己理清逻辑。
处理对话和上下文#
8K 上下文窗口很快就会被工具结果填满。大约 30 轮对话后就达到了限制。
解决方案:对话压缩。我让模型进行总结:
const compactPrompt = `Summarize this conversation in under 200 words.
Include: user's goal, customers/jobs created (with IDs), and any unfinished tasks.
${conversationHistory.map(m => `${m.role}: ${m.content}`).join('\n\n')}`;
const summary = await chat([{ role: "user", content: compactPrompt }]);
// Replace history with summary
conversationHistory = [
{ role: "user", content: `[CONTEXT SUMMARY]\n${summary}\n[END SUMMARY]` }
];50 条消息的对话压缩到约 150 个 token。模型可以从摘要继续,不会丢失已创建的工作或客户 ID。
会话持久化#
聊天会话保存到我的 Django 后端:
interface SessionMessage {
role: "user" | "assistant";
content: string; // Stripped for display
raw: string; // Full with <think> and <action> tags
}
await api.createAIChatSession({
organization_id: orgId,
title: firstUserMessage.slice(0, 60),
messages: [
{ role: "user", content: userText, raw: userText },
{ role: "assistant", content: cleanedResponse, raw: fullModelOutput }
]
});双重存储(content vs raw)实现了:
- UI 回放:在聊天历史中显示干净的消息
- 精确恢复:使用包含 action 的完整上下文恢复
用户可以通过历史记录下拉菜单在对话之间切换,类似于 ChatGPT。
性能:生产环境基准测试#
iPhone 14 Pro(A16 Bionic,6GB RAM):
- 模型加载:约 3 秒
- 首个 token:180-220ms
- 流式输出:14-16 tokens/秒
- 内存:约 1.8 GB
Samsung Galaxy S23(Snapdragon 8 Gen 2,8GB RAM):
- 模型加载:约 4 秒
- 首个 token:250-300ms
- 流式输出:10-12 tokens/秒
- 内存:约 2.1 GB
iPhone 11(A13,4GB RAM):
- 模型加载:约 6 秒
- 首个 token:400-500ms
- 流式输出:6-8 tokens/秒
- 内存:约 2.2 GB(低内存时偶尔崩溃)
建议:6GB+ RAM 以获得流畅体验。4GB 可以工作但可能需要在应用进入后台时卸载模型。
内存管理#
如果应用使用过多内存,iOS 会杀掉它。在后台时卸载模型:
useEffect(() => {
const subscription = AppState.addEventListener("change", (nextAppState) => {
if (nextAppState === "background") {
llamaContext?.release(); // Free ~2 GB
} else if (nextAppState === "active") {
loadModel(); // Reload when foregrounded
}
});
return () => subscription.remove();
}, []);这可以防止内存警告并保持应用响应。
电池影响#
在设备端运行推理会消耗电量。实际测试结果:
| 使用模式 | 额外电池消耗 |
|---|---|
| 轻度(每天 5-10 次查询) | 每天 <1% |
| 中度(每天 20-30 次查询) | 每天约 3-4% |
| 重度(每天 50+ 次查询) | 每天约 6-8% |
GPU 加速更快但比 CPU 消耗更多电量。这个权衡是值得的——用户更喜欢即时响应。
真实世界的结果#
在约 200 名测试用户中使用两个月后:
最常见的用例:
- “Create a job for [customer name]” - 68% 的查询
- “Find jobs for [customer]” - 15%
- “Update job [ID] to [status]” - 9%
- 关于应用的一般问题 - 8%
成功率: 91% 的查询在首次尝试时成功完成
失败模式:
- 未找到客户(用户拼写错误) - 4%
- 上下文溢出(很长的对话) - 3%
- 幻觉数据(模型编造客户 ID) - 2%
在添加了带有明确 customer_id=42 字段的结构化工具结果后,幻觉问题得到了显著改善。小模型需要这种脚手架。
成本比较#
设备端(当前):
- 基础设施:$0/月
- 每用户成本:$0(一次性 1.1 GB 下载)
- 可扩展到:无限用户
云端替代方案(估计):
- 平均对话:约 2,000 tokens
- 200 用户 × 30 次对话/月 = 6,000 次对话
- 6,000 × 2,000 = 1200 万 tokens/月
- OpenAI GPT-3.5:$18/月
- OpenAI GPT-4:$360/月
- Anthropic Claude:$180/月
设备端方案立即收回了成本。即使在规模化(10,000 用户)时,成本仍然是 $0。
需要了解的限制#
1. 模型能力 Qwen 1.7B 擅长结构化任务,但不擅长:
- 复杂推理(多跳问题)
- 事实知识(它不是搜索引擎)
- 创意内容生成
- 细微的语言理解
围绕小模型擅长的领域设计功能。
2. 设备要求 最低:3 GB RAM,2 GB 可用存储空间 推荐:6 GB RAM,64 位处理器,GPU 支持
旧设备(iPhone 8、Galaxy S9)表现不佳。考虑功能检测和优雅降级。
3. 模型更新 要更新模型,用户需要重新下载 1.1 GB。我在存储路径中进行了版本控制:
const MODEL_PATH = `${FileSystem.documentDirectory}llama-models/v2/Qwen3-1.7B-Q4_K_M.gguf`;发布新模型版本时递增路径。旧模型在卸载时自动删除。
平台差异:iOS vs Android#
iOS(Metal):
- 推理更快(比 Android 快约 20%)
- 更好的内存管理
- 模型默认备份到 iCloud(可通过
NSURLIsExcludedFromBackupKey禁用)
Android(Vulkan/OpenCL):
- 设备间差异更大
- 一些旧 GPU 不支持 Vulkan(回退到 CPU)
- 存储不自动备份
在两个平台上都要测试——性能可能差异很大。
调试技巧#
1. 启用详细日志
const ctx = await initLlama({
model: MODEL_PATH,
n_ctx: 8192,
n_gpu_layers: 99,
verbose: true, // Logs every token + timings
});2. 追踪 token 数量
const tokens = fullResponse.split(/\s+/).length * 1.3; // Rough estimate
console.log(`Generated ${tokens} tokens in ${duration}ms`);3. 监控内存
import { MemoryInfo } from 'react-native-device-info';
const memoryUsage = await MemoryInfo.getUsedMemory();
console.log(`Memory: ${(memoryUsage / 1024 / 1024).toFixed(0)} MB`);4. 离线测试
开启飞行模式并验证一切正常。模型应该从缓存加载,推理应该完成,只有后端持久化应该优雅地失败。
接下来#
多模态支持 llama.cpp 支持视觉模型(LLaVA、Qwen2-VL)。想象一下:“这是窗帘面料的照片——添加到工作中。”
微调 在领域特定数据(过去的工作描述、客户交互)上训练 LoRA 适配器。可以提高 20-30% 的准确率。
语音接口 结合 Whisper.cpp 实现设备端语音识别。完全离线的语音助手。
联邦学习 在不共享原始数据的情况下聚合用户间的模型改进。隐私保护的个性化。
你应该构建这个吗?#
适合的场景:
- 处理敏感数据的业务应用
- 离线优先的工作流
- 结构化任务(分类、数据录入、搜索)
- 大规模时对成本敏感
不适合的场景:
- 开放式聊天(使用 Claude/GPT-4)
- 知识密集型任务(小模型训练数据有限)
- 需要支持低端设备
- 快速变化的领域知识
总结#
设备端 AI 提供即时响应、离线功能和零边际成本。隐私是额外的好处。
技术栈:
- llama.rn:llama.cpp 的 React Native 绑定
- Qwen 1.7B Q4_K_M:紧凑的指令遵循模型
- 自定义工具系统:基于 XML 的函数调用
- 流式优先架构:逐 token 的 UI 更新
总实现时间:约 12 小时开发,$0/月基础设施成本,离线工作,完全隐私。
如果你正在构建处理敏感数据的业务应用,这现在是一个切实可行的选择。工具好用,性能到位,成本很难被超越。
有问题?反馈?我是 @jaredlynskey。llama.rn 库在 github.com/mybigday/llama.rn。

