给你的 agent 加上语音
原文:Add voice to your agent Source: https://blog.cloudflare.com/voice-agents/
2026-04-15
对我们许多人来说,初次接触 AI agent 都是通过聊天框输入文字。每天都在使用 agent 的人,大概也已经擅长写详细的 prompt 或 markdown 文件来指导它们。
但 agent 最有用的某些时刻并不总是文字优先。你可能在长途通勤中,正在多条会话之间切换,或就是想自然地对 agent 说话、让它回应你、并继续交互。
为 agent 加上语音不应需要把它搬进另一个语音框架。今天,我们为 Agents SDK 发布一个实验性语音管线。
通过 @cloudflare/voice,你可以为已经在用的同一 Agent 架构添加实时语音。语音只是又一种与同一个 Durable Object 对话的方式,使用 Agents SDK 已经提供的同一套工具、持久化与 WebSocket 连接模型。
@cloudflare/voice 是 Agents SDK 的一个实验性包,提供:
-
withVoice(Agent)用于完整对话语音 agent -
withVoiceInput(Agent)用于只需语音转文字的场景,比如听写或语音搜索 -
用于 React 应用的
useVoiceAgent与useVoiceInputhook -
与框架无关的
VoiceClient -
内置 Workers AI 提供方,无需外部 API key 即可上手:
-
使用 Deepgram Flux 的连续 STT
-
使用 Deepgram Nova 3 的连续 STT
-
使用 Deepgram Aura 的文字转语音
-
也就是说,你现在可以构建一个用户可通过单个 WebSocket 连接实时与之对话的 agent,同时保留同一个 Agent 类、同一个 Durable Object 实例,以及同一份基于 SQLite 的对话历史。
同样重要的是,我们希望它不止于一种固定默认栈。@cloudflare/voice 中的提供方接口刻意做小,我们希望语音、电话与传输提供方与我们共建,让开发者能为自己的用例混搭合适的组件,而不是被锁定在单一语音架构里。
上手语音
下面是 Agents SDK 中语音 agent 最小化的服务端模式:
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>;
这就是整个服务端。你添加一个连续 transcriber、一个文字转语音提供方,并实现 onTurn()。在客户端,你可以用 React hook 连接:
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>
{interimTranscript && <p><em>{interimTranscript}</em></p>}
<ul>
{transcript.map((msg, i) => (
<li key={i}>
<strong>{msg.role}:</strong> {msg.text}
</li>
))}
</ul>
<button onClick={startCall}>Start Call</button>
<button onClick={endCall}>End Call</button>
<button onClick={toggleMute}>Mute / Unmute</button>
</div>
);
}
如果你不用 React,可以直接从 @cloudflare/voice/client 使用 VoiceClient。
语音管线如何工作
借助 Agents SDK,每个 agent 都是一个 Durable Object — 一个有状态、可寻址的服务器实例,自带 SQLite 数据库、WebSocket 连接 与应用逻辑。语音管线是对该模型的扩展而非替换。
总体上,流程长这样:
下面逐步分解:
-
音频传输: 浏览器捕获麦克风音频,通过 agent 已经在用的同一个 WebSocket 连接以 16 kHz 单声道 PCM 流式传输。
-
STT 会话建立: 通话开始时,agent 创建一个持续整通通话期的连续 transcriber 会话。
-
STT 输入: 音频持续流入该会话。
-
STT 转向检测: 语音转文字模型自身决定用户何时结束一段话语,并为该轮发出稳定的转写。
-
LLM/应用逻辑: 语音管线把该转写传给你的
onTurn()方法。 -
TTS 输出: 你的回复被合成为音频并发回客户端。如果
onTurn()返回流,管线按句切块,并在句子准备好时开始发送音频。 -
持久化: 用户与 agent 的消息持久化到 SQLite,对话历史在重连与部署后依然保留。
为什么语音应该与 agent 的其他部分一起成长
许多语音框架专注语音回路本身:音频进、转写、模型回复、音频出。这些是重要的原语,但 agent 远不止语音。
实际生产中的 agent 会成长。它们需要状态、调度、持久化、工具、工作流、电话以及在多通道间保持一致。当 agent 复杂度上升时,语音不再是独立功能,而成为更大系统的一部分。
我们希望 Agents SDK 中的语音从这个假设出发。我们没有把语音作为独立栈构建,而是构建在同一个基于 Durable Object 的 agent 平台之上,这样你可以引入需要的其他原语,而无须事后重构应用。
语音与文字共享同一状态
用户可能从打字开始,切到语音,再切回文字。在 Agents SDK 中,这些只是同一个 agent 的不同输入。同一份对话历史存在 SQLite 中,同一套工具可用。这给你更清晰的心智模型,也让应用架构更简单。
更低的延迟来自……
更短的网络路径
语音体验的好坏很快就能感知。用户停止说话后,系统需要尽快转写、思考并开口回话,才能感觉是对话。
很多语音延迟并非纯模型时间,而是在不同地方的不同服务间来回搬运音频与文字的成本。音频要去 STT,转写要去 LLM,响应要去 TTS — 每一次交接都带来网络开销。
借助 Agents SDK 语音管线,agent 在 Cloudflare 网络上运行,内置提供方使用 Workers AI 绑定。这让管线更紧凑,也减少了你自己拼接基础设施的工作量。
内置流式
语音 agent 交互如果能很快说出第一句(也称首音时延 Time-to-First Audio)会自然得多。当 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()。在通话进行中,响应可以被开口同时显示为文字。通话之外,它可以保持纯文字。
这给你一个真正的多模态 agent,无需把实现拆成不同代码路径。
你还能构建什么?
由于语音 agent 仍然是 agent,所有正常的 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?");
}
}
你可以像其他任何 agent 一样调度语音提醒并向 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);
}
}
在客户端,你可以通过 hook 传入查询参数:
const voiceAgent = useVoiceAgent({
agent: "my-voice-agent",
query: { model: "nova-3" }
});
管线钩子
你也可以在阶段之间拦截数据:
-
afterTranscribe(transcript, connection) -
beforeSynthesize(text, connection) -
afterSynthesize(audio, text, connection)
这些钩子用于内容过滤、文本规范化、特定语言的转换或自定义日志。
电话与传输选项
默认情况下,语音管线使用单条 WebSocket 连接,作为 1:1 语音 agent 最简单的路径。但这并非唯一选项。
通过 Twilio 接通电话
你可以使用 Twilio 适配器把电话呼叫连到同一个 agent:
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 })
);
}
};
这让同一个 agent 同时处理 Web 语音、文字输入与电话呼叫。
一个注意点:默认 Workers AI TTS 提供方返回 MP3,而 Twilio 期望 mulaw 8kHz 音频。对于生产电话,你可能需要使用直接输出 PCM 或 mulaw 的 TTS 提供方。
WebRTC
如果你需要更适应困难网络条件或需要多方参与的传输,语音包还包含 SFU 工具并支持自定义传输。今天默认模型是 WebSocket 原生,但我们计划开发更多适配器接入我们的 全球 SFU 基础设施。
与我们共建
语音管线设计上是与提供方无关的。
底层每个阶段由一个小接口定义:transcriber 打开一个连续会话并在音频帧到达时接受它们,而 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>;
}
我们不希望 Agents SDK 中的语音支持只能配某一种固定的模型与传输组合。我们希望默认路径简单,同时让接入其他提供方也容易,让生态自然成长。
内置提供方使用 Workers AI,无需外部 API key 即可上手:
-
WorkersAIFluxSTT用于对话流式 STT -
WorkersAINova3STT用于听写式流式 STT -
WorkersAITTS用于文字转语音
但更大的目标是互操作。如果你维护语音或语音服务,这些接口足够小,无需理解 SDK 内部其余部分即可实现。如果你的 STT 提供方接受流式音频并能检测话语边界,就能满足 transcriber 接口。如果你的 TTS 提供方能流式输出音频,那就更好。
我们非常愿意与以下方共同推进互操作:
-
STT 提供方,如 AssemblyAI、Rev.ai、Speechmatics 或任何具实时转写 API 的服务
-
TTS 提供方,如 PlayHT、LMNT、Cartesia、Coqui、Amazon Polly 或 Google Cloud TTS
-
面向 Vonage、Telnyx、Bandwidth 等平台的电话适配器
-
用于 WebRTC data channel、SFU bridge 与其他音频传输层的传输实现
我们也对超越单个提供方的合作感兴趣:
-
跨 STT + LLM + TTS 组合的延迟基准
-
多语言支持以及对非英语语音 agent 的更好文档
-
无障碍工作,尤其是围绕多模态界面与言语障碍
如果你正在构建语音基础设施并希望获得一流集成,提交 PR 或与我们联系。
现在就试试
语音管线今天作为实验性包提供:
npm create cloudflare@latest -- --template cloudflare/agents-starter
加上 @cloudflare/voice,给你的 agent 一个 transcriber 与一个 TTS 提供方,部署它,然后开始与之对话。你也可以阅读 API 参考。
如果你构建了有趣的东西,在 github.com/cloudflare/agents 上提个 issue 或 PR。语音不应需要单独的栈,我们认为最好的语音 agent 将是构建在与其他一切相同的持久应用模型之上的那些。