Part 1: วิธีสร้าง Chatbot ด้วย T3 Turbo & Gemini

Founders ส่วนใหญ่มักติดหล่ม 'Setup Hell' แต่ผมเพิ่งสร้าง AI Chatbot แบบ Type-safe เต็มระบบเสร็จในบ่ายเดียว นี่คือ Stack ที่ใช้—Next.js, tRPC และ Gemini—พร้อมโค้ดให้คุณนำไปสร้างเองได้ทันที

Part 1: วิธีสร้าง Chatbot ด้วย T3 Turbo & Gemini
Feng LiuFeng Liu
12 ธันวาคม 2568

ความซับซ้อนคือ "นักฆ่าเงียบ" ของ Startup ในช่วงเริ่มต้น คุณเริ่มต้นด้วยไอเดียง่ายๆ อย่าง "ผมอยากทำแชทบอทที่คุยสไตล์ Tony Stark"—แต่สามอาทิตย์ต่อมา คุณกลับยังนั่งงมกับการตั้งค่า Webpack, ตบตีกับ Docker containers หรือนั่งแก้บั๊กระบบล็อกอิน (Authentication) ที่ยังไม่มี User คนไหนได้ใช้เลยด้วยซ้ำ

มันเป็นกับดักที่ผมเห็นวิศวกรเก่งๆ ตกหลุมพรางมานับครั้งไม่ถ้วน เรารักเครื่องมือของเรา เรารักการปรับแต่งให้ดีที่สุด (Optimizing) แต่ในเกมของ Startup การปล่อยของ (Shipping) คือตัววัดผลเดียวที่สำคัญครับ

ถ้าคุณไม่ได้ติดตาม Ecosystem ของ TypeScript สมัยใหม่ในช่วงนี้ คุณอาจจะประหลาดใจ ยุคของการเอา API ต่างๆ มาปะติดปะต่อกันแล้วภาวนาให้มันไม่พังนั้นผ่านไปแล้วครับ เราได้ก้าวเข้าสู่ยุคของ "Vibe Coder"—ยุคที่ระยะห่างระหว่าง "ไอเดีย" กับ "โปรดักต์ที่ใช้งานได้จริง" วัดกันเป็นชั่วโมง ไม่ใช่เป็น Sprint

วันนี้ ผมจะพาคุณไปดู Tech Stack ที่ให้ความรู้สึกเหมือนใช้สูตรโกง: Create T3 Turbo ผสมผสานกับ Google Gemini AI มันมีความ Type-safe (ปลอดภัยเรื่องชนิดข้อมูล) ตั้งแต่ Database ยัน Frontend มันเร็วแบบเหลือเชื่อ และพูดตรงๆ คือ มันนำความสนุกกลับมาสู่การเขียนโค้ดอีกครั้ง

ทำไม Stack นี้ถึงสำคัญ

คุณอาจจะกำลังคิดว่า "Feng Liu ทำไมต้องมี Stack ใหม่อีกแล้ว? ผมใช้ Python กับ Streamlit ไม่ได้เหรอ?"

ได้ครับ ถ้าเป็นแค่ตัวต้นแบบ (Prototype) แต่ถ้าคุณกำลังสร้างโปรดักต์—สิ่งที่ต้องขยายสเกล (Scale) ได้, รองรับ User ได้ และมีการจัดการ State—คุณต้องการสถาปัตยกรรมจริงๆ ปัญหาคือคำว่า "สถาปัตยกรรมจริงๆ" มักจะหมายถึง "การเขียน Boilerplate เป็นอาทิตย์"

The T3 Stack (Next.js, tRPC, Tailwind) พลิกเกมนี้ครับ มันให้ความแข็งแกร่งระดับ Full-stack application แต่มีความเร็วในการพัฒนาเหมือนเขียน Script ง่ายๆ เมื่อคุณเติม Drizzle ORM (เบาหวิว, เขียนคล้าย SQL) และ Google Gemini (เร็วและให้ใช้ฟรีเยอะมาก) เข้าไป คุณก็จะได้กล่องเครื่องมือที่ทำให้ Founder ฉายเดี่ยวสามารถทำงานแซงหน้าทีมที่มีกันเป็นสิบคนได้

มาสร้างของจริงกันดีกว่าครับ

ขั้นตอนที่ 1: ติดตั้งด้วยคำสั่งเดียว (The One-Command Setup)

ลืมเรื่องการมานั่งตั้งค่า ESLint และ Prettier เองไปได้เลย เราจะใช้ create-t3-turbo ซึ่งจะสร้างโครงสร้างแบบ Monorepo มาให้เลย ซึ่งมันเพอร์เฟกต์มากเพราะมันแยก Logic ของ API ออกจาก Frontend ของ Next.js ทำให้รองรับอนาคตเผื่อคุณจะทำแอปมือถือด้วย React Native ในภายหลัง

pnpm create t3-turbo@latest my-chatbot
cd my-chatbot
pnpm install

ตอนที่ระบบถาม ผมเลือก Next.js, tRPC, และ PostgreSQL ผมข้ามเรื่อง Auth ไปก่อน เพราะย้ำอีกครั้งว่า เราเน้นที่การปล่อยของ (Shipping) ไม่ใช่ความสมบูรณ์แบบ คุณค่อยมาเติม NextAuth ทีหลังได้ใน 10 นาที

โครงสร้าง Monorepo ที่คุณจะได้:

my-chatbot/
├── apps/nextjs/          # เว็บแอปของคุณ
├── packages/
│   ├── api/              # tRPC routers (Logic ที่ใช้ร่วมกัน)
│   ├── db/               # Database schema + Drizzle
│   └── ui/               # Components ที่ใช้ร่วมกัน

การแยกส่วนแบบนี้หมายความว่า Logic ของ API คุณสามารถนำไปใช้ซ้ำได้ทั้งบนเว็บ, มือถือ หรือแม้แต่ CLI apps ผมเคยเห็นทีมเสียเวลาเป็นเดือนๆ ในการ Refactor โค้ดเพราะพวกเขาเริ่มต้นด้วยการยัดทุกอย่างไว้ในโฟลเดอร์เดียว

ขั้นตอนที่ 2: สมองของระบบ (Gemini)

OpenAI นั้นยอดเยี่ยมครับ แต่คุณเคยลอง Gemini Flash หรือยัง? มันเร็วอย่างเหลือเชื่อและราคาก็ดุเดือดมาก สำหรับ Chat Interface ที่ความหน่วง (Latency) คือตัวทำลายบรรยากาศ ความเร็วคือฟีเจอร์ที่สำคัญที่สุดครับ

ทำไมต้อง Gemini Flash แทนที่จะเป็น GPT-3.5/4?

  • ความเร็ว: ตอบกลับใน ~800ms เทียบกับ 2-3 วินาที
  • ต้นทุน: ถูกกว่า GPT-4 ถึง 60 เท่า
  • Context: รองรับ Context window ถึง 1 ล้าน token (ใช่ครับ หนึ่งล้าน)

เราต้องใช้ AI SDK เพื่อทำให้การคุยกับ LLM เป็นมาตรฐานเดียวกัน

cd packages/api
pnpm add ai @ai-sdk/google

ตั้งค่า .env ของคุณใน project root อย่าคิดมากเรื่อง Database ในเครื่องครับ ใช้ Local Postgres ธรรมดาก็พอ

POSTGRES_URL="postgresql://user:pass@localhost:5432/chatbot"
GOOGLE_GENERATIVE_AI_API_KEY="your_key_here"

Pro tip: ไปเอา Gemini API key ได้ที่ https://aistudio.google.com/app/apikey แพ็กเกจฟรีใจป้ำมาก—ให้ถึง 60 requests ต่อนาที คุณน่าจะเจอ Product-Market Fit ก่อนที่จะชน Rate limit แน่นอน

ขั้นตอนที่ 3: กำหนดความจริง (The Schema)

นี่คือจุดที่ Drizzle เฉิดฉายครับ ในสมัยก่อน คุณต้องเขียน Migration ด้วยมือ แต่ตอนนี้ คุณกำหนด Schema ใน TypeScript แล้ว Database จะเชื่อฟังตามนั้น

ใน packages/db/src/schema.ts เราจะนิยามว่า "Message" คืออะไร สังเกตไหมครับว่าเราใช้ drizzle-zod? สิ่งนี้จะสร้าง Validation schemas สำหรับ API ของเราโดยอัตโนมัติ นี่คือหลักการ "Don't Repeat Yourself" (อย่าทำซ้ำซ้อน) ในทางปฏิบัติครับ

import { pgTable } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod/v4";

// ตาราง Message สำหรับ chatbot
export const Message = pgTable("message", (t) => ({
  id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
  role: t.varchar({ length: 20 }).notNull(), // 'user' หรือ 'assistant'
  content: t.text().notNull(),
  createdAt: t.timestamp().defaultNow().notNull(),
}));

// Zod schema ที่สร้างอัตโนมัติจากนิยามตาราง
export const CreateMessageSchema = createInsertSchema(Message, {
  role: z.enum(["user", "assistant"]),
  content: z.string().min(1).max(10000),
}).omit({ id: true, createdAt: true });

สั่ง Push เลยครับ: pnpm db:push เสร็จเรียบร้อย Database ของคุณเกิดขึ้นจริงแล้ว

เกิดอะไรขึ้นเมื่อกี้? Drizzle ดูนิยาม TypeScript ของคุณแล้วสร้างตารางให้ ไม่ต้องเขียน SQL ไม่ต้องมีไฟล์ Migration นี่คือเวทมนตร์ของ Schema-driven development ครับ

ถ้าอยากตรวจสอบ ลองรัน: pnpm db:studio แล้วคุณจะเห็น Web UI ที่ https://local.drizzle.studio พร้อมตาราง message ของคุณ รอรับข้อมูลอยู่

ขั้นตอนที่ 4: ระบบประสาท (tRPC)

นี่คือส่วนที่มักจะทำให้ผู้คนอ้าปากค้างครับ ถ้าใช้ REST หรือ GraphQL คุณต้องกำหนด Endpoints, Types และ Fetchers แยกกัน แต่ด้วย tRPC ฟังก์ชัน Backend ของคุณ คือ ฟังก์ชัน Frontend ของคุณ

เราจะสร้าง Procedure ที่บันทึกข้อความของ User, ดึงประวัติการคุย (Context คือพระเจ้าในโลก AI), ส่งไปหา Gemini, และบันทึกคำตอบ

สร้างไฟล์ packages/api/src/router/chat.ts:

import type { TRPCRouterRecord } from "@trpc/server";
import { google } from "@ai-sdk/google";
import { generateText } from "ai";
import { z } from "zod/v4";

import { desc } from "@acme/db";
import { Message } from "@acme/db/schema";

import { publicProcedure } from "../trpc";

const SYSTEM_PROMPT = "You are a helpful AI assistant.";

export const chatRouter = {
  sendChat: publicProcedure
    .input(z.object({ content: z.string().min(1).max(10000) }))
    .mutation(async ({ ctx, input }) => {
      // 1. บันทึกข้อความ User
      await ctx.db
        .insert(Message)
        .values({ role: "user", content: input.content });

      // 2. ดึง Context (10 ข้อความล่าสุด)
      const history = await ctx.db
        .select()
        .from(Message)
        .orderBy(desc(Message.createdAt))
        .limit(10);

      // 3. ถาม Gemini
      const { text } = await generateText({
        model: google("gemini-1.5-flash"),
        system: SYSTEM_PROMPT,
        messages: history.reverse().map((m) => ({
          role: m.role as "user" | "assistant",
          content: m.content,
        })),
      });

      // 4. บันทึกคำตอบ AI
      return await ctx.db
        .insert(Message)
        .values({ role: "assistant", content: text })
        .returning();
    }),

  getMessages: publicProcedure.query(({ ctx }) =>
    ctx.db.select().from(Message).orderBy(Message.createdAt),
  ),

  clearMessages: publicProcedure.mutation(({ ctx }) => ctx.db.delete(Message)),
} satisfies TRPCRouterRecord;

ลงทะเบียน router ใน packages/api/src/root.ts:

import { chatRouter } from "./router/chat";
import { createTRPCRouter } from "./trpc";

export const appRouter = createTRPCRouter({
  chat: chatRouter,
});

export type AppRouter = typeof appRouter;

ดู Flow นั่นสิครับ มันเป็นเส้นตรง อ่านง่าย และ Fully typed ถ้าคุณเปลี่ยน Schema ของ Database โค้ดตรงนี้จะขึ้นตัวแดงทันที ไม่มีเรื่องเซอร์ไพรส์ตอนรันโปรแกรม (Runtime surprises)

ทำไมต้อง .reverse()? เราดึงข้อความแบบเรียงจากใหม่ไปเก่า (Descending) แต่ LLM ต้องการลำดับตามเวลาจริง (เก่าไปใหม่) นี่เป็นรายละเอียดเล็กๆ น้อยๆ ที่ช่วยป้องกันไม่ให้บทสนทนาสับสนครับ

Modular Architecture Visualization

ขั้นตอนที่ 5: หน้าตาการใช้งาน (The Interface)

ใน apps/nextjs/src/app/chat/page.tsx เราจะเชื่อมต่อทุกอย่างเข้าด้วยกัน เพราะเราใช้ tRPC เราจึงได้ React Query มาใช้ฟรีๆ useQuery จะจัดการเรื่องการดึงข้อมูล (Fetching), การเก็บ Cache, และสถานะ Loading ให้เราโดยไม่ต้องเขียน useEffect เพื่อดึงข้อมูลเองแม้แต่บรรทัดเดียว

(ผมใส่ useEffect ไว้แค่อันเดียวเพื่อเลื่อนหน้าจอลงมาล่างสุด—เพราะ UX เป็นเรื่องสำคัญครับ)

"use client";

import { useEffect, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { RouterOutputs } from "@acme/api";
import { useTRPC } from "~/trpc/react";

export default function ChatPage() {
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const endRef = useRef<HTMLDivElement>(null);

  const trpc = useTRPC();
  const queryClient = useQueryClient();

  // ดึงข้อมูลอัตโนมัติพร้อม Caching
  const { data: messages } = useQuery(trpc.chat.getMessages.queryOptions());

  // Mutation พร้อม Optimistic updates
  const sendMsg = useMutation(
    trpc.chat.sendChat.mutationOptions({
      onSuccess: async () => {
        await queryClient.invalidateQueries(trpc.chat.pathFilter());
        setInput("");
        setLoading(false);
      },
      onError: (err) => {
        console.error("Failed:", err);
        setLoading(false);
      },
    }),
  );

  // เลื่อนลงมาข้อความล่าสุดอัตโนมัติ
  useEffect(() => {
    endRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!input.trim() || loading) return;
    setLoading(true);
    sendMsg.mutate({ content: input.trim() });
  };

  return (
    <div className="flex h-screen flex-col bg-gray-50">
      {/* Header */}
      <div className="border-b bg-white p-4">
        <h1 className="text-xl font-bold">AI Chat</h1>
      </div>

      {/* Messages */}
      <div className="flex-1 overflow-y-auto p-4">
        <div className="mx-auto max-w-4xl space-y-4">
          {messages?.map((m: RouterOutputs["chat"]["getMessages"][number]) => (
            <div key={m.id} className={m.role === "user" ? "text-right" : ""}>
              <div
                className={`inline-block rounded-2xl px-4 py-3 ${
                  m.role === "user"
                    ? "bg-blue-500 text-white"
                    : "bg-white border shadow-sm"
                }`}
              >
                <p className="whitespace-pre-wrap">{m.content}</p>
              </div>
            </div>
          ))}
          {loading && (
            <div className="flex gap-2">
              <div className="h-2 w-2 animate-bounce rounded-full bg-gray-400" />
              <div className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:0.2s]" />
              <div className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:0.4s]" />
            </div>
          )}
          <div ref={endRef} />
        </div>
      </div>

      {/* Input */}
      <form onSubmit={handleSubmit} className="border-t bg-white p-4">
        <div className="mx-auto flex max-w-4xl gap-2">
          <input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="Type your message..."
            className="flex-1 rounded-lg border px-4 py-3 focus:ring-2 focus:ring-blue-500 focus:outline-none"
            disabled={loading}
          />
          <button
            type="submit"
            disabled={!input.trim() || loading}
            className="rounded-lg bg-blue-500 px-6 py-3 font-medium text-white hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
          >
            Send
          </button>
        </div>
      </form>
    </div>
  );
}

อย่าลืมหน้า Homepage ด้วยครับ อัปเดต apps/nextjs/src/app/page.tsx:

import Link from "next/link";

export default function HomePage() {
  return (
    <main className="flex min-h-screen items-center justify-center bg-gradient-to-b from-blue-500 to-blue-700">
      <div className="text-center text-white">
        <h1 className="text-5xl font-bold">AI Chatbot</h1>
        <p className="mt-4 text-xl">Built with T3 Turbo + Gemini</p>
        <Link
          href="/chat"
          className="mt-8 inline-block rounded-full bg-white px-10 py-3 font-semibold text-blue-600 hover:bg-gray-100 transition"
        >
          Start Chatting
        </Link>
      </div>
    </main>
  );
}

รัน pnpm dev แล้วเข้าไปที่ http://localhost:3000 คลิก "Start Chatting" แล้วคุณก็จะได้ AI chatbot ที่ใช้งานได้จริง

ความมหัศจรรย์ของ tRPC: สังเกตไหมครับว่าเราไม่เคยเขียน API fetch เลย? ไม่มี fetch(), ไม่มี URL strings, ไม่มีการจัดการ Error ด้วยมือ TypeScript รู้ ว่า sendMsg.mutate() คาดหวังข้อมูลอะไร ถ้าคุณเปลี่ยน Input schema ที่ Backend โค้ด Frontend ของคุณจะ Compile ไม่ผ่าน นี่แหละครับคืออนาคต

ขั้นตอนที่ 6: ใส่จิตวิญญาณ (เช็ค Vibe)

ผู้ช่วยแบบทั่วๆ ไปมันน่าเบื่อครับ และผู้ช่วยที่น่าเบื่อมักจะโดนลบ ความสวยงามของ LLM คือพวกมันสวมบทบาทได้เก่งมาก

ผมพบว่าการใส่ "ความเห็นที่ชัดเจน" (Strong opinion) ให้บอทของคุณ ทำให้มันน่าสนใจขึ้น 10 เท่า อย่าแค่ Prompt ว่า "You are helpful." แต่จง Prompt ให้มี บุคลิกภาพ

มาแก้ Backend ให้รับค่า Persona กันครับ อัปเดต packages/api/src/router/chat.ts:

const PROMPTS = {
  default: "You are a helpful AI assistant. Be concise and clear.",
  luffy:
    "You are Monkey D. Luffy from One Piece. You're energetic, optimistic, love meat and adventure. You often say 'I'm gonna be King of the Pirates!' Speak simply and enthusiastically.",
  stark:
    "You are Tony Stark (Iron Man). You're a genius inventor, witty, and sarcastic. You love technology and often mention Stark Industries. Call people 'kid' or 'buddy'. Be charming but arrogant.",
};

export const chatRouter = {
  sendChat: publicProcedure
    .input(
      z.object({
        content: z.string().min(1).max(10000),
        character: z.enum(["default", "luffy", "stark"]).optional(),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      // เลือกบุคลิก
      const systemPrompt = PROMPTS[input.character || "default"];

      await ctx.db
        .insert(Message)
        .values({ role: "user", content: input.content });

      const history = await ctx.db
        .select()
        .from(Message)
        .orderBy(desc(Message.createdAt))
        .limit(10);

      const { text } = await generateText({
        model: google("gemini-1.5-flash"),
        system: systemPrompt, // ← Prompt แบบ Dynamic
        messages: history.reverse().map((m) => ({
          role: m.role as "user" | "assistant",
          content: m.content,
        })),
      });

      return await ctx.db
        .insert(Message)
        .values({ role: "assistant", content: text })
        .returning();
    }),

  // ... ส่วนที่เหลือเหมือนเดิม
};

อัปเดต Frontend เพื่อส่งค่าตัวละครที่เลือก:

// ใน ChatPage component, เพิ่ม state สำหรับ character
const [character, setCharacter] = useState<"default" | "luffy" | "stark">("default");

// อัปเดตการเรียก mutation
sendMsg.mutate({ content: input.trim(), character });

// เพิ่ม dropdown ก่อนช่อง input:
<select
  value={character}
  onChange={(e) => setCharacter(e.target.value as any)}
  className="rounded-lg border px-3 py-2"
>
  <option value="default">🤖 Default</option>
  <option value="luffy">👒 Luffy</option>
  <option value="stark">🦾 Tony Stark</option>
</select>

ตอนนี้คุณไม่ได้แค่สร้าง Chatbot แล้วครับ แต่คุณสร้าง แพลตฟอร์มโต้ตอบกับตัวละคร นั่นแหละคือ "โปรดักต์"

รายละเอียดทางเทคนิคที่คุณน่าจะอยากรู้

ทำไมไม่ใช้ Prisma?

Prisma ก็ดีครับ แต่ Drizzle เร็วกว่า เรากำลังพูดถึงประสิทธิภาพ Query ที่ดีกว่า 2-3 เท่า เมื่อคุณเป็น Founder ฉายเดี่ยว ทุกมิลลิวินาทีมีผลทบต้นครับ แถม Syntax ที่คล้าย SQL ของ Drizzle ยังช่วยลดภาระสมอง (Mental overhead) ได้ด้วย

แล้วเรื่อง Streaming responses ล่ะ?

Vercel AI SDK รองรับ Streaming มาตั้งแต่แกะกล่องครับ แค่เปลี่ยน generateText เป็น streamText และใช้ hook useChat ที่ Frontend ผมข้ามเรื่องนี้ไปเพราะสำหรับบทเรียนสอน การทำแบบ Request/Response มันเข้าใจง่ายกว่า แต่ถ้าเป็น Production? ทำ Streaming เถอะครับ User จะรู้สึกว่ามัน "เร็วกว่า" แม้เวลาโดยรวมจะเท่ากันก็ตาม

การจัดการ Context window

ตอนนี้เราดึงมาแค่ 10 ข้อความล่าสุด มันเวิร์กจนกระทั่งมันไม่เวิร์กนั่นแหละครับ ถ้าคุณจะทำโปรดักต์จริงจัง ควรทำตัวนับ Token และปรับจำนวนประวัติแบบ Dynamic ซึ่ง AI SDK มีเครื่องมือสำหรับเรื่องนี้อยู่แล้ว

import { anthropic } from "@ai-sdk/anthropic";

const { text } = await generateText({
  model: anthropic("claude-3-5-sonnet-20241022"),
  maxTokens: 1000, // ควบคุมต้นทุน
  // ...
});

Database connection pooling

Local Postgres ใช้สำหรับ Dev ได้สบายครับ แต่สำหรับ Production ให้ใช้ Vercel Postgres หรือ Supabase พวกเขาจัดการ Connection pooling ให้โดยอัตโนมัติ การใช้ Serverless + Database connections เป็นกับดักครับ—อย่าจัดการเองเด็ดขาด

สิ่งที่ควรจำไปใช้

ถ้าคุณอ่านมาถึงตรงนี้แล้วรู้สึกคันไม้คันมืออยากเขียนโค้ด นี่คือคำแนะนำจากผม:

  1. อย่าเริ่มจากศูนย์ Boilerplate คือศัตรูของโมเมนตัม ใช้ T3 Turbo หรือโครงสร้างที่คล้ายกัน
  2. Type safety คือความเร็ว มันอาจจะรู้สึกช้าในชั่วโมงแรก แต่จะเร็วขึ้นในอีกสิบปีถัดไป มันช่วยดักจับบั๊กที่มักจะโผล่มาตอน Demo งาน
  3. Context คือกุญแจสำคัญ Chatbot ที่ไม่มีประวัติการคุยก็คือ Search bar หรูๆ นี่เอง ส่งข้อความล่าสุดไปให้ LLM เสมอครับ
  4. บุคลิกภาพ > ฟีเจอร์ บอทที่คุยเหมือน Tony Stark จะดึงดูดคนได้มากกว่าบอททั่วๆ ไปที่มีฟีเจอร์มากกว่า 10 อย่าง

ความจริงที่ยุ่งเหยิง

การสร้างสิ่งนี้ไม่ได้ราบรื่นไปซะหมดหรอกครับ ตอนแรกผมใส่ Database connection string ผิด แล้วก็นั่งงงอยู่ 20 นาทีว่าทำไม Drizzle ถึงโวยวายใส่ผม แถมผมยังชน Rate limit ของ Gemini เพราะตอนแรกส่ง History ไปเยอะเกิน (บทเรียน: เริ่มจาก .limit(5) ก่อนเสมอ แล้วค่อยขยาย)

ส่วน Loading animation น่ะเหรอ? ผมแก้ไปสามรอบกว่าจะออกมาดี เพราะ CSS animations ยังคงเป็นมนต์ดำในปี 2024 อย่างน่าประหลาด

แต่ประเด็นคือ: เพราะผมใช้ Stack ที่แข็งแกร่ง ปัญหาพวกนั้นเลยเป็นแค่ปัญหาเชิง Logic ไม่ใช่ปัญหาเชิง โครงสร้าง รากฐานยังคงมั่นคง ผมไม่ต้องมานั่ง Refactor API ทั้งหมดเพียงเพราะเลือก Abstraction ผิด

ปล่อยของซะ (Ship It)

เรากำลังอยู่ในยุคทองของการสร้างสรรค์ครับ เครื่องมือทรงพลัง AI ก็ฉลาด และกำแพงในการเริ่มต้นไม่เคยต่ำขนาดนี้มาก่อน

คุณมีโค้ดแล้ว คุณมี Stack แล้ว คุณเข้าใจข้อดีข้อเสียแล้ว

ออกไปสร้างสิ่งที่ "ไม่น่าจะมีอยู่จริง" แล้วปล่อยของ (Ship) ก่อนมื้อเย็นซะ

เวลาที่ใช้สร้างทั้งหมด: ~2 ชั่วโมง จำนวนบรรทัดที่เขียนจริง: ~200 บั๊กที่เจอใน Production: 0 (ณ ตอนนี้)

T3 stack + Gemini ไม่ใช่แค่เร็ว—แต่มัน น่าเบื่อ ในแบบที่ดีที่สุดครับ ไม่มีเซอร์ไพรส์ ไม่มีคำว่า "Works on my machine" มีแต่การสร้างของ

ขอให้สนุกกับการเขียนโค้ดครับ


แหล่งข้อมูล:

โค้ดฉบับเต็ม: github.com/giftedunicorn/my-chatbot

แชร์สิ่งนี้

Feng Liu

Feng Liu

shenjian8628@gmail.com

Part 1: วิธีสร้าง Chatbot ด้วย T3 Turbo & Gemini | Feng Liu