À mesure que mes projets grossissent, je sature la fenêtre de contexte : les modèles oublient des règles, inventent des patterns, cassent l’architecture. Réflexe d’ingénieur, je cherche à reprendre le contrôle. Ce chapitre est une archive d’exploration : certaines solutions sont déjà dépassées, mais le problème reste le même, donner à un agent le bon contexte, au bon moment, sans noyer la fenêtre de contexte.
C’est là que naît l’idée L0/L1/L2. Au départ, une convention de nommage. Rapidement, l’ambition monte : un noyau de règles, plus des modules de contexte chargés à la demande.
Phase 1 : le kernel (L0 / L1 / L2)
Je pars sur une architecture en couches : un noyau toujours chargé, puis des contextes de plus en plus précis quand la tâche demande d’aller plus loin. Dans une codebase, on navigue déjà comme ça : vue d’ensemble, module, fichier, détail. J’ai appliqué la même logique au contexte des LLM.
Je ne veux pas tout mettre dans le prompt de départ. Je veux un système qui part léger, puis descend dans les couches quand la tâche le demande.
- L0 : le bootstrap. Un fichier maître (
L0.md) chargé en permanence, avec les règles globales, les seuils de complexité et une table de routage. Exemple : si la tâche parle d’auth, le système associe ce signal au contexte L1 d’authentification. - L1 : les contraintes de domaine. Chaque fichier décrit ce qu’il ne faut jamais faire, les patterns à suivre, les playbooks utiles et les contextes voisins à charger si la tâche déborde.
- L2 : le détail d’implémentation. Specs, ADR, notes longues, guides détaillés : le fond du tiroir. Je ne le charge pas par défaut, seulement quand il faut comprendre le pourquoi ou toucher une partie précise.
Ça marche, mais c’est trop lourd. Chaque interaction commence par charger plusieurs milliers de tokens avant même de traiter la demande. À l’époque, avec des fenêtres de contexte beaucoup plus étroites qu’aujourd’hui, ce n’est pas un détail : je gaspille une partie du budget juste pour initialiser le système.
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)
Le langage naturel restait flou à mon goût. J’ai basculé à l’autre extrême : traiter le contexte comme du code. C’est devenu le DCL (Dynamic Context Language) : une grammaire YAML avec quatre opérateurs (@context, !constraints, ~procedures, &triggers), des domaines typés, des schémas, des fixtures, des tests.
Autour, un pipeline complet : parser, validateur, compilateur (Source → AST → prompt), moteur de triggers. Un même fichier source pouvait produire un prompt Claude, un prompt GPT, ou une version minimale.
@context[domain:auth]
!constraints:
never: [commit_secrets, raw_sql]
~procedures:
add_endpoint: [validate, authorize, audit]
Au bout d’un moment, l’évidence : je réinventais la roue. Les LLM savent déjà lire du langage naturel. Leur imposer un pseudo-code maison ajoutait de la friction, et la structure finissait par coûter plus cher que le flou qu’elle voulait supprimer. De la sur-ingénierie.
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 : les fichiers déclarent leur contexte
Je supprime alors le kernel central. Plus de fichier maître censé savoir à l’avance quel contexte charger. Ici, ce sont les fichiers qui annoncent les contextes dont ils dépendent, avec des tags presque comme des imports.
// @L1: auth, api
J’appelle ça HeadsAndTails : au lieu de lire tout le fichier, le scanner regarde seulement le début et la fin, là où les tags peuvent vivre. L’idée est simple : découvrir le contexte avant de charger le détail.
Le gain n’est pas seulement une question de tokens. Je retire surtout le goulot central. Le contexte n’est plus décidé par un routeur global, il émerge de la proximité entre la tâche, les fichiers touchés et les tags déclarés.
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 : le détour par MCP
Après les tags, je tente autre chose : arrêter de tout mettre dans le prompt. Je ne veux plus décider à l’avance de tout ce que l’agent doit savoir. Je veux lui donner un point d’entrée stable, puis des outils pour aller chercher le reste.
Le point d’entrée, c’est AI.md : quelques règles, quelques conventions, et surtout l’idée que le contexte peut être demandé au moment utile. Les détails, eux, passent dans des fichiers context.ai.
Le MCP devient la porte d’accès. Si la tâche parle d’auth, de média ou de base de données, l’agent peut chercher le contexte correspondant, lire le fichier, puis travailler avec ces règles chargées à la demande.
Sur le papier, c’est plus souple que le kernel, moins rigide que le DCL, moins intrusif que les tags dans tous les fichiers. Mais le piège revient sous une autre forme : je gagne du chargement à la demande, et je paie en surface outillée. Outils à définir, serveur à maintenir, définitions d’outils (mode d'emploi des outils, ajouté au contexte avant même que la demande soit traitée) à injecter dans le contexte initial. Quand je commence à mesurer ce qui part juste dans ces définitions, je comprends que j’ai surtout déplacé le problème.
Sur le papier, c’est plus souple que le kernel, moins rigide que le DCL, moins intrusif que les tags dans tous les fichiers. Mais chaque outil MCP arrive avec sa description, ses paramètres et son schéma d’appel. Ces définitions d’outils (mode d'emploi des outils, ajouté au contexte avant même que la demande soit traitée) entrent dans la fenêtre de contexte dès l’ouverture de session. Même si l’agent n’appelle jamais l’outil, le budget est déjà consommé. Quand je commence à mesurer ce coût, je comprends que le problème n’est pas seulement l’appel d’outil, mais le menu d’outils chargé au départ.
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) : “Fichiercontext.aitrouvé.” IA (Action MCP) :context_loader.load("src/modules/payment/context.ai")IA (Pensée) : “Contexte chargé. Règles critiques : utiliserPaymentGatewayPort, wrapper dans une transaction DB. Ok, je peux commencer le plan.”
Phase 5 : le retour à l’essentiel (la constitution)
À ce moment-là, AI.md sert encore surtout d’index vers du contexte externe. La bascule, c’est de lui retirer cette ambition : il devient une constitution. Un socle court, stable, versionné, que les agents lisent avant d’agir.
Dans la pratique, ce n’est pas toujours un seul fichier. Il y a AI.md comme source commune, des alias quand un outil attend un autre nom (CLAUDE.md, AGENTS.md, parfois CURSOR.md ou GEMINI.md), et des règles plus locales quand le projet en a besoin.
J’expérimente déjà avec .cursor/rules/, et avec des CLAUDE.md disséminés dans l’arborescence. L’idée est bonne : rapprocher les règles de la zone de code qu’elles concernent. Mais côté Claude Code, ce chargement local reste fragile dans mes essais. Je mets des CLAUDE.md imbriqués un peu partout, en espérant que l’agent les récupère quand il descend dans l’arborescence. En pratique, ils apparaissent rarement au bon moment. Une partie de mon acharnement vient de là.
Mise à jour (10 décembre 2025)
Quelques semaines après la publication initiale de ce chapitre, Claude Code 2.0.64 ajoute le support de
.claude/rules/. J’y retrouve cette intuition dans un support plus officiel : règles modulaires, scoping par chemin.Mes bricolages allaient dans cette direction ; je peux enfin supprimer une partie de la mécanique maison.
Ce que contient AI.md
AI.md ne sert pas à tout expliquer. Il sert à fixer ce qui ne doit pas dériver.
J’y mets d’abord les garde-fous : les NEVER, les MUST, les limites que l’agent ne doit pas franchir même s’il pense gagner du temps.
- NEVER throw in domain/application code
- NEVER violate layer boundaries
- MUST validate touched areas before committing
J’y mets aussi les règles d’architecture. C’est une loi locale du projet, pas une préférence de style : quelles couches peuvent se parler, quelles dépendances sont interdites, quelles commandes valident le résultat.
Et surtout, j’ajoute le pourquoi. Une règle sèche se contourne facilement. Une règle accompagnée de sa raison tient mieux dans le contexte : si l’agent comprend qu’on découple le domaine de la technologie pour garder le cœur testable et portable, il respecte mieux la contrainte.
Une source commune, plusieurs points d’entrée
Le but n’est pas d’avoir un fichier sacré, mais d’éviter les copies divergentes. Quand un outil attend CLAUDE.md, AGENTS.md ou un autre nom, je préfère le faire pointer vers la même source plutôt que réécrire les mêmes règles à plusieurs endroits.
# 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.mdExtrait 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
Cette exploration m’a appris une chose simple : le contexte n’est pas gratuit, même quand on le charge intelligemment.
Le kernel coûte au démarrage. Le DCL coûte en langage à maintenir. Les tags coûtent en discipline de fichiers. Le MCP coûte en surface outillée. Même la constitution coûte en attention : il faut l’écrire, la garder juste, éviter qu’elle devienne un fourre-tout.
J’arrête de chercher le bon routeur. Le sujet devient plus simple : garder le minimum de contexte stable qui permet à l’agent d’agir sans tout casser.
Le reste doit être vérifié après coup. Pas par confiance, mais par système : types, lint, tests, règles d’architecture, validateurs.
Dans le journal, j’ai un chapitre dédié aux garde-fous. Mais avant ça, je passe par l’orchestration : organiser le travail des agents entre eux.
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 : le compilateur DCL et les tags HeadsAndTails sont lisibles si tu veux voir comment ça a échoué.