跳转到内容
搜索文档

语音

最后更新 查看 MarkdownAgent 设置

使用语音转文本、文本转语音和对话持久化构建实时语音 Agent。音频通过 WebSocket 流式传输——无需 SFU 或会议基础设施。Beta

概览

@cloudflare/voice 提供两个服务端 mixin 及匹配的客户端库:

导出 导入 用途
withVoice @cloudflare/voice 完整语音 Agent:STT、LLM、TTS、持久化
withVoiceInput @cloudflare/voice 仅 STT:转录而不生成回复
useVoiceAgent @cloudflare/voice/react 用于 withVoice Agent 的 React hook
useVoiceInput @cloudflare/voice/react 用于 withVoiceInput Agent 的 React hook
VoiceClient @cloudflare/voice/client 与框架无关的客户端

基于 Cloudflare Durable Objects,你将获得:

  • 实时音频 — 麦克风音频以二进制 WebSocket 帧流式传输,TTS 音频流式返回
  • 自动对话持久化 — 消息存储在 SQLite 中,重启后仍然保留
  • 流式 TTS — LLM token 按句子分块并并发合成
  • 中断处理 — 播放期间的用户语音可取消当前回复
  • 连续 STT — 每次通话的转录会话,模型处理话轮检测
  • 管道钩子 — 在每个阶段拦截并转换文本

快速入门

安装

npm install @cloudflare/voice agents

服务端

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

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	async onTurn(transcript, context) {
		return "Hello! I heard you say: " + transcript;
	}
}
import { Agent } 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 "Hello! I heard you say: " + transcript;
	}
}

客户端(React)

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

function VoiceUI() {
	const {
		status,
		transcript,
		interimTranscript,
		audioLevel,
		isMuted,
		startCall,
		endCall,
		toggleMute,
	} = useVoiceAgent({ agent: "MyAgent" });

	return (
		<div>
			<p>Status: {status}</p>

			<button onClick={status === "idle" ? startCall : endCall}>
				{status === "idle" ? "Start Call" : "End Call"}
			</button>

			<button onClick={toggleMute}>{isMuted ? "Unmute" : "Mute"}</button>

			{interimTranscript && (
				<p>
					<em>{interimTranscript}</em>
				</p>
			)}

			{transcript.map((msg, i) => (
				<p key={i}>
					<strong>{msg.role}:</strong> {msg.text}
				</p>
			))}
		</div>
	);
}

Wrangler 配置

{
	"ai": {
		"binding": "AI"
	},
	"durable_objects": {
		"bindings": [
			{
				"name": "MyAgent",
				"class_name": "MyAgent"
			}
		]
	},
	"migrations": [
		{
			"tag": "v1",
			"new_sqlite_classes": ["MyAgent"]
		}
	]
}
[ai]
binding = "AI"

[[durable_objects.bindings]]
name = "MyAgent"
class_name = "MyAgent"

[[migrations]]
tag = "v1"
new_sqlite_classes = [ "MyAgent" ]

工作原理

Browser                              Durable Object (withVoice)
┌──────────┐                         ┌──────────────────────────┐
│ Mic      │   binary PCM (16kHz)    │ Transcriber session      │
│          │ ──────────────────────► │ (per-call, continuous)   │
│          │                         │   ↓ model detects turn   │
│          │   JSON: transcript      │ onTurn() → your LLM code │
│          │ ◄────────────────────── │   ↓ (sentence chunking)  │
│          │   binary: audio         │ TTS                      │
│ Speaker  │ ◄────────────────────── │                          │
└──────────┘                         └──────────────────────────┘
  1. 客户端捕获麦克风音频,以二进制 WebSocket 帧(16kHz 单声道 16 位 PCM)发送。
  2. 音频持续流式传输到 transcriber 会话(在 start_call 时创建,贯穿整个通话)。
  3. STT model 检测用户何时结束一次发言并触发 onUtterance。所有 provider 均使用 model 驱动的话轮检测——客户端无需为 STT 发送 end-of-speech 信号。
  4. 你的 onTurn() 方法运行——通常是 LLM 调用。
  5. 响应按句子分块并通过 TTS 合成。
  6. 音频流式返回客户端播放。

客户端在用户说话时收到带部分结果的 transcript_interim 消息,可在 UI 中显示实时反馈。

服务端 API:withVoice

withVoice(Agent) 为 Agent 类添加完整语音管道。

提供商

将 provider 设为类属性。类字段初始化在 super() 之后运行,因此 this.env 可用。

属性 类型 必需 描述
transcriber Transcriber 每次通话的连续 STT provider
tts TTSProvider 文本转语音
import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);
}
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);
}

要在运行时切换 model(例如 Flux 与 Nova 3 下拉菜单),可 override createTranscriber

export class MyAgent extends VoiceAgent {
	tts = new WorkersAITTS(this.env.AI);

	createTranscriber(connection) {
		return new WorkersAIFluxSTT(this.env.AI);
	}
}
export class MyAgent extends VoiceAgent<Env> {
	tts = new WorkersAITTS(this.env.AI);

	createTranscriber(connection: Connection): Transcriber {
		return new WorkersAIFluxSTT(this.env.AI);
	}
}

onTurn(transcript, context)

必需。 用户说完且 transcript 就绪时调用。

返回 stringAsyncIterable<string>ReadableStream 以流式响应。

简单响应:

export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	async onTurn(transcript, context) {
		return "You said: " + transcript;
	}
}
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;
	}
}

流式响应(LLM 推荐):

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

export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	async onTurn(transcript, context) {
		const workersai = createWorkersAI({ binding: this.env.AI });

		const result = streamText({
			model: workersai("@cf/moonshotai/kimi-k2.6"),
			system: "You are a helpful voice assistant. Keep responses concise.",
			messages: [
				...context.messages.map((m) => ({
					role: m.role,
					content: m.content,
				})),
				{ role: "user", content: transcript },
			],
			abortSignal: context.signal,
		});

		return result.textStream;
	}
}
import { streamText } from "ai";
import { createWorkersAI } from "workers-ai-provider";

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 workersai = createWorkersAI({ binding: this.env.AI });

		const result = streamText({
			model: workersai("@cf/moonshotai/kimi-k2.6"),
			system: "You are a helpful voice assistant. Keep responses concise.",
			messages: [
				...context.messages.map((m) => ({
					role: m.role as "user" | "assistant",
					content: m.content,
				})),
				{ role: "user", content: transcript },
			],
			abortSignal: context.signal,
		});

		return result.textStream;
	}
}

context 对象提供:

字段 类型 描述
connection Connection WebSocket 连接
messages Array<{ role: string; content: string }> 来自 SQLite 的对话历史
signal AbortSignal 中断或断开时 abort

生命周期钩子

方法 描述
beforeCallStart(connection) 返回 false 拒绝通话
onCallStart(connection) 通话被接受后调用
onCallEnd(connection) 通话结束时调用
onInterrupt(connection) 播放期间用户中断时调用

管道 hook

在各管道阶段 intercept 并 transform 数据。返回 null 跳过当前 utterance。

方法 接收 可跳过?
afterTranscribe(transcript, connection) STT 文本
beforeSynthesize(text, connection) TTS 前文本
afterSynthesize(audio, text, connection) TTS 后 audio
import {} from "agents";

export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	afterTranscribe(transcript, connection) {
		if (transcript.length < 3) return null;
		return transcript;
	}

	beforeSynthesize(text, connection) {
		return text.replace(/\bAI\b/g, "A.I.");
	}

	async onTurn(transcript, context) {
		return transcript;
	}
}
import { type Connection } from "agents";

export class MyAgent extends VoiceAgent<Env> {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	afterTranscribe(transcript: string, connection: Connection) {
		if (transcript.length < 3) return null;
		return transcript;
	}

	beforeSynthesize(text: string, connection: Connection) {
		return text.replace(/\bAI\b/g, "A.I.");
	}

	async onTurn(transcript: string, context: VoiceTurnContext) {
		return transcript;
	}
}

便捷方法

方法 描述
speak(connection, text) 向单个连接合成并发送 audio
speakAll(text) 向所有连接合成并发送 audio
forceEndCall(connection) 以编程方式结束通话
saveMessage(role, text) 将消息持久化到对话历史
getConversationHistory() 从 SQLite 检索对话历史

配置选项

将选项作为第二个参数传给 withVoice()

const VoiceAgent = withVoice(Agent, {
	historyLimit: 20,
	audioFormat: "mp3",
	maxMessageCount: 1000,
});
const VoiceAgent = withVoice(Agent, {
	historyLimit: 20,
	audioFormat: "mp3",
	maxMessageCount: 1000,
});
选项 类型 默认值 描述
historyLimit number 20 加载上下文的最大消息数
audioFormat string "mp3" 发送给客户端的 audio 格式
maxMessageCount number 1000 SQLite 中存储的最大消息数

服务端 API:withVoiceInput

withVoiceInput(Agent) 仅添加 STT 语音输入——无 TTS、无 LLM、无响应生成。适用于听写、语音搜索或任何只需 speech-to-text 而不需对话 Agent 的 UI。

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

const InputAgent = withVoiceInput(Agent);

export class DictationAgent extends InputAgent {
	transcriber = new WorkersAINova3STT(this.env.AI);

	onTranscript(text, connection) {
		console.log("User said:", text);
	}
}
import { Agent } 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);
	}
}

onTranscript(text, connection)

每次话语转录完成后调用。重写此方法以处理转写文本。

钩子(Hook)

withVoiceInput 支持与 withVoice 相同的生命周期钩子:

  • beforeCallStart(connection) — 返回 false 拒绝
  • onCallStart(connection)onCallEnd(connection)onInterrupt(connection)
  • createTranscriber(connection) — override 以在运行时切换 model
  • afterTranscribe(transcript, connection) — 过滤或 transform transcript

包含 TTS hook(beforeSynthesizeafterSynthesize)或 onTurn

客户端 API:React hook

useVoiceAgent

withVoice Agent 封装 VoiceClient。管理连接、麦克风采集、播放、静音检测与中断检测。

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

const selectedSpeakerId = "default";

const {
	status, // "idle" | "listening" | "thinking" | "speaking"
	transcript, // TranscriptMessage[] — conversation history
	interimTranscript, // string | null — real-time partial transcript
	metrics, // VoicePipelineMetrics | null
	audioLevel, // number (0–1) — current mic RMS level
	isMuted, // boolean
	connected, // boolean — WebSocket connected
	error, // string | null
	outputDeviceError, // string | null — non-fatal speaker routing issue
	startCall, // () => Promise<void>
	endCall, // () => void
	toggleMute, // () => void
	sendText, // (text: string) => void — bypass STT
	sendJSON, // (data: Record<string, unknown>) => void
	lastCustomMessage, // unknown — last non-voice message from server
} = useVoiceAgent({
	agent: "MyAgent",
	name: "default",
	host: window.location.host,
	outputDeviceId: selectedSpeakerId, // Optional audiooutput device ID
	enabled: true,
});

当应用必须等待异步连接前提(例如用户 scope 的 capability token)时,使用 enabled: false。禁用期间 hook 不会创建或连接 VoiceClient,返回 idle 断开 state,且 startCall()sendText()sendJSON() 等操作回调为安全 no-op。

enabled 变为 true 时,hook 以当前选项连接。首次 enable 视为初始连接,因此 onReconnect 仅在 hook 保持 enabled 时后续连接 identity 变化时触发。

输出设备选择

当浏览器支持 HTMLMediaElement.setSinkId() 时,传入 outputDeviceId 将 assistant 播放路由到所选扬声器:

const [outputDeviceId, setOutputDeviceId] = useState("default");

const voice = useVoiceAgent({
	agent: "MyAgent",
	outputDeviceId,
});
const [outputDeviceId, setOutputDeviceId] = useState("default");

const voice = useVoiceAgent({
	agent: "MyAgent",
	outputDeviceId,
});

使用 navigator.mediaDevices.enumerateDevices()kind === "audiooutput"MediaDeviceInfo.deviceId"default"undefined 使用系统默认输出。不支持 sink 选择的浏览器继续通过默认输出播放,请求非默认输出时设置 outputDeviceError。设备标签在用户授予麦克风权限前可能为空,若显示扬声器选择器,请在 startCall() 后刷新设备列表。

调优选项

选项 类型 默认值 描述
enabled boolean true 为 false 时延迟客户端创建与连接
silenceThreshold number 0.04 低于此 RMS 视为静音
silenceDurationMs number 500 end_of_speech 前的静音时长(ms)
interruptThreshold number 0.05 播放期间检测语音的 RMS
interruptChunks number 2 触发 interrupt 的连续高 RMS 块数

更改调优选项会触发客户端重连(连接 key 包含这些选项)。

useVoiceInput

用于听写与语音转文字的轻量 hook。将用户 transcript 累积为单个字符串。

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

function Dictation() {
	const {
		transcript, // string — accumulated text from all utterances
		interimTranscript, // string | null — current partial transcript
		isListening, // boolean
		audioLevel, // number (0–1)
		isMuted, // boolean
		error, // string | null
		start, // () => Promise<void>
		stop, // () => void
		toggleMute, // () => void
		clear, // () => void — clear accumulated transcript
	} = useVoiceInput({ agent: "DictationAgent" });

	return (
		<div>
			<textarea
				value={transcript + (interimTranscript ? " " + interimTranscript : "")}
				readOnly
			/>
			<button onClick={isListening ? stop : start}>
				{isListening ? "Stop" : "Dictate"}
			</button>
		</div>
	);
}

客户端 API:VoiceClient

适用于无 React 环境的与框架无关客户端。

import { VoiceClient } from "@cloudflare/voice/client";

const client = new VoiceClient({ agent: "MyAgent" });

client.addEventListener("statuschange", (status) => {
	console.log("Status:", status);
});

client.addEventListener("transcriptchange", (messages) => {
	console.log("Transcript:", messages);
});

client.addEventListener("error", (err) => {
	console.error("Error:", err);
});

client.connect();
await client.startCall();

// Switch assistant playback without reconnecting the call.
await client.setOutputDevice(selectedSpeakerId);

// Later:
client.endCall();
client.disconnect();
import { VoiceClient } from "@cloudflare/voice/client";

const client = new VoiceClient({ agent: "MyAgent" });

client.addEventListener("statuschange", (status) => {
	console.log("Status:", status);
});

client.addEventListener("transcriptchange", (messages) => {
	console.log("Transcript:", messages);
});

client.addEventListener("error", (err) => {
	console.error("Error:", err);
});

client.connect();
await client.startCall();

// Switch assistant playback without reconnecting the call.
await client.setOutputDevice(selectedSpeakerId);

// Later:
client.endCall();
client.disconnect();

事件

事件 数据类型 描述
statuschange VoiceStatus 管道 state 变化
transcriptchange TranscriptMessage[] Transcript 更新
interimtranscript string | null 流式 STT 的 interim transcript
metricschange VoicePipelineMetrics 管道计时指标
audiolevelchange number 麦克风 audio 电平(0–1)
connectionchange boolean WebSocket 连接/断开
mutechange boolean 静音 state 变化
error string | null 发生错误
outputdeviceerror string | null 非致命扬声器路由问题
custommessage unknown 来自服务端的非语音消息

高级选项

选项 类型 描述
transport VoiceTransport 自定义传输(默认:通过 PartySocket 的 WebSocket)
audioInput VoiceAudioInput 自定义麦克风采集(默认:内置 AudioWorklet)
preferredFormat VoiceAudioFormat 服务端 audio 格式提示(仅供参考)
outputDeviceId string assistant 播放的首选 audiooutput 设备

提供商

内置(Workers AI)

无需 API 密钥——使用 Workers AI 绑定(binding):

类型 默认 model 推荐用于
WorkersAIFluxSTT 连续 STT @cf/deepgram/flux withVoice
WorkersAINova3STT 连续 STT @cf/deepgram/nova-3 withVoiceInput
WorkersAITTS TTS @cf/deepgram/aura-1 两者
import { Agent } from "agents";
import {
	withVoice,
	WorkersAIFluxSTT,
	WorkersAINova3STT,
	WorkersAITTS,
} from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

// Default usage
export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);
}

// Custom options
export class CustomAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI, {
		eotThreshold: 0.8,
		keyterms: ["Cloudflare", "Workers"],
	});
	tts = new WorkersAITTS(this.env.AI, {
		model: "@cf/deepgram/aura-1",
		speaker: "asteria",
	});
}
import { Agent } from "agents";
import {
	withVoice,
	WorkersAIFluxSTT,
	WorkersAINova3STT,
	WorkersAITTS,
} from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

// Default usage
export class MyAgent extends VoiceAgent<Env> {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);
}

// Custom options
export class CustomAgent extends VoiceAgent<Env> {
	transcriber = new WorkersAIFluxSTT(this.env.AI, {
		eotThreshold: 0.8,
		keyterms: ["Cloudflare", "Workers"],
	});
	tts = new WorkersAITTS(this.env.AI, {
		model: "@cf/deepgram/aura-1",
		speaker: "asteria",
	});
}

第三方 provider

描述
@cloudflare/voice-deepgram DeepgramSTT 连续 STT
@cloudflare/voice-elevenlabs ElevenLabsTTS 高质量 TTS
@cloudflare/voice-twilio TwilioAdapter 电话

ElevenLabs TTS:

import { ElevenLabsTTS } from "@cloudflare/voice-elevenlabs";

export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new ElevenLabsTTS({
		apiKey: this.env.ELEVENLABS_API_KEY,
		voiceId: "21m00Tcm4TlvDq8ikWAM",
	});
}
import { ElevenLabsTTS } from "@cloudflare/voice-elevenlabs";

export class MyAgent extends VoiceAgent<Env> {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new ElevenLabsTTS({
		apiKey: this.env.ELEVENLABS_API_KEY,
		voiceId: "21m00Tcm4TlvDq8ikWAM",
	});
}

Deepgram STT:

import { DeepgramSTT } from "@cloudflare/voice-deepgram";

export class MyAgent extends VoiceAgent {
	transcriber = new DeepgramSTT({
		apiKey: this.env.DEEPGRAM_API_KEY,
	});
	tts = new WorkersAITTS(this.env.AI);
}
import { DeepgramSTT } from "@cloudflare/voice-deepgram";

export class MyAgent extends VoiceAgent<Env> {
	transcriber = new DeepgramSTT({
		apiKey: this.env.DEEPGRAM_API_KEY,
	});
	tts = new WorkersAITTS(this.env.AI);
}

电话(Twilio)

使用 Twilio adapter 将电话通话连接到 voice agent:

npm install @cloudflare/voice-twilio

Adapter 在 Twilio Media Streams 与 VoiceAgent 之间桥接:

Phone → Twilio → WebSocket → TwilioAdapter → WebSocket → VoiceAgent

WorkersAITTS 返回 MP3,无法在 Workers 运行时解码为 PCM。使用 Twilio adapter 时,请使用输出 raw PCM 的 TTS provider(例如 ElevenLabs 配合 outputFormat: "pcm_16000")。

文本消息

withVoice Agent 也可接收文本消息,完全绕过 STT。适用于与语音并存的聊天式输入。

const { sendText } = useVoiceAgent({ agent: "MyAgent" });

// Send text — goes straight to onTurn() without STT
sendText("What is the weather like today?");

文本消息在活跃通话期间与通话外均可用。通话期间,响应通过 TTS 朗读。通话外,响应以纯文本 transcript 消息发送。

自定义消息

在语音协议消息旁发送与接收应用级 JSON 消息。非语音消息传递到服务端 onMessage handler,并在客户端触发 custommessage 事件。

服务端:

export class MyAgent extends VoiceAgent {
	onMessage(connection, message) {
		const data = JSON.parse(message);
		if (data.type === "kick_speaker") {
			this.forceEndCall(connection);
		}
	}
}
export class MyAgent extends VoiceAgent<Env> {
	onMessage(connection: Connection, message: WSMessage) {
		const data = JSON.parse(message as string);
		if (data.type === "kick_speaker") {
			this.forceEndCall(connection);
		}
	}
}

客户端:

const { sendJSON, lastCustomMessage } = useVoiceAgent({ agent: "MyAgent" });

sendJSON({ type: "kick_speaker" });

useEffect(() => {
	if (lastCustomMessage) {
		console.log("Custom message:", lastCustomMessage);
	}
}, [lastCustomMessage]);

单说话人限制

使用 beforeCallStart 限制谁可以开始通话。本示例强制单说话人——同一时间仅一个连接可为活跃说话人:

import {} from "agents";

export class MyAgent extends VoiceAgent {
	#speakerId = null;

	beforeCallStart(connection) {
		if (this.#speakerId !== null) {
			return false;
		}
		this.#speakerId = connection.id;
		return true;
	}

	onCallEnd(connection) {
		if (this.#speakerId === connection.id) {
			this.#speakerId = null;
		}
	}
}
import { type Connection } from "agents";

export class MyAgent extends VoiceAgent<Env> {
	#speakerId: string | null = null;

	beforeCallStart(connection: Connection) {
		if (this.#speakerId !== null) {
			return false;
		}
		this.#speakerId = connection.id;
		return true;
	}

	onCallEnd(connection: Connection) {
		if (this.#speakerId === connection.id) {
			this.#speakerId = null;
		}
	}
}

管道指标

withVoice Agent 在每轮后发出计时指标:

const { metrics } = useVoiceAgent({ agent: "MyAgent" });

// metrics: {
//   llm_ms: 850,
//   tts_ms: 200,
//   first_audio_ms: 950,
//   total_ms: 1200,
// }

对话历史

withVoice 自动将对话消息持久化到 SQLite。在 onTurn 中通过 context.messages 访问历史,或直接访问:

const history = this.getConversationHistory(20);

this.saveMessage("assistant", "Welcome! How can I help?");
const history = this.getConversationHistory(20);

this.saveMessage("assistant", "Welcome! How can I help?");

历史在 Durable Object 重启与客户端重连后仍然保留。Voice Agent 使用 keepAlive 防止活跃通话期间被驱逐。

这篇文档对您有帮助吗?