Reglas para agentes de IA: por qué escribirlas no basta

Imagen generada por IA para el blog de Orbitant sobre reglas para agentes de IA, que muestra un compás de precisión trazando un arco metálico sobre un fondo oscuro.

Tenía la regla escrita, clara y delante del agente en cada sesión y la ignoraba una y otra vez: el modelo cumplía; lo que fallaba era mi idea de lo que una regla escrita podía garantizar.

LITA GARCÍA
Software Engineer

El obstáculo con el que llevo meses peleándome es muy concreto: cómo escribir reglas para agentes de IA que el agente cumpla de verdad y no solo lea. Porque mi primer instinto —el de casi todo el mundo— fue el equivocado: cuando la IA se saltaba una norma, yo escribía más normas.

Y no funcionaba. Lo que cambió las cosas no fue escribir más, sino reescribir la que ya tenía de una forma muy distinta. Este es el caso que me lo enseñó.

El error: una regla clara que el agente ignoraba

En el frontend, trabajamos con TanStack Form. Su API es cómoda, pero tiene una trampa: invita a pasar los validadores y listeners como arrow functions escritas directamente dentro del JSX. El agente entraba en ella una y otra vez, generando cosas así:

<form.Field
  name="clientDeliveryDate"
  validators={{
    onChange: ({ value, fieldApi }) =>
      fieldApi.form.getFieldValue('status') === TaxCampaignStatus.BILLED && !value
        ? billedRequiresDateMessage
        : undefined,
  }}
/>

Ese patrón viola una regla explícita de nuestro docs/coding-standards.md: nada de lógica ejecutable dentro del JSX. La regla existe por buenas razones —testabilidad, legibilidad, que la vista no acumule comportamiento— y la había escrito yo, con su ejemplo ✅/❌ y todo. Aun así, el agente metía validadores y listeners inline en un pull request tras otro.

Lo primero que descarté fue lo evidente: no era un fallo de carga. La regla estaba en el contexto desde el arranque de cada sesión. Tampoco era un modelo «malo». El error, en realidad, era una suposición mía: había dado por hecho que documentar la norma equivalía a que se cumpliera. Y no. Tener una regla en el contexto no es lo mismo que aplicarla de forma determinista, y confundir las dos cosas fue lo que me tuvo semanas tropezando con el mismo diff.

Por qué una regla escrita no basta

Un agente genera código token a token y, en ese momento, su «atención» la dominan tres cosas: la API de la librería que usa, los patrones que vio millones de veces durante el entrenamiento, y el problema concreto que resuelve. TanStack Form empuja hacia el validador inline, y hay una montaña de ejemplos públicos con arrow functions en props. Una frase en prosa —»no metas lógica en el JSX»— compite con todo eso, y pierde.

flowchart TD
    API["API de TanStack Form - empuja al validador inline"] --> OUT["Código que el agente genera"]
    TR["Patrones del entrenamiento - millones de ejemplos inline"] --> OUT
    TA["La tarea concreta que el agente resuelve"] --> OUT
    T["Tu regla en prosa - no metas lógica en el JSX"] -. mucho menos peso .-> OUT
    OUT --> R{"Gana el patrón inline: la regla se ignora"}

Ayuda distinguir dos maneras de que una regla llegue al agente:

VíaQué hace con la reglaFiabilidad
Texto en CLAUDE.md / coding-standardsLa persuade: el modelo la lee y normalmente la sigueAlta, pero probabilística
Tooling (lint, hook, CI)La impone: control externo que falla si se incumpleMáxima, pero no siempre existe

Y ojo, porque tener tooling tampoco es garantía automática si está mal enchufado. Me pasó en el backend: tenía un hook de Claude Code que prohibía cierta acción, y no saltó, porque los hooks solo se cargan desde la carpeta donde abres la sesión y yo la había abierto en otra.

Dos caras de la misma moneda —texto que no se sigue, barrera que no se dispara— con la misma raíz: confié en que la regla existía, sin comprobar qué la hacía cumplirse de verdad. Esa es la pregunta que aprendí a hacerme, y la que me llevó a reescribir la regla de arriba.

Reglas para agentes de IA que sí se cumplen: hazlas quirúrgicas

Lo que no funcionó fue lo obvio y merece la pena decirlo para que nadie pierda el tiempo ahí:

  • Añadir más normas generales.
  • Mover la regla a un sitio «más visible» —ya lo era—.
  • Confiar en que el agente «recuerde los standards».

Lo que sí cambió las cosas fue reescribir la regla que ya tenía, con cuatro decisiones muy concretas:

  1. Escribir el anti-patrón con la sintaxis exacta que dispara el fallo (validators={{, listeners={{, onSubmit={{ seguido de una arrow function), en vez de una descripción abstracta. El agente reconoce el patrón literal mucho mejor que una prohibición conceptual.
  2. Nombrar la trampa: «cuidado con las librerías de formularios (TanStack Form, React Hook Form, Formik)». Nombrar el patrón concreto lo desactiva mejor que una regla genérica.
  3. Poner la regla cerca del código que aplica: la referencié en .claude/rules/features.md, uno de esos archivos que Claude Code carga solo al editar src/features/**, no solo en el documento general que casi nunca vuelves a abrir.
  4. Darle un ejemplo del fallo real, no uno de juguete: el ✅/❌ con el validador exacto que el agente había escrito mal, extraído a una función nombrada.

El resultado se notó: nombrar el patrón exacto y ponerlo donde el agente lo lee justo al editar un formulario bajó de forma clara la frecuencia con la que reaparecía el error. La regla pasó de ser una intención que el modelo «debía» seguir a una instrucción tan concreta que era difícil no verla. Y eso lo tenía cien por cien en mi mano: no dependía de ninguna herramienta nueva, solo de escribir mejor lo que ya estaba escrito.

Así quedó el mismo caso del principio, con el validador extraído a una función nombrada antes del return y referenciado desde el JSX:

// ✅ — lógica fuera del JSX, referenciada por nombre
const validateClientDeliveryDateRequired = ({
  value,
  fieldApi,
}: {
  value: string | null;
  fieldApi: FieldApi<...>;
}) =>
  fieldApi.form.getFieldValue('status') === TaxCampaignStatus.BILLED && !value
    ? billedRequiresDateMessage
    : undefined;

const handleStatusChange = () => {
  form.validateField('clientDeliveryDate', 'change');
};

return (
  <>
    <form.Field name="status" listeners={{ onChange: handleStatusChange }}>
      {/* ... */}
    </form.Field>

    <form.Field
      name="clientDeliveryDate"
      validators={{
        onChange: validateClientDeliveryDateRequired,
        onSubmit: validateClientDeliveryDateRequired,
      }}
    >
      {/* ... */}
    </form.Field>
  </>
);

El JSX vuelve a leerse de un vistazo: dice qué valida cada campo, no cómo. Y el anti-patrón que el agente repetía queda a la vista en el propio diff, que es justo lo que buscaba la regla.

Dónde me deja esto

Después de meses incorporando agentes al día a día de desarrollo, este caso me dejó una técnica que aplico ya en cada repo: cuando un agente ignora una regla, la solución rara vez pasa por escribir más normas; pasa por reescribir la que ya tienes con la sintaxis exacta del error que quieres evitar. Nombrar el patrón, ponerlo donde el agente lo lee y enseñarle el fallo real cambia por completo cuántas veces tropieza con lo mismo.

Eso no convierte una convención de estilo en una ley infalible, para eso está el tooling: una regla de lint o un check en CI cuando la norma es lo bastante crítica como para no dejarla a la interpretación de nadie. Pero para la enorme mayoría de convenciones de un equipo, la diferencia entre una regla que el agente ignora y una que sigue está en cómo de específica es. Y esa parte la tienes hoy en tus manos, en cada CLAUDE.md que escribes.

Preguntas frecuentes

¿No se arregla esto con un modelo mejor?
Ayuda, pero no del todo. Un modelo más nuevo sigue el texto de las reglas con más fidelidad, así que la persuasión mejora. Pero sigue siendo persuasión: escribir el anti-patrón exacto le da al modelo algo mucho más concreto que seguir, y eso rinde con cualquier versión. Un modelo mejor refuerza tu texto; no sustituye a escribirlo bien.

¿Y si la regla es realmente crítica?
Ahí sí compensa respaldarla con tooling que la imponga: una regla de lint (por ejemplo con no-restricted-syntax) que corra en el pre-commit, o un check en CI. Es la capa más fuerte, porque falla igual para el agente que para un humano. Para una convención de estilo como la de este caso, afinar el texto ya bajó mucho el error; automatizarlo es la evolución natural cuando el equipo decide que merece la pena.

¿No es más simple confiar en la revisión de código?
La revisión es una red importante, pero es humana: se cansa, se salta cosas y no escala al ritmo al que un agente genera diffs. Una regla escrita con precisión hace que el agente falle menos de entrada, y deja a quien revisa para lo que de verdad necesita criterio, en vez de cazar el mismo patrón una y otra vez.

Sigue nuestro viaje construyendo agentes IA desde dentro