Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

ナレッジベースに話しかける

最終更新 Markdown で表示Agent セットアップ

このチュートリアルでは、話しかけられる音声エージェントを作ります。エージェントは AI Search のナレッジベースから声で答えます。Cloudflare Agents@cloudflare/voice パッケージで音声パイプラインを構成し、AI Search をエージェントのナレッジベースとして公開します。モデルが呼び出す検索ツールです。

作るもの: 発話を文字起こしし、AI Search でインデックス済みナレッジベースから関連コンテンツを取得し、根拠のある回答を生成して、声で返す音声エージェントです。

仕組み

@cloudflare/voice パッケージは、Cloudflare Agent に音声パイプライン一式を追加します。speech-to-text (STT)、応答を作る「ターン」ハンドラー、text-to-speech (TTS) です。パイプラインは Durable Object を裏にした 1 つの Worker で動き、ブラウザーは WebSocket で接続します。

書くメソッドは onTurn() だけです。ユーザーの文字起こしを受け取り、読み上げるテキストを返します。ここに AI Search を組み込みます。言語モデルを実行し、AI Search を検索ツールとして渡します。モデルはナレッジベースを検索するタイミングを決め、返ってきた結果に基づいて応答を作り、回答テキストを返します。パイプラインがそのテキストを読み上げます。

ブラウザマイク
Workers AI音声認識
Cloudflare AgentsonTurn(transcript)
Workers AI音声合成
ブラウザスピーカー

前提条件

  1. Cloudflare アカウント に登録します。
  2. Node.js をインストールします。

Node.js のバージョンマネージャー

権限の問題を避け、Node.js のバージョンを切り替えられるよう、Voltanvm などの Node バージョンマネージャーを使います。このガイドの後半で説明する Wrangler には、Node バージョン 16.17.0 以降が必要です。

インデックス済みコンテンツを含む AI Search インスタンスも必要です。これが話しかけるナレッジベースになります。作成とコンテンツ追加は はじめに を参照してください。

1. 音声エージェントを作成する

Durable Object の配線と React クライアントを含む、音声スターターテンプレートで Cloudflare Agents プロジェクトを作成します。

npm create cloudflare@latest voice-knowledge-base -- --template cloudflare/agents-starter
cd voice-knowledge-base

音声パッケージをインストールします。

npm i @cloudflare/voice

@cloudflare/voice パッケージは withVoice mixin と、Workers AI プロバイダー(WorkersAIFluxSTTWorkersAITTS)を提供します。ブラウザークライアントを含む音声エージェント本体の詳細は、音声エージェントの例 を参照してください。

2. AI Search バインディングを追加する

Wrangler 設定ファイルAI Search バインディング を追加します。Workers AI バインディングとエージェントの Durable Object と並べます。my-instance はインスタンス名に置き換えます。

{
	"name": "voice-knowledge-base",
	"main": "src/server.ts",
	// Set this to today's date
	"compatibility_date": "2026-09-20",
	"compatibility_flags": ["nodejs_compat"],
	"ai": {
		"binding": "AI",
		"remote": true
	},
	"ai_search": [
		{
			"binding": "AI_SEARCH",
			"instance_name": "my-instance",
			"remote": true
		}
	],
	"durable_objects": {
		"bindings": [
			{
				"name": "TalkToDocs",
				"class_name": "TalkToDocs"
			}
		]
	},
	"migrations": [
		{
			"tag": "v1",
			"new_sqlite_classes": ["TalkToDocs"]
		}
	]
}
name = "voice-knowledge-base"
main = "src/server.ts"
# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = [ "nodejs_compat" ]

[ai]
binding = "AI"
remote = true

[[ai_search]]
binding = "AI_SEARCH"
instance_name = "my-instance"
remote = true

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

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

env.AIenv.AI_SEARCH が型付けされるよう、バインディング型を再生成します。

npx wrangler types

3. ナレッジベースから回答する

src/server.ts を更新します。withVoice mixin でエージェントを組み立て、STT と TTS のプロバイダーを設定し、onTurn() で Workers AI モデルを実行します。モデルは AI Search を検索ツールとして呼び出します。

モデルには、AI Search の search() を呼び出して検索する searchKnowledgeBase ツールを渡します。ナレッジベースの事実が必要なときにツールを呼び、返ってきたチャンクに基づいて回答し、生成したテキストをパイプラインが読み上げるよう返します。エージェントは会話履歴を自動で保存するため、フォローアップの質問には context.messages を渡せます。

src/server.jsjs
import { Agent, routeAgentRequest } from "agents";
import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from "@cloudflare/voice";
import { generateText, tool, stepCountIs } from "ai";
import { createWorkersAI } from "workers-ai-provider";
import { z } from "zod";

const VoiceAgent = withVoice(Agent);

export class TalkToDocs extends VoiceAgent {
	// Workers AI powers speech-to-text and text-to-speech (no API keys needed).
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	// Called each time the user finishes speaking. The agent's model generates
	// the reply, calling AI Search as a retrieval tool when it needs facts from
	// your knowledge base.
	async onTurn(transcript, context) {
		const workersai = createWorkersAI({ binding: this.env.AI });

		const result = await generateText({
			// Use a Workers AI model that supports function calling.
			model: workersai("@cf/zai-org/glm-5.2"),
			system:
				"You are a helpful voice assistant that answers from a Cloudflare AI Search knowledge base. " +
				"For questions about the product, call the searchKnowledgeBase tool first and answer using the results. " +
				"Skip the tool for greetings and small talk. Keep replies short and conversational.",
			messages: [
				...context.messages.map((message) => ({
					role: message.role,
					content: message.content,
				})),
				{ role: "user", content: transcript },
			],
			tools: {
				searchKnowledgeBase: tool({
					description:
						"Search the knowledge base for information to answer the user's question.",
					inputSchema: z.object({
						query: z.string().describe("A focused search query"),
					}),
					execute: async ({ query }) => {
						// search() runs retrieval only and returns the matching chunks.
						const res = await this.env.AI_SEARCH.search({
							query,
							ai_search_options: { retrieval: { max_num_results: 5 } },
						});
						return res.chunks.map((chunk) => chunk.text).join("\n\n");
					},
				}),
			},
			// Let the model call the tool, then answer from the results.
			stopWhen: stepCountIs(4),
		});

		// Return the generated answer for the pipeline to speak.
		return result.text;
	}
}

export default {
	async fetch(request, env) {
		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
};
src/server.tsts
import { Agent, routeAgentRequest } from "agents";
import {
	withVoice,
	WorkersAIFluxSTT,
	WorkersAITTS,
	type VoiceTurnContext,
} from "@cloudflare/voice";
import { generateText, tool, stepCountIs } from "ai";
import { createWorkersAI } from "workers-ai-provider";
import { z } from "zod";

const VoiceAgent = withVoice(Agent);

export class TalkToDocs extends VoiceAgent<Env> {
	// Workers AI powers speech-to-text and text-to-speech (no API keys needed).
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	// Called each time the user finishes speaking. The agent's model generates
	// the reply, calling AI Search as a retrieval tool when it needs facts from
	// your knowledge base.
	async onTurn(transcript: string, context: VoiceTurnContext) {
		const workersai = createWorkersAI({ binding: this.env.AI });

		const result = await generateText({
			// Use a Workers AI model that supports function calling.
			model: workersai("@cf/zai-org/glm-5.2"),
			system:
				"You are a helpful voice assistant that answers from a Cloudflare AI Search knowledge base. " +
				"For questions about the product, call the searchKnowledgeBase tool first and answer using the results. " +
				"Skip the tool for greetings and small talk. Keep replies short and conversational.",
			messages: [
				...context.messages.map((message) => ({
					role: message.role as "user" | "assistant",
					content: message.content,
				})),
				{ role: "user" as const, content: transcript },
			],
			tools: {
				searchKnowledgeBase: tool({
					description:
						"Search the knowledge base for information to answer the user's question.",
					inputSchema: z.object({
						query: z.string().describe("A focused search query"),
					}),
					execute: async ({ query }) => {
						// search() runs retrieval only and returns the matching chunks.
						const res = await this.env.AI_SEARCH.search({
							query,
							ai_search_options: { retrieval: { max_num_results: 5 } },
						});
						return res.chunks.map((chunk) => chunk.text).join("\n\n");
					},
				}),
			},
			// Let the model call the tool, then answer from the results.
			stopWhen: stepCountIs(4),
		});

		// Return the generated answer for the pipeline to speak.
		return result.text;
	}
}

export default {
	async fetch(request: Request, env: Env) {
		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

search() が返す各チャンクにはソースアイテムと関連度スコアが含まれるため、引用を出したり、エージェントが取得した内容を記録したりできます。

4. クライアントを作る

src/client.tsx を、useVoiceAgent フックを使う React コンポーネントに置き換えます。フックはマイク、エージェントへの WebSocket 接続、音声再生、割り込み検出を管理するため、コンポーネントはコントロールを描画するだけで済みます。agent にはエージェントクラス名 TalkToDocs を設定します。

src/client.tsxtsx
import { useVoiceAgent } from "@cloudflare/voice/react";

function App() {
	// useVoiceAgent connects to your agent over WebSocket, captures the
	// microphone, plays the spoken response, and exposes the live call state.
	// `agent` matches your Durable Object class name.
	const {
		// Pipeline state: "idle" | "listening" | "thinking" | "speaking".
		status,
		// Finalized conversation turns (your speech and the agent's replies).
		transcript,
		// Live partial transcription of what you are currently saying.
		interimTranscript,
		// Whether the WebSocket connection to the agent is open.
		connected,
		startCall,
		endCall,
		toggleMute,
		isMuted,
	} = useVoiceAgent({ agent: "TalkToDocs" });

	const inCall = status !== "idle";

	return (
		<div>
			<h1>Talk to your knowledge base</h1>

			{/* Shows "thinking" while the agent searches the KB and generates a reply. */}
			<p>Status: {status}</p>

			{/* Toggle the call. Disabled until the agent connection is open. */}
			<button
				onClick={inCall ? endCall : startCall}
				disabled={!connected && !inCall}
			>
				{!connected ? "Connecting…" : inCall ? "End call" : "Start call"}
			</button>

			{/* Mute only applies once a call is active. */}
			{inCall && (
				<button onClick={toggleMute}>{isMuted ? "Unmute" : "Mute"}</button>
			)}

			{/* Lightweight loading state while the agent works on a reply. */}
			{status === "thinking" && <p>Thinking…</p>}

			{/* Live partial transcript, updated as you speak. */}
			{interimTranscript && (
				<p>
					<em>{interimTranscript}</em>
				</p>
			)}

			{/* Finalized turns from both you and the agent. */}
			{transcript.map((message, index) => (
				<p key={index}>
					<strong>{message.role}:</strong> {message.text}
				</p>
			))}
		</div>
	);
}

export default App;

フックがマイクと再生を扱うため、プッシュトゥトークボタンは不要です。モデルは話し終わりを検出し、onTurn() を実行し、音声の回答を自動で再生します。

5. ローカルで実行する

ローカル開発サーバーを起動します。

npm run dev

ブラウザーでアプリを開き、Start call を選んでマイクへのアクセスを許可したあと、コンテンツが答えられる質問をします。発話がリアルタイムで文字起こしされ、エージェントはナレッジベースから声で答えます。処理に合わせて status の値は listeningthinkingspeaking と進みます。

6. デプロイする

エージェントをデプロイし、インターネットから使えるようにします。

npx wrangler deploy

複数人でナレッジベースに話しかける

このチュートリアルは、1 人向けの音声エージェントを作ります。複数人が同じルームでナレッジベースに話しかける場合は、マルチパーティの音声と映像に RealtimeKit を使い、AI Search から答える部品としてこの音声エージェントを残します。RealtimeKit は会議室と文字起こしを提供しますが、回答エンジンはホストしません。

次のステップ

役に立ちましたか?