← Создание агентов на Cloudflare / agents / examples
Чат-агент
Создайте чат-агента, который передаёт ответы ИИ потоково, вызывает инструменты на стороне сервера, выполняет инструменты на стороне клиента в браузере и запрашивает подтверждение пользователя перед чувствительными действиями.
Что вы создадите: Чат-агент на основе Workers AI с тремя типами инструментов: автоматическими, клиентскими и требующими подтверждения.
Время: ~15 минут
Это руководство начинается с минимального Worker Hello World, чтобы вы могли увидеть каждую его часть. Если вам нужно готовое стартовое приложение с уже собранными основными частями, начните с быстрый старт а затем вернитесь сюда, чтобы понять, как части чата соединяются друг с другом.
Предварительные требования:
- Node.js 18+
- Аккаунт Cloudflare (подходит бесплатный тариф)
1. Создайте проект
npm create cloudflare@latest chat-agentВыберите Worker «Hello World» при появлении запроса. Затем установите зависимости:
cd chat-agent
npm install agents @cloudflare/ai-chat ai workers-ai-provider zod2. Настройте Wrangler
Замените ваш wrangler.jsonc с:
{
"name": "chat-agent",
"main": "src/server.ts",
// Set this to today's date
"compatibility_date": "2026-08-28",
"compatibility_flags": ["nodejs_compat"],
"ai": { "binding": "AI" },
"durable_objects": {
"bindings": [{ "name": "ChatAgent", "class_name": "ChatAgent" }],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatAgent"] }],
}name = "chat-agent"
main = "src/server.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]
[ai]
binding = "AI"
[[durable_objects.bindings]]
name = "ChatAgent"
class_name = "ChatAgent"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "ChatAgent" ]Основные настройки:
aiпривязывает Workers AI: ключ API не требуетсяdurable_objectsрегистрирует ваш класс чат-агентаnew_sqlite_classesвключает хранилище SQLite для сохранения сообщений
3. Напишите сервер
Создайте src/server.ts. Именно здесь живёт ваш агент:
import { AIChatAgent } from "@cloudflare/ai-chat";
import { routeAgentRequest } from "agents";
import { createWorkersAI } from "workers-ai-provider";
import {
streamText,
convertToModelMessages,
pruneMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/meta/llama-4-scout-17b-16e-instruct"),
system:
"You are a helpful assistant. You can check the weather, " +
"get the user's timezone, and run calculations.",
messages: pruneMessages({
messages: await convertToModelMessages(this.messages),
toolCalls: "before-last-2-messages",
}),
tools: {
// Server-side tool: runs automatically on the server
getWeather: tool({
description: "Get the current weather for a city",
inputSchema: z.object({
city: z.string().describe("City name"),
}),
execute: async ({ city }) => {
// Replace with a real weather API in production
const conditions = ["sunny", "cloudy", "rainy"];
const temp = Math.floor(Math.random() * 30) + 5;
return {
city,
temperature: temp,
condition:
conditions[Math.floor(Math.random() * conditions.length)],
};
},
}),
// Client-side tool: no execute function — the browser handles it
getUserTimezone: tool({
description: "Get the user's timezone from their browser",
inputSchema: z.object({}),
}),
// Approval tool: requires user confirmation before executing
calculate: tool({
description:
"Perform a math calculation with two numbers. " +
"Requires user approval for large numbers.",
inputSchema: z.object({
a: z.coerce.number().describe("First number"),
b: z.coerce.number().describe("Second number"),
operator: z
.enum(["+", "-", "*", "/", "%"])
.describe("Arithmetic operator"),
}),
needsApproval: async ({ a, b }) =>
Math.abs(a) > 1000 || Math.abs(b) > 1000,
execute: async ({ a, b, operator }) => {
const ops = {
"+": (x, y) => x + y,
"-": (x, y) => x - y,
"*": (x, y) => x * y,
"/": (x, y) => x / y,
"%": (x, y) => x % y,
};
if (operator === "/" && b === 0) {
return { error: "Division by zero" };
}
return {
expression: `${a} ${operator} ${b}`,
result: ops[operator](a, b),
};
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}
export default {
async fetch(request, env) {
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
},
};import { AIChatAgent } from "@cloudflare/ai-chat";
import { routeAgentRequest } from "agents";
import { createWorkersAI } from "workers-ai-provider";
import {
streamText,
convertToModelMessages,
pruneMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/meta/llama-4-scout-17b-16e-instruct"),
system:
"You are a helpful assistant. You can check the weather, " +
"get the user's timezone, and run calculations.",
messages: pruneMessages({
messages: await convertToModelMessages(this.messages),
toolCalls: "before-last-2-messages",
}),
tools: {
// Server-side tool: runs automatically on the server
getWeather: tool({
description: "Get the current weather for a city",
inputSchema: z.object({
city: z.string().describe("City name"),
}),
execute: async ({ city }) => {
// Replace with a real weather API in production
const conditions = ["sunny", "cloudy", "rainy"];
const temp = Math.floor(Math.random() * 30) + 5;
return {
city,
temperature: temp,
condition:
conditions[Math.floor(Math.random() * conditions.length)],
};
},
}),
// Client-side tool: no execute function — the browser handles it
getUserTimezone: tool({
description: "Get the user's timezone from their browser",
inputSchema: z.object({}),
}),
// Approval tool: requires user confirmation before executing
calculate: tool({
description:
"Perform a math calculation with two numbers. " +
"Requires user approval for large numbers.",
inputSchema: z.object({
a: z.coerce.number().describe("First number"),
b: z.coerce.number().describe("Second number"),
operator: z
.enum(["+", "-", "*", "/", "%"])
.describe("Arithmetic operator"),
}),
needsApproval: async ({ a, b }) =>
Math.abs(a) > 1000 || Math.abs(b) > 1000,
execute: async ({ a, b, operator }) => {
const ops: Record<string, (x: number, y: number) => number> = {
"+": (x, y) => x + y,
"-": (x, y) => x - y,
"*": (x, y) => x * y,
"/": (x, y) => x / y,
"%": (x, y) => x % y,
};
if (operator === "/" && b === 0) {
return { error: "Division by zero" };
}
return {
expression: `${a} ${operator} ${b}`,
result: ops[operator](a, b),
};
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;Что делает каждый тип инструмента
| Инструмент | execute? |
needsApproval? |
Поведение |
|---|---|---|---|
getWeather |
Да | Нет | Автоматически выполняется на сервере |
getUserTimezone |
Нет | Нет | Отправлено клиенту; браузер предоставляет результат |
calculate |
Да | Да (большие числа) | Приостанавливается для подтверждения пользователем, затем выполняется на сервере |
4. Напишите клиент
Создайте src/client.tsx:
import { useAgent } from "agents/react";
import { useAgentChat, getToolApproval } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ChatAgent" });
const {
messages,
sendMessage,
clearHistory,
addToolApprovalResponse,
status,
} = useAgentChat({
agent,
// Handle client-side tools (tools with no server execute function)
onToolCall: async ({ toolCall, addToolOutput }) => {
if (toolCall.toolName === "getUserTimezone") {
addToolOutput({
toolCallId: toolCall.toolCallId,
output: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
localTime: new Date().toLocaleTimeString(),
},
});
}
},
});
return (
<div>
<div>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>
{msg.parts.map((part, i) => {
if (part.type === "text") {
return <span key={i}>{part.text}</span>;
}
// Render approval UI for tools that need confirmation
if (part.state === "approval-requested") {
const approval = getToolApproval(part);
if (!approval) return null;
return (
<div key={part.toolCallId}>
<p>
Approve <strong>{part.toolName}</strong>?
</p>
<pre>{JSON.stringify(part.input, null, 2)}</pre>
<button
onClick={() =>
addToolApprovalResponse({
id: approval.id,
approved: true,
})
}
>
Approve
</button>
<button
onClick={() =>
addToolApprovalResponse({
id: approval.id,
approved: false,
})
}
>
Reject
</button>
</div>
);
}
// Show completed tool results
if (part.state === "output-available") {
return (
<details key={part.toolCallId}>
<summary>{part.toolName} result</summary>
<pre>{JSON.stringify(part.output, null, 2)}</pre>
</details>
);
}
return null;
})}
</div>
))}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.elements.namedItem("message");
sendMessage({ text: input.value });
input.value = "";
}}
>
<input name="message" placeholder="Try: What's the weather in Paris?" />
<button type="submit" disabled={status === "streaming"}>
Send
</button>
</form>
<button onClick={clearHistory}>Clear history</button>
</div>
);
}
export default function App() {
return <Chat />;
}import { useAgent } from "agents/react";
import { useAgentChat, getToolApproval } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ChatAgent" });
const { messages, sendMessage, clearHistory, addToolApprovalResponse, status } =
useAgentChat({
agent,
// Handle client-side tools (tools with no server execute function)
onToolCall: async ({ toolCall, addToolOutput }) => {
if (toolCall.toolName === "getUserTimezone") {
addToolOutput({
toolCallId: toolCall.toolCallId,
output: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
localTime: new Date().toLocaleTimeString(),
},
});
}
},
});
return (
<div>
<div>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>
{msg.parts.map((part, i) => {
if (part.type === "text") {
return <span key={i}>{part.text}</span>;
}
// Render approval UI for tools that need confirmation
if (part.state === "approval-requested") {
const approval = getToolApproval(part);
if (!approval) return null;
return (
<div key={part.toolCallId}>
<p>
Approve <strong>{part.toolName}</strong>?
</p>
<pre>{JSON.stringify(part.input, null, 2)}</pre>
<button
onClick={() =>
addToolApprovalResponse({
id: approval.id,
approved: true,
})
}
>
Approve
</button>
<button
onClick={() =>
addToolApprovalResponse({
id: approval.id,
approved: false,
})
}
>
Reject
</button>
</div>
);
}
// Show completed tool results
if (part.state === "output-available") {
return (
<details key={part.toolCallId}>
<summary>{part.toolName} result</summary>
<pre>{JSON.stringify(part.output, null, 2)}</pre>
</details>
);
}
return null;
})}
</div>
))}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.elements.namedItem(
"message",
) as HTMLInputElement;
sendMessage({ text: input.value });
input.value = "";
}}
>
<input name="message" placeholder="Try: What's the weather in Paris?" />
<button type="submit" disabled={status === "streaming"}>
Send
</button>
</form>
<button onClick={clearHistory}>Clear history</button>
</div>
);
}
export default function App() {
return <Chat />;
}Основные понятия на стороне клиента
useAgentподключается к вашемуChatAgentчерез WebSocketuseAgentChatуправляет жизненным циклом чата (сообщения, потоковая передача, инструменты)onToolCallобрабатывает инструменты на стороне клиента: когда LLM вызываетgetUserTimezone, браузер предоставляет результат, и разговор продолжается автоматическиaddToolApprovalResponseодобряет или отклоняет инструменты, у которых естьneedsApproval- Сообщения, потоковая передача и возобновление обрабатываются автоматически
5. Запустите локально
Сгенерируйте типы и запустите dev-сервер:
npx wrangler types
npm run devПопробуйте эти промпты:
- «Какая погода в Токио?» : вызывает на стороне сервера
getWeatherинструмент - «В каком я часовом поясе?» : вызывает на стороне клиента
getUserTimezoneинструмент (ответ предоставляет браузер) - «Сколько будет 5000 умножить на 3?» : перед выполнением запускает UI одобрения (числа больше 1000)
6. Разверните
npx wrangler deployТеперь ваш агент работает в глобальной сети Cloudflare. Сообщения сохраняются в SQLite, потоки возобновляются после разрыва соединения, а в простое агент переходит в гибернацию для экономии ресурсов.
Что вы создали
У вашего чат-агента есть:
- Потоковая передача ответов ИИ через Workers AI (без API-ключей)
- Персистентность сообщений в SQLite: разговоры переживают перезапуски
- Инструменты на стороне сервера которые выполняются автоматически
- Инструменты на стороне клиента которые выполняются в браузере и передают результаты обратно в LLM
- Подтверждение с участием человека для чувствительных операций
- Возобновляемый стриминг : если клиент отключается посреди потока, он продолжает с того места, где остановился