给 AI Agent 打电话:基于 OpenAI Agents SDK 与 Twilio 的实时语音通话 Agent(call-my-agent 实战拆解)
2026/9/18 9:29:30 网站建设 项目流程

给 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配置

从代码结构看,整条链路由三个环节组成:

  1. 入站呼叫:Twilio 在接到来电时,向 Worker 的/incoming-call端点发送 POST 请求,Worker 返回一段 TwiML,要求 Twilio 将通话媒体流转发到一个 WebSocket 地址(.../agents/my-agent/123/media-stream)。
  2. 实时会话:WebSocket 连接被routeAgentRequest路由到MyAgent(一个 Durable Object)的onConnect,在media-stream路径下创建RealtimeAgentRealtimeSession,并通过TwilioRealtimeTransportLayer把电话音频与 OpenAI Realtime API 双向对接。
  3. 实时转录:会话的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 能理解的消息。
  • RealtimeSessionRealtimeAgent与传输层绑定在一起,形成一条"电话 ↔ 传输层 ↔ 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 })historyRealtimeItem[])写入 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-agent123分别对应 Agent 标识与连接名(与前端useAgentagent: "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/reactuseAgent钩子订阅 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(如Bufferevents等)依赖该标志才能正常工作,属必选项。
  • 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 startvite dev本地开发,启动 Vite 开发服务器(含 Cloudflare 模拟运行时)
npm run deployvite build && wrangler deploy构建前端资源并部署 Worker
npm run typeswrangler types env.d.ts --include-runtime false依据 wrangler 配置重新生成Env类型

典型工作流:

  1. 安装依赖后,在环境变量(或.dev.vars)中配置OPENAI_API_KEY
  2. 执行npm run start启动本地开发环境,先在本地验证 Agent 与前端页面;
  3. 在 Twilio 控制台把电话号码的 Webhook 指向本地隧道(如wrangler dev输出的地址)或线上地址的/incoming-call
  4. 确认一切正常后执行npm run deploy发布到 Cloudflare,并把 Twilio Webhook 更新为线上域名。

七、扩展方向与实现要点小结

从该示例出发,可以做如下扩展(均可在这个最小闭环上叠加):

  • 自定义系统提示词与工具调用RealtimeAgentinstructions与工具配置决定了"接电话的 AI"的个性与能力,可替换为客服、导购、预约等场景话术;
  • 呼叫方识别与多会话隔离:Durable Object 以name维度隔离实例,可在onConnect中根据呼叫方号码或会话 ID 动态路由到不同 Agent 实例;
  • 转录持久化history_updated中目前只是setState,可进一步写入 SQLite 或下游存储;
  • 状态机扩展:前端把callStatus绑定到history非空这一简单判据,实际场景可结合 Twilio 回调事件(如呼叫结束)细化状态流转。

总结这个示例最核心的工程要点:

  1. 媒体流对接靠 WebSocket:TwiML 中<Connect><Stream>指定 WebSocket 地址,Worker 侧由routeAgentRequest路由到 Durable Object;
  2. 传输层解耦TwilioRealtimeTransportLayer屏蔽了 Twilio 媒体协议的细节,RealtimeSession只关心与 OpenAI Realtime API 的对话;
  3. 前后端共享 Agent 状态setState+useAgent的组合让"通话"与"实时转录"天然同步;
  4. 部署前提明确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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询