AizuDemy

Tutorial Build AI Agent Offline di Browser Pakai WebGPU dan Transformers.js

๐ŸŽง
Dengarkan Artikel Ini
Suara AI Otomatis โ€ข 6 mnt baca baca
โšก TL;DR

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`).
๐Ÿ“‹ Daftar Isi Materi Tutup โ–ด

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.

BackendAkses HardwareThroughput TokenUkuran Memori Maksimum
WASM (CPU)Single/Multi Thread CPUSangat Lambat (2-5 TPS)Terbatas RAM 4GB per tab
WebGLFragment Shader (Graphics)Sedang (10-15 TPS)VRAM terbatas, Overhead tinggi
WebGPUCompute 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/transformers

Konfigurasi `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