L'odyssée du contexte | IdeAs — Augustin B
FR EN ES
Chapitre 4

L'odyssée du contexte

De la sur-ingénierie à la simplification radicale.

Introduction : la fausse piste du “prompt engineering”

Au début de 2025, à mesure que mes projets grossissaient, je me suis pris le mur du contexte. Les modèles oubliaient des règles, inventaient des patterns et cassaient mon architecture. Ma première réaction d’ingénieur a été immédiate : construire un système de contrôle rigide.

C’est ici qu’a émergé mon idée d’architecture de contexte : “L0/L1/L2”. Ce n’était pas juste une convention de nommage, c’était une tentative de créer un véritable système d’exploitation pour IA.


Phase 1 : le kernel (L0 / L1 / L2)

L’époque du contrôle centralisé.

J’ai conçu une architecture hiérarchique stricte, inspirée des niveaux de privilèges système.

Le problème : l’approche fonctionnait, mais à un coût prohibitif. Le “boot” du système coûtait environ 3 500 tokens à chaque interaction, juste pour dire “bonjour”. En plus, la maintenance du L0 central devenait un goulot d’étranglement : pour ajouter une fonctionnalité, il fallait toucher au kernel.

Requête arrive
Phase 1 : Architecture Kernel L0/L1/L2

Exemple du kernel (L0.md) :

# L0 KERNEL (ROOT CONTEXT)

## MISSION

Act as a senior software architect and lead developer. Your goal is to produce maintainable, scalable, and secure code following the project's established patterns. You must orchestrate the loading of specialized contexts (L1) based on the task at hand.

## CRITICAL RULES (THE "NEVER" LIST)

1.  **NEVER** commit secrets or credentials.
2.  **NEVER** bypass the established hexagonal architecture layers.
3.  **NEVER** introduce new dependencies without explicit approval (ADR required).
4.  **NEVER** write "spaghetti code" (high cyclomatic complexity).
5.  **NEVER** ignore linter or type-checker errors.

## L1 CONTEXT MAP (ROUTING TABLE)

| Priority | Pattern (Regex) | L1 Context File | Description | Load Strategy |
| :------- | :-------------- | :-------------- | :---------- | :------------ | --------------- | ------------------------------ | ----------------------------------- | ----------------------------------- | ---- |
| 1        | `/\b(auth       | login           | signup      | session       | jwt             | oauth)\b/i`                    | `L1/Auth.md`                        | Authentication & Security protocols | AUTO |
| 2        | `/\b(db         | database        | prisma      | drizzle       | sql             | migration)\b/i`                | `L1/Database.md`                    | Data persistence & schema rules     | AUTO |
| 3        | `/\b(api        | endpoint        | route       | controller    | trpc            | graphql)\b/i`                  | `L1/API.md`                         | API design & interface standards    | AUTO |
| 4        | `/\b(ui         | component       | tailwind    | css           | front)\b/i`     | `L1/UI.md`                     | User Interface components & styling | AUTO                                |
| 5        | `/\b(test       | spec            | e2e         | mock)\b/i`    | `L1/Testing.md` | Testing strategies & standards | AUTO                                |

## OPERATING PROCEDURE

1.  **Analyze Task:** Read the user's prompt.
2.  **Scan for Patterns:** Match prompt content against the `L1 CONTEXT MAP` regex patterns.
3.  **Load L1 Contexts:** For each match, load the corresponding `L1` file.
    - _NOTE:_ L1 contexts provide domain-specific constraints and patterns.
4.  **Execute:** Perform the task, adhering strictly to L0 rules AND the loaded L1 rules.
5.  **Consult L2 (Optional):** If deep clarification is needed on a specific decision, consult the referenced L2 specs (ADRs) mentioned in the L1 files.

Phase 2 : le piège du compilateur (Dynamic Context Language)

Quand l’ingénieur veut trop ingénierer.

Frustré par le flou du langage naturel, je suis parti dans l’extrême inverse : traiter le contexte comme du code. J’ai créé le DCL (Dynamic Context Language).

Au lieu d’écrire des instructions, j’écrivais des spécifications en YAML avec des opérateurs personnalisés :

@context[domain:auth]
  !constraints:
    never: [commit_secrets, raw_sql]
  ~procedures:
    add_endpoint: [validate, authorize, audit]

J’ai même bâti un pipeline de compilation (Source → AST → Prompt optimisé). L’échec productif : j’ai fini par comprendre que je réinventais la roue. Les LLM sont déjà des compilateurs de langage naturel. Les forcer à lire du pseudo-code ajoutait de la friction sans gain réel. C’était de la sur-ingénierie pure.

Phase 2

Exemple de DCL :

# ==========================================
# DCL (Dynamic Context Language) - Example
# ==========================================
# Domain: Authentication Module
# ==========================================

# Définit le contexte global du domaine
@context[domain:auth]:
  # Contraintes strictes (l'IA ne doit JAMAIS faire cela)
  !constraints:
    - never: [commit_secrets, raw_sql_queries]
    - enforce: [use_orm, secure_password_hashing]

  # Dépendances nécessaires pour ce contexte
  ^dependencies:
    - lib: [bcrypt, jsonwebtoken]
    - service: [user_service, email_service]

  # Procédures et patterns à suivre
  ~procedures:
    # Pattern pour l'inscription d'un utilisateur
    user_signup:
      - step: validate_input
        desc: "Vérifier la complexité du mot de passe et le format de l'email."
      - step: hash_password
        desc: "Utiliser bcrypt pour le hachage. Ne jamais stocker en clair."
      - step: create_user
        desc: "Appeler le user_service pour la création en base."
      - step: generate_token
        desc: "Générer un JWT pour la session."
      - step: send_welcome_email
        desc: "Appeler l'email_service."

    # Pattern pour la connexion
    user_login:
      - step: find_user
        desc: "Rechercher l'utilisateur par email."
      - step: verify_password
        desc: "Comparer le hachage avec bcrypt."
      - step: generate_token
        desc: "Générer un nouveau JWT."

  # Modèles de données attendus
  #schema:
    User:
      - id: [uuid, pk]
      - email: [string, unique]
      - password_hash: [string]
      - created_at: [datetime]

Phase 3 : l’émergence distribuée

Le contexte ne se charge pas, il émerge.

J’ai alors supprimé le kernel central pour passer à une approche distribuée. Au lieu d’un fichier maître qui sait tout, chaque fichier déclarait ses besoins via des tags, comme des imports : // @L1: auth, api.

J’ai développé un scanner léger, “HeadsAndTails”, qui ne lisait que le début et la fin des fichiers pour découvrir ces tags.

Phase 3

Exemple de fichier avec tags de contexte :

// ==========================================
// FILE: src/modules/user/core/application/use-cases/CreateUser.ts
//
// @L1: auth, database, domain-events
// @architect: hexagonal/use-case
// ==========================================

import { User } from "../../domain/User";
import { IUserRepository } from "../../ports/IUserRepository";
// ... autres imports

export class CreateUserUseCase {
  constructor(
    private readonly userRepo: IUserRepository,
    // ...
  ) {}

  async execute(command: CreateUserCommand): Promise<Result<User>> {
    // Logique métier...
    // L'IA sait qu'elle doit respecter les règles "auth" et "database"
    // car elles ont été déclarées dans le header.
  }
}

// ==========================================
// @security: low-risk
// ==========================================

Phase 4 : lazy loading outillé (AI.md + MCP custom)

De la règle statique à la récupération ciblée.

Cette phase ne s’ajoute pas aux précédentes : je mets de côté les mécanismes d’avant pour tester une nouvelle approche de context engineering.

Je garde une base stable toujours chargée : AI.md. C’est la constitution commune à tous mes agents. Dans ce fichier, je définis un MCP custom (mes tools et leurs règles d’usage).

Quand j’ouvre un nouveau chat / agent, ce coût est payé une fois : la définition de ce MCP est injectée dans le contexte initial.

Ensuite, le chargement devient à la demande :

  1. L’agent reçoit une tâche (ex: authentification).
  2. Il appelle mon MCP custom pour récupérer le contexte pertinent.
  3. Le MCP renvoie les fichiers context.ai liés au domaine et aux fichiers ciblés.
  4. L’agent charge uniquement ce contexte, puis exécute.

Le point clé : dans mon cas, le MCP sert à retrouver et charger le contexte pertinent à la tâche. C’est ce mécanisme qui permet un chargement à la demande.

Avec le recul, c’était déjà une forme de RAG maison.

Phase 4

Exemple de fichier de contexte (context.ai) :

# ==========================================
# LOCATION: src/modules/payment/context.ai
# ==========================================

# Métadonnées pour l'agent
meta:
  domain: payment
  description: "Règles critiques pour le traitement des paiements."

# Règles de chargement conditionnel
# L'agent évalue ces règles avant de décider de charger ce contexte.
load_if:
  - rule: "Task involves money, transactions, or stripe."
    reason: "High risk domain."
  - rule: "User explicitly mentions 'payment' or 'checkout'."
    reason: "Explicit request."
  - rule: "Files being edited are in 'src/modules/payment/'."
    reason: "Proximity."

# Le contexte lui-même, chargé uniquement si une règle est remplie.
context:
  # Contraintes critiques
  constraints:
    - "NEVER log full credit card numbers or CVV."
    - "MUST use the 'PaymentGatewayPort' for all external calls."
    - "All transactions MUST be wrapped in a database transaction."

  # Patterns d'implémentation
  patterns:
    - name: "Idempotency"
      description: "Ensure payment requests are idempotent using a unique key."

Simulation : l’agent avec MCP custom en action

Utilisateur : “J’ai besoin de rembourser un paiement. C’est sensible.”

IA (Pensée) : “Tâche critique dans le domaine payment. Je ne code rien sans vérifier les règles.” IA (Action MCP) : fs_scanner.scan("src/modules/payment", look_for=["context.ai"]) IA (Résultat) : “Fichier context.ai trouvé.” IA (Action MCP) : context_loader.load("src/modules/payment/context.ai") IA (Pensée) : “Contexte chargé. Règles critiques : utiliser PaymentGatewayPort, wrapper dans une transaction DB. Ok, je peux commencer le plan.”


Phase 5 : le retour à l’essentiel (la constitution)

Maturité et lâcher-prise.

Aujourd’hui, j’ai remplacé cette architecture par une version plus simple. Pourquoi ? Parce qu’à l’époque de ces tests, les modèles (Claude 4.0 et suivants) étaient devenus plus intelligents : ils cherchaient mieux l’information et comprenaient mieux le contexte. Ils n’avaient plus besoin qu’on leur tienne la main en permanence ; ils avaient surtout besoin d’une constitution.

Mon système actuel tient en un seul fichier : AI.md. Pour assurer la compatibilité avec tous mes outils (Cursor, Windsurf, script CLI), j’utilise des liens symboliques : CLAUDE.md, GEMINI.md, CURSOR.md pointent tous vers ce fichier unique.

Ce que contient la constitution (AI.md) :

C’est un cadre de règles clair, strict et pragmatique :

  1. Les garde-fous (the guardrails) : une liste de NEVER et MUST.
    • NEVER throw in domain
    • NEVER violate layer boundaries
  2. L’architecture comme loi : la structure du projet n’est pas une suggestion. Le fichier définit strictement ce que chaque couche a le droit d’importer et comment elles communiquent.
  3. La prévention de la dérive (drift prevention) : au lieu de balancer des ordres aveugles, j’explique le pourquoi.
    • “Pourquoi ces règles existent ? Pour découpler le domaine de la technologie.”
    • En donnant du sens, l’IA adhère mieux à la contrainte.

L’architecture de la constitution

Une seule source de vérité pour tous les agents, zéro duplication.

# Retour aux bases : Simplicité
$ ls -l .ai/
-rw-r--r--  1 user  staff  4096 Nov 21 AI.md       # La constitution (source unique)
# Les alias pour la compatibilité des outils. Ils pointent tous vers le même fichier.
lrwxr-xr-x  1 user  staff     5 Nov 21 CLAUDE.md -> AI.md
lrwxr-xr-x  1 user  staff     5 Nov 21 CURSOR.md -> AI.md
lrwxr-xr-x  1 user  staff     5 Nov 21 GEMINI.md -> AI.md

Extrait de la constitution (AI.md) :

# Constitution

## TL;DR (The "NEVER" List)

**NEVER:**

- Mute lint/type errors (fix root causes)
- Violate layer boundaries (core→npm, application→infrastructure)
- Throw in domain/application (return `Result<T, E>`)

## Principles & Rationale (Drift Prevention)

**Why these rules exist:**

1. **Architecture**: We decouple the _core domain_ from _technology_...
   ...

Conclusion : la leçon de l’obsolescence

Bilan

Je ne cherche plus à “piloter” l’IA à la micro-seconde avec des compilateurs ou des routeurs complexes. Je lui donne une boussole (AI.md), une carte (l’arborescence), puis je la laisse naviguer.

Le code est meilleur, mon esprit est plus libre, et le système est enfin stable. Less is more.

Ressenti quelques mois après

En relisant ce parcours quelques mois plus tard, un point saute aux yeux : une partie de ces constructions est déjà obsolète (c’est le prix de l’apprentissage dans un domaine qui évolue vite).

Tout ce travail (kernel L0, compilateur DCL, scanner de tags) a été dépassé par l’amélioration constante des modèles et l’arrivée de nouveaux standards. C’est la règle du jeu dans ce domaine : on construit des ponts temporaires en attendant que la techno change de niveau.

Mais ce n’était pas du temps perdu. C’était l’apprentissage nécessaire pour comprendre ce qui compte vraiment : l’architecture et les contraintes métier.

J’ai nettoyé et publié le code de cette exploration en open source : ai-context-layers. C’est une archive éducative des Phases 1 à 4, pas un produit maintenu, mais les patterns et le compilateur DCL peuvent servir d’inspiration.

La suite ?

Ma logique a donc évolué. Puisque je ne peux plus (et n’ai plus besoin de) tout contrôler a priori dans le prompt, je me concentre sur la validation a posteriori.

Mon nouveau chantier est la création d’un validateur custom. Pas juste un linter, mais un moteur de règles architecturales spécifiques à mes projets (via Regex, analyse d’AST, dependency-cruiser) qui tourne après que l’IA a travaillé. Je ne lui dis plus comment faire, je vérifie qu’elle n’a pas cassé les fondations.

Mais ça, c’est le sujet d’un autre chapitre : les garde-fous.