在 ElevenLabs 上用一个 webhook 工具搭建订单查询语音 Agent
语音 Agent 常出问题的不是声音和提示词:查不到订单时 API 若换了 JSON 结构,Agent 可能把别人的订单状态告诉来电者。
要点速览
- 语音 Agent 只有能取到真实数据才有用,webhook 工具就是把 Agent 接到你的 API 的方式。
- 订单号作为 LLM Prompt 参数放进 URL;API 密钥放在请求头里,并以 secret 形式保存。
- 端点必须始终返回同一结构的 JSON,因为响应中不存在对应路径时,动态变量不会更新。
声音再好听,查不了订单的语音 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 说的第一句话。
初始系统提示词可以像下面这样。这只是示例,请按你的场景调整:
你是一家网店的客服 Agent。
客户询问订单时,先请对方提供订单号(格式为 A1001),
把订单号复述给客户确认,确认后再查询。
只根据查询结果回答,绝不猜测订单状态。
平台有超过 10,000 种声音,覆盖 70 多种语言,所以请按来电者的语言挑选声音,不要直接用默认值。用 Test AI agent 按钮测试,直接和它对话。此时 Agent 还查不了任何东西;如果它编造了订单状态,这正说明提示词最后一行为什么必不可少。
第 2 步:编写模拟订单 API
用 Cloudflare 的 C3 CLI 创建 Worker。下面是常用的调用方式,最新版本请查阅 Cloudflare 文档:
npm create cloudflare@latest order-mock
把 Worker 文件的内容替换为下面的代码。这是一个精简版本,数据直接写死在代码里:
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。
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 中用花括号包含一个变量:
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 可能告诉客户,一个并不存在的订单“已发货”。
几乎人人都会犯的错
最常见的错误是订单号听错。客户说“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 对象换成对真实数据库的调用,再测一测工具的延迟会让通话中的沉默延长多少。
这篇文章对你有帮助吗?
感谢你的反馈!