在我小时候,我记得妈妈有一部安卓手机,那是她在丢失了黑莓手机后不久买的三星Galaxy Note 3。在那个时候,拥有16GB存储空间的手机就已经被视为尖端科技了;能够在一部手机上存储五部720p分辨率的电影,这种体验真的让人觉得不可思议。
当时大多数旗舰手机的RAM容量都在2GB到8GB之间,而它们的GPU性能也远不如我们现在使用的那些设备。我妈妈的Galaxy Note 3配备了高通Adreno 330 GPU,拥有32个统一着色器核心,运行频率最高可达578MHz——在那个时候,这已经是一台非常强大的手机了。
如今,我们口袋里的手机功能强大得多,效率也高了很多,而且确实能够完成一些在过去人们还会认为属于科幻范畴的事情。
不过,先不说我妈妈的手机了。我想说的是:我们完全不需要每个月花费数百美元来订阅各种AI服务或购买相关token,因为我们现在就已经拥有了这些功能强大的设备。
现代智能手机都配备了专门的AI加速芯片,具有出色的散热性能,并且拥有足够的计算能力,可以本地化地运行轻量级的语言模型,而且完全不需要依赖云服务。这意味着我们可以获得更好的隐私保护,能够完全掌控自己的聊天记录,延迟也会更低,同时还能在不依赖云服务的情况下使用AI技术。
在这篇文章中,我们将开发一个React Native应用程序,让它能够与直接运行在设备上的大语言模型进行交互。实现这一目标的关键工具是QVAC——这是一套专为在本地运行AI模型而设计的推理工具。
目录
先决条件
要想充分理解这篇文章的内容,你需要对前端开发以及React有一定的了解。你不一定需要成为一名移动应用开发者,但掌握React知识会对你有很大帮助。
什么是QVAC?
QVAC(QuantumVerse Automatic Computer)是由Tether公司开发的一款以本地计算为主的人工智能推理平台。它的设计目的就是让人工智能技术摆脱对集中式云系统的依赖,将计算能力重新带回用户的设备中。
大多数现代人工智能工具都严重依赖于那些由少数几家公司控制的远程服务器、API密钥以及云基础设施。虽然这种架构使得人工智能技术得以被广泛使用,但它同时也引发了关于隐私保护、内容审查、供应商锁定现象、对互联网的依赖性以及用户数据归属权等一系列重大问题。每当用户发出指令、进行对话或上传文件时,这些数据往往都会经过第三方服务器的处理,而用户对这些第三方服务器几乎没有任何控制权。
QVAC的设计目的就是为了解决这个问题——它允许人工智能模型和代理直接在智能手机、笔记本电脑以及嵌入式系统等设备上运行,而且即使在完全离线的情况下也能正常工作。用户无需将个人对话内容或敏感数据发送到云端,而是可以在自己的硬件设备上本地处理所有这些信息。
该平台还通过点对点的通信方式来实现去中心化,从而减少了对集中式基础设施的依赖,并消除了单点故障的风险。这种设计使得人工智能系统更加私密、更具弹性、更自主,同时也更容易在互联网连接不稳定或数据隐私要求严格的环境中使用。
简单来说,QVAC的存在就是为了让人工智能真正成为用户的私有财产——优先在本地运行,默认设置为私密模式,并且不受中央控制的约束。
环境配置
为了加快配置流程,我准备了一个已经安装好了所有依赖项的React Native入门项目。不过在这篇文章中,我们重点会介绍如何安装和配置QVAC,因此这里提供了一个仓库链接:仓库链接。
或者你也可以运行下面的命令来克隆这个入门项目。
git clone --branch ft-ui-implementation --single-branch https://github.com/DjibrilM/QVAC-offline-Chatbot-Article-Project-
QVAC的安装
运行以下命令来安装SDK:`npm i @qvac/sdk`。你可以使用任何你喜欢的包管理器来进行安装;而我则选择使用`npm`。
接下来,将以下依赖项添加到你的`package.json`文件中:
{
"dependencies": {
"@qvac/sdk": "^0.7.0",
+ "bare-rpc": "^1.0.0",
"expo": "~54.0.33",
"expo-status-bar": "~3.0.9",
"react": "19.1.0",
"react-native": "0.81.5",
+ "react-native-bare-kit": "^0.11.5"
},
"devDependencies": {
"@types/react": "~19.1.0",
"bare-pack": "^1.5.1",
"typescript": "~5.9.2"
}
}
还需要安装以下额外的依赖项:
npx expo install expo-file-system expo-build-properties expo-device
接下来,配置`expo-build-properties`,并将`@qvac/sdk/expo-plugin`添加到你的`app.json`文件中的`plugins`数组里:
{
"expo": {
"plugins": [
"expo-router",
"@qvac/sdk/expo-plugin",
[
"expo-splash-screen",
{
"backgroundColor": "#208AEF",
"android": {
"image": "./assets/images/splash-icon.png",
"imageWidth": 76
}
}
]
]
}
}
运行以下命令来构建原生模块:
npx expo prebuild
注意: QVAC在内部实际上使用了lama.cpp。由于优化需求以及与硬件的依赖关系,QVAC SDK无法在模拟器上运行。因此,您需要使用已开启开发者模式的真实物理设备来进行测试。
要在您的物理设备上运行该应用程序,请执行以下操作:
# 对于Android设备:
npx expo run:android --device
# 对于iOS设备:
npx expo run:ios --device
模型管理
QVAC的模型管理系统完全采用本地优先、去中心化的设计。它负责处理从文件下载到生命周期优化的整个流程,并通过简洁的实用API将所有相关功能抽象出来。
可暂停与去重下载功能(downloadAsset)
该系统会将临时数据块写入本地磁盘。如果网络连接中断,已下载的部分数据会得到保留,并在网络恢复后自动继续下载。此外,当多个组件同时尝试下载同一资源时,QVAC会通过单一的网络流来处理这些下载请求。
内存生命周期管理(loadModel与unloadModel)
loadModel会将资源文件直接映射到内存中,并将其关联到您的硬件设备(如设备的GPU),同时会生成一个临时的modelId。由于在移动设备上进行推理运算需要消耗大量内存,因此调用unloadModel可以立即释放系统内存空间,同时保留已下载的文件。
自定义模型
由于QVAC基于优化过的llama.cpp代码实现,因此它与开源AI生态系统具有高度兼容性。如果您打算加载自定义模型,请确保这些模型符合以下要求:
-
格式:必须采用GGUF格式(文件扩展名为
.gguf)。 -
量化精度:对于移动设备和边缘计算设备而言,应优先选择
Q4_0、Q4_K_M或Q8_0配置方案,以确保模型能够适应移动设备的内存限制。
完整实现代码
现在,让我们用完整的实现代码替换原有的主文件逻辑。这些代码将UI布局、用户交互状态、模型生命周期管理以及实时推理处理等功能整合成一个紧密连贯的整体结构。
请用以下代码替换您的入口文件:
import { ChatInput } from "@/components/chat-input";
import { ChatMessage, Message } from "@components/chat-message";
import { ModelLoader } from "@components/model-loader";
import { Button } from "@components/ui/button";
import { Text } from "@components/ui/text";
import {
completion,
deleteCache,
downloadAsset,
LLAMA_3_2_1B_INST_Q4_0,
loadModel,
type ModelProgressUpdate,
VERBOSITY,
} from "@qvac/sdk";
import { SymbolView } from "expo-symbols";
import { useEffect, useRef, useState } from "react";
import {
Clipboard,
KeyboardAvoidingView,
Platform,
SafeAreaView,
ScrollView,
View,
} from "react-native";
const makeId = () => Math.random().toString(36).substring(2, 9);
export default function Index() {
const [messages, setMessages] = useState<Message[]>> [];
const [input, setInput] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
// 模型加载状态相关变量
const [modelId, setModelId] = useState<string | null>>(null);
const [isModelLoaded, setIsModelLoaded] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const [downloadProgress, setDownloadProgress] = useState(0);
const scrollViewRef = useRef<ScrollView>(null);
const messagesRef = useRef<Message[]>> [];
useEffect(() =&; {
messagesRef.current = messages;
}, [messages]);
const startDownload = () => {
setIsDownloading(true);
setupModel();
};
// 当消息列表更新时自动滚动到页面底部
useEffect(() =&; {
if (scrollViewRef.current) {
setTimeout(() =&; {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, 100);
}
}, [messages, isGenerating]);
const copyToClipboard = (text: string) => {
if (Platform.OS === "web") {
navigator.clipboard.writeText(text);
} else {
Clipboard.setString(text);
}
};
async function setupModel() =&; {
try {
setIsDownloading(true);
setDownloadProgress(0);
// 1. 执行本地文件下载操作
await downloadAsset({
assetSrc: LLAMA_3_2_1B_INST_Q4_0,
onProgress: (progress: ModelProgressUpdate) =&; {
setDownloadProgress(progress.percentage / 100);
},
});
setDownloadProgress(1);
// 2. 将模型加载到运行时内存中
const loadedModel = await loadModel({
modelSrc: LLAMA_3_2_1B_INST_Q4_0,
modelType: "llm",
modelConfig: {
device: "gpu",
ctxSize: 2048,
verbosity: VERBOSITY.ERROR,
},
});
setModelId(loadedModel);
setIsModelLoaded(true);
setIsDownloading(false);
} catch (e: any) {
console.error("在设置模型时出现错误:", e);
setIsDownloading(false);
}
};
async function handleSend() {
// 确保模型已加载完成或生成过程未进行中后再执行发送操作。
if (!modelId || isGenerating) return;
const trimmed = input.trim();
if (!trimmed) return;
setInput("");
setIsGenerating(true);
// 添加用户输入的内容以及辅助系统的提示信息
const userMsg: Message = {
id: makeId(),
role: "user",
content: trimmed,
};
const assistantId = makeId();
const assistantMsg: Message = {
id: assistantId,
role: "assistant",
content: "",
};
setMessages((prev) =&; [...prev, userMsg, assistantMsg]);
try {
// 为完成请求构建聊天历史记录
const history = [...messagesRef.current, userMsg].map((m) =&> ({
role: m.role,
content: m.content,
}));
// 执行流式处理来完成任务并更新辅助系统的提示信息
const result = completion({
modelId,
history,
stream: true,
});
let acc = "";
for await (const token of result.tokenStream) {
acc += token;
// 仅更新辅助系统的提示信息内容
setMessages((prev) =&;
prev.map((m) =&;
m.id === assistantId ? { ...m, content: acc } : m
)
);
}
// 可选:记录完成任务的性能统计数据
try {
const stats = await result.stats;
console.log("📊 完成任务的性能统计信息:", stats);
} catch {}
} catch (e: any) {
// 在辅助系统的提示框中显示错误信息
setMessages((prev) =&;
prev.map((m) =&;
m.id === assistantId
? { ...m, content: `❌ 错误信息:${e?.message ?? String(e)}` }
: m
)
);
} finally {
setIsGenerating(false);
}
}
if (!isModelLoaded) {
return (
>
本地Llama 3.2
离线引擎
>
{messages.filter(m =&; m.content !== "" || m.role === "assistant").map((msg) =&> (
);
}
代码库构成分析
让我们来看看这个统一组件是如何管理本地模型工作流程以及实时用户界面流式的。
1. 模型状态跟踪与异步同步
在这个组件的核心部分,我们会同时追踪用户交互界面的状态以及底层的QVAC运行时相关数据:
const [messages, setMessages] = useState:;
const [modelId, setModelId] = useState(null);
const [isModelLoaded, setIsModelLoaded] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const [downloadProgress, setDownloadProgress] = useState(0);
由于React中的状态设置操作是异步进行的,因此流式处理循环可能会意外地获取到过时的聊天记录信息。
为了解决这个问题,一个可变的messagesRef被用作当前会话状态的实时唯一数据源:
const messagesRef = useRef。>;
useEffect(() => {
messagesRef.current = messages;
}, [messages]);
2. 下载操作的组织与内存实例化过程
当用户点击下载按钮时,应用程序会调用setupModel()函数。这个函数会将相关任务明确地分配到本地存储缓存层和硬件资源分配层中:
await downloadAsset({
assetSrc: LLAMA_3_2_1B_INST_Q4_0,
onProgress: (progress: ModelProgressUpdate) => {
setDownloadProgress(progress.percentage / 100);
},
});
-
存储同步:
downloadAsset函数会从服务器下载指定的标准模型文件,并将其保存到移动设备的磁盘上。 -
硬件资源调用: 一旦文件成功保存到磁盘上,
loadModel函数就会被执行,从而启动引擎的运行。
const loadedModel = await loadModel({
modelSrc: LLAMA_3_2_1B_INST_Q4_0,
modelType: "llm",
modelConfig: {
device: "gpu",
ctxSize: 2048,
verbosity: VERBOSITY.ERROR,
},
});
通过指定device: "gpu",我们可以让QVAC在智能手机的图形处理硬件上运行硬件加速内核,从而确保应用程序能够获得快速的性能表现,而不会因为依赖速度较慢的CPU循环而导致性能下降。
3. 数据流处理流程与内容生成循环
一旦用户确认提示信息已经准备就绪,handleSend()函数就会创建用户交互界面,并生成一个空的助手占位卡来接收生成的token数据。
应用程序会直接将messagesRef.current中存储的数据转换成结构化的语法格式,然后再进行进一步的处理。
const result = completion({
modelId,
history,
stream: true,
});
当启用stream: true时,QVAC不会让应用程序线程因等待长字符串序列的完成而停滞不前。相反,它会生成一个异步的可迭代流,立即输出最新的更新内容:
let acc = "";
for await (const token of result.tokenStream) {
acc += token;
setMessages((prev) =>
prev.map((m) =>
m.id === assistantId ? { ...m, content: acc } : m
)
);
}
这个循环会不断将生成的字符串变量添加到跟踪累积器acc中,并且只会将这些更新内容与应用中的占位符标识符assistantId进行匹配。这样一来,在用户的设备上完全离线运行时,也能提供极其快速的输入体验。
结论
构建以本地计算为主的人工智能应用,已经不再是一个仅限于高端台式机或专业研究实验室的概念。正如我们所看到的,我们每天随身携带的智能手机,已经具备了足够的计算能力和专用的硬件加速功能,能够完全离线运行高性能的语言模型。
通过使用React Native和QVAC SDK,我们成功绕过了传统的依赖云服务的架构模式。我们不再需要复杂的服务器基础设施、API密钥管理,也不必支付反复产生的令牌订阅费用,同时还能在用户的设备上直接提供超私密、低延迟的流式聊天体验。
随着开源语言模型规模的不断缩小以及功能的不断增强,边缘计算将会成为那些重视隐私保护、离线可用性及成本效益的开发者的必备技术。计算能力终于重新回到了用户手中。
资源与延伸阅读
如果你想深入了解本地推理技术、查看源代码,或者探索适用于移动应用的高级配置方案,请参考以下资源:
-
QVAC Expo集成教程 – 官方提供的逐步指导文档,帮助你在Expo和React Native环境中配置QVAC。
-
项目GitHub仓库 – 可以获取完整的源代码,包括UI布局组件、起始主题以及本指南中使用的所有配置文件。
-
Llama.cpp官方仓库> 了解为QVAC的硬件加速本地执行提供支持的基础推理引擎。
-
Hugging Face GGUF模型 > 你可以下载数千种开源的量化模型,在自己的本地应用中对其进行测试和使用。



