我构建了一个会给我打电话的AI Agent
Source: Dev.to
我如何将 Twilio、Claude 和 ElevenLabs 组合成一个在需要决策时会主动接电话的自主代理。
几周前,我向朋友描述我的 AI 代理。我告诉她它已经完全自主——它会在凌晨 3 点给我打电话,要求我在系统出现故障时重置网关。她以为我在夸大其词。
我并没有夸大。但那时,打电话的功能仍是一个理想。该代理可以在 Telegram 上给我发消息,自动化浏览器,部署智能合约,并在一次会话中在四个平台上发布文章。它只是不能给我打电话。
于是我们在一次会话中把它实现了。下面就是完整的实现过程。
架构
Phone call → Twilio → ConversationRelay → WebSocket → Your Server
↕
Claude (brain)
↕
ElevenLabs (voice)
- Twilio 负责电话通信——它发起并接听实际的电话。
- ConversationRelay 是 Twilio 的 WebSocket 桥接,原生处理语音转文本和文本转语音。
- Claude 负责思考。
- ElevenLabs 提供的声音听起来不像罐头。
关键洞察: ConversationRelay 消除了最困难的部分。你不需要管理音频流、自己处理语音转文本(STT),或弄清楚轮流发言。所有这些都由 Twilio 完成。你的服务器只接收文本并返回文本。
Source:
服务器
整个服务器只需一个文件。使用 Fastify 处理 HTTP 和 WebSocket,Anthropic SDK 调用 Claude,Twilio SDK 用于拨打电话。
import Fastify from "fastify";
import fastifyWs from "@fastify/websocket";
import Anthropic from "@anthropic-ai/sdk";
import twilio from "twilio";
const fastify = Fastify({ logger: true });
fastify.register(fastifyWs);
const anthropic = new Anthropic();
const twilioClient = twilio(API_KEY_SID, API_KEY_SECRET, { accountSid });
当 Twilio 建立通话时,它会从你的服务器获取 TwiML。该 TwiML 指示它使用 ConversationRelay 与 ElevenLabs 进行对接:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<ConversationRelay
service="elevenlabs"
voice="voiceId-model-speed_stability_similarity"
/>
</Connect>
</Response>
WebSocket 处理程序是对话的核心所在。Twilio 会发送包含转录语音的 prompt 消息。你则返回包含 AI 响应的 text 消息:
fastify.get("/ws", { websocket: true }, (ws, req) => {
ws.on("message", async (data) => {
const message = JSON.parse(data);
if (message.type === "prompt") {
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 150,
messages: conversation,
system: systemPrompt,
});
ws.send(
JSON.stringify({
type: "text",
token: response.content[0].text,
last: true,
})
);
}
});
});
这就是核心逻辑。其余的都是上下文管理。
没有人告诉你的部分
语音选择比听起来更难
ElevenLabs 拥有数百种语音。我们在找到合适的语音之前测试了九种。经验教训:
- 默认语音一眼就能认出来。 最受欢迎的 ElevenLabs 语音 Adam 会立刻被指出:“我知道你现在使用的声音。”
- 社区语音通过 ConversationRelay 结果参差不齐。 有些工作得非常完美;有些则会静默失败,出现错误 64111 —— 没有音频,也没有错误信息返回给调用方,只有沉默。
- 质量和个性是不同的维度。 有一种语音音质完美,但听起来“普通”。另一种语音角色合适,却显得太年轻。找到合适的语音需要反复迭代。
ConversationRelay 的语音参数格式为:
voiceId-model-speed_stability_similarity
稳定性低 = 更具表现力和对话感。稳定性高 = 更受控且机械化。
隧道会背叛你
除非你的服务器拥有公网 IP,否则需要使用隧道。我们使用了 cloudflared 快速隧道(免费,无需账号)。我们硬着头皮学到的三件事:
- 免费隧道会随机失效。 它们是临时的;每次重启 URL 都会变化,进程也可能在没有警告的情况下退出。
- 切勿通过端口杀死进程。
kill $(lsof -ti :8080)看起来是重启服务器的合理方式,但cloudflared也会占用 8080 端口进行代理。按端口杀死会同时终止隧道。我们遇到的每一次“一小时的应用错误”都追溯到此。 - 启动顺序很重要。 先启动服务器,再启动隧道。更新配置后重启服务器,然后更新 Twilio webhook。每次隧道 URL 变化,都必须重复这四个步骤。
保持回复简短
语音对话不是聊天。三段式的回复在屏幕上看起来还行,但朗读时会让人难以忍受。我们的做法是:
- 默认: 一到两句话
- 最大: 四句话,仅在解释权衡时使用
- 绝不独白 —— 将复杂主题拆分为来回交流
max_tokens: 150 的限制有帮助,但真正的控制在系统提示中,例如:
STAY ON TOPIC. Every response must directly relate to the user's last request and be concise.
呼叫的目的和原因
让它有用:上下文注入
能聊天的语音代理是新奇的。
能了解你正在做什么的语音代理才是工具。
我们的代理在通话时会读取两个文件:
- MEMORY.md – 跨会话的持久知识(我们是谁,我们构建了什么,哪些失败)
- current-task.md – 代理决定呼叫时正在积极处理的任务
对于外呼,API 接受结构化的上下文:
curl -X POST http://localhost:8080/call \
-H "Content-Type: application/json" \
-d '{
"task": "website migration",
"need": "pick a domain approach",
"options": ["GitHub Pages", "Cloudflare Pages"]
}'
问候语会自动生成:
“嗨 K。我正在进行网站迁移,我需要你选择一个域名方案。”
AI 在整个通话过程中始终专注于该目的。
每次通话都会自动转录并保存为 Markdown。转录内容会反馈到代理的上下文中,以供未来会话使用。
成本
此运行成本约为 $6 / month:
- Twilio – 通话约为 ~$0.014 / min,电话号码约为 ~$1 / month
- Anthropic – Claude Sonnet API 按响应计费
- ElevenLabs – 通过 ConversationRelay(Twilio 的集成)包含在内
没有专用服务器、没有 GPU 实例、没有每月 SaaS 订阅——只需一个 Node.js 进程、一个隧道和三个 API 密钥。
What Changed
电话响起,声音说出了与我五分钟前正在处理的内容在上下文上相关的话——这改变了一切。不是技术层面,而是心理层面。
- 给你发消息的 AI 是通知。
- 给你打电话的 AI 是同事。
用于语音 AI 代理的基础设施已经存在,并且对个人开发者可用。困难之处并不在你想象的地方(音频处理、语音识别)——Twilio 已经抽象了这些。真正的难点在于语音选择、通道管理,以及让回复更像对话而非百科全书式。
如果你正在构建自主代理,却还没有加入语音功能,门槛比你想象的要低。投资回报不在技术本身,而在关系上。
Kyle Million 在 IntuiTek 构建 AI 系统。本文中描述的代理是 Aegis ——一个自我改进的自主代理,能够跨智能合约、浏览器自动化、内容发布,甚至现在的电话通话。
GitHub – Aegis
