KlarkLabs
Tous les projets
Case Study
20 août 2025

AI Document Agent

Agent IA autonome pour le traitement de documents juridiques

Client: Cabinet juridique français

100K+

Documents traités

97%

Précision

80%

Temps gagné

Stack technologique

PythonLangChainOpenAIPineconeFastAPI
PythonLangChainOpenAIIA

Contexte

Un cabinet d'avocats parisien spécialisé en droit des affaires et fusions-acquisitions traitait chaque semaine des centaines de documents contractuels : due diligences, NDA, contrats de cession, pactes d'actionnaires. Les juristes passaient 60 à 70% de leur temps à des tâches de lecture et d'extraction d'informations standardisées — travail à forte valeur ajoutée apparente mais en réalité répétitif pour des professionnels qualifiés.

La demande initiale était un simple outil d'extraction de clauses. La solution livrée est allée bien plus loin : un agent IA autonome capable de comprendre le contexte d'un document, d'extraire les informations structurées, de détecter les clauses inhabituelles ou risquées, et de produire des mémos de synthèse.

Défis techniques

Traitement de documents complexes

Les documents juridiques présentent des défis spécifiques : longues pages denses, références croisées entre articles, tableaux, annexes et signatures. Le pipeline d'ingestion devait préserver la structure logique du document.

# document_processor/ingestion.py
from langchain.document_loaders import UnstructuredPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from pinecone import Pinecone
import hashlib
 
class LegalDocumentProcessor:
    def __init__(self):
        self.embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
        self.pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
        self.index = self.pc.Index("legal-docs")
 
    def process_document(self, file_path: str, metadata: dict) -> str:
        loader = UnstructuredPDFLoader(
            file_path,
            mode="elements",
            strategy="hi_res",
            infer_table_structure=True,
        )
        elements = loader.load()
 
        # Reconstruction des sections logiques
        sections = self._reconstruct_sections(elements)
 
        splitter = RecursiveCharacterTextSplitter(
            chunk_size=1500,
            chunk_overlap=300,
            separators=["\n\nArticle ", "\n\n", "\n", ". "]
        )
 
        doc_id = hashlib.sha256(file_path.encode()).hexdigest()[:16]
        vectors = []
 
        for i, chunk in enumerate(splitter.split_documents(sections)):
            embedding = self.embeddings.embed_query(chunk.page_content)
            vectors.append({
                "id": f"{doc_id}-{i}",
                "values": embedding,
                "metadata": {
                    **metadata,
                    "doc_id": doc_id,
                    "chunk_index": i,
                    "text": chunk.page_content,
                }
            })
 
        self.index.upsert(vectors=vectors, namespace=metadata["client_id"])
        return doc_id
 
    def _reconstruct_sections(self, elements):
        # Regroupe les éléments par section logique
        # en utilisant les titres comme marqueurs
        sections = []
        current_section = []
        for el in elements:
            if el.metadata.get("category") == "Title":
                if current_section:
                    sections.append("\n".join(current_section))
                current_section = [el.page_content]
            else:
                current_section.append(el.page_content)
        if current_section:
            sections.append("\n".join(current_section))
        return sections

Agent d'analyse multi-étapes

L'agent utilise une architecture ReAct (Reasoning + Acting) avec des outils spécialisés pour chaque type d'analyse.

# agent/legal_agent.py
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI
from langchain.tools import tool
 
@tool
def extract_parties(doc_id: str) -> dict:
    """Extrait les parties (nom, rôle, représentant légal) d'un contrat."""
    chunks = retrieve_relevant_chunks(doc_id, "parties contractantes signataires représentants")
    response = llm.invoke(EXTRACTION_PROMPT.format(
        task="parties",
        schema='{"parties": [{"name": str, "role": str, "legal_rep": str}]}',
        context="\n".join(chunks)
    ))
    return json.loads(response.content)
 
@tool
def detect_unusual_clauses(doc_id: str, contract_type: str) -> list[dict]:
    """Détecte les clauses inhabituelles ou potentiellement risquées."""
    reference_clauses = get_standard_clauses(contract_type)
    doc_chunks = retrieve_all_chunks(doc_id)
 
    unusual = []
    for chunk in doc_chunks:
        similarity = compute_clause_similarity(chunk, reference_clauses)
        if similarity < 0.65:
            unusual.append({
                "text": chunk,
                "risk_level": classify_risk(chunk),
                "explanation": generate_risk_explanation(chunk)
            })
    return unusual
 
@tool
def generate_summary_memo(doc_id: str, language: str = "fr") -> str:
    """Génère un mémo de synthèse juridique structuré."""
    parties = extract_parties(doc_id)
    key_terms = extract_key_terms(doc_id)
    unusual_clauses = detect_unusual_clauses(doc_id, "generic")
 
    return llm.invoke(MEMO_PROMPT.format(
        parties=parties,
        key_terms=key_terms,
        unusual_clauses=unusual_clauses,
        language=language
    )).content
 
legal_agent = AgentExecutor(
    agent=create_openai_tools_agent(
        llm=ChatOpenAI(model="gpt-4o", temperature=0),
        tools=[extract_parties, detect_unusual_clauses, generate_summary_memo],
        prompt=LEGAL_AGENT_PROMPT,
    ),
    tools=[extract_parties, detect_unusual_clauses, generate_summary_memo],
    verbose=True,
    max_iterations=10,
)

API FastAPI avec file d'attente

# api/main.py
from fastapi import FastAPI, BackgroundTasks, UploadFile
from redis import Redis
import rq
 
app = FastAPI()
redis_conn = Redis(host="redis", port=6379)
queue = rq.Queue("document_processing", connection=redis_conn)
 
@app.post("/documents/analyze")
async def analyze_document(
    file: UploadFile,
    client_id: str,
    contract_type: str,
    background_tasks: BackgroundTasks,
):
    file_path = await save_temp_file(file)
 
    job = queue.enqueue(
        process_and_analyze,
        file_path,
        client_id,
        contract_type,
        job_timeout=300,
    )
 
    return {"job_id": job.id, "status": "queued"}
 
@app.get("/documents/{job_id}/status")
async def get_analysis_status(job_id: str):
    job = rq.job.Job.fetch(job_id, connection=redis_conn)
    return {
        "status": job.get_status(),
        "result": job.result if job.is_finished else None,
    }

Solution déployée

L'agent tourne sur un cluster AWS ECS avec auto-scaling basé sur la longueur de la file RQ. Pinecone héberge les index vectoriels avec un namespace par client pour l'isolation des données. Les documents traités sont chiffrés au repos (AES-256) et en transit (TLS 1.3).

Un tableau de bord Next.js permet aux juristes de soumettre des documents, suivre l'avancement, valider les extractions et annoter les cas où l'agent s'est trompé — ces annotations alimentent un fine-tuning mensuel du modèle d'extraction.

Résultats

Depuis le déploiement en août 2025, l'agent a traité plus de 100 000 documents juridiques. Le taux de précision sur l'extraction de clauses standardisées atteint 97%, mesuré par validation humaine sur un échantillon représentatif. Les juristes ont réduit leur temps sur les tâches d'extraction et de revue préliminaire de 80%, se concentrant désormais sur l'analyse stratégique et le conseil client.

Klark Labs
Technology Studio