NEWAuto-scale sesuai traffic
Bonus 25%!0h
Detail
nextjsaitool-callingvercel-ai-sdktutorialtypescript

Bikin AI Chat dengan Tool Calling di Next.js — Panduan Lengkap

Tim Helipod

8 menit baca

Step-by-step membangun AI chat assistant dengan tool calling menggunakan Next.js dan Vercel AI SDK. Contoh e-commerce: cek pesanan, proses refund, dan kelola produk.

Membuat AI chatbot yang cuma jawab pertanyaan memang simpel. Tapi bagaimana kalau AI-nya bisa benar-benar melakukan sesuatu — query database, proses pesanan, atau kelola produk?

Artikel ini akan membimbing kamu membangun AI chat assistant dengan tool calling menggunakan Next.js dan Vercel AI SDK. Kita akan bangun chat assistant untuk e-commerce — bisa cek pesanan, proses refund, dan kelola produk langsung dari chat.

Kalau kamu belum familiar dengan konsepnya, baca dulu Apa Itu Tool Calling dan Cara Kerja AI Chatbot.

Apa yang Kita Bangun

Chat assistant untuk e-commerce yang:

  • Menggunakan Claude/GPT sebagai brain
  • Query data pesanan, produk, dan pelanggan dari database
  • Proses refund dengan konfirmasi
  • Streaming response tanpa lag

Tech Stack

  • Next.js 15+ — App Router
  • Vercel AI SDKai + @ai-sdk/anthropic (atau @ai-sdk/openai)
  • Zod — validasi parameter tool
  • React — UI components

Step 1: Install Dependencies

npm install ai @ai-sdk/anthropic zod

Step 2: Define Tools

Tools adalah fungsi yang bisa dipanggil AI. Setiap tool punya nama, deskripsi, parameter, dan fungsi execute.

Buat file lib/tools.ts:

import { tool } from "ai";
import { z } from "zod";

export const tools = {
  // Cek pesanan pelanggan
  getOrder: tool({
    description: "Cari pesanan berdasarkan nomor pesanan atau nama pelanggan",
    parameters: z.object({
      query: z.string().describe("Nomor pesanan (ORD-xxx) atau nama pelanggan"),
    }),
    execute: async ({ query }) => {
      const order = await db.order.findFirst({
        where: {
          OR: [
            { orderNumber: query },
            { customerName: { contains: query } },
          ],
        },
        include: { items: true, payments: true },
      });
      return order ?? { error: "Pesanan tidak ditemukan" };
    },
  }),

  // Lihat daftar produk
  getProducts: tool({
    description: "Lihat daftar produk dan stok",
    parameters: z.object({
      category: z.string().optional().describe("Filter berdasarkan kategori"),
    }),
    execute: async ({ category }) => {
      const where = category ? { category } : {};
      const products = await db.product.findMany({ where, take: 20 });
      return { products };
    },
  }),

  // Proses refund (butuh approval!)
  processRefund: tool({
    description: "Proses refund untuk pesanan tertentu",
    parameters: z.object({
      orderId: z.string().describe("ID pesanan yang akan di-refund"),
      reason: z.string().describe("Alasan refund"),
    }),
    needsApproval: true, // ← Minta konfirmasi user sebelum eksekusi
    execute: async ({ orderId, reason }) => {
      const order = await db.order.findUnique({ where: { id: orderId } });
      if (!order) return { error: "Pesanan tidak ditemukan" };

      await db.refund.create({
        data: {
          orderId,
          amount: order.totalAmount,
          reason,
          status: "processed",
        },
      });

      return {
        success: true,
        refundAmount: order.totalAmount,
        message: `Refund Rp ${order.totalAmount.toLocaleString("id-ID")} berhasil diproses`,
      };
    },
  }),

  // Update stok produk
  updateStock: tool({
    description: "Update stok produk",
    parameters: z.object({
      productId: z.string().describe("ID produk"),
      quantity: z.number().describe("Stok baru"),
    }),
    needsApproval: true,
    execute: async ({ productId, quantity }) => {
      await db.product.update({
        where: { id: productId },
        data: { stock: quantity },
      });
      return { success: true, message: `Stok berhasil diupdate ke ${quantity}` };
    },
  }),
};

Tip: Gunakan needsApproval: true untuk aksi destruktif seperti refund atau update stok. AI SDK akan otomatis menampilkan tombol approve/deny di UI.

Step 3: Buat API Route

API route menerima pesan dari client, memanggil LLM dengan tools, dan mengembalikan streaming response.

Buat file app/api/chat/route.ts:

import { streamText, convertToModelMessages, stepCountIs } from "ai";
import { createAnthropic } from "@ai-sdk/anthropic";
import { tools } from "@/lib/tools";

const anthropic = createAnthropic({
  baseURL: process.env.ANTHROPIC_BASE_URL,
  authToken: process.env.ANTHROPIC_AUTH_TOKEN,
});

export async function POST(req: Request) {
  const { messages } = await req.json();

  const modelMessages = await convertToModelMessages(messages);

  const result = streamText({
    model: anthropic("claude-sonnet-4-20250514"),
    system: `Kamu adalah asisten customer service untuk toko online.
      Bantu pelanggan dengan pesanan, produk, dan refund.
      Gunakan tools untuk mengambil data dari database.
      Selalu jelaskan apa yang akan dilakukan sebelum memanggil tool.`,
    messages: modelMessages,
    tools,
    stopWhen: stepCountIs(10),
  });

  return result.toUIMessageStreamResponse();
}

Catatan penting:

  • convertToModelMessages mengkonversi UI messages ke format yang dipahami model
  • stopWhen: stepCountIs(10) mencegah AI loop tak terbatas memanggil tools
  • toUIMessageStreamResponse() mengembalikan format yang dipahami useChat hook

Step 4: Buat Chat UI

Gunakan useChat hook dari Vercel AI SDK untuk handle streaming, tool calls, dan approval.

Buat file components/chat-panel.tsx:

"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";

export function ChatPanel() {
  const {
    messages,
    sendMessage,
    status,
    addToolApprovalResponse,
  } = useChat({
    transport: new DefaultChatTransport({
      api: "/api/chat",
    }),
    sendAutomaticallyWhen:
      lastAssistantMessageIsCompleteWithApprovalResponses,
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const input = (e.target as HTMLFormElement).elements.namedItem("input") as HTMLInputElement;
    if (!input.value.trim()) return;
    sendMessage({ text: input.value });
    input.value = "";
  };

  return (
    <div className="flex flex-col h-full">
      <div className="flex-1 overflow-y-auto p-4">
        {messages.map((msg) => (
          <Message key={msg.id} message={msg} />
        ))}
      </div>

      <form onSubmit={handleSubmit} className="border-t p-4">
        <input name="input" className="w-full border rounded-lg p-2" />
      </form>
    </div>
  );
}

Step 5: Render Tool Calls

Setiap message punya parts yang berisi text dan tool calls. Render tool call sebagai card terpisah.

function Message({ message }: { message: UIMessage }) {
  const toolParts = message.parts.filter((p) =>
    p.type.startsWith("tool-")
  );
  const textParts = message.parts.filter((p) => p.type === "text");

  return (
    <div>
      {toolParts.map((part, i) => (
        <ToolCallCard
          key={i}
          part={part}
          onApprove={(id) =>
            addToolApprovalResponse({ id, approved: true })
          }
          onDeny={(id) =>
            addToolApprovalResponse({ id, approved: false })
          }
        />
      ))}

      {textParts.map((part, i) => (
        <div key={i}>{part.text}</div>
      ))}
    </div>
  );
}

function ToolCallCard({ part, onApprove, onDeny }: any) {
  const isApproval = part.state === "approval-requested";
  const isDone = part.state === "output-available";

  const toolLabels: Record<string, string> = {
    getOrder: "Cek Pesanan",
    getProducts: "Lihat Produk",
    processRefund: "Proses Refund",
    updateStock: "Update Stok",
  };
  const label = toolLabels[part.type.replace("tool-", "")] ?? part.type;

  return (
    <div className="border rounded-lg p-3 my-2">
      <div className="text-xs font-mono text-gray-500">{label}</div>

      {isApproval && (
        <div className="flex gap-2 mt-2">
          <button
            onClick={() => onApprove(part.approval.id)}
            className="px-3 py-1 bg-green-500 text-white rounded"
          >
            Jalankan
          </button>
          <button
            onClick={() => onDeny(part.approval.id)}
            className="px-3 py-1 border rounded"
          >
            Tolak
          </button>
        </div>
      )}

      {isDone && (
        <pre className="text-xs mt-2 overflow-auto">
          {JSON.stringify(part.output, null, 2)}
        </pre>
      )}
    </div>
  );
}

Step 6: Handle Approval Flow

Ketika AI memanggil tool dengan needsApproval: true, tool part akan masuk ke state approval-requested. User harus klik approve atau deny sebelum tool dieksekusi.

User: "Refund pesanan ORD-12345, barang rusak"
  ↓
AI: [tool-call: processRefund, state: approval-requested]
  ↓
UI: [Tombol: Jalankan] [Tombol: Tolak]
  ↓
User klik "Jalankan"
  ↓
addToolApprovalResponse({ id, approved: true })
  ↓
AI SDK auto-resume → API route → execute tool → stream result

Penting: sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses wajib di-setting agar stream otomatis resume setelah approval. Tanpa ini, user harus kirim pesan manual untuk melanjutkan.

Step 7: System Prompt

System prompt menentukan bagaimana AI bersikap dan kapan harus menggunakan tools.

const systemPrompt = `Kamu adalah asisten customer service toko online "Toko Berkah".

## Aturan
- Bantu pelanggan dengan pesanan, produk, dan refund
- Gunakan tools untuk mengambil data dari database
- Untuk refund, selalu tampilkan detail pesanan dulu, lalu konfirmasi
- Jawab dengan ramah dalam Bahasa Indonesia

## Tools yang tersedia
- getOrder: Cari pesanan (butuh nomor pesanan atau nama pelanggan)
- getProducts: Lihat daftar produk dan stok
- processRefund: Proses refund (butuh approval user)
- updateStock: Update stok produk (butuh approval user)
`;

Tips Penting

1. Jangan Over-Tool

Jangan buat tool untuk setiap hal kecil. Fokus pada operasi yang sering dilakukan user. Terlalu banyak tools membuat AI bingung memilih yang tepat.

2. Validasi Input dengan Zod

Selalu validasi parameter tool dengan Zod:

parameters: z.object({
  query: z.string().min(1).describe("Nomor pesanan atau nama pelanggan"),
  reason: z.enum(["rusak", "salah_kirim", "tidak_sesuai"]).describe("Alasan refund"),
})

3. Handle Error di Execute

Tool execute harus handle error dengan graceful:

execute: async ({ orderId }) => {
  try {
    const order = await db.order.findUnique({ where: { id: orderId } });
    if (!order) return { error: "Pesanan tidak ditemukan" };
    return { order };
  } catch (error) {
    return { error: "Gagal mengambil data pesanan" };
  }
}

4. Batasi Step Count

Gunakan stopWhen: stepCountIs(N) untuk mencegah AI loop tak terbatas. 5-10 steps biasanya cukup.

5. Streaming untuk UX

Selalu gunakan streaming (streamText + toUIMessageStreamResponse). Response yang muncul kata per kata terasa jauh lebih cepat dari response yang muncul sekaligus.

Contoh Percakapan

User: Pesanan saya ORD-12345 statusnya gimana?

AI: [panggil tool getOrder({ query: "ORD-12345" })]
    Saya cek ya pesanan ORD-12345...

    Pesanan #ORD-12345
    Pelanggan: Budi Santoso
    Status: Dikirim (JNE - JN012345678)
    Total: Rp 450.000
    Item: Widget A (2x), Gadget B (1x)

    Ada yang bisa dibantu lagi?

User: Tolong refund ya, barangnya rusak

AI: [panggil tool processRefund({ orderId: "ORD-12345", reason: "rusak" })]
    Saya proses refund untuk pesanan ORD-12345 ya.

    Detail refund:
    - Pesanan: ORD-12345
    - Jumlah: Rp 450.000
    - Alasan: Barang rusak

    [Tombol: Jalankan] [Tombol: Tolak]

Siap coba Helipod?

Deploy aplikasi kamu sekarang. Gratis, tanpa kartu kredit.

Mulai Gratis →