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.
Tóm tắt nhanh
- Voice agent chỉ có ích khi gọi được dữ liệu thật, và webhook tool là cách nối agent với API của bạn.
- Mã đơn nằm trong URL dưới dạng tham số kiểu LLM Prompt, còn API key đặt ở header và lưu dưới dạng secret.
- Endpoint luôn phải trả JSON cùng một khuôn dạng, vì đường dẫn nào không tồn tại thì dynamic variable sẽ không được cập nhật.
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:
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:
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:
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.
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:
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”.
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.
Bài này có hữu ích không?
Cảm ơn bạn đã góp ý!