ตอนที่ 2: สร้าง AI Agent ตัวแรกด้วย LangChain: คู่มือฉบับลงมือทำจริง
Tutorial สร้าง AI Agent ส่วนใหญ่มักข้ามส่วนที่ยุ่งยากไป มาดูวิธีที่ผมสร้าง Agent ที่ใช้งานได้จริงด้วย LangChain, tRPC และ PostgreSQL พร้อมแชร์ข้อผิดพลาดที่ผมเจอระหว่างทางแบบไม่มีกั๊ก

กระแส Hype ของ AI Agent นี่ของจริงครับ ใครๆ ก็พูดถึงระบบอัตโนมัติที่คิดเองได้ วางแผนได้ และทำงานให้เราได้ แต่มีเรื่องนึงที่ไม่มีใครบอกคุณครับ: Tutorial ส่วนใหญ่มักจะสอนแต่เคสที่ราบรื่น (happy path) แต่ข้ามตอนที่พังยับเยินไปซะงั้น
อาทิตย์ที่แล้ว ผมใช้เวลาสองวันสร้าง AI Agent ขึ้นมาตัวนึงจากศูนย์ ไม่ใช่แค่โปรเจกต์ของเล่นนะครับ แต่เป็นตัวที่ใช้งานจริง จัดการระบบบล็อก สร้าง User เขียนบทความ และทำงานได้จริงๆ ผมจะพาไปดูว่าผมทำมันขึ้นมายังไง รวมถึงส่วนที่ "ไม่เวิร์ก" ในตอนแรกด้วย
Full code: github.com/giftedunicorn/my-ai-agent
สิ่งที่เรากำลังจะสร้างกันจริงๆ
ลืมตัวอย่างนามธรรมพวกนั้นไปก่อนครับ เรากำลังจะสร้าง Agent ที่:
- สร้างและจัดการ User ในฐานข้อมูล PostgreSQL
- สร้างบทความ Blog ได้ตามสั่ง
- ตอบโต้แบบบทสนทนาได้ในขณะที่เรียกใช้ Tools ต่างๆ
- จำประวัติการคุยได้ (Conversation history)
- Deploy ขึ้นงานจริงได้ (ไม่ใช่แค่รันบน localhost)
Tech Stack ที่ใช้: Next.js, tRPC, Drizzle ORM, LangChain, และ Google's Gemini ที่เลือกชุดนี้ไม่ใช่เพราะมันเท่ แต่เพราะมัน type-safe, เร็ว และใช้ใน production ได้จริงครับ
สถาปัตยกรรม (ง่ายกว่าที่คุณคิด)
สิ่งที่ทำให้ผมแปลกใจคือ: AI Agent ไม่ได้ซับซ้อนขนาดนั้นครับ โดยแก่นแล้วมันก็แค่:
- LLM ที่สามารถเรียกใช้ฟังก์ชันได้
- ชุดเครื่องมือ (Tools) ที่ LLM สามารถหยิบไปใช้
- Loop การทำงานเพื่อรันเครื่องมือพวกนั้น
- หน่วยความจำ (Memory) เพื่อรักษา Context
แค่นั้นเลยครับ ความซับซ้อนมันอยู่ที่การทำให้ชิ้นส่วนพวกนี้ทำงานร่วมกันได้อย่างเสถียรต่างหาก
Database Schema
เริ่มจากรากฐานก่อน เราต้องการตารางสำหรับ users, posts และ messages:
export const User = pgTable("user", (t) => ({
id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
name: t.varchar({ length: 255 }).notNull(),
email: t.varchar({ length: 255 }).notNull().unique(),
bio: t.text(),
createdAt: t.timestamp().defaultNow().notNull(),
updatedAt: t.timestamp().defaultNow().notNull(),
}));
export const Post = pgTable("post", (t) => ({
id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
userId: t
.integer()
.notNull()
.references(() => User.id, { onDelete: "cascade" }),
title: t.varchar({ length: 500 }).notNull(),
content: t.text().notNull(),
published: t.boolean().default(false).notNull(),
createdAt: t.timestamp().defaultNow().notNull(),
updatedAt: t.timestamp().defaultNow().notNull(),
}));
ไม่มีอะไรหวือหวาครับ แค่ข้อมูลเชิงสัมพันธ์ (Relational data) คลีนๆ ด้วย PostgreSQL ส่วนตาราง Message จะเก็บประวัติการสนทนา ซึ่งสำคัญมากสำหรับการรักษา Context ระหว่าง Request
การสร้าง Tools (จุดที่เวทมนตร์บังเกิด)
ตรงนี้แหละที่ Tutorial ส่วนใหญ่มักจะพูดกว้างๆ ว่า "ก็แค่สร้าง Tools สิ" เดี๋ยวผมจะให้ดูว่าหน้าตาจริงๆ มันเป็นยังไง
Tools คือฟังก์ชันที่ AI ของคุณสามารถเรียกใช้ได้ ด้วย DynamicStructuredTool ของ LangChain คุณต้องกำหนด:
- Tool นี้ทำอะไร (Description)
- มันต้องการ Input อะไรบ้าง (Schema ด้วย Zod)
- มันทำงานจริงๆ ยังไง (Function)
นี่คือ Tool สำหรับสร้าง User ครับ:
const createUserTool = new DynamicStructuredTool({
name: "create_user",
description:
"Create a new user in the database. Use this when asked to add, create, or register a user.",
schema: z.object({
name: z.string().describe("The user's full name"),
email: z.string().email().describe("The user's email address"),
bio: z.string().optional().describe("Optional biography"),
}),
func: async (input) => {
const { name, email, bio } = input as {
name: string;
email: string;
bio?: string;
};
const user = await caller.user.create({ name, email, bio });
return `Successfully created user: ${user.name} (ID: ${user.id}, Email: ${user.email})`;
},
});
Description สำคัญกว่าที่คุณคิดเยอะครับ LLM จะใช้ข้อความตรงนี้เพื่อตัดสินใจว่าจะเรียกใช้ Tool นี้เมื่อไหร่ จงระบุให้ชัดเจนครับ
ส่วนค่าที่ Return กลับไป? นั่นคือสิ่งที่ LLM จะเห็นครับ ผมเลือก Return เป็นข้อความที่มีโครงสร้างพร้อมรายละเอียดที่จำเป็น เช่น IDs, ชื่อ, และการยืนยัน สิ่งนี้ช่วยให้ LLM ตอบกลับ User ได้ดียิ่งขึ้น
The Agent: ประกอบร่าง
ตรงนี้เริ่มน่าสนุกแล้วครับ LangChain API ใหม่ (v1.2+) ทำให้ทุกอย่างง่ายขึ้นมาก:
const agent = createAgent({
model: new ChatGoogleGenerativeAI({
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
model: "gemini-2.0-flash-exp",
temperature: 0.7,
}),
tools: [...createUserTools(caller), ...createPostTools(caller)],
systemPrompt: AGENT_SYSTEM_PROMPT,
});
const result = await agent.invoke({
messages: conversationMessages,
});
แค่นั้นเลยครับ ไม่ต้องมี ChatPromptTemplate, ไม่ต้องมี AgentExecutor, ไม่ต้องมี Chain ซับซ้อน แค่ createAgent กับ invoke ก็จบ
System Prompt (บุคลิกของ Agent)
นี่คือที่ที่คุณสอน Agent ว่าต้องทำตัวยังไง:
const AGENT_SYSTEM_PROMPT = `You are an AI assistant that helps manage a blog platform.
You have access to tools for:
- User management (create, read, list, count)
- Post management (create, list)
When users ask you to perform actions:
1. Use the appropriate tools to complete the task
2. Be conversational and friendly
3. Provide clear confirmation with specific details
4. When creating mock data, use realistic names and content
Always confirm successful operations with relevant details.`;
ผมเรียนรู้เรื่องนี้จากความเจ็บปวดเลยครับ: ต้องชัดเจน บอก Agent ไปเลยว่าต้องทำอะไร ตอบยังไง และต้องรวมรายละเอียดอะไรบ้าง Prompt ที่คลุมเครือจะนำไปสู่พฤติกรรมที่คลุมเครือครับ
การจัดการ Conversation History
ตัวอย่างส่วนใหญ่ข้ามตรงนี้ไป แต่มันสำคัญมากสำหรับ UX ที่ดี นี่คือวิธีที่ผมจัดการครับ:
// Get last 10 messages from database
const history = await ctx.db
.select()
.from(Message)
.orderBy(desc(Message.createdAt))
.limit(10);
// Convert to LangChain format
const conversationMessages = [
...history.reverse().map((msg) => ({
role: msg.role === "user" ? "user" : "assistant",
content: msg.content,
})),
{ role: "user", content: input.message },
];
เรียบง่าย แต่ได้ผลครับ ตอนนี้ Agent จำบทสนทนา 10 ครั้งล่าสุดได้ เพียงพอสำหรับ Context แต่ไม่เยอะจนทำให้งงหรือเปลือง Token
ส่วนที่เละเทะ (สิ่งที่พังจริงๆ)
Circular Dependencies: ความพยายามครั้งแรกของผมล้มเหลวเพราะ agent.ts import appRouter ซึ่งดันไป import agentRouter อีกที กลายเป็นการเรียกวนลูป ทางแก้? สร้าง Router ชั่วคราวแบบ Inline ที่มีแค่ Router ที่จำเป็นสำหรับ Tools ก็พอ
Tool Response Extraction: Format การตอบกลับของ LangChain เปลี่ยนไปใน v1.2 ผลลัพธ์ตอนนี้ไปอยู่ที่ result.messages[result.messages.length - 1].content ไม่ใช่ result.output อีกต่อไป ผมงมอยู่เป็นชั่วโมงกว่าจะรู้เรื่องนี้
Type Safety: พารามิเตอร์ func ของ Tool ต้องการการระบุ Type ที่ชัดเจน คุณจะ Destructure เลยไม่ได้ ต้อง Cast input ก่อน TypeScript ช่วยคุณตรงนี้ไม่ได้หรอกครับ
เริ่มสร้างของตัวเอง
นี่คือสิ่งที่คุณต้องใช้จริงๆ ครับ:
- ติดตั้ง dependencies:
pnpm add @langchain/core @langchain/google-genai langchain drizzle-orm
- Environment variables:
POSTGRES_URL="your-database-url" # ลองใช้ Vercel Postgres, Supabase, หรือ local PostgreSQL
GOOGLE_GENERATIVE_AI_API_KEY="your-gemini-key" # ไปเอาได้ที่ https://aistudio.google.com/app/apikey
- Database setup:
pnpm db:push # สร้าง tables จาก schema
-
เริ่มลงมือสร้าง:
- กำหนด Database schema
- สร้าง tRPC procedures สำหรับ CRUD operations
- สร้าง LangChain tools ที่ไปครอบ procedures พวกนั้นอีกที
- สร้าง Agent พร้อมกับ Tools ของคุณ
- เชื่อมต่อกับ Frontend
สิ่งที่ผมจะทำต่างไปจากเดิม
ถ้าให้ผมเริ่มใหม่พรุ่งนี้:
เริ่มจาก Tool น้อยๆ ก่อน: ตอนแรกผมสร้างทีเดียว 7 tools เลย ให้เริ่มจาก 3-4 ตัวหลักๆ ก่อน ทำให้มันทำงานได้เป๊ะๆ แล้วค่อยขยาย
เทส Tool แยกต่างหาก: อย่ารอจนสร้าง Agent เสร็จค่อยเทส Tools ลองเรียกใช้มันตรงๆ ด้วยข้อมูลทดสอบดูก่อน
มอนิเตอร์การใช้งาน Tool: ผมเพิ่ม Logging เพื่อดูว่า Agent เรียกใช้ Tool ตัวไหนและทำไม สิ่งนี้ช่วยให้เห็นว่า Description ของ Tool ผมยังต้องปรับปรุง
ใช้ Streaming: ตอนนี้ User ต้องรอจนกว่าจะตอบเสร็จ Streaming จะทำให้รู้สึกว่ามันเร็วขึ้น แม้จะใช้เวลาประมวลผลเท่าเดิม
ความเป็นจริง (Reality Check)
การสร้าง AI Agent ไม่ใช่เวทมนตร์ แต่ก็ไม่ใช่เรื่องหมูๆ เหมือนกัน คุณจะใช้เวลาส่วนใหญ่ไปกับ:
- การออกแบบ Tool (แต่ละ Tool ควรทำอะไร?)
- Prompt Engineering (ทำยังไงให้ Agent ทำตัวถูกต้อง?)
- Error Handling (ถ้า Database ล่มล่ะ? ถ้า LLM หลอนล่ะ?)
- Type Safety (ทำให้ TypeScript แฮปปี้กับคำตอบแบบ Dynamic ของ LLM)
มากกว่าตัว AI จริงๆ ซะอีก
ลองเล่นดูครับ
โค้ดในบทความนี้ของจริงครับ ผมสร้างมันไปพร้อมกับตอนเขียนบทความนี้เลย คุณสามารถ:
- เทสด้วยคำสั่ง: "create 3 mock users"
- ลองสั่ง: "create 2 blog posts for user 1"
- ถามว่า: "how many users do we have?"
Agent จะจัดการคำสั่งพวกนี้ทั้งหมดโดยตัดสินใจว่าจะเรียก Tool ตัวไหน รันคำสั่ง และตอบกลับแบบเป็นธรรมชาติ
ก้าวต่อไป
นี่เป็นแค่รากฐานครับ จากตรงนี้คุณสามารถ:
- เพิ่ม Authentication (ใครสร้างอะไรได้บ้าง?)
- ทำ Streaming responses
- เพิ่ม Tools ที่ซับซ้อนขึ้น (ค้นหา, Analytics, เชื่อมต่อภายนอก)
- สร้าง Feedback loop (Tool ทำงานสำเร็จไหม?)
- เพิ่ม Rate limiting (อย่าให้ User สร้าง 10,000 โพสต์เล่นๆ)
แต่เริ่มจากง่ายๆ ก่อนครับ ทำให้ Tool เดียวทำงานได้ดีก่อนจะเพิ่มอีกสิบตัวที่ทำงานได้ครึ่งๆ กลางๆ
ส่วนที่ดีที่สุด? พอคุณเข้าใจ Pattern นี้ — Tools + LLM + Memory — คุณจะสร้าง Agent สำหรับอะไรก็ได้ ไม่ว่าจะจัดการ Database, Customer Support, สร้าง Content หรืออะไรก็ตาม
ส่วนที่ยากไม่ใช่โค้ดครับ แต่มันคือการออกแบบ Tool ที่แก้ปัญหาได้จริงๆ ต่างหาก
Resources:
- Full source code: github.com/giftedunicorn/my-ai-agent
- สร้างด้วย Create T3 Turbo
- LangChain Docs: js.langchain.com
- รับ Gemini API key: aistudio.google.com
แชร์สิ่งนี้

Feng Liu
shenjian8628@gmail.com