AWS - Giới thiệu
Mục tiêu
- Giúp bạn nắm trọn kiến trúc, luồng dữ liệu, các thành phần chính, cách cấu hình/chạy local, và cách mở rộng (thêm tool mới, đổi model).
- Đi kèm ví dụ code ngắn gọn, đường dẫn file quan trọng, và liên kết tài liệu gốc để tự tra cứu sâu hơn.
Tổng quan kiến trúc
- Frontend: Next.js App Router (RSC + Client Components), Tailwind + shadcn/ui, React 19 RC.
- AI layer: AI SDK (Vercel) cho chat streaming, tool-calls, object streaming; provider mặc định xAI (có reasoning).
- Auth: Auth.js (NextAuth) với
Credentials
(email/password) +guest
tự tạo tài khoản dùng thử. - DB: Drizzle ORM với Postgres (khuyến nghị Neon Serverless).
- Storage: Vercel Blob cho upload file (JPEG/PNG).
- Streaming: SSE + resumable stream (tùy chọn Redis) để khôi phục stream khi chuyển trang/SSR.
Luồng xử lý chính (từ nhập câu hỏi → stream về UI)
sequenceDiagram participant U as User participant UI as Chat UI (useChat) participant API as /api/chat (POST) participant LLM as AI SDK streamText (+tools) participant DB as Drizzle/Postgres participant SSE as SSE Stream U->>UI: Gõ tin nhắn, Enter UI->>API: POST {id, message, model, visibility} API->>DB: Lưu message user, tạo streamId API->>LLM: streamText(system, messages, tools) LLM-->>API: deltas (text/reasoning/tool calls) API->>SSE: UIMessageStream (Json→SSE) SSE-->>UI: stream phần trả lời API->>DB: onFinish → lưu message assistant (persist) opt Resume UI->>API: GET /api/chat/:id/stream API->>SSE: resumableStream(recentStreamId) SSE-->>UI: phần còn lại/khôi phục dòng cuối end
Thành phần và file quan trọng
-
Middleware & Auth
- Chặn route, tự tạo phiên
guest
khi chưa đăng nhập, ngăn vào/login
khi đã có token.
if (pathname.startsWith('/api/auth')) { return NextResponse.next(); } const token = await getToken({ req: request, secret: process.env.AUTH_SECRET, secureCookie: !isDevelopmentEnvironment }); if (!token) { return NextResponse.redirect(new URL(`/api/auth/guest?redirectUrl=${redirectUrl}`, request.url)); }
- Auth cấu hình
Credentials
+ providerguest
, nhúngid
vàtype
vào JWT/Session.
export const { handlers: { GET, POST }, auth, signIn, signOut } = NextAuth({ ...authConfig, providers: [ /* credentials & guest */ ], callbacks: { jwt, session } });
- Chặn route, tự tạo phiên
-
Chat API (POST /api/chat)
- Parse body (Zod), xác thực, chống spam theo ngày, tạo/lấy
Chat
, lưu message user, tạostreamId
, stream ra UI, lưu message assistant khi xong.
const stream = createUIMessageStream({ execute: ({ writer }) => { const result = streamText({ model: myProvider.languageModel(selectedChatModel), system: systemPrompt(...), messages: convertToModelMessages(uiMessages), stopWhen: stepCountIs(5), experimental_transform: smoothStream({ chunking: 'word' }), tools: { getWeather, createDocument: createDocument({ session, dataStream: writer }), updateDocument: updateDocument({ session, dataStream: writer }), requestSuggestions: requestSuggestions({ session, dataStream: writer }) } }); result.consumeStream(); writer.merge(result.toUIMessageStream({ sendReasoning: true })); }, onFinish: async ({ messages }) => saveMessages(...), onError: () => 'Oops...' }); return new Response(stream.pipeThrough(new JsonToSseTransformStream()));
- Schema body:
export const postRequestBodySchema = z.object({ id: z.string().uuid(), message: z.object({ id: z.string().uuid(), role: z.enum(['user']), parts: z.array(partSchema) }), selectedChatModel: z.enum(['chat-model','chat-model-reasoning']), selectedVisibilityType: z.enum(['public','private']) });
- Parse body (Zod), xác thực, chống spam theo ngày, tạo/lấy
-
Resumable stream (GET /api/chat/:id/stream)
- Dựa vào
resumable-stream
+ Redis (tùy chọn). Nếu thiếuREDIS_URL
, sẽ tắt khôi phục stream.
const stream = await streamContext.resumableStream(recentStreamId, () => emptyDataStream.pipeThrough(new JsonToSseTransformStream())); if (!stream) { /* khôi phục tin nhắn cuối nếu trong ~15s */ }
- Dựa vào
-
UI Chat
- Kết nối
useChat
vớiDefaultChatTransport
; thêmDataStreamProvider
gom các data parts (Artifact).
transport: new DefaultChatTransport({ api: '/api/chat', fetch: fetchWithErrorHandlers, prepareSendMessagesRequest({ messages, id, body }) { return { body: { id, message: messages.at(-1), selectedChatModel: initialChatModel, selectedVisibilityType: visibilityType, ...body } }; } }),
DataStreamHandler
đọc stream parts (id/title/kind/textDelta/codeDelta/...), cập nhật Artifact UI:
newDeltas.forEach((delta) => { /* gọi onStreamPart theo artifact kind, cập nhật title/id/kind/clear/finish */ });
- Kết nối
-
Artifacts (tạo/chỉnh sửa tài liệu bằng tool-calls)
- Định nghĩa handler cho
text | code | image | sheet
; mỗi handler stream nội dung về UI và lưu version vào DB.
export function createDocumentHandler({...}) { return { kind, onCreateDocument: async (...) => { const draft = await config.onCreateDocument(...); await saveDocument(...); }, onUpdateDocument: async (...) => { const draft = await config.onUpdateDocument(...); await saveDocument(...); } }; }
- Ví dụ handler Code:
const { fullStream } = streamObject({ model: myProvider.languageModel('artifact-model'), system: codePrompt, prompt: title, schema: z.object({ code: z.string() }) }); for await (const delta of fullStream) { if (delta.type === 'object') { const { code } = delta.object; if (code) { dataStream.write({ type: 'data-codeDelta', data: code, transient: true }); draftContent = code; } } }
- Định nghĩa handler cho
-
AI Provider/Models/Prompts
- Trỏ mặc định xAI, reasoning kèm
extractReasoningMiddleware('think')
.
languageModels: { 'chat-model': xai('grok-2-vision-1212'), 'chat-model-reasoning': wrapLanguageModel({ model: xai('grok-3-mini-beta'), middleware: extractReasoningMiddleware({ tagName: 'think' }) }), 'title-model': xai('grok-2-1212'), 'artifact-model': xai('grok-2-1212') }, imageModels: { 'small-model': xai.imageModel('grok-2-image') }
- Prompt hệ thống: nếu model reasoning sẽ ngắn gọn, nếu thường sẽ bổ sung hướng dẫn Artifact tools.
export const systemPrompt = ({ selectedChatModel, requestHints }) => selectedChatModel === 'chat-model-reasoning' ? `${regularPrompt}\n\n${requestPrompt}` : `${regularPrompt}\n\n${requestPrompt}\n\n${artifactsPrompt}`;
- Trỏ mặc định xAI, reasoning kèm
-
DB & Truy vấn (Drizzle)
- Bảng:
User, Chat, Message_v2, Vote_v2, Document, Suggestion, Stream
. - Các truy vấn CRUD, phân trang lịch sử, đếm message theo user/day (rate-limit), stream ids, vote, suggestions.
export async function getChatsByUserId({ id, limit, startingAfter, endingBefore }) { /* phân trang keyset theo createdAt */ }
export async function getMessageCountByUserId({ id, differenceInHours }) { /* đếm message role='user' trong 24h */ }
- Bảng:
-
Upload file
- Validate kích thước/loại file bằng Zod, lưu lên Vercel Blob:
const validatedFile = FileSchema.safeParse({ file }); const data = await put(`${filename}`, fileBuffer, { access: 'public' });
Cấu hình & chạy local
- Cài đặt:
- Node 18+, pnpm 9+
- Postgres URL (khuyến nghị Neon)
- Biến môi trường thiết yếu:
POSTGRES_URL
: chuỗi kết nối Postgres (ví dụ Neon)AUTH_SECRET
: chuỗi bí mật cho Auth.jsXAI_API_KEY
: khóa xAI nếu dùng provider mặc định- (Tùy chọn)
REDIS_URL
: bật resumable streams - (Tùy chọn, local Blob)
BLOB_READ_WRITE_TOKEN
: nếu cần thao tác Vercel Blob local
- Lệnh:
- Cài:
pnpm install
- Migrate DB:
pnpm db:migrate
hoặc khi buildpnpm build
- Dev:
pnpm dev
mởhttp://localhost:3000
- Test e2e:
pnpm test
- Cài:
- Tạo user thường: call
createUser
ởlib/db/queries.ts
(tự bọc route admin nếu cần), còn mặc định middleware sẽ sinhguest
khi chưa đăng nhập.
Tutorial thực hành nhanh (15 phút)
- Khởi chạy và gửi tin nhắn
- Set env như trên; chạy
pnpm dev
. - Vào trang chủ, bạn được auto đăng nhập
guest
. - Gõ câu hỏi, xem stream chạy mượt. Reasoning model sẽ hiển thị khối “Thinking” nếu chọn model này.
- Tạo tài liệu (Artifact Text)
- Hỏi: “Viết dàn ý ngắn về kiến trúc dự án”.
- Model sẽ gọi tool
createDocument
→ Artifact panel mở bên phải; nội dung text stream dần. - Bạn có thể chỉnh nội dung, hệ thống sẽ tự lưu version (POST
/api/document?id=...
).
- Sửa tài liệu (Update)
- Yêu cầu: “Rút gọn đoạn 2 còn 3 câu”.
- Tool
updateDocument
chạy, nội dung stream thay đổi; version mới xuất hiện.
- Upload ảnh
- Bấm kẹp giấy, chọn
*.png
/*.jpg
< 5MB. - Gửi kèm ảnh + văn bản; message parts gồm
file
+text
.
- Xem lịch sử/điều hướng
- Sidebar chứa lịch sử cuộc hội thoại của bạn (phân trang).
- Mở lại một chat → auto resume stream nếu còn phiên trong ~15s (và/hoặc có Redis).
Mở rộng
- Đổi model provider (ví dụ OpenAI)
// lib/ai/providers.ts (ví dụ minh họa) import { customProvider } from 'ai'; import { openai } from '@ai-sdk/openai'; // cần cài @ai-sdk/openai export const myProvider = customProvider({ languageModels: { 'chat-model': openai('gpt-4o-mini'), // hoặc model khác 'chat-model-reasoning': openai('o4-mini'), 'title-model': openai('gpt-4o-mini'), 'artifact-model': openai('gpt-4o-mini'), }, imageModels: { 'small-model': openai.imageModel('gpt-image-1'), }, });
- Thêm tool AI mới (ví dụ “getExchangeRate”)
// lib/ai/tools/get-exchange-rate.ts import { tool } from 'ai'; import { z } from 'zod'; export const getExchangeRate = tool({ description: 'Get FX rate base→quote (e.g. USD→VND)', inputSchema: z.object({ base: z.string().length(3), quote: z.string().length(3), }), execute: async ({ base, quote }) => { const r = await fetch( `https://api.exchangerate.host/latest?base=${base}&symbols=${quote}` ); const j = await r.json(); const rate = j?.rates?.[quote]; if (!rate) return { error: 'Rate not found' }; return { base, quote, rate }; }, });
- Đăng ký tool vào chat:
// app/(chat)/api/chat/route.ts (trích đoạn trong streamText) tools: { getWeather, createDocument: createDocument({ session, dataStream }), updateDocument: updateDocument({ session, dataStream }), requestSuggestions: requestSuggestions({ session, dataStream }), getExchangeRate, // thêm mới },
- Thêm loại Artifact mới
- Tạo handler server tại
artifacts/<kind>/server.ts
(giống 4 mẫu sẵn). - Thêm vào
documentHandlersByArtifactKind
vàartifactDefinitions
(client). - Tạo component client hiển thị nội dung artifact mới.
- Tạo handler server tại
API cheat‑sheet (cURL ví dụ)
- Gửi chat:
curl -N -X POST "http://localhost:3000/api/chat" \ -H "Content-Type: application/json" \ --cookie "authjs.session-token=..." \ -d '{"id":"<chat-uuid>","message":{"id":"<message-uuid>","role":"user","parts":[{"type":"text","text":"Hello"}]},"selectedChatModel":"chat-model","selectedVisibilityType":"private"}'
- Lịch sử chat đang đăng nhập:
curl "http://localhost:3000/api/history?limit=10" --cookie "authjs.session-token=..."
- Document:
curl "http://localhost:3000/api/document?id=<documentId>" --cookie "authjs.session-token=..."
Lỗi thường gặp
- Thiếu
POSTGRES_URL
: migrate/build thất bại. - Thiếu
AUTH_SECRET
: Auth.js không cấp token. - Thiếu
XAI_API_KEY
: gọi model xAI thất bại. - Thiếu
REDIS_URL
: vẫn chạy bình thường, chỉ tắt resumable stream (log cảnh báo). - Upload không thành công: file > 5MB hoặc sai MIME (chỉ JPEG/PNG).
Tài nguyên tham khảo (đáng tin cậy)
- Next.js App Router: https://nextjs.org/docs/app
- Vercel AI SDK (streamText, tools, object streaming): https://sdk.vercel.ai/docs
- xAI Provider: https://sdk.vercel.ai/providers/xai
- Auth.js (NextAuth): https://authjs.dev
- Drizzle ORM: https://orm.drizzle.team
- Neon Postgres: https://neon.tech/docs
- Vercel Blob: https://vercel.com/docs/storage/vercel-blob
- Resumable Stream: https://www.npmjs.com/package/resumable-stream
- shadcn/ui: https://ui.shadcn.com
Gợi ý tối ưu/clean code
- Hạn chế logic phức tạp trong component; đẩy điều kiện vào custom hooks.
- Các hàm nên ngắn gọn (dưới 20 dòng), rõ ràng; tách nhỏ tool logic nếu cần.
- Kiểm tra input bằng Zod ở boundary (API/Tool).
- Đặt tên rõ nghĩa; tránh tắt/bí danh.
— — —
- Nếu muốn, tôi có thể tạo thêm một bản “QuickStart.md” trong repo với các bước cài đặt + lệnh mẫu, hoặc một “ARCHITECTURE.md” tóm lược kiến trúc và luồng dữ liệu.
Tóm tắt ngắn
- Trình bày kiến trúc tổng thể (Next.js + AI SDK + Drizzle + Auth + Blob), sơ đồ luồng stream, các file/route quan trọng, Artifact system, và cấu hình chạy local.
- Cung cấp tutorial thao tác (gửi chat, tạo/sửa artifact, upload), cách mở rộng (thêm tool, đổi provider, thêm artifact).
- Đính kèm trích đoạn code thực tế trong repo và link tài liệu gốc để tự đào sâu.