Phần 2: Xây dựng AI Agent đầu tiên - Hướng dẫn thực chiến với LangChain
Hầu hết các hướng dẫn về AI agent thường bỏ qua những phần rắc rối nhất. Đây là cách tôi xây dựng một agent hoàn chỉnh với LangChain, tRPC và PostgreSQL - bao gồm cả những sai lầm thực tế mà tôi đã gặp phải trong quá trình phát triển.

Cơn sốt AI agent là có thật. Mọi người đều đang bàn tán về các hệ thống tự động có khả năng suy nghĩ, lên kế hoạch và thực thi nhiệm vụ. Nhưng có một điều mà chẳng ai nói cho bạn biết: hầu hết các bài hướng dẫn chỉ cho bạn thấy "kịch bản suôn sẻ" (happy path) và bỏ qua những đoạn mọi thứ đổ bể.
Tuần trước, tôi đã dành hai ngày để xây dựng một AI agent từ con số không. Không phải là một ví dụ kiểu "đồ chơi" đâu nhé - mà là một agent thực thụ có thể quản lý nền tảng blog, tạo user, viết bài và hoạt động trơn tru. Tôi sẽ chia sẻ chính xác cách mình đã làm, bao gồm cả những phần không chạy ngay trong lần thử đầu tiên.
Full code: github.com/giftedunicorn/my-ai-agent
Chúng Ta Thực Sự Đang Xây Dựng Cái Gì?
Quên mấy ví dụ trừu tượng đi. Chúng ta đang xây dựng một agent có khả năng:
- Tạo và quản lý user trong cơ sở dữ liệu PostgreSQL
- Tạo bài viết blog theo yêu cầu
- Phản hồi theo phong cách hội thoại trong khi sử dụng các công cụ (tools)
- Duy trì lịch sử trò chuyện
- Deploy thực tế (chứ không chỉ demo trên localhost)
Tech stack bao gồm: Next.js, tRPC, Drizzle ORM, LangChain, và Google's Gemini. Không phải vì chạy theo trend - mà vì nó type-safe (an toàn kiểu), nhanh và thực sự hoạt động tốt trong môi trường production.
Kiến Trúc (Đơn Giản Hơn Bạn Nghĩ)
Đây là điều làm tôi bất ngờ: AI agent không phức tạp đến thế. Về cốt lõi, chúng chỉ là:
- Một LLM có thể gọi hàm (function calling)
- Một tập hợp các công cụ (tools) mà LLM có thể sử dụng
- Một vòng lặp để thực thi các công cụ đó
- Bộ nhớ (memory) để duy trì ngữ cảnh
Chỉ vậy thôi. Sự phức tạp đến từ việc làm cho các mảnh ghép này hoạt động cùng nhau một cách tin cậy.
Schema Cơ Sở Dữ Liệu
Đầu tiên là nền móng. Chúng ta cần các bảng cho user, post và message:
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(),
}));
Không có gì cao siêu cả. Chỉ là dữ liệu quan hệ sạch sẽ với PostgreSQL. Bảng Message sẽ lưu trữ lịch sử hội thoại - yếu tố sống còn để duy trì ngữ cảnh giữa các request.
Xây Dựng Tools (Nơi Phép Màu Xảy Ra)
Đây là chỗ mà hầu hết các hướng dẫn đều nói chung chung. Họ bảo "Cứ tạo vài cái tool đi". Để tôi chỉ cho bạn thấy thực tế nó trông như thế nào.
Tools là các hàm mà AI của bạn có thể gọi. Với DynamicStructuredTool của LangChain, bạn cần định nghĩa:
- Tool làm gì (mô tả/description)
- Nó cần đầu vào gì (schema với Zod)
- Nó thực thi cái gì (hàm/function)
Đây là tool để tạo 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})`;
},
});
Phần mô tả (description) quan trọng hơn bạn nghĩ nhiều. LLM sử dụng nó để quyết định khi nào nên gọi tool này. Hãy mô tả thật cụ thể về thời điểm sử dụng nó.
Còn giá trị trả về? Đó là những gì LLM nhìn thấy. Tôi trả về văn bản có cấu trúc với tất cả các chi tiết liên quan - ID, tên, xác nhận. Điều này giúp LLM đưa ra phản hồi tốt hơn cho người dùng.
Agent: Ghép Nối Mọi Thứ
Đây là phần thú vị nhất. API mới của LangChain (v1.2+) đã đơn giản hóa mọi thứ:
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,
});
Chỉ thế thôi. Không cần ChatPromptTemplate, không AgentExecutor, không có các chain phức tạp. Chỉ cần createAgent và invoke.
System Prompt (Tính Cách Cho Agent Của Bạn)
Đây là nơi bạn dạy cho agent cách cư xử:
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.`;
Tôi đã học được bài học xương máu này: hãy rõ ràng. Nói cho agent biết chính xác phải làm gì, phản hồi như thế nào và bao gồm những chi tiết gì. Prompt mơ hồ sẽ dẫn đến hành vi mơ hồ.
Xử Lý Lịch Sử Hội Thoại
Hầu hết các ví dụ đều bỏ qua phần này, nhưng nó cực kỳ quan trọng cho trải nghiệm người dùng tốt. Đây là cách tôi xử lý:
// 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 },
];
Đơn giản nhưng hiệu quả. Agent giờ đây nhớ được 10 lần trao đổi gần nhất. Đủ cho ngữ cảnh, nhưng không quá nhiều khiến nó bị rối hoặc tốn kém chi phí token.
Những Phần "Khoai" (Thứ Đã Thực Sự Đổ Bể)
Phụ thuộc vòng (Circular Dependencies): Lần thử đầu tiên của tôi thất bại vì agent.ts import appRouter, mà appRouter lại import agentRouter, tạo ra một vòng lặp phụ thuộc. Giải pháp? Tạo một router tạm thời (inline) chỉ chứa các router cần thiết cho tools.
Trích xuất phản hồi từ Tool: Định dạng phản hồi của LangChain đã thay đổi trong v1.2. Kết quả bây giờ nằm trong result.messages[result.messages.length - 1].content, chứ không phải result.output. Tôi mất cả tiếng đồng hồ mới nhận ra điều này.
Type Safety: Tham số func của tool cần được định kiểu rõ ràng. Bạn không thể cứ thế mà destructure - bạn cần ép kiểu (cast) input trước. TypeScript sẽ không cứu được bạn ở đoạn này đâu.
Tự Thiết Lập Cho Riêng Bạn
Đây là những gì bạn thực sự cần:
- Cài đặt dependencies:
pnpm add @langchain/core @langchain/google-genai langchain drizzle-orm
- Biến môi trường (Environment variables):
POSTGRES_URL="your-database-url" # Thử dùng Vercel Postgres, Supabase, hoặc local PostgreSQL
GOOGLE_GENERATIVE_AI_API_KEY="your-gemini-key" # Lấy tại https://aistudio.google.com/app/apikey
- Thiết lập Database:
pnpm db:push # Tạo các bảng từ schema
- Bắt đầu xây dựng:
- Định nghĩa database schema
- Tạo các thủ tục tRPC cho các thao tác CRUD
- Xây dựng LangChain tools bao bọc các thủ tục đó
- Tạo agent với các tools của bạn
- Kết nối nó với frontend
Những Điều Tôi Sẽ Làm Khác Đi
Nếu được bắt đầu lại vào ngày mai:
Bắt đầu với ít tools hơn. Ban đầu tôi xây dựng tới 7 tools. Hãy chỉ bắt đầu với 3-4 cái cốt lõi thôi. Làm cho chúng hoạt động hoàn hảo, rồi hẵng mở rộng.
Test tools độc lập. Đừng đợi đến khi agent xây xong mới test tools. Hãy gọi chúng trực tiếp với dữ liệu test trước đã.
Theo dõi việc sử dụng tool. Tôi đã thêm logging để xem agent gọi tool nào và tại sao. Điều này giúp tôi nhận ra mô tả tool của mình cần phải sửa lại.
Sử dụng streaming. Hiện tại, người dùng phải đợi phản hồi hoàn chỉnh. Streaming sẽ tạo cảm giác nhanh hơn, ngay cả khi tổng thời gian xử lý là như nhau.
Kiểm Chứng Thực Tế
Xây dựng AI agent không phải là phép thuật, nhưng cũng không phải chuyện nhỏ. Bạn sẽ dành nhiều thời gian hơn cho:
- Thiết kế Tool (mỗi tool nên làm gì?)
- Prompt engineering (làm sao để agent cư xử đúng?)
- Xử lý lỗi (nếu database sập thì sao? nếu LLM bị "ảo giác" thì sao?)
- Type safety (làm cho TypeScript "vui vẻ" với các phản hồi động từ LLM)
Nhiều hơn là thời gian dành cho phần AI thực sự.
Tự Mình Trải Nghiệm
Code cho bài hướng dẫn này là hàng thật - tôi đã build nó trong khi viết bài này. Bạn có thể:
- Test với câu lệnh: "create 3 mock users"
- Thử: "create 2 blog posts for user 1"
- Hỏi: "how many users do we have?"
Agent sẽ xử lý tất cả những yêu cầu này bằng cách quyết định gọi tool nào, thực thi chúng và phản hồi lại như một cuộc trò chuyện.
Bước Tiếp Theo
Đây mới chỉ là nền tảng. Từ đây, bạn có thể:
- Thêm xác thực (ai được quyền tạo cái gì?)
- Triển khai phản hồi dạng streaming
- Thêm các tool phức tạp hơn (tìm kiếm, analytics, tích hợp bên thứ ba)
- Xây dựng vòng lặp phản hồi (việc gọi tool có thành công không?)
- Thêm giới hạn tốc độ (rate limiting - đừng để user tạo 10.000 bài viết)
Nhưng hãy bắt đầu đơn giản. Hãy làm cho một tool hoạt động thật tốt trước khi thêm mười cái dở dở ương ương.
Phần hay nhất? Một khi bạn hiểu được pattern này - tools + LLM + memory - bạn có thể xây dựng agent cho bất cứ thứ gì. Quản lý database, hỗ trợ khách hàng, tạo nội dung, gì cũng được.
Phần khó không nằm ở code. Nó nằm ở việc thiết kế các tools thực sự giải quyết được vấn đề thực tế.
Tài nguyên:
- Full source code: github.com/giftedunicorn/my-ai-agent
- Được xây dựng với Create T3 Turbo
- Tài liệu LangChain: js.langchain.com
- Lấy Gemini API key: aistudio.google.com
Chia sẻ

Feng Liu
shenjian8628@gmail.com