Cloudflare为Agents SDK新增语音功能,支持实时交互

Cloudflare AI··作者 Korinne Alpers

关键信息

语音管道利用了Agents SDK原有的Durable Object模型、SQLite后端的历史记录和WebSocket连接;内置Workers AI提供商(如Deepgram Flux用于语音识别,Aura用于语音合成),并设计为可扩展,支持自定义提供商。

资讯摘要

Cloudflare为其Agents SDK引入了一个实验性的语音管道,允许通过与文本提示相同的代理架构实现实时语音交互。开发者可以使用`withVoice()`函数或`useVoiceAgent()`钩子添加语音功能,并从内置的语音识别(STT)和语音合成(TTS)提供商(如Deepgram Flux和Aura)中选择。该系统通过Durable Object和SQLite保持持久状态,确保在单个WebSocket连接上无缝延续对话。

这种方法避免了需要单独的语音框架,同时支持完整对话和单向语音输入两种场景。Cloudflare强调模块化设计,将提供者接口设计得简洁且可插拔,让开发者可根据需求自由组合组件。

Cloudflare为Agents SDK新增语音功能,支持实时交互

资讯正文

为你的代理添加语音功能

对我们许多人来说,初次接触AI代理的方式都是通过在聊天框中输入文字。对于那些每天使用代理的人来说,我们可能已经非常擅长编写详细的提示词或Markdown文件来引导它们。

但有些场景下,代理最能发挥作用的时候并不总是以文本为主。你可能正在长途通勤中,同时处理多个开放会话,或者只是想自然地与代理对话,让它回应你,并继续交互。

为代理添加语音功能不应要求将其迁移到独立的语音框架中。今天,我们发布了面向Agents SDK的实验性语音管道。

借助@cloudflare/voice,你可以将实时语音集成到你已有的代理架构中。语音只是另一种与同一Durable Object通信的方式,和你之前使用的工具、持久化存储以及WebSocket连接模型完全一致。

@cloudflare/voice是一个针对Agents SDK的实验性包,提供以下功能:

withVoice(Agent)——用于构建完整的语音对话代理

withVoiceInput(Agent)——适用于仅需语音转文字的场景,比如语音输入或语音搜索

useVoiceAgent和useVoiceInput——适用于React应用的钩子

VoiceClient——适用于无框架依赖的客户端

内置Workers AI提供商,让你无需外部API密钥即可快速开始:

连续语音识别(STT)支持Deepgram Flux

连续语音识别(STT)支持Deepgram Nova 3

文本转语音(TTS)支持Deepgram Aura

这意味着你现在可以构建一个用户可以通过单一WebSocket连接实时对话的代理,同时保持相同的代理类、Durable Object实例和基于SQLite的对话历史记录。

更重要的是,我们希望这个方案不止于一套固定的默认技术栈。@cloudflare/voice中的提供者接口设计得非常简洁,我们期待语音、电话和传输服务提供商与我们一起协作,让开发者可以根据自身需求灵活组合合适的组件,而不是被锁定在一个单一的语音架构中。

开始使用语音功能

以下是Agents SDK中语音代理的最小服务器端模式:

import { Agent, routeAgentRequest } from "agents";

import {

withVoice,

WorkersAIFluxSTT,

WorkersAITTS,

type VoiceTurnContext

} from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent<Env> {

transcriber = new WorkersAIFluxSTT(this.env.AI);

tts = new WorkersAITTS(this.env.AI);

async onTurn(transcript: string, context: VoiceTurnContext) {

return `You said: ${transcript}`;

}

}

export default {

async fetch(request: Request, env: Env) {

return (

(await routeAgentRequest(request, env)) ??

new Response("Not found", { status: 404 })

);

}

} satisfies ExportedHandler<Env>;

这就是整个服务器端代码。你只需添加一个持续语音转文字处理器、一个文本转语音提供者,并实现onTurn()方法。

在客户端,你可以通过React钩子连接到它:

import { useVoiceAgent } from "@cloudflare/voice/react";

function App() {

const {

status,

transcript,

interimTranscript,

startCall,

endCall,

toggleMute

} = useVoiceAgent({ agent: "my-agent" });

return (

<div>

<p>Status: {status}</p>

为你的代理添加语音功能

如果未使用 React,你可以直接从 @cloudflare/voice/client 使用 VoiceClient。

语音处理流程的工作原理

使用 Agents SDK,每个代理都是一个 Durable Object——一种具有独立 SQLite 数据库、WebSocket 连接和应用逻辑的状态化、可寻址服务器实例。语音流程扩展了这一模型,而非取代它。

整体流程如下:

以下是该流程的逐步分解:

音频传输:浏览器捕获麦克风音频,并通过代理已使用的同一 WebSocket 连接以 16 kHz 单声道 PCM 格式流式传输。

语音转文字(STT)会话设置:通话开始时,代理创建一个持续的转录会话,该会话在整个通话期间保持活跃。

STT 输入:音频持续流入该会话。

STT 转换检测:语音转文字模型自身判断用户是否完成了一次发言,并为该轮对话输出稳定的结果文本。

大语言模型(LLM)/应用逻辑:语音流程将此文本传递给你的 onTurn() 方法。

文本转语音(TTS)输出:你的回复被合成语音并发送回客户端。如果 onTurn() 返回的是流,则流程会按句子分块,并在句子准备好后立即开始发送音频。

持久化存储:用户和代理的消息会被保存到 SQLite 中,因此即使重新连接或部署,对话历史也能保留。

为什么语音应与代理其他部分一同发展

许多语音框架专注于语音循环本身:输入音频、转录、模型响应、输出音频。这些是重要的基础组件,但代理远不止语音功能。

实际运行中的代理会不断成长,它们需要状态管理、调度、持久化、工具、工作流、电话功能,以及跨渠道保持一致性的方法。随着代理变得越来越复杂,语音不再是一个孤立的功能,而成为更大系统的一部分。

我们希望在 Agents SDK 中实现语音功能时,就基于这个前提。与其把语音单独作为一个堆栈构建,我们选择基于相同的基于 Durable Object 的代理平台来实现,这样你就不必后期重构整个应用就能获取所需的所有基础功能。

语音与文本共享相同的状态

用户可能先用文字输入,切换到语音,然后再回到文字。在 Agents SDK 中,这些只是同一个代理的不同输入方式。相同的对话历史记录保存在 SQLite 中,可用工具也完全一致。这为你提供了更清晰的思维模型,也让应用程序架构更加简单易懂。

更低延迟来自更短的网络路径

语音体验的好坏会在很短时间内体现出来。一旦用户停止说话,系统必须快速完成转录、思考并开始回应,才能让人感觉像自然对话。

为你的代理添加语音功能

许多语音延迟并非来自模型本身的处理时间,而是音频和文本在不同服务之间传输所付出的代价。音频需要送往语音转文字(STT)服务,转录结果再交给大语言模型(LLM),最后响应内容还要通过文本转语音(TTS)模型输出——每一次交接都会增加网络开销。

借助 Agents SDK 的语音处理管道,代理运行在 Cloudflare 的网络上,内置的服务提供商使用 Workers AI 绑定。这使得整个流程更加紧凑,减少了你需要自行搭建的基础设施数量。

内置流式处理

如果语音代理能在第一时间快速说出第一句话(也称为首次音频响应时间),交互体验会显得更自然。当 onTurn() 返回一个流时,管道会将其切分为句子,并在每个句子完成后立即开始合成语音。这意味着用户可以在剩余内容仍在生成时,就听到答案的开头部分。

更真实的后端实现

以下是一个完整的示例,它会流式输出 LLM 的响应,并逐句开始播放语音:

import { Agent, routeAgentRequest } from "agents";

import {

withVoice,

WorkersAIFluxSTT,

WorkersAITTS,

type VoiceTurnContext

} from "@cloudflare/voice";

import { streamText } from "ai";

import { createWorkersAI } from "workers-ai-provider";

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent<Env> {

transcriber = new WorkersAIFluxSTT(this.env.AI);

tts = new WorkersAITTS(this.env.AI);

async onTurn(transcript: string, context: VoiceTurnContext) {

const ai = createWorkersAI({ binding: this.env.AI });

const result = streamText({

model: ai("@cf/cloudflare/gpt-oss-20b"),

system: "You are a helpful voice assistant. Be concise.",

messages: [

...context.messages.map((m) => ({

role: m.role as "user" | "assistant",

content: m.content

}))

{ role: "user" as const, content: transcript }

abortSignal: context.signal

});

return result.textStream;

}

}

export default {

async fetch(request: Request, env: Env) {

return (

(await routeAgentRequest(request, env)) ??

new Response("Not found", { status: 404 })

);

}

} satisfies ExportedHandler<Env>;

context.messages 提供最近的 SQLite 支持的对话历史记录,而 context.signal 允许在用户打断时中止 LLM 调用。

语音作为输入:withVoiceInput

并非所有语音接口都需要回音回应。有时你可能希望实现语音输入、转录或语音搜索。针对这些场景,可以使用 withVoiceInput。

import { Agent, type Connection } from "agents";

import { withVoiceInput, WorkersAINova3STT } from "@cloudflare/voice";

const InputAgent = withVoiceInput(Agent);

export class DictationAgent extends InputAgent<Env> {

transcriber = new WorkersAINova3STT(this.env.AI);

onTranscript(text: string, _connection: Connection) {

console.log("User said:", text);

}

}

在客户端,useVoiceInput 提供了一个以转录为核心的轻量级界面:

import { useVoiceInput } from "@cloudflare/voice/react";

const { transcript, interimTranscript, isListening, start, stop, clear } =

useVoiceInput({ agent: "DictationAgent" });

这种模式适用于语音仅作为输入方式的场景,无需构建完整的对话循环。

语音与文本在同一连接中

同一个客户端可以发起调用

sendText("What’s the weather?")

这会绕过语音转文字(STT)步骤,直接将文本发送到

onTurn()

。在通话过程中,响应既可以被语音播报,也可以以文本形式显示;而在非通话状态下,则可以仅以文本形式呈现。

这样你就能构建一个真正的多模态代理,而无需将实现拆分为不同的代码路径。

你还能构建什么?

因为语音代理仍然是一个代理,所以所有标准的 Agents SDK 功能依然适用。

工具和调度功能

你可以在会话开始时向来电者打招呼:

import { Agent, type Connection } from "agents";

import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent<Env> {

transcriber = new WorkersAIFluxSTT(this.env.AI);

tts = new WorkersAITTS(this.env.AI);

async onTurn(transcript: string) {

return `You said: ${transcript}`;

}

async onCallStart(connection: Connection) {

await this.speak(connection, "Hi! How can I help you today?");

}

}

你还可以像其他代理一样安排语音提醒,并向你的大语言模型(LLM)暴露工具:

import { Agent } from "agents";

import {

withVoice,

WorkersAIFluxSTT,

WorkersAITTS,

type VoiceTurnContext

} from "@cloudflare/voice";

import { streamText, tool } from "ai";

import { createWorkersAI } from "workers-ai-provider";

import { z } from "zod";

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent<Env> {

transcriber = new WorkersAIFluxSTT(this.env.AI);

tts = new WorkersAITTS(this.env.AI);

async speakReminder(payload: { message: string }) {

await this.speakAll(`Reminder: ${payload.message}`);

}

async onTurn(transcript: string, context: VoiceTurnContext) {

const ai = createWorkersAI({ binding: this.env.AI });

const result = streamText({

model: ai("@cf/cloudflare/gpt-oss-20b"),

messages: [

...context.messages.map((m) => ({

role: m.role as "user" | "assistant",

content: m.content

}))

,{ role: "user" as const, content: transcript }

tools: {

set_reminder: tool({

description: "Set a spoken reminder after a delay",

inputSchema: z.object({

message: z.string(),

delay_seconds: z.number()

}),

execute: async ({ message, delay_seconds }) => {

await this.schedule(delay_seconds, "speakReminder", { message });

return { confirmed: true };

}

})

},

abortSignal: context.signal

});

return result.textStream;

}

}

运行时模型切换

语音处理流程还允许你根据每个连接动态选择转录模型。

例如,你可能希望在对话轮次中使用 Flux 模型,在需要高精度录入时使用 Nova 3 模型。你可以通过重写

createTranscriber()

来实现在运行时切换:

import { Agent, type Connection } from "agents";

import {

withVoice,

WorkersAIFluxSTT,

WorkersAINova3STT,

WorkersAITTS,

type Transcriber

} from "@cloudflare/voice";

export class MyAgent extends VoiceAgent<Env> {

tts = new WorkersAITTS(this.env.AI);

createTranscriber(connection: Connection): Transcriber {

const url = new URL(connection.url ?? "http://localhost");

const model = url.searchParams.get("model");

if (model === "nova-3") {

return new WorkersAINova3STT(this.env.AI);

}

return new WorkersAIFluxSTT(this.env.AI);

}

}

在客户端,你可以通过钩子传递查询参数:

const voiceAgent = useVoiceAgent({

agent: "my-voice-agent",

query: { model: "nova-3" }

});

管道钩子

你还可以在各个阶段之间拦截数据:

afterTranscribe(transcript, connection)

beforeSynthesize(text, connection)

afterSynthesize(audio, text, connection)

这些钩子适用于内容过滤、文本标准化、语言特定转换或自定义日志记录。

电话和传输选项

默认情况下,语音管道使用单一 WebSocket 连接作为一对一语音代理的最简单路径。但这并不是唯一选择。

通过 Twilio 的电话呼叫

你可以使用 Twilio 适配器将电话呼叫连接到同一个代理:

import { TwilioAdapter } from "@cloudflare/voice-twilio";

export default {

async fetch(request: Request, env: Env) {

if (new URL(request.url).pathname === "/twilio") {

return TwilioAdapter.handleRequest(request, env, "MyAgent");

}

return (

(await routeAgentRequest(request, env)) ??

new Response("Not found", { status: 404 })

);

}

};

这使得同一个代理可以处理网络语音、文本输入和电话呼叫。

一个需要注意的地方是:默认的 Workers AI 文本转语音(TTS)提供者返回 MP3 格式音频,而 Twilio 要求的是 mulaw 8kHz 音频。对于生产级电话通信,你可能希望使用直接输出 PCM 或 mulaw 格式的 TTS 提供者。

WebRTC

如果你需要一种更适合复杂网络条件或包含多个参与者的传输方式,语音包还包含 SFU 工具并支持自定义传输。目前默认模型是基于 WebSocket 的,但我们计划开发更多适配器以连接到我们的全球 SFU 基础设施。

与我们一起构建

语音管道从设计上就是供应商无关的。

底层实现中,每个阶段都由一个小型接口定义:转录器开启一个持续会话,并接收逐帧到达的音频;而 TTS 提供者接收文本并返回音频。如果某个提供者能够流式输出音频,该管道也可以利用这一点。

interface Transcriber {

createSession(options?: TranscriberSessionOptions): TranscriberSession;

}

interface TranscriberSession {

feed(chunk: ArrayBuffer): void;

close(): void;

}

interface TTSProvider {

synthesize(text: string, signal?: AbortSignal): Promise<ArrayBuffer | null>;

}

我们并不希望 Agent SDK 中的语音支持仅限于固定的一组模型和传输方式。我们希望默认路径足够简单,同时也能轻松接入其他提供者,随着生态系统的发展。

内置提供者使用 Workers AI,因此你无需外部 API 密钥即可开始使用:

WorkersAIFluxSTT

用于对话式流式语音识别(STT)

WorkersAINova3STT

用于逐字记录风格的流式语音识别(STT)

WorkersAITTS

用于文本转语音

但更大的目标是互操作性。如果你维护一个语音服务,这些接口足够小,无需了解SDK的其他内部机制即可实现。如果你的语音转文字(STT)提供商支持流式音频并能检测话语边界,它就能满足转录器接口的要求。如果你的文本转语音(TTS)提供商能够输出流式音频,那就更好了。

我们希望与以下各方合作实现互操作性:

- STT提供商,例如AssemblyAI、Rev.ai、Speechmatics,或任何提供实时转录API的服务

- TTS提供商,例如PlayHT、LMNT、Cartesia、Coqui、Amazon Polly或Google Cloud TTS

- 针对Vonage、Telnyx或Bandwidth等平台的电话适配器

- WebRTC数据通道、SFU桥接器及其他音频传输层的传输实现

我们也对超越单一提供商的合作感兴趣:

- 在STT + 大语言模型(LLM)+ TTS组合中的延迟基准测试

- 多语言支持及非英语语音代理的更好文档

- 可访问性工作,特别是围绕多模态界面和言语障碍者的支持

如果你正在构建语音基础设施,并希望看到一流的集成,请:

- 提交一个Pull Request(PR)

- 或直接联系我们。

现在就可以尝试

语音处理管道目前已作为实验性包提供:

npm create cloudflare@latest -- --template cloudflare/agents-starter

添加 @cloudflare/voice,为你的代理配置一个转录器和一个TTS提供商,部署后即可开始与之对话。你也可以阅读 API参考文档。

如果你构建了有趣的东西,请在 github.com/cloudflare/agents 上提交问题或PR。语音功能不应需要单独的堆栈,我们认为最好的语音代理将基于与其它应用相同的持久化应用模型构建。

来源与参考

  1. 原始链接
  2. Add voice to your agent

收录于 2026-04-16