# Dựng voice agent tra đơn hàng trên ElevenLabs bằng một webhook tool

> Phần dễ hỏng nhất của một voice agent không phải giọng đọc hay prompt: nếu API trả JSON lệch khuôn khi không tìm thấy đơn, agent có thể đọc cho khách trạng thái của một đơn khác.

Bản gốc: https://fdetimes.net/vi/bach-khoa/thuc-hanh-voice-agent-elevenlabs-webhook-tool-javascript/

Một voice agent nói hay mà không tra được đơn hàng thì chỉ là cái máy trả lời tự động có giọng dễ nghe. Khách gọi tới hỏi "đơn của tôi đến đâu rồi", và câu trả lời nằm trong hệ thống của doanh nghiệp, không nằm trong mô hình ngôn ngữ.

Bài này dựng đúng mảnh ghép đó. Bạn sẽ có một agent trên nền tảng agent của ElevenLabs, một API đơn hàng giả lập viết bằng JavaScript chạy trên Cloudflare Worker, và một webhook tool nối hai thứ lại. Thời gian chừng một buổi tối. Bạn cần một tài khoản ElevenLabs, một tài khoản Cloudflare, Node.js và curl.

Nếu bạn làm FDE hoặc đang nhắm tới vai trò này, thử hình dung phiên bản thật của bài tập: dữ liệu đơn hàng nằm sau một API có sẵn của khách, và việc của bạn là bọc nó lại để agent gọi an toàn, rồi chứng minh luồng chạy được từ đầu đến cuối.

## Agent đang làm gì khi bạn nói?

Theo tài liệu của ElevenLabs, nền tảng điều phối bốn thành phần: nhận dạng giọng nói (ASR), một LLM do bạn chọn, mô hình chuyển văn bản thành giọng nói (TTS) độ trễ thấp, và một mô hình turn-taking riêng để canh nhịp hội thoại. Trong bài này, bạn quyết định ba thứ: giọng đọc, LLM và những tool mà LLM được phép gọi.

ElevenLabs có nhiều loại tool: client, webhook, code, MCP và system. Webhook tool gọi API bên ngoài của bạn. Code tool chạy JavaScript trong sandbox trên hạ tầng của ElevenLabs, nghĩa là bạn không phải tự host endpoint.

Bài này chọn webhook vì nó gần với thực tế triển khai hơn: ở khách hàng, dữ liệu đơn và key truy cập nằm trong hệ thống của họ, và webhook để chúng ở nguyên chỗ đó.

## Bước 1: tạo agent trống và cho nó một giọng

Trong dashboard, tạo assistant mới, đặt tên và chọn template Blank. Sau đó điền ba thứ: system prompt, first message và giọng đọc. System prompt quy định cách agent hành xử. First message là câu agent nói ngay khi cuộc gọi bắt đầu.

Một system prompt khởi đầu có thể như sau. Đây chỉ là ví dụ minh hoạ, bạn tự chỉnh cho phù hợp:

```text
Bạn là nhân viên chăm sóc khách hàng của một cửa hàng online.
Khi khách hỏi về đơn hàng, hãy xin mã đơn (dạng A1001),
đọc lại mã để khách xác nhận, rồi mới tra cứu.
Chỉ trả lời dựa trên kết quả tra cứu. Không tự đoán trạng thái.
```

Về giọng, nền tảng có hơn 10.000 giọng trên hơn 70 ngôn ngữ, nên hãy chọn giọng theo ngôn ngữ của người gọi chứ đừng chọn giọng mặc định. Kiểm tra bằng nút Test AI agent rồi nói chuyện trực tiếp. Lúc này agent chưa tra được gì, và nếu nó bịa ra trạng thái đơn thì đó là lý do bạn cần dòng cuối trong prompt.

## Bước 2: viết API đơn hàng giả lập

Tạo một Worker bằng C3 CLI của Cloudflare. Lệnh dưới đây là cách gọi thông dụng, bạn nên đối chiếu với tài liệu Cloudflare để dùng bản mới nhất:

```bash
npm create cloudflare@latest order-mock
```

Thay nội dung file Worker bằng đoạn mã sau. Đây là bản rút gọn, dữ liệu nằm cứng trong code:

```js
const ORDERS = {
  A1001: { status: "shipped", eta: "2 ngày nữa" },
  A1002: { status: "processing", eta: null },
};

export default {
  async fetch(request, env) {
    if (request.headers.get("x-api-key") !== env.ORDER_API_KEY) {
      return Response.json({ error: "unauthorized" }, { status: 401 });
    }
    const m = new URL(request.url).pathname.match(/^\/orders\/([^/]+)$/);
    if (!m) return Response.json({ error: "bad_path" }, { status: 404 });

    const id = decodeURIComponent(m[1]).replace(/\s|-/g, "").toUpperCase();
    const found = ORDERS[id];
    return Response.json({
      order: {
        id,
        status: found ? found.status : "not_found",
        eta: found ? found.eta : null,
      },
    });
  },
};
```

Có hai điểm cần để ý trong đoạn mã. Mọi việc bất đồng bộ trong Worker phải chạy bên trong handler chứ không đặt ở global scope, nên khi bạn thay object `ORDERS` bằng lệnh `fetch` tới hệ thống thật, hãy đặt lệnh đó trong `fetch(request, env)`.

Điểm còn lại: đơn không tồn tại vẫn trả về đúng khuôn `{ order: { id, status, eta } }` với `status: "not_found"`. Lý do sẽ rõ ở bước 5.

`ORDER_API_KEY` là biến môi trường. Hãy đặt nó thành secret của Worker theo hướng dẫn của Cloudflare, đừng ghi thẳng vào code.

## Bước 3: deploy và gọi thử trước khi đụng tới agent

Deploy bằng Wrangler lên một subdomain `*.workers.dev`. ElevenLabs cần đúng thứ này: một URL HTTPS công khai.

```bash
npx wrangler deploy
curl -H "x-api-key: $KEY" https://order-mock.<ten-ban>.workers.dev/orders/A1001
curl -H "x-api-key: $KEY" https://order-mock.<ten-ban>.workers.dev/orders/Z9999
curl https://order-mock.<ten-ban>.workers.dev/orders/A1001
```

Lệnh đầu phải trả `shipped`, lệnh thứ hai trả `not_found`, lệnh thứ ba trả 401. Nếu không ra đủ ba kết quả này thì chưa nên sang bước sau. Kiểm tra backend riêng trước giúp bạn khỏi phải đoán lỗi nằm ở prompt, ở tool hay ở API.

## Bước 4: gắn webhook tool

Trong phần Tools của agent, thêm một webhook tool với method GET và URL có biến đặt trong ngoặc nhọn:

```text
https://order-mock.<ten-ban>.workers.dev/orders/{order_id}
```

Khai báo `order_id` là path parameter với value type LLM Prompt. Với kiểu này, mô hình tự trích giá trị từ cuộc hội thoại, nên phần mô tả tham số cần ghi rõ, ví dụ "mã đơn dạng một chữ cái và bốn chữ số, như A1001".

Thêm header `x-api-key` và lưu nó dưới dạng secret, theo đúng khuyến nghị của ElevenLabs cho API key. Bỏ qua phần content type của body, vì thiết lập đó chỉ áp dụng cho POST, PUT và PATCH. Một lệnh tra cứu GET chỉ cần tham số trên đường dẫn hoặc query.

## Bước 5: giữ trạng thái đơn trong dynamic variable

Nếu tool trả về một JSON object hợp lệ, nó có thể tạo hoặc cập nhật dynamic variable. Bạn khai báo đường dẫn theo dot-notation: ánh xạ `order.status` vào biến `order_status` và `order.eta` vào biến `order_eta`. Từ lúc đó, phần sau của cuộc hội thoại dùng được hai biến này mà không cần gọi lại API.

Đây là lý do bước 2 bắt Worker luôn trả cùng một khuôn. Nếu field hoặc đường dẫn không có trong response thì biến không được cập nhật. Thử hình dung Worker trả `{ "error": "not found" }` cho đơn sai mã: `order_status` vẫn giữ giá trị của đơn tra trước đó, và agent có thể báo cho khách rằng một đơn không tồn tại "đã giao".

**Điểm mấu chốt:** Voice agent chỉ nói đúng khi API phía sau luôn trả dữ liệu theo một khuôn cố định.

## Những lỗi gần như ai cũng gặp

Lỗi phổ biến nhất là mã đơn bị nghe sai. Khách đọc "A một không không một", ASR có thể chép ra "A 1001" hoặc "a-1001". Dòng `replace` trong Worker xử lý khoảng trắng và gạch nối, còn câu "đọc lại mã để khách xác nhận" trong prompt xử lý phần còn lại. Cần cả hai lớp, vì chỉ trông vào một lớp thì sẽ sót.

Lỗi kế tiếp là để agent tự diễn giải mã trạng thái kỹ thuật. `processing` đọc to lên nghe rất máy móc, nên hãy thêm vào prompt bảng diễn giải từng trạng thái sang câu nói tự nhiên. Cũng đừng để API key trong URL hoặc trong code: key sẽ lộ ra trong log, trong khi header kết hợp secret giải quyết được chuyện này.

Kiểm tra cuối cùng: bấm Test AI agent, hỏi về A1001, rồi Z9999, rồi tra lại A1001 và hỏi tiếp "vậy khi nào tới". Câu cuối kiểm tra xem agent có dùng `order_eta` hay không, còn lượt tra Z9999 ở giữa cho thấy `order_status` đã chuyển sang `not_found` chứ không giữ giá trị cũ.

## Ở khách hàng, phần khó nằm ở đâu?

Worker giả lập trong bài này sẽ trở thành lớp đệm giữa agent và hệ thống quản lý đơn của khách. Ở lớp đó, bạn chuẩn hoá mã đơn, ẩn những field agent không cần thấy, và trả về một khuôn cố định dù hệ thống phía sau có thay đổi.

Câu hỏi đầu tiên nên đặt cho đội kỹ thuật của khách là: API tra đơn trả về những gì khi mã sai, và cấp key riêng cho agent như thế nào.

Nếu bạn đang nhắm tới vị trí FDE mảng applied AI, hãy đưa dự án này lên GitHub kèm phần ghi chép các ca lỗi ở trên.

Trong phỏng vấn, kể được vì sao endpoint luôn trả cùng một khuôn sẽ thuyết phục hơn nhiều so với một bản demo nói trơn tru.

Bước tiếp theo đáng thử là thay object `ORDERS` bằng một lệnh gọi tới cơ sở dữ liệu thật, rồi đo xem độ trễ của tool làm khoảng lặng trong cuộc gọi dài thêm bao nhiêu.

**Thử ngay tuần này:**

- Deploy Worker giả lập trong bài lên workers.dev rồi gọi thử bằng curl với ba trường hợp: đơn có thật, đơn không tồn tại và thiếu API key.
- Gắn webhook tool vào agent, gọi thử trong dashboard 10 lần với những cách đọc mã đơn khác nhau, rồi ghi lại số lần agent điền sai order_id.
- Viết một README ngắn có sơ đồ luồng, các ca lỗi và cách xử lý, rồi đưa link repo vào CV dưới mục voice agent.

## Nguồn

- [ElevenAgents](https://elevenlabs.io/docs/agents-platform/overview)

- [Tools](https://elevenlabs.io/docs/agents-platform/customization/tools)

- [Quickstart](https://elevenlabs.io/docs/agents-platform/quickstart)

- [Webhook tools](https://elevenlabs.io/docs/agents-platform/customization/tools/server-tools)

- [Dynamic variables](https://elevenlabs.io/docs/agents-platform/customization/personalization/dynamic-variables)

- [CLI](https://developers.cloudflare.com/workers/get-started/guide/)

- [Fetch](https://developers.cloudflare.com/workers/runtime-apis/fetch/)
