Tous les articles
·Ingénierie·9 min

Vercel AI SDK dans Next.js : streaming, tool calling et gestion du contexte en 2026

Intégrez le Vercel AI SDK dans Next.js App Router : streaming de réponses, tool calling, historique de conversation et bonnes pratiques production.

Le Vercel AI SDK a changé la façon d'intégrer l'IA dans une application Next.js. Là où il fallait autrefois gérer manuellement les streams de l'API OpenAI ou Anthropic, le SDK abstrait la complexité et expose des primitives cohérentes — côté serveur avec streamText et generateObject, côté client avec useChat et useCompletion. En 2026, c'est devenu le standard de facto pour les projets Next.js App Router qui embarquent de l'IA générative.

Ce guide couvre l'essentiel : installation, streaming de réponses, tool calling, gestion de l'historique de conversation, génération d'objets structurés et pièges courants à éviter en production. Les exemples utilisent le provider Anthropic (Claude), mais la même logique s'applique à OpenAI, Google, Mistral ou tout autre provider supporté.

Interface de chat avec streaming de réponses IA intégré dans une application Next.js
Chat IA avec streaming en temps réel dans Next.js App Router via le Vercel AI SDK

Installation et configuration initiale

bun add ai @ai-sdk/anthropic
# ou avec npm/pnpm selon votre projet
npm install ai @ai-sdk/anthropic

Le SDK sépare le core (ai) des providers (@ai-sdk/anthropic, @ai-sdk/openai, etc.). Cette séparation permet de changer de modèle sans toucher au code applicatif — seule l'initialisation du provider change. Centraliser cette configuration dans un module dédié est la première bonne pratique à adopter : si vous décidez de migrer d'OpenAI vers Anthropic, ou de tester un nouveau modèle, une seule ligne change dans toute la codebase.

// lib/ai.ts
import { anthropic } from '@ai-sdk/anthropic'

export const model = anthropic('claude-sonnet-5')
// Pour switcher sur OpenAI : import { openai } from '@ai-sdk/openai' puis openai('gpt-4o')

La variable ANTHROPIC_API_KEY est récupérée automatiquement depuis process.env. Pas de configuration supplémentaire — Next.js et le SDK gèrent ça nativement côté serveur.

Route Handler pour le streaming de réponses

Le streaming côté serveur repose sur un Route Handler Next.js qui retourne un ReadableStream HTTP. Le SDK formate ce stream avec toDataStreamResponse() dans un protocole que useChat côté client sait consommer directement. Ce protocole inclut les métadonnées de fin de stream, les erreurs, et les données des tool calls — tout ce dont le hook client a besoin.

// app/api/chat/route.ts
import { streamText } from 'ai'
import { model } from '@/lib/ai'
import { z } from 'zod'

const requestSchema = z.object({
  messages: z.array(z.object({
    role: z.enum(['user', 'assistant']),
    content: z.string().min(1).max(10000),
  })).max(50), // Limiter l'historique entrant
})

export async function POST(req: Request) {
  // Valider l'entrée — ne jamais faire confiance au corps d'une requête
  const body = requestSchema.parse(await req.json())

  const result = streamText({
    model,
    system: 'Tu es un assistant expert en développement Next.js.',
    messages: body.messages,
    maxTokens: 2048,
    temperature: 0.7,
  })

  return result.toDataStreamResponse()
}

Deux points importants : streamText retourne un objet lazy — le stream ne commence qu'à l'appel de toDataStreamResponse(). Et valider l'entrée avec Zod n'est pas optionnel — les inputs non validés constituent un vecteur d'injection de prompt, particulièrement critique dans un contexte où le modèle a accès à des outils côté serveur.

Hook useChat côté client

useChat gère automatiquement l'état des messages, l'envoi de requêtes, la réception du stream et les états de chargement. C'est lui qui transforme un Route Handler lambda en expérience de chat fluide, sans gérer manuellement fetch, ReadableStream ou AbortController. L'option experimental_throttle est particulièrement utile pour les modèles rapides.

// components/Chat.tsx
'use client'

import { useChat } from 'ai/react'
import { useEffect, useRef } from 'react'

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit, isLoading, stop } = useChat({
    api: '/api/chat',
    experimental_throttle: 50, // Re-renders limités à toutes les 50ms
  })

  const bottomRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages])

  return (
    <div className="flex flex-col h-screen max-w-2xl mx-auto">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((m) => (
          <div key={m.id} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
            <div className={`p-3 rounded-lg max-w-[80%] ${m.role === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-100'}`}>
              {m.content}
            </div>
          </div>
        ))}
        <div ref={bottomRef} />
      </div>
      <form onSubmit={handleSubmit} className="p-4 border-t flex gap-2">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Votre message..."
          className="flex-1 border rounded-lg p-2"
          disabled={isLoading}
        />
        {isLoading ? (
          <button type="button" onClick={stop} className="px-4 py-2 bg-red-500 text-white rounded-lg">
            Stop
          </button>
        ) : (
          <button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded-lg">
            Envoyer
          </button>
        )}
      </form>
    </div>
  )
}

experimental_throttle: 50 limite les re-renders à 50ms minimum — sans ça, les modèles rapides qui émettent des tokens courts peuvent déclencher des dizaines de renders par seconde sur des conversations longues. Le bouton stop appelle AbortController en interne et interrompt proprement le stream côté client comme côté serveur.

Tool calling : étendre les capacités du modèle

Le tool calling permet au modèle d'appeler des fonctions définies côté serveur — interroger une base de données, appeler une API externe, effectuer des calculs. Le SDK valide automatiquement les arguments avec Zod avant d'exécuter la fonction, ce qui garantit la sécurité des types sans code de validation manuel.

// app/api/chat/route.ts — avec tools
import { streamText, tool } from 'ai'
import { z } from 'zod'
import { db } from '@/lib/db' // Prisma client

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

  const result = streamText({
    model,
    messages,
    tools: {
      searchArticles: tool({
        description: 'Recherche des articles par mot-clé dans la base de données',
        parameters: z.object({
          query: z.string().describe('Terme de recherche'),
          limit: z.number().min(1).max(10).default(5),
        }),
        // execute s'exécute côté serveur uniquement, jamais exposé au client
        execute: async ({ query, limit }) => {
          return db.article.findMany({
            where: { title: { contains: query, mode: 'insensitive' } },
            take: limit,
            select: { title: true, slug: true, description: true },
          })
        },
      }),
    },
    maxSteps: 5, // Crucial : permet plusieurs tours tool call → réponse finale
  })

  return result.toDataStreamResponse()
}

maxSteps est le paramètre le plus souvent oublié : sans lui, le modèle s'arrête après avoir appelé un outil sans formuler de réponse finale pour l'utilisateur. Avec maxSteps: 5, il peut enchaîner jusqu'à 5 appels successifs avant de rendre la main — utile pour des workflows multi-étapes (chercher des données, les analyser, formuler une recommandation).

Gestion de l'historique et persistance

useChat maintient l'historique en mémoire locale — il disparaît au refresh. Pour persister les conversations, il faut sauvegarder les messages côté serveur à chaque échange complet. Le SDK expose un callback onFinish déclenché une fois la réponse complète reçue, avec le texte final et les métriques de tokens.

// Persister après chaque réponse complète
const result = streamText({
  model,
  messages,
  onFinish: async ({ text, usage, finishReason }) => {
    await db.message.create({
      data: {
        conversationId,
        role: 'assistant',
        content: text,
        promptTokens: usage.promptTokens,
        completionTokens: usage.completionTokens,
        finishReason,
      },
    })
  },
})

Pour charger une conversation existante, passer les messages via initialMessages dans useChat. Le hook les intègre dans l'historique local sans requête supplémentaire au montage du composant.

Générer des objets structurés avec generateObject

Pour l'extraction de données, la classification ou la génération de contenu structuré, generateObject est plus adapté que streamText. Il force le modèle à répondre selon un schéma Zod — pas de JSON.parse manuel, pas de validation custom, et des retries automatiques si le modèle produit un JSON non conforme.

import { generateObject } from 'ai'
import { model } from '@/lib/ai'
import { z } from 'zod'

const articleMetaSchema = z.object({
  seoTitle: z.string().max(60).describe('Titre SEO court et percutant'),
  seoDescription: z.string().min(140).max(155),
  readingTimeMinutes: z.number().int().positive(),
  tags: z.array(z.string()).max(5),
})

export async function generateArticleMeta(content: string) {
  const { object } = await generateObject({
    model,
    schema: articleMetaSchema,
    prompt: `Génère les métadonnées SEO pour cet article :\n\n${content.slice(0, 3000)}`,
  })

  // object est inféré automatiquement depuis articleMetaSchema
  return object
}

En pratique, les modèles récents (Claude Sonnet, GPT-4o) sont très fiables sur les schémas Zod simples — les retries restent rares. Pour les schémas complexes avec des unions ou des discriminants, préférer décomposer en plusieurs appels plutôt que d'empiler la complexité dans un seul schéma.

En pratique

En production sur des projets Next.js embarquant de l'IA, quelques patterns s'imposent rapidement. Centraliser l'initialisation du modèle dans lib/ai.ts pour pouvoir changer de provider en une ligne. Toujours valider les messages entrants avec Zod avant de les passer au SDK. Encapsuler les tools dans des modules séparés (lib/ai/tools/) plutôt que les définir inline dans le Route Handler quand ils deviennent nombreux. Logguer systématiquement les métriques tokens dans onFinish — sans ça, la première facture surprise arrivera plus tôt que prévu.

Pour l'observabilité en production, Langfuse et Helicone s'intègrent via des wrappers compatibles avec le SDK : ils capturent les traces, les coûts et les latences par modèle sans modifier le code applicatif.

Le SDK s'intègre naturellement avec la stack Kreio : Zod pour la validation des inputs avant tout appel au modèle, Prisma pour la persistance des conversations et des métriques, et Better Auth ou Clerk pour sécuriser les Route Handlers IA derrière une authentification.

Sources