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)
| Cabinet | Avocats FR | Spécialités | Budget innovation estimé |
|---|---|---|---|
| Gide Loyrette Nouel | ~600 | M&A, contentieux, banque | 3-5M€/an |
| Bredin Prat | ~200 | M&A, contentieux haut de gamme | 1-2M€/an |
| Cleary Gottlieb (Paris) | ~100 | M&A international | 1-2M€/an |
| A&O Shearman (Paris) (fusion A&O × Shearman, mai 2024) | ~250 | Finance, M&A | 2-3M€/an |
| Linklaters (Paris) | ~170-200 | Corporate, banque | 2-3M€/an |
| August Debouzy | ~150 | Corporate, IP, tech | 1-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, ououtput_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 $) etclaude-haiku-4-5(1 $/5 $). Anthropic n'expose plus de budget de thinking fixe (budget_tokensrenvoie HTTP 400 sur 4.7/4.8) : on pilote la profondeur de raisonnement parthinking: {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
| Profil | TJM | Conditions |
|---|---|---|
| Junior AI gen | 500-700€ | Pas vertical-fit, gros risque |
| AI Engineer généraliste | 700-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énior | 1100-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)
# 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)
# 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 leJSON.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 destring.includes("429"). Le thinking se pilote enadaptive+output_config.effort—budget_tokensrenvoie HTTP 400 sur Opus 4.8.
// 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)
// 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
# 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 CenterROI mesuré (M+3 après go-live)
| KPI | Avant | Après | Gain |
|---|---|---|---|
| Temps moyen recherche jurisprudence | 7.2h | 2.8h | -61% |
| Temps moyen rédaction conclusion | 4.5h | 2.1h | -53% |
| Coût horaire moyen junior (chargé) | 75€ | 75€ | — |
| Volume conclusions/an | 480 | 480 | — |
| Économies annuelles | — | — | ~243 K€ |
| NPS associés | n/a | 52 | — |
| Taux d'usage hebdo | n/a | 89% | — |
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)
# 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)
# 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 out3. Export synthèse DD vers Word (Python python-docx)
# 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é
| KPI | Avant | Après | Gain |
|---|---|---|---|
| Temps moyen DD complète (heures équipe) | 1 850h | 1 080h | -42% |
| Taux miss clauses CoC | 2-3% | <0.3% | qualité forte |
| Délai livraison synthèse client | 35j | 22j | -37% |
| Capacité DD/an (équipe constante) | 25 | 38 | +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
| Acteur | Positionnement | Forces | Faiblesses | Comment se différencier |
|---|---|---|---|---|
| Doctrine.fr | Search jurisprudence | Marque connue, base données | Pas de RAG profond, pas génération | Tu vends l'IA "in-house" sur leur base ou complémentaire |
| Predictice | Analytics jurisprudence | Stats sortie procès | Peu de génération | Mêmes ouvertures |
| Hyperlex (Septeo) | Contract review | Bien implanté grands comptes | Stack legacy, peu IA gen | Solution moderne, plus rapide à déployer |
| Legalfly | LegalOps IA | Bon produit, levée récente | Belge, peu présent FR | Ancrage local + custom |
| Legalstart / Captain Contrat | Self-service PME | Marketing fort | Pas adapté grands cabinets | Tu 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)
- Souveraineté + secret pro : "Tout chez Mistral + Scaleway, jamais OpenAI direct."
- Office add-in : les autres font des SaaS séparés. Toi tu vis DANS Word.
- iManage native : les autres font de l'OCR + upload manuel. Toi tu plug iManage.
- Pricing transparent : forfait audit/POC/prod, pas un "demander un devis".
- 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)
- Couverture : "AI Engineer LegalTech — Proposition [Cabinet X]"
- 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."
- Solution proposée : architecture en 1 schéma, en français
- Périmètre POC (15j) : 1 use case ciblé, 1 livrable précis
- Méthodologie : sprint 1 — découverte, sprint 2 — build, sprint 3 — démo + benchmark
- Stack & souveraineté : Mistral + Scaleway, conformité RGPD/CNB
- Sécurité : DPIA, chiffrement, audit logs, secret pro
- Coût POC : 22 000 € HT, forfait, 50% à la commande, 50% à la livraison
- Roadmap post-POC : si validation, production 80-120K€, run 30K€/an
- 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 :
- Secret professionnel (RIN art. 2)
- Conditions d'usage OpenAI ambiguës (training data, juridiction US)
- 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écision | Option A | Option B | Ce que choisit le senior |
|---|---|---|---|
| Modèle de génération | Opus 4.8 partout | Routing Haiku/Sonnet/Opus | Routing — Opus seulement sur l'enjeu, sinon la marge fond |
| Sortie structurée | Prompt « renvoie du JSON » | messages.parse() + schéma (structured outputs natifs) | Natif — l'API contraint le schéma, plus de parsing fragile |
| Profondeur de raisonnement | budget_tokens fixe | thinking: adaptive + effort | Adaptive — budget_tokens renvoie HTTP 400 sur 4.7/4.8 |
| Hébergement | OpenAI direct | Mistral FR / Bedrock Paris + Qdrant self-host | Souverain — secret pro, sinon comité d'éthique bloque |
| Vérif citations | Confiance dans le modèle | Vérif programmatique + gate de confiance | Programmatique — l'hallucination est un risque RC pro |
| Coût batch | Live API | Batches API (-50 %) + prompt caching | Batches + cache sur tout ce qui n'est pas temps réel |
Modes de défaillance (et la parade)
- Hallucination de jurisprudence → RAG strict +
assertCitationsExistqui 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
usagepar dossier + alerte si le coût/dossier dépasse X. Un cache invalidé (timestamp dans le system prompt) peut multiplier ta facture — auditcache_read_input_tokens= 0 sur des requêtes au préfixe identique. - Rate limit en pic de DD →
Semaphorecôté client,max_retriesSDK, et bascule Batches pour le volume non interactif. - Refus de sécurité du modèle (
stop_reason: "refusal") → testerstop_reasonAVANT de lirecontent, sinon crash surcontent[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 (low→max), 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.