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 + provider guest, nhúng idtype vào JWT/Session.
    export const { handlers: { GET, POST }, auth, signIn, signOut } = NextAuth({ ...authConfig, providers: [ /* credentials & guest */ ], callbacks: { jwt, session } });
    
  • 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ạo streamId, 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']) });
    
  • Resumable stream (GET /api/chat/:id/stream)

    • Dựa vào resumable-stream + Redis (tùy chọn). Nếu thiếu REDIS_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 */ }
    
  • UI Chat

    • Kết nối useChat với DefaultChatTransport; thêm DataStreamProvider 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 */ });
    
  • 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; } } }
    
  • 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}`;
    
  • 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 */ }
    
  • 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.js
    • XAI_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 build pnpm build
    • Dev: pnpm dev mở http://localhost:3000
    • Test e2e: pnpm test
  • Tạo user thường: call createUserlib/db/queries.ts (tự bọc route admin nếu cần), còn mặc định middleware sẽ sinh guest khi chưa đăng nhập.

Tutorial thực hành nhanh (15 phút)

  1. 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.
  1. 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=...).
  1. 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.
  1. 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.
  1. 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 documentHandlersByArtifactKindartifactDefinitions (client).
    • Tạo component client hiển thị nội dung artifact mớ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)

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.