邊緣裝置上的大語言模型推理:透過 React Native 在手機上執行大語言模型的趣味簡易指南!

釋出日期:2025年3月7日
在 GitHub 上更新

隨著大型語言模型(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 的啟發,加入了一些新特性。
  • 檔案大小更小,但在某些硬體上可能更慢。
  • 最適合計算能力強但記憶體有限的裝置。

推薦嘗試的模型

以下是一些在移動裝置上表現良好的模型:

  1. SmolLM2-1.7B-Instruct
  2. Qwen2-0.5B-Instruct
  3. Llama-3.2-1B-Instruct
  4. DeepSeek-R1-Distill-Qwen-1.5B

查詢更多模型

要在 Hugging Face 上查詢更多 GGUF 模型:

  1. 訪問 huggingface.co/models
  2. 使用搜索過濾器
    • 訪問 GGUF 模型頁面
    • 在搜尋欄中指定模型大小
    • 對於對話模型,查詢名稱中包含“chat”或“instruct”的模型

Hugging Face search filters

在選擇模型時,請同時考慮引數數量和量化級別。例如,使用 Q2_K 量化的 7B 模型可能比使用 Q8_0 量化的 2B 模型執行得更好。因此,如果您的裝置可以輕鬆容納小型模型,請嘗試使用更大的量化模型,它可能會有更好的效能。

1. 設定您的環境

React Native 是一個流行的框架,用於使用 JavaScript 和 React 構建移動應用程式。它允許開發人員建立在 Android 和 iOS 平臺上執行的應用程式,同時共享大量程式碼,從而加快開發過程並減少維護工作。

在開始使用 React Native 編碼之前,您需要正確設定您的環境。

所需工具

  1. Node.js: Node.js 是一個 JavaScript 執行時,允許您執行 JavaScript 程式碼。它對於管理 React Native 專案中的包和依賴項至關重要。您可以從 Node.js 下載頁面安裝它。

  2. react-native-community/cli: 此命令安裝 React Native 命令列介面 (CLI),它提供用於建立、構建和管理您的 React Native 專案的工具。執行以下命令安裝:

npm i @react-native-community/cli

虛擬裝置設定

要在開發過程中執行您的應用程式,您需要一個模擬器或模擬器。

  • 如果您使用的是 macOS:

    • 對於 iOS:安裝 Xcode -> 開啟開發者工具 -> 模擬器 alt text
    • 對於 Android:安裝 Java Runtime 和 Android Studio -> 轉到裝置管理器並建立一個模擬器 alt text
  • 如果您使用的是 Windows 或 Linux:

    • 對於 iOS:我們需要依賴基於雲的模擬器,如 LambdaTestBrowserStack
    • 對於 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>

專案結構

應用程式資料夾組織如下:

預設檔案/資料夾

  1. android/

    • 包含原生 Android 專案檔案。
    • 目的:用於在 Android 裝置上構建和執行應用程式。
  2. ios/

    • 包含原生 iOS 專案檔案。
    • 目的:用於在 iOS 裝置上構建和執行應用程式。
  3. node_modules/

    • 目的:儲存專案中使用的所有 npm 依賴項。
  4. App.tsx

    • 您應用程式的主要根元件,使用 TypeScript 編寫。
    • 目的:應用程式 UI 和邏輯的入口點。
  5. 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. 執行演示和專案

執行演示

要執行專案並在您自己的虛擬裝置上檢視其外觀,請按照以下步驟操作:

  1. 克隆倉庫:

    git clone https://github.com/MekkCyber/EdgeLLM.git
    
  2. 導航到專案目錄:

    cd EdgeLLMPlus 
    #or 
    cd EdgeLLMBasic
    
  3. 安裝依賴:

    npm install
    
  4. 導航到 iOS 資料夾並安裝:

    cd ios
    pod install
    
  5. 啟動 Metro Bundler:在專案資料夾(EdgeLLMPlus 或 EdgeLLMBasic)中執行以下命令:

    npm start
    
  6. 在 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 app

“Hello World”文字沒有正確顯示,因為我們使用的是一個簡單的 View 元件,我們需要使用 SafeAreaView 元件來正確顯示文字,我們將在下一節中處理這個問題。

現在讓我們思考一下我們的應用程式目前需要跟蹤什麼:

  1. 與聊天相關的::

    • 對話歷史(使用者和 AI 之間的訊息)
    • 當前使用者輸入
  2. 與模型相關的::

    • 選定的模型格式(如 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”)和內容(實際訊息文字)。

現在我們已經設定了基本的狀態管理,我們需要考慮如何:

  1. Hugging Face 獲取可用的 GGUF 模型
  2. 在本地下載和管理模型
  3. 建立聊天介面
  4. 處理訊息生成

讓我們在接下來的部分中逐一解決這些問題……

從 Hub 獲取可用的 GGUF 模型

首先,讓我們定義應用程式將支援的模型格式及其倉庫。當然,llama.rnllama.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 InstructDeepSeek R1 Distill Qwen 1.5BQwen 2 0.5B InstructSmolLM2 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>

這應該看起來像這樣:

Available GGUF Files

注意:到目前為止的完整程式碼,您可以在 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 函式,該函式接受三個引數:modelNamedownloadUrl 和一個 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 作為模型格式,函式才能工作,我們應該會看到類似以下的內容:

Download Model

注意:到目前為止的完整程式碼,您可以在 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。如果一切正常,我們應該會在控制檯中看到打印出的上下文。alt text

注意:到目前為止的完整程式碼,您可以在 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。結果應該如下所示:

Chat

注意:到目前為止的完整程式碼,您可以在 EdgeLLMBasic 資料夾的 fourth_checkpoint 分支 此處 檢視。

使用者介面與邏輯

現在我們已經實現了核心功能,我們可以專注於 UI。UI 很簡單,由一個模型選擇螢幕(包含模型列表)和一個聊天介面(包含對話歷史和使用者輸入欄位)組成。在模型下載階段,會顯示一個進度條。我們有意避免新增許多螢幕,以使應用程式保持簡單並專注於其核心功能。為了跟蹤應用程式的哪個部分正在使用,我們將使用另一個狀態變數 currentPage,它將是一個字串,可以是 modelSelectionconversation。我們將其新增到 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 檔案列表。螢幕在您的裝置上應如下所示:

Model Selection Start

接下來,我們將在模型格式選擇部分的下方新增檢視,以顯示所選模型格式已有的 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 檔案列表,這意味著使用者已選擇模型格式。

Available GGUF Files

我們需要在 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 檔案後,我們應該會看到一個警報,確認或取消下載。

Confirm Download

我們可以向檢視新增一個簡單的 ActivityIndicator,以在獲取可用 GGUF 檔案時顯示載入狀態。為此,我們需要從 react-native 匯入 ActivityIndicator,並將 isFetching 定義為一個布林狀態變數,該變數在 fetchAvailableGGUFs 函式開始時設定為 true,在函式結束時設定為 false,如您在此處 程式碼 中所見,並將 ActivityIndicator 新增到檢視中,緊鄰 {availableGGUFs.map((file, index) => (...))} 之前,以在獲取可用 GGUF 檔案時顯示載入狀態。

{isFetching && (
  <ActivityIndicator size="small" color="#2563EB" />
)}

當 GGUF 檔案正在獲取時,應用程式應該會短暫地顯示如下介面:

Download Indicator

現在我們應該能夠看到每次點選模型格式時,對應可用的不同 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>
...

確認模型下載後,我們應該會看到如下螢幕:

Download Progress Bar

注意:到目前為止的完整程式碼,您可以在 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 分支 此處 檢視。

對話頁面現在應該如下所示:

Whole Basic App

恭喜您,您剛剛構建了第一個人工智慧聊天機器人的核心功能,程式碼可在此處 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,例如:來處理模型的內部推理或“思考”。當遇到token 時,應用程式進入“思考塊”,在那裡它積累表示模型推理的 token。一旦檢測到結束token,提取累積的思考並將其與訊息關聯,允許使用者切換模型推理的可見性。為了實現這一點,我們需要向 Message 型別新增 thoughtshowThought 屬性。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 開發中一樣。

  1. 在 Metro bundler 終端中按 j 啟動 Chrome DevTools。
  2. 導航到“Sources”選項卡。

alt text 3. 找到您的原始檔。
4. 透過點選行號設定斷點。
5. 使用除錯控制元件(右上角)。

  • Step Over - 執行當前行
  • Step Into - 進入函式呼叫
  • Step Out - 退出當前函式
  • Continue - 執行直到下一個斷點

常見除錯技巧

  1. 控制檯日誌
console.log('Debug value:', someValue);
console.warn('Warning message');
console.error('Error details');

這將在 Chrome DevTools 的控制檯中記錄輸出。

  1. Metro Bundler 問題 如果您遇到 Metro Bundler 的問題,您可以嘗試先清除快取:
# Clear Metro bundler cache
npm start --reset-cache
  1. 構建錯誤
# Clean and rebuild
cd android && ./gradlew clean
cd ios && pod install

6. 我們可以新增的額外功能

為了提升使用者體驗,我們可以新增一些功能,例如:

  • 模型管理

    • 允許使用者從裝置中刪除模型
    • 新增一個功能,刪除裝置中所有已下載的模型
    • 在使用者介面中新增一個性能跟蹤功能,以跟蹤記憶體和CPU使用情況
  • 模型選擇

    • 允許使用者搜尋模型
    • 允許使用者按名稱、大小等對模型進行排序。
    • 在使用者介面中顯示模型大小
    • 新增對VLMs的支援
  • 聊天介面

    • 彩色顯示程式碼
    • 數學格式化

我敢肯定你能想到一些非常酷的功能可以新增到應用程式中,請隨意實現它們並與社群分享🤗

7. 致謝

我要感謝以下人員審閱了這篇部落格文章並提供了寶貴的反饋:

他們的專業知識和建議有助於提高本指南的質量和準確性。

8. 結論

現在你擁有了一個可以正常執行的React Native應用程式,它可以:

  • 從Hugging Face下載模型
  • 在本地執行推理
  • 提供流暢的聊天體驗
  • 跟蹤模型效能

此實現為構建更復雜的AI驅動移動應用程式奠定了基礎。請記住在選擇模型和調整引數時考慮裝置功能。

祝您編碼愉快!🚀

社群

非常非常棒的工作!

如果你能讓我們更好地瞭解這個專案的磁碟大小就好了。上傳到Play/iOS商店的考慮。也許可以將pytorch整合到tensorflow中,以利用Pixel中的M晶片來真正讓這些東西飛速執行,使用像google-ai-edge/ai-edge-torch這樣的工具。一個很好的開始。

安裝後下載模型是一個好主意,因為它能保持應用程式的輕量級,並讓使用者可以靈活選擇最適合他們需求的模型。

你好,我能把VLMs轉換成GGUF嗎?

註冊登入 以評論

© . This site is unofficial and not affiliated with Hugging Face, Inc.