---
title: "Bikin AI Chat dengan Tool Calling di Next.js: Panduan Lengkap"
url: "https://helipod.io/blog/bikin-ai-chat-tool-calling-nextjs"
date: "2026-06-04"
updated: "2026-06-04"
author: "Tim Helipod"
tags: ["nextjs", "ai", "tool-calling", "vercel-ai-sdk", "tutorial", "typescript"]
excerpt: "Step-by-step bikin AI chat dengan tool calling di Next.js dan Vercel AI SDK. Contoh e-commerce: cek pesanan, proses refund, dan kelola produk."
---

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

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](/blog/apa-itu-tool-calling-ai) dan [Cara Kerja AI Chatbot](/blog/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 SDK**: `ai` + `@ai-sdk/anthropic` (atau `@ai-sdk/openai`)
- **Zod**: validasi parameter tool
- **React**: UI components

## Step 1: Install Dependencies

```bash
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`:

```typescript
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`:

```typescript
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`:

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

```tsx
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.

```typescript
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:

```typescript
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:

```typescript
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]
```

## Related Articles

- [Apa Itu Tool Calling di AI?](/blog/apa-itu-tool-calling-ai): Konsep dasar tool calling
- [Cara Kerja AI Chatbot](/blog/cara-kerja-ai-chatbot): Bagaimana LLM dan chatbot bekerja
- [AI Assistant untuk DevOps](/blog/ai-assistant-untuk-devops): Penerapan AI assistant di infrastruktur
- [Heli Crew: AI Assistant Helipod](/blog/heli-crew-ai-assistant), Contoh penerapan di platform deploy
