Dự án học tập - Xây dựng AI Agent Platform đầy đủ tính năng trên Cloudflare Workers với Next.js
AI Hub là một nền tảng quản lý và triển khai AI Agent, cho phép tạo, cấu hình và sử dụng các chatbot AI với khả năng RAG (Retrieval-Augmented Generation) và Web Search. Dự án này sử dụng hoàn toàn các dịch vụ của Cloudflare để có chi phí thấp và hiệu suất cao.
- Kiến Trúc Tổng Quan
- Công Nghệ Sử Dụng
- Cloudflare Services Chi Tiết
- Cấu Trúc Thư Mục
- Database Schema
- API Endpoints
- Cài Đặt và Chạy
- Hướng Dẫn Scale Tính Năng Mới
- Best Practices
flowchart TB
subgraph Client
Browser["Client (Browser)"]
end
subgraph CF["Cloudflare Workers"]
subgraph NextJS["Next.js"]
ReactUI["React UI<br/>(App Router)"]
APIRoutes["API Routes<br/>(/api/*)"]
Hono["Hono Backend<br/>(REST API)"]
end
end
subgraph AISDK["AI SDK"]
SDK["Unified Interface<br/>(streamText, generateText)"]
Tools["Tool Calling<br/>(semanticSearch, webSearch)"]
end
subgraph Providers["LLM Providers"]
OpenAI["OpenAI<br/>(@ai-sdk/openai)"]
Google["Google AI<br/>(@ai-sdk/google)"]
Others["Other Providers<br/>(Anthropic, etc.)"]
end
subgraph Services["Cloudflare Services"]
D1[("D1 Database<br/>(SQLite)")]
Vectorize[("Vectorize<br/>(Vector DB)")]
WorkersAI["Workers AI<br/>(Embeddings)"]
KV[("KV Storage<br/>(Cache)")]
end
Browser --> CF
Hono --> SDK
SDK --> Tools
SDK --> OpenAI
SDK --> Google
SDK --> Others
Tools --> Vectorize
Tools --> WorkersAI
NextJS --> D1
NextJS --> KV
sequenceDiagram
participant User
participant API as Chat API
participant LLM as LLM Provider
participant RAG as semanticSearchTool
participant Web as webSearchTool
User->>API: Gửi message
API->>API: Load Agent Config (KV/D1)
API->>LLM: Stream request với tools
loop Tool Calling Loop (max 5 steps)
LLM->>LLM: Quyết định sử dụng tool
alt semanticSearchTool
LLM->>RAG: Query semantic search
RAG->>RAG: Generate embedding (Workers AI)
RAG->>RAG: Search Vectorize
RAG-->>LLM: Kết quả tìm kiếm
else webSearchTool
LLM->>Web: Query web search
Web->>Web: Ollama Web Search API
Web-->>LLM: Kết quả từ internet
end
end
LLM-->>API: Final response
API-->>User: Stream response
- User gửi message → Chat API nhận request
- Load Agent Config → Lấy từ KV cache hoặc D1 database
- Agentic Tool Calling: LLM tự động quyết định sử dụng tools khi cần
semanticSearchTool: Tìm kiếm trong knowledge base (Vectorize)webSearchTool: Tìm kiếm thông tin mới nhất từ internet
- LLM Response → Tổng hợp kết quả từ tools và trả lời
- Stream Response → Trả về client real-time
Note
Flow sử dụng AI SDK với toolChoice: "auto" và stepCountIs(5) để giới hạn số lần gọi tool tối đa là 5.
| Công nghệ | Mục đích |
|---|---|
| Next.js 15 | React framework với App Router |
| React 19 | UI library |
| TailwindCSS 4 | Styling |
| Radix UI | Headless UI components |
| React Query | Server state management |
| nuqs | URL state management |
| AI SDK (Vercel) | Chat UI và AI integration |
| Công nghệ | Mục đích |
|---|---|
| Hono | Lightweight web framework cho API |
| Drizzle ORM | Type-safe database ORM |
| Zod | Schema validation |
| Better Auth | Authentication library |
| Service | Mục đích |
|---|---|
| Workers | Serverless compute runtime |
| D1 | SQLite database |
| Vectorize | Vector database cho RAG |
| Workers AI | Embedding generation |
| KV | Key-value cache storage |
| Rate Limiting | API rate limiting |
| Images | Image optimization |
D1 là database SQL serverless của Cloudflare, sử dụng SQLite engine. Phù hợp cho ứng dụng với read-heavy workload.
Cấu hình trong wrangler.jsonc:
Sử dụng trong code (src/lib/db.ts):
import { drizzle } from 'drizzle-orm/d1';
import { getCloudflareContext } from '@opennextjs/cloudflare';
export async function getDb() {
const { env } = await getCloudflareContext({ async: true });
return drizzle(env.AIHUB_DB, { schema });
}Commands quản lý:
# Generate migration từ schema changes
npm run db:generate
# Apply migrations lên remote D1
npm run db:push
# Mở Drizzle Studio để xem data
npm run db:studioVectorize lưu trữ vector embeddings để thực hiện semantic search (RAG).
Cấu hình trong wrangler.jsonc:
"vectorize": [{
"binding": "VECTORIZE",
"index_name": "aihub-vectorize"
}]Tạo index (chạy 1 lần):
wrangler vectorize create aihub-vectorize \
--dimensions=768 \
--metric=cosineSử dụng trong code (src/lib/vectorize.ts):
// Upsert vector với metadata
export async function upsertVector(
datasourceId: number,
datasourceGroupId: number,
content: string
) {
const { vectorize } = await getBindings();
const embedding = await generateEmbedding(content); // Workers AI
await vectorize.upsert([{
id: `datasource:${datasourceId}`,
values: embedding,
metadata: { datasourceId, datasourceGroupId, content }
}]);
}
// Search với metadata filter
export async function searchVectors(
query: string,
datasourceGroupIds: number[],
topK: number = 5
) {
const queryEmbedding = await generateEmbedding(query);
return vectorize.query(queryEmbedding, {
topK,
returnMetadata: 'all',
filter: {
datasourceGroupId: { $in: datasourceGroupIds } // Filter by groups
}
});
}Lưu ý quan trọng:
- Dimensions phải khớp với model embedding (768 cho
bge-base-en-v1.5) - Hỗ trợ operators:
$eq,$ne,$in,$nin,$lt,$lte,$gt,$gte
Metadata và Multi-Agent Isolation:
Trong project này, mỗi vector được lưu với metadata datasourceGroupId:
flowchart LR
subgraph Agent1["Agent A"]
G1["Datasource Group 1"]
G2["Datasource Group 2"]
end
subgraph Agent2["Agent B"]
G3["Datasource Group 3"]
end
subgraph Vectorize["Vectorize Index"]
V1["Vector 1<br/>metadata: groupId=1"]
V2["Vector 2<br/>metadata: groupId=2"]
V3["Vector 3<br/>metadata: groupId=3"]
end
G1 --> V1
G2 --> V2
G3 --> V3
- Khi Agent A search, chỉ tìm trong
groupId: [1, 2] - Khi Agent B search, chỉ tìm trong
groupId: [3] - Đảm bảo mỗi Agent chỉ truy cập knowledge base được assign
Workers AI cung cấp inference cho các AI models, trong project này dùng để generate embeddings.
Cấu hình trong wrangler.jsonc:
"ai": {
"binding": "AI"
}Sử dụng:
export async function generateEmbedding(text: string): Promise<number[]> {
const { env } = await getCloudflareContext({ async: true });
const response = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
text: [text]
});
return response.data[0]; // 768-dimensional vector
}Models có sẵn:
@cf/baai/bge-base-en-v1.5- Text embeddings (768 dims)@cf/meta/llama-3.1-8b-instruct- LLM inference@cf/stabilityai/stable-diffusion-xl- Image generation
KV (Key-Value) storage dùng để cache data, giảm load cho database.
Cấu hình trong wrangler.jsonc:
"kv_namespaces": [{
"binding": "AIHUB_KV",
"id": "xxx-xxx-xxx"
}]Tạo KV namespace:
wrangler kv namespace create AIHUB_KV
# Copy ID vào wrangler.jsoncSử dụng (src/lib/kv.ts):
// Cache với TTL
export async function putKV(
key: string,
value: string,
options?: { expirationTtl?: number }
) {
const { env } = await getCloudflareContext({ async: true });
await env.AIHUB_KV.put(key, value, options);
}
// Get cached value
export async function getKV<T>(key: string, type: "json" | "text" = "text") {
const { env } = await getCloudflareContext({ async: true });
return env.AIHUB_KV.get(key, type);
}Pattern Cache trong project:
// Agent caching với 15 phút TTL
const CACHE_PREFIX = 'agent:';
const CACHE_TTL_SECONDS = 15 * 60;
export async function getAgentById(id: number) {
const cacheKey = `${CACHE_PREFIX}${id}`;
// Try cache first
const cached = await getKV(cacheKey, 'json');
if (cached) return cached;
// Fetch from DB
const agent = await fetchAgentFromDb(id);
// Cache result
await putKV(cacheKey, JSON.stringify(agent), {
expirationTtl: CACHE_TTL_SECONDS
});
return agent;
}Rate Limiting bảo vệ API khỏi abuse và DDoS.
Cấu hình trong wrangler.jsonc:
"ratelimits": [{
"name": "AIHUB_RATE_LIMITER",
"namespace_id": "1001",
"simple": {
"limit": 50, // 50 requests
"period": 60 // per 60 seconds
}
}]Sử dụng trong router:
.post("/completions/:agentId", async (c) => {
const { env } = await getCloudflareContext({ async: true });
// Get client IP
const ipAddress = c.req.header("cf-connecting-ip") || "";
// Check rate limit
const { success } = await env.AIHUB_RATE_LIMITER.limit({
key: ipAddress
});
if (!success) {
return c.json({ error: 'Rate limit exceeded' }, 429);
}
// Continue processing...
})OpenNext cho phép deploy Next.js application lên Cloudflare Workers.
Cấu hình open-next.config.ts:
import type { OpenNextConfig } from '@opennextjs/cloudflare';
export default {
// Config options
} satisfies OpenNextConfig;Self-reference binding cho caching:
"services": [{
"binding": "WORKER_SELF_REFERENCE",
"service": "aihub" // Phải match worker name
}]aihub/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── (auth)/ # Auth pages (login, signup)
│ │ ├── (dashboard)/ # Protected dashboard pages
│ │ │ ├── (admin)/ # Admin CRUD pages
│ │ │ └── playground/ # Chat playground
│ │ ├── api/ # API routes
│ │ ├── layout.tsx # Root layout
│ │ └── globals.css # Global styles
│ │
│ ├── backend/ # Hono API server
│ │ ├── index.ts # Router aggregation
│ │ └── middleware/ # Auth middleware
│ │
│ ├── features/ # Feature-based architecture
│ │ ├── agents/ # Agent management
│ │ │ ├── components/ # UI components
│ │ │ ├── hooks/ # React hooks
│ │ │ ├── server/ # API routes & services
│ │ │ ├── params.ts # URL params schema
│ │ │ └── query-options.ts # React Query options
│ │ ├── llms/ # LLM configuration
│ │ ├── datasources/ # Knowledge base content
│ │ ├── datasource-groups/ # Knowledge base groups
│ │ ├── chat/ # Chat functionality
│ │ │ └── server/
│ │ │ ├── routers.ts # Chat endpoints
│ │ │ ├── service.ts # AI response logic
│ │ │ └── tools.ts # AI SDK tools
│ │ ├── playground/ # Playground UI
│ │ └── auth/ # Auth components
│ │
│ ├── components/ # Shared UI components
│ │ └── ui/ # shadcn/ui components
│ │
│ ├── config/ # App configuration
│ │ └── constants.ts # AI providers & models
│ │
│ ├── hooks/ # Shared hooks
│ │
│ └── lib/ # Core utilities
│ ├── db.ts # Drizzle D1 setup
│ ├── kv.ts # KV helpers
│ ├── vectorize.ts # Vector operations
│ ├── schema.ts # Database schema
│ ├── logger.ts # Logging utility
│ ├── auth/ # Better Auth setup
│ └── api/ # API client (ky)
│
├── drizzle/ # Database migrations
│ └── migrations/
│
├── public/ # Static assets
├── wrangler.jsonc # Cloudflare config
├── drizzle.config.ts # Drizzle config
├── open-next.config.ts # OpenNext config
└── package.json
erDiagram
user {
int id PK
string name
string email
string image
timestamp createdAt
}
session {
int id PK
int userId FK
string token
timestamp expiresAt
string ipAddress
string userAgent
}
account {
int id PK
int userId FK
string providerId
string accessToken
string refreshToken
}
llms {
int id PK
string name
string provider
string model
string baseUrl
string apiKey
}
agents {
int id PK
int llmId FK
string name
string description
string systemPrompt
int temperature
int topK
int maxTokens
}
datasource_groups {
int id PK
string name
string description
}
agent_datasource_groups {
int agentId FK
int datasourceGroupId FK
}
datasources {
int id PK
int datasourceGroupId FK
string content
}
user ||--o{ session : "has"
user ||--o{ account : "has"
llms ||--o{ agents : "powers"
agents ||--o{ agent_datasource_groups : "uses"
datasource_groups ||--o{ agent_datasource_groups : "belongs to"
datasource_groups ||--o{ datasources : "contains"
| Bảng | Mô tả |
|---|---|
user |
Người dùng hệ thống |
session |
Phiên đăng nhập |
account |
OAuth accounts (Google, etc.) |
verification |
Email verification tokens |
llms |
Cấu hình LLM providers (OpenAI, Google, etc.) |
agents |
AI Agents với system prompt và settings |
datasource_groups |
Nhóm knowledge base |
datasources |
Nội dung knowledge base (embedded vào Vectorize) |
agent_datasource_groups |
Many-to-many: Agent ↔ Datasource Groups |
ollama_keys |
API keys cho Ollama web search |
POST /api/auth/sign-up # Đăng ký (⚠️ chỉ có trên develop env)
POST /api/auth/sign-in # Đăng nhập
POST /api/auth/sign-out # Đăng xuất
GET /api/auth/session # Lấy session hiện tại
Note
Route /signup chỉ tồn tại trên môi trường develop, dùng để tạo tài khoản admin.
GET /api/llms # List LLMs với pagination
POST /api/llms # Tạo LLM mới
GET /api/llms/:id # Chi tiết LLM
PATCH /api/llms/:id # Cập nhật LLM
DELETE /api/llms/:id # Xóa LLM
GET /api/agents # List agents với search
POST /api/agents # Tạo agent mới
GET /api/agents/:id # Chi tiết agent
PATCH /api/agents/:id # Cập nhật agent
DELETE /api/agents/:id # Xóa agent
GET /api/datasource-groups # List groups
POST /api/datasource-groups # Tạo group mới
GET /api/datasource-groups/:id # Chi tiết group
PATCH /api/datasource-groups/:id # Cập nhật group
DELETE /api/datasource-groups/:id # Xóa group
GET /api/datasources/group/:groupId # List datasources theo group
POST /api/datasources # Tạo datasource (auto-embed)
GET /api/datasources/:id # Chi tiết datasource
PATCH /api/datasources/:id # Cập nhật datasource
DELETE /api/datasources/:id # Xóa datasource
# Protected - dùng trong Playground
POST /api/chat/playground/:agentId
# Public với Rate Limiting - dùng cho integration
POST /api/chat/completions/:agentId
Request body (UIMessage format):
{
"stream": true,
"messages": [
{
"id": "msg-1",
"role": "user",
"parts": [
{
"type": "text",
"text": "Xin chào!"
}
]
}
]
}- Node.js 18+
- npm hoặc pnpm
- Cloudflare account
- Wrangler CLI (
npm install -g wrangler)
git clone <repository-url>
cd aihub
npm installwrangler login# Tạo D1 Database
wrangler d1 create aihub-db
# Tạo KV Namespace
wrangler kv namespace create AIHUB_KV
# Tạo Vectorize Index
wrangler vectorize create aihub-vectorize \
--dimensions=768 \
--metric=cosineCopy các ID từ output của bước 3 vào wrangler.jsonc:
{
"d1_databases": [{
"database_id": "<your-d1-id>"
}],
"kv_namespaces": [{
"id": "<your-kv-id>"
}]
}# .dev.vars (local development)
NEXTJS_ENV=development# Generate migrations từ schema
npm run db:generate
# Apply migrations
npm run db:push# Chạy Next.js dev server
npm run devnpm run previewnpm run deployGiả sử bạn muốn thêm feature "Conversations" để lưu lịch sử chat:
src/features/conversations/
├── components/
│ ├── conversation-list.tsx
│ └── conversation-dialog.tsx
├── hooks/
│ └── use-conversations.ts
├── server/
│ ├── routers.ts
│ └── service.ts
├── params.ts
├── query-options.ts
└── index.ts
export const conversations = sqliteTable("conversations", {
id: int("id").primaryKey({ autoIncrement: true }),
agentId: int("agent_id").references(() => agents.id),
userId: text("user_id").references(() => user.id),
title: text("title"),
createdAt: integer("created_at", { mode: "timestamp" })
.$defaultFn(() => new Date()),
});
export const messages = sqliteTable("messages", {
id: int("id").primaryKey({ autoIncrement: true }),
conversationId: int("conversation_id").references(() => conversations.id),
role: text("role").notNull(), // "user" | "assistant"
content: text("content").notNull(),
createdAt: integer("created_at", { mode: "timestamp" })
.$defaultFn(() => new Date()),
});npm run db:migrateimport { getDb } from '@/lib/db';
import { conversations, messages } from '@/lib/schema';
export async function createConversation(agentId: number, userId: string) {
const db = await getDb();
const result = await db.insert(conversations)
.values({ agentId, userId, title: "New Conversation" })
.returning();
return result[0];
}
export async function addMessage(
conversationId: number,
role: string,
content: string
) {
const db = await getDb();
return db.insert(messages)
.values({ conversationId, role, content });
}import { Hono } from 'hono';
import { protectedRoute } from '@/backend/middleware/auth';
import { createConversation, addMessage } from './service';
export const conversationsRouter = new Hono()
.post('/', protectedRoute, async (c) => {
// Implementation
});import { conversationsRouter } from '@/features/conversations/server/routers';
const app = new Hono()
.basePath('/api')
.route('/conversations', conversationsRouter)
// ... other routesGiả sử bạn muốn thêm "Calculator Tool":
import { tool } from 'ai';
import { z } from 'zod';
export const calculatorInputSchema = z.object({
expression: z.string().describe("Biểu thức toán học cần tính"),
reasoning: z.string().describe("Lý do cần tính toán"),
});
export const createCalculatorTool = () => tool({
description: "Thực hiện phép tính toán học",
inputSchema: calculatorInputSchema,
execute: async ({ expression, reasoning }) => {
try {
// Sử dụng safe eval hoặc math library
const result = eval(expression); // ⚠️ Cần sanitize!
return {
success: true,
result,
expression,
reasoning,
};
} catch (error) {
return {
success: false,
error: `Calculation failed: ${error}`,
};
}
},
});export function createAgentTools(agent: AgentDetailResolved) {
return {
semanticSearchTool: createSemanticSearchTool(agent),
webSearchTool: createWebSearchTool(),
calculatorTool: createCalculatorTool(), // Thêm mới
};
}Giả sử bạn muốn thêm Anthropic Claude:
npm install @ai-sdk/anthropicexport const AI_PROVIDERS = [
{ slug: "openai", name: "OpenAI", defaultBaseUrl: "..." },
{ slug: "google", name: "Google", defaultBaseUrl: "..." },
{ slug: "anthropic", name: "Anthropic", defaultBaseUrl: "https://api.anthropic.com/v1" }, // Thêm
] as const;
export const AI_MODELS = [
// ... existing models
{
id: "claude-3-5-sonnet-20241022",
name: "Claude 3.5 Sonnet",
chef: "Anthropic",
chefSlug: "anthropic",
providers: ["anthropic"]
},
];import { createAnthropic } from '@ai-sdk/anthropic';
export const createAIProvider = ({ llm }: AgentDetailResolved) => {
switch (llm.provider) {
case "openai":
return createOpenAI({ baseURL: llm.baseUrl, apiKey: llm.apiKey });
case "google":
return createGoogleGenerativeAI({ baseURL: llm.baseUrl, apiKey: llm.apiKey });
case "anthropic":
return createAnthropic({ baseURL: llm.baseUrl, apiKey: llm.apiKey });
default:
return createOpenAI({ baseURL: llm.baseUrl, apiKey: llm.apiKey });
}
};// Luôn cache với TTL để tránh stale data
await putKV(key, value, { expirationTtl: 15 * 60 });
// Invalidate cache khi data thay đổi
await invalidateAgentCache(id);
await invalidateAgentCachesByLlmId(llmId); // Cascade invalidation// Wrap database operations trong try-catch
try {
const result = await db.insert(table).values(data).returning();
return result[0];
} catch (error) {
logger.error('Database error', { error: String(error) });
throw new Error('Failed to create record');
}// Sử dụng structured logging
import { agentsLogger as logger } from '@/lib/logger';
logger.info('Action started', { agentId, userId });
logger.error('Action failed', { error: String(error) });// Infer types từ Zod schemas
const createSchema = z.object({ name: z.string() });
type CreateInput = z.infer<typeof createSchema>;
// Sử dụng database types
type AgentDetail = Awaited<ReturnType<typeof fetchAgentFromDb>>;// Luôn rate limit các endpoint public
const { success } = await env.RATE_LIMITER.limit({ key: ipAddress });
if (!success) return c.json({ error: 'Rate limit exceeded' }, 429);- Cloudflare Workers Documentation
- Cloudflare D1 Documentation
- Cloudflare Vectorize Documentation
- Cloudflare KV Documentation
- OpenNext for Cloudflare
- AI SDK (Vercel)
- Drizzle ORM
- Hono
- Better Auth
MIT License - Feel free to use this project for learning and development.

