บทความ React Native ส่วนใหญ่หยุดแค่ UI layer — แสดงวิธีทำ chat bubble แต่ข้ามเรื่อง backend ด้วยคำแนะนำคลุมเครือว่า "เรียก OpenAI API จากแอปโดยตรง"
วิธีนั้นมีปัญหาใหญ่สองอย่าง: หนึ่ง — API Key ที่ฝังอยู่ใน binary ถูกดึงออกได้โดยใครก็ตามที่ decompile แอป สอง — ไม่มีการควบคุมฝั่ง server: ไม่มี rate limiting, ไม่มี user context, ไม่สามารถเปลี่ยน model ได้โดยไม่ต้อง push app update
บทความนี้เดินตามแนวทาง production จริง: สร้าง FastAPI backend ที่เชื่อมต่อ LLM พร้อม Streaming Server-Sent Events (SSE) แล้วเชื่อมกับ Expo (React Native) ที่แสดง response ทีละ token ขณะที่ AI กำลังพิมพ์
Pattern นี้รองรับการ Deploy ภายใต้ข้อกำหนด PDPA (พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล พ.ศ. 2562) เพราะ LLM inference สามารถอยู่ใน region ไทยหรือบน infrastructure ของลูกค้าได้ทั้งหมด
สถาปัตยกรรมของระบบ
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 ให้ความยืดหยุ่นในการเพิ่ม middleware สำหรับ authentication, logging สำหรับ audit trail ตามที่ PDPA กำหนด และ integration กับ private LLM บน infrastructure ของลูกค้าได้โดยตรง — สิ่งที่ serverless route ทำได้ยากกว่า
ทำไมถึงใช้ SSE แทน WebSocket
SSE เป็น HTTP/1.1 และ proxy-friendly กว่า ง่ายต่อการวาง reverse proxy, cache และ load balance ใน production บน AWS Singapore หรือ datacenter ไทย
เลือก Model ให้เหมาะกับงาน
ก่อนเขียนโค้ด เลือก model ที่ตรงกับงานก่อน — ตาราง pricing เดือนมิถุนายน 2026:
| Model | Input / 1M tokens | Output / 1M tokens | เหมาะกับ |
|---|---|---|---|
| Claude Haiku 4.5 | $1.00 | $5.00 | Chatbot ปริมาณสูง, บอต FAQ |
| Claude Sonnet 4.6 | $3.00 | $15.00 | Reasoning ซับซ้อน, Sales Assistant |
| DeepSeek V4 Flash | $0.14 | $0.28 | Deployment ที่ต้องควบคุมต้นทุนในตลาด ASEAN |
สำหรับ chatbot ส่วนใหญ่ในแอปมือถือ — บอต support, ผู้ช่วย onboarding, คำถามที่พบบ่อย — Claude Haiku 4.5 คือตัวเลือกที่คุ้มค่าที่สุด: context window 200K token, ราคาต่อ conversation ต่ำกว่า Sonnet ถึง 60 เท่า
ส่วนที่ 1: FastAPI Backend
ติดตั้ง
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 Corp"
Endpoint แบบ Streaming
# 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="Unauthorized")
if not request.messages:
raise HTTPException(status_code=400, detail="messages ว่างเปล่า")
return StreamingResponse(
stream_response(request.messages),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
รันในเครื่อง:
uvicorn main:app --reload --port 8000
ส่วนที่ 2: React Native Chat UI (Expo SDK 54)
ปัญหาของ Streaming บน React Native
React Native ไม่มี EventSource แบบ native แต่ตั้งแต่ RN 0.79+ เป็นต้นมา response.body.getReader() รองรับการอ่าน chunked response แบบ incremental ได้ — โดยส่ง option reactNative: { textStreaming: true } เพิ่มเข้าไปใน fetch
// 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("Stream error:", err);
} finally {
setIsStreaming(false);
}
}, [messages]);
return { messages, sendMessage, isStreaming };
}
ส่วนที่ 3: ข้อควรระวังเฉพาะบนมือถือ
เน็ตหลุดระหว่าง Stream — เครือข่ายมือถือไม่เสถียร ใช้ try/catch รอบ reader loop และแสดงปุ่ม "แตะเพื่อลองใหม่" เก็บข้อความที่รับมาแล้วไว้ ไม่ให้ผู้ใช้ต้องเริ่มใหม่ทั้งหมด
FlatList re-render บ่อย — ทุก token ทำให้ component re-render ใช้ useCallback สำหรับ renderItem และเปิด removeClippedSubviews บน FlatList
การรักษาความปลอดภัย API Key — แม้จะใช้ header pattern แล้ว key ยังอยู่ใน bundle สำหรับแอปที่ต้องการความปลอดภัยสูง ให้ออกแบบ short-lived token: แอปยืนยันตัวตนกับ backend ผ่านระบบ auth ปกติ แล้ว backend ออก token ที่มีอายุ 15 นาทีสำหรับ chat endpoint
Deploy บน AWS Singapore เพื่อผู้ใช้ในไทย
สำหรับ latency ต่ำในไทย Deploy บน AWS ap-southeast-1 (Singapore) หรือ region ที่รองรับ VPC private endpoint เพื่อให้ traffic ระหว่าง backend กับ LLM provider ไม่ผ่านเน็ตสาธารณะ — ข้อกำหนดที่สำคัญสำหรับข้อมูลที่อยู่ภายใต้ PDPA และ พ.ร.บ. ความมั่นคงปลอดภัยไซเบอร์
เมื่อลูกค้าต้องการ private LLM บน on-premise เปลี่ยนแค่ base_url ใน FastAPI client — React Native app ไม่ต้องเปลี่ยนเลย
คำถามที่พบบ่อย
จำเป็นต้องมี FastAPI หรือสามารถเรียก LLM โดยตรงจาก React Native ได้เลย?
ในทางเทคนิคทำได้ แต่ไม่ควรทำใน production เพราะ API Key ที่ฝังอยู่ใน binary สามารถถูกดึงออกได้เมื่อ decompile และไม่มีการควบคุมฝั่ง server ใดๆ
React Native ไม่มี EventSource — Streaming ทำงานได้ยังไง?
ตั้งแต่ RN 0.79+ ใช้ response.body.getReader() พร้อม option reactNative: { textStreaming: true } ในการอ่าน chunked response แบบ incremental แล้ว parse SSE format เอง
ควรเลือก Model อะไรสำหรับ Support Bot ที่มีปริมาณข้อความสูง?
เริ่มด้วย Claude Haiku 4.5 — ราคา $1.00/M input tokens ออกแบบมาสำหรับ use case นี้โดยเฉพาะ ย้ายขึ้น Sonnet 4.6 เมื่อ conversation ต้องการ reasoning ซับซ้อนหรือ document synthesis
ขั้นตอนถัดไป
เราได้สร้างรากฐาน: FastAPI backend แบบ streaming, Expo chat UI และการจัดการกับปัญหาเฉพาะบนมือถือ
ขั้นตอนถัดไปในซีรีส์ R:
- On-Device AI — รัน quantized model บนอุปกรณ์โดยตรงโดยไม่ต้องมี backend
- เชื่อมต่อ Chatbot กับข้อมูลของคุณ — ผสาน FastAPI backend กับ RAG pipeline เพื่อให้ chatbot ตอบคำถามจากเอกสารภายใน
มีโปรเจกต์ React Native ที่ต้องการเพิ่ม AI layer? ติดต่อทีม Simplico
บทความล่าสุด
- พนักงานของคุณมี 24 รหัสผ่าน ธุรกิจของคุณมี 24 ช่องโหว่ June 11, 2026
- ความเสี่ยงด้านความปลอดภัยที่ซ่อนอยู่ในองค์กรวิศวกรรมของคุณ June 8, 2026
- SOAR กับ Alert Fatigue: ทำไม SOC ของคุณถึงจมอยู่กับ Alert (และ Automation ช่วยได้จริงอย่างไร) June 7, 2026
- MES vs ERP: ต่างกันอย่างไร และโรงงานของคุณต้องการอะไรกันแน่? June 7, 2026
- React Native vs Flutter ปี 2026: เลือกอะไรดีสำหรับโปรเจกต์ของคุณ June 4, 2026
- React Native ในปี 2026: ยังคุ้มค่าที่จะใช้สร้างแอปอยู่ไหม? June 3, 2026
