Стварэнне інтэрфейсаў агента ШІ з кількама крокамі за дапамою Next.js і SDK ШІ.
Інтерфейс користувача для агента ШІ у Next.js з SDK ШІ: інструменты з підтримкою типізації, багатоэтапныя циклы та компоненты інтерфейсу, створюваныя під час роботы агента, написаныя на TypeScript.
Большасць нарадчыкаў, якія распаведваюць пра системы AI, зупыняюцца самэ там, дзе спачатку пачынае мяць значэнне тое, што вы робіце. Вы пішаце невялікі скрыпт, адправляеце запит да модэлю і спагледзяеце, як токены выводзяцца ў тэрмінале — і гэта вже здаецца прынцыпам праўкі.
Прыглушаныя труднасці праказуюцца тады, калі вы прабуеце ператворыць гэта на рэальны продукт.
Тады стае ясна, што бізнес-выкарыстоўнач не мае інтерэсу да суровага блока тексту у формате markdown. Яны чакаюць інтэрактыўных табелей дадзеных, кроку параджэння перад выкананнем будзь-якай деструктыўной дзеяння і павольнага стрімування выходных дадзеных, якое не вызывае перескаквання стороніцы.
На самай працоўцы ў іх ёсць модэль, які можа координаваць калькі выклікаў інструментоў па архіве, пры чым інтэрфейс відображае належна скарыстаныя компоненты React для кожнага этапу гэтага процесу.
Проектаванне інтерфейса для агента означае розгляд моделі як актыўнага двыжка выканання, а не простага генератора тексту. Ёсць план стварэння інтерфейса агента высокага стандарта ў Next.js з выкарыстоўваннем Vercel AI SDK — таког, які падтрымляе багатоэтапны вызов інструментаў, сильна типаваныя даны і элементы UI, якія генеруюцца дынамічна праз потокі адпаведзей.
Архітэктура: за межы простых потакоў тексту
Надзеяны пайплайн агента зовсім не нагадвае просты чатбот:
[ Client: useChat() ]
│ ▲
│ │ Data Stream (Tokens + Typed Tool Invocations)
▼ │
[ Next.js Route Handler: streamText() ]
│ ▲
│ │ Model generates tool arguments
▼ │
[ Tool Execution (e.g. DB Query / Stripe API) ]
│
└──> (Loop back to LLM if more steps are required)
- Транспорт з станам: кліент адправляе вхідныя даны пользователя да обробніка маршруту, які працуе на Edge або Node runtime.
- Выкананне інструментаў на серверае: модель выявляе, чыму ў яе вже є достатнькі даныя для адпаведзі, чыму яй трэба вызваць заздалегідь заданы інструмент, перакананы за дапамогою схемы Zod, каб запрашаць зовнішнія даны.
1. Адзначэнне API агента з тыпаванымі інструментамі
Пачніце з стварэння маршруту API за дапамою App Router. Мета — агент, які можа запрашваць актуальную статыстыку рачунка і запускать транзакцыйныя дзеяння, такія як стварэнне рахунку-фактуры.
Установіце неабходныя пакеты:
npm install ai @ai-sdk/openai @ai-sdk/react zod
Потым адзначыце працоўнік маршруту ў файле app/api/agent/route.ts:
import { openai } from '@ai-sdk/openai';
import { streamText, tool } from 'ai';
import { z } from 'zod';
// Prevent function timeouts on long agent loops
export const maxDuration = 60;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
messages,
// System instructions establish agent personality and boundaries
system: `You are an internal operations assistant.
Use the available tools to answer queries and perform operations.
Always confirm parameters before invoking actions that write data.`,
// Allow the model to execute a tool, inspect output, and take subsequent steps
maxSteps: 5,
tools: {
getAccountMetrics: tool({
description: 'Fetch real-time metrics for a customer account',
parameters: z.object({
accountId: z.string().describe('The alphanumeric account identifier'),
timeframe: z.enum(['7d', '30d', '90d']),
}),
execute: async ({ accountId, timeframe }) => {
// Replace with real database or external API call
await new Promise((r) => setTimeout(r, 800)); // Simulating latency
return {
accountId,
timeframe,
revenue: 24500,
activeUsers: 342,
churnRisk: 'low',
};
},
}),
generateInvoiceDraft: tool({
description: 'Prepare an invoice draft for customer review',
parameters: z.object({
recipientEmail: z.string().email(),
amount: z.number().positive(),
currency: z.string().default('USD'),
}),
execute: async ({ recipientEmail, amount, currency }) => {
// Simulating invoice creation
return {
invoiceId: `inv_${Math.random().toString(36).substring(2, 9)}`,
status: 'draft',
recipientEmail,
amount,
currency,
createdAt: new Date().toISOString(),
};
},
}),
},
});
return result.toDataStreamResponse();
}
Чаму важнае значэння maxSteps
Старыя падходы да багатаэтапнага разумавання агентаў базаваліся на незграбных цыклах while, ручным выкарыстоўванні для змены масэвай спамяці з прыемнымі, а таксама на окрэслёваных запусках да LLM для кожнага этапу.
Установка maxSteps: 5 дазволяе SDK перэявіць на сабе гэты рекурсыўны цыкл выкарыстоўвання інструментаў на сервере, пры чым кожны проміжны стан адразу перадаецца кліенту ў момент яго стварэння.
2. Створэнне кліента: стрімаванне типавых складовых
У фронтэндзе сучасныя інтарфейсы агентаў дзелюць кожны прыйняты ўтварэнне на часткі, замест таго каб тратаваць яго як адну цэлую текстовую масу. Адны ўтварэння можа місцаваць у сабе фрагменты звычайнага тексту, вызовы інструментаў, якія ўсё яшчэ чакаюць на адпаведзь, і рэзультаты інструментаў, якія вучора ўжо былі атрыманы.
Установіце контейнер чату ўнутрь app/page.tsx:
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
import { MetricsCard } from '@/components/tools/MetricsCard';
import { InvoiceCard } from '@/components/tools/InvoiceCard';
export default function AgentConsole() {
const [input, setInput] = useState('');
const { messages, append, status } = useChat({
api: '/api/agent',
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
append({ role: 'user', content: input });
setInput('');
};
const isLoading = status === 'streaming' || status === 'submitted';
return (
<main className="max-w-4xl mx-auto py-10 px-4 flex flex-col h-screen">
<header className="border-b pb-4 mb-6">
<h1 className="text-xl font-semibold tracking-tight">Operations Intelligence Agent</h1>
<p className="text-sm text-neutral-500">Autonomous workflow orchestration and metrics exploration.</p>
</header>
{/* Message Feed */}
<div className="flex-1 overflow-y-auto space-y-6 pr-2">
{messages.map((m) => (
<div
key={m.id}
className={`flex flex-col ${
m.role === 'user' ? 'items-end' : 'items-start'
}`}
>
<div
className={`rounded-2xl px-4 py-3 max-w-[85%] text-sm ${
m.role === 'user'
? 'bg-blue-600 text-white'
: 'bg-neutral-100 text-neutral-900 border border-neutral-200'
}`}
>
{/* Handle granular message parts */}
{m.parts?.map((part, index) => {
if (part.type === 'text') {
return <p key={index} className="whitespace-pre-wrap">{part.text}</p>;
}
// Handle tool rendering based on typed part identifier
if (part.type === 'tool-invocation') {
const { toolName, state, args } = part.toolInvocation;
if (state !== 'result') {
return (
<div key={index} className="my-2 py-2 px-3 bg-neutral-50 rounded border border-dashed border-neutral-300 text-xs text-neutral-500 animate-pulse">
Executing tool: <code className="font-mono">{toolName}</code> with {JSON.stringify(args)}...
</div>
);
}
const { result } = part.toolInvocation;
// Render specialized components for each tool
if (toolName === 'getAccountMetrics') {
return <MetricsCard key={index} data={result} />;
}
if (toolName === 'generateInvoiceDraft') {
return <InvoiceCard key={index} data={result} />;
}
}
return null;
}) ?? m.content}
</div>
</div>
))}
</div>
{/* Input Form */}
<form onSubmit={handleSubmit} className="mt-4 flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask for metrics or draft an invoice..."
disabled={isLoading}
className="flex-1 rounded-lg border border-neutral-300 px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-neutral-900 disabled:opacity-50"
/>
<button
type="submit"
disabled={isLoading}
className="bg-neutral-900 text-white rounded-lg px-5 py-2 text-sm font-medium hover:bg-neutral-800 disabled:opacity-50 transition"
>
{isLoading ? 'Thinking...' : 'Send'}
</button>
</form>
</main>
);
}
3. Атрыманне компонентаў UI
У зямест на выкарыстоўванне необработанага JSON, вы перадаеце выход датыфайну шэмы безпасова на компаненты прыводу.
Ось файл components/tools/MetricsCard.tsx:
interface MetricsCardProps {
data: {
accountId: string;
timeframe: string;
revenue: number;
activeUsers: number;
churnRisk: string;
};
}
export function MetricsCard({ data }: MetricsCardProps) {
return (
<div className="my-3 bg-white border border-neutral-200 rounded-xl p-4 shadow-sm text-neutral-800">
<div className="flex items-center justify-between pb-2 border-b border-neutral-100 mb-3">
<span className="font-mono text-xs text-neutral-400">ID: {data.accountId}</span>
<span className="bg-emerald-50 text-emerald-700 text-xs px-2 py-0.5 rounded-full font-medium">
Window: {data.timeframe}
</span>
</div>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<p className="text-xs text-neutral-400 uppercase font-semibold">Revenue</p>
<p className="text-lg font-bold">${data.revenue.toLocaleString()}</p>
</div>
<div>
<p className="text-xs text-neutral-400 uppercase font-semibold">Users</p>
<p className="text-lg font-bold">{data.activeUsers}</p>
</div>
<div>
<p className="text-xs text-neutral-400 uppercase font-semibold">Churn Risk</p>
<p className="text-lg font-bold capitalize text-emerald-600">{data.churnRisk}</p>
</div>
</div>
</div>
);
}
А таксама файл components/tools/InvoiceCard.tsx:
interface InvoiceCardProps {
data: {
invoiceId: string;
status: string;
recipientEmail: string;
amount: number;
currency: string;
createdAt: string;
};
}
export function InvoiceCard({ data }: InvoiceCardProps) {
return (
<div className="my-3 bg-white border border-neutral-200 rounded-xl p-4 shadow-sm text-neutral-800">
<div className="flex justify-between items-center mb-2">
<h4 className="text-xs uppercase font-bold tracking-wider text-neutral-400">Invoice Draft Created</h4>
<span className="text-xs bg-amber-100 text-amber-800 px-2 py-0.5 rounded font-mono font-medium">
{data.status}
</span>
</div>
<p className="text-xl font-bold font-mono my-1">
${data.amount} <span className="text-xs font-sans text-neutral-400">{data.currency}</span>
</p>
<p className="text-xs text-neutral-500">Recipient: <span className="text-neutral-700">{data.recipientEmail}</span></p>
<div className="mt-3 flex gap-2">
<button
onClick={() => alert(`Invoice ${data.invoiceId} sent!`)}
className="w-full bg-neutral-900 hover:bg-neutral-800 text-white text-xs font-semibold py-1.5 rounded transition"
>
Approve & Send
</button>
</div>
</div>
);
}
4. Урокі з рэальнай експлуатацыі
Перанесенне такога інтэфейсу з localhost у режым жывойя дыстанцыйнай експлуатацыі часта прыводзіць да быстрага выяўлення спецыяльных кейсаў. Памяркавайце следуючыя захоны.
1. Налаганне таймаутаў на сервер у Vercel
Цыклі агента, якіе выкарыстоўваюць кальчаванне кількох вызоваў інструментаў, можу лёгка перасягнуць стандартны таймаут для функцый без сервера (зазвычай 10–15 секунд). Бяспечцеся, што у вашай маршрутнай структуры App Router будзе задана явна трываласць выконання:
export const maxDuration = 60; // Set according to your deployment plan limits
2. Ідэмпотентнасц экспанацыі інструментаў
Калі модель неправяча адразу свой выход або перзапускае які-небудзь крок з-за нечыстых інструкцый, яна можа выклікаць інструмент мутацыі больш чым адна раз. Будзь-які інструмент для зберагчыцтва дадзеных, такі як той, што запрашоўвае платеж або стварае новы запис у абліку, павінен выкарыстоўваць ключ ідэмпотэнцыі, які можа быць паданы з самай інструкцыі або выведзены з ідэнтыфікатора сесіі.
3. Пяракананне з участю чалавека
Агенту ніколі не павінна быць дазволена самастоятельна выканаць незворачныя операцыі, такія як выдаленне записаў, адправка рэальных электраняў або пераказ грошаў.
- Агенту следуе стварыць толькі праект стану, пазначаны чымось у дзеўянстве
status: 'pending_approval'. - Паказаць інтэрактыўную картку, якая містіць кнопку ручнага пяраканання.
- Заставіць корыстніка явна санкціонаваць і запусціць дзеянне через стандартную функцыю Next.js Server Action.
Новы стандарт для інтэрфейсаў AI
Звычныя падлокі для дапрацоўкі чат-паведамленняя стаюць чымось з праўёнага. Сягоднішнія корыстувальнікі не шукаюць аб’екта для дыялогу без фізычнага відображэння; яны хочуць інструменты, якія зменшуюць перакосы у рабочым процесе, заставаючы іх у моці прыймання рашэнняў.
Спалучаючы стрімінг Next.js App Router з типаваным рэантаймам інструментаў AI SDK, вы ператвараеце мовныя моделі на надзеяныя функцыональныя елементы, а не на непрадбачальных генератораў тексту.
Рэзультатам яе ёсць система, якая спалюе прыемлівасць строго типаваных компонентаў TypeScript з гнучкай спроможнасцю доўгахвілевых модэляў, што якраз і є балансам, неабходным для корпаратыўнага програмнага забезпечэння.