Agents SDK:即時 WebSocket 與 React 前端 | Cloudflare 完整教學
這一篇要讓你的 Agent「活起來」。前兩篇我們搭好了有狀態、會排程的 Agent 骨架,也讓它能被 AI 生態系當工具呼叫;但互動都還是「一問一答」。這一篇要打造即時的聊天式 Agent 前端:伺服器端用
onConnect/onMessage處理 WebSocket、React 端用useAgent讓 state 自動同步、用useAgentChat把 AI 回應串流到 UI,並整合 Durable Objects 的 WebSocket Hibernation、完成部署。API 名稱以官方文件為準。
前言
上一篇《Agents SDK:MCP Server》,我們讓 Agent 學會被 Claude 等 AI 應用當成工具來呼叫。但不管是最早的入門篇,還是 MCP 篇,我們跟 Agent 的互動幾乎都是「一來一往」的請求——你送一個 HTTP request,它回一個 response,連線就結束了。
這種模式對很多場景夠用,但有一整類體驗是它做不到的:即時。想想看你用過的即時聊天、多人協作白板、線上遊戲房間、即時儀表板——它們的共同點是「伺服器狀態一變,所有正在看的人立刻同步看到」,而且是雙向的:客戶端能主動推、伺服器也能主動推。這需要一條持續開著的雙向連線,那就是 WebSocket。
先給一句話定義:
Cloudflare Agents SDK 讓你在伺服器端用
onConnect/onMessage處理 WebSocket 即時連線,並提供useAgent、useAgentChat兩個 React Hook——前者讓 Agent 的state自動雙向同步到前端、後者讓 AI 聊天回應即時串流到 UI;整條即時通道底層建在 Durable Objects 上,享有 WebSocket Hibernation 帶來的「連線不斷、閒置省資源」。
這裡有兩個關鍵詞。第一個是 WebSocket——一條建立後就「一直開著」的雙向連線,不像 HTTP 每次都要重新握手。第二個是 state 自動同步——這是 Agents SDK 最迷人的地方:你在伺服器端呼叫 setState 改了狀態,所有連著的前端會自動收到新狀態,你幾乎不用手寫任何「同步」邏輯。
打個比方會更清楚。傳統 HTTP 請求像寄信:你寄一封、對方回一封,一來一回都要重新投遞,你不主動問就不會知道對方那邊有什麼變化。WebSocket 則像一直沒掛斷的電話:線接通後,雙方誰有話都能直接講,對方立刻聽到。而 Agents SDK 的 state 同步,更像是一塊大家共用的即時白板——伺服器在白板上寫字,所有看著白板的人瞬間都看到了,不用誰去「重新整理」。
為什麼這件事在 Cloudflare 上特別漂亮?因為每個 Agent 底層是一個 Durable Object——它是「有身份、有記憶、能維持連線」的物件。多個使用者連到「同一個 Agent 實例」(例如同一個聊天室 ID),就自動被路由到同一個 DO,共享同一份狀態。這正是我們在《Durable Objects 的 WebSocket 與 Hibernation》(第 026 篇)談過的能力,只是 Agents SDK 把它包裝得更好用了。
讀完這篇你會掌握:
- 即時連線的伺服器端——怎麼用
onConnect(connection)/onMessage(connection, message)處理 WebSocket 生命週期、用broadcast廣播給所有人。 - state 自動同步——
setState如何一次做到「持久化 + 廣播到所有前端」,以及useAgent怎麼在 React 端接住它。 - 把 AI 回應串流到 UI——用
useAgentChat搭配AIChatAgent,做一個逐字冒出來的聊天介面。 - Hibernation 與部署——為什麼閒置連線不會白白斷、也不會一直燒錢,以及怎麼上線。
小提醒:Agents SDK 與其 React 套件都在快速演進中,以下的 Hook 名稱、方法與選項(如
useAgent、useAgentChat、onStateUpdate、agent.stub、handleSubmit)以當下 Cloudflare 官方文件為準。若 API 名稱有出入,請以官方文件為主;本篇聚焦「觀念 + 最小可用範例」。
核心概念
一、即時的兩個方向:客戶端推、伺服器推
要理解即時通訊,先分清楚「訊息的兩個方向」:
| 方向 | 誰發起 | Agents SDK 的做法 |
|---|---|---|
| 客戶端 → 伺服器 | 前端使用者(打字、按按鈕) | 前端傳 WebSocket 訊息,伺服器 onMessage 收;或呼叫 RPC(agent.stub.xxx()) |
| 伺服器 → 客戶端 | 伺服器(狀態變了、有新事件) | 伺服器 setState 自動廣播,或 broadcast(...) / connection.send(...) 手動推 |
HTTP 只擅長第一個方向(客戶端問、伺服器答)。WebSocket 的價值在第二個方向——伺服器可以在「沒人問」的情況下,主動把變化推給正在連線的每一個人。這就是為什麼即時聊天、即時通知、多人同步一定要用 WebSocket。
二、setState 是即時同步的心臟
Agents SDK 最核心的一個設計,是 this.setState() 一個呼叫同時做三件事:
- 持久化——把新狀態寫進 Agent 的 SQLite,重啟或休眠後還在。
- 廣播——把新狀態即時推送給所有連著這個 Agent 的 WebSocket 客戶端。
- 觸發
onStateChanged()——通知你的伺服器端程式「狀態變了」(廣播之後,盡力執行)。
這代表:你只要專心「改狀態」,同步這件事 SDK 幫你做完了。 你不用手寫「遍歷所有連線、序列化、逐一 send」的樣板程式。舉例:
// 伺服器端:改狀態就好,前端會自動同步
this.setState({
...this.state,
score: this.state.score + 10,
});
// ↑ 這一行,就讓所有連著的前端都看到新分數了
對應到前端,useAgent 會透過 onStateUpdate 回調(或 agent.state)把新狀態交給你,你把它塞進 React state,畫面就更新了。一條資料流,從伺服器一路自動流到每個瀏覽器。
重要原則:
state適合放「需要即時同步、且不大」的資料(UI 狀態、即時計數器、活躍會話);大型集合或歷史資料應該放 SQLite(this.sql),因為 state 每次變更都會整包廣播,太大會讓同步變慢。這一點「常見錯誤」會再強調。
三、Agent 即時連線 + state 同步到 React 的全貌
把整條路徑串起來,一個即時 Agent 前端大致是這樣運作:
┌───────────────────────┐ ┌──────────────────────────────────┐
│ React 前端(瀏覽器) │ │ Cloudflare Worker + Durable │
│ │ WS 連線 │ Object(單一 Agent 實例) │
│ useAgent({ │◀───────▶│ │
│ agent: "RoomAgent"│ │ class RoomAgent extends Agent { │
│ name: "room-123" │ │ onConnect(connection) {...} │
│ }) │ │ onMessage(conn, msg) {...} │
│ │ │ setState(...) ──┐ │
│ onStateUpdate ◀─────┼─────────┼── 自動廣播 ◀────────┘ │
│ agent.stub.xxx() ───┼────────▶│ @callable 方法(RPC) │
└───────────────────────┘ │ this.state / this.sql(持久化) │
└──────────────────────────────────┘
多個使用者連同一個 name(room-123)→ 路由到同一個 DO → 共享同一份 state
流程拆解:
- 前端
useAgent建立連線——指定agent(類別名)與name(實例名)。連同一個name的所有人,會被路由到同一個 Durable Object 實例,共享同一份狀態。 - 伺服器
onConnect在連線建立時觸發——你可以在這裡驗證身份、記錄「誰進來了」、廣播「有人加入」。 - 雙向溝通——前端可以送訊息(
onMessage接)或呼叫 RPC(agent.stub.xxx());伺服器可以setState(自動同步)或broadcast(手動推)。 - state 自動流回前端——伺服器一
setState,useAgent就透過onStateUpdate把新狀態交給每個前端。
跟第 026 篇的關係:那篇我們手刻 Durable Object 的 WebSocket、自己管理連線與 Hibernation。這一篇的 Agent 底層就是那套東西,只是 Agents SDK 把「連線管理、狀態廣播、前端 Hook」都幫你包好了——你享受同樣的 Hibernation 好處,卻幾乎不用寫樣板。
四、關鍵術語一次記牢
| 術語 | 是什麼 |
|---|---|
onConnect(connection, ctx) | 伺服器端:WebSocket 連線建立時觸發,connection 代表這條連線 |
onMessage(connection, message) | 伺服器端:收到某條連線傳來的訊息時觸發 |
connection.send(...) / this.broadcast(...) | 對單一連線送 / 對所有連線廣播 |
this.setState(...) | 改狀態:持久化 + 自動廣播到所有前端 + 觸發 onStateChanged |
useAgent | React Hook(agents/react):建立連線、雙向 state 同步、RPC |
agent.stub.xxx() | 前端型別安全地呼叫伺服器 @callable 方法(RPC) |
useAgentChat | React Hook:搭配 AIChatAgent,提供 messages / handleSubmit / isLoading / stop |
| WebSocket Hibernation | 閒置時 Agent 休眠、連線由 Cloudflare 維持,訊息到達自動喚醒 |
實作範例
我們分兩段實作。第一段做一個即時計數器/協作房間,示範 onConnect/onMessage/setState 與 useAgent 的 state 自動同步;第二段做一個串流聊天介面,示範 AIChatAgent + useAgentChat 把 AI 回應逐字串流到 UI。
1. 安裝與設定
先裝好套件。做即時連線只需要 agents 本體;做 AI 聊天再加上聊天與 AI SDK 相關套件:
# 即時連線 + React Hook
npm install agents
# 若要做 AI 聊天串流,額外安裝(套件組合以官方 starter 為準)
npm install @cloudflare/ai-chat ai @ai-sdk/react workers-ai-provider
wrangler.jsonc 的關鍵設定。Agent 建在 Durable Objects 上,所以要綁 DO binding、用 migrations 宣告為 SQLite-backed 類別;做 AI 聊天則加上 Workers AI binding:
// wrangler.jsonc
{
"name": "realtime-agent",
"main": "src/server.ts",
"compatibility_date": "2026-09-01",
"compatibility_flags": ["nodejs_compat"],
// 每個 Agent 類別各需要一個 DO binding
"durable_objects": {
"bindings": [
{ "name": "RoomAgent", "class_name": "RoomAgent" },
{ "name": "ChatAgent", "class_name": "ChatAgent" }
]
},
// 宣告為 SQLite-backed 的 Agent 類別
// 提醒:舊的 migration 條目永遠不要修改,只新增帶新 tag 的條目
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["RoomAgent", "ChatAgent"] }
],
// 做 AI 聊天需要 Workers AI binding
"ai": { "binding": "AI" },
// 前端靜態資源(打包後的 React app)
"assets": { "directory": "./dist/client" }
}
提醒:Agents SDK 的 TypeScript 設定通常繼承
agents/tsconfig,且不應開啟experimentalDecorators(會破壞@callable)。這些細節以官方 starter 範本與文件為準。
2. 伺服器端:即時房間 Agent(onConnect / onMessage / setState)
先做伺服器端的 RoomAgent。它維護一份共享狀態(在線人數、目前分數),在連線建立/收到訊息時更新——注意每次 setState 都會自動同步到所有前端:
// src/server.ts
import { Agent, routeAgentRequest, callable, type Connection } from "agents";
type Env = {
RoomAgent: DurableObjectNamespace;
ChatAgent: DurableObjectNamespace;
AI: Ai;
};
// 這份 state 會被自動同步到所有連著的前端
type RoomState = {
online: number; // 在線人數
score: number; // 共享分數
};
export class RoomAgent extends Agent<Env, RoomState> {
// 初始狀態:新實例第一次啟動時使用
initialState: RoomState = { online: 0, score: 0 };
// WebSocket 連線建立時觸發
async onConnect(connection: Connection, ctx: { request: Request }) {
const url = new URL(ctx.request.url);
const name = url.searchParams.get("name") ?? "訪客";
// 把這條連線自己的狀態存在 connection 上(休眠後仍保留)
connection.setState({ name });
// 在線人數 +1 → setState 會自動把新 state 推給所有前端
this.setState({ ...this.state, online: this.state.online + 1 });
// 也可以「手動」廣播一則事件訊息給其他人(排除剛連進來的自己)
this.broadcast(
JSON.stringify({ type: "join", name }),
[connection.id], // 排除清單:不送給自己
);
}
// 收到某條連線傳來的訊息時觸發
async onMessage(connection: Connection, message: string | ArrayBuffer) {
// 只處理字串訊息;二進位(ArrayBuffer)這裡忽略
if (typeof message !== "string") return;
const data = JSON.parse(message) as { type: string };
if (data.type === "add") {
// 改共享分數 → 一樣自動同步給所有人
this.setState({ ...this.state, score: this.state.score + 1 });
}
}
// 連線關閉時觸發:在線人數 -1
async onClose(connection: Connection) {
const next = Math.max(0, this.state.online - 1);
this.setState({ ...this.state, online: next });
}
// 也可以暴露一個 RPC 方法,讓前端用 agent.stub.reset() 直接呼叫
@callable()
reset() {
this.setState({ ...this.state, score: 0 });
}
}
// Worker 入口:把請求路由到對應的 Agent 實例
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;
這段有幾個重點值得停下來看:
onConnect(connection, ctx)是連線的入口。connection代表「這一條」連線,connection.setState({...})存的是這條連線專屬的狀態(例如它是誰);而this.setState({...})改的是整個房間共享的狀態。兩者別搞混。this.setState(...)就是同步的全部。你不需要在onConnect/onMessage裡手寫「通知每個前端」——改了 state,SDK 自動廣播。this.broadcast(...)則用在「想推一則一次性事件訊息(如 join/leave 通知),而不是狀態」的時候。onClose記得清理——連線斷了要把在線人數減回去,否則數字會越加越高、對不上真實人數。
3. React 前端:useAgent 讓 state 自動同步
前端用 useAgent 連到 RoomAgent。重點是:我們幾乎不用寫任何同步邏輯——onStateUpdate 一收到伺服器推來的新狀態,就更新 React state,畫面自動重繪:
// src/client.tsx
import { useState } from "react";
import { useAgent } from "agents/react";
import type { RoomAgent, RoomState } from "./server";
export default function Room() {
const [state, setLocalState] = useState<RoomState>({ online: 0, score: 0 });
const [connected, setConnected] = useState(false);
const agent = useAgent<RoomAgent, RoomState>({
agent: "RoomAgent", // 要連的 Agent 類別
name: "room-123", // 實例名:連同一個 name 的人共享狀態
query: { name: "小明" }, // 會變成 URL query,伺服器 onConnect 讀得到
onOpen: () => setConnected(true),
onClose: () => setConnected(false),
// 伺服器一 setState,這裡就會收到最新狀態
onStateUpdate: (next) => setLocalState(next),
});
return (
<div>
<p>連線狀態:{connected ? "已連線" : "連線中…"}</p>
<p>在線人數:{state.online}</p>
<p style={{ fontSize: "2rem" }}>共享分數:{state.score}</p>
{/* 送一則 WebSocket 訊息,伺服器 onMessage 會收到 */}
<button onClick={() => agent.send(JSON.stringify({ type: "add" }))}>
+1(送訊息)
</button>
{/* 或直接呼叫伺服器的 @callable 方法(型別安全 RPC) */}
<button onClick={() => agent.stub.reset()}>重置(RPC)</button>
</div>
);
}
打開兩個瀏覽器分頁、都連到 room-123,你會看到:在其中一個分頁按「+1」,另一個分頁的分數也立刻跟著變——因為兩個分頁連的是同一個 Agent 實例,伺服器 setState 自動把新狀態同步給雙方。這就是「即時協作」最核心的體驗,而你幾乎沒寫同步程式。
useAgent<RoomAgent, RoomState>(...)帶入型別參數後,agent.stub的方法(如reset())就有型別檢查與自動補全。onStateUpdate是 state 同步的接點——把它交給 React state 就完成了資料流。agent.send(...)送原始 WebSocket 訊息(對應onMessage);agent.stub.xxx()是型別安全的 RPC 呼叫。兩種都能用,RPC 通常更好維護。- 連線的建立、重連(指數退避)、元件卸載時清理,
useAgent都自動處理,你不用碰。
4. 串流 AI 回應到 UI:AIChatAgent + useAgentChat
即時的殺手級應用就是串流聊天——AI 一個字一個字冒出來,而不是等整段生成完才「啪」一次出現。伺服器端用 AIChatAgent,它把「串流、訊息持久化、多裝置同步」都包好了:
// src/server.ts(接續前面,新增 ChatAgent)
import { AIChatAgent } from "@cloudflare/ai-chat";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages } from "ai";
export class ChatAgent extends AIChatAgent<Env> {
// 只保留最近 200 則訊息在 SQLite(超過刪最舊的)
maxPersistedMessages = 200;
// 每次有新使用者訊息時觸發:回傳一個「串流回應」
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/meta/llama-3.3-70b-instruct-fp8-fast"),
system: "你是一個友善、簡潔的中文 AI 助理。",
// this.messages 是自動持久化的對話歷史
messages: await convertToModelMessages(this.messages),
});
// 轉成 UI 訊息串流:SDK 會透過 WebSocket 把 token 逐段推到前端
return result.toUIMessageStreamResponse();
}
}
前端用 useAgentChat——它建構在 useAgent 之上,直接給你聊天該有的一切:
// src/chat.tsx
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
export default function Chat({ userId }: { userId: string }) {
// 先用 useAgent 取得到 ChatAgent 的連線
const agent = useAgent({ agent: "ChatAgent", name: userId });
// 再交給 useAgentChat,拿到聊天所需的一切
const {
messages, // 對話訊息陣列(含正在串流的助理訊息)
input, // 目前輸入框內容
handleInputChange,
handleSubmit, // 送出訊息
isLoading, // AI 是否正在生成
stop, // 中止生成
} = useAgentChat({ agent });
return (
<div>
<div className="messages">
{messages.map((msg) => (
<div key={msg.id} className={`msg ${msg.role}`}>
{/* 訊息由 parts 組成,逐段串流時這裡會即時增量更新 */}
{msg.parts?.map((part, i) =>
part.type === "text" ? <span key={i}>{part.text}</span> : null,
)}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="輸入訊息…"
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
{isLoading ? "思考中…" : "傳送"}
</button>
{isLoading && (
<button type="button" onClick={stop}>停止</button>
)}
</form>
</div>
);
}
重點在:你完全沒寫任何「串流」的底層程式。伺服器 streamText(...).toUIMessageStreamResponse() 產出串流,useAgentChat 這頭把逐段抵達的 token 增量塞進對應訊息的 parts,React 因為 messages 變了而重繪——於是你看到助理的回覆一段一段浮現。而且因為訊息持久化在 Agent 的 SQLite,同一個 name(這裡是 userId)換裝置登入,歷史對話還在。
5. 部署
寫好後,先本地開發,再部署:
# 本地開發:同時跑前端與 Worker
npm run dev
# 部署到 Cloudflare 邊緣
npx wrangler deploy
部署後,前端靜態資源由 assets 提供,WebSocket 連線由 routeAgentRequest 路由到對應的 Agent 實例。你的即時聊天/協作 App 就跑在 Cloudflare 全球網路上了。
常見錯誤與最佳實踐
即時應用的坑,大多不在「連不上」,而在「同步慢」「連線沒清乾淨」「UI 沒好好處理串流」。以下是最高頻的幾個。
坑一:把大型陣列/歷史資料塞進 state,同步越來越慢。
setState 的代價是「每次變更都整包序列化、廣播給所有連線」。如果你把「所有聊天訊息」「上千筆紀錄」放進 this.state,那每加一筆,整包資料就重新傳一次給每個人,連線多、資料大時會明顯卡頓。正確做法:state 只放「需要即時同步且輕量」的資料(在線人數、目前分數、最後一則訊息 ID);歷史與大型集合放 SQLite(this.sql),需要時再查。 一句話:state 保持輕、SQL 扛重量。
坑二:連線關閉時沒清理,計數/名單越來越髒。
只在 onConnect 加、忘了在 onClose 減,是超常見的 bug——在線人數會越加越高、離線的人還掛在名單上。正確做法:凡是 onConnect 建立的狀態(人數、成員清單),都要在 onClose(以及必要時 onError)對稱地清理。 前端這頭,useAgent 已經幫你在元件卸載時關連線,但伺服器端的狀態清理是你的責任。
坑三:串流 UI 不做增量渲染,失去「逐字浮現」的體驗。
有人拿到串流卻等它整段結束才 render,或每次 token 進來就重畫整個訊息列表,結果不是不流暢就是很卡。用 useAgentChat 時,它已經幫你把 token 增量併進對應訊息的 parts,你只要正常 messages.map(...) 渲染即可;真正要注意的是別在每次更新做昂貴的重算(例如整串 Markdown 每個 token 都重新 parse 一次)。正確做法:讓 React 只重繪「有變動的那則訊息」,必要時用 key、memo 避免整列表重繪。
坑四:誤以為 WebSocket 閒置就會斷、或會一直全速計費。
Agent 建在 Durable Objects 上,有 WebSocket Hibernation:閒置時 Agent 休眠、釋放資源,但連線由 Cloudflare 維持,訊息一到自動喚醒。你需要配合的是:把要跨休眠存活的資料放進 this.state / connection.state / SQLite,而不是類別的一般屬性、計時器或本地快取——因為這些在休眠後不保留。呼應第 026 篇:這正是 DO 的 Hibernation,Agents SDK 只是幫你包好了。
坑五:多人本該共享狀態,卻連到不同的 name。
useAgent 的 name 決定連到「哪一個 Agent 實例」。想讓一群人共享同一份即時狀態(同一個聊天室、同一張協作白板),他們就得連同一個 name;若每個人各自用不同 name(例如各自的 userId),他們會被路由到各自獨立的 DO,彼此看不到對方。正確做法:依「協作範圍」設計 name——房間就用 room ID、個人助理才用 user ID。 別把「該共享」的做成各自獨立。
最佳實踐小結: state 保持輕、大資料丟 SQL;onClose 對稱清理連線狀態;串流 UI 靠 useAgentChat 增量渲染、避免昂貴重算;記得 Hibernation 只保留 state/SQLite、別依賴記憶體;用 name 精準設計「誰跟誰共享」。API 名稱與細節請以官方文件為準。
小結
上一篇《Agents SDK:MCP Server》,我們讓 Agent 能被 AI 生態系當工具呼叫;但互動仍是一問一答。這一篇《Agents SDK:即時 WebSocket 與 React》,我們讓 Agent「活了起來」:
- 即時的伺服器端——
onConnect(connection)處理連線建立、onMessage(connection, message)收訊息、onClose清理;this.broadcast(...)手動推事件。 - state 自動同步——
this.setState(...)一次做完「持久化 + 廣播到所有前端 + 觸發onStateChanged」,是即時同步的心臟;前端用useAgent的onStateUpdate接住,幾乎不用寫同步邏輯。 - 串流 AI 到 UI——伺服器
AIChatAgent+streamText(...).toUIMessageStreamResponse(),前端useAgentChat給你messages/handleSubmit/isLoading/stop,做出逐字浮現的聊天介面。 - Hibernation 與部署——底層是 Durable Objects,閒置休眠、連線不斷(呼應第 026 篇);把要存活的資料放 state/SQLite。
到這裡,我們的 CF-5 AI 層就收尾了——從 Workers AI、Vectorize RAG,一路到 Agents SDK 的入門、MCP Server,再到今天的即時前端,你已經能在 Cloudflare 上打造有記憶、會排程、被生態系呼叫、還能即時互動的 AI 應用。接下來我們把視角拉回「全端與周邊」。下一篇《Pages 與 Static Assets 總覽》,我們要看看 Cloudflare 怎麼託管你的前端與靜態網站——這也是把今天做的 React 前端漂亮上線的另一條路。我們下一篇見。
想深入 Agents SDK 的即時通訊與 React Hook 完整 API,可以參考 Cloudflare Agents 官方文件(Hook、方法與選項名稱以官方文件為準)。