Tutorial Build AI Agent Offline di Browser Pakai WebGPU dan Transformers.js
Poin Kunci Artikel Ini:
- Chrome / Edge: Versi 113+ (Windows, macOS, ChromeOS, Android).
- Safari: Versi 18+ (macOS Sequoia, iOS 18+ via Feature Flags).
- Firefox: Versi Nightly / 125+ (Perlu mengaktifkan `dom.webgpu.enabled`).
Arsitektur Local-First AI: WebGPU vs WebGL vs WASM
Model AI cloud tradisional picu tiga masalah sentral: latensi jaringan, biaya infrasruktur per token, dan kebocoran data privasi. Arsitektur local-first memindahkan eksekusi inferensi langsung ke perangkat client (edge). Seluruh pemrosesan teks terjadi di VRAM lokal tanpa transmisi data keluar browser.
WebGPU menggantikan komputasi grafik WebGL dan eksekusi CPU via WebAssembly (WASM). WebGPU menyediakan akses langsung ke hardware GPU modern lewat compute shaders. Keunggulan WebGPU meliputi binding memori langsung, throughput komputasi FP16/INT4 hingga 10x lebih tinggi dibanding WASM, serta latensi per token sub-50ms pada model ukuran kecil.
| Backend | Akses Hardware | Throughput Token | Ukuran Memori Maksimum |
|---|---|---|---|
| WASM (CPU) | Single/Multi Thread CPU | Sangat Lambat (2-5 TPS) | Terbatas RAM 4GB per tab |
| WebGL | Fragment Shader (Graphics) | Sedang (10-15 TPS) | VRAM terbatas, Overhead tinggi |
| WebGPU | Compute Shader (Direct GPU) | Sangat Cepat (30-80+ TPS) | Akses VRAM sistem fleksibel |
Komponen Stack Teknologi
- Transformers.js v3: Runtime inferensi JavaScript buatan Hugging Face. Mendukung binding ONNX Runtime Web dengan eksekutor WebGPU bawaan.
- Quantized ONNX SLM: Model bahasa kecil (Small Language Model) seperti Qwen1.5-0.5B, SmolLM-360M, atau Phi-3-mini yang dikuantisasi ke format 4-bit (q4/q4f16) untuk efisiensi VRAM.
- Web Workers API: Layer eksekusi background thread agar proses inferensi tensor tidak menyumbat UI main thread (60 FPS).
- Origin Private File System (OPFS): Storage lokal browser untuk menyimpan file binary bobot model (.onnx) secara permanen.
Prasyarat & Matriks Kompatibilitas Browser
WebGPU membutuhkan versi browser dan sistem operasi dengan dukungan driver GPU modern:
- Chrome / Edge: Versi 113+ (Windows, macOS, ChromeOS, Android).
- Safari: Versi 18+ (macOS Sequoia, iOS 18+ via Feature Flags).
- Firefox: Versi Nightly / 125+ (Perlu mengaktifkan `dom.webgpu.enabled`).
- Hardware: GPU terintegrasi (Intel Iris/Apple Silicon M-Series) atau GPU diskrit (NVIDIA RTX/AMD Radeon) dengan driver WebGPU/Vulkan/Metal aktif.
Langkah 1: Setup Proyek Vite & Dependensi
Inisialisasi proyek TypeScript frontend baru menggunakan Vite dan pasang library `@huggingface/transformers` versi 3.x.
npm create vite@latest ai-local-agent -- --template vanilla-ts
cd ai-local-agent
npm install @huggingface/transformersKonfigurasi `vite.config.ts` untuk mengizinkan Cross-Origin Isolation jika menggunakan SharedArrayBuffer:
import { defineConfig } from 'vite';
export default defineConfig({
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
});Langkah 2: Deteksi Hardware WebGPU & Capability Check
Sebelum mendownload model, periksa ketersediaan API `navigator.gpu` dan kemampuan alokasi VRAM adapter.
// src/gpuCheck.ts
export interface GPUStats {
supported: boolean;
adapterName?: string;
maxBufferSize?: number;
}
export async function checkWebGPUSupport(): Promise<GPUStats> {
if (!navigator.gpu) {
return { supported: false };
}
try {
const adapter = await navigator.gpu.requestAdapter({
powerPreference: 'high-performance'
});
if (!adapter) return { supported: false };
const info = await adapter.requestAdapterInfo();
return {
supported: true,
adapterName: info.device || info.architecture || 'Generic WebGPU Device',
maxBufferSize: adapter.limits.maxBufferSize
};
} catch (error) {
console.error("Gagal inisialisasi WebGPU:", error);
return { supported: false };
}
}Langkah 3: Arsitektur Web Worker untuk Inferensi Non-Blocking
Menjalankan model AI di UI main thread memicu penundaan render DOM (frame drop). Pindahkan seluruh lifecycle model ke Dedicated Web Worker.
Buat file `src/worker.ts` untuk menangani eksekusi pipeline dalam background thread:
// src/worker.ts
import { pipeline, env, TextGenerationPipeline } from '@huggingface/transformers';
// Konfigurasi cachenya agar disimpan di IndexedDB/OPFS
env.allowLocalModels = false;
env.useBrowserCache = true;
class PipelineSingleton {
static task = 'text-generation' as const;
static model = 'Xenova/Qwen1.5-0.5B-Chat';
static instance: Promise<TextGenerationPipeline> | null = null;
static async getInstance(progressCallback?: (progress: number) => void) {
if (!this.instance) {
this.instance = pipeline(this.task, this.model, {
device: 'webgpu',
dtype: 'q4',
progress_callback: (info: any) => {
if (info.status === 'progress' && progressCallback) {
progressCallback(info.progress);
}
}
}) as Promise<TextGenerationPipeline>;
}
return this.instance;
}
}
self.addEventListener('message', async (event: MessageEvent) => {
const { type, data } = event.data;
if (type === 'LOAD') {
try {
await PipelineSingleton.getInstance((progress) => {
self.postMessage({ type: 'DOWNLOAD_PROGRESS', progress });
});
self.postMessage({ type: 'READY' });
} catch (error: any) {
self.postMessage({ type: 'ERROR', error: error.message });
}
}
if (type === 'GENERATE') {
try {
const generator = await PipelineSingleton.getInstance();
const output = await generator(data.messages, {
max_new_tokens: data.maxTokens || 512,
temperature: data.temperature || 0.7,
top_p: 0.9,
do_sample: true,
callback_function: (beams: any[]) => {
const decodedText = generator.tokenizer.decode(beams[0].output_token_ids, {
skip_special_tokens: true,
});
self.postMessage({ type: 'TOKEN', token: decodedText });
}
});
self.postMessage({ type: 'COMPLETE', result: output });
} catch (error: any) {
self.postMessage({ type: 'ERROR', error: error.message });
}
}
});Langkah 4: Class Agent Engine & Streaming Token Interface
Buat abstraction layer `LocalAgent` di `src/agent.ts` untuk memfasilitasi komunikasi IPC (Inter-Process Communication) antara main thread dan Web Worker.
// src/agent.ts
export interface AgentConfig {
onProgress?: (progress: number) => void;
onToken?: (token: string) => void;
onReady?: () => void;
onError?: (error: string) => void;
}
export class LocalAgent {
private worker: Worker;
constructor(config: AgentConfig) {
this.worker = new Worker(new URL('./worker.ts', import.meta.url), {
type: 'module'
});
this.worker.onmessage = (event: MessageEvent) => {
const { type, progress, token, result, error } = event.data;
switch (type) {
case 'DOWNLOAD_PROGRESS':
config.onProgress?.(progress);
break;
case 'READY':
config.onReady?.();
break;
case 'TOKEN':
config.onToken?.(token);
break;
case 'COMPLETE':
// Proses selesai
break;
case 'ERROR':
config.onError?.(error);
break;
}
};
}
public loadModel(): void {
this.worker.postMessage({ type: 'LOAD' });
}
public generate(messages: Array<{ role: string; content: string }>, maxTokens = 512): void {
this.worker.postMessage({
type: 'GENERATE',
data: { messages, maxTokens }
});
}
public terminate(): void {
this.worker.terminate();
}
}Langkah 5: Pattern Tool Calling / Function Calling Offline
Agar model berfungsi sebagai AI Agent (bukan sekadar sistem chat), agent harus bisa mengeksekusi instruksi aksi (tools) di browser lokal seperti manipulasi DOM, perhitungan matematika, atau akses IndexedDB.
Spesifikasi Schema JSON Tooling
Definisikan system prompt yang memaksa LLM memberikan output dalam format JSON terstruktur saat membutuhkan eksekusi fungsi.
// src/tools.ts
export interface Tool {
name: string;
description: string;
parameters: Record<string, any>;
execute: (args: any) => Promise<string> | string;
}
export const localTools: Record<string, Tool> = {
calculate_math: {
name: 'calculate_math',
description: 'Menghitung ekspresi matematika dasar',
parameters: { expression: 'string' },
execute: (args: { expression: string }) => {
try {
// Evaluasi matematika sederhana yang aman
const sanitized = args.expression.replace(/[^0-9+\-*/().]/g, '');
return Function(`"use strict"; return (${sanitized})`)().toString();
} catch {
return "Error kalkulasi";
}
}
},
get_system_time: {
name: 'get_system_time',
description: 'Mendapatkan waktu lokal sistem client',
parameters: {},
execute: () => new Date().toISOString()
}
};
export const SYSTEM_PROMPT_AGENT = `Kamu adalah AI Agent lokal.
Kamu memiliki akses ke tool berikut:
${JSON.stringify(Object.values(localTools).map(t => ({ name: t.name, description: t.description, parameters: t.parameters })), null, 2)}
Jika perlu menggunakan tool, kamu HARUS merespons HANYA dengan JSON format:
{"tool": "nama_tool", "args": {"key": "value


