# 在 ElevenLabs 上用一个 webhook 工具搭建订单查询语音 Agent

> 语音 Agent 常出问题的不是声音和提示词：查不到订单时 API 若换了 JSON 结构，Agent 可能把别人的订单状态告诉来电者。

原文: https://fdetimes.net/zh/guides/elevenlabs-voice-agent-order-lookup-webhook/

声音再好听，查不了订单的语音 Agent 也只是一台嗓音悦耳的答录机。客户打电话来问“我的订单到哪了？”，答案在公司的系统里，不在语言模型里。

本指南要搭的正是这一块。最终你会得到三样东西：ElevenLabs Agent 平台上的一个 Agent，一个用 JavaScript 编写、运行在 Cloudflare Worker 上的模拟订单 API，以及连接二者的 webhook 工具。大约一个晚上就能完成。你需要一个 ElevenLabs 账号、一个 Cloudflare 账号、Node.js 和 curl。

如果你是前线部署工程师（FDE），或者正以这个岗位为目标，可以想象这个练习的真实版本：订单数据位于客户现有的 API 之后，你的工作是把它封装好，让 Agent 能安全调用，再证明整个流程端到端可用。

## 你说话时，Agent 在做什么？

根据 ElevenLabs 的文档，平台编排四个组件：自动语音识别（ASR）、一个由你选择的 LLM、一个低延迟的文本转语音（TTS）模型，以及一个单独负责对话节奏的轮次管理模型。本指南中由你决定三件事：声音、LLM，以及允许 LLM 调用的工具。

ElevenLabs 提供多种工具：client、webhook、code、MCP 和 system。webhook 工具调用你的外部 API。code 工具在 ElevenLabs 基础设施上的沙箱里运行 JavaScript，你不必自己托管端点。

本指南选用 webhook，因为它更接近真实部署：在客户那里，订单数据和访问密钥都在客户的系统里，webhook 让它们留在原处。

## 第 1 步：创建空白 Agent 并配置声音

在控制台中新建一个 assistant，起个名字，选择 Blank 模板。然后填写三项：系统提示词、首条消息和声音。系统提示词决定 Agent 的行为方式，首条消息是通话一接通 Agent 说的第一句话。

初始系统提示词可以像下面这样。这只是示例，请按你的场景调整：

```text
你是一家网店的客服 Agent。
客户询问订单时，先请对方提供订单号（格式为 A1001），
把订单号复述给客户确认，确认后再查询。
只根据查询结果回答，绝不猜测订单状态。
```

平台有超过 10,000 种声音，覆盖 70 多种语言，所以请按来电者的语言挑选声音，不要直接用默认值。用 Test AI agent 按钮测试，直接和它对话。此时 Agent 还查不了任何东西；如果它编造了订单状态，这正说明提示词最后一行为什么必不可少。

## 第 2 步：编写模拟订单 API

用 Cloudflare 的 C3 CLI 创建 Worker。下面是常用的调用方式，最新版本请查阅 Cloudflare 文档：

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

把 Worker 文件的内容替换为下面的代码。这是一个精简版本，数据直接写死在代码里：

```js
const ORDERS = {
  A1001: { status: "shipped", eta: "in 2 days" },
  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,
      },
    });
  },
};
```

这段代码有两点值得注意。Worker 中的所有异步操作都必须在 handler 内执行，不能放在全局作用域，所以把 `ORDERS` 对象换成对真实系统的 `fetch` 调用时，要把调用写在 `fetch(request, env)` 里面。

另一点：不存在的订单同样返回完全相同的 `{ order: { id, status, eta } }` 结构，只是 `status: "not_found"`。原因到第 5 步就清楚了。

`ORDER_API_KEY` 是环境变量。按 Cloudflare 的说明把它设为 Worker secret，不要写进代码。

## 第 3 步：先部署测试，再碰 Agent

用 Wrangler 部署到 `*.workers.dev` 子域名。这正是 ElevenLabs 需要的：一个公开的 HTTPS URL。

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

第一条命令必须返回 `shipped`，第二条返回 `not_found`，第三条返回 401。三个结果都对了再往下走。先单独测试后端，出问题时就不用猜到底是提示词、工具还是 API 的毛病。

## 第 4 步：挂载 webhook 工具

在 Agent 的 Tools 部分添加一个 webhook 工具，方法选 GET，URL 中用花括号包含一个变量：

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

把 `order_id` 声明为路径参数，值类型选 LLM Prompt。选这个类型后，模型会自己从对话中提取值，所以参数描述必须写清楚，例如“订单号，由一个字母加四位数字组成，如 A1001”。

添加 `x-api-key` 请求头，并按 ElevenLabs 对 API 密钥的建议，以 secret 形式保存。body content type 不用设置，它只适用于 POST、PUT 和 PATCH。GET 查询只需要路径参数或查询参数。

## 第 5 步：用动态变量保存订单状态

如果工具返回的是合法的 JSON 对象，就可以创建或更新动态变量。你用点号写法声明路径：把 `order.status` 映射到变量 `order_status`，把 `order.eta` 映射到 `order_eta`。之后的对话就能直接使用这两个变量，不必再次调用 API。

这就是第 2 步强制 Worker 始终返回同一结构的原因。响应中缺少某个字段或路径时，对应的变量不会更新。假设 Worker 对错误的订单号返回 `{ "error": "not found" }`：`order_status` 会保留上一次查询的值，Agent 可能告诉客户，一个并不存在的订单“已发货”。

**要点:** 只有背后的 API 始终以固定结构返回数据，语音 Agent 才能说对话。

## 几乎人人都会犯的错

最常见的错误是订单号听错。客户说“A 一零零一”，ASR 可能转写成“A 1001”或“a-1001”。Worker 里的 `replace` 那一行处理空格和连字符，提示词里“把订单号复述给客户确认”的指令负责其余情况。两层都要有，只靠一层就会漏掉错误。

其次是让 Agent 自己去解读技术状态码。`processing` 念出来很生硬，所以在提示词里加一张表，把每种状态转成自然的说法。另外，不要把 API 密钥放在 URL 或代码里：它会泄露到日志中，而请求头配合 secret 就能避免这个问题。

最后检查一遍：点击 Test AI agent，先问 A1001，再问 Z9999，然后再查一次 A1001，并追问“那什么时候能到？”。最后这个问题检验 Agent 是否用上了 `order_eta`；中间那次 Z9999 查询则能看出 `order_status` 是否切换成了 `not_found`，而不是保留旧值。

## 在客户现场，难点在哪里？

本指南中的模拟 Worker，到了客户那里就成为 Agent 与客户订单管理系统之间的缓冲层。在这一层里，你要规范化订单号，隐藏 Agent 不需要看到的字段，即使后端系统变了，也要返回固定结构。

要向客户工程团队提出的第一个问题是：订单查询 API 遇到错误订单号会返回什么？怎样给 Agent 单独签发一个密钥？

如果你的目标是应用 AI 方向的 FDE 岗位，把这个项目放到 GitHub 上，并附上对上述失败场景的说明。

面试时，讲清楚端点为什么始终返回同一结构，远比一个对答流畅的演示更有说服力。

值得做的下一步：把 `ORDERS` 对象换成对真实数据库的调用，再测一测工具的延迟会让通话中的沉默延长多少。

**本周动手试试:**

- 把本指南的模拟 Worker 部署到 workers.dev，用 curl 测试三种情况：真实订单、不存在的订单、缺少 API 密钥。
- 把 webhook 工具挂到 Agent 上，在控制台用不同的订单号说法测试 10 次，记录 Agent 填错 order_id 的次数。
- 写一份简短的 README，包含流程图、失败场景及各自的处理方式，然后把仓库链接写进简历的语音 Agent 项目下。

## 信源

- [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/)
