Aller au contenu principal
Tous les articles
·6 min de lecture

Mistral AI dans un SaaS B2B : 5 use-cases concrets en TypeScript

Intégrer une IA souveraine française dans votre SaaS — extraction de données, résumés clients, classification d'emails, génération de contenu et search sémantique.

Pourquoi Mistral plutôt qu'OpenAI ?

Trois raisons concrètes pour un SaaS B2B français :

  • Souveraineté des données — vos données clients ne sortent pas de l'UE. Argument RGPD béton face à un acheteur grand compte.
  • Prixmistral-small-latest coûte ~10x moins cher que GPT-4o pour des tâches d'extraction structurée.
  • Latence en EU — les serveurs Mistral sont à Paris, pas en Virginie. Vos webhooks répondent en 200ms au lieu de 600ms.

Setup minimal :

// src/lib/ai/mistral.ts
import { Mistral } from "@mistralai/mistralai";
import { env } from "~/env";
 
export const mistral = new Mistral({ apiKey: env.MISTRAL_API_KEY });

Use case 1 — Extraction de données depuis un PDF

Le client envoie une facture fournisseur en PDF. Vous voulez extraire automatiquement le montant, la date, le numéro.

import { z } from "zod";
 
const InvoiceSchema = z.object({
  invoiceNumber: z.string(),
  date: z.string(),
  totalHT: z.number(),
  totalTTC: z.number(),
  vatRate: z.number(),
  supplierName: z.string(),
});
 
export async function extractInvoiceFromText(text: string) {
  const response = await mistral.chat.complete({
    model: "mistral-small-latest",
    responseFormat: { type: "json_object" },
    messages: [
      {
        role: "system",
        content:
          "Tu extrais les données structurées de factures françaises. Réponds uniquement en JSON valide.",
      },
      {
        role: "user",
        content: `Extrait ces champs de la facture : invoiceNumber, date (YYYY-MM-DD), totalHT, totalTTC, vatRate, supplierName.
 
Facture :
${text}`,
      },
    ],
  });
 
  const raw = response.choices[0]?.message.content;
  if (typeof raw !== "string") throw new Error("Empty response");
 
  return InvoiceSchema.parse(JSON.parse(raw));
}

Use case 2 — Résumé d'historique client pour le CRM

Quand un commercial ouvre une fiche client, il veut comprendre en 5 secondes ce qui s'est passé : derniers échanges, signaux d'achat, points de friction.

// src/server/api/routers/crm.ts
import { mistral } from "~/lib/ai/mistral";
 
summarizeClientHistory: requirePermission("crm:read")
  .input(z.object({ clientId: z.string() }))
  .query(async ({ ctx, input }) => {
    const interactions = await ctx.orgDb.clientInteraction.findMany({
      where: { clientId: input.clientId },
      orderBy: { createdAt: "desc" },
      take: 20,
    });
 
    if (interactions.length === 0) return null;
 
    const transcript = interactions
      .map((i) => `[${i.type}] ${i.createdAt.toISOString()} : ${i.content}`)
      .join("\n");
 
    const response = await mistral.chat.complete({
      model: "mistral-small-latest",
      maxTokens: 200,
      messages: [
        {
          role: "system",
          content:
            "Tu es un assistant CRM. Résume en 3 puces : statut actuel, dernier signal d'achat, prochaine action recommandée.",
        },
        { role: "user", content: transcript },
      ],
    });
 
    return response.choices[0]?.message.content ?? null;
  }),

Use case 3 — Classification d'emails entrants

Vous recevez les emails clients dans une boîte support. Vous voulez les router automatiquement vers le bon agent.

const Category = z.enum([
  "BUG_REPORT",
  "FEATURE_REQUEST",
  "BILLING",
  "ONBOARDING",
  "CHURN_RISK",
  "OTHER",
]);
 
export async function classifyEmail(subject: string, body: string) {
  const response = await mistral.chat.complete({
    model: "mistral-small-latest",
    responseFormat: { type: "json_object" },
    maxTokens: 50,
    messages: [
      {
        role: "system",
        content: `Tu classifies des emails de support. Réponds en JSON : {"category": "...", "urgent": boolean}.
Catégories valides : BUG_REPORT, FEATURE_REQUEST, BILLING, ONBOARDING, CHURN_RISK, OTHER.`,
      },
      { role: "user", content: `Sujet : ${subject}\n\n${body}` },
    ],
  });
 
  const result = z
    .object({ category: Category, urgent: z.boolean() })
    .parse(JSON.parse(response.choices[0]?.message.content ?? "{}"));
 
  return result;
}

Performance typique : 92% de précision après 200 emails labellisés à la main pour few-shot prompting.

Use case 4 — Génération de contenu (descriptions produits)

Un utilisateur ajoute un produit dans son catalogue. Il rentre le nom et 3 caractéristiques. Vous générez la description marketing.

generateProductDescription: requirePermission("catalog:write")
  .input(
    z.object({
      productName: z.string(),
      keyFeatures: z.array(z.string()).min(1).max(5),
      tone: z.enum(["pro", "casual", "premium"]).default("pro"),
    }),
  )
  .mutation(async ({ ctx, input }) => {
    // Freemium guard avant l'appel
    const guard = await checkFreemiumLimit(ctx, "aiGenerations");
    if (!guard.allowed) {
      throw new TRPCError({
        code: "FORBIDDEN",
        message: `Quota IA atteint (${guard.used}/${guard.limit})`,
      });
    }
 
    const response = await mistral.chat.complete({
      model: "mistral-small-latest",
      maxTokens: 250,
      messages: [
        {
          role: "system",
          content: `Tu rédiges des descriptions produit en français, ton ${input.tone}. 80 mots max.`,
        },
        {
          role: "user",
          content: `Produit : ${input.productName}\nCaractéristiques :\n- ${input.keyFeatures.join("\n- ")}`,
        },
      ],
    });
 
    // Incrémenter le compteur APRÈS le succès
    await ctx.db.subscription.update({
      where: { organizationId: ctx.session.user.organizationId },
      data: { aiGenerationsUsed: { increment: 1 } },
    });
 
    return response.choices[0]?.message.content ?? "";
  }),

Use case 5 — Search sémantique avec embeddings

L'utilisateur tape "facture impayée Dupont" et vous voulez retrouver des résultats même si le mot exact n'est pas dans la base.

// 1. À la création d'une ressource, calculer son embedding
export async function indexDocument(id: string, text: string) {
  const response = await mistral.embeddings.create({
    model: "mistral-embed",
    inputs: [text],
  });
 
  const embedding = response.data[0]?.embedding;
  if (!embedding) throw new Error("Embedding failed");
 
  // Stockage avec pgvector
  await db.$executeRaw`
    UPDATE "Document"
    SET embedding = ${embedding}::vector
    WHERE id = ${id}
  `;
}
 
// 2. Au moment de la recherche
export async function semanticSearch(query: string, orgId: string) {
  const queryEmbedding = (
    await mistral.embeddings.create({
      model: "mistral-embed",
      inputs: [query],
    })
  ).data[0]?.embedding;
 
  return db.$queryRaw`
    SELECT id, title, 1 - (embedding <=> ${queryEmbedding}::vector) AS similarity
    FROM "Document"
    WHERE "organizationId" = ${orgId}
    ORDER BY embedding <=> ${queryEmbedding}::vector
    LIMIT 10
  `;
}

Coût : mistral-embed est à 0.10€ / 1M tokens. Indexer 10 000 documents de 500 mots = ~50 centimes. La recherche elle-même coûte des millièmes de centime.

Le pattern de coût global

Use caseCoût/appelFréquence typiqueCoût mensuel (1000 users)
Extraction PDF~0.005€5/user/mois25€
Résumé CRM~0.002€30/user/mois (cached)60€
Classification email~0.0003€50/user/mois15€
Génération produit~0.003€10/user/mois30€
Embedding (index + search)~0.0001€200/user/mois20€
Total~150€/mois

Vous facturez vos plans 30-150€/mois. La marge sur l'IA est confortable.

Erreurs à éviter

  1. Appeler Mistral depuis un Server Component sans cache — un refresh = un appel API. Toujours wrapper avec unstable_cache ou Redis.
  2. Pas de retry sur erreur réseau — Mistral a un SLA de 99.9% mais des spikes. Implémentez 3 retries avec backoff exponentiel.
  3. Streamer dans une Edge Function de plus de 25s — Vercel coupe à 30s. Pour les longs streams, utilisez un endpoint Node.js classique.
  4. Oublier le freemium guard avant l'appel — sans ça, un utilisateur du plan gratuit peut consommer 1000€ d'IA en 1 nuit.

Conclusion

Mistral n'est pas "OpenAI mais français". C'est une stack adaptée aux SaaS européens : RGPD-friendly, économique, et avec des modèles assez bons pour 90% des cas d'usage B2B. Pour les 10% restants (raisonnement complexe, code generation), gardez un fallback vers Claude ou GPT-4o — mais commencez par Mistral.

Dans HeartCo, l'intégration Mistral est pré-câblée : freemium guard, retry, cache Redis, et exemples dans src/lib/ai/. Vous ajoutez votre use case en 30 lignes.

Partager
Mistral AI dans un SaaS B2B : 5 use-cases concrets en TypeScript | HeartCo Dev Blog