← Vytvářejte agenty na Cloudflare / agents / examples
Hlasový agent
Sestavte hlasového agenta, který uživatele poslouchá, přemýšlí pomocí LLM a odpovídá hlasem, to vše v reálném čase přes WebSocket. Beta
Na konci této příručky budete mít:
- Hlasový agent na straně serveru s převodem řeči na text a textu na řeč
- Poháněné LLM
onTurnhandler, který streamuje odpovědi - Nástroje, které může agent volat během konverzace
- Klient React s UI ve stylu push-to-talk
Předpoklady
- Účet Cloudflare s Workers AI přístup
- Node.js 18+
1. Vytvořte projekt
Vytvořte nový projekt Workers pomocí Vite a React a poté přidejte závislosti pro hlas:
npm create cloudflare@latest voice-agent -- --template cloudflare/agents-starter
cd voice-agent
npm install @cloudflare/voiceStarter vám poskytne funkční nastavení Vite + React + Cloudflare Workers. Serverový a klientský kód nahradíte v následujících krocích.
2. Nakonfigurujte wrangler
Aktualizujte wrangler.jsonc k zahrnutí vazby Workers AI a Durable Object pro vašeho hlasového agenta:
{
"name": "voice-agent",
// Set this to today's date
"compatibility_date": "2026-08-28",
"compatibility_flags": ["nodejs_compat"],
"main": "src/server.ts",
"ai": {
"binding": "AI"
},
"durable_objects": {
"bindings": [
{
"name": "MyVoiceAgent",
"class_name": "MyVoiceAgent"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyVoiceAgent"]
}
]
}name = "voice-agent"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]
main = "src/server.ts"
[ai]
binding = "AI"
[[durable_objects.bindings]]
name = "MyVoiceAgent"
class_name = "MyVoiceAgent"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "MyVoiceAgent" ]3. Sestavte server
Nahraďte src/server.ts s následujícím. withVoice mixin přidává kompletní hlasový pipeline (STT, rozdělování na věty, TTS a perzistenci konverzace) do standardního Agent třídu.
import { Agent, routeAgentRequest } from "agents";
import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from "@cloudflare/voice";
import { streamText, tool, stepCountIs } from "ai";
import { createWorkersAI } from "workers-ai-provider";
import { z } from "zod";
const VoiceAgent = withVoice(Agent);
export class MyVoiceAgent 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 — you are being spoken aloud.",
messages: [
...context.messages.map((m) => ({
role: m.role,
content: m.content,
})),
{ role: "user", content: transcript },
],
tools: {
get_current_time: tool({
description: "Get the current date and time.",
inputSchema: z.object({}),
execute: async () => ({
time: new Date().toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
}),
}),
}),
},
stopWhen: stepCountIs(3),
abortSignal: context.signal,
});
return result.textStream;
}
async onCallStart(connection) {
await this.speak(connection, "Hi there! How can I help you today?");
}
}
export default {
async fetch(request, env) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
};import { Agent, routeAgentRequest, type Connection } from "agents";
import {
withVoice,
WorkersAIFluxSTT,
WorkersAITTS,
type VoiceTurnContext,
} from "@cloudflare/voice";
import { streamText, tool, stepCountIs } from "ai";
import { createWorkersAI } from "workers-ai-provider";
import { z } from "zod";
const VoiceAgent = withVoice(Agent);
export class MyVoiceAgent 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 — you are being spoken aloud.",
messages: [
...context.messages.map((m) => ({
role: m.role as "user" | "assistant",
content: m.content,
})),
{ role: "user" as const, content: transcript },
],
tools: {
get_current_time: tool({
description: "Get the current date and time.",
inputSchema: z.object({}),
execute: async () => ({
time: new Date().toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
}),
}),
}),
},
stopWhen: stepCountIs(3),
abortSignal: context.signal,
});
return result.textStream;
}
async onCallStart(connection: Connection) {
await this.speak(connection, "Hi there! How can I help you today?");
}
}
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;Klíčové body:
WorkersAIFluxSTTzajišťuje průběžný převod řeči na text. Model rozpozná, kdy uživatel dokončí mluvení.WorkersAITTSpřevádí odpověď LLM na zvuk, větu po větě.onTurnpřijímá přepis a vrací stream. Mixin se stará o rozdělení streamu na věty a syntézu každé z nich.onCallStartodešle pozdrav, jakmile se uživatel připojí.context.messagesobsahuje celou historii konverzace ze SQLite.context.signalse přeruší, pokud uživatel zasáhne nebo se odpojí.
4. Sestavte klienta
Nahraďte src/client.tsx s komponentou React pomocí useVoiceAgent hook. Tento hook spravuje připojení WebSocket, zachytávání mikrofonu, přehrávání zvuku a detekci přerušení.
import { useVoiceAgent } from "@cloudflare/voice/react";
function App() {
const {
status,
transcript,
interimTranscript,
metrics,
audioLevel,
isMuted,
startCall,
endCall,
toggleMute,
} = useVoiceAgent({ agent: "MyVoiceAgent" });
return (
<div>
<h1>Voice Agent</h1>
<p>Status: {status}</p>
<div>
<button onClick={status === "idle" ? startCall : endCall}>
{status === "idle" ? "Start Call" : "End Call"}
</button>
{status !== "idle" && (
<button onClick={toggleMute}>{isMuted ? "Unmute" : "Mute"}</button>
)}
</div>
{interimTranscript && (
<p>
<em>{interimTranscript}</em>
</p>
)}
{transcript.map((msg, i) => (
<p key={i}>
<strong>{msg.role}:</strong> {msg.text}
</p>
))}
{metrics && (
<p>
LLM: {metrics.llm_ms}ms | TTS: {metrics.tts_ms}ms | First audio:{" "}
{metrics.first_audio_ms}ms
</p>
)}
</div>
);
} status pole prochází "idle" → "listening" → "thinking" → "speaking" → "listening", takže máte vše potřebné k vytvoření responzivního UI.
5. Spusťte to
npm run devOtevřete aplikaci v prohlížeči a vyberte Zahájit hovor, a mluvte. Uvidíte, jak se v reálném čase objevuje přepis, a odpověď agenta se přehraje z vašich reproduktorů.
Přidání hooků pipeline
Data můžete zachytávat a transformovat v každé fázi pipeline. Můžete například odfiltrovat krátké přepisy (šum) a upravit výslovnost před TTS:
export class MyVoiceAgent 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 "You said: " + transcript;
}
}export class MyVoiceAgent 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 "You said: " + transcript;
}
}Vracení null z afterTranscribe promluvu zcela zahodí, což je užitečné pro filtrování šumu nebo velmi krátkých přepisů.
Používání poskytovatelů třetích stran
Zaměňte poskytovatele STT nebo TTS třetích stran, aniž byste museli měnit logiku agenta:
import { ElevenLabsTTS } from "@cloudflare/voice-elevenlabs";
import { DeepgramSTT } from "@cloudflare/voice-deepgram";
export class MyVoiceAgent extends VoiceAgent {
transcriber = new DeepgramSTT({
apiKey: this.env.DEEPGRAM_API_KEY,
});
tts = new ElevenLabsTTS({
apiKey: this.env.ELEVENLABS_API_KEY,
voiceId: "21m00Tcm4TlvDq8ikWAM",
});
async onTurn(transcript, context) {
return "You said: " + transcript;
}
}import { ElevenLabsTTS } from "@cloudflare/voice-elevenlabs";
import { DeepgramSTT } from "@cloudflare/voice-deepgram";
export class MyVoiceAgent extends VoiceAgent<Env> {
transcriber = new DeepgramSTT({
apiKey: this.env.DEEPGRAM_API_KEY,
});
tts = new ElevenLabsTTS({
apiKey: this.env.ELEVENLABS_API_KEY,
voiceId: "21m00Tcm4TlvDq8ikWAM",
});
async onTurn(transcript: string, context: VoiceTurnContext) {
return "You said: " + transcript;
}
}