AI Chatbot

如何在 React Native 应用中添加 AI 聊天机器人(附 FastAPI 后端)

大多数 React Native 教程止步于 UI 层——展示如何渲染聊天气泡,然后用一句含糊的"直接从应用调用 OpenAI API"带过后端部分。

这种方式存在两个根本性问题。其一:嵌入移动端二进制文件的 API 密钥可被任何人通过反编译提取。其二:完全没有服务端控制——无速率限制、无用户上下文注入、无日志审计,也无法在不发版的情况下更换模型。

本文采用面向生产环境的方案:构建一个处理 LLM 连接的 FastAPI 后端(使用流式 Server-Sent Events),并将其接入 Expo(React Native)前端,实现逐 token 实时渲染。

这一架构同样适用于满足《数据安全法》《个人信息保护法(PIPL)》等保2.0(尤其是涉及工控系统的 OT 环境)的私有化部署需求——推理服务可完全部署在客户自有基础设施上,数据不出境。


系统架构

flowchart TD
  A["Mobile App Expo SDK 54"] --> B["FastAPI Backend"]
  B --> C["LLM Provider"]
  C --> D["SSE Stream"]
  D --> E["Chunked Fetch RN 0.81"]
  E --> F["Chat UI 逐 token 渲染"]

为什么选择 FastAPI

FastAPI 提供中间件依赖注入用于身份认证、符合等保2.0 合规要求的操作日志记录,以及与 Python 生态私有 LLM(Ollama、vLLM、LiteLLM、Qwen 系列)的无缝集成。当客户需要从公有云 LLM 切换到私有部署时,只需修改 FastAPI 侧的 base_url,移动端代码无需任何变更。

为什么用 SSE 而非 WebSocket

SSE 基于 HTTP/1.1,对反向代理友好,更易于在国内云环境(阿里云、腾讯云、华为云)上配置负载均衡和审计日志。


模型选型(2026 年 6 月行情)

模型 输入 / 百万 token 输出 / 百万 token 适用场景
Claude Haiku 4.5 $1.00 $5.00 高并发聊天机器人、FAQ 机器人
Claude Sonnet 4.6 $3.00 $15.00 复杂推理、销售助手
DeepSeek V4 Flash $0.14 $0.28 成本敏感的国内移动端部署
Qwen3.5-Plus $0.40 $2.40 中文优化场景、阿里云生态

对于大多数移动端聊天机器人——客服机器人、新手引导助手、FAQ 自动回复——Claude Haiku 4.5 是性价比最优选择:200K token 上下文窗口,单次对话成本约为 Sonnet 4.6 的 1/60。如需对接企业知识库(用友、金蝶等 ERP 导出的文档)进行复杂问答,可升级至 Sonnet 4.6。

若部署环境对数据出境有严格限制,DeepSeek V4 Flash 或国内版 Qwen3.5-Plus 可在阿里云/腾讯云上通过 OpenAI 兼容接口直接替换,后端代码无需修改。


第一部分:FastAPI 后端

环境搭建

mkdir chatbot-api && cd chatbot-api
python -m venv .venv && source .venv/bin/activate
pip install fastapi uvicorn anthropic python-dotenv

创建 .env 文件:

ANTHROPIC_API_KEY=your_key_here
MODEL_ID=claude-haiku-4-5
SYSTEM_PROMPT="你是 Acme 公司的智能客服助手。"

流式聊天接口

# main.py
import os
from fastapi import FastAPI, HTTPException, Header
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import List
import anthropic
from dotenv import load_dotenv

load_dotenv()

app = FastAPI()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
MODEL = os.getenv("MODEL_ID", "claude-haiku-4-5")
SYSTEM = os.getenv("SYSTEM_PROMPT", "你是一个有帮助的助手。")

class Message(BaseModel):
    role: str
    content: str

class ChatRequest(BaseModel):
    messages: List[Message]

def stream_response(messages: List[Message]):
    with client.messages.stream(
        model=MODEL,
        max_tokens=1024,
        system=SYSTEM,
        messages=[m.model_dump() for m in messages],
    ) as stream:
        for text in stream.text_stream:
            yield f"data: {text}\n\n"
    yield "data: [DONE]\n\n"

@app.post("/chat")
async def chat(request: ChatRequest, x_api_key: str = Header(...)):
    if x_api_key != os.getenv("APP_API_KEY"):
        raise HTTPException(status_code=401, detail="未授权")
    if not request.messages:
        raise HTTPException(status_code=400, detail="消息不能为空")
    return StreamingResponse(
        stream_response(request.messages),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
    )

@app.get("/health")
async def health():
    return {"status": "ok", "model": MODEL}

本地启动:

uvicorn main:app --reload --port 8000

第二部分:React Native 聊天 UI(Expo SDK 54)

React Native 上的 SSE 流式处理

React Native 没有原生 EventSource,但从 RN 0.79+ 开始,通过在 fetch 选项中传入 reactNative: { textStreaming: true },可以使用 response.body.getReader() 逐块读取响应。

// hooks/useChat.ts
import { useState, useCallback } from "react";

export interface Message {
  id: string;
  role: "user" | "assistant";
  content: string;
}

const API_URL = process.env.EXPO_PUBLIC_API_URL ?? "http://localhost:8000";
const API_KEY = process.env.EXPO_PUBLIC_APP_API_KEY ?? "";

export function useChat() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [isStreaming, setIsStreaming] = useState(false);

  const sendMessage = useCallback(async (text: string) => {
    const userMessage: Message = { id: Date.now().toString(), role: "user", content: text };
    const updated = [...messages, userMessage];
    setMessages(updated);
    setIsStreaming(true);

    const assistantId = (Date.now() + 1).toString();
    setMessages((prev) => [...prev, { id: assistantId, role: "assistant", content: "" }]);

    try {
      const response = await fetch(`${API_URL}/chat`, {
        method: "POST",
        headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
        body: JSON.stringify({ messages: updated.map(({ role, content }) => ({ role, content })) }),
        reactNative: { textStreaming: true },
      } as RequestInit);

      const reader = response.body?.getReader();
      const decoder = new TextDecoder();
      while (reader) {
        const { done, value } = await reader.read();
        if (done) break;
        const chunk = decoder.decode(value, { stream: true });
        for (const line of chunk.split("\n")) {
          if (line.startsWith("data: ")) {
            const token = line.slice(6);
            if (token === "[DONE]") break;
            setMessages((prev) =>
              prev.map((m) => m.id === assistantId ? { ...m, content: m.content + token } : m)
            );
          }
        }
      }
    } catch (err) {
      console.error("流式错误:", err);
    } finally {
      setIsStreaming(false);
    }
  }, [messages]);

  return { messages, sendMessage, isStreaming };
}

第三部分:移动端特有注意事项

网络中断处理 — 移动网络不稳定。用 try/catch 包裹 reader 循环,在流中断时显示"点击重试"按钮,保留已接收内容。

FlatList 重渲染优化 — 每个 token 都会触发 state 更新。用 useCallback 记忆化 renderItem,并开启 removeClippedSubviews

API 密钥安全 — 即使使用 Header 方案,密钥仍存在于 bundle 中。对安全要求更高的应用,建议实现短期令牌机制:应用通过正常认证登录后端,后端为聊天接口签发 15 分钟有效令牌。等保2.0 三级及以上系统建议记录所有 LLM 调用日志以满足审计要求。


切换至私有 LLM(等保2.0 & 数据不出境)

若业务需要满足等保2.0 对工控系统(OT/工业控制系统)的特殊要求,或 PIPL 的数据本地化要求,可将 FastAPI 后端接入私有部署的推理服务:

# 切换至 Ollama / vLLM / Qwen 私有部署
from openai import OpenAI

client = OpenAI(
    base_url="http://your-private-llm:11434/v1",
    api_key="not-needed",
)

React Native 应用无需任何修改,流式协议完全兼容。


常见问题

一定需要 FastAPI 后端吗?能从 React Native 直接调用 LLM API 吗?

技术上可行,但生产环境不推荐。嵌入移动端 bundle 的 API 密钥可被反编译提取,且无法实现服务端的任何控制(限流、日志、模型切换)。

React Native 没有 EventSource,流式是怎么实现的?

RN 0.79+ 支持在 fetch 选项中传入 reactNative: { textStreaming: true },通过 response.body.getReader() 逐块读取,手动解析 SSE 的 data: 前缀。

大并发客服机器人应该选哪个模型?

从 Claude Haiku 4.5 开始——$1.00/M 输入 token,专为此类场景设计,200K 上下文窗口可容纳较长对话历史。若对话需要复杂推理或文档综合,再升级至 Sonnet 4.6。国内数据不出境场景可考虑 DeepSeek V4 Flash 或 Qwen3.5-Plus。

Android 和 iOS 上的流式代码一致吗?

是的。RN 0.81 + Expo SDK 54 在两个平台上的 chunked fetch 方案完全一致。


下一步

本文建立了基础:FastAPI 流式后端、生产级 Expo 聊天 UI,以及教程通常忽略的移动端特有处理。

R 系列接下来:

  • 端侧 AI — 无需后端,直接在设备上运行量化模型
  • 将聊天机器人接入业务数据 — 结合 RAG 流水线(pgvector + 私有文档),实现基于企业知识库的问答

有 React Native + AI 集成需求?联系 Simplico 团队,我们为东南亚和日本客户构建生产级移动端 AI 功能。