跳过正文
  1. 文章/

React Native 中的设备端 AI:使用 llama.cpp 运行 Qwen 1.7B

· loading · loading ·
杰瑞德·林斯基
作者
杰瑞德·林斯基
居住在韩国首尔的新兴领导者和软件工程师

我最近在我的 Curtain Estimator 应用中上线了一个完全在设备端运行的 AI 助手。没有 API 调用,没有云依赖,完全隐私。用户可以使用自然语言创建工作、搜索客户和管理项目——完全离线运行。

这篇文章介绍了我如何使用 llama.rn(llama.cpp 的 React Native 绑定)和 Qwen 1.7B(一个出人意料地强大的小型语言模型)来构建它。

云端 AI 的隐私问题
#

大多数移动应用中的 AI 功能是这样工作的:

  1. 用户输入消息
  2. 应用发送到 OpenAI/Anthropic/Google
  3. 响应返回
  4. 账单累积

对于窗帘安装业务应用,这意味着:

  • 客户姓名、地址、电话号码 → 发送给第三方
  • 项目详情、报价、备注 → 存储在外部服务器上
  • 合规问题(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”

  1. 模型<action>{"type":"search_customers","query":"Smith"}</action>
  2. 系统[TOOL_RESULT] Found: Jane Smith (id:42), Bob Smith (id:89)
  3. 模型:“我找到了两个 Smith——Jane 和 Bob。您要哪个?”
  4. 用户:“Jane”
  5. 模型<action>{"type":"create_job","customer_id":42}</action>
  6. 系统[TOOL_RESULT] Job J-0073 created
  7. 模型:“完成!已为 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 名测试用户中使用两个月后:

最常见的用例:

  1. “Create a job for [customer name]” - 68% 的查询
  2. “Find jobs for [customer]” - 15%
  3. “Update job [ID] to [status]” - 9%
  4. 关于应用的一般问题 - 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