Construire un Chatbot IA avec OpenAI Assistants API et Next.js

Noqta Team
Par Noqta Team ·

Chargement du lecteur de synthèse vocale...

OpenAI Assistants API est fondamentalement différent du Chat Completions API. Au lieu de gérer vous-même l'historique des conversations, Assistants API vous offre des threads persistants, des outils intégrés comme la recherche de fichiers et l'interpréteur de code, et une exécution basée sur les runs qui gère tout automatiquement.

Dans ce tutoriel, vous construirez un chatbot IA complet avec Next.js qui exploite ces capacités — du streaming des réponses au téléchargement de documents que votre assistant peut consulter.

Pourquoi Assistants API ? Le Chat Completions API exige de gérer l'état de la conversation, d'implémenter des pipelines RAG et de construire des boucles d'exécution d'outils manuellement. Assistants API gère tout cela nativement — les threads persistent automatiquement, les fichiers sont indexés pour la recherche et le code s'exécute dans un environnement sandboxé.

Ce que vous apprendrez

À la fin de ce tutoriel, vous serez capable de :

  • Créer et configurer un Assistant OpenAI avec des instructions personnalisées
  • Gérer des threads de conversation persistants
  • Streamer les réponses de l'assistant en temps réel
  • Activer la recherche de fichiers pour que votre assistant puisse répondre à partir de documents téléchargés
  • Activer l'interpréteur de code pour que votre assistant puisse écrire et exécuter du code Python
  • Construire une interface de chat soignée avec Next.js et React
  • Gérer les erreurs et implémenter les bonnes pratiques de production

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé (node --version)
  • Des connaissances en TypeScript (generics, async/await)
  • Une clé API OpenAI avec accès à Assistants API (obtenez-la sur platform.openai.com)
  • Les fondamentaux de Next.js (App Router, Server Actions)
  • Un éditeur de code — VS Code recommandé

Ce que vous allez construire

Un chatbot IA complet qui prend en charge :

  • Conversations persistantes — les utilisateurs peuvent reprendre des discussions précédentes
  • Streaming des réponses — les tokens apparaissent au fur et à mesure que l'assistant génère sa réponse
  • Q&A sur documents — téléchargez des PDF et posez des questions sur leur contenu
  • Exécution de code — l'assistant peut écrire et exécuter du code Python, retournant résultats et graphiques
  • Threads multiples — gérez des contextes de conversation séparés

Étape 1 : Configuration du projet

Créez un nouveau projet Next.js et installez les dépendances requises :

npx create-next-app@latest openai-chatbot --typescript --tailwind --app --src-dir
cd openai-chatbot

Installez le SDK OpenAI :

npm install openai

Créez votre fichier d'environnement :

# .env.local
OPENAI_API_KEY=sk-proj-your-api-key-here
OPENAI_ASSISTANT_ID=  # Nous le remplirons à l'étape 3

Ajoutez les types de variables d'environnement dans src/env.d.ts :

declare namespace NodeJS {
  interface ProcessEnv {
    OPENAI_API_KEY: string;
    OPENAI_ASSISTANT_ID: string;
  }
}

Étape 2 : Comprendre l'architecture d'Assistants API

Avant d'écrire du code, il est important de comprendre les concepts clés :

Assistant — Une entité IA configurée avec des instructions spécifiques, un modèle et des outils activés. Considérez-le comme un persona qui persiste à travers les conversations.

Thread — Une session de conversation. Les threads stockent l'historique complet des messages et sont persistants — ils survivent aux redémarrages du serveur et peuvent être repris à tout moment.

Message — Un seul message utilisateur ou assistant dans un thread. Les messages peuvent inclure du texte, des images et des pièces jointes.

Run — Une exécution de l'assistant sur un thread. Lorsque vous créez un run, l'assistant lit le thread, décide d'appeler des outils ou non, et génère une réponse.

Run Step — Une action granulaire dans un run (appel d'outil, création de message). Utile pour le débogage et l'affichage de la progression.

Le flux ressemble à ceci :

Créer Assistant → Créer Thread → Ajouter Message → Créer Run → Streamer Réponse
                                        ↑                          |
                                        └──────────────────────────┘
                                           (la conversation continue)

Étape 3 : Création de l'Assistant

Vous pouvez créer un assistant via l'API ou le tableau de bord OpenAI. Faisons-le de manière programmatique pour que la configuration vive dans le code.

Créez src/lib/openai.ts :

import OpenAI from "openai";
 
export const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

Créez un script de configuration dans scripts/create-assistant.ts :

import OpenAI from "openai";
 
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});
 
async function createAssistant() {
  const assistant = await openai.beta.assistants.create({
    name: "Noqta Assistant",
    instructions: `You are a helpful, knowledgeable assistant. Follow these rules:
- Be concise but thorough
- Use code examples when explaining technical concepts
- When using file search results, cite the source document
- When asked to analyze data, use code interpreter to create visualizations
- Always respond in the same language the user writes in`,
    model: "gpt-4o",
    tools: [
      { type: "file_search" },
      { type: "code_interpreter" },
    ],
  });
 
  console.log("Assistant created:", assistant.id);
  console.log("Add this to your .env.local:");
  console.log(`OPENAI_ASSISTANT_ID=${assistant.id}`);
}
 
createAssistant();

Exécutez-le :

npx tsx scripts/create-assistant.ts

Copiez l'identifiant de l'assistant dans votre fichier .env.local.

Astuce : Vous pouvez également créer et gérer des assistants depuis le OpenAI Playground. Le tableau de bord vous offre une interface visuelle pour ajuster les instructions et tester les outils de manière interactive.


Étape 4 : Construction de l'API de gestion des threads

Les threads sont la colonne vertébrale d'Assistants API. Créez les routes API pour les gérer.

Créez src/app/api/threads/route.ts :

import { NextResponse } from "next/server";
import { openai } from "@/lib/openai";
 
export async function POST() {
  try {
    const thread = await openai.beta.threads.create();
 
    return NextResponse.json({ threadId: thread.id });
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to create thread" },
      { status: 500 }
    );
  }
}

Ce endpoint crée un nouveau thread de conversation. Chaque thread reçoit un identifiant unique que vous stockerez côté client pour reprendre les conversations.


Étape 5 : Implémentation du streaming des réponses

C'est ici que la magie opère. Assistants API prend en charge le streaming via Server-Sent Events, ce qui donne à vos utilisateurs un effet de frappe en temps réel.

Créez src/app/api/chat/route.ts :

import { openai } from "@/lib/openai";
 
export async function POST(request: Request) {
  const { threadId, message } = await request.json();
 
  if (!threadId || !message) {
    return new Response("Missing threadId or message", { status: 400 });
  }
 
  await openai.beta.threads.messages.create(threadId, {
    role: "user",
    content: message,
  });
 
  const stream = openai.beta.threads.runs.stream(threadId, {
    assistant_id: process.env.OPENAI_ASSISTANT_ID,
  });
 
  const encoder = new TextEncoder();
 
  const readable = new ReadableStream({
    async start(controller) {
      try {
        for await (const event of stream) {
          if (event.event === "thread.message.delta") {
            const delta = event.data.delta;
            if (delta.content) {
              for (const block of delta.content) {
                if (block.type === "text" && block.text?.value) {
                  controller.enqueue(
                    encoder.encode(`data: ${JSON.stringify({
                      type: "text",
                      content: block.text.value,
                    })}\n\n`)
                  );
                }
              }
            }
          }
 
          if (event.event === "thread.run.completed") {
            controller.enqueue(
              encoder.encode(`data: ${JSON.stringify({ type: "done" })}\n\n`)
            );
          }
 
          if (event.event === "thread.run.failed") {
            controller.enqueue(
              encoder.encode(`data: ${JSON.stringify({
                type: "error",
                content: "Run failed",
              })}\n\n`)
            );
          }
        }
      } catch (err) {
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify({
            type: "error",
            content: "Stream error",
          })}\n\n`)
        );
      } finally {
        controller.close();
      }
    },
  });
 
  return new Response(readable, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

Cette route :

  1. Ajoute le message utilisateur au thread
  2. Crée un run en streaming contre l'assistant
  3. Envoie les deltas de texte au client sous forme de Server-Sent Events
  4. Signale la fin ou les erreurs

Étape 6 : Activation de la recherche de fichiers (RAG)

La recherche de fichiers permet à votre assistant de répondre aux questions à partir de documents téléchargés. L'API découpe, vectorise et indexe automatiquement vos fichiers — aucune base de données vectorielle externe nécessaire.

Créez src/app/api/files/route.ts :

import { NextResponse } from "next/server";
import { openai } from "@/lib/openai";
 
export async function POST(request: Request) {
  const formData = await request.formData();
  const file = formData.get("file") as File;
  const threadId = formData.get("threadId") as string;
 
  if (!file || !threadId) {
    return NextResponse.json(
      { error: "Missing file or threadId" },
      { status: 400 }
    );
  }
 
  const uploadedFile = await openai.files.create({
    file,
    purpose: "assistants",
  });
 
  await openai.beta.threads.messages.create(threadId, {
    role: "user",
    content: "I've uploaded a document for reference.",
    attachments: [
      {
        file_id: uploadedFile.id,
        tools: [{ type: "file_search" }],
      },
    ],
  });
 
  return NextResponse.json({
    fileId: uploadedFile.id,
    fileName: file.name,
  });
}

Lorsqu'un utilisateur télécharge un fichier :

  1. Le fichier est envoyé au stockage OpenAI
  2. Il est attaché à un message dans le thread avec l'outil file_search
  3. L'assistant peut maintenant rechercher dans le contenu du document pour répondre aux questions

Types de fichiers supportés : PDF, DOCX, TXT, MD, JSON, CSV, HTML et plus encore. Chaque fichier peut atteindre 512 Mo. L'API gère automatiquement le découpage et la vectorisation — vous n'avez pas besoin de gérer un vector store manuellement pour les pièces jointes au niveau du thread.


Étape 7 : Activation de l'interpréteur de code

L'interpréteur de code permet à votre assistant d'écrire et d'exécuter du code Python dans un environnement sandboxé. C'est puissant pour l'analyse de données, les mathématiques, la génération de graphiques et le traitement de fichiers.

L'interpréteur de code est déjà activé dans la configuration de l'assistant à l'étape 3. Gérons maintenant la sortie dans la route de streaming.

Mettez à jour le gestionnaire de streaming dans src/app/api/chat/route.ts pour gérer les événements de l'interpréteur de code :

for await (const event of stream) {
  if (event.event === "thread.message.delta") {
    const delta = event.data.delta;
    if (delta.content) {
      for (const block of delta.content) {
        if (block.type === "text" && block.text?.value) {
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify({
              type: "text",
              content: block.text.value,
            })}\n\n`)
          );
        }
 
        if (block.type === "image_file" && block.image_file) {
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify({
              type: "image",
              fileId: block.image_file.file_id,
            })}\n\n`)
          );
        }
      }
    }
  }
 
  if (event.event === "thread.run.step.delta") {
    const stepDelta = event.data.delta;
    if (stepDelta.step_details?.type === "tool_calls") {
      for (const toolCall of stepDelta.step_details.tool_calls ?? []) {
        if (
          toolCall.type === "code_interpreter" &&
          toolCall.code_interpreter?.input
        ) {
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify({
              type: "code",
              content: toolCall.code_interpreter.input,
            })}\n\n`)
          );
        }
      }
    }
  }
 
  if (event.event === "thread.run.completed") {
    controller.enqueue(
      encoder.encode(`data: ${JSON.stringify({ type: "done" })}\n\n`)
    );
  }
 
  if (event.event === "thread.run.failed") {
    controller.enqueue(
      encoder.encode(`data: ${JSON.stringify({
        type: "error",
        content: "Run failed",
      })}\n\n`)
    );
  }
}

Le stream émet maintenant trois types de contenu :

  • text — messages réguliers de l'assistant
  • code — code Python exécuté par l'interpréteur de code
  • image — graphiques ou visualisations générés (retournés en tant qu'identifiants de fichiers)

Pour afficher les images générées par l'interpréteur de code, ajoutez un endpoint pour récupérer les fichiers :

// src/app/api/files/[fileId]/route.ts
import { NextResponse } from "next/server";
import { openai } from "@/lib/openai";
 
export async function GET(
  _request: Request,
  { params }: { params: Promise<{ fileId: string }> }
) {
  const { fileId } = await params;
 
  const response = await openai.files.content(fileId);
  const buffer = Buffer.from(await response.arrayBuffer());
 
  return new NextResponse(buffer, {
    headers: {
      "Content-Type": "image/png",
      "Cache-Control": "public, max-age=3600",
    },
  });
}

Étape 8 : Construction de l'interface de chat

Construisons maintenant le frontend. Créez un hook personnalisé pour gérer la connexion de streaming :

Créez src/hooks/use-assistant-chat.ts :

"use client";
 
import { useState, useCallback, useRef } from "react";
 
interface ChatMessage {
  id: string;
  role: "user" | "assistant";
  content: string;
  codeBlocks?: string[];
  images?: string[];
}
 
export function useAssistantChat() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [threadId, setThreadId] = useState<string | null>(null);
  const abortRef = useRef<AbortController | null>(null);
 
  const initThread = useCallback(async () => {
    const res = await fetch("/api/threads", { method: "POST" });
    const { threadId: id } = await res.json();
    setThreadId(id);
    return id;
  }, []);
 
  const sendMessage = useCallback(
    async (content: string) => {
      const currentThreadId = threadId ?? (await initThread());
      setIsLoading(true);
 
      const userMessage: ChatMessage = {
        id: crypto.randomUUID(),
        role: "user",
        content,
      };
 
      const assistantMessage: ChatMessage = {
        id: crypto.randomUUID(),
        role: "assistant",
        content: "",
        codeBlocks: [],
        images: [],
      };
 
      setMessages((prev) => [...prev, userMessage, assistantMessage]);
 
      abortRef.current = new AbortController();
 
      try {
        const res = await fetch("/api/chat", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            threadId: currentThreadId,
            message: content,
          }),
          signal: abortRef.current.signal,
        });
 
        const reader = res.body?.getReader();
        const decoder = new TextDecoder();
 
        if (!reader) throw new Error("No reader available");
 
        let buffer = "";
 
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
 
          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split("\n\n");
          buffer = lines.pop() ?? "";
 
          for (const line of lines) {
            if (!line.startsWith("data: ")) continue;
 
            const data = JSON.parse(line.slice(6));
 
            if (data.type === "text") {
              setMessages((prev) => {
                const updated = [...prev];
                const last = updated[updated.length - 1];
                last.content += data.content;
                return updated;
              });
            }
 
            if (data.type === "code") {
              setMessages((prev) => {
                const updated = [...prev];
                const last = updated[updated.length - 1];
                last.codeBlocks = [
                  ...(last.codeBlocks ?? []),
                  data.content,
                ];
                return updated;
              });
            }
 
            if (data.type === "image") {
              setMessages((prev) => {
                const updated = [...prev];
                const last = updated[updated.length - 1];
                last.images = [
                  ...(last.images ?? []),
                  `/api/files/${data.fileId}`,
                ];
                return updated;
              });
            }
 
            if (data.type === "done") {
              setIsLoading(false);
            }
 
            if (data.type === "error") {
              setIsLoading(false);
            }
          }
        }
      } catch (err) {
        if ((err as Error).name !== "AbortError") {
          setIsLoading(false);
        }
      }
    },
    [threadId, initThread]
  );
 
  const stopGeneration = useCallback(() => {
    abortRef.current?.abort();
    setIsLoading(false);
  }, []);
 
  const resetChat = useCallback(() => {
    setMessages([]);
    setThreadId(null);
    setIsLoading(false);
  }, []);
 
  return {
    messages,
    isLoading,
    threadId,
    sendMessage,
    stopGeneration,
    resetChat,
  };
}

Maintenant, construisez le composant de chat. Créez src/components/chat.tsx :

"use client";
 
import { useState, useRef, useEffect } from "react";
import { useAssistantChat } from "@/hooks/use-assistant-chat";
 
export function Chat() {
  const { messages, isLoading, sendMessage, stopGeneration, resetChat } =
    useAssistantChat();
  const [input, setInput] = useState("");
  const messagesEndRef = useRef<HTMLDivElement>(null);
 
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);
 
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!input.trim() || isLoading) return;
    sendMessage(input.trim());
    setInput("");
  };
 
  return (
    <div className="flex flex-col h-screen max-w-3xl mx-auto">
      <header className="flex items-center justify-between p-4 border-b">
        <h1 className="text-lg font-semibold">Assistant IA</h1>
        <button
          onClick={resetChat}
          className="text-sm text-gray-500 hover:text-gray-700"
        >
          Nouvelle conversation
        </button>
      </header>
 
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.length === 0 && (
          <div className="text-center text-gray-400 mt-20">
            <p className="text-xl mb-2">Démarrer une conversation</p>
            <p className="text-sm">
              Posez des questions, téléchargez des fichiers ou demandez une analyse de données.
            </p>
          </div>
        )}
 
        {messages.map((msg) => (
          <div
            key={msg.id}
            className={`flex ${
              msg.role === "user" ? "justify-end" : "justify-start"
            }`}
          >
            <div
              className={`max-w-[80%] rounded-2xl px-4 py-3 ${
                msg.role === "user"
                  ? "bg-blue-600 text-white"
                  : "bg-gray-100 text-gray-900"
              }`}
            >
              <p className="whitespace-pre-wrap">{msg.content}</p>
 
              {msg.codeBlocks?.map((code, i) => (
                <pre
                  key={i}
                  className="mt-2 p-3 bg-gray-900 text-green-400 rounded-lg text-sm overflow-x-auto"
                >
                  <code>{code}</code>
                </pre>
              ))}
 
              {msg.images?.map((src, i) => (
                <img
                  key={i}
                  src={src}
                  alt="Graphique généré"
                  className="mt-2 rounded-lg max-w-full"
                />
              ))}
            </div>
          </div>
        ))}
 
        {isLoading && messages[messages.length - 1]?.content === "" && (
          <div className="flex justify-start">
            <div className="bg-gray-100 rounded-2xl px-4 py-3">
              <div className="flex space-x-1">
                <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
                <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:0.1s]" />
                <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:0.2s]" />
              </div>
            </div>
          </div>
        )}
 
        <div ref={messagesEndRef} />
      </div>
 
      <form onSubmit={handleSubmit} className="p-4 border-t">
        <div className="flex gap-2">
          <input
            type="text"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="Tapez votre message..."
            className="flex-1 rounded-xl border border-gray-300 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
            disabled={isLoading}
          />
          {isLoading ? (
            <button
              type="button"
              onClick={stopGeneration}
              className="px-6 py-3 rounded-xl bg-red-500 text-white font-medium hover:bg-red-600 transition-colors"
            >
              Arrêter
            </button>
          ) : (
            <button
              type="submit"
              disabled={!input.trim()}
              className="px-6 py-3 rounded-xl bg-blue-600 text-white font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
            >
              Envoyer
            </button>
          )}
        </div>
      </form>
    </div>
  );
}

Enfin, ajoutez le chat à votre page. Mettez à jour src/app/page.tsx :

import { Chat } from "@/components/chat";
 
export default function Home() {
  return <Chat />;
}

Étape 9 : Ajout du téléchargement de fichiers à l'interface

Ajoutons un bouton de téléchargement de fichiers à l'interface de chat pour que les utilisateurs puissent joindre des documents.

Créez src/components/file-upload.tsx :

"use client";
 
import { useRef, useState } from "react";
 
interface FileUploadProps {
  threadId: string | null;
  onUploadComplete: (fileName: string) => void;
  disabled?: boolean;
}
 
export function FileUpload({
  threadId,
  onUploadComplete,
  disabled,
}: FileUploadProps) {
  const fileRef = useRef<HTMLInputElement>(null);
  const [uploading, setUploading] = useState(false);
 
  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file || !threadId) return;
 
    setUploading(true);
 
    const formData = new FormData();
    formData.append("file", file);
    formData.append("threadId", threadId);
 
    try {
      const res = await fetch("/api/files", {
        method: "POST",
        body: formData,
      });
 
      if (res.ok) {
        const { fileName } = await res.json();
        onUploadComplete(fileName);
      }
    } finally {
      setUploading(false);
      if (fileRef.current) fileRef.current.value = "";
    }
  };
 
  return (
    <>
      <input
        ref={fileRef}
        type="file"
        onChange={handleUpload}
        accept=".pdf,.docx,.txt,.md,.csv,.json"
        className="hidden"
      />
      <button
        type="button"
        onClick={() => fileRef.current?.click()}
        disabled={disabled || uploading || !threadId}
        className="p-3 rounded-xl border border-gray-300 hover:bg-gray-50 disabled:opacity-50 transition-colors"
        title="Télécharger un fichier"
      >
        {uploading ? (
          <svg
            className="w-5 h-5 animate-spin text-gray-500"
            viewBox="0 0 24 24"
            fill="none"
          >
            <circle
              className="opacity-25"
              cx="12"
              cy="12"
              r="10"
              stroke="currentColor"
              strokeWidth="4"
            />
            <path
              className="opacity-75"
              fill="currentColor"
              d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
            />
          </svg>
        ) : (
          <svg
            className="w-5 h-5 text-gray-500"
            fill="none"
            viewBox="0 0 24 24"
            strokeWidth={1.5}
            stroke="currentColor"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              d="M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13"
            />
          </svg>
        )}
      </button>
    </>
  );
}

Étape 10 : Persistance des threads avec le stockage local

Pour permettre aux utilisateurs de reprendre des conversations après rechargement de la page, persistez les identifiants de threads dans le stockage local.

Mettez à jour src/hooks/use-assistant-chat.ts pour ajouter la persistance :

const STORAGE_KEY = "openai-chat-threads";
 
interface ThreadInfo {
  id: string;
  title: string;
  createdAt: string;
}
 
function getStoredThreads(): ThreadInfo[] {
  if (typeof window === "undefined") return [];
  const stored = localStorage.getItem(STORAGE_KEY);
  return stored ? JSON.parse(stored) : [];
}
 
function storeThread(thread: ThreadInfo) {
  const threads = getStoredThreads();
  threads.unshift(thread);
  localStorage.setItem(STORAGE_KEY, JSON.stringify(threads.slice(0, 50)));
}

Ajoutez la route API pour l'historique du thread. Créez src/app/api/threads/[threadId]/messages/route.ts :

import { NextResponse } from "next/server";
import { openai } from "@/lib/openai";
 
export async function GET(
  _request: Request,
  { params }: { params: Promise<{ threadId: string }> }
) {
  const { threadId } = await params;
 
  try {
    const messages = await openai.beta.threads.messages.list(threadId, {
      order: "asc",
    });
 
    return NextResponse.json({ messages: messages.data });
  } catch {
    return NextResponse.json(
      { error: "Thread not found" },
      { status: 404 }
    );
  }
}

Étape 11 : Considérations de production

Avant le déploiement, adressez ces points importants :

Limitation du débit

Protégez vos routes API contre les abus :

// src/lib/rate-limit.ts
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
 
export function rateLimit(
  identifier: string,
  maxRequests = 20,
  windowMs = 60_000
): boolean {
  const now = Date.now();
  const entry = rateLimitMap.get(identifier);
 
  if (!entry || now > entry.resetTime) {
    rateLimitMap.set(identifier, { count: 1, resetTime: now + windowMs });
    return true;
  }
 
  if (entry.count >= maxRequests) {
    return false;
  }
 
  entry.count++;
  return true;
}

Gestion des coûts

Assistants API facture :

  • Utilisation des tokens — tokens d'entrée et de sortie au tarif du modèle
  • Stockage de fichiers — 0,20 $ par Go par jour
  • Sessions de l'interpréteur de code — 0,03 $ par session

Pour contrôler les coûts :

  • Définissez max_prompt_tokens et max_completion_tokens sur les runs
  • Nettoyez régulièrement les anciens threads et fichiers
  • Surveillez l'utilisation via le tableau de bord OpenAI
const stream = openai.beta.threads.runs.stream(threadId, {
  assistant_id: process.env.OPENAI_ASSISTANT_ID,
  max_prompt_tokens: 50000,
  max_completion_tokens: 4096,
});

Test de l'implémentation

Lancez le serveur de développement :

npm run dev

Testez ces scénarios :

  1. Conversation basique — Envoyez un message et vérifiez que le streaming fonctionne
  2. Questions de suivi — Confirmez que l'assistant se souvient du contexte précédent dans le thread
  3. Téléchargement de fichier — Téléchargez un PDF et posez des questions sur son contenu
  4. Interpréteur de code — Demandez "Créez un diagramme en barres montrant la population des 5 plus grands pays"
  5. Récupération d'erreur — Déconnectez brièvement votre réseau et vérifiez que l'interface gère correctement
  6. Nouvelle conversation — Cliquez sur "Nouvelle conversation" et vérifiez qu'un nouveau thread est créé

Dépannage

Erreur "Assistant not found" Vérifiez que votre OPENAI_ASSISTANT_ID dans .env.local correspond à un assistant dans votre compte OpenAI. Les assistants sont liés à l'organisation de la clé API.

Le streaming s'arrête en cours de réponse Cela signifie généralement que le run a atteint une limite de tokens ou a expiré. Augmentez max_prompt_tokens ou vérifiez les temps d'exécution longs des outils.

La recherche de fichiers ne renvoie pas de résultats Assurez-vous que le fichier a été téléchargé avec purpose: "assistants". Les fichiers téléchargés à d'autres fins ne sont pas indexés pour la recherche. Vérifiez également que le format de fichier est supporté.

L'interpréteur de code expire Les calculs complexes peuvent dépasser le timeout d'exécution. Décomposez les requêtes complexes en étapes plus petites ou demandez à l'assistant de simplifier son approche.

Erreur "Run already active" Un seul run peut être actif par thread à la fois. Attendez que le run actuel se termine avant d'en démarrer un autre, ou annulez le run actif avec openai.beta.threads.runs.cancel(threadId, runId).


Prochaines étapes

Maintenant que vous avez un chatbot IA fonctionnel, envisagez ces améliorations :

  • Authentification — Ajoutez l'authentification utilisateur avec NextAuth.js ou Clerk pour isoler les threads par utilisateur
  • Vector stores — Créez des vector stores partagés pour des bases de connaissances à l'échelle de l'organisation
  • Appel de fonctions — Ajoutez des fonctions personnalisées pour que l'assistant puisse interagir avec vos propres API
  • Rendu Markdown — Utilisez react-markdown avec coloration syntaxique pour les réponses formatées
  • Responsive mobile — Optimisez la mise en page du chat pour les petits écrans
  • Analytique — Suivez l'utilisation des tokens, les temps de réponse et la satisfaction des utilisateurs

Conclusion

Vous avez construit un chatbot IA complet avec OpenAI Assistants API et Next.js. Assistants API élimine une grande partie du code répétitif lié à la construction d'applications IA — la gestion des threads, l'indexation des documents et l'exécution de code sont tous gérés par la plateforme.

L'avantage principal de cette approche est la persistance. Contrairement aux complétions de chat sans état, vos threads préservent l'historique complet des conversations, les fichiers attachés restent indexés et les utilisateurs peuvent reprendre là où ils se sont arrêtés. Cela rend Assistants API particulièrement adapté au support client, aux bases de connaissances internes et aux outils d'analyse de données.

Le code de ce tutoriel est une base solide. Étendez-le avec l'authentification, des outils personnalisés et une interface soignée pour créer un assistant IA adapté à votre cas d'utilisation spécifique.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur 11 Les Bases de Laravel 11 : Generation d'URL.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes