边缘设备上的大语言模型推理:通过 React Native 在手机上运行大语言模型的趣味简易指南!
随着大型语言模型(LLM)的不断发展,它们变得更小、更智能,从而可以直接在您的手机上运行。以 DeepSeek R1 Distil Qwen 2.5 为例,这个拥有15亿参数的模型真正展示了先进的人工智能现在如何能够掌握在您的掌中!
在这篇博客中,我们将指导您创建一个移动应用程序,让您可以在本地与这些强大的模型进行聊天。本教程的完整代码可在我们的 EdgeLLM 仓库中找到。如果您曾因开源项目的复杂性而感到不知所措,请不要担心!受 Pocket Pal 应用程序的启发,我们将帮助您构建一个简单的 React Native 应用程序,从 Hugging Face 中心下载 LLM,确保一切都是私有的并在您的设备上运行。我们将利用 llama.rn
(一个用于 llama.cpp
的绑定)来高效加载 GGUF 文件!
为什么您应该遵循本教程?
本教程专为以下人群设计:
- 对将人工智能集成到移动应用程序中感兴趣
- 希望使用 React Native 创建兼容 Android 和 iOS 的对话式应用程序
- 寻求开发完全离线运行的注重隐私的人工智能应用程序
通过本指南,您将拥有一个功能齐全的应用程序,可以与您喜欢的模型进行交互。
0. 选择合适的模型
在深入构建应用程序之前,我们先来讨论哪些模型适合移动设备,以及在选择时需要考虑什么。
模型大小考量
在移动设备上运行大型语言模型(LLMs)时,模型大小至关重要。
- 小型模型(1-3B 参数):适用于大多数移动设备,提供良好性能且延迟极低。
- 中型模型(4-7B 参数):在新式高端设备上运行良好,但可能导致旧手机速度变慢。
- 大型模型(8B+ 参数):通常对大多数移动设备而言资源消耗过大,但如果量化为低精度格式(如 Q2_K 或 Q4_K_M),则可以使用。
GGUF 量化格式
下载 GGUF 模型时,您会遇到各种量化格式。了解这些可以帮助您在模型大小和性能之间取得适当的平衡。
传统量化(Q4_0、Q4_1、Q8_0)
- 基本、直接的量化方法。
- 每个块都存储有:
• 量化值(压缩后的权重)。
• 一个 (_0) 或两个 (_1) 缩放常数。 - 速度快但效率低于新方法 => 不再广泛使用。
K-量化(Q3_K_S, Q5_K_M, ...)
- 此 PR 中引入。
- 比传统量化更智能的比特分配。
- “K-量化”中的 K 指的是一种混合量化格式,意味着某些层会获得更多比特以提高精度。
- 诸如 _XS、_S 或 _M 之类的后缀指的是特定的量化混合(越小表示压缩越多),例如:
• Q3_K_S 对所有张量使用 Q3_K。
• Q3_K_M 对 attention.wv、attention.wo 和 feed_forward.w2 张量使用 Q4_K,其余使用 Q3_K。
• Q3_K_L 对 attention.wv、attention.wo 和 feed_forward.w2 张量使用 Q5_K,其余使用 Q5_K。
I-量化(IQ2_XXS, IQ3_S, ...)
- 它仍然使用基于块的量化,但受到 QuIP 的启发,加入了一些新特性。
- 文件大小更小,但在某些硬件上可能更慢。
- 最适合计算能力强但内存有限的设备。
推荐尝试的模型
以下是一些在移动设备上表现良好的模型:
查找更多模型
要在 Hugging Face 上查找更多 GGUF 模型:
- 访问 huggingface.co/models
- 使用搜索过滤器
- 访问 GGUF 模型页面
- 在搜索栏中指定模型大小
- 对于对话模型,查找名称中包含“chat”或“instruct”的模型
在选择模型时,请同时考虑参数数量和量化级别。例如,使用 Q2_K 量化的 7B 模型可能比使用 Q8_0 量化的 2B 模型运行得更好。因此,如果您的设备可以轻松容纳小型模型,请尝试使用更大的量化模型,它可能会有更好的性能。
1. 设置您的环境
React Native 是一个流行的框架,用于使用 JavaScript 和 React 构建移动应用程序。它允许开发人员创建在 Android 和 iOS 平台上运行的应用程序,同时共享大量代码,从而加快开发过程并减少维护工作。
在开始使用 React Native 编码之前,您需要正确设置您的环境。
所需工具
Node.js: Node.js 是一个 JavaScript 运行时,允许您运行 JavaScript 代码。它对于管理 React Native 项目中的包和依赖项至关重要。您可以从 Node.js 下载页面安装它。
react-native-community/cli: 此命令安装 React Native 命令行界面 (CLI),它提供用于创建、构建和管理您的 React Native 项目的工具。运行以下命令安装:
npm i @react-native-community/cli
虚拟设备设置
要在开发过程中运行您的应用程序,您需要一个模拟器或仿真器。
如果您使用的是 macOS:
如果您使用的是 Windows 或 Linux:
- 对于 iOS:我们需要依赖基于云的模拟器,如 LambdaTest 和 BrowserStack
- 对于 Android:安装 Java Runtime 和 Android Studio -> 转到设备管理器并创建一个模拟器
如果您对模拟器和仿真器之间的区别感到好奇,可以阅读这篇文章:模拟器和仿真器之间的区别,但简单来说,仿真器复制硬件和软件,而模拟器只复制软件。
有关 Android Studio 的设置,请遵循 Expo 的这份出色教程:Android Studio 模拟器指南
2. 创建应用程序
让我们开始这个项目吧!
您可以在 EdgeLLM
仓库 这里 找到该项目的完整代码,其中包含两个文件夹:
EdgeLLMBasic
: 应用程序的基本实现,带有一个简单的聊天界面。EdgeLLMPlus
: 应用程序的增强版本,带有一个更复杂的聊天界面和附加功能。
首先,我们需要使用 @react-native-community/cli 初始化应用程序。
npx @react-native-community/cli@latest init <ProjectName>
项目结构
应用程序文件夹组织如下:
默认文件/文件夹
android/
- 包含原生 Android 项目文件。
- 目的:用于在 Android 设备上构建和运行应用程序。
ios/
- 包含原生 iOS 项目文件。
- 目的:用于在 iOS 设备上构建和运行应用程序。
node_modules/
- 目的:保存项目中使用的所有 npm 依赖项。
App.tsx
- 您应用程序的主要根组件,使用 TypeScript 编写。
- 目的:应用程序 UI 和逻辑的入口点。
index.js
- 注册根组件 (
App
)。 - 目的:React Native 运行时的入口点。您无需修改此文件。
- 注册根组件 (
附加配置文件
tsconfig.json
:配置 TypeScript 设置。babel.config.js
:配置 Babel 用于转换现代 JavaScript/TypeScript,这意味着它将现代 JS/TS 代码转换为与旧浏览器或设备兼容的旧 JS/TS 代码。jest.config.js
:配置 Jest 用于测试 React Native 组件和逻辑。metro.config.js
:自定义项目的 Metro 打包器。它是一个专门为 React Native 设计的 JavaScript 打包器。它会获取您项目的 JavaScript 和资产,将它们打包成一个文件(或多个文件以实现高效加载),并在开发过程中将其提供给应用程序。Metro 针对快速增量构建进行了优化,支持热重载,并处理 React Native 的平台特定文件(.ios.js 或 .android.js)。.watchmanconfig
:配置 Watchman,一个由 React Native 用于热重载的文件监听服务。
3. 运行演示和项目
运行演示
要运行项目并在您自己的虚拟设备上查看其外观,请按照以下步骤操作:
克隆仓库:
git clone https://github.com/MekkCyber/EdgeLLM.git
导航到项目目录:
cd EdgeLLMPlus #or cd EdgeLLMBasic
安装依赖:
npm install
导航到 iOS 文件夹并安装:
cd ios pod install
启动 Metro Bundler:在项目文件夹(EdgeLLMPlus 或 EdgeLLMBasic)中运行以下命令:
npm start
在 iOS 或 Android 模拟器上启动应用程序:打开另一个终端并运行:
# For iOS npm run ios # For Android npm run android
这将在您的模拟器/仿真器上构建并启动应用程序,以便在开始编码之前测试项目。
运行项目
运行 React Native 应用程序需要模拟器/仿真器或物理设备。我们将重点介绍使用模拟器,因为它通过您的代码编辑器和调试工具并排提供了更流畅的开发体验。
我们首先确保开发环境已准备就绪,我们需要进入项目文件夹并运行以下命令:
# Install dependencies
npm install
# Start the Metro bundler
npm start
在一个新的终端中,我们将在所选平台上启动应用程序:
# For iOS
npm run ios
# For Android
npm run android
这应该会在您的模拟器/仿真器上构建并启动应用程序。
4. 应用程序实现
安装依赖项
首先,让我们安装所需的包。我们的目标是从 Hugging Face Hub 加载模型并在本地运行它们。为此,我们需要安装:
llama.rn
:用于 React Native 应用程序的llama.cpp
绑定。react-native-fs
:允许我们在 React Native 环境中管理设备的 文件系统。axios
:一个用于向 Hugging Face Hub API 发送请求的库。
npm install axios react-native-fs llama.rn
让我们像之前展示的那样在我们的模拟器/仿真器上运行应用程序,这样我们就可以开始开发了。
状态管理
我们将从删除 App.tsx
文件中的所有内容开始,并创建一个空的代码结构,如下所示:
App.tsx
import React from 'react';
import {StyleSheet, Text, View} from 'react-native';
function App(): React.JSX.Element {
return <View> <Text>Hello World</Text> </View>;
}
const styles = StyleSheet.create({});
export default App;
在 App
函数的 return
语句中,我们定义了渲染的 UI,而在其外部我们定义了逻辑,但所有代码都将在 App
函数内部。
我们将有一个看起来像这样的屏幕:

“Hello World”文本没有正确显示,因为我们使用的是一个简单的 View
组件,我们需要使用 SafeAreaView
组件来正确显示文本,我们将在下一节中处理这个问题。
现在让我们思考一下我们的应用程序目前需要跟踪什么:
与聊天相关的::
- 对话历史(用户和 AI 之间的消息)
- 当前用户输入
与模型相关的::
- 选定的模型格式(如 Llama 1B 或 Qwen 1.5B)
- 每个模型格式可用的 GGUF 文件列表
- 要下载的选定 GGUF 文件
- 模型下载进度
- 一个用于存储已加载模型的上下文
- 一个布尔值,用于检查模型是否正在下载
- 一个布尔值,用于检查模型是否正在生成响应
以下是我们使用 React 的 useState 钩子(我们需要从 react 中导入它)实现这些状态的方法:
状态管理代码
import { useState } from 'react';
...
type Message = {
role: 'system' | 'user' | 'assistant';
content: string;
};
const INITIAL_CONVERSATION: Message[] = [
{
role: 'system',
content:
'This is a conversation between user and assistant, a friendly chatbot.',
},
];
const [conversation, setConversation] = useState<Message[]>(INITIAL_CONVERSATION);
const [selectedModelFormat, setSelectedModelFormat] = useState<string>('');
const [selectedGGUF, setSelectedGGUF] = useState<string | null>(null);
const [availableGGUFs, setAvailableGGUFs] = useState<string[]>([]);
const [userInput, setUserInput] = useState<string>('');
const [progress, setProgress] = useState<number>(0);
const [context, setContext] = useState<any>(null);
const [isDownloading, setIsDownloading] = useState<boolean>(false);
const [isGenerating, setIsGenerating] = useState<boolean>(false);
这将被添加到 App.tsx
文件中的 App
函数内部,但在 return
语句之外,因为它是逻辑的一部分。
Message 类型定义了聊天消息的结构,指定每条消息必须有一个角色(“user”、“assistant”或“system”)和内容(实际消息文本)。
现在我们已经设置了基本的状态管理,我们需要考虑如何:
- 从 Hugging Face 获取可用的 GGUF 模型
- 在本地下载和管理模型
- 创建聊天界面
- 处理消息生成
让我们在接下来的部分中逐一解决这些问题……
从 Hub 获取可用的 GGUF 模型
首先,让我们定义应用程序将支持的模型格式及其仓库。当然,llama.rn
是 llama.cpp
的绑定,所以我们需要加载 GGUF
文件。要查找我们希望支持的模型的 GGUF 仓库,我们可以使用 Hugging Face 上的搜索栏搜索特定模型的 GGUF
文件,或者使用 此处 提供的脚本 quantize_gguf.py
来量化模型并将其文件上传到我们的 hub 仓库。
const modelFormats = [
{label: 'Llama-3.2-1B-Instruct'},
{label: 'Qwen2-0.5B-Instruct'},
{label: 'DeepSeek-R1-Distill-Qwen-1.5B'},
{label: 'SmolLM2-1.7B-Instruct'},
];
const HF_TO_GGUF = {
"Llama-3.2-1B-Instruct": "medmekk/Llama-3.2-1B-Instruct.GGUF",
"DeepSeek-R1-Distill-Qwen-1.5B":
"medmekk/DeepSeek-R1-Distill-Qwen-1.5B.GGUF",
"Qwen2-0.5B-Instruct": "medmekk/Qwen2.5-0.5B-Instruct.GGUF",
"SmolLM2-1.7B-Instruct": "medmekk/SmolLM2-1.7B-Instruct.GGUF",
};
HF_TO_GGUF
对象将用户友好的模型名称映射到其相应的 Hugging Face 仓库路径。例如:
- 当用户选择“Llama-3.2-1B-Instruct”时,它映射到
medmekk/Llama-3.2-1B-Instruct.GGUF
,这是包含 Llama 3.2 1B Instruct 模型 GGUF 文件的其中一个仓库。
modelFormats
数组包含将显示在选择屏幕上的模型选项列表,我们选择了 Llama 3.2 1B Instruct、DeepSeek R1 Distill Qwen 1.5B、Qwen 2 0.5B Instruct 和 SmolLM2 1.7B Instruct,因为它们是最流行的小型模型。
接下来,让我们创建一个方法来从 hub 中获取并显示我们所选模型格式的可用 GGUF 模型文件。
当用户选择一个模型格式时,我们使用我们在 HF_TO_GGUF
对象中映射的仓库路径向 Hugging Face 发出 API 调用。我们特别查找以 '.gguf' 扩展名结尾的文件,这些文件是我们的量化模型文件。
一旦收到响应,我们只提取这些 GGUF 文件的文件名,并使用 setAvailableGGUFs
将它们存储在我们的 availableGGUFs
状态中。这使我们能够向用户显示一个可用 GGUF 模型变体的列表,供他们下载。
获取可用的 GGUF 文件
const fetchAvailableGGUFs = async (modelFormat: string) => {
if (!modelFormat) {
Alert.alert('Error', 'Please select a model format first.');
return;
}
try {
const repoPath = HF_TO_GGUF[modelFormat as keyof typeof HF_TO_GGUF];
if (!repoPath) {
throw new Error(
`No repository mapping found for model format: ${modelFormat}`,
);
}
const response = await axios.get(
`https://huggingface.co/api/models/${repoPath}`,
);
if (!response.data?.siblings) {
throw new Error('Invalid API response format');
}
const files = response.data.siblings.filter((file: {rfilename: string}) =>
file.rfilename.endsWith('.gguf'),
);
setAvailableGGUFs(files.map((file: {rfilename: string}) => file.rfilename));
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Failed to fetch .gguf files';
Alert.alert('Error', errorMessage);
setAvailableGGUFs([]);
}
};
注意:如果尚未导入,请确保在文件顶部导入 axios 和 Alert。
我们需要测试函数是否正常工作,让我们在 UI 中添加一个按钮来触发该函数,而不是使用 View
,我们将使用 SafeAreaView
(稍后会详细介绍)组件,并将在 ScrollView
组件中显示可用的 GGUF 文件。当按钮被按下时,onPress
函数会被触发。
<TouchableOpacity onPress={() => fetchAvailableGGUFs('Llama-3.2-1B-Instruct')}>
<Text>Fetch GGUF Files</Text>
</TouchableOpacity>
<ScrollView>
{availableGGUFs.map((file) => (
<Text key={file}>{file}</Text>
))}
</ScrollView>
这应该看起来像这样:

注意:到目前为止的完整代码,您可以在
EdgeLLMBasic
文件夹的first_checkpoint
分支 此处 查看。
模型下载实现
现在让我们在 handleDownloadModel
函数中实现模型下载功能,该函数应在用户点击下载按钮时调用。这将从 Hugging Face 下载选定的 GGUF 文件并将其存储在应用程序的 Documents 目录中。
模型下载函数
const handleDownloadModel = async (file: string) => {
const downloadUrl = `https://huggingface.co/${
HF_TO_GGUF[selectedModelFormat as keyof typeof HF_TO_GGUF]
}/resolve/main/${file}`;
// we set the isDownloading state to true to show the progress bar and set the progress to 0
setIsDownloading(true);
setProgress(0);
try {
// we download the model using the downloadModel function, it takes the selected GGUF file, the download URL, and a progress callback function to update the progress bar
const destPath = await downloadModel(file, downloadUrl, progress =>
setProgress(progress),
);
} catch (error) {
const errorMessage =
error instanceof Error
? error.message
: 'Download failed due to an unknown error.';
Alert.alert('Error', errorMessage);
} finally {
setIsDownloading(false);
}
};
我们可以在 handleDownloadModel
函数中实现 api
请求,但为了保持代码清晰可读,我们将它保存在一个单独的文件中。handleDownloadModel
调用位于 src/api
中的 downloadModel
函数,该函数接受三个参数:modelName
、downloadUrl
和一个 progress
回调函数。此回调函数在下载过程中触发以更新进度。在下载之前,我们需要将 selectedModelFormat
状态设置为我们想要下载的模型格式。
在 downloadModel
函数内部,我们使用 RNFS
模块(react-native-fs
库的一部分)来访问设备的 文件系统。它允许开发人员在设备的存储中读写和管理文件。在这种情况下,模型存储在应用程序的 Documents 文件夹中,使用 RNFS.DocumentDirectoryPath
,确保下载的文件对应用程序是可访问的。进度条会相应更新,以反映当前的下载状态,进度条组件在 components
文件夹中定义。
让我们创建 src/api/model.ts
并从仓库中的 src/api/model.ts
文件复制代码。逻辑应该很简单。src/components
文件夹中的进度条组件 src/components
也是如此,它是一个简单的彩色 View
,其宽度就是下载的进度。
现在我们需要测试 handleDownloadModel
函数,让我们在 UI 中添加一个按钮来触发该函数,并将显示进度条。这将添加到我们之前添加的 ScrollView
下方。
下载模型按钮
<View style={{ marginTop: 30, marginBottom: 15 }}>
{Object.keys(HF_TO_GGUF).map((format) => (
<TouchableOpacity
key={format}
onPress={() => {
setSelectedModelFormat(format);
}}
>
<Text> {format} </Text>
</TouchableOpacity>
))}
</View>
<Text style={{ marginBottom: 10, color: selectedModelFormat ? 'black' : 'gray' }}>
{selectedModelFormat
? `Selected: ${selectedModelFormat}`
: 'Please select a model format before downloading'}
</Text>
<TouchableOpacity
onPress={() => {
handleDownloadModel("Llama-3.2-1B-Instruct-Q2_K.gguf");
}}
>
<Text>Download Model</Text>
</TouchableOpacity>
{isDownloading && <ProgressBar progress={progress} />}
在 UI 中,我们显示支持的模型格式列表和一个下载模型的按钮,当用户选择模型格式并点击按钮时,应该显示进度条并开始下载。在测试中,我们硬编码要下载的模型 Llama-3.2-1B-Instruct-Q2_K.gguf
,因此我们需要选择 Llama-3.2-1B-Instruct
作为模型格式,函数才能工作,我们应该会看到类似以下的内容:

注意:到目前为止的完整代码,您可以在
EdgeLLMBasic
文件夹的second_checkpoint
分支 此处 查看。
模型加载和初始化
接下来,我们将实现一个函数,用于将下载的模型加载到 Llama 上下文中,具体细节请参考 llama.rn
文档 这里。如果上下文中已存在模型,我们将先释放它,将上下文设置为 null
,并将对话重置为初始状态。然后,我们将利用 initLlama
函数将模型加载到新的上下文中,并用新初始化的上下文更新我们的状态。
模型加载函数
import {initLlama, releaseAllLlama} from 'llama.rn';
import RNFS from 'react-native-fs'; // File system module
...
const loadModel = async (modelName: string) => {
try {
const destPath = `${RNFS.DocumentDirectoryPath}/${modelName}`;
// Ensure the model file exists before attempting to load it
const fileExists = await RNFS.exists(destPath);
if (!fileExists) {
Alert.alert('Error Loading Model', 'The model file does not exist.');
return false;
}
if (context) {
await releaseAllLlama();
setContext(null);
setConversation(INITIAL_CONVERSATION);
}
const llamaContext = await initLlama({
model: destPath,
use_mlock: true,
n_ctx: 2048,
n_gpu_layers: 1
});
console.log("llamaContext", llamaContext);
setContext(llamaContext);
return true;
} catch (error) {
Alert.alert('Error Loading Model', error instanceof Error ? error.message : 'An unknown error occurred.');
return false;
}
};
我们需要在用户点击下载按钮时调用 loadModel
函数,所以我们需要在 handleDownloadModel
函数中下载成功后立即添加它。
// inside the handleDownloadModel function, just after the download is complete
if (destPath) {
await loadModel(file);
}
为了测试模型加载,让我们在 loadModel
函数内部添加一个 console.log
来打印上下文,这样我们就可以看到模型是否正确加载。我们保持 UI 不变,因为点击下载按钮会触发 handleDownloadModel
函数,并且 loadModel
函数会在其中被调用。要查看 console.log
输出,我们需要打开开发者工具,为此,我们在运行 npm start
的终端中按 j
。如果一切正常,我们应该会在控制台中看到打印出的上下文。
注意:到目前为止的完整代码,您可以在
EdgeLLMBasic
文件夹的third_checkpoint
分支 此处 查看。
聊天实现
模型现在已加载到我们的上下文中,我们可以继续实现对话逻辑。我们将定义一个名为 handleSendMessage
的函数,该函数将在用户提交输入时触发。此函数将更新对话状态,并通过 context.completion
将更新后的对话发送到模型。然后,模型返回的响应将用于进一步更新对话,这意味着在此函数中对话将更新两次。
聊天函数
const handleSendMessage = async () => {
// Check if context is loaded and user input is valid
if (!context) {
Alert.alert('Model Not Loaded', 'Please load the model first.');
return;
}
if (!userInput.trim()) {
Alert.alert('Input Error', 'Please enter a message.');
return;
}
const newConversation: Message[] = [
// ... is a spread operator that spreads the previous conversation array to which we add the new user message
...conversation,
{role: 'user', content: userInput},
];
setIsGenerating(true);
// Update conversation state and clear user input
setConversation(newConversation);
setUserInput('');
try {
// we define list the stop words for all the model formats
const stopWords = [
'</s>',
'<|end|>',
'user:',
'assistant:',
'<|im_end|>',
'<|eot_id|>',
'<|end▁of▁sentence|>',
'<|end▁of▁sentence|>',
];
// now that we have the new conversation with the user message, we can send it to the model
const result = await context.completion({
messages: newConversation,
n_predict: 10000,
stop: stopWords,
});
// Ensure the result has text before updating the conversation
if (result && result.text) {
setConversation(prev => [
...prev,
{role: 'assistant', content: result.text.trim()},
]);
} else {
throw new Error('No response from the model.');
}
} catch (error) {
// Handle errors during inference
Alert.alert(
'Error During Inference',
error instanceof Error ? error.message : 'An unknown error occurred.',
);
} finally {
setIsGenerating(false);
}
};
要测试 handleSendMessage
函数,我们需要在 UI 中添加一个文本输入框和一个按钮来触发该函数,并将在 ScrollView
组件中显示对话。
简单聊天 UI
<View
style={{
flexDirection: "row",
alignItems: "center",
marginVertical: 10,
marginHorizontal: 10,
}}
>
<TextInput
style={{flex: 1, borderWidth: 1}}
value={userInput}
onChangeText={setUserInput}
placeholder="Type your message here..."
/>
<TouchableOpacity
onPress={handleSendMessage}
style={{backgroundColor: "#007AFF"}}
>
<Text style={{ color: "white" }}>Send</Text>
</TouchableOpacity>
</View>
<ScrollView>
{conversation.map((msg, index) => (
<Text style={{marginVertical: 10}} key={index}>{msg.content}</Text>
))}
</ScrollView>
如果一切都正确实现,我们应该能够向模型发送消息并在 ScrollView
组件中看到对话,它当然不是很美观,但这是一个好的开始,我们稍后会改进 UI。结果应该如下所示:

注意:到目前为止的完整代码,您可以在
EdgeLLMBasic
文件夹的fourth_checkpoint
分支 此处 查看。
用户界面与逻辑
现在我们已经实现了核心功能,我们可以专注于 UI。UI 很简单,由一个模型选择屏幕(包含模型列表)和一个聊天界面(包含对话历史和用户输入字段)组成。在模型下载阶段,会显示一个进度条。我们有意避免添加许多屏幕,以使应用程序保持简单并专注于其核心功能。为了跟踪应用程序的哪个部分正在使用,我们将使用另一个状态变量 currentPage
,它将是一个字符串,可以是 modelSelection
或 conversation
。我们将其添加到 App.tsx
文件中。
const [currentPage, setCurrentPage] = useState<
'modelSelection' | 'conversation'
>('modelSelection'); // Navigation state
对于 CSS,我们将使用与 EdgeLLMBasic 仓库相同的样式,您可以从那里复制样式。
我们将从 App.tsx 文件中的模型选择屏幕开始工作,我们将添加一个模型格式列表(您需要进行必要的导入并删除我们用于测试的 SafeAreaView
组件中的先前代码)。
模型选择 UI
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollView}>
<Text style={styles.title}>Llama Chat</Text>
{/* Model Selection Section */}
{currentPage === 'modelSelection' && (
<View style={styles.card}>
<Text style={styles.subtitle}>Choose a model format</Text>
{modelFormats.map(format => (
<TouchableOpacity
key={format.label}
style={[
styles.button,
selectedModelFormat === format.label && styles.selectedButton,
]}
onPress={() => handleFormatSelection(format.label)}>
<Text style={styles.buttonText}>{format.label}</Text>
</TouchableOpacity>
))}
</View>
)}
</ScrollView>
</SafeAreaView>
我们使用 SafeAreaView
来确保应用程序在不同屏幕尺寸和方向的设备上正确显示,就像我们在上一节中所做的那样,我们使用 ScrollView
来允许用户滚动浏览模型格式。我们还使用 modelFormats.map
来遍历 modelFormats
数组,并将每种模型格式显示为一个按钮,其样式会随着模型格式的选择而改变。我们还使用 currentPage
状态,仅当 currentPage
状态设置为 modelSelection
且没有正在进行模型下载时,才显示模型选择屏幕,这是通过使用 &&
运算符实现的。TouchableOpacity
组件用于允许用户通过按下它来选择模型格式。
现在,让我们在 App.tsx 文件中定义 handleFormatSelection
。
const handleFormatSelection = (format: string) => {
setSelectedModelFormat(format);
setAvailableGGUFs([]); // Clear any previous list
fetchAvailableGGUFs(format);
};
我们将选择的模型格式存储在状态中,并清除先前选择的 GGUF 文件列表,然后为所选格式获取新的 GGUF 文件列表。屏幕在您的设备上应如下所示:

接下来,我们将在模型格式选择部分的下方添加视图,以显示所选模型格式已有的 GGUF 文件列表。
可用的 GGUF 文件 UI
{
selectedModelFormat && (
<View>
<Text style={styles.subtitle}>Select a .gguf file</Text>
{availableGGUFs.map((file, index) => (
<TouchableOpacity
key={index}
style={[
styles.button,
selectedGGUF === file && styles.selectedButton,
]}
onPress={() => handleGGUFSelection(file)}>
<Text style={styles.buttonTextGGUF}>{file}</Text>
</TouchableOpacity>
))}
</View>
)
}
我们只需要在 selectedModelFormat
状态不为 null 时显示 GGUF 文件列表,这意味着用户已选择模型格式。

我们需要在 App.tsx
文件中定义 handleGGUFSelection
,它是一个函数,将触发警报以确认下载所选的 GGUF 文件。如果用户点击“是”,下载将开始;否则,选定的 GGUF 文件将被清除。
确认下载警报
const handleGGUFSelection = (file: string) => {
setSelectedGGUF(file);
Alert.alert(
'Confirm Download',
`Do you want to download ${file}?`,
[
{
text: 'No',
onPress: () => setSelectedGGUF(null),
style: 'cancel',
},
{text: 'Yes', onPress: () => handleDownloadAndNavigate(file)},
],
{cancelable: false},
);
};
const handleDownloadAndNavigate = async (file: string) => {
await handleDownloadModel(file);
setCurrentPage('conversation'); // Navigate to conversation after download
};
handleDownloadAndNavigate
是一个简单的函数,它将通过调用 handleDownloadModel
(在前面的部分中实现)来下载选定的 GGUF 文件,并在下载完成后导航到对话屏幕。
现在,点击 GGUF 文件后,我们应该会看到一个警报,确认或取消下载。

我们可以向视图添加一个简单的 ActivityIndicator
,以在获取可用 GGUF 文件时显示加载状态。为此,我们需要从 react-native
导入 ActivityIndicator
,并将 isFetching
定义为一个布尔状态变量,该变量在 fetchAvailableGGUFs
函数开始时设置为 true,在函数结束时设置为 false,如您在此处 代码 中所见,并将 ActivityIndicator
添加到视图中,紧邻 {availableGGUFs.map((file, index) => (...))}
之前,以在获取可用 GGUF 文件时显示加载状态。
{isFetching && (
<ActivityIndicator size="small" color="#2563EB" />
)}
当 GGUF 文件正在获取时,应用程序应该会短暂地显示如下界面:

现在我们应该能够看到每次点击模型格式时,对应可用的不同 GGUF 文件,并且在点击 GGUF 时应该看到确认是否要下载模型的提示。接下来,我们需要将进度条添加到模型选择屏幕,我们可以像之前一样,通过从 src/components/ProgressBar.tsx
导入 ProgressBar
组件到 App.tsx
文件中,并将其添加到视图中,紧接在 {availableGGUFs.map((file, index) => (...))}
之后,以便在模型下载时显示进度条。
下载进度条
{
isDownloading && (
<View style={styles.card}>
<Text style={styles.subtitle}>Downloading : </Text>
<Text style={styles.subtitle2}>{selectedGGUF}</Text>
<ProgressBar progress={progress} />
</View>
);
}
下载进度条现在将位于模型选择屏幕的底部。然而,这意味着用户可能需要向下滚动才能看到它。为了解决这个问题,我们将修改显示逻辑,以便仅当 currentPage
状态设置为“modelSelection”且没有正在进行模型下载时,才显示模型选择屏幕。
{currentPage === 'modelSelection' && !isDownloading && (
<View style={styles.card}>
<Text style={styles.subtitle}>Choose a model format</Text>
...
确认模型下载后,我们应该会看到如下屏幕:

注意:到目前为止的完整代码,您可以在
EdgeLLMBasic
文件夹的fifth_checkpoint
分支 此处 查看。
现在我们有了模型选择屏幕,我们可以开始在聊天界面上开发对话屏幕。当 currentPage
设置为 conversation
时,将显示此屏幕。我们将向屏幕添加对话历史记录和用户输入字段。对话历史记录将显示在一个可滚动视图中,用户输入字段将显示在屏幕底部,位于可滚动视图之外,以保持可见。每条消息将根据消息的角色(用户或助手)以不同的颜色显示。
我们只需要在模型选择屏幕下方添加对话屏幕的视图即可。
对话 UI
{currentPage == 'conversation' && !isDownloading && (
<View style={styles.chatContainer}>
<Text style={styles.greetingText}>
🦙 Welcome! The Llama is ready to chat. Ask away! 🎉
</Text>
{conversation.slice(1).map((msg, index) => (
<View key={index} style={styles.messageWrapper}>
<View
style={[
styles.messageBubble,
msg.role === 'user'
? styles.userBubble
: styles.llamaBubble,
]}>
<Text
style={[
styles.messageText,
msg.role === 'user' && styles.userMessageText,
]}>
{msg.content}
</Text>
</View>
</View>
))}
</View>
)}
我们对用户消息和模型消息使用不同的样式,并且我们使用 conversation.slice(1)
来删除对话中的第一条消息,即系统消息。
我们现在可以在屏幕底部添加用户输入字段和发送按钮(它们不应位于 ScrollView
内部)。正如我之前提到的,我们将使用 handleSendMessage
函数将用户消息发送到模型,并使用模型响应更新对话状态。
发送按钮和输入字段
{currentPage === 'conversation' && (
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="Type your message..."
placeholderTextColor="#94A3B8"
value={userInput}
onChangeText={setUserInput}
/>
<View style={styles.buttonRow}>
<TouchableOpacity
style={styles.sendButton}
onPress={handleSendMessage}
disabled={isGenerating}>
<Text style={styles.buttonText}>
{isGenerating ? 'Generating...' : 'Send'}
</Text>
</TouchableOpacity>
</View>
</View>
)}
当用户点击发送按钮时,handleSendMessage
函数将被调用,isGenerating
状态将被设置为 true。然后发送按钮将被禁用,文本将变为“正在生成...”。当模型完成生成响应时,isGenerating
状态将被设置为 false,文本将变回“发送”。
注意:到目前为止的完整代码,您可以在
EdgeLLMBasic
文件夹的main
分支 此处 查看。
对话页面现在应该如下所示:

恭喜您,您刚刚构建了第一个人工智能聊天机器人的核心功能,代码可在此处 EdgeLLM/blob/main/EdgeLLMBasic/App.tsx 获得!现在您可以开始为应用程序添加更多功能,使其更易于使用和高效。
其他功能
应用程序现在功能齐全,您可以下载模型,选择 GGUF 文件,并与模型聊天,但用户体验不是最好的。在 EdgeLLMPlus
仓库中,我添加了一些其他功能,例如即时生成、自动滚动、推理速度跟踪、模型的思考过程(如 deepseek-qwen-1.5B)等,我们在此不详细介绍,因为它会使博客过长,我们将介绍一些想法以及如何实现它们,但完整代码可在仓库中找到。
即时生成
该应用程序以增量方式生成响应,一次生成一个 token,而不是一次性传递整个响应。这种方法增强了用户体验,允许用户在响应形成时就开始阅读。我们通过在 context.completion
中使用 callback
函数来实现这一点,该函数在每个 token 生成后触发,使我们能够相应地更新 conversation
状态。
自动滚动
自动滚动功能通过在新内容添加时自动将聊天视图滚动到底部,确保用户始终能看到最新的消息或 token。为了实现这一点,我们需要使用对 ScrollView
的引用,以允许对滚动位置进行编程控制,并且我们使用 scrollToEnd
方法在将新消息添加到 conversation
状态时滚动到 ScrollView
的底部。我们还定义了一个 autoScrollEnabled
状态变量,当用户向上滚动距离 ScrollView
底部超过 100 像素时,该变量将设置为 false。
推理速度跟踪
推理速度跟踪是一个功能,它跟踪每个 token 的生成时间,并在模型生成的每条消息下方显示。这个功能很容易实现,因为 context.completion
函数返回的 CompletionResult
对象包含一个 timings
属性,这是一个字典,其中包含许多关于推理过程的度量。我们可以使用 predicted_per_second
度量来跟踪模型的速度。
思维过程
思维过程是一个显示模型思考过程的功能,例如 deepseek-qwen-1.5B。应用程序识别特殊 token,例如:Message
类型添加 thought
和 showThought
属性。message.thought
将存储模型的推理,message.showThought
将是一个布尔值,当用户点击消息以切换思考的可见性时,它将被设置为 true。
Markdown 渲染
该应用程序使用 react-native-markdown-display
包在对话中渲染 Markdown。此包允许我们以更好的格式渲染代码。
模型管理
我们在 App.tsx
文件中添加了一个 checkDownloadedModels
函数,该函数将检查模型是否已下载到设备上,如果未下载,我们将下载它;如果已下载,我们将直接将其加载到上下文中,并且我们在 UI 中添加了一些元素来显示模型是否已下载。
停止/返回按钮
我们在 UI 中添加了两个重要的按钮:停止按钮和返回按钮。停止按钮将停止响应的生成,返回按钮将导航到模型选择屏幕。为此,我们在 App.tsx
文件中添加了一个 handleStopGeneration
函数,该函数将通过调用 context.stop
来停止响应的生成,并将 isGenerating
状态设置为 false。我们还在 App.tsx
文件中添加了一个 handleBack
函数,该函数将通过将 currentPage
状态设置为 modelSelection
来导航到模型选择屏幕。
5. 如何调试
Chrome DevTools 调试
我们使用 Chrome DevTools 进行调试,就像在 Web 开发中一样。
- 在 Metro bundler 终端中按
j
启动 Chrome DevTools。 - 导航到“Sources”选项卡。
3. 找到您的源文件。
4. 通过点击行号设置断点。
5. 使用调试控件(右上角)。
- Step Over - 执行当前行
- Step Into - 进入函数调用
- Step Out - 退出当前函数
- Continue - 运行直到下一个断点
常见调试技巧
- 控制台日志
console.log('Debug value:', someValue);
console.warn('Warning message');
console.error('Error details');
这将在 Chrome DevTools 的控制台中记录输出。
- Metro Bundler 问题 如果您遇到 Metro Bundler 的问题,您可以尝试先清除缓存:
# Clear Metro bundler cache
npm start --reset-cache
- 构建错误
# Clean and rebuild
cd android && ./gradlew clean
cd ios && pod install
6. 我们可以添加的额外功能
为了提升用户体验,我们可以添加一些功能,例如:
模型管理
- 允许用户从设备中删除模型
- 添加一个功能,删除设备中所有已下载的模型
- 在用户界面中添加一个性能跟踪功能,以跟踪内存和CPU使用情况
模型选择
- 允许用户搜索模型
- 允许用户按名称、大小等对模型进行排序。
- 在用户界面中显示模型大小
- 添加对VLMs的支持
聊天界面
- 彩色显示代码
- 数学格式化
我敢肯定你能想到一些非常酷的功能可以添加到应用程序中,请随意实现它们并与社区分享🤗
7. 致谢
我要感谢以下人员审阅了这篇博客文章并提供了宝贵的反馈:
他们的专业知识和建议有助于提高本指南的质量和准确性。
8. 结论
现在你拥有了一个可以正常运行的React Native应用程序,它可以:
- 从Hugging Face下载模型
- 在本地运行推理
- 提供流畅的聊天体验
- 跟踪模型性能
此实现为构建更复杂的AI驱动移动应用程序奠定了基础。请记住在选择模型和调整参数时考虑设备功能。
祝您编码愉快!🚀