Introducción: la pista falsa del “prompt engineering”
A medida que mis proyectos crecen, saturo la ventana de contexto: los modelos olvidan reglas, inventan patrones, rompen la arquitectura. Reflejo de ingeniero, busco recuperar el control. Este capítulo es un archivo de exploración: algunas soluciones ya quedaron viejas, pero el problema sigue siendo el mismo, darle a un agente el contexto correcto, en el momento correcto, sin ahogar la ventana.
Ahí nace la idea L0/L1/L2. Al principio, una convención de nombres. Rápido la ambición sube: un núcleo de reglas, más módulos de contexto cargados a demanda.
Fase 1: el kernel (L0 / L1 / L2)
Arranco con una arquitectura por capas: un núcleo siempre cargado, después contextos cada vez más precisos cuando la tarea pide ir más lejos. En una codebase ya navegamos así: vista general, módulo, archivo, detalle. Apliqué la misma lógica al contexto de los LLM.
No quiero meter todo en el prompt inicial. Quiero un sistema que arranca liviano y baja por las capas cuando la tarea lo pide.
- L0: el bootstrap. Un archivo maestro (
L0.md) cargado de forma permanente, con las reglas globales, los umbrales de complejidad y una tabla de ruteo. Ejemplo: si la tarea mencionaauth, el sistema asocia esa señal al contexto L1 de autenticación. - L1: las restricciones de dominio. Cada archivo describe lo que nunca hay que hacer, los patrones a seguir, los playbooks útiles y los contextos vecinos a cargar si la tarea se desborda.
- L2: el detalle de implementación. Specs, ADR, notas largas, guías detalladas: el fondo del cajón. No lo cargo por defecto, solo cuando hay que entender el porqué o tocar una parte específica.
Funciona, pero pesa demasiado. Cada interacción empieza cargando varios miles de tokens antes incluso de tratar el pedido. En aquella época, con ventanas de contexto mucho más estrechas que hoy, no es un detalle: gasto una parte del presupuesto solo para inicializar el sistema.
Ejemplo del 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. Fase 2: la trampa del compilador (Dynamic Context Language)
El lenguaje natural me parecía borroso. Salté al otro extremo: tratar el contexto como código. Así nació el DCL (Dynamic Context Language): una gramática YAML con cuatro operadores (@context, !constraints, ~procedures, &triggers), dominios tipados, esquemas, fixtures, tests.
Alrededor, una pipeline completa: parser, validador, compilador (Source → AST → prompt), motor de triggers. Un mismo archivo fuente podía producir un prompt Claude, un prompt GPT o una versión mínima.
@context[domain:auth]
!constraints:
never: [commit_secrets, raw_sql]
~procedures:
add_endpoint: [validate, authorize, audit]
Después de un tiempo, lo evidente: estaba reinventando la rueda. Los LLM ya saben leer lenguaje natural. Imponerles un pseudocódigo casero agregaba fricción, y la estructura terminaba costando más que el borroso que pretendía eliminar. Sobreingeniería.
Ejemplo de DCL:
# ==========================================
# DCL (Dynamic Context Language) - Example
# ==========================================
# Domain: Authentication Module
# ==========================================
# Define el contexto global del dominio
@context[domain:auth]:
# Restricciones estrictas (la IA NUNCA debe hacer esto)
!constraints:
- never: [commit_secrets, raw_sql_queries]
- enforce: [use_orm, secure_password_hashing]
# Dependencias necesarias para este contexto
^dependencies:
- lib: [bcrypt, jsonwebtoken]
- service: [user_service, email_service]
# Procedimientos y patrones a seguir
~procedures:
# Patrón para el registro de usuario
user_signup:
- step: validate_input
desc: "Verificar complejidad de contraseña y formato de email."
- step: hash_password
desc: "Usar bcrypt para hash. Nunca guardar en texto plano."
- step: create_user
desc: "Llamar a user_service para creación en base de datos."
- step: generate_token
desc: "Generar un JWT para la sesión."
- step: send_welcome_email
desc: "Llamar a email_service."
# Patrón para inicio de sesión
user_login:
- step: find_user
desc: "Buscar usuario por email."
- step: verify_password
desc: "Comparar hash con bcrypt."
- step: generate_token
desc: "Generar un nuevo JWT."
# Modelos de datos esperados
#schema:
User:
- id: [uuid, pk]
- email: [string, unique]
- password_hash: [string]
- created_at: [datetime] Fase 3: los archivos declaran su contexto
Entonces elimino el kernel central. Ya no hay archivo maestro que se supone sabe de antemano qué contexto cargar. Acá son los archivos los que anuncian los contextos de los que dependen, con tags casi como imports.
// @L1: auth, api
A esto lo llamo HeadsAndTails: en lugar de leer todo el archivo, el escáner mira solo el principio y el final, donde los tags pueden vivir. La idea es simple: descubrir el contexto antes de cargar el detalle.
La ganancia no es solo cuestión de tokens. Sobre todo elimino el cuello de botella central. El contexto ya no lo decide un router global, emerge de la proximidad entre la tarea, los archivos tocados y los tags declarados.
Ejemplo de archivo con tags de contexto:
// ==========================================
// 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";
// ... otras importaciones
export class CreateUserUseCase {
constructor(
private readonly userRepo: IUserRepository,
// ...
) {}
async execute(command: CreateUserCommand): Promise<Result<User>> {
// Lógica de negocio...
// La IA sabe que debe respetar las reglas "auth" y "database"
// porque fueron declaradas en el encabezado.
}
}
// ==========================================
// @security: low-risk
// ========================================== Fase 4: el rodeo del MCP
Después de los tags, intento otra cosa: dejar de meter todo en el prompt. Ya no quiero decidir de antemano todo lo que el agente tiene que saber. Quiero darle un punto de entrada estable, y herramientas para ir a buscar el resto.
El punto de entrada es AI.md: unas pocas reglas, unas pocas convenciones, y sobre todo la idea de que el contexto se puede pedir en el momento útil. Los detalles van en archivos context.ai.
El MCP se vuelve la puerta de acceso. Si la tarea menciona auth, media o base de datos, el agente puede buscar el contexto correspondiente, leer el archivo y trabajar con esas reglas cargadas a demanda.
Sobre el papel, es más flexible que el kernel, menos rígido que el DCL, menos invasivo que los tags en todos los archivos. Pero la trampa vuelve con otra forma: gano carga a demanda y pago en superficie de herramientas. Herramientas por definir, servidor por mantener, definiciones de herramientas (instrucciones y esquema de las herramientas, agregados al contexto antes incluso de procesar el pedido) inyectadas en el contexto inicial. Cuando empiezo a medir lo que se va solo en esas definiciones, entiendo que sobre todo desplacé el problema.
Sobre el papel, es más flexible que el kernel, menos rígido que el DCL, menos invasivo que los tags en todos los archivos. Pero cada herramienta MCP llega con su descripción, sus parámetros y su esquema de llamada. Esas definiciones de herramientas (instrucciones y esquema de las herramientas, agregados al contexto antes incluso de procesar el pedido) entran en la ventana de contexto al abrir la sesión. Aunque el agente nunca llame a la herramienta, el presupuesto ya está consumido. Cuando empiezo a medir ese costo, entiendo que el problema no es solo la llamada a una herramienta, sino el menú de herramientas cargado al inicio.
Ejemplo de archivo de contexto (context.ai):
# ==========================================
# LOCATION: src/modules/payment/context.ai
# ==========================================
# Metadatos para el agente
meta:
domain: payment
description: "Reglas críticas para el procesamiento de pagos."
# Reglas de carga condicional
# El agente evalúa estas reglas antes de decidir cargar este contexto.
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."
# El contexto en sí, cargado solo si se cumple una regla.
context:
# Restricciones críticas
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."
# Patrones de implementación
patterns:
- name: "Idempotency"
description: "Ensure payment requests are idempotent using a unique key."Simulación: el agente con MCP custom en acción
Usuario: “Necesito reembolsar un pago. Es sensible.”
IA (pensamiento): “Tarea crítica en el dominio
payment. No codeo nada sin verificar las reglas.” IA (acción MCP):fs_scanner.scan("src/modules/payment", look_for=["context.ai"])IA (resultado): “Archivocontext.aiencontrado.” IA (acción MCP):context_loader.load("src/modules/payment/context.ai")IA (pensamiento): “Contexto cargado. Reglas críticas: usarPaymentGatewayPort, envolver en transacción DB. Ok, puedo arrancar el plan.”
Fase 5: la vuelta a lo esencial (la constitución)
En ese momento, AI.md sirve sobre todo de índice hacia contexto externo. El cambio es quitarle esa ambición: pasa a ser una constitución. Una base corta, estable, versionada, que los agentes leen antes de actuar.
En la práctica, no siempre es un único archivo. Está AI.md como fuente común, alias cuando una herramienta espera otro nombre (CLAUDE.md, AGENTS.md, a veces CURSOR.md o GEMINI.md), y reglas más locales cuando el proyecto las necesita.
Ya estoy experimentando con .cursor/rules/ y con CLAUDE.md repartidos por el árbol del proyecto. La idea es buena: acercar las reglas a la zona de código que rigen. Pero del lado de Claude Code, esa carga local sigue siendo frágil en mis pruebas. Pongo CLAUDE.md anidados por todos lados, esperando que el agente los recupere cuando baja por el árbol. En la práctica, aparecen rara vez en el momento correcto. Una parte de mi insistencia viene de ahí.
Actualización (10 de diciembre de 2025)
Algunas semanas después de la publicación inicial de este capítulo, Claude Code 2.0.64 agrega el soporte de
.claude/rules/. Reencuentro esa intuición en un soporte más oficial: reglas modulares, scoping por ruta.Mis bricolajes iban en esa dirección; finalmente puedo eliminar parte de la mecánica casera.
Lo que contiene AI.md
AI.md no sirve para explicar todo. Sirve para fijar lo que no debe derivar.
Pongo primero las barandas: los NEVER, los MUST, los límites que el agente no debe cruzar aunque crea que gana tiempo.
- NEVER throw in domain/application code
- NEVER violate layer boundaries
- MUST validate touched areas before committing
Pongo también las reglas de arquitectura. Es una ley local del proyecto, no una preferencia de estilo: qué capas pueden hablarse, qué dependencias están prohibidas, qué comandos validan el resultado.
Y sobre todo, agrego el porqué. Una regla seca se esquiva fácil. Una regla acompañada de su razón aguanta mejor en el contexto: si el agente entiende que desacoplamos el dominio de la tecnología para mantener el núcleo testeable y portable, respeta mejor la restricción.
Una fuente común, varios puntos de entrada
El objetivo no es tener un archivo sagrado, sino evitar copias divergentes. Cuando una herramienta espera CLAUDE.md, AGENTS.md u otro nombre, prefiero hacerlo apuntar a la misma fuente en lugar de reescribir las mismas reglas en varios lugares.
# Vuelta a las bases: simplicidad
$ ls -l .ai/
-rw-r--r-- 1 user staff 4096 Nov 21 AI.md # La constitución (fuente única)
# Los alias para la compatibilidad de herramientas. Todos apuntan al mismo archivo.
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.mdExtracto de la constitución (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_...
... Conclusión: la lección de la obsolescencia
Esta exploración me enseñó una cosa simple: el contexto no es gratis, ni siquiera cuando se carga con inteligencia.
El kernel cuesta al arranque. El DCL cuesta en lenguaje por mantener. Los tags cuestan en disciplina de archivos. El MCP cuesta en superficie de herramientas. Hasta la constitución cuesta en atención: hay que escribirla, mantenerla justa, evitar que se vuelva un cajón de sastre.
Dejo de buscar el router perfecto. El tema se vuelve más simple: mantener el mínimo de contexto estable que le permite al agente actuar sin romper todo.
El resto se verifica después. No por confianza, sino por sistema: tipos, lint, tests, reglas de arquitectura, validadores.
En el journal tengo un capítulo dedicado a las barandas. Pero antes paso por la orquestación: organizar el trabajo de los agentes entre ellos.
Limpié y publiqué el código de esta exploración en código abierto: ai-context-layers. Es un archivo educativo de las Fases 1 a 4: el compilador DCL y los tags HeadsAndTails se pueden leer si querés ver cómo fracasó.