Tutorial · TypeScript · AI

Vercel AI SDK 7: φτιάξε το δικό σου chatbot, βήμα βήμα

Από το πρώτο streaming μήνυμα μέχρι tools, durable agents και telemetry. Τι άλλαξε στην έκδοση 7 και πώς το χρησιμοποιείς σε πραγματικό project.

thedarkmonster.com · ~22 λεπτά ανάγνωση · Οκτώβριος 2026

01 Τι φέρνει το SDK 7

Το AI SDK 7 κυκλοφόρησε τον Ιούνιο του 2026 και, σύμφωνα με τη Vercel, δεν είναι απλώς ένα wrapper για κλήσεις μοντέλων. Στοχεύει σε agents που τρέχουν σε production, με υποστήριξη για text, audio, realtime, εικόνα και video στους μεγάλους providers.

♾️

WorkflowAgent

Durable execution για agents μεγάλης διάρκειας, μέσω του @ai-sdk/workflow.

📡

Νέο telemetry

Ξεχωριστό πακέτο @ai-sdk/otel με OpenTelemetry GenAI spans, callbacks και στατιστικά ανά step.

🧰

HarnessAgent

Ίδιο API για Claude Code, Codex, Pi. Αλλάζεις harness όπως αλλάζεις μοντέλο.

🧠

Reasoning & tools

Top-level reasoning, typed tool context, tool approvals και λεπτομερή timeouts.

🖥️

Terminal UI

Το @ai-sdk/tui σου επιτρέπει να δοκιμάσεις έναν agent στο τερματικό πριν γράψεις UI.

📦

Uploads

Provider-independent αρχεία, ώστε το ίδιο attachment να δουλεύει σε διαφορετικά μοντέλα.

Προσοχή: το SDK 7 απαιτεί ESM imports (import ή αρχεία .mjs). Αν το project σου χρησιμοποιεί require(), θα χρειαστεί μετατροπή.

02 Αρχιτεκτονική του chatbot

Το chatbot που θα φτιάξουμε έχει τρία κομμάτια: ένα UI που στέλνει μηνύματα, ένα route που μιλάει με το μοντέλο και ένα stream που γυρίζει πίσω token-token.

useChatReact UI /api/chatstreamText AI Gatewayrouting ΜοντέλοClaude / GPT / ... stream απάντησης (UI message stream)

03 Εγκατάσταση

  1. Φτιάξε project: npx create-next-app@latest my-chat
  2. Πρόσθεσε τα πακέτα: pnpm add ai @ai-sdk/react zod
  3. Βάλε στο .env.local το κλειδί του AI Gateway (AI_GATEWAY_API_KEY) ή τον provider που προτιμάς.

04 Το API route

Το streamText καλεί το μοντέλο και επιστρέφει stream. Το convertToModelMessages μετατρέπει τα μηνύματα του UI στη μορφή που καταλαβαίνει το μοντέλο.

app/api/chat/route.ts
import { streamText, convertToModelMessages, type UIMessage } from 'ai';

export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: 'anthropic/claude-sonnet-4.5', // άλλαξέ το με όποιο μοντέλο θέλεις
    system: 'Είσαι ένας φιλικός βοηθός. Απαντάς σύντομα και καθαρά.',
    messages: await convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse();
}
Το μοντέλο δίνεται ως απλό string (provider/model), οπότε η αλλαγή μοντέλου είναι αλλαγή μίας γραμμής.

05 Το UI με useChat

Το useChat κρατά τα μηνύματα, το status και το streaming. Κάθε μήνυμα έχει parts: κείμενο, reasoning, tool calls κ.ά., γι' αυτό τα εμφανίζεις ανά τύπο.

app/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';

export default function Chat() {
  const { messages, sendMessage, status } = useChat();
  const [input, setInput] = useState('');

  return (
    <main>
      {messages.map(m => (
        <div key={m.id}>
          <b>{m.role}:</b>{' '}
          {m.parts.map((p, i) =>
            p.type === 'text' ? <span key={i}>{p.text}</span> : null)}
        </div>
      ))}
      <form onSubmit={e => {
        e.preventDefault();
        if (!input.trim()) return;
        sendMessage({ text: input });
        setInput('');
      }}>
        <input value={input} onChange={e => setInput(e.target.value)}
               disabled={status !== 'ready'} placeholder="Γράψε κάτι..." />
      </form>
    </main>
  );
}

Τρέξε pnpm dev και θα έχεις ήδη ένα chatbot με streaming. Το status (submitted, streaming, ready, error) σου δίνει όλα όσα χρειάζεσαι για loading states και κουμπί «Stop».

06 Πώς δουλεύει το streaming

Όταν στέλνεις μήνυμα, το route δεν περιμένει να ολοκληρωθεί η απάντηση. Το μοντέλο παράγει tokens και το SDK τα προωθεί στον browser μέσω ενός UI message stream, πάνω από HTTP με server-sent events. Ο browser δεν λαμβάνει απλό κείμενο αλλά τυποποιημένα γεγονότα, και αυτό είναι που επιτρέπει στο useChat να χτίζει τα parts χωρίς δικό σου parsing.

έναρξη μηνύματος→κείμενο (δέλτα)→κλήση tool→αποτέλεσμα tool→κείμενο (δέλτα)→τέλος
⚡

Time to first token

Ο χρήστης βλέπει την πρώτη λέξη σε κλάσματα δευτερολέπτου, αντί να περιμένει ολόκληρη την απάντηση. Η αίσθηση ταχύτητας μετράει περισσότερο από τον συνολικό χρόνο.

🛑

Ακύρωση

Αν ο χρήστης πατήσει Stop ή κλείσει το tab, το request ακυρώνεται και σταματάς να πληρώνεις tokens που κανείς δεν θα διαβάσει.

🧩

Parts αντί για string

Κείμενο, reasoning και tool calls έρχονται ως ξεχωριστά κομμάτια, άρα μπορείς να τα σχεδιάσεις διαφορετικά (π.χ. collapsible reasoning).

07 Καλύτερο UX: Stop, Retry, λάθη και reasoning

Ένα chatbot που δουλεύει στο ευτυχές σενάριο δεν αρκεί. Το useChat επιστρέφει έτοιμα εργαλεία για τα υπόλοιπα:

app/page.tsx (αλλαγές)
const { messages, sendMessage, status, stop, regenerate, error } = useChat();

{status === 'streaming' && <button onClick={stop}>Stop</button>}
{error && <button onClick={() => regenerate()}>Ξαναδοκίμασε</button>}

// μέσα στο m.parts.map:
p.type === 'reasoning' ? <details key={i}><summary>Σκέψη</summary>{p.text}</details> : null

Στον server, μην αφήνεις να διαρρέουν εσωτερικά μηνύματα σφάλματος (URLs, κλειδιά, stack traces) στον χρήστη. Το toUIMessageStreamResponse δέχεται onError, όπου επιστρέφεις ένα ουδέτερο μήνυμα:

return result.toUIMessageStreamResponse({
  onError: () => 'Κάτι πήγε στραβά. Δοκίμασε ξανά.',
});
Συμβουλή: το reasoning είναι χρήσιμο για debugging, αλλά δεν είναι πάντα καλή ιδέα να το δείχνεις στους τελικούς χρήστες. Βάλε το πίσω από ένα collapsible ή απενεργοποίησέ το σε production.

08 Αποθήκευση συνομιλιών

Χωρίς persistence, ένα refresh σβήνει τη συζήτηση. Το πρότυπο είναι απλό: κάθε συνομιλία έχει ένα chatId, αποθηκεύεις τα μηνύματα όταν τελειώνει η απάντηση και τα φορτώνεις ως αρχική κατάσταση.

  1. Δημιούργησε chatId (π.χ. στο URL /chat/[id]) και πίνακα στη βάση σου (Postgres, Redis, ό,τι χρησιμοποιείς).
  2. Στο route πέρνα τα προηγούμενα μηνύματα και αποθήκευσε το αποτέλεσμα στο onFinish.
  3. Στον client φόρτωσε τα αποθηκευμένα μηνύματα ως αρχική τιμή.
route.ts · client
// server
return result.toUIMessageStreamResponse({
  originalMessages: messages,
  onFinish: async ({ messages }) => { await saveChat(chatId, messages); },
});

// client
const chat = useChat({ id: chatId, messages: initialMessages });

Το saveChat και το initialMessages είναι δικές σου συναρτήσεις. Το SDK δεν σε δεσμεύει σε συγκεκριμένη βάση.

09 Δώσε του tools

Ένα chatbot γίνεται χρήσιμο όταν μπορεί να κάνει πράγματα: να ψάξει, να υπολογίσει, να καλέσει ένα API. Ορίζεις το tool με schema (zod) και συνάρτηση execute.

route.ts (προσθήκη)
import { tool, isStepCount } from 'ai';
import { z } from 'zod';

const result = streamText({
  model: 'anthropic/claude-sonnet-4.5',
  messages: await convertToModelMessages(messages),
  stopWhen: isStepCount(5), // μέχρι 5 γύροι tool → απάντηση
  tools: {
    weather: tool({
      description: 'Επιστρέφει τη θερμοκρασία μιας πόλης',
      inputSchema: z.object({ city: z.string() }),
      execute: async ({ city }) => ({ city, tempC: 21 }), // εδώ καλείς πραγματικό API
    }),
  },
});

Το μοντέλο αποφασίζει πότε θα καλέσει το tool, το SDK εκτελεί τη συνάρτηση και στέλνει το αποτέλεσμα πίσω στο μοντέλο, που συνεχίζει την απάντηση. Όλο αυτό ονομάζεται tool loop.

Approvals για επικίνδυνες ενέργειες

Στο SDK 7 οι εγκρίσεις (human-in-the-loop) χτίζονται μέσα στο tool layer. Για tools που στέλνουν email, σβήνουν δεδομένα ή χρεώνουν, ζητάς επιβεβαίωση του χρήστη πριν εκτελεστούν. Δες στα docs τη ακριβή σύνταξη της έκδοσής σου.

10 Structured output: απαντήσεις σε JSON, όχι σε κείμενο

Όταν το αποτέλεσμα θα το καταναλώσει κώδικας (φόρμα, βάση, UI κάρτα), δεν θες ελεύθερο κείμενο. Δίνεις ένα zod schema και το SDK επιστρέφει τυποποιημένο αντικείμενο, επικυρωμένο απέναντι στο schema.

lib/extract.ts
import { generateText, Output } from 'ai';
import { z } from 'zod';

const { output } = await generateText({
  model: 'anthropic/claude-sonnet-4.5',
  output: Output.object({
    schema: z.object({
      sentiment: z.enum(['θετικό', 'ουδέτερο', 'αρνητικό']),
      topics: z.array(z.string()).describe('Έως 3 θέματα'),
      urgent: z.boolean(),
    }),
  }),
  prompt: 'Ταξινόμησε: "Το site έπεσε ξανά και χάνουμε πελάτες!"',
});
// output.sentiment, output.topics, output.urgent: πλήρως typed

Καλά schemas

Enums αντί για ελεύθερα strings και .describe() σε κάθε πεδίο. Το μοντέλο διαβάζει τις περιγραφές.

Πού αξίζει

Ταξινόμηση tickets, εξαγωγή στοιχείων από email, παραγωγή quiz, routing αιτημάτων σε agents.

Το schema επικυρώνει τη μορφή, όχι την αλήθεια. Ένα έγκυρο JSON μπορεί να έχει λάθος τιμές, οπότε έλεγξε τα κρίσιμα πεδία.

11 Πλήρες tutorial RAG: chatbot που απαντά από τα δικά σου έγγραφα

Το μοντέλο δεν ξέρει τα εσωτερικά σου έγγραφα και μπορεί να «εφεύρει» απαντήσεις. Το RAG (Retrieval-Augmented Generation) το λύνει: πριν απαντήσει, το chatbot ψάχνει στα έγγραφά σου και χρησιμοποιεί μόνο ό,τι βρήκε.

έγγραφο→chunks→embeddings→vector store⇢ερώτηση→top-k αποτελέσματα→μοντέλο→απάντηση

Βήμα 1: ingestion και αναζήτηση

Σπάμε το κείμενο σε κομμάτια με μικρή επικάλυψη, τα μετατρέπουμε σε διανύσματα (embeddings) και τα αποθηκεύουμε. Εδώ χρησιμοποιώ μνήμη, ώστε να τρέχει χωρίς βάση.

lib/rag.ts
import { embed, embedMany, cosineSimilarity } from 'ai';

const embeddingModel = 'openai/text-embedding-3-small';
const store: { text: string; embedding: number[] }[] = [];

export function chunk(text: string, size = 800, overlap = 100) {
  const out: string[] = [];
  for (let i = 0; i < text.length; i += size - overlap) out.push(text.slice(i, i + size));
  return out;
}

export async function ingest(text: string) {
  const chunks = chunk(text);
  const { embeddings } = await embedMany({ model: embeddingModel, values: chunks });
  chunks.forEach((t, i) => store.push({ text: t, embedding: embeddings[i] }));
}

export async function search(query: string, k = 4) {
  const { embedding } = await embed({ model: embeddingModel, value: query });
  return store
    .map(c => ({ text: c.text, score: cosineSimilarity(embedding, c.embedding) }))
    .sort((a, b) => b.score - a.score)
    .slice(0, k);
}

Βήμα 2: η αναζήτηση γίνεται tool

Δίνουμε το search στο μοντέλο ως tool, ώστε να αποφασίζει μόνο του πότε χρειάζεται (π.χ. όχι για ένα «γεια σου»).

app/api/chat/route.ts
import { search } from '@/lib/rag';

const result = streamText({
  model: 'anthropic/claude-sonnet-4.5',
  system: 'Απάντα ΜΟΝΟ με βάση τα αποτελέσματα του searchDocs. ' +
          'Αν δεν βρεις σχετική πληροφορία, πες ότι δεν ξέρεις.',
  messages: await convertToModelMessages(messages),
  stopWhen: isStepCount(4),
  tools: {
    searchDocs: tool({
      description: 'Ψάχνει στη βάση γνώσης και επιστρέφει σχετικά αποσπάσματα',
      inputSchema: z.object({ query: z.string() }),
      execute: async ({ query }) => (await search(query)).map(r => r.text),
    }),
  },
});

Βήμα 3: φόρτωσε έγγραφα

Φτιάξε ένα route (π.χ. /api/ingest) που δέχεται κείμενο και καλεί ingest(text). Για PDF, εξήγαγε πρώτα το κείμενο με κάποια βιβλιοθήκη.

ΠαράμετροςΤι ρυθμίζειΠρακτική συμβουλή
Μέγεθος chunkΠόση πληροφορία έχει κάθε κομμάτιΠολύ μικρό χάνει νόημα, πολύ μεγάλο θολώνει την αναζήτηση. Ξεκίνα στους 500-1000 χαρακτήρες.
OverlapΕπικάλυψη μεταξύ chunks10-15%, ώστε να μην κόβονται προτάσεις στη μέση.
k (top-k)Πόσα αποσπάσματα στέλνεις στο μοντέλο3-6. Περισσότερα σημαίνει θόρυβο και κόστος.
Για production: η μνήμη χάνεται σε κάθε restart. Αντικατάστησε το store με vector βάση (π.χ. Postgres με pgvector). Το ίδιο μοντέλο embeddings πρέπει να χρησιμοποιείται σε ingestion και αναζήτηση. Πρόσθεσε metadata (πηγή, σελίδα) ώστε το chatbot να παραθέτει από πού πήρε την απάντηση.

12 Όταν ο agent χρειάζεται ώρες: WorkflowAgent

Ένα απλό request/response σταματά να αρκεί όταν ο agent περιμένει έγκριση, καλεί αργά APIs ή τρέχει για λεπτά. Το WorkflowAgent προσθέτει durable execution: αν ο server κάνει restart ή το function λήξει, η εκτέλεση συνεχίζει από εκεί που έμεινε αντί να ξεκινά από την αρχή.

ΑνάγκηΛύση στο SDK 7
Απλό chat με streamingstreamText + useChat
Πολλαβλά βήματα με toolsTool loop + stopWhen
Μακρά, ανθεκτική εκτέλεσηWorkflowAgent (@ai-sdk/workflow)
Coding agents (Claude Code, Codex, Pi)HarnessAgent σε sandbox

Το HarnessAgent επιστρέφει αποτελέσματα συμβατά με το AI SDK, άρα μπορεί να αντικαταστήσει τον agent σου χωρίς να αλλάξεις το UI με useChat. Τη στιγμή που γράφεται το άρθρο, οι πρώτες εκδόσεις του ήταν διαθέσιμες στο canary κανάλι, οπότε έλεγξε την κατάστασή του πριν το βάλεις σε production.

13 Πλήρες παράδειγμα WorkflowAgent

Ο στόχος: ένας agent αναζήτησης πτήσεων που επιβιώνει από restarts. Το WorkflowAgent τρέχει το ίδιο agent loop με το ToolLoopAgent, αλλά μέσα σε workflow, και αντικαθιστά το παλιό DurableAgent.

pnpm add @ai-sdk/workflow workflow ai zod

Το workflow δίνει το runtime (getWritable, οι οδηγίες 'use workflow' και 'use step'). Τα ai και zod είναι peer dependencies.

workflow/agent-chat.ts
import { WorkflowAgent } from '@ai-sdk/workflow';
import { tool, isStepCount, type UIMessage } from 'ai';
import { getWritable } from 'workflow';
import { z } from 'zod';

const searchFlights = tool({
  description: 'Ψάχνει πτήσεις',
  inputSchema: z.object({ from: z.string(), to: z.string() }),
  execute: async ({ from, to }) => {
    'use step'; // durable κλήση: retries + αποθήκευση αποτελέσματος
    const r = await fetch(`https://api.flights.example/search?from=${from}&to=${to}`);
    return r.json();
  },
});

export async function chatWorkflow(messages: UIMessage[]) {
  'use workflow'; // η συνάρτηση γίνεται durable workflow

  const agent = new WorkflowAgent({
    model: 'anthropic/claude-sonnet-4.5',
    instructions: 'Βοηθάς στην εύρεση πτήσεων.',
    tools: { searchFlights },
    stopWhen: isStepCount(10),
  });

  // Το stream γράφεται στο getWritable(). Δες στα docs την ακριβή
  // υπογραφή του agent.stream() και τον μετασχηματισμό σε UI chunks.
}

Στο frontend αντικαθιστάς το transport του useChat με το WorkflowChatTransport, που επανασυνδέεται αυτόματα στο stream αν κοπεί η σύνδεση ή γίνει refresh.

app/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
import { WorkflowChatTransport } from '@ai-sdk/workflow';

const { messages, sendMessage } = useChat({
  transport: new WorkflowChatTransport({ api: '/api/chat' }),
});
🔁

Retries

Κάθε tool με 'use step' έχει retries και δική του γραμμή στο workflow dashboard.

⏸️

Έγκριση χρήστη

Ο agent παγώνει μέχρι να εγκρίνει ο άνθρωπος και συνεχίζει χωρίς να χάσει κατάσταση.

📦

Serializable context

Στο context μπαίνουν μόνο δεδομένα, όχι functions ή clients βάσης. Δημιούργησέ τα μέσα στα steps.

Κανόνες που πιάνουν τους περισσότερους: (1) Tools με primitives του workflow, όπως sleep() ή createHook(), ΔΕΝ παίρνουν 'use step'. (2) Ό,τι δεν έχει 'use step' τρέχει ως κανονική συνάρτηση μνήμης, χωρίς εγγυήσεις durability. (3) Το maxSteps αντικαταστάθηκε από τα κοινά stop conditions, π.χ. isStepCount(10).

14 Παρατήρηση και debugging

Στο SDK 6 έβαζες experimental_telemetry σε κάθε κλήση. Στο 7 το telemetry είναι ξεχωριστό και καταχωρείται μία φορά για όλες τις συναρτήσεις του SDK. Το @ai-sdk/otel εκπέμπει spans και metrics σύμφωνα με τα OpenTelemetry GenAI conventions, άρα συνδέεται με Datadog, Grafana, Honeycomb και όποιο OTel backend χρησιμοποιείς.

Τι μετράς

Latency ανά step, token usage, tool calls, σφάλματα.

Γιατί έχει σημασία

Ένας agent που «κολλάει» ή ξοδεύει tokens δεν φαίνεται χωρίς traces.

15 Σύνοψη αλλαγών του SDK 7

ΑλλαγήΤι σημαίνει για σένα
Νέο telemetry (@ai-sdk/otel)Ρυθμίζεις το tracing μία φορά και καλύπτει όλες τις κλήσεις του SDK.
WorkflowAgentAgents που επιβιώνουν από restarts και τρέχουν για πολλή ώρα.
HarnessAgentΕνιαίο API για Claude Code, Codex, Pi, μέσα σε sandbox.
Top-level reasoningΡυθμίζεις το reasoning με ενιαίο τρόπο, χωρίς εξαρτήσεις από τον provider.
Provider-independent uploadsΤο ίδιο αρχείο δουλεύει σε διαφορετικά μοντέλα.
Typed tool contextΤο context που περνάς στα tools ελέγχεται από τον TypeScript compiler.
Granular timeoutsΞεχωριστά όρια για συνολική διάρκεια, ανά step και ανά chunk.
ESM onlyΧρειάζεται import. Το require() δεν υποστηρίζεται.

16 Μετάβαση από το SDK 6

  1. Κάνε ενημέρωση: pnpm add ai@latest
  2. Τρέξε το codemod: npx @ai-sdk/codemod v7
  3. Μετάτρεψε τα imports σε ESM αν χρειάζεται.
  4. Αντικατάστησε τα experimental_telemetry με τη νέα ρύθμιση telemetry.
  5. Τρέξε τα tests και έλεγξε τα tool και streaming flows.

17 Checklist πριν το production

🔐

Ασφάλεια

Auth στο route, rate limits, ποτέ API keys στον client.

💸

Κόστος

Όρια tokens και budgets ανά κλειδί στο AI Gateway.

🛡️

Tools

Approvals για ευαίσθητες ενέργειες, validation στα inputs.

📈

Observability

Traces και alerts για λάθη και αργά steps.

18 Συχνές ερωτήσεις

Πρέπει να χρησιμοποιήσω το AI Gateway;

Όχι. Μπορείς να καλέσεις απευθείας έναν provider με τα πακέτα του (π.χ. @ai-sdk/openai). Το Gateway προσθέτει ενιαίο κλειδί, routing και budgets, αλλά είναι προαιρετικό.

Δουλεύει εκτός Next.js;

Ναι. Το core λειτουργεί σε οποιοδήποτε Node runtime και υπάρχουν UI bindings και για άλλα frameworks. Το Next.js απλώς κάνει το tutorial πιο σύντομο.

Πώς αποφεύγω ατέρμονα tool loops;

Με stopWhen: isStepCount(n). Βάλε ρεαλιστικό όριο (3 έως 8) ανάλογα με το πόσα βήματα χρειάζεται πραγματικά ο agent σου.

Πού μπαίνουν τα API keys;

Μόνο στον server (env variables). Ο client μιλάει πάντα με το δικό σου route, ποτέ απευθείας με τον provider.

Μπορώ να δοκιμάσω έναν agent χωρίς να γράψω UI;

Ναι, με το @ai-sdk/tui τον τρέχεις σε διαδραστικό τερματικό και βλέπεις reasoning, tools και markdown πριν χτίσεις εφαρμογή.

19 Συμπέρασμα

Με λιγότερο από 50 γραμμές κώδικα έχεις chatbot με streaming και tools. Το SDK 7 προσθέτει όσα χρειάζονται όταν το demo γίνεται προϊόν: durability, approvals, telemetry και ευελιξία σε μοντέλα και agent harnesses. Ξεκίνα απλά με streamText και useChat και πρόσθεσε τα υπόλοιπα μόνο όταν τα χρειαστείς.

🔌

MCP Server Guide

Στήσε δικό σου MCP server με Python & FastMCP για agentic ροές.

Διάβασέ το →

🛡️

Prompt Injection Playground

Δοκίμασε prompts ενάντια σε injection και jailbreaks πριν το production.

Άνοιξε το εργαλείο →

💰

LLM Cost Calculator

Υπολόγισε το κόστος API ανά μοντέλο και task πριν διαλέξεις.

Υπολόγισε →

Πηγές: ai-sdk.dev (επίσημα docs) · vercel.com (ανακοίνωση AI SDK 7 & changelog) · github.com/vercel/ai (repo). Οι ακριβείς υπογραφές των νέων APIs μπορεί να αλλάζουν ανά patch — επιβεβαίωσέ τες στα επίσημα docs.