Back to blog
    React Native 端侧 AI:使用 llama.rn 集成指南
    React Nativellama.cppon-device AIcross-platformllama.rnsegment:mobile-builder

    React Native 端侧 AI:使用 llama.rn 集成指南

    如何在 React Native 应用中直接在用户手机上运行语言模型。使用 llama.rn 进行设置、模型加载、流式生成和跨平台注意事项。

    EErtas Team·

    llama.rn 是一个 React Native 库,提供 llama.cpp 的 JavaScript 绑定。它通过相同的 JavaScript API 在 iOS(Metal)和 Android(CPU/Vulkan)上原生运行 GGUF 语言模型。

    对于 React Native 开发者来说,这意味着一套代码库、一个 API,零云端依赖的端侧 AI。

    安装

    npm install llama.rn
    # 或
    yarn add llama.rn

    对于 iOS,运行 pod install:

    cd ios && pod install

    对于 Android,原生库通过 autolinking 自动包含。

    Expo

    如果您使用 Expo,由于 llama.rn 包含原生代码,需要开发构建(不能使用 Expo Go):

    npx expo prebuild
    npx expo run:ios  # 或 run:android

    加载模型

    import { initLlama, LlamaContext } from "llama.rn";
    
    let context: LlamaContext | null = null;
    
    async function loadModel(modelPath: string) {
      context = await initLlama({
        model: modelPath,
        n_ctx: 2048,        // 上下文窗口
        n_threads: 4,       // CPU 线程数
        n_gpu_layers: 99,   // 卸载到 GPU (Metal/Vulkan)
        use_mlock: true,    // 锁定模型在内存中
      });
    
      console.log("Model loaded successfully");
    }

    模型路径

    模型路径必须指向设备文件系统上的本地文件。如何将文件放到设备上取决于您的分发策略:

    与应用打包:

    // iOS:复制到应用 bundle,通过 RNFS 引用
    import RNFS from "react-native-fs";
    const modelPath = `${RNFS.MainBundlePath}/model.gguf`;
    
    // Android:首次启动时从 assets 复制到 files 目录
    const modelPath = `${RNFS.DocumentDirectoryPath}/model.gguf`;

    安装后下载:

    import RNFS from "react-native-fs";
    
    const modelUrl = "https://cdn.example.com/model.gguf";
    const modelPath = `${RNFS.DocumentDirectoryPath}/model.gguf`;
    
    const download = RNFS.downloadFile({
      fromUrl: modelUrl,
      toFile: modelPath,
      progress: (res) => {
        const percentage = (res.bytesWritten / res.contentLength) * 100;
        setDownloadProgress(percentage);
      },
    });
    
    await download.promise;

    生成文本

    简单生成

    async function generate(prompt: string): Promise<string> {
      if (!context) throw new Error("Model not loaded");
    
      const result = await context.completion({
        prompt: prompt,
        n_predict: 256,
        temperature: 0.7,
        top_p: 0.9,
        stop: ["</s>", "<|eot_id|>"],  // 停止标记
      });
    
      return result.text;
    }

    流式生成

    async function generateStream(
      prompt: string,
      onToken: (token: string) => void
    ): Promise<string> {
      if (!context) throw new Error("Model not loaded");
    
      const result = await context.completion(
        {
          prompt: prompt,
          n_predict: 256,
          temperature: 0.7,
          stop: ["</s>", "<|eot_id|>"],
        },
        (data) => {
          // 每个生成的 token 都会调用
          onToken(data.token);
        }
      );
    
      return result.text;
    }

    对话补全

    对于多轮对话,使用模型的聊天模板格式化提示:

    interface Message {
      role: "system" | "user" | "assistant";
      content: string;
    }
    
    function formatChat(messages: Message[]): string {
      // Llama 3.2 聊天模板
      let prompt = "<|begin_of_text|>";
    
      for (const msg of messages) {
        prompt += `<|start_header_id|>${msg.role}<|end_header_id|>\n\n${msg.content}<|eot_id|>`;
      }
    
      prompt += "<|start_header_id|>assistant<|end_header_id|>\n\n";
      return prompt;
    }
    
    async function chat(messages: Message[], onToken: (token: string) => void) {
      const prompt = formatChat(messages);
      return generateStream(prompt, onToken);
    }

    React Hook 模式

    import { useState, useCallback, useRef } from "react";
    import { initLlama, LlamaContext } from "llama.rn";
    
    export function useLlama(modelPath: string) {
      const contextRef = useRef<LlamaContext | null>(null);
      const [isLoaded, setIsLoaded] = useState(false);
      const [isGenerating, setIsGenerating] = useState(false);
      const [response, setResponse] = useState("");
    
      const load = useCallback(async () => {
        contextRef.current = await initLlama({
          model: modelPath,
          n_ctx: 2048,
          n_threads: 4,
          n_gpu_layers: 99,
        });
        setIsLoaded(true);
      }, [modelPath]);
    
      const generate = useCallback(async (prompt: string) => {
        if (!contextRef.current) return;
        setIsGenerating(true);
        setResponse("");
    
        await contextRef.current.completion(
          {
            prompt,
            n_predict: 256,
            temperature: 0.7,
            stop: ["</s>", "<|eot_id|>"],
          },
          (data) => {
            setResponse((prev) => prev + data.token);
          }
        );
    
        setIsGenerating(false);
      }, []);
    
      const unload = useCallback(() => {
        contextRef.current?.release();
        contextRef.current = null;
        setIsLoaded(false);
      }, []);
    
      return { load, generate, unload, isLoaded, isGenerating, response };
    }

    在组件中使用

    function AiChat() {
      const { load, generate, unload, isLoaded, isGenerating, response } =
        useLlama(modelPath);
      const [input, setInput] = useState("");
    
      useEffect(() => {
        load();
        return () => unload();
      }, []);
    
      return (
        <View style={styles.container}>
          <ScrollView style={styles.responseArea}>
            <Text>{response}</Text>
          </ScrollView>
          <View style={styles.inputRow}>
            <TextInput
              value={input}
              onChangeText={setInput}
              style={styles.input}
              editable={!isGenerating}
            />
            <Button
              title="Send"
              onPress={() => {
                generate(input);
                setInput("");
              }}
              disabled={!isLoaded || isGenerating}
            />
          </View>
        </View>
      );
    }

    跨平台注意事项

    性能一致性

    llama.rn 在两个平台上运行原生代码。JavaScript 桥接仅用于传递提示和接收 token。实际推理性能与原生 Swift/Kotlin 集成一致:

    设备1B 模型 (tok/s)3B 模型 (tok/s)
    iPhone 15 Pro35-4518-25
    iPhone 1425-3214-18
    Galaxy S2435-4518-25
    中端 Android18-258-12

    JS 桥接每个 token 增加不到 1 毫秒的开销。可忽略不计。

    模型文件路径差异

    iOS 和 Android 将文件存储在不同位置。使用 react-native-fs 获取平台对应的路径:

    import RNFS from "react-native-fs";
    import { Platform } from "react-native";
    
    const modelDir = Platform.OS === "ios"
      ? RNFS.DocumentDirectoryPath
      : RNFS.DocumentDirectoryPath;  // 相同 API,不同底层路径

    内存管理

    React Native 不暴露直接的内存 API。如果需要在加载前验证可用 RAM,可以通过原生模块进行平台特定检查。或者,捕获加载失败并显示适当的消息。

    React Native 中的模型分发

    策略 1:小模型打包

    对于 1B 模型(约 600MB),与应用打包是可行的:

    • iOS:作为资源添加到 Xcode 项目
    • Android:超过 150MB 的文件使用 Android Asset Delivery

    策略 2:所有模型下载安装

    对于 3B 模型(约 1.7GB)或希望保持初始下载较小:

    async function ensureModelReady(): Promise<string> {
      const modelPath = `${RNFS.DocumentDirectoryPath}/model.gguf`;
      const exists = await RNFS.exists(modelPath);
    
      if (exists) return modelPath;
    
      // 带进度的下载
      await RNFS.downloadFile({
        fromUrl: MODEL_CDN_URL,
        toFile: modelPath,
        progress: (res) => {
          updateProgress(res.bytesWritten / res.contentLength);
        },
      }).promise;
    
      // 验证完整性
      const hash = await RNFS.hash(modelPath, "sha256");
      if (hash !== EXPECTED_HASH) {
        await RNFS.unlink(modelPath);
        throw new Error("Model download corrupted");
      }
    
      return modelPath;
    }

    生产最佳实践

    1. 延迟加载模型。 仅在用户访问 AI 功能时加载。
    2. 失焦时卸载。 当 AI 界面不在焦点时释放模型内存。
    3. 优雅处理错误。 低内存设备可能加载失败。显示清晰的消息。
    4. 验证下载。 下载后进行 SHA256 哈希检查。损坏的模型会导致崩溃。
    5. 缓冲 token。 每 2-3 个 token 批量更新 UI 以获得更流畅的文本显示。
    6. 支持取消。 允许用户中途停止生成。

    模型质量取决于微调。基础模型给出通用回复。在您的领域数据上微调的模型(通过 Ertas 等平台)给出针对应用特定用例的定制回复,在相同硬件上以相同速度运行。

    Ship AI that runs on your users' devices.

    Early bird pricing starts at $14.50/mo — locked in for life. Plans for builders and agencies.

    Keep reading