给 AI Agent 打电话:基于 OpenAI Agents SDK 与 Twilio 的实时语音通话 Agent(call-my-agent 实战拆解)
【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents
导读
call-my-agent是当前仓库中一个完整可运行的最小示例:它展示如何把一个基于 OpenAI Agents SDK 构建的 RealtimeAgent,接入 Twilio 电话线路,部署在 Cloudflare Workers(Agents 框架)上,让用户"打一个电话"就能与 AI 对话,并在浏览器中实时看到通话转录文本。读完本文,你将掌握 Agent 与 Twilio 媒体流(Media Streams)WebSocket 对接的完整链路、TwiML 入站呼叫配置、Durable Object 中的实时会话管理,以及前端实时转录面板的实现方式。
一、项目概览:一条"电话 → AI"的实时语音链路
该示例的源码结构如下(相对仓库根目录):
- README.md —— 项目说明(一句话:做一个可以接电话的 Agent,由 OpenAI Agents SDK 和 Twilio 驱动)
- src/server.ts —— Worker 入口,包含 TwiML 响应与 Agent(Durable Object)实现
- src/client.tsx —— React 前端,展示通话状态与实时转录
- wrangler.jsonc —— Cloudflare Worker / Durable Object 配置
- package.json —— 开发、构建、部署脚本
- vite.config.ts —— Vite +
@cloudflare/vite-plugin配置
从代码结构看,整条链路由三个环节组成:
- 入站呼叫:Twilio 在接到来电时,向 Worker 的
/incoming-call端点发送 POST 请求,Worker 返回一段 TwiML,要求 Twilio 将通话媒体流转发到一个 WebSocket 地址(.../agents/my-agent/123/media-stream)。 - 实时会话:WebSocket 连接被
routeAgentRequest路由到MyAgent(一个 Durable Object)的onConnect,在media-stream路径下创建RealtimeAgent与RealtimeSession,并通过TwilioRealtimeTransportLayer把电话音频与 OpenAI Realtime API 双向对接。 - 实时转录:会话的
history_updated事件把对话历史写入 Agent 状态,前端通过useAgent订阅状态,实时渲染通话转录。
二、服务端核心:MyAgent 与 Twilio 实时传输层
整个服务端逻辑集中在 src/server.ts。MyAgent继承自agents包提供的Agent基类,本质上是一个由 Wrangler 配置的 Durable Object(详见下文 wrangler 配置)。
export class MyAgent extends Agent { // don't use hibernation, the dependencies will manually add their own handlers async onConnect(connection: Connection, ctx: ConnectionContext) { if (ctx.request.url.includes("media-stream")) { // ...实时会话初始化 } } onMessage() {} // just a blank, the transport layer will add its own handlers onClose(connection: Connection) { connection.close(); } }这里有几个值得注意的实现细节:
- 关闭休眠(hibernation):注释明确指出不能使用 Durable Object 的休眠特性,因为 Twilio 传输层会自行注册事件处理器(
onMessage()被留空,正是"传输层自己加 handler"的体现)。若启用休眠,连接可能被挂起导致媒体流中断。 - 按 URL 分流:
onConnect中通过ctx.request.url.includes("media-stream")判断该连接是否为电话媒体流。这保证了同一个 Agent 既可以处理媒体流 WebSocket,也可以承载前端的普通 WebSocket 连接(前端useAgent也连到同一个 Agent)。
2.1 RealtimeAgent:定义"接电话的 AI"行为
const agent = new RealtimeAgent({ instructions: "You are a helpful assistant that starts every conversation with a creative greeting.", name: "Triage Agent" });RealtimeAgent来自@openai/agents/realtime,是 OpenAI Agents SDK 的实时(语音)Agent 形态。instructions定义系统提示词,这里要求助手每次都以一句有创意的问候开场;name用于标识。你可以在此处扩展更复杂的工具调用或语境设定,这一层与普通文本 Agent 的配置方式一致。
2.2 TwilioRealtimeTransportLayer:音频的"接线员"
const twilioTransportLayer = new TwilioRealtimeTransportLayer({ twilioWebSocket: connection }); const session = new RealtimeSession(agent, { transport: twilioTransportLayer }); await session.connect({ apiKey: process.env.OPENAI_API_KEY as string });TwilioRealtimeTransportLayer来自@openai/agents-extensions,接收 Twilio 的 WebSocket 连接(connection即上一步由 Agents 框架路由进来的媒体流连接),负责把电话一端的音频帧翻译成 Realtime Session 能理解的消息。RealtimeSession将RealtimeAgent与传输层绑定在一起,形成一条"电话 ↔ 传输层 ↔ OpenAI Realtime API"的闭环。session.connect({ apiKey })使用OPENAI_API_KEY环境变量建立与 OpenAI Realtime API 的连接。因此在本地运行或部署时,必须在环境变量(或 Cloudflare Worker 的 Secrets)中配置OPENAI_API_KEY。
2.3 会话历史:把对话"沉淀"到状态里
session.on("history_updated", (history) => { this.setState({ history }); });每当通话内容更新,就通过this.setState({ history })把history(RealtimeItem[])写入 Agent 状态。这一步是前后端联动的关键:前端正是通过读取这份状态来实现"实时转录"效果的(见第四节)。
三、入站电话接入:/incoming-call 与 TwiML
Worker 的fetch处理器在收到POST /incoming-call时,返回一段 TwiML 指令,告诉 Twilio 如何接管这通电话:
if (path === "/incoming-call" && request.method === "POST") { const twimlResponse = ` <?xml version="1.0" encoding="UTF-8"?> <Response> <Say>O.K. you can start talking!</Say> <Connect> <Stream url="wss://call-my-agent.threepointone.workers.dev/agents/my-agent/123/media-stream" /> </Connect> </Response>`.trim(); return new Response(twimlResponse, { headers: { "Content-Type": "text/xml" } }); }对 TwiML 的拆解:
<Say>:让 Twilio 先用文本转语音(TTS)播报一句"O.K. you can start talking!",给来电者一个清晰的开始提示。<Connect><Stream url="wss://...">:这是核心指令。Twilio 会把通话双方的音频流转发到指定的 WebSocket 地址——即上面MyAgent.onConnect处理的media-stream端点。- 响应头
Content-Type: text/xml是 Twilio 识别 TwiML 所必需的。
注意:示例中的
wss://call-my-agent.threepointone.workers.dev/...是示例部署时的占位地址。实际使用时,应替换为你自己部署后的 Worker 域名。该 WebSocket 路径由 Agents 框架的路由约定生成,其my-agent与123分别对应 Agent 标识与连接名(与前端useAgent的agent: "my-agent"、name: "123"一一对应,见下节)。
当请求不是/incoming-call时,fetch会把请求交给routeAgentRequest处理,后者负责把/agents/...路径的 WebSocket / HTTP 请求路由到对应的 Agent Durable Object 实例;没有命中任何路由时返回 404:
return ( (await routeAgentRequest(request, env, { cors: true })) || new Response("Not found", { status: 404 }) );{ cors: true }开启跨域支持,使浏览器前端可以直接连接 Worker。
Twilio 侧配置(依据代码行为推断):要让来电真正触达/incoming-call,需要在 Twilio 控制台购买一个电话号码,并把该号码的"Voice → A Call Comes In"Webhook 指向https://你的Worker域名/incoming-call,方法选择POST。随后 Twilio 会按上述流程工作。
四、前端:通话状态与实时转录面板
src/client.tsx 实现了一个仿通话界面的 React 页面,通过agents/react的useAgent钩子订阅 Agent 状态:
useAgent<{ history: RealtimeItem[] }>({ agent: "my-agent", name: "123", onStateUpdate(newState) { setState(newState); if (newState.history && newState.history.length > 0) { setCallStatus("connected"); } } });agent/name与 TwiML 中 WebSocket URL 的my-agent/123保持一致,前端建立的连接会路由到同一个 Agent Durable Object 实例。onStateUpdate在服务端setState({ history })后被触发,前端据此刷新转录内容,并在一开始有历史记录时把通话状态切换为connected。
页面主体是一个"通话面板":头部显示"Live Call Transcription"标题、通话时长计时器(callDuration,每 1 秒递增)和状态指示灯(Connecting / Connected / Disconnected);中间区域按history渲染消息气泡——用户(role: "user")与助手(role: "assistant")分列两侧,每一条消息根据status显示✓(completed)/●(in_progress,同时展示打字指示动画);底部是静音、挂断、扬声器三个装饰性控制按钮。未接到来电时,界面显示"Waiting for call to begin..."的脉冲动画。
转录内容来自message.content?.[0]?.transcript,即 OpenAI Realtime 会话返回的文本转录字段;代码中为音频内容保留了audio字段(类型来自 OpenAI SDK 未导出的内部类型,以any标注并显式屏蔽了 lint 检查)。
五、部署配置:Wrangler、Durable Object 与迁移
wrangler.jsonc 定义了 Worker 的运行环境:
{ "compatibility_date": "2026-06-11", "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [ { "class_name": "MyAgent", "name": "MyAgent" } ] }, "main": "src/server.ts", "migrations": [ { "new_sqlite_classes": ["MyAgent"], "tag": "v1" } ], "name": "call-my-agent" }要点说明:
compatibility_flags: ["nodejs_compat"]:启用 Node.js 兼容层,@openai/agents-extensions与@openai/agents/realtime依赖的 Node 风格 API(如Buffer、events等)依赖该标志才能正常工作,属必选项。durable_objects绑定 +new_sqlite_classes迁移:MyAgent被声明为一个基于 SQLite 存储的 Durable Object 类。Agents 框架在此基础上提供 WebSocket 会话、状态同步(setState)能力;迁移标签v1是首次部署时必须执行的版本标记,后续新增类需追加新迁移条目。main: "src/server.ts":Worker 入口,导出的默认对象需满足ExportedHandler<Env>约束(源码中已用satisfies校验)。
Env类型由wrangler types自动生成(见 env.d.ts),其中MyAgent: DurableObjectNamespace<...>的类型绑定与 wrangler 配置一一对应。
六、本地开发与部署:一条命令从 dev 到上线
package.json 提供了三个脚本:
| 命令 | 脚本内容 | 用途 |
|---|---|---|
npm run start | vite dev | 本地开发,启动 Vite 开发服务器(含 Cloudflare 模拟运行时) |
npm run deploy | vite build && wrangler deploy | 构建前端资源并部署 Worker |
npm run types | wrangler types env.d.ts --include-runtime false | 依据 wrangler 配置重新生成Env类型 |
典型工作流:
- 安装依赖后,在环境变量(或
.dev.vars)中配置OPENAI_API_KEY; - 执行
npm run start启动本地开发环境,先在本地验证 Agent 与前端页面; - 在 Twilio 控制台把电话号码的 Webhook 指向本地隧道(如
wrangler dev输出的地址)或线上地址的/incoming-call; - 确认一切正常后执行
npm run deploy发布到 Cloudflare,并把 Twilio Webhook 更新为线上域名。
七、扩展方向与实现要点小结
从该示例出发,可以做如下扩展(均可在这个最小闭环上叠加):
- 自定义系统提示词与工具调用:
RealtimeAgent的instructions与工具配置决定了"接电话的 AI"的个性与能力,可替换为客服、导购、预约等场景话术; - 呼叫方识别与多会话隔离:Durable Object 以
name维度隔离实例,可在onConnect中根据呼叫方号码或会话 ID 动态路由到不同 Agent 实例; - 转录持久化:
history_updated中目前只是setState,可进一步写入 SQLite 或下游存储; - 状态机扩展:前端把
callStatus绑定到history非空这一简单判据,实际场景可结合 Twilio 回调事件(如呼叫结束)细化状态流转。
总结这个示例最核心的工程要点:
- 媒体流对接靠 WebSocket:TwiML 中
<Connect><Stream>指定 WebSocket 地址,Worker 侧由routeAgentRequest路由到 Durable Object; - 传输层解耦:
TwilioRealtimeTransportLayer屏蔽了 Twilio 媒体协议的细节,RealtimeSession只关心与 OpenAI Realtime API 的对话; - 前后端共享 Agent 状态:
setState+useAgent的组合让"通话"与"实时转录"天然同步; - 部署前提明确:
nodejs_compat兼容标志、Durable Object 迁移、OPENAI_API_KEY环境变量三者缺一不可。
参照 openai-sdk/call-my-agent 的完整源码,即可搭建出属于自己的"可以打电话的 AI Agent"。
【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考