1. 从零跑通 React-Native + AI:为什么你的 Expo 项目一接大模型就崩
如果你正在用 Expo 管理的 React-Native 项目做移动端 AI 应用,大概率会遇到这几个场景:聊天界面流式输出变成一坨乱码、真机跑起来内存一路飙红、想接本地大模型却发现 Expo Go 根本加载不了原生推理库。这不是你代码写得差,而是移动端 AI 开发和 Web 端 AI 开发在底层约束上完全是两回事。
React-Native 本身是跨平台移动端框架,一套代码同时输出 Android 和 iOS,而 Expo 是它上面的一层工作流封装,把原生构建、依赖管理、热更新这些脏活都包掉了。把 AI 能力接进来之后,你要面对的是三件事的叠加:大模型 API 的网络调用、流式数据的渲染、以及端侧推理对原生模块的依赖。这三件事在 Web 上都有成熟方案,搬到 RN 上就各有各的坑。
这篇内容面向的是已经会写基础 React-Native、想在自己的 Expo 项目里落地第一个 AI 功能的开发者。我会用一条统一的 Key/API 通道把云端大模型和本地模型调用串起来,交付可以直接复制的app.json、settings.json和config.toml骨架,再给出真机联调和错误排查的验证动作。你跟着走完,能跑通一个带流式对话和工具调用雏形的移动端 AI 功能。
先说清楚两条技术路线,后面所有配置都围绕它们展开。第一条是纯云端调用,移动端只负责发请求、渲染结果,Agent 循环和工具执行全部放在后端,Expo Go 就能直接预览,开发效率最高。第二条是端侧本地推理,模型跑在手机上,隐私优先、可离线,但必须用npx expo run:android或run:ios做原生构建,Expo Go 不支持带原生 C++ 推理库的模块。绝大多数商业项目最终走的是混合架构:有网走云端,无网降级到端侧小模型。
我试过把 Web 端的 AI 逻辑直接复制到 RN 项目里,结果流式输出在 Hermes 引擎上丢 token、dangerouslyAllowBrowser没开导致请求直接被拦、上下文无限追加把内存撑爆。这些坑后面会逐个给排查动作。现在先把开发环境和统一接入通道搭起来。
2. TaoToken 前置准备:统一 Key 打通云端与本地大模型调用
在动手写代码之前,先把接入通道这件事定下来。移动端 AI 项目最忌讳的就是把 API Key 硬编码进前端,因为 RN 打包后的产物完全可以被逆向,Key 一旦泄露就是被人盗刷。所以正确的做法是:移动端只请求一个统一的 API 通道,Key 的保管、鉴权、限流、模型路由全部放在这个通道后面。
TaoToken 在这里扮演的就是这个统一通道的角色。它提供 OpenAI 兼容的接口协议,意味着你在 RN 里用的openaiSDK 不用改任何调用方式,只需要把baseURL指向它,就能同时调用云端大模型和转发到本地/自建的推理服务。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数。
你需要先拿到一个 API Key。进入控制台创建密钥的地址是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,创建完之后把 Key 复制出来,后面配置里会用到。如果你还不确定该选哪个模型,可以先去模型对话页面试一下效果:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite ,在网页上验证通了再写进代码,能省掉很多来回调试的时间。
这里要强调一个工程原则:移动端前端永远不直接持有长期有效的 Key。开发阶段可以用EXPO_PUBLIC_前缀的环境变量临时调试,但正式打包前必须换成自建后端代理,移动端只请求你自己的后端,由后端去调 TaoToken。这样即使前端被逆向,泄露的也只是你后端的地址,而不是能直接盗刷的密钥。
对于长期做编码类、Agent 类项目的同学,可以考虑 Coding Plan,它更适合持续性的开发调用场景:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到协议细节问题可以对照查。
环境准备清单如下,版本不对会直接导致构建失败,别跳过:
| 依赖 | 版本要求 | 作用 |
|---|---|---|
| Node.js | 20 LTS ~ 22 LTS | JS 运行时,不要用 23+ |
| JDK | 17(强制) | Android 编译,版本错直接报错 |
| Android Studio | 最新稳定版 | SDK、模拟器、NDK |
| Xcode | macOS 16+ | iOS 编译与模拟器 |
| Watchman | 最新 | 文件监听,macOS 推荐 brew 安装 |
macOS 上装基础依赖:
brew install node watchman node -v npm install -g expoWindows 用户注意,只能编译 Android,要做 iOS 必须 macOS。装好 Node 20 LTS、JDK 17 并配置JAVA_HOME,Android Studio 里配好ANDROID_HOME。另外 Windows 不要用 WSL2 做 Expo 开发,会出现 adb 设备识别异常。校验环境是否正常:
node -v expo --version adb devicesadb devices能列出模拟器或真机,说明 Android 环境通了。这一步没过,后面所有原生构建都会失败。
3. 可复制配置:app.json、settings.json 与 config.toml 骨架
这一节直接给可复制的配置骨架。创建项目用 TypeScript 模板:
npx create-expo-app rn-ai-demo --template expo-template-typescript cd rn-ai-demo先装依赖。云端调用用openaiSDK,它兼容 OpenAI 协议、支持流式输出;本地存储对话历史用 AsyncStorage;语音输入可选装 expo-audio 和 expo-speech-recognition:
npm install openai npx expo install @react-async-storage/async-storage npx expo install expo-audio expo-speech-recognition3.1 app.json 配置骨架
app.json是 Expo 项目的核心配置,AI 项目要额外注意权限声明和原生模块的 plugin 注册。下面这份可以直接改:
{ "expo": { "name": "rn-ai-demo", "slug": "rn-ai-demo", "version": "1.0.0", "orientation": "portrait", "scheme": "rnaidemo", "userInterfaceStyle": "automatic", "newArchEnabled": true, "ios": { "supportsTablet": true, "bundleIdentifier": "com.demo.rnaidemo", "infoPlist": { "NSMicrophoneUsageDescription": "用于语音输入对话内容", "NSCameraUsageDescription": "用于拍照识别场景" } }, "android": { "package": "com.demo.rnaidemo", "permissions": [ "RECORD_AUDIO", "INTERNET" ], "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" } }, "plugins": [ "expo-router", [ "expo-audio", { "microphonePermission": "允许 $(PRODUCT_NAME) 访问麦克风" } ] ], "extra": { "eas": { "projectId": "your-eas-project-id" } } } }newArchEnabled设为 true 是因为新架构对原生模块的调用性能更好,端侧推理场景尤其明显。plugins里注册的每个原生模块,都意味着你不能再用 Expo Go 预览,必须走开发构建。
3.2 settings.json 与本地模型配置
如果你走端侧本地推理路线,模型文件的管理需要一个配置。这里给一份settings.json骨架,放在项目根目录,用于描述本地模型的下载源和缓存策略:
{ "localModel": { "enabled": true, "provider": "executorch", "modelName": "llama-3.2-1b-instruct", "quantization": "q4", "downloadUrl": "https://your-cdn.example.com/models/llama-3.2-1b-q4.pte", "cacheDir": "models", "maxContextTokens": 2048, "temperature": 0.7 }, "cloudModel": { "baseUrl": "https://taotoken.net/api", "modelId": "deepseek-chat", "stream": true, "timeoutMs": 30000 }, "agent": { "maxLoopSteps": 5, "toolCallTimeoutMs": 15000, "enableMcpBridge": true, "mcpBridgeUrl": "wss://your-backend.example.com/mcp" } }注意cloudModel.baseUrl指向的是 TaoToken 的 API 入口,modelId按你实际要用的模型填。agent段里的mcpBridgeUrl是移动端 Agent 的关键:手机端不能直接跑 STDIO 传输的 MCP Server,必须通过后端 WebSocket 桥接。
3.3 config.toml 配置骨架
如果你用 EAS Build 做云构建,eas.json之外还可以用config.toml管理构建 profile。下面这份覆盖开发、预览、生产三档:
[build.development] distribution = "internal" developmentClient = true android = { buildType = "apk" } ios = { simulator = true } [build.preview] distribution = "internal" android = { buildType = "apk" } ios = { simulator = false } [build.production] android = { buildType = "app-bundle" } ios = { simulator = false } [submit.production] android = { serviceAccountKeyPath = "./secrets/play-service-account.json" } ios = { appleId = "your-apple-id@example.com", ascAppId = "1234567890" }developmentClient = true是本地模型调试的前提,它生成的是带开发客户端的构建,能加载原生推理模块。生产档用app-bundle而不是 apk,是为了上架 Google Play。
3.4 环境变量与安全红线
新建.env文件,开发阶段临时用:
EXPO_PUBLIC_AI_BASE_URL=https://taotoken.net/api EXPO_PUBLIC_AI_API_KEY=sk-你的开发密钥 EXPO_PUBLIC_AI_MODEL=deepseek-chat注意:
EXPO_PUBLIC_前缀的变量会被打进前端产物,任何人都能逆向拿到。这只适合开发调试,正式打包前必须换成自建后端代理,移动端只请求你自己的服务地址。
到这里配置骨架就齐了。下一步是把这些配置真正用起来,写一个能跑的流式对话。
4. 验证请求:流式对话与 Agent 工具调用的成功结果
配置写完不验证等于没写。这一节给一个最小可跑的流式聊天实现,再扩展到 Agent 工具调用,最后给出成功结果的判断标准。
4.1 流式对话核心代码
在app/(tabs)/chat.tsx里写:
import { View, Text, TextInput, Button, ScrollView } from 'react-native'; import AsyncStorage from '@react-async-storage/async-storage'; import OpenAI from 'openai'; import { useState, useEffect } from 'react'; const openai = new OpenAI({ baseURL: process.env.EXPO_PUBLIC_AI_BASE_URL, apiKey: process.env.EXPO_PUBLIC_AI_API_KEY, dangerouslyAllowBrowser: true, }); type MsgItem = { role: 'user' | 'assistant'; content: string }; export default function ChatPage() { const [msgList, setMsgList] = useState<MsgItem[]>([]); const [inputText, setInputText] = useState(''); useEffect(() => { (async () => { const raw = await AsyncStorage.getItem('chat_history'); if (raw) setMsgList(JSON.parse(raw)); })(); }, []); const sendMessage = async () => { if (!inputText.trim()) return; const userMsg: MsgItem = { role: 'user', content: inputText.trim() }; const all = [...msgList, userMsg]; setMsgList(all); setInputText(''); const stream = await openai.chat.completions.create({ model: process.env.EXPO_PUBLIC_AI_MODEL || 'deepseek-chat', messages: all, stream: true, }); let fullResp = ''; for await (const chunk of stream) { const delta = chunk.choices[0]?.delta?.content || ''; fullResp += delta; setMsgList([...all, { role: 'assistant', content: fullResp }]); } await AsyncStorage.setItem( 'chat_history', JSON.stringify([...all, { role: 'assistant', content: fullResp }]) ); }; return ( <View style={{ flex: 1, padding: 16 }}> <ScrollView style={{ flex: 1 }}> {msgList.map((m, i) => ( <Text key={i} style={{ marginVertical: 4 }}> {m.role === 'user' ? '用户:' : 'AI:'}{m.content} </Text> ))} </ScrollView> <TextInput value={inputText} onChangeText={setInputText} style={{ borderWidth: 1, padding: 8 }} /> <Button title="发送" onPress={sendMessage} /> </View> ); }dangerouslyAllowBrowser: true在 RN 环境里是必须的,否则 SDK 会拒绝在非 Node 环境发起请求。流式循环里每次拿到 delta 就更新 state,这样界面能逐字渲染。
4.2 Agent 工具调用扩展
移动端做 Agent,核心原则是前端只做 UI 和会话管理,Agent Loop 和工具执行全部下沉到后端。前端通过 WebSocket 连接后端的 MCP 桥接服务,拿到工具列表,把用户输入发给后端,后端跑完循环再把结果流式回传。
const ws = new WebSocket(process.env.EXPO_PUBLIC_MCP_BRIDGE_URL!); ws.onopen = () => { ws.send(JSON.stringify({ type: 'list_tools' })); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'tool_list') { console.log('可用工具:', data.tools); } if (data.type === 'agent_delta') { setMsgList((prev) => { const last = prev[prev.length - 1]; if (last?.role === 'assistant') { return [...prev.slice(0, -1), { ...last, content: last.content + data.delta }]; } return [...prev, { role: 'assistant', content: data.delta }]; }); } };后端收到用户消息后,负责调用大模型、解析工具调用意图、执行 MCP 工具、把结果再喂回模型,整个循环在后端完成。前端只负责把agent_delta渲染出来。
4.3 成功结果的判断标准
跑通之后你应该看到这些现象:输入一句话,AI 回复逐字出现而不是等半天一次性弹出;杀掉 APP 重进,历史对话还在;后端日志里能看到工具调用记录,前端收到的是流式增量。如果这三点都满足,说明云端链路和 Agent 桥接都通了。
端侧本地模型验证方式不同。执行npx expo run:android构建后,在 APP 里触发本地推理,观察日志里模型加载耗时和首 token 延迟。1B 量化模型在中端 Android 机上首 token 延迟通常在几百毫秒到两秒之间,如果超过十秒或者直接闪退,多半是内存不够或模型文件损坏。
5. 本篇常见错排查:401、local proxy failed、reading choices 与 OAuth
这一节按真实报错来。每个错误给出触发条件和排查动作,你对照自己的日志找。
5.1 401 Unauthorized
最常见。触发原因是 Key 无效、过期、或者请求头没带上。排查顺序:先确认.env里的EXPO_PUBLIC_AI_API_KEY没有多余空格和换行;再确认baseURL指向的是https://taotoken.net/api而不是别的地址;最后去控制台确认这个 Key 还在有效期内。如果是在 Expo Go 里跑,改完.env必须重启 dev server,环境变量不会热更新。
5.2 local proxy failed
这个报错通常出现在你配置了本地代理或者后端转发,但转发目标不可达。检查settings.json里的cloudModel.baseUrl是否写成了内网地址而真机不在同一网段。真机联调时,手机和电脑要在同一个 Wi-Fi 下,且后端服务监听的是0.0.0.0而不是127.0.0.1。用curl在电脑上先验证后端通不通,再让手机请求。
5.3 reading 'choices' of undefined
流式解析时chunk.choices为 undefined。原因通常是返回的不是标准 OpenAI 格式,或者请求被拦截返回了错误 JSON。排查动作:把stream临时设为 false,打印完整响应体看结构;确认model字段填的模型 ID 在 TaoToken 侧是存在的;检查是不是把baseURL末尾多写了/v1导致路径拼接错误。TaoToken 的 API 入口是https://taotoken.net/api,SDK 会自动拼/v1/chat/completions,不要手动重复。
5.4 OAuth 相关报错
如果你在用 Claude Code 或类似的编码工具接入,可能会遇到 OAuth 认证失败。这类工具通常需要三件套配齐:Base URL、API Key、Model ID。Base URL 填https://taotoken.net/api,Key 用控制台创建的密钥,Model ID 按工具要求填。三者缺一或者 Model ID 写错都会报 OAuth 或认证类错误。Claude Code 的接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,对照检查配置项。
5.5 其他高频坑
JDK 版本不对会直接导致 Android 编译失败,必须是 17。Expo Go 跑不了本地推理库,报错通常是 "native module not found",换成npx expo run:android即可。iOS 上pod install超时可以执行pod cache clean --all后重试。流式输出丢 token 或乱码,优先升级 Hermes 引擎版本,并确认dangerouslyAllowBrowser已开启。内存持续上涨,是因为消息列表无限追加,必须做上下文截断,只保留最近 N 轮对话。
6. 语义一致 CTA:把这条链路用到你的项目里
走到这里,你已经有了一个能跑的 Expo + AI 移动端骨架:统一通道打通了云端和本地模型调用,流式对话验证通过,Agent 工具调用通过后端桥接落地,常见报错也有了排查路径。
接下来最值得做的一件事,是把你自己的业务场景接进去。如果你还在选模型阶段,先去模型对话页面把几个候选模型都试一遍,看哪个在移动端场景下响应质量和速度更合适:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。确定之后回到控制台创建正式密钥:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,把开发用的临时 Key 换掉。
如果你的项目是长期迭代的编码类或 Agent 类应用,Coding Plan 会比按量调用更省心:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。协议细节和参数说明随时查接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
最后一个实操建议:在正式打包前,务必把前端直连改成后端代理。移动端只保留你后端服务的地址,Key 全部收进后端环境变量。这一步做完,你的移动端 AI 项目才算真正具备上线条件。