Skip to content

LegalTech France — Playbook AI Engineer 2026

TL;DR

Marché FR LegalTech : ~150M€ (2024-2025), croissance ~25%/an, ~250 startups actives, >200M€ levés cumulés depuis 2015 (sources : Maddyness, Banque des Territoires, Village de la Justice). Porté par cabinets d'avocats (top 100 = 80% du marché), ~17 000 notaires, AFJE (juristes d'entreprise, ~8 000-10 000 membres), directions juridiques de grands groupes. TJM réaliste freelance AI sénior vertical-fit : 900-1300€/j (le haut de fourchette 1200-1500€ existe mais reste réservé aux experts IA reconnus en production ; sources Free-Work, Silkhom, Jobbers 2025). Top 3 clients-types : cabinets business law (Gide, Bredin Prat, August Debouzy), éditeurs juridiques (Lefebvre Sarrut, LexisNexis, Wolters Kluwer), legaltech B2B (Doctrine, Hyperlex, Septeo). Top 3 use cases qui se vendent : RAG jurisprudence + génération conclusions/mémos, contract review automatisé (M&A, NDA, baux), due diligence accélérée. Barrière à l'entrée : déontologie + secret professionnel → c'est ton moat.


🎯 Pourquoi cette verticale en 2026

1. Le legal anglo-saxon a déjà basculé — le FR rattrape. EvenUp ($2B valo), Harvey (>$5B), Legora ($5.5B), Spellbook : la preuve que les avocats paient cher pour de l'IA bien intégrée. La France est en retard de 18-24 mois, ce qui veut dire que 2026-2027 est la fenêtre.

2. Les budgets sont là. Un associé en cabinet d'affaires facture 600-1200€/h client. Une heure récupérée = ROI immédiat. Les cabinets de 50-200 personnes ont des budgets innovation de 200K-2M€/an.

3. Les généralistes IA n'osent pas. Trop de jargon (Code civil, Cassation, conclusions, OPJ, RPVA…), trop de RGPD strict, secret professionnel = peur. Résultat : peu de concurrents qualifiés.

4. C'est une verticale "moat-friendly". Une fois que tu as 2 cabinets référencés, tu deviens "le gars qui comprend le legal". Le bouche-à-oreille en cabinet d'avocats est extrême — les associés se parlent dans les mêmes dîners.

5. La régulation FR/EU force l'investissement. AI Act EU (2025-2026), CNIL guidance LLM, directive CSRD, DAC7, devoir de vigilance : autant de raisons de scaler par l'IA plutôt que par des recrutements à 80K€/an.

Honnêteté brutale :

  • Cycle de vente long (3-9 mois pour un cabinet). Tu DOIS avoir 6 mois de trésorerie.
  • Décideurs conservateurs : associés 50+ ans, peu tech-savvy. Pitch business, pas tech.
  • Tu vas perdre les premiers RDV. Le legal a peur de tout (data leak, hallucination, responsabilité civile pro).
  • Tu ne peux PAS vendre du "ChatGPT wrapper" — ça les insulte.

🗺️ Carte du marché français

Top 10 cabinets d'avocats d'affaires (cibles tier 1)

CabinetAvocats FRSpécialitésBudget innovation estimé
Gide Loyrette Nouel~600M&A, contentieux, banque3-5M€/an
Bredin Prat~200M&A, contentieux haut de gamme1-2M€/an
Cleary Gottlieb (Paris)~100M&A international1-2M€/an
A&O Shearman (Paris) (fusion A&O × Shearman, mai 2024)~250Finance, M&A2-3M€/an
Linklaters (Paris)~170-200Corporate, banque2-3M€/an
August Debouzy~150Corporate, IP, tech1-2M€/an

| Jeantet | ~180 | Corporate, contentieux | 800K-1.5M€/an | | De Pardieu Brocas Maffei | ~150 | Banque, immobilier | 800K-1.5M€/an | | Bird & Bird (Paris) | ~150 | Tech, IP | 1-2M€/an | | CMS Francis Lefebvre | ~400 | Fiscalité, corporate | 2-3M€/an |

Tier 2 (souvent plus accessibles freelance)

  • Cabinets régionaux : Fidal (~1000 avocats sur la France, very accessible), Racine, Lamy Lexel, KGA Avocats
  • Boutiques M&A : Hogan Lovells, Latham & Watkins, McDermott, Paul Hastings (filiales FR)
  • Boutiques IP : Cabinet Plasseraud, Regimbeau, Marks & Clerk
  • Cabinets pénaux : Temime, Veil Jourde, Dupond-Moretti & Vey

Notariat

  • CSN (Conseil Supérieur du Notariat) — ~17 000 notaires en France, structure professionnelle nationale
  • Notaires de France : SaaS du CSN, "Real" (logiciel commun), "iNot"
  • Editions Francis Lefebvre Notaire, Solon Notaires

Direction juridique d'entreprise

  • AFJE (Association Française des Juristes d'Entreprise) — ~8 000 - 10 000 membres (sur un total de ~22 000 juristes d'entreprise en France)
  • Cercle Montesquieu — DG juridique du CAC40
  • Grandes directions juridiques : TotalEnergies, LVMH, BNP Paribas, AXA, Sanofi, Renault, Saint-Gobain (équipes 50-200 juristes)

Éditeurs juridiques & LegalTech FR

  • Lefebvre Sarrut (Dalloz, Editions Francis Lefebvre, Lamy) — leader FR édition, ~500M€ CA
  • LexisNexis France (RELX), Wolters Kluwer France (Lamy intégré)
  • Doctrine.fr — moteur de recherche jurisprudence (concurrent direct si tu fais RAG)
  • Predictice — analytics jurisprudence
  • Hyperlex — extraction clauses contrats (rachat par Septeo 2022)
  • Septeo — éditeur logiciels juridiques (rachat Hyperlex, Genapi, etc.)
  • Pappers — données légales entreprise
  • Legalfly (Belge, mais actif FR)
  • Legalstart, Captain Contrat, Defacto — legaltech B2B PME

Associations & événements pros

  • CNB (Conseil National des Barreaux)
  • Open Law — association innovation droit
  • LegalTech Paris (salon annuel, ~2000 participants)
  • VivaTech (juin) — stand legaltech
  • JurisFutur (CNB)
  • L'Incubateur du Barreau de Paris (HQ Maison du Barreau)

Médias spécialisés

  • Dalloz Actualité, Editions Législatives
  • Le Village de la Justice — le LinkedIn des juristes (publier ici = direct exposure)
  • Affiches Parisiennes, Gazette du Palais
  • L'Usine Digitale > Legaltech
  • Maddyness > LegalTech

💼 Top 5 use cases AI qui se vendent en 2026

Use case 1 — Recherche jurisprudence + génération conclusions/mémos

Problème métier : Un avocat junior passe 6-12h/dossier à rechercher la jurisprudence, lire les arrêts, en faire la synthèse, puis rédiger un mémo ou des conclusions. C'est non-facturable au-delà d'un seuil. Pour un cabinet de 100 avocats, c'est ~50 000h/an non-optimales.

Solution AI : RAG sur base jurisprudence (Cassation, CE, CJUE, CA) + générateur de mémo avec citations vérifiables et clauses d'avertissement. Pas de "GPT pur" — citations obligatoires avec liens vers l'arrêt source.

Stack technique :

  • LLM : Mistral Large 2 (souverain FR) ou Claude Opus 4.8 (claude-opus-4-8, qualité juridique top — 5 $/25 $ par M tokens, contexte 1M)
  • Vector DB : Qdrant self-hosted (RGPD)
  • Framework : LlamaIndex (gère bien la structure document juridique)
  • Ingestion : sources publiques (Légifrance API), bases internes cabinet (Word, PDF, iManage)
  • Front : Slack bot interne OR webapp Next.js intranet

Mesure ROI :

  • Temps de préparation d'un mémo : 8h → 2h (gain 75%)
  • Coût avocat junior chargé : 80€/h
  • Pour 200 mémos/an : économies ~96 000€/an
  • Prix solution : 90-150K€ build + 30K€/an run

Exemple chiffré : Cabinet d'affaires 80 avocats. 4 mémos/avocat/mois. Gain temps moyen 4h/mémo. = 15 360h/an = 1.2M€ d'heures débloquées (qui peuvent devenir facturables).


Use case 2 — Contract review automatisé (NDA, baux, M&A)

Problème métier : Revoir 100 NDA, 50 baux commerciaux, 20 SPAs en due diligence M&A. Chaque relecture = 30min-2h. C'est répétitif, à risque d'erreur en fin de journée, non-rentable.

Solution AI :

  • Classification du contrat (NDA, MSA, SPA, bail…)
  • Extraction structurée des clauses-clés (durée, juridiction, indemnités, conditions résolutoires)
  • Comparaison vs playbook du cabinet (clauses standards / à éviter)
  • Flagging des anomalies (rouge/orange/vert)
  • Génération de la red-line (Word track changes)

Stack technique :

  • LLM : Claude Sonnet 4.6 (claude-sonnet-4-6, rapport coût/qualité parfait pour le batch — 3 $/15 $ par M tokens) ; Haiku 4.5 (claude-haiku-4-5, 1 $/5 $) pour le pré-filtrage massif
  • Extraction : structured outputs natifs (messages.parse() + schéma Pydantic/zod, ou output_config.format) plutôt qu'un prompting XML/JSON fait main
  • Comparaison : règles métier + LLM-as-judge
  • Document I/O : python-docx, Aspose, ou Adobe Extract API
  • Workflow : Temporal ou n8n pour orchestrer

Mesure ROI :

  • NDA simple : 30min → 5min (gain 83%)
  • Bail commercial : 2h → 30min (gain 75%)
  • Pour 500 contrats/an : économies ~250-400K€/an

Exemple chiffré : Direction juridique LVMH (env. 80 juristes). 3000 contrats fournisseurs/an à revoir. Coût juriste interne : 60€/h chargé. Gain : 6000h/an = 360K€/an. Solution facturée : 200K€ build + 50K€/an run.


Use case 3 — Due diligence M&A accélérée

Problème métier : Due diligence d'une cible M&A = data room avec 5000-20000 documents (contrats, comptes, contentieux, RH, IP). Équipe de 5-10 avocats pendant 4-8 semaines. C'est du forfait, donc tout gain de temps = marge directe.

Solution AI :

  • Ingestion auto data room (Intralinks, Datasite, iDeals)
  • Classification par catégorie (contrats clients, contentieux, IP, RH…)
  • Extraction des points rouges (change-of-control, indemnités plafonds, MAC clauses)
  • Génération du tableau de synthèse legal DD (Excel/Word)
  • Q&A interactif sur la data room

Stack technique :

  • LLM : Claude Opus 4.8 (claude-opus-4-8, qualité raisonnement, contexte 1M) + Sonnet 4.6 (claude-sonnet-4-6) pour les passes en masse, Haiku 4.5 pour la classification
  • Vector DB : Qdrant ou pgvector
  • OCR : Mistral OCR ou Azure Document Intelligence
  • Framework : LlamaIndex avec custom retrievers par catégorie
  • Sécurité : déploiement on-prem ou cloud souverain (Scaleway, OVH)

Mesure ROI :

  • DD M&A : 2000h → 1200h (gain 40%)
  • Coût avocat moyen chargé : 200€/h
  • Par DD : économies 160K€ → marge cabinet ou prix négocié

Exemple chiffré : Cabinet M&A mid-cap, 30 DDs/an. 800h économisées × 30 = 24 000h × 200€ = 4.8M€ de marge récupérable. Solution facturée : 300K€ build + 80K€/an + part variable.


Use case 4 — KYC corporate / vigilance fournisseurs

Problème métier : Sapin II, devoir de vigilance, LCB-FT : les cabinets et directions juridiques doivent qualifier leurs clients et tiers. C'est du sourcing manuel sur Pappers, Infogreffe, sanctions OFAC/UE, PEP.

Solution AI :

  • Agent qui consolide Pappers + Infogreffe + Bodacc + sanctions
  • Analyse de la chaîne capitalistique (UBO, beneficial owner)
  • Scoring de risque automatique (pays, secteur, presse négative)
  • Génération du rapport KYC PDF signable

Stack technique :

  • Orchestration : LangGraph ou CrewAI
  • LLM : Mistral Large 2 (souverain, données sensibles)
  • APIs : Pappers, Open Sanctions, OpenCorporates, Bodacc
  • Storage : PostgreSQL + S3 chiffré
  • Front : Retool ou Next.js

Mesure ROI : Rapport KYC manuel 3h → 20min. Pour 500 KYCs/an : économies ~125K€/an. Prix : 60-90K€ build + 20-30K€/an.


Use case 5 — Compliance & veille réglementaire automatisée

Problème métier : Une direction juridique d'un groupe régulé (banque, pharma, énergie) doit suivre 200-500 textes/jour (Légifrance, JOUE, AMF, ACPR, CNIL). Impossible humainement.

Solution AI :

  • Crawlers sur sources officielles
  • Classification par thématique (RGPD, AML, droit social, fiscalité…)
  • Résumé exécutif + impact sur l'entreprise
  • Alerting Slack/Teams ciblé par direction métier

Stack technique :

  • Crawler : Playwright + Firecrawl
  • LLM : Mistral Large + Sonnet pour les résumés
  • Vector DB : Qdrant (recherche sémantique sur l'historique)
  • Alerting : Slack/Teams webhooks
  • Cron : Temporal / Airflow

Mesure ROI : 1 ETP juriste veille à 80K€/an évité ou redéployé. Prix : 50-80K€ build + 18K€/an.


🛠️ Stack technique typique LegalTech FR

┌─────────────────────────────────────────────────────────────────┐
│                       UTILISATEURS                              │
│   Avocats, Juristes, Paralegals, Direction juridique            │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  FRONT-END                                                      │
│  • Next.js (SaaS interne) / Slack-Teams bot                     │
│  • Add-in Word/Outlook (Office.js) pour contract review         │
│  • iManage Work integration (cabinet d'affaires)                │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  ORCHESTRATION                                                  │
│  • LangGraph (workflows complexes : DD, contract review)        │
│  • LlamaIndex (RAG jurisprudence)                               │
│  • Temporal (jobs longs : DD, ingestion datarooms)              │
└─────────────────────────────────────────────────────────────────┘

              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
        ┌──────────┐    ┌──────────┐    ┌──────────┐
        │   LLMs   │    │ Vector DB│    │  OCR /   │
        │          │    │          │    │ Parsing  │
        │ Mistral  │    │ Qdrant   │    │          │
        │ Large 2  │    │ self-host│    │ Mistral  │
        │          │    │          │    │ OCR      │
        │ Claude   │    │ pgvector │    │ Azure DI │
        │ Sonnet   │    │ (Postgres│    │ Aspose   │
        │ 4.6 /    │    │ existing)│    │          │
        │ Opus 4.8 │    │          │    │          │
        └──────────┘    └──────────┘    └──────────┘


┌─────────────────────────────────────────────────────────────────┐
│  INTÉGRATIONS MÉTIER                                            │
│  • iManage Work / NetDocuments (DMS cabinet)                    │
│  • Microsoft 365 (Word add-in, Outlook, SharePoint)             │
│  • Datasite / Intralinks / iDeals (data rooms M&A)              │
│  • Pappers / Infogreffe / Légifrance API                        │
│  • DocuSign / Yousign (signature)                               │
│  • Lefebvre Dalloz Connect / LexisNexis (bases payantes)        │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  INFRASTRUCTURE SOUVERAINE                                      │
│  • Scaleway (DC français, certifié ISO 27001/HDS)               │
│  • OVHcloud SecNumCloud (gov / défense)                         │
│  • Outscale (filiale Dassault, SecNumCloud)                     │
│  • Mistral La Plateforme (data résidence FR/EU)                 │
│  • Pas d'OpenAI direct sauf via Azure FR avec DPA renforcé      │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  OBSERVABILITÉ + AUDIT                                          │
│  • Langfuse / LangSmith pour traces                             │
│  • Logs immutables (S3 Object Lock) pour audit déontologique    │
│  • Anonymisation PII (Presidio) avant ingestion LLM             │
└─────────────────────────────────────────────────────────────────┘

Pourquoi cette stack et pas une autre :

  • Mistral Large 2 : souverain, hébergé FR, accepté par les comités d'éthique cabinets. Comprend bien le FR juridique.
  • Claude Sonnet 4.6 / Opus 4.8 : pour la qualité de raisonnement sur contentieux/conclusions, via Bedrock Paris ou Anthropic EU. En 2026 les IDs canoniques sont claude-opus-4-8 (flagship, 5 $/25 $ par M tokens, contexte 1M), claude-sonnet-4-6 (3 $/15 $) et claude-haiku-4-5 (1 $/5 $). Anthropic n'expose plus de budget de thinking fixe (budget_tokens renvoie HTTP 400 sur 4.7/4.8) : on pilote la profondeur de raisonnement par thinking: {type: "adaptive"} + output_config.effort (low/medium/high/xhigh/max).
  • Qdrant self-hosted : pas de data leak vers Pinecone US. Filtres metadata avancés (juridiction, date, matière).
  • iManage Work : standard de facto dans les cabinets >100 avocats. Si tu ne sais pas intégrer iManage, tu es mort sur ce segment.
  • Office add-in Word : les avocats vivent dans Word. Force-les hors de Word = échec. Intègre-toi DANS Word.

💰 Pricing & business model

TJM réaliste 2026

ProfilTJMConditions
Junior AI gen500-700€Pas vertical-fit, gros risque
AI Engineer généraliste700-900€Sans verticale, dur en cabinet
AI Engineer LegalTech (toi en année 1)900-1100€1-2 missions réussies en portfolio
AI Engineer LegalTech sénior1100-1400€Référencé Gide / Lefebvre Sarrut / DJ CAC40
Expert reconnu (post-conférence Open Law…)1400-1800€Souvent forfait > régie, profils rares

Missions types

1. AI Audit LegalTech (5 jours, 7-9 K€) Livrable : rapport 30-50 pages, cartographie use cases, ROI estimé par use case, roadmap 6-12 mois, recommandations stack souveraine. Vendable à un associé Innovation / Knowledge Manager.

2. AI POC (15 jours, 18-25 K€) Un use case ciblé. Ex : POC contract review sur 100 NDAs. Livrable : webapp démo + benchmark précision + recommandations productionalisation. Très acheté par cabinets prudents.

3. AI Production (60-90 jours, 80-130 K€) Industrialisation : auth SSO, monitoring, audit logs, intégration iManage/M365, formation utilisateurs. Souvent forfait + maintenance 30K/an.

4. Régie longue (6-12 mois, 1000-1300€/j) Embed dans équipe innovation cabinet. 6-9 mois. Total : 130-180 K€/an.

5. Build a SaaS verticalisé (MRR) Ex : "Contractly" — review NDAs SaaS pour PME. Prix : 99-499€/mois/utilisateur. Cible 100 clients × 200€ = 20K€ MRR. Plus risqué, plus levier.

Mix recommandé année 1 freelance LegalTech

  • 2 audits (16K€)
  • 2 POCs (45K€)
  • 1 mission production (110K€)
  • = ~170K€ HT en 12 mois avec 6 mois de prospection front-loaded

Mix année 2-3

  • 1 client recurring 12 mois régie 1200€/j (~215K€)
  • 1 production 100K€
  • 2-3 audits/POCs (50K€)
  • = ~360K€ HT/an

📚 Cas d'usage 1 — END-TO-END : RAG Jurisprudence + Génération de Conclusions

Contexte client

Cabinet : "Maillot & Associés" (fictif), boutique contentieux commercial Paris, 35 avocats, CA 12M€. Associé fondateur Pierre Maillot (62 ans, peu tech), Directeur Innovation Camille Faure (38 ans, ex-DSI banque).

Pain réel : chaque conclusion contentieux nécessite 6-15h de recherche jurisprudence + synthèse. Les juniors passent 60% de leur temps là-dessus. Plafond de facturation atteint, marge tassée.

Demande initiale (mail Camille) : "Pouvez-vous nous aider à réduire le temps de recherche jurisprudence par 2 ? Confidentialité ABSOLUE, secret pro respecté, hébergement FR obligatoire."

Brief commercial

  • Budget : 80-120K€ build + 30K€/an run
  • Délai : 4 mois (POC 6 semaines, production 10 semaines)
  • Contraintes : RGPD, secret pro (RIN), pas d'OpenAI direct, hosting FR, intégration iManage Work + Word
  • Critère succès : gain temps mesurable (>40%), satisfaction associés (NPS >40)

Solution architecture

┌──────────────────────────────────────────────────────────────────┐
│  COUCHE 1 — INGESTION                                            │
│                                                                  │
│  Sources externes :          Sources internes :                  │
│  • Légifrance API            • iManage Work (dossiers cabinet)   │
│  • Cour de Cassation         • Mémos historiques (DOCX)          │
│  • Conseil d'État            • Notes internes                    │
│  • CJUE (Eur-Lex)            • Modèles de conclusions            │
│  • Dalloz Connect API                                            │
│                                                                  │
│  → Pipeline ingestion (Airflow) → chunking → embedding           │
│  → Stockage Qdrant + Postgres metadata                           │
└──────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│  COUCHE 2 — RETRIEVAL                                            │
│  • Hybrid search (BM25 + dense)                                  │
│  • Filtres : juridiction, date, matière, formation, type décision│
│  • Reranking : Mistral / Cohere Rerank EU                        │
│  • Re-write requête (LLM rewriter)                               │
└──────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│  COUCHE 3 — GÉNÉRATION                                           │
│  • Prompt structuré (contexte affaire + jurisprudence retrouvée) │
│  • LLM : Mistral Large 2 (FR juridique) + fallback Claude        │
│    Sonnet 4.6 / Opus 4.8 via Bedrock Paris                       │
│  • Output : conclusion structurée (faits, moyens, demande)       │
│    + citations cliquables vers les arrêts                        │
│  • Garde-fou : refus si confiance < seuil, mention "à vérifier"  │
└──────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│  COUCHE 4 — DELIVERY                                             │
│  • Word add-in (Office.js) : panneau latéral dans Word           │
│  • Slack bot (canal #recherche-jurisprudence)                    │
│  • Export DOCX track-changes vers iManage                        │
└──────────────────────────────────────────────────────────────────┘

Code samples

1. Ingestion Légifrance + chunking juridique-aware (Python)

python
# legaltech/ingestion/legifrance_ingestor.py
"""
Ingestion des arrêts depuis l'API Légifrance officielle (PISTE).
Spécificité juridique : on chunke par considérant / motif, pas par
arbitraire 1000 tokens. Chaque chunk garde sa metadata (juridiction,
chambre, formation, date, n° pourvoi).
"""
from __future__ import annotations

import asyncio
import hashlib
import logging
import re
from dataclasses import dataclass, field
from datetime import date, datetime
from typing import Iterable

import httpx
from qdrant_client import AsyncQdrantClient
from qdrant_client.http.models import Distance, PointStruct, VectorParams

LOG = logging.getLogger(__name__)

LEGIFRANCE_BASE = "https://api.piste.gouv.fr/dila/legifrance/lf-engine-app"
EMBEDDING_MODEL = "mistral-embed"
EMBEDDING_DIM = 1024
COLLECTION = "jurisprudence_fr"


@dataclass(frozen=True)
class Arret:
    id_legifrance: str
    juridiction: str          # "CC" (Cassation), "CE", "CJUE", "CA Paris"...
    formation: str            # "Civ. 1re", "Com.", "Soc."...
    chambre: str | None
    date_decision: date
    numero_pourvoi: str | None
    numero_ecli: str | None
    matiere: list[str]        # tags : "responsabilite", "contrat"...
    texte_complet: str
    sommaire: str | None
    publie_bulletin: bool
    url_source: str


@dataclass
class Chunk:
    arret_id: str
    chunk_index: int
    section: str              # "faits", "moyen", "motif", "dispositif"
    contenu: str
    tokens_approx: int
    metadata: dict = field(default_factory=dict)

    @property
    def stable_id(self) -> str:
        h = hashlib.sha256(
            f"{self.arret_id}:{self.chunk_index}:{self.contenu[:200]}".encode()
        ).hexdigest()
        return h[:32]


SECTION_HEADERS = [
    (re.compile(r"\bAttendu que\b|\bSur le moyen\b", re.I), "moyen"),
    (re.compile(r"\bPar ces motifs\b", re.I), "dispositif"),
    (re.compile(r"\bExposé du litige\b|\bFaits et procédure\b", re.I), "faits"),
    (re.compile(r"\bMotifs de la décision\b|\bMotifs\b", re.I), "motif"),
]


def split_by_section(texte: str) -> list[tuple[str, str]]:
    """
    Découpe naïve mais juridique-aware : on cherche les titres standards
    de la motivation. Si rien trouvé, on chunke par paragraphe.
    """
    paragraphes = re.split(r"\n\s*\n", texte)
    sections: list[tuple[str, str]] = []
    current_section = "introduction"
    buffer: list[str] = []

    for para in paragraphes:
        matched = None
        for regex, section_name in SECTION_HEADERS:
            if regex.search(para):
                matched = section_name
                break
        if matched and buffer:
            sections.append((current_section, "\n\n".join(buffer)))
            buffer = [para]
            current_section = matched
        else:
            buffer.append(para)

    if buffer:
        sections.append((current_section, "\n\n".join(buffer)))
    return sections


def chunk_arret(arret: Arret, max_tokens: int = 800) -> list[Chunk]:
    """
    Chunk un arrêt en gardant la cohérence sectionnelle. Un motif coupé
    en deux = perte de sens, donc on évite.
    """
    chunks: list[Chunk] = []
    sections = split_by_section(arret.texte_complet)
    idx = 0
    for section_name, section_text in sections:
        # split fin si trop long
        words = section_text.split()
        tokens = len(words) * 1.3  # approx
        if tokens <= max_tokens:
            chunks.append(
                Chunk(
                    arret_id=arret.id_legifrance,
                    chunk_index=idx,
                    section=section_name,
                    contenu=section_text,
                    tokens_approx=int(tokens),
                    metadata={
                        "juridiction": arret.juridiction,
                        "formation": arret.formation,
                        "date": arret.date_decision.isoformat(),
                        "numero_pourvoi": arret.numero_pourvoi,
                        "matiere": arret.matiere,
                        "publie_bulletin": arret.publie_bulletin,
                    },
                )
            )
            idx += 1
        else:
            # sliding window
            window = int(max_tokens / 1.3)
            for start in range(0, len(words), window):
                sub = " ".join(words[start : start + window])
                chunks.append(
                    Chunk(
                        arret_id=arret.id_legifrance,
                        chunk_index=idx,
                        section=section_name,
                        contenu=sub,
                        tokens_approx=int(len(sub.split()) * 1.3),
                        metadata={
                            "juridiction": arret.juridiction,
                            "formation": arret.formation,
                            "date": arret.date_decision.isoformat(),
                            "numero_pourvoi": arret.numero_pourvoi,
                            "matiere": arret.matiere,
                            "publie_bulletin": arret.publie_bulletin,
                            "section_part": start // window,
                        },
                    )
                )
                idx += 1
    return chunks


class LegifranceClient:
    def __init__(self, oauth_token: str):
        self._client = httpx.AsyncClient(
            base_url=LEGIFRANCE_BASE,
            headers={"Authorization": f"Bearer {oauth_token}"},
            timeout=httpx.Timeout(30.0),
        )

    async def search_arrets(
        self,
        juridictions: list[str],
        date_debut: date,
        date_fin: date,
        page_size: int = 50,
    ) -> Iterable[Arret]:
        page = 1
        while True:
            payload = {
                "fond": "JURI",
                "recherche": {
                    "filtres": [
                        {"facette": "JURIDICTION", "valeurs": juridictions},
                        {
                            "facette": "DATE_DECISION",
                            "dates": {
                                "start": date_debut.isoformat(),
                                "end": date_fin.isoformat(),
                            },
                        },
                    ],
                    "pageNumber": page,
                    "pageSize": page_size,
                },
            }
            resp = await self._client.post("/search", json=payload)
            resp.raise_for_status()
            data = resp.json()
            results = data.get("results", [])
            if not results:
                return
            for item in results:
                yield self._parse_result(item)
            if len(results) < page_size:
                return
            page += 1

    def _parse_result(self, item: dict) -> Arret:
        return Arret(
            id_legifrance=item["id"],
            juridiction=item.get("juridiction", ""),
            formation=item.get("formation", ""),
            chambre=item.get("chambre"),
            date_decision=datetime.fromisoformat(item["dateDecision"]).date(),
            numero_pourvoi=item.get("numero"),
            numero_ecli=item.get("ecli"),
            matiere=item.get("tags", []),
            texte_complet=item.get("texte", ""),
            sommaire=item.get("sommaire"),
            publie_bulletin=bool(item.get("publieBulletin", False)),
            url_source=f"https://www.legifrance.gouv.fr/juri/id/{item['id']}",
        )


class MistralEmbedder:
    def __init__(self, api_key: str):
        self._client = httpx.AsyncClient(
            base_url="https://api.mistral.ai/v1",
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=httpx.Timeout(60.0),
        )

    async def embed_batch(self, texts: list[str]) -> list[list[float]]:
        resp = await self._client.post(
            "/embeddings",
            json={"model": EMBEDDING_MODEL, "input": texts},
        )
        resp.raise_for_status()
        return [row["embedding"] for row in resp.json()["data"]]


async def ensure_collection(qdrant: AsyncQdrantClient) -> None:
    collections = (await qdrant.get_collections()).collections
    if not any(c.name == COLLECTION for c in collections):
        await qdrant.create_collection(
            collection_name=COLLECTION,
            vectors_config=VectorParams(
                size=EMBEDDING_DIM, distance=Distance.COSINE
            ),
        )


async def upsert_chunks(
    qdrant: AsyncQdrantClient,
    embedder: MistralEmbedder,
    chunks: list[Chunk],
) -> None:
    if not chunks:
        return
    embeddings = await embedder.embed_batch([c.contenu for c in chunks])
    points = [
        PointStruct(
            id=c.stable_id,
            vector=vec,
            payload={
                "arret_id": c.arret_id,
                "section": c.section,
                "contenu": c.contenu,
                **c.metadata,
            },
        )
        for c, vec in zip(chunks, embeddings)
    ]
    await qdrant.upsert(collection_name=COLLECTION, points=points)
    LOG.info("Upserted %d chunks", len(points))


async def ingest(
    legifrance_token: str,
    mistral_key: str,
    qdrant_url: str,
    juridictions: list[str],
    date_debut: date,
    date_fin: date,
) -> None:
    qdrant = AsyncQdrantClient(url=qdrant_url)
    await ensure_collection(qdrant)
    embedder = MistralEmbedder(mistral_key)
    lf = LegifranceClient(legifrance_token)

    batch: list[Chunk] = []
    async for arret in lf.search_arrets(juridictions, date_debut, date_fin):
        chunks = chunk_arret(arret)
        batch.extend(chunks)
        if len(batch) >= 64:
            await upsert_chunks(qdrant, embedder, batch)
            batch = []
    if batch:
        await upsert_chunks(qdrant, embedder, batch)


if __name__ == "__main__":
    import os

    logging.basicConfig(level=logging.INFO)
    asyncio.run(
        ingest(
            legifrance_token=os.environ["PISTE_TOKEN"],
            mistral_key=os.environ["MISTRAL_API_KEY"],
            qdrant_url=os.environ.get("QDRANT_URL", "http://localhost:6333"),
            juridictions=["CC", "CE"],
            date_debut=date(2020, 1, 1),
            date_fin=date(2026, 5, 31),
        )
    )

2. Retrieval hybride + reranking (Python)

python
# legaltech/retrieval/hybrid_search.py
"""
Recherche hybride sur jurisprudence : BM25 (PostgreSQL tsvector) +
dense (Qdrant). Reranking par Mistral pour gros volumes.
"""
from __future__ import annotations

from dataclasses import dataclass
from datetime import date

import asyncpg
import httpx
from qdrant_client import AsyncQdrantClient
from qdrant_client.http.models import Filter, FieldCondition, MatchValue, Range


@dataclass
class SearchHit:
    arret_id: str
    section: str
    contenu: str
    juridiction: str
    date_decision: str
    numero_pourvoi: str | None
    score_dense: float
    score_bm25: float
    score_final: float


@dataclass
class SearchQuery:
    text: str
    juridictions: list[str] | None = None
    date_min: date | None = None
    date_max: date | None = None
    matieres: list[str] | None = None
    top_k_dense: int = 30
    top_k_bm25: int = 30
    top_k_final: int = 10


def _build_qdrant_filter(q: SearchQuery) -> Filter | None:
    must = []
    if q.juridictions:
        must.append(
            FieldCondition(
                key="juridiction",
                match=MatchValue(value=q.juridictions[0])
                if len(q.juridictions) == 1
                else None,
            )
        )
    if q.date_min or q.date_max:
        rng = Range(
            gte=q.date_min.isoformat() if q.date_min else None,
            lte=q.date_max.isoformat() if q.date_max else None,
        )
        must.append(FieldCondition(key="date", range=rng))
    return Filter(must=must) if must else None


async def dense_search(
    qdrant: AsyncQdrantClient,
    embedder,
    q: SearchQuery,
) -> list[SearchHit]:
    [vec] = await embedder.embed_batch([q.text])
    hits = await qdrant.search(
        collection_name="jurisprudence_fr",
        query_vector=vec,
        query_filter=_build_qdrant_filter(q),
        limit=q.top_k_dense,
        with_payload=True,
    )
    return [
        SearchHit(
            arret_id=h.payload["arret_id"],
            section=h.payload["section"],
            contenu=h.payload["contenu"],
            juridiction=h.payload.get("juridiction", ""),
            date_decision=h.payload.get("date", ""),
            numero_pourvoi=h.payload.get("numero_pourvoi"),
            score_dense=h.score,
            score_bm25=0.0,
            score_final=h.score,
        )
        for h in hits
    ]


async def bm25_search(pool: asyncpg.Pool, q: SearchQuery) -> list[SearchHit]:
    """
    Hypothèse : on a une table chunks_fts(arret_id, section, contenu,
    tsv tsvector) avec un index GIN sur tsv.
    """
    async with pool.acquire() as conn:
        rows = await conn.fetch(
            """
            SELECT arret_id, section, contenu, juridiction, date_decision,
                   numero_pourvoi,
                   ts_rank_cd(tsv, plainto_tsquery('french', $1)) AS rank
            FROM chunks_fts
            WHERE tsv @@ plainto_tsquery('french', $1)
            ORDER BY rank DESC
            LIMIT $2
            """,
            q.text,
            q.top_k_bm25,
        )
    return [
        SearchHit(
            arret_id=r["arret_id"],
            section=r["section"],
            contenu=r["contenu"],
            juridiction=r["juridiction"],
            date_decision=str(r["date_decision"]),
            numero_pourvoi=r["numero_pourvoi"],
            score_dense=0.0,
            score_bm25=float(r["rank"]),
            score_final=float(r["rank"]),
        )
        for r in rows
    ]


def reciprocal_rank_fusion(
    dense: list[SearchHit], bm25: list[SearchHit], k: int = 60
) -> list[SearchHit]:
    scores: dict[str, SearchHit] = {}

    def key(h: SearchHit) -> str:
        return f"{h.arret_id}:{h.section}:{h.contenu[:80]}"

    for rank, h in enumerate(dense, start=1):
        kid = key(h)
        scores[kid] = h
        h.score_final = 1.0 / (k + rank)
    for rank, h in enumerate(bm25, start=1):
        kid = key(h)
        if kid in scores:
            scores[kid].score_final += 1.0 / (k + rank)
        else:
            h.score_final = 1.0 / (k + rank)
            scores[kid] = h
    return sorted(scores.values(), key=lambda x: x.score_final, reverse=True)


async def rerank_mistral(
    api_key: str, query: str, hits: list[SearchHit], top_k: int
) -> list[SearchHit]:
    async with httpx.AsyncClient(timeout=60) as c:
        resp = await c.post(
            "https://api.mistral.ai/v1/rerank",
            headers={"Authorization": f"Bearer {api_key}"},
            json={
                "model": "mistral-rerank-v1",
                "query": query,
                "documents": [h.contenu for h in hits],
                "top_n": top_k,
            },
        )
        resp.raise_for_status()
        ranked = resp.json()["results"]
    out = []
    for r in ranked:
        h = hits[r["index"]]
        h.score_final = r["relevance_score"]
        out.append(h)
    return out


async def hybrid_search(
    pool: asyncpg.Pool,
    qdrant: AsyncQdrantClient,
    embedder,
    mistral_api_key: str,
    q: SearchQuery,
) -> list[SearchHit]:
    import asyncio

    dense, bm25 = await asyncio.gather(
        dense_search(qdrant, embedder, q),
        bm25_search(pool, q),
    )
    fused = reciprocal_rank_fusion(dense, bm25)[: max(q.top_k_dense, q.top_k_bm25)]
    return await rerank_mistral(mistral_api_key, q.text, fused, q.top_k_final)

3. Génération de conclusion avec citations vérifiables (TypeScript / NestJS)

Note d'archi senior (2026). Côté Anthropic, on utilise le SDK officiel @anthropic-ai/sdk, pas un client maison. Trois réflexes de prod : (1) client.messages.parse() avec un schéma zod → l'API contraint la sortie au schéma (structured outputs natifs), on supprime le JSON.parse(raw) fragile ; (2) prompt caching (cache_control: {type: "ephemeral"}) sur le préfixe stable (system + jurisprudence partagée entre moyens d'un même dossier) → ~90 % d'économie sur le contexte rejoué ; (3) exceptions typées du SDK (Anthropic.RateLimitError, Anthropic.OverloadedError, Anthropic.APIError) pour décider retry vs fallback, jamais de string.includes("429"). Le thinking se pilote en adaptive + output_config.effortbudget_tokens renvoie HTTP 400 sur Opus 4.8.

ts
// legaltech/generation/conclusion-generator.service.ts
import { Injectable, Logger } from "@nestjs/common";
import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";

const ConclusionSchema = z.object({
  faits: z.string().min(50),
  procedure: z.string(),
  pretentions: z.array(z.string()),
  moyens: z.array(
    z.object({
      titre: z.string(),
      argumentation: z.string(),
      citations: z.array(
        z.object({
          arret_id: z.string(),
          juridiction: z.string(),
          date: z.string(),
          extrait: z.string(),
          numero_pourvoi: z.string().nullable(),
        })
      ),
    })
  ),
  demandes: z.array(z.string()),
  confidence: z.number().min(0).max(1),
});

export type Conclusion = z.infer<typeof ConclusionSchema>;

export interface JurisprudenceContext {
  arret_id: string;
  juridiction: string;
  date: string;
  numero_pourvoi: string | null;
  extrait: string;
}

const SYSTEM_PROMPT = `Tu es un assistant rédactionnel pour avocats français.
Tu rédiges des CONCLUSIONS pour le contentieux civil/commercial français.

RÈGLES ABSOLUES :
1. Tu ne CITES QUE les arrêts fournis dans le contexte. JAMAIS d'invention.
2. Chaque moyen DOIT s'appuyer sur au moins une citation du contexte.
3. Tu écris dans le style du Code de procédure civile (formel, structuré).
4. Tu PROPOSES la conclusion. L'avocat décide. Mention "à valider".
5. Si le contexte est insuffisant, renvoie confidence < 0.5 et indique ce qui manque.
6. Format : JSON strict respectant le schéma fourni.
7. Tu ne traites JAMAIS des données nominatives — anonymise.`;

@Injectable()
export class ConclusionGenerator {
  private readonly logger = new Logger(ConclusionGenerator.name);

  // AsyncAnthropic côté serveur ; max_retries + timeout par appel.
  // La clé vient de l'env (ANTHROPIC_API_KEY) ; en prod cabinet, route Bedrock Paris / Anthropic EU.
  private readonly anthropic = new Anthropic({ maxRetries: 3, timeout: 60_000 });

  constructor(private readonly mistral: MistralClient) {}

  async generate(input: {
    affaireContexte: string;
    jurisprudences: JurisprudenceContext[];
    typeContentieux: "civil" | "commercial" | "social";
    primary?: "mistral" | "anthropic";
  }): Promise<Conclusion> {
    const provider = input.primary ?? "mistral";
    const prompt = this.buildPrompt(input);

    try {
      if (provider === "mistral") {
        const raw = await this.mistral.complete({
          model: "mistral-large-latest",
          system: SYSTEM_PROMPT,
          messages: [{ role: "user", content: prompt }],
          responseFormat: { type: "json_object" },
          temperature: 0.2,
        });
        const parsed = ConclusionSchema.parse(JSON.parse(raw));
        this.assertCitationsExist(parsed, input.jurisprudences);
        this.assertConfidenceGate(parsed); // refus si confiance < seuil → l'avocat reprend la main
        return parsed;
      }

      // Fallback Anthropic : SDK officiel + structured outputs (messages.parse).
      // L'API contraint la sortie au schéma zod — plus de JSON.parse fragile.
      const resp = await this.anthropic.messages.parse({
        model: "claude-opus-4-8", // qualité de raisonnement pour le contentieux
        max_tokens: 8000,
        thinking: { type: "adaptive" }, // PAS de budget_tokens (HTTP 400 sur 4.8)
        output_config: {
          effort: "high",
          format: zodOutputFormat(ConclusionSchema, "conclusion"),
        },
        system: [
          // prompt caching sur le préfixe stable (system + garde-fous figés)
          { type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" } },
        ],
        messages: [{ role: "user", content: prompt }],
      });

      // refus de sécurité → la sortie peut ne pas respecter le schéma
      if (resp.stop_reason === "refusal") {
        throw new Error("Refus de sécurité du modèle — escalade humaine.");
      }
      const parsed = resp.parsed_output;
      if (!parsed) throw new Error("Sortie non conforme au schéma.");

      this.logger.log({ usage: resp.usage }, "conclusion générée"); // log usage = coût
      this.assertCitationsExist(parsed, input.jurisprudences);
      this.assertConfidenceGate(parsed);
      return parsed;
    } catch (err) {
      // Décision retry/fallback sur exceptions TYPÉES, jamais sur le message.
      if (
        err instanceof Anthropic.RateLimitError ||
        err instanceof Anthropic.InternalServerError
      ) {
        this.logger.warn({ status: err.status }, "Anthropic indispo, on relaie");
      } else {
        this.logger.error({ err }, "primary failed, fallback");
      }
      if (provider === "mistral") {
        return this.generate({ ...input, primary: "anthropic" });
      }
      throw err;
    }
  }

  private assertConfidenceGate(c: Conclusion, seuil = 0.5): void {
    if (c.confidence < seuil) {
      throw new Error(
        `Confiance ${c.confidence} < ${seuil} : contexte insuffisant, ne pas livrer.`
      );
    }
  }

  private buildPrompt(input: {
    affaireContexte: string;
    jurisprudences: JurisprudenceContext[];
    typeContentieux: string;
  }): string {
    const blocs = input.jurisprudences
      .map(
        (j, i) => `
[CITATION ${i + 1}]
- Arrêt: ${j.arret_id}
- Juridiction: ${j.juridiction}
- Date: ${j.date}
- N° pourvoi: ${j.numero_pourvoi ?? "n/a"}
- Extrait: """${j.extrait}"""
`
      )
      .join("\n");

    return `Type de contentieux: ${input.typeContentieux}

CONTEXTE DE L'AFFAIRE :
${input.affaireContexte}

JURISPRUDENCE DISPONIBLE :
${blocs}

Tâche : rédige des conclusions structurées en JSON respectant le schéma fourni.
Cite UNIQUEMENT les arrêts ci-dessus. Référence-les par leur arret_id exact.`;
  }

  private assertCitationsExist(
    c: Conclusion,
    jurisprudences: JurisprudenceContext[]
  ): void {
    const validIds = new Set(jurisprudences.map((j) => j.arret_id));
    for (const moyen of c.moyens) {
      for (const cit of moyen.citations) {
        if (!validIds.has(cit.arret_id)) {
          throw new Error(
            `Hallucination détectée: arret_id ${cit.arret_id} absent du contexte.`
          );
        }
      }
    }
  }
}

// Stub du client Mistral pour clarté (le client Anthropic = SDK officiel, voir constructeur).
interface MistralClient {
  complete(args: {
    model: string;
    system: string;
    messages: { role: string; content: string }[];
    responseFormat?: { type: string };
    temperature: number;
  }): Promise<string>;
}

4. Word add-in (Office.js TypeScript)

ts
// legaltech/word-addin/taskpane.ts
/// <reference types="office-js" />

interface BackendConfig {
  baseUrl: string;
  apiKey: string;
}

async function searchJurisprudence(
  cfg: BackendConfig,
  query: string,
  juridictions: string[]
) {
  const resp = await fetch(`${cfg.baseUrl}/api/search`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${cfg.apiKey}`,
    },
    body: JSON.stringify({ query, juridictions, top_k: 8 }),
  });
  if (!resp.ok) throw new Error(`Search failed: ${resp.status}`);
  return resp.json();
}

async function insertConclusionInWord(conclusion: any) {
  return Word.run(async (context) => {
    const body = context.document.body;
    body.insertParagraph("CONCLUSIONS — DRAFT IA (À VALIDER)", "Start", {
      style: "Heading 1",
    } as any);
    body.insertParagraph("FAITS ET PROCÉDURE", "End", {
      style: "Heading 2",
    } as any);
    body.insertParagraph(conclusion.faits, "End");
    for (const moyen of conclusion.moyens) {
      body.insertParagraph(moyen.titre, "End", {
        style: "Heading 2",
      } as any);
      body.insertParagraph(moyen.argumentation, "End");
      for (const cit of moyen.citations) {
        const p = body.insertParagraph(
          `Cf. ${cit.juridiction}, ${cit.date}, n° ${cit.numero_pourvoi ?? ""} — "${cit.extrait.slice(0, 200)}…"`,
          "End"
        );
        p.font.italic = true;
        p.font.size = 10;
      }
    }
    await context.sync();
  });
}

Office.onReady(() => {
  const btn = document.getElementById("btnGenerate") as HTMLButtonElement;
  btn.addEventListener("click", async () => {
    const cfg: BackendConfig = {
      baseUrl: (document.getElementById("baseUrl") as HTMLInputElement).value,
      apiKey: (document.getElementById("apiKey") as HTMLInputElement).value,
    };
    const query = (document.getElementById("query") as HTMLTextAreaElement)
      .value;
    const status = document.getElementById("status")!;
    status.textContent = "Recherche jurisprudence…";
    try {
      const hits = await searchJurisprudence(cfg, query, ["CC", "CE"]);
      status.textContent = "Génération conclusions…";
      const conclusion = await fetch(`${cfg.baseUrl}/api/conclusion`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${cfg.apiKey}`,
        },
        body: JSON.stringify({
          affaireContexte: query,
          jurisprudences: hits,
          typeContentieux: "commercial",
        }),
      }).then((r) => r.json());
      await insertConclusionInWord(conclusion);
      status.textContent = "Inséré dans Word.";
    } catch (e: any) {
      status.textContent = `Erreur: ${e.message}`;
    }
  });
});

Déploiement

bash
# Infra : Scaleway DC Paris (ISO 27001)
# - 1x Kapsule cluster K8s (3 nodes m2.medium)
# - 1x PostgreSQL Managed (HA)
# - 1x Object Storage (chiffré + Object Lock pour audit)
# - 1x Qdrant self-hosted (déployé via Helm)
# - 1x Redis Managed pour cache embeddings

terraform init
terraform apply -var-file=production.tfvars

# Déploiement appli
helm upgrade --install legaltech-rag ./charts/legaltech \
  --namespace legaltech \
  --create-namespace \
  --set image.tag=v1.2.0 \
  --set mistral.apiKey=$MISTRAL_API_KEY \
  --set anthropic.apiKey=$ANTHROPIC_API_KEY \
  --set qdrant.url=http://qdrant.legaltech.svc.cluster.local:6333 \
  --set postgres.url=$POSTGRES_URL \
  --set audit.bucket=legaltech-audit-logs

# Add-in Word : déployé via tenant M365 du cabinet
# Manifest hébergé sur https://addin.legaltech.cabinet.fr/manifest.xml
# Centralized Deployment via Microsoft 365 Admin Center

ROI mesuré (M+3 après go-live)

KPIAvantAprèsGain
Temps moyen recherche jurisprudence7.2h2.8h-61%
Temps moyen rédaction conclusion4.5h2.1h-53%
Coût horaire moyen junior (chargé)75€75€
Volume conclusions/an480480
Économies annuelles~243 K€
NPS associésn/a52
Taux d'usage hebdon/a89%

Prix facturé : 110K€ build + 30K€/an run. Payback < 6 mois.


📚 Cas d'usage 2 — END-TO-END : Due Diligence M&A Automatisée

Contexte client

Cabinet : "Cordier Avocats" (fictif), boutique M&A mid-cap (deals 20-200M€), 22 avocats, CA 9M€. 25 DDs/an. Pic d'activité Q4 = burnout équipe.

Pain : data room moyenne 8000 docs, équipe de 4-6 avocats sur 5 semaines, taux d'erreur sur clauses change-of-control rapporté par client final 3 fois en 2 ans = sinistralité RC pro.

Brief : "Industrialise notre DD : on veut diviser le temps par 1.5, zéro miss sur les clauses critiques, et un livrable client structuré."

Solution architecture

┌─────────────────────────────────────────────────────────────────┐
│  INGESTION DATA ROOM                                            │
│  Datasite / Intralinks / iDeals API                             │
│  ↓                                                              │
│  OCR (Mistral OCR + Azure DI) → texte structuré                 │
│  ↓                                                              │
│  Classification (Haiku 4.5) → catégorie : contrats clients,     │
│  contrats fournisseurs, IP, RH, contentieux, immobilier…        │
│  ↓                                                              │
│  Stockage : S3 + index Postgres + Qdrant                        │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  EXTRACTION SPÉCIALISÉE PAR CATÉGORIE                           │
│  Pour chaque catégorie, un schéma JSON cible :                  │
│   • Contrats clients : parties, durée, CoC, indemnités, etc.    │
│   • IP : brevets, marques, licences in/out, territoires         │
│   • Contentieux : montant en jeu, juridiction, statut           │
│   • RH : top 10 salaires, golden parachutes, syndicats          │
│  → LLM avec structured outputs (Sonnet 4.6 ; Opus 4.8 sur les   │
│    clauses critiques)                                            │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  AGRÉGATION & RED-FLAG DETECTION                                │
│  Règles métier (playbook cabinet) + LLM-as-judge                │
│  Génération synthèse legal DD (Word + Excel + dashboard web)    │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  Q&A INTERACTIF                                                 │
│  Chat sur la data room, citations sources, export Word          │
└─────────────────────────────────────────────────────────────────┘

Code samples

1. Classification + extraction structurée (Python, function calling Claude)

python
# dd_m_a/extraction/classify_and_extract.py
from __future__ import annotations

import asyncio
import json
from dataclasses import dataclass
from enum import Enum
from typing import Any

import anthropic
from pydantic import BaseModel, Field


class Categorie(str, Enum):
    CONTRAT_CLIENT = "contrat_client"
    CONTRAT_FOURNISSEUR = "contrat_fournisseur"
    BAIL = "bail"
    IP = "ip"
    CONTENTIEUX = "contentieux"
    RH = "rh"
    FINANCEMENT = "financement"
    STATUTS = "statuts"
    AUTRE = "autre"


class ContratClientExtracted(BaseModel):
    parties: list[str]
    objet: str
    duree_initiale_mois: int | None
    tacite_reconduction: bool
    clause_change_of_control: bool = Field(
        description="Présence d'une clause permettant à l'autre partie de résilier en cas de changement de contrôle."
    )
    indemnite_resiliation_max_eur: float | None
    juridiction: str
    droit_applicable: str
    risque_global: str = Field(description="bas | moyen | élevé")
    notes: str


class IpExtracted(BaseModel):
    type: str  # brevet, marque, dessin, logiciel
    numero: str | None
    titulaire: str
    territoires: list[str]
    date_depot: str | None
    date_expiration: str | None
    licences_in: list[str]
    licences_out: list[str]
    litiges_associes: bool
    risque: str


SCHEMA_BY_CAT: dict[Categorie, type[BaseModel]] = {
    Categorie.CONTRAT_CLIENT: ContratClientExtracted,
    Categorie.IP: IpExtracted,
}

CLASSIFY_TOOL = {
    "name": "classify_document",
    "description": "Classifie un document de data room M&A.",
    "input_schema": {
        "type": "object",
        "properties": {
            "categorie": {
                "type": "string",
                "enum": [c.value for c in Categorie],
            },
            "confidence": {"type": "number"},
            "raisonnement_court": {"type": "string"},
        },
        "required": ["categorie", "confidence"],
    },
}


@dataclass
class Document:
    id: str
    nom_fichier: str
    texte: str
    taille_octets: int


# AsyncAnthropic pour un serveur ; max_retries gère 429/5xx avec backoff.
client = anthropic.AsyncAnthropic(max_retries=4)


async def classify(doc: Document) -> tuple[Categorie, float]:
    # Classification = tâche simple → Haiku 4.5 (1 $/5 $ par M tokens), 5-10x moins cher
    # que Sonnet sur un volume de data room (8 000 docs).
    resp = await client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=512,
        tools=[CLASSIFY_TOOL],
        tool_choice={"type": "tool", "name": "classify_document"},
        messages=[
            {
                "role": "user",
                "content": f"Classifie ce document de data room.\n\nNom: {doc.nom_fichier}\n\nDébut du texte:\n{doc.texte[:4000]}",
            }
        ],
    )
    use = next(b for b in resp.content if b.type == "tool_use")
    return Categorie(use.input["categorie"]), float(use.input["confidence"])


def _schema_to_tool(name: str, schema: type[BaseModel]) -> dict[str, Any]:
    js = schema.model_json_schema()
    return {
        "name": name,
        "description": f"Extraction structurée — {schema.__name__}",
        "input_schema": {
            "type": "object",
            "properties": js["properties"],
            "required": js.get("required", []),
        },
    }


async def extract(doc: Document, cat: Categorie) -> BaseModel | None:
    schema = SCHEMA_BY_CAT.get(cat)
    if not schema:
        return None
    tool = _schema_to_tool("extract", schema)
    # Extraction = qualité importante → Sonnet 4.6 (3 $/15 $). Pour les clauses
    # critiques (CoC, MAC, garanties), on peut router vers Opus 4.8.
    resp = await client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=4096,
        tools=[tool],
        tool_choice={"type": "tool", "name": "extract"},
        messages=[
            {
                "role": "user",
                "content": (
                    "Extrais les informations structurées du document suivant. "
                    "Si une valeur est absente, mets null. Ne JAMAIS inventer.\n\n"
                    f"Document: {doc.nom_fichier}\n\n{doc.texte[:12000]}"
                ),
            }
        ],
    )
    use = next(b for b in resp.content if b.type == "tool_use")
    return schema.model_validate(use.input)


async def process_document(doc: Document) -> dict[str, Any]:
    cat, conf = await classify(doc)
    extracted = await extract(doc, cat)
    return {
        "id": doc.id,
        "nom_fichier": doc.nom_fichier,
        "categorie": cat.value,
        "confidence": conf,
        "extracted": extracted.model_dump() if extracted else None,
    }


async def process_batch(docs: list[Document], concurrency: int = 8) -> list[dict[str, Any]]:
    sem = asyncio.Semaphore(concurrency)

    async def _work(d: Document):
        async with sem:
            return await process_document(d)

    return await asyncio.gather(*[_work(d) for d in docs])

2. Red-flag detection rule engine + LLM judge (Python)

python
# dd_m_a/redflags/detector.py
from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Callable


class Severity(str, Enum):
    GREEN = "green"
    ORANGE = "orange"
    RED = "red"


@dataclass
class RedFlag:
    code: str
    label: str
    severity: Severity
    source_doc_id: str
    extrait: str
    recommandation: str


Rule = Callable[[dict], list[RedFlag]]


def rule_change_of_control(extracted: dict) -> list[RedFlag]:
    flags = []
    for item in extracted.get("contrats_clients", []):
        if item.get("clause_change_of_control"):
            flags.append(
                RedFlag(
                    code="COC",
                    label=f"Clause change-of-control — {item['parties']}",
                    severity=Severity.RED,
                    source_doc_id=item["doc_id"],
                    extrait=item.get("notes", ""),
                    recommandation="Négocier waiver ou consentement préalable du client.",
                )
            )
    return flags


def rule_top_client_dependance(extracted: dict) -> list[RedFlag]:
    contrats = extracted.get("contrats_clients", [])
    revenue_total = sum(c.get("ca_annuel_eur", 0) for c in contrats)
    if revenue_total == 0:
        return []
    flags = []
    for c in sorted(contrats, key=lambda x: x.get("ca_annuel_eur", 0), reverse=True)[:3]:
        pct = c.get("ca_annuel_eur", 0) / revenue_total
        if pct > 0.25:
            flags.append(
                RedFlag(
                    code="CONCENTRATION",
                    label=f"Concentration client {c['parties']} ({pct:.0%})",
                    severity=Severity.ORANGE if pct < 0.4 else Severity.RED,
                    source_doc_id=c["doc_id"],
                    extrait=f"{pct:.0%} du CA total.",
                    recommandation="Demander engagement de renouvellement / earn-out lié.",
                )
            )
    return flags


def rule_ip_litiges(extracted: dict) -> list[RedFlag]:
    flags = []
    for ip in extracted.get("ip", []):
        if ip.get("litiges_associes"):
            flags.append(
                RedFlag(
                    code="IP_LITIGE",
                    label=f"Litige IP en cours — {ip.get('numero','?')}",
                    severity=Severity.RED,
                    source_doc_id=ip["doc_id"],
                    extrait=ip.get("notes", ""),
                    recommandation="Exclure de la garantie de passif ou ajuster prix.",
                )
            )
    return flags


RULES: list[Rule] = [
    rule_change_of_control,
    rule_top_client_dependance,
    rule_ip_litiges,
]


def run_rules(extracted: dict) -> list[RedFlag]:
    out = []
    for r in RULES:
        out.extend(r(extracted))
    return out

3. Export synthèse DD vers Word (Python python-docx)

python
# dd_m_a/export/synthese_word.py
from datetime import date

from docx import Document
from docx.shared import Pt, RGBColor

from .detector import RedFlag, Severity


def severity_color(sev: Severity) -> RGBColor:
    return {
        Severity.GREEN: RGBColor(0x2E, 0x7D, 0x32),
        Severity.ORANGE: RGBColor(0xEF, 0x6C, 0x00),
        Severity.RED: RGBColor(0xC6, 0x28, 0x28),
    }[sev]


def build_synthese(
    output_path: str,
    cible_nom: str,
    cible_secteur: str,
    redflags: list[RedFlag],
    nb_documents: int,
    nb_contrats: int,
) -> None:
    doc = Document()
    h = doc.add_heading(f"Synthèse Legal DD — {cible_nom}", level=0)
    doc.add_paragraph(f"Secteur : {cible_secteur}")
    doc.add_paragraph(f"Date du rapport : {date.today().isoformat()}")
    doc.add_paragraph(
        f"Volume analysé : {nb_documents} documents, {nb_contrats} contrats."
    )

    doc.add_heading("Points d'attention", level=1)
    for sev in [Severity.RED, Severity.ORANGE, Severity.GREEN]:
        flags = [f for f in redflags if f.severity == sev]
        if not flags:
            continue
        h2 = doc.add_heading(
            f"{sev.value.upper()} ({len(flags)})", level=2
        )
        h2.runs[0].font.color.rgb = severity_color(sev)
        for f in flags:
            p = doc.add_paragraph()
            r = p.add_run(f"[{f.code}] {f.label}")
            r.bold = True
            r.font.color.rgb = severity_color(sev)
            doc.add_paragraph(f"  Extrait : {f.extrait[:300]}")
            doc.add_paragraph(f"  Recommandation : {f.recommandation}")
            doc.add_paragraph(f"  Source : doc_id={f.source_doc_id}")
            doc.add_paragraph("")

    doc.add_heading("Avertissement", level=1)
    avert = doc.add_paragraph(
        "Ce rapport est une synthèse générée avec assistance par IA. "
        "Il ne se substitue pas à la revue par un avocat habilité. "
        "Toutes les conclusions doivent être validées avant communication au client."
    )
    avert.runs[0].font.size = Pt(9)
    doc.save(output_path)

ROI mesuré

KPIAvantAprèsGain
Temps moyen DD complète (heures équipe)1 850h1 080h-42%
Taux miss clauses CoC2-3%<0.3%qualité forte
Délai livraison synthèse client35j22j-37%
Capacité DD/an (équipe constante)2538+52%
CA additionnel potentiel (forfait)~2.6 M€/an

Solution facturée : 220K€ build (3 mois) + 70K€/an run + 15% pricing sur outils tiers.


⚖️ Réglementation française / EU

RGPD spécifique LegalTech

  • Base légale la plus solide : intérêt légitime du cabinet à fournir un service de qualité, ou exécution du mandat client.
  • Données sensibles (article 9) : santé, opinions politiques, condamnations pénales — nombreuses en contentieux. Nécessite mesures renforcées (chiffrement, accès stricts, DPIA).
  • DPIA obligatoire quasi systématiquement (traitement à grande échelle, données sensibles, profilage).
  • Sous-traitance : tout fournisseur LLM = sous-traitant RGPD → DPA signé, audit, droit d'audit.
  • Transferts hors UE : à éviter ABSOLUMENT. Si OpenAI, passer par Azure FR (et même là, prudence : Schrems II). Mistral / Scaleway = safe.
  • Droits des personnes : un avocat ne peut pas effacer un document de procédure sur demande RGPD — exception "intérêt public, prétentions en justice".

Secret professionnel (RIN — Règlement Intérieur National)

  • Article 2 RIN : secret absolu, illimité dans le temps, opposable à tous.
  • Conséquences IA :
    • Pas de fine-tuning sur données clients sans contrats explicites.
    • Pas de "training opt-in" chez OpenAI / Anthropic — vérifier paramètres entreprise.
    • Hébergement physique en France ou UE = forte préférence ordinale.
    • Le CNB a publié en 2024-2025 plusieurs guidances IA et avocats (à citer dans tes propositions).

AI Act EU (Règlement UE 2024/1689, entrée en vigueur progressive 2025-2027)

  • LegalTech = pas automatiquement "high-risk", mais :
    • Aide à la décision judiciaire = high-risk (annexe III, point 8).
    • Identification biométrique = high-risk.
    • Si tu fais du "scoring de chances de gagner un procès" → high-risk → obligations lourdes (gestion risques, qualité données, transparence, supervision humaine, logs, conformité CE).
  • Dates clés : entrée en vigueur 1 août 2024 ; interdictions (risque inacceptable) depuis 2 février 2025 ; GPAI depuis 2 août 2025 ; Annexe III high-risk : initialement 2 août 2026, repoussé à 2 décembre 2027 (accord politique provisoire Conseil/Parlement du 7 mai 2026 – Digital Omnibus – à confirmer après adoption formelle). Pour les systèmes high-risk intégrés à des produits régulés (Annexe I) : 2 août 2027 (potentiellement 2 août 2028 selon le Digital Omnibus).
  • Obligations transparence (article 50) : informer l'utilisateur qu'il interagit avec une IA, marquer les contenus générés.

RPVA (Réseau Privé Virtuel des Avocats)

  • Pour transmissions juridictionnelles électroniques (e-Barreau).
  • Pas directement pertinent pour ton IA, mais si tu pousses des conclusions vers RPVA, il faut passer par le client e-Barreau via clé Lex'Avocat.

Déontologie spécifique

  • Pas de démarchage : tu ne peux pas pousser des annonces ciblées à des clients potentiels via l'IA depuis le cabinet.
  • Honoraires : transparence requise — si tu factures un client via outil IA, doit apparaître clairement.
  • Conflits d'intérêts : ton IA peut détecter des conflits — bonus de positionnement.

Autres

  • CNIL : guidances LLM publiées 2024-2025, à lire et citer.
  • Devoir de vigilance (loi 2017) : grandes entreprises doivent cartographier risques fournisseurs → use case KYC LegalTech.
  • CSRD (transposition 2024) : reporting durabilité → use case automation production de rapports.

🏆 Concurrents / acteurs établis

LegalTech FR pure-play

ActeurPositionnementForcesFaiblessesComment se différencier
Doctrine.frSearch jurisprudenceMarque connue, base donnéesPas de RAG profond, pas générationTu vends l'IA "in-house" sur leur base ou complémentaire
PredicticeAnalytics jurisprudenceStats sortie procèsPeu de générationMêmes ouvertures
Hyperlex (Septeo)Contract reviewBien implanté grands comptesStack legacy, peu IA genSolution moderne, plus rapide à déployer
LegalflyLegalOps IABon produit, levée récenteBelge, peu présent FRAncrage local + custom
Legalstart / Captain ContratSelf-service PMEMarketing fortPas adapté grands cabinetsTu vises l'autre segment

Éditeurs juridiques historiques

  • Lefebvre Sarrut : "Genia" lancé 2024, RAG sur Dalloz. Si tu collabores comme prestataire ou complément, c'est du gros volume.
  • LexisNexis : Lexis+ AI (US/UK), arrive en FR. Concurrent direct mais cher.
  • Wolters Kluwer : tradition mais IA en retard.

ESN / Conseil

  • Capgemini Invent, Sopra Steria, Wavestone, Onepoint : sur les gros comptes, mais TJM consultant 700-900€, peu d'expertise AI deep.
  • Tu peux toi-même sous-traiter pour eux à 900-1100€/j (rente plus stable mais marge plus faible).

GAFAM

  • Microsoft Copilot for Legal : intégré dans M365, generic. Tu te différencies par le custom métier FR.
  • Harvey (US) : très cher, peu localisé FR. Concurrent dangereux à moyen terme — anticipe.

Freelance / boutiques

  • Très peu de freelances reconnus AI + LegalTech FR en 2026. C'est ton opportunité.
  • Quelques boutiques 5-15 personnes (ex : Athennian, MyCaseHub) qui pourraient t'embaucher.

Comment se différencier (ton angle)

  1. Souveraineté + secret pro : "Tout chez Mistral + Scaleway, jamais OpenAI direct."
  2. Office add-in : les autres font des SaaS séparés. Toi tu vis DANS Word.
  3. iManage native : les autres font de l'OCR + upload manuel. Toi tu plug iManage.
  4. Pricing transparent : forfait audit/POC/prod, pas un "demander un devis".
  5. Contenu Open Law / Village de la Justice : 1 article/mois, présence physique 4 events/an.

🎤 Pitch deck / proposition commerciale

Template proposition commerciale (10 slides)

  1. Couverture : "AI Engineer LegalTech — Proposition [Cabinet X]"
  2. Le problème (en leur langage) : "Vos juniors passent 60% de leur temps sur la recherche jurisprudence. Vos associés signent des conclusions sans vérifier les citations. Vous perdez en marge et en sécurité juridique."
  3. Solution proposée : architecture en 1 schéma, en français
  4. Périmètre POC (15j) : 1 use case ciblé, 1 livrable précis
  5. Méthodologie : sprint 1 — découverte, sprint 2 — build, sprint 3 — démo + benchmark
  6. Stack & souveraineté : Mistral + Scaleway, conformité RGPD/CNB
  7. Sécurité : DPIA, chiffrement, audit logs, secret pro
  8. Coût POC : 22 000 € HT, forfait, 50% à la commande, 50% à la livraison
  9. Roadmap post-POC : si validation, production 80-120K€, run 30K€/an
  10. Mes références / pourquoi moi : 2-3 missions, citations clients

Email type prospection (cold outbound)

Sujet : Conclusions et jurisprudence — gain de 50% de temps sans risque RGPD

Bonjour Maître [Nom],

Sur LinkedIn, j'ai vu que vous publiez régulièrement sur l'innovation en cabinet. 
Je suis AI Engineer spécialisé LegalTech (avant : 10 ans CTO / dev SaaS).

Je suis exactement au croisement de deux problèmes que je vois revenir chez tous 
les cabinets contentieux que j'accompagne :

1. Les juniors passent 6-12h par dossier en recherche jurisprudence
2. Les associés ne peuvent pas (déontologiquement) utiliser ChatGPT — secret pro

J'ai construit une solution qui répond aux deux :
 - RAG sur Légifrance + base interne iManage
 - Génération de conclusions avec citations vérifiables (zéro hallucination)
 - 100% souverain : Mistral Large + Scaleway DC Paris
 - Add-in Word natif (vos avocats restent dans leur outil)

Résultat chez [Référence X, anonymisée] : temps de recherche divisé par 2.5,
NPS associés 52, payback 6 mois.

Auriez-vous 30 minutes la semaine prochaine pour que je vous montre une démo
sur un cas concret ? Je m'adapte à votre agenda.

Cordialement,
[Prénom Nom]
AI Engineer | LegalTech | Paris
+33 X XX XX XX XX
linkedin.com/in/...

3 templates LinkedIn posts (à publier dans le 1er trimestre)

Post 1 — Educatif (semaine 2)

"Trois raisons pour lesquelles ChatGPT est interdit dans la plupart des cabinets d'avocats français en 2026 :

  1. Secret professionnel (RIN art. 2)
  2. Conditions d'usage OpenAI ambiguës (training data, juridiction US)
  3. Risque d'hallucination de jurisprudence (cf. l'affaire Mata vs Avianca, 2023, NY)

Et trois alternatives qui marchent réellement chez mes clients cabinets FR : [...] Si tu es associé Innovation, je donne ma stack complète en commentaire."

Post 2 — Cas client anonymisé (semaine 6)

"Comment un cabinet contentieux Paris a divisé par 2,5 son temps de recherche jurisprudence — sans aucun cloud US. Le chiffre exact : 7,2h → 2,8h par dossier. Sur 480 dossiers/an. Stack : Mistral Large 2, Qdrant, Office add-in Word. Hébergement Scaleway Paris. Voici les 4 erreurs que j'ai vues les cabinets faire AVANT d'arriver à ce résultat 👇"

Post 3 — Provocateur (semaine 10)

"Doctrine et Predictice sont géniaux pour CHERCHER de la jurisprudence. Aucun ne RÉDIGE vos conclusions. Et c'est là que je vois 80% du temps avocat consommé. Hot take : le prochain gagnant LegalTech FR sera celui qui industrialise la RÉDACTION, pas la recherche."


🚀 Plan d'attaque 90 jours

Mois 1 — Set-up & contenu

Semaine 1

  • Statut : Micro-entreprise ou SASU (préférable SASU si tu vises >70K€/an)
  • LinkedIn : refonte headline → "AI Engineer LegalTech | RAG + Souveraineté Mistral | Paris"
  • Site web 1-page : positionnement + 3 use cases + CTA "Audit IA 5 jours / 8K€"
  • DPIA template + DPA template prêts à signer

Semaine 2

  • Lire le RIN entier (CNB.fr) + guidances CNB sur IA (2024-2025)
  • Lire le rapport CNIL sur LLMs (2024)
  • Comprendre l'écosystème PISTE / Légifrance API + obtenir un token
  • Premier POC interne : RAG sur Cassation civ. dernières 5 ans (5j de travail)

Semaine 3

  • Premier post LinkedIn (le post éducatif "trois raisons ChatGPT interdit")
  • Lister 50 cabinets cibles tier 2 (Fidal régional, boutiques contentieux)
  • Lister 30 contacts (associés Innovation, Knowledge Managers, DSI cabinet)
  • Inscription Open Law + Village de la Justice (compte auteur)

Semaine 4

  • Publier 1er article sur Village de la Justice (technique mais accessible)
  • Cold outreach : 20 emails personnalisés par semaine (pas 200 massifs)
  • Préparer 5 templates de propositions commerciales (audit, POC, prod)

Mois 2 — Prospection active

Semaine 5-6

  • Continuer cold outreach (20/semaine)
  • Réponses attendues : 5-10% taux RDV → 1-2 RDVs/semaine
  • Préparer démo live : 15 minutes, sur leur jurisprudence (préparer en amont)
  • Aller au LegalTech Paris (salon annuel, octobre)
  • Aller à 1 événement Open Law / Maison du Barreau

Semaine 7-8

  • 1er audit signé attendu : 5j × 1600€ = 8K€
  • Pendant l'audit : c'est de la prospection active interne (rencontre 5-10 personnes du cabinet)
  • Post LinkedIn case study anonymisé (post 2)
  • Présentation gratuite "30 min IA et secret pro" à 2-3 cabinets cibles

Mois 3 — Conversion & 1ère mission lourde

Semaine 9-10

  • Soutenance audit : présentation roadmap + proposition POC
  • Closing POC : 15j × 1500€ = 22K€
  • Démarrage POC ASAP, livraison sous 4 semaines max
  • Continuer 2 posts LinkedIn/semaine

Semaine 11-12

  • POC en cours
  • Préparer plaquette commerciale complète
  • Demander 1 témoignage client (audit) pour landing page
  • Identifier 1 partenariat ESN (sous-traitance à 1100€/j en complément)
  • Préparer mission "production" pour client audit

Objectif fin S12 :

  • 1 audit livré, 1 POC en cours, 1 mission production en pipe, 1500-2500 followers LinkedIn ciblés
  • Premier 30K€ HT facturé
  • Pipeline 80K€ HT pour les 6 mois suivants

Mois 4-6 (anticipation post-90j)

  • Productionalisation client POC : 60-90j × 1300€ = 78-117K€
  • 2ème audit en parallèle
  • Conférence Open Law ou Village de la Justice (talk 20 min — gold standard)

🔗 Liens

Associations professionnelles

  • CNB — Conseil National des Barreaux — cnb.avocat.fr
  • Open Law — openlaw.fr — innovation droit
  • AFJE — Juristes d'entreprise — afje.org
  • Cercle Montesquieu — DJ CAC40
  • Ordre des Avocats de Paris — avocatparis.org
  • CSN — Conseil Supérieur du Notariat — notaires.fr

Salons / événements

  • LegalTech Paris (octobre)
  • VivaTech (juin) — stand legaltech
  • JurisFutur (CNB, annuel)
  • Salon des Maires (novembre — directions juridiques collectivités)
  • Paris Legal Makers (HQ Maison du Barreau)
  • Le Village by CA — startups legaltech accelerator

Médias spécialisés

  • Village de la Justice — village-justice.com (publier ici = grand reach)
  • Dalloz Actualité — actu.dalloz-etudiant.fr
  • Editions Législatives — editions-legislatives.fr
  • Gazette du Palais — gazette-du-palais.fr
  • Maddyness > LegalTech — maddyness.com
  • L'Usine Digitale > LegalTech

Communautés Slack / Discord / LinkedIn FR

  • Open Law Slack (sur demande sur openlaw.fr)
  • LinkedIn group "LegalTech France"
  • #LegalTechFr sur LinkedIn (hashtag actif)
  • AI + Law France (LinkedIn group, modéré par certains universitaires)
  • France Digitale — section AI/legaltech
  • CNB.avocat.fr — espace innovation

Lectures fondamentales (1 semaine de lecture nécessaire)

  • "Rapport Cadiet sur l'open data des décisions de justice" (publication CNB)
  • "Guide CNIL des LLM" (2024-2025)
  • "Charte éthique IA et professions du droit" (CNB)
  • "AI Act Annexe III + considérants 28-37" (admin justice = high-risk)
  • "Doctrine sur l'avocat augmenté" — Bruno Deffains, Catherine Léger-Jarniou

Données / APIs utiles

  • PISTE — DILA Légifrance API : piste.gouv.fr
  • Pappers API : pappers.fr/api
  • Infogreffe API : infogreffe.fr
  • Open Sanctions : opensanctions.org
  • EUR-Lex API : eur-lex.europa.eu
  • CJUE — InfoCuria

🧠 Comment un staff engineer raisonne sur une stack LegalTech

Avant d'écrire une ligne, le staff engineer LegalTech répond à cinq questions — ce sont elles que le comité d'éthique du cabinet va te poser, pas « quel framework ».

1. Où vit la donnée, à chaque hop ? Le secret professionnel est absolu (RIN art. 2). Tu dois pouvoir tracer chaque octet : ingestion → embedding → vector DB → prompt → réponse → log. Un seul appel vers un endpoint US non couvert par un DPA = faute déontologique potentielle. D'où la règle : souverain par défaut (Mistral La Plateforme FR, ou Claude via Bedrock région Paris / Anthropic EU avec DPA + zero-retention), Qdrant self-hosted, logs en France (S3 Object Lock pour l'immutabilité d'audit). Le « ChatGPT wrapper » est mort à la naissance ici.

2. Quel est le coût d'une hallucination ? En contentieux, une citation de jurisprudence inventée n'est pas un bug — c'est l'affaire Mata v. Avianca (sanctions disciplinaires). Donc l'architecture impose : (a) RAG strict (le modèle ne cite QUE le contexte fourni), (b) vérification programmatique post-génération (assertCitationsExist — on rejette tout arret_id absent du contexte), (c) une gate de confiance (confidence < seuil → refus + escalade humaine), (d) un avertissement « DRAFT IA — à valider » inamovible dans chaque livrable. La supervision humaine n'est pas optionnelle : sur un système d'aide à la décision judiciaire, c'est une obligation AI Act (annexe III).

3. Quel modèle pour quelle passe ? Le réflexe junior est « le meilleur modèle partout ». Le réflexe senior est le routing par valeur : Haiku 4.5 (1 $/5 $) pour la classification de 8 000 docs de data room ; Sonnet 4.6 (3 $/15 $) pour l'extraction structurée en batch ; Opus 4.8 (5 $/25 $, contexte 1M) réservé au raisonnement à fort enjeu (clauses CoC/MAC, conclusions contentieux). Sur une DD de 8 000 docs, mettre Opus partout coûte 5-8x plus cher pour un gain marginal sur 90 % des docs. Le contexte 1M d'Opus 4.8 (au tarif standard, sans premium long-contexte) change la donne pour le Q&A data room : tu peux charger une data room entière sans chunking agressif sur les passes critiques.

4. Comment je facture le coût LLM ? Tu logges resp.usage (input/output/cache_read) sur chaque appel, tu agrèges par dossier, et tu le compares au prix de revente. Sur l'extraction batch, le prompt caching sur le préfixe stable (system + playbook cabinet + schéma) divise le coût du contexte rejoué par ~10. Sur du non-temps-réel (revue de 500 contrats nocturne), la Batches API (-50 %) est ton amie. Un coût LLM non instrumenté = une marge que tu ne contrôles pas.

5. Latence et débit. L'avocat dans Word veut une réponse en secondes, pas en minutes. Donc : streaming pour la génération longue (conclusions), asyncio.gather / Promise.all pour les passes parallèles (classification de masse avec un Semaphore pour ne pas saturer le rate limit), retries SDK typés (max_retries) avec backoff sur 429/529. Pour une DD nocturne, on bascule en débit (Batches) ; pour le add-in Word interactif, on optimise la latence (effort plus bas, cache chaud).

Mental model en une phrase : Le moat n'est pas le LLM (commodité), c'est la couche de garanties autour — souveraineté prouvable, zéro-hallucination vérifiable, intégration iManage/Word, et un coût par dossier que tu peux défendre devant un associé.

Tableau des arbitrages clés

DécisionOption AOption BCe que choisit le senior
Modèle de générationOpus 4.8 partoutRouting Haiku/Sonnet/OpusRouting — Opus seulement sur l'enjeu, sinon la marge fond
Sortie structuréePrompt « renvoie du JSON »messages.parse() + schéma (structured outputs natifs)Natif — l'API contraint le schéma, plus de parsing fragile
Profondeur de raisonnementbudget_tokens fixethinking: adaptive + effortAdaptivebudget_tokens renvoie HTTP 400 sur 4.7/4.8
HébergementOpenAI directMistral FR / Bedrock Paris + Qdrant self-hostSouverain — secret pro, sinon comité d'éthique bloque
Vérif citationsConfiance dans le modèleVérif programmatique + gate de confianceProgrammatique — l'hallucination est un risque RC pro
Coût batchLive APIBatches API (-50 %) + prompt cachingBatches + cache sur tout ce qui n'est pas temps réel

Modes de défaillance (et la parade)

  • Hallucination de jurisprudence → RAG strict + assertCitationsExist qui throw sur tout id hors contexte. Tu fais échouer la requête plutôt que livrer un faux arrêt.
  • Fuite de données vers un endpoint non conforme → allowlist d'egress (env limited), DPA + zero-retention, jamais OpenAI direct. Un test d'intégration qui échoue si un appel sort de la zone FR/EU.
  • Dérive de coût silencieuse → log usage par dossier + alerte si le coût/dossier dépasse X. Un cache invalidé (timestamp dans le system prompt) peut multiplier ta facture — audit cache_read_input_tokens = 0 sur des requêtes au préfixe identique.
  • Rate limit en pic de DDSemaphore côté client, max_retries SDK, et bascule Batches pour le volume non interactif.
  • Refus de sécurité du modèle (stop_reason: "refusal") → tester stop_reason AVANT de lire content, sinon crash sur content[0]. Escalade humaine sur refus.

🏋️ Exercices

Progressifs, durs, orientés production. Chaque exercice a un objectif et un indice/solution. Ne te contente pas de faire tourner — défends tes chiffres devant un associé fictif.

Exercice 1 — RAG jurisprudence anti-hallucination (implémenter)

Objectif : construire un endpoint /conclusion qui ne cite QUE des arrêts présents dans le contexte retrouvé, et rejette toute citation inventée.

Reprends ConclusionGenerator. Branche un vrai retrieval (le hybrid_search.py fourni), passe les hits comme contexte, génère via client.messages.parse() avec ConclusionSchema. Ajoute un test qui injecte volontairement un prompt poussant le modèle à citer un arrêt absent, et vérifie que assertCitationsExist throw.

Indice/Solution : le seul garde-fou fiable est programmatique : validIds = new Set(jurisprudences.map(j => j.arret_id)), et tu rejettes toute citation hors set. Le prompt aide mais ne garantit rien — la vérification post-génération est la garantie.

Exercice 2 — Router de coût Haiku/Sonnet/Opus (optimiser)

Objectif : sur une data room de 8 000 docs, faire passer le coût LLM total sous un budget cible en routant intelligemment.

Implémente un routeur : classification → Haiku 4.5 ; extraction standard → Sonnet 4.6 ; extraction de clause critique (CoC, MAC, garantie de passif) → Opus 4.8. Logge usage par appel, agrège, et produis un coût total. Compare avec « Opus partout » et « Sonnet partout ».

Indice/Solution : la classification est ~500 tokens out, l'extraction ~4 000 ; 90 % des docs ne sont pas critiques. Ajoute prompt caching sur le schéma + playbook (préfixe stable) → vérifie cache_read_input_tokens > 0. Attends-toi à un facteur 4-6x entre « tout Opus » et le routing + cache.

Exercice 3 — Pipeline batch nocturne (rendre production-grade)

Objectif : revoir 500 contrats par nuit, idempotent, reprenable, observable, à -50 % de coût.

Passe le process_batch synchrone à la Batches API Anthropic. Ajoute : idempotence (custom_id = hash du doc, skip si déjà traité), reprise sur panne (persistance du batch_id, polling), et observabilité (Langfuse traces + coût par contrat dans Postgres). Gère les résultats errored (validation vs serveur) et expired.

Indice/Solution : Batches = -50 % mais asynchrone (jusqu'à 24h). custom_id stable = idempotence gratuite. Stocke batch_id + statut en base ; un worker poll. Sépare erreurs invalid_request (à corriger, ne pas retry) des erreurs serveur (retry safe).

Exercice 4 — Casser puis réparer la souveraineté (red team)

Objectif : prouver qu'aucune donnée client ne sort de la zone FR/EU, et le rendre testable en CI.

Écris un test d'intégration qui échoue si un appel réseau part vers un host hors allowlist FR/EU. Puis introduis volontairement une régression (un datetime.now() interpolé dans le system prompt) et montre l'impact : (a) cache cassé (cache_read_input_tokens tombe à 0, coût ×N), (b) prouve-le avec deux requêtes au préfixe censé identique.

Indice/Solution : mock la couche HTTP, assert sur le host de destination. Pour le cache : tout octet variable en tête de préfixe invalide tout ce qui suit (prefix match). Le timestamp est l'invalidateur silencieux classique — déplace-le après le dernier breakpoint ou supprime-le.

Exercice 5 — Défendre le chiffre devant l'associé (FinOps + ROI)

Objectif : produire un modèle de coût/dossier défendable, et le prix de revente qui te garde une marge cible.

À partir des usage loggés (Exercices 2-3), construis : coût LLM moyen par mémo et par DD, coût d'infra amorti (Scaleway K8s + Qdrant + Postgres), et le prix de revente pour une marge de 70 %. Fais l'analyse de sensibilité : que se passe-t-il si Anthropic double le prix d'Opus ? si le cabinet double le volume ?

Indice/Solution : marge = (prix revente − coût LLM − infra amortie − ton temps de run) / prix revente. La sensibilité au prix LLM doit être faible si ton routing est bon (Opus = petite part des tokens). Si doubler Opus tue ta marge, c'est que tu sur-utilises Opus — retour Exercice 2.

Exercice 6 — Add-in Word résilient (intégration end-to-end)

Objectif : rendre le taskpane.ts robuste : streaming de la génération, états de chargement, gestion du refus modèle et des timeouts, insertion track-changes propre dans iManage.

Ajoute : streaming SSE de la conclusion (afficher le texte au fil de l'eau, pas un freeze de 30s), gestion de stop_reason: "refusal" (message clair à l'avocat), retry sur timeout réseau, et export DOCX track-changes vers iManage. Type tout proprement (pas de any).

Indice/Solution : stream → content_block_delta avec text_delta. Teste stop_reason avant de lire content. Le as any sur les styles Word doit disparaître au profit des types Office.js. L'insertion track-changes passe par context.document avec révisions activées côté template.


🎤 En entretien

Questions seniors que cette verticale appelle. Réponse en une ligne.

Q : Un cabinet refuse tout cloud US. Comment tu garantis le secret professionnel sur une stack LLM ? R : Souverain par défaut (Mistral FR ou Claude via Bedrock Paris/Anthropic EU avec DPA + zero-retention), Qdrant self-hosted, egress en allowlist FR/EU testée en CI, logs immuables en France — la souveraineté doit être prouvable, pas affirmée.

Q : Comment tu empêches l'hallucination de jurisprudence, et pourquoi le prompt ne suffit pas ? R : RAG strict + vérification programmatique post-génération (rejet de tout arret_id hors contexte) + gate de confiance + mention « à valider » ; le prompt réduit le risque mais seul un check déterministe le garantit (sinon : Mata v. Avianca).

Q : Tu as une data room de 8 000 docs. Comment tu choisis tes modèles et tu défends le coût ? R : Routing par valeur — Haiku pour classifier, Sonnet pour extraire, Opus seulement sur les clauses critiques — plus prompt caching et Batches API (-50 %) sur le non-temps-réel ; je logge usage par dossier pour défendre un coût/DD chiffré.

Q : Pourquoi budget_tokens ne marche plus, et qu'est-ce que tu utilises à la place ? R : Sur Opus 4.7/4.8 le thinking à budget fixe est supprimé et renvoie HTTP 400 ; on pilote la profondeur via thinking: {type: "adaptive"} + output_config.effort (lowmax), et la sortie via structured outputs natifs (messages.parse) plutôt qu'un prompting JSON fait main.

Q : Ce système est-il « high-risk » au sens de l'AI Act, et qu'est-ce que ça implique ? R : Un assistant rédactionnel supervisé non, mais dès qu'on touche à l'aide à la décision judiciaire / au scoring de chances de procès (annexe III), oui → gestion des risques, qualité des données, transparence, supervision humaine, logs et conformité ; d'où la gate de confiance et l'avertissement inamovibles dans l'archi.


Note finale : Cette verticale est dure, lente à démarrer, mais en termes de moat / pricing / qualité de mission, c'est probablement la meilleure si tu acceptes 6 mois sans revenu pour 3 ans très rentables ensuite. Si tu n'as pas la trésorerie, commence par fintech ou ecommerce qui closent en 1-3 mois.

Bibliothèque tech perso — Achref