Portada del artículo: Ingeniería de prompts avanzada: API y modelos locales

Ingeniería de prompts avanzada: API y modelos locales

Escribes un prompt que en el chat funciona de maravilla, lo pegas en tu código y de repente la respuesta llega con un saludo de más, el JSON viene envuelto en comillas invertidas o, peor, un día simplemente no llega completo. No es mala suerte: en una API o en un modelo local nadie te adorna la conversación, y todo lo que el chat resolvía por detrás ahora te toca resolverlo a ti.

Este artículo continúa la guía práctica de ingeniería de prompts, donde están los principios que sirven en cualquier IA (claridad, contexto, ejemplos, etiquetas, formato) y la versión básica de la prompt injection. Aquí no los repito: construyo encima de ellos lo que esa guía dejó fuera.

Para quién es: para quien desarrolla o está por integrar un modelo en una aplicación, un script o un flujo automático, y también para quien tiene curiosidad y quiere entender qué pasa detrás del chat. Defino los términos técnicos cuando aparecen, así que no necesitas venir con todo sabido.

Qué te llevas: por qué el mismo prompt se comporta distinto en web, API y local; cómo usar roles y system prompt; qué es el prompt caching y cómo ordenar el prompt para aprovecharlo; cómo obtener JSON fiable; qué hacen los parámetros de muestreo; cómo configurar Ollama y llama.cpp; técnicas avanzadas (few-shot bien diseñado, razonamiento, cadenas de prompts, evaluación); y cómo validar y proteger todo en producción.

Una advertencia antes de empezar: las APIs cambian rápido. Donde algo depende del producto, digo “según la documentación de X” y te dejo el enlace al final. Si un nombre de parámetro o un límite te importa, confírmalo ahí antes de depender de él.

Por qué el mismo prompt se comporta distinto en web, API y local

En el chat web, la herramienta añade su propio system prompt, administra el historial y decide cómo mostrarte la respuesta. En la API, nada de eso viene incluido. En un modelo local, tampoco, pero además eliges tú el modelo, su memoria y sus ajustes. La guía práctica explica la parte del chat y del editor; esta tabla suma las otras dos columnas.

Tabla 1: Chat web, modelo local y API, qué controlas en cada uno

AspectoChat webModelo local (Ollama, llama.cpp)API (OpenAI, Anthropic, Gemini)
System promptLo define el servicio; tú no lo vesLo defines tú (en Ollama, la instrucción SYSTEM de un Modelfile)Lo envías tú, de forma explícita, en un campo o rol propio
Historial y contextoLo gestiona la aplicación; cada producto decide qué conservaLo limita tu memoria: el tamaño de contexto es un ajuste (num_ctx)Lo gestionas tú: en una API sin estado reenvías los mensajes en cada llamada
ParámetrosLos fija el servicioAjustables por flags o por Modelfile (--temp, --top-k, --repeat-penalty)Por petición, y según el modelo algunos se pueden cambiar y otros no
Formato de salidaPensado para personas (Markdown, tablas, bloques de código)Texto plano que puedes encadenar con pipes, o salida restringida por gramáticaSalidas estructuradas con esquema JSON y llamadas a herramientas
Uso idealExplorar ideas, tareas interactivas rápidasPrivacidad de los datos, automatizar en la terminal, trabajar sin conexiónIntegrar el modelo en software, flujos automáticos y agentes

Cómo leerla: cada fila es una decisión que en el chat ya viene tomada y en los otros dos entornos te toca a ti. La última fila no es una regla, es una orientación. Dos consecuencias prácticas: en la web no hace falta que repitas “responde en Markdown” porque el servicio ya se ocupa, y en local los modelos pequeños necesitan instrucciones más directas y ejemplos más claros, algo que veremos más abajo.

La API: roles y system prompt

Una llamada a una API de modelos no es “un texto”: es una lista de mensajes con roles. El rol dice quién habla y cuánto peso tiene lo que dice.

  • System o developer: las instrucciones permanentes de quien construye la aplicación (rol, reglas, formato, límites). Es el system prompt.
  • User: lo que pide la persona o el programa en esta petición, normalmente datos que cambian cada vez.
  • Assistant: lo que el modelo respondió antes, si lo reenvías como historial.

Cada proveedor lo nombra a su manera, y conviene mirarlo en su documentación:

  • Anthropic: la referencia de su API de mensajes describe system como un parámetro de primer nivel, no como un rol dentro de messages, y dice textualmente que no hay rol "system" para los mensajes de entrada.
  • OpenAI: su guía de texto describe tres roles, developer, user y assistant. Los mensajes developer son instrucciones de quien desarrolla la aplicación y tienen prioridad sobre los de user. El rol system de integraciones anteriores ya no aparece en esa guía. También existe un parámetro instructions que aplica solo a la petición actual: si encadenas turnos con previous_response_id, las instrucciones de turnos anteriores no están en el contexto y tienes que reenviarlas.
  • Gemini: se usa el parámetro system_instruction, en el nivel superior de la petición, no dentro de la configuración de generación.

Lo que sí es común: las instrucciones estables van en el system prompt y los datos de cada petición van en el mensaje de usuario. Así el system prompt se vuelve un activo reutilizable que puedes versionar y probar, y los datos no se mezclan con las reglas. Ojo con venderlo como una barrera de seguridad: separar ayuda al modelo a distinguir qué es qué, pero no impide que obedezca un texto malicioso (lo veremos en la sección de seguridad).

La API de mensajes de Anthropic, según su documentación, se usa en conversaciones sin estado (stateless): no recuerda nada entre llamadas, y tú envías los turnos anteriores en cada petición. Eso tiene un efecto directo en el costo y en la ventana de contexto: cada vez reenvías más historia, y esa historia cuenta. OpenAI ofrece además formas de conservar el estado de la conversación en su lado, pero el principio sigue siendo que lo que el modelo “ve” es lo que viaja en la petición.

Un system prompt con etiquetas

Este es un system prompt de ejemplo para un revisor de código, estructurado con etiquetas XML (la técnica de la guía práctica, ahora aplicada a un system prompt completo):

<rol>
Eres un Arquitecto de Software especialista en refactorización de código legado
en Python y diseño de APIs RESTful.
</rol>
<contexto>
Trabajas revisando código para una plataforma financiera de alta concurrencia.
La prioridad absoluta es la seguridad, el tipado estricto y el rendimiento
(baja latencia).
</contexto>
<instrucciones>
1. Analiza el código proporcionado dentro de las etiquetas <codigo_fuente>.
2. Identifica vulnerabilidades de seguridad, cuellos de botella de rendimiento
   y violaciones a PEP 8.
3. Reescribe el código aplicando tipado estático (type hints) y manejo robusto
   de excepciones.
</instrucciones>
<restricciones>
- NO utilices librerías externas que no pertenezcan a la librería estándar de
  Python, salvo Pydantic o FastAPI.
- NO incluyas saludos, introducciones ni despedidas conversacionales.
- Si el código de entrada está incompleto, asume las importaciones estándar sin
  pedir aclaraciones.
</restricciones>
<formato_salida>
Responde estrictamente con la siguiente estructura Markdown:
1. **Diagnóstico**: lista de viñetas (máximo 1 línea por hallazgo).
2. **Código refactorizado**: bloque de código Python.
3. **Complejidad**: breve nota con la complejidad temporal (Big O) antes y después.
</formato_salida>

Dónde y cómo usarlo: esto va en el campo de system prompt de tu llamada a la API, y el código a revisar va aparte, en el mensaje de usuario. Con la API de mensajes de Anthropic, el cuerpo de la petición tiene esta forma (cambia MODEL_ID por el identificador de un modelo de la lista de tu proveedor):

{
  "model": "MODEL_ID",
  "max_tokens": 2048,
  "system": "<rol>Eres un Arquitecto de Software... (el texto de arriba)</rol>",
  "messages": [
    {
      "role": "user",
      "content": "<codigo_fuente>\ndef total(items): ...\n</codigo_fuente>"
    }
  ]
}

Dónde y cómo usarlo: es el cuerpo JSON que enviarías al endpoint de mensajes de Anthropic con tu clave de API. En OpenAI, el mismo texto iría en el parámetro instructions o en un mensaje con rol developer; en Gemini, en system_instruction. Lo que cambia es el nombre del campo, no la idea.

Rol condicionado

Un system prompt suele empezar con un rol. Anthropic lo documenta así: poner un rol en el system prompt enfoca el comportamiento y el tono del modelo para tu caso, y bastan una o dos frases. La versión útil no es “eres un experto”, sino un perfil concreto que ancle el vocabulario y la profundidad, como “Eres un ingeniero DevOps senior especializado en seguridad de contenedores en Kubernetes” frente a un genérico “ayúdame con esto”. Lo que no hace un rol es dar conocimiento que el modelo no tiene: orienta el estilo y el nivel, no sustituye datos ni verificación.

Prompt caching: lo estático primero, lo variable al final

Si tu system prompt mide miles de tokens y lo mandas en cada petición, estás pagando y esperando por procesar lo mismo una y otra vez. El prompt caching permite que el proveedor reutilice el procesamiento de la parte inicial que se repite. Según la documentación de Anthropic, reduce latencia y costo para el contenido repetido; según la de OpenAI, está activado por defecto en los modelos compatibles.

La regla de oro la dicen los dos: el caché funciona por prefijo, así que lo que no cambia va primero y lo que cambia va al final.

  • Anthropic: el prompt se cubre en el orden tools → system → messages. Una entrada de caché abarca todo hasta el bloque marcado con cache_control, y un cambio en un nivel invalida ese nivel y los siguientes. Para acertar, el contenido debe ser idéntico: la documentación advierte que, por ejemplo, una marca de tiempo antes del punto de corte hace que nunca haya aciertos.
  • OpenAI: reutilizar el caché exige que todo el prefijo coincida. Recomienda poner primero instrucciones y material de referencia estables, y al final lo dinámico (marcas de tiempo, datos del usuario), y agregar mensajes nuevos en vez de reescribir turnos anteriores.
{
  "model": "MODEL_ID",
  "max_tokens": 1024,
  "system": [
    {
      "type": "text",
      "text": "<instrucciones>... reglas largas y estables ...</instrucciones>",
      "cache_control": { "type": "ephemeral" }
    }
  ],
  "messages": [
    {
      "role": "user",
      "content": "<transcripcion>... texto de hoy ...</transcripcion>"
    }
  ]
}

Dónde y cómo usarlo: es la misma petición de la sección anterior, con cache_control en el último bloque del system prompt que no cambia entre peticiones. Lo variable (la transcripción de hoy) queda después. Para saber si funcionó, revisa en la respuesta los campos de uso, cache_creation_input_tokens y cache_read_input_tokens: si ambos son 0, esa petición no se cacheó.

Lo que dice la documentación de Anthropic sobre números, y solo eso: la vida del caché por defecto es de 5 minutos y se renueva con cada acierto (hay una opción de 1 hora); escribir en el caché cuesta 1,25 veces el precio normal de entrada con la vida de 5 minutos, y leer cuesta una fracción (0,1 veces en el caso general, con otros factores en algunos modelos); y los prompts demasiado cortos simplemente no se cachean, con un mínimo que según el modelo va de 512 a 4.096 tokens. OpenAI indica que el descuento en tokens cacheados puede llegar hasta el 95 %. Revisa la página de precios de tu proveedor, porque estas cifras cambian.

Una consecuencia de diseño: no metas la fecha, el nombre del usuario ni ningún dato variable al comienzo del system prompt. Si lo necesitas, ponlo al final o en el mensaje de usuario.

Salidas estructuradas y llamadas a herramientas

En la guía práctica quedó pendiente una pregunta: ¿cómo consigo que la respuesta sea JSON válido siempre? Hay dos niveles, de menos a más fiable, y una variante.

Nivel 1, solo prompt. Lo pides en el texto. Es lo que haces en un chat y sirve para pruebas rápidas:

Devuelve ÚNICAMENTE un objeto JSON válido, sin bloques de código Markdown,
siguiendo este esquema:
{"sentimiento": "POSITIVO | NEUTRAL | NEGATIVO", "confianza": 0.0,
 "palabras_clave": ["string"], "resumen_ejecutivo": "string (máx 20 palabras)"}

Dónde y cómo usarlo: al final del prompt de usuario o del system prompt, para un script que lea la respuesta con un parser JSON. Funciona bastante bien, pero ninguna palabra del prompt garantiza que el modelo la cumpla.

Nivel 2, salida estructurada con esquema. Le entregas a la API un esquema JSON (una descripción formal de qué campos y de qué tipo debe tener la respuesta) y el proveedor restringe lo que el modelo puede generar. La documentación de OpenAI lo describe como text.format con tipo json_schema y strict activado, y recomienda usarlo en lugar del modo JSON simple, que solo garantiza JSON válido, no que respete tu esquema. Anthropic lo expone con output_config.format y tipo json_schema. Gemini documenta una configuración con tipo MIME application/json y un esquema (confirma el nombre exacto del campo en tu SDK).

Ese es el centro de las salidas estructuradas. Veámoslo con una tarea concreta: extraer tareas pendientes de la transcripción de una reunión. El system prompt (con etiquetas y un ejemplo, la técnica few-shot) sería este:

<instrucciones>
Eres un motor de extracción de entidades. Tu única función es analizar
transcripciones dentro de <transcripcion> y devolver un objeto JSON con las
tareas detectadas.
</instrucciones>
<restricciones>
- Salida exclusiva en JSON válido. Cero texto adicional.
- Si no hay tareas, devuelve {"tareas": []}.
</restricciones>
<ejemplo>
Entrada: <transcripcion>Juan revisará el servidor el martes y María actualizará
la documentación.</transcripcion>
Salida: {"tareas": [{"descripcion": "Revisar el servidor", "responsable": "Juan",
"plazo": "Martes"}, {"descripcion": "Actualizar documentación",
"responsable": "María", "plazo": null}]}
</ejemplo>

Y el mensaje de usuario solo trae el dato:

<transcripcion>
Necesitamos que Carlos despliegue el parche de seguridad mañana antes de las
5pm. Además, el equipo de QA debe validar el entorno de staging el jueves.
</transcripcion>

Dónde y cómo usarlo: el primer bloque va en el system prompt y el segundo en el mensaje de usuario de la misma petición; el tercer bloque, más abajo, le añade el esquema. Reemplaza la transcripción de ejemplo por la tuya.

La petición a la API de Anthropic, con el esquema, queda así:

{
  "model": "MODEL_ID",
  "max_tokens": 1024,
  "system": "<instrucciones>... (el system prompt de arriba)</instrucciones>",
  "messages": [
    { "role": "user", "content": "<transcripcion>...</transcripcion>" }
  ],
  "output_config": {
    "format": {
      "type": "json_schema",
      "schema": {
        "type": "object",
        "properties": {
          "tareas": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "descripcion": { "type": "string" },
                "responsable": {
                  "anyOf": [{ "type": "string" }, { "type": "null" }]
                },
                "plazo": { "anyOf": [{ "type": "string" }, { "type": "null" }] }
              },
              "required": ["descripcion", "responsable", "plazo"],
              "additionalProperties": false
            }
          }
        },
        "required": ["tareas"],
        "additionalProperties": false
      }
    }
  }
}

Dónde y cómo usarlo: es el cuerpo JSON de una petición al endpoint de mensajes de Anthropic, según el formato que documentan hoy. El campo output_config.format sustituye al antiguo parámetro output_format, que la propia documentación marca como obsoleto. Con OpenAI el esquema va en text.format (con strict: true), y con Gemini en su propia configuración; la forma del esquema es la misma idea.

Los límites son parte del diseño, no un detalle:

  • Cada proveedor soporta un subconjunto de JSON Schema. Anthropic, por ejemplo, no admite esquemas recursivos ni restricciones numéricas como minimum o maximum, ni largos de texto como minLength; si usas algo no soportado, devuelve un error 400. Gemini sí menciona minimum y maximum. OpenAI pide, en modo estricto, additionalProperties: false y todos los campos como requeridos (los opcionales se modelan permitiendo null). Un esquema que sirve en uno puede no servir en otro.
  • El esquema garantiza la forma, no el contenido. Gemini lo dice sin rodeos: la salida es JSON sintácticamente válido, pero hay que validar los valores en tu aplicación, porque una respuesta puede cumplir el esquema y estar equivocada.
  • Pueden romperse por causas ajenas al esquema. Si la respuesta se corta por el límite de tokens (stop_reason: "max_tokens" en Anthropic, estado incomplete en OpenAI) o el modelo se niega a responder (refusal), el JSON puede venir incompleto o no cumplir el esquema. Anthropic lo advierte explícitamente.

Llamadas a herramientas (tool calling). Es una variante: en vez de devolver texto, el modelo devuelve la petición de que tu código ejecute una función con ciertos argumentos. La documentación de OpenAI es clara en un punto clave: el modelo no ejecuta la función, la ejecuta tu aplicación y le devuelve el resultado. Las llamadas a herramientas también aceptan modo estricto (strict: true en la definición de la herramienta, en OpenAI y en Anthropic) para que los argumentos respeten el esquema. Es la base de los agentes, y también donde más conviene desconfiar, como verás en la sección de seguridad.

Parámetros de muestreo: temperatura, top-p y compañía

Un modelo no “elige” la siguiente palabra: calcula probabilidades para cada token posible y sortea entre ellos. Los parámetros de muestreo controlan cuánto margen tiene ese sorteo.

Tabla 2: Parámetros de muestreo y límites de generación

ParámetroQué hace, en palabras sencillasCuándo tocarloQué comprobar
temperatureMás alta, respuestas más variadas; más baja, más predeciblesBaja para extracción y clasificación; más alta para ideas y redacciónVarios modelos recientes ya no te dejan cambiarla (ver abajo)
top_pSolo sortea entre los tokens más probables, hasta sumar una probabilidad pRara vez; se suele ajustar o la temperatura o esta, no las dosMismo caso: puede estar bloqueado en modelos recientes
top_kSolo sortea entre los k tokens más probablesModelos locales; en la nube, solo si lo documentanValores y disponibilidad según el servidor
min_pDescarta tokens cuya probabilidad es muy baja comparada con la del más probableModelos locales (Ollama, llama.cpp)Disponible como min_p en Ollama y --min-p en llama.cpp
max_tokens (max_output_tokens, num_predict)El máximo de tokens que el modelo puede generarSiempre: define un tope razonableSi lo alcanza, la respuesta queda cortada
seedFija la semilla del sorteo para repetir resultadosPruebas reproduciblesEn local sí; en la nube, “al mejor esfuerzo” (ver abajo)
stop (stop_sequences)Cadenas de texto que, al aparecer, detienen la generaciónCortar antes de que el modelo siga “conversando”Nombre y límites según el proveedor

Cómo leerla: lee de arriba abajo como una cadena: primero la temperatura reparte el margen, luego top_p, top_k o min_p recortan las opciones raras, y max_tokens y stop ponen el punto final. La última columna es la importante, porque aquí es donde las APIs más han cambiado.

Tres cosas que verás en las documentaciones:

  • La temperatura y top_p ya no son libres en muchos modelos. Según la documentación de Anthropic, en una larga lista de modelos recientes (entre ellos los Opus 4.7 en adelante, Sonnet 5 y Haiku 5.5), cualquier valor distinto del predeterminado de temperature, top_p o top_k devuelve un error 400. OpenAI indica que, cuando el esfuerzo de razonamiento no es none, debes quitar temperature y top_p. Google indica que Gemini 4 y los Gemini 3.6 en adelante ignoran los valores personalizados y recomienda eliminar esos ajustes. Conclusión práctica: si tu código manda temperature: 0 por costumbre, puede que ya no funcione con el modelo nuevo; y para controlar la variabilidad ahora dependes más del prompt, de las salidas estructuradas y de la validación.
  • max_tokens incluye el razonamiento. En los modelos que “piensan” antes de responder, ese pensamiento cuenta para el tope. La documentación de Anthropic dice que los tokens de pensamiento se facturan como salida y cuentan contra max_tokens; la de OpenAI dice que max_output_tokens incluye los tokens de razonamiento y que la respuesta puede quedar incompleta antes de producir texto visible, y sugiere reservar bastante margen al empezar a experimentar (menciona al menos 25.000 tokens para razonamiento y respuesta).
  • seed no es una garantía. En local, Ollama documenta que la semilla fija el resultado para el mismo prompt. En la referencia de OpenAI para Chat Completions, el parámetro aparece como beta, marcado como obsoleto, y la documentación aclara que el determinismo no está garantizado.

Modelos locales: Ollama y llama.cpp

Un modelo local corre en tu máquina. Ganas privacidad (el texto no sale de tu equipo), funcionamiento sin conexión y control total; a cambio, el rendimiento depende de tu hardware y los modelos pequeños suelen necesitar más ayuda en el prompt.

Ollama: Modelfile, system prompt y contexto

Ollama es un programa que descarga y ejecuta modelos abiertos en tu equipo. Para dejar configurado un modelo con su system prompt y sus parámetros, usa un Modelfile, un archivo de texto con instrucciones como FROM (qué modelo base), SYSTEM (el system prompt) y PARAMETER (ajustes). Estas instrucciones están en la documentación de Ollama:

FROM llama3

SYSTEM """
Eres un asistente que extrae tareas de transcripciones de reuniones.
Responde solo con el formato pedido, sin saludos ni explicaciones.
Si falta un dato, escribe NULL.
"""

PARAMETER temperature 0.2
PARAMETER num_ctx 8192

Dónde y cómo usarlo: guarda el archivo como Modelfile (sin extensión), reemplaza llama3 por un modelo que tengas descargado y ejecuta en la terminal ollama create extractor-tareas -f ./Modelfile y luego ollama run extractor-tareas. Los valores 0.2 y 8192 son solo de ejemplo.

Sobre num_ctx, que fija el tamaño del contexto: la documentación indica que un contexto mayor consume más memoria, y la página de contexto de Ollama dice que el valor por defecto se decide según la VRAM disponible (4k por debajo de 24 GiB, 32k entre 24 y 48, y 256k desde 48; la página del Modelfile aún lista 2048 como valor por defecto, así que no des por hecho un número: compruébalo con ollama ps, que muestra el contexto asignado). Si tu prompt más los datos no caben, el modelo no verá todo lo que le mandaste.

Un ejemplo de terminal con Ollama

La misma tarea de extraer tareas, ahora en la terminal y con salida en CSV para abrirla en una hoja de cálculo. Ollama documenta que ollama run acepta el prompt como argumento. Su documentación no especifica cómo se combina ese argumento con texto que llega por una tubería (pipe), así que en vez de cat archivo | ollama run ... leo el archivo dentro del propio prompt:

PROMPT="Extrae las tareas de este texto.
REGLAS ESTRICTAS:
1. Devuelve SOLO formato CSV válido con encabezados: tarea,responsable,fecha_limite,prioridad
2. No escribas texto antes ni después del CSV. No uses bloques markdown.
3. Si falta un dato, usa NULL.

Texto:
$(cat transcripcion.txt)"

ollama run llama3 "$PROMPT" > tareas.csv

Dónde y cómo usarlo: en una terminal tipo Bash (Linux, macOS o WSL), con transcripcion.txt en la misma carpeta. En Windows con PowerShell cambia la primera parte por $texto = Get-Content .\transcripcion.txt -Raw y usa $texto dentro de una cadena entre comillas dobles. Ojo con dos cosas: el operador > de PowerShell equivale a Out-File, así que si te importa la codificación del CSV usa Out-File -Encoding; y revisa siempre tareas.csv, porque un modelo pequeño puede colarse un “Claro, aquí tienes” pese a la regla. Para transcripciones muy largas, la línea de comandos tiene un límite de tamaño y conviene llamar a la API local de Ollama.

Esa API local admite el parámetro format: según la documentación de Ollama, con "json" devuelve JSON genérico y con un esquema JSON restringe la respuesta a esa estructura. Recomiendan además incluir el esquema en el prompt para orientar al modelo y bajar la temperatura (sugieren 0) para más consistencia. Esa función no está disponible en los modelos en la nube de Ollama.

llama.cpp: flags de muestreo y gramáticas

llama.cpp es un motor de inferencia para correr modelos en tu equipo, con control fino sobre cómo se genera cada token. Su documentación de herramientas lista, entre otros, estos flags: --temp, --top-k, --top-p, --min-p, --repeat-penalty, --seed, --ctx-size y --n-predict. Los valores por defecto varían entre la tabla de ayuda y el texto de la documentación, así que comprueba los de tu versión con -h.

Dos ideas que no existen en el chat:

  • El orden de los muestreadores importa. llama.cpp expone la lista y el orden con --samplers; en su ayuda, el orden por defecto aplica los filtros de recorte (top_k, top_p, min_p) y deja temperature para el final. Si cambias el orden, cambias el resultado.
  • Min-p es un método de muestreo que propone un artículo presentado en ICLR 2025: recorta las opciones usando la probabilidad del token más probable como escala, en lugar de un umbral fijo como top-p. Sus autores informan mejoras de calidad y diversidad, sobre todo con temperaturas altas, en los modelos y pruebas que evaluaron. No hay un valor “óptimo” universal; si lo usas, pruébalo con tu tarea.

Y la herramienta más potente para salidas estructuradas en local: las gramáticas GBNF. Una gramática describe qué texto es válido, y llama.cpp solo deja generar tokens que respeten la gramática. Se pasan con --grammar o --grammar-file, y también hay un flag para pasar directamente un esquema JSON (--json-schema, o -j) que se convierte en gramática. Un ejemplo mínimo, para forzar que el sentimiento sea una de tres opciones:

root ::= "{\"sentimiento\": \"" ("POSITIVO" | "NEUTRAL" | "NEGATIVO") "\"}"

Dónde y cómo usarlo: guarda la línea en un archivo sentimiento.gbnf y pásalo con --grammar-file sentimiento.gbnf al ejecutar llama.cpp, junto con tu modelo y tu prompt. Lo escribí siguiendo la sintaxis de la documentación, pero pruébalo en tu versión antes de depender de él.

Con tres advertencias de la propia documentación de llama.cpp: la conversión de esquema JSON a gramática omite en silencio las funciones que no soporta (revisa la gramática generada y pruébala con su validador); el esquema restringe la salida pero el modelo no lo “ve”, así que describe la estructura también en el prompt; y las gramáticas tienen casos de rendimiento lento.

Modelos pequeños: más directos y con ejemplos

Un modelo de unos pocos miles de millones de parámetros (digamos 7B a 14B) suele seguir peor las instrucciones largas o abstractas que uno grande. No es una ley y depende del modelo, pero la pauta práctica es la misma: instrucciones cortas y literales, un par de ejemplos del formato exacto, reglas explícitas como “no escribas nada antes ni después” cuando rediriges la salida a un archivo, y una restricción real (formato json de Ollama o una gramática) para lo que no puede fallar.

Técnicas avanzadas

Ejemplos bien diseñados (few-shot)

La guía práctica explicó few-shot; aquí va el matiz de diseño. La documentación de Anthropic pide ejemplos relevantes, diversos (que cubran casos límite y no enseñen patrones sin querer) y envueltos en etiquetas <example>, y sugiere de 3 a 5. La de Gemini recomienda mantener la estructura y el formato de todos consistentes y avisa de que demasiados ejemplos pueden llevar al modelo a imitarlos en exceso. Un truco útil: incluye entre tus ejemplos el caso vacío o incómodo (en la extracción de tareas, una reunión sin ninguna tarea), porque es donde los modelos improvisan.

Razonamiento: cadena de pensamiento y modelos de razonamiento

La cadena de pensamiento (chain of thought) consiste en pedirle al modelo que razone paso a paso antes de dar el veredicto. Ayuda en tareas con varios pasos de razonamiento, como lógica, cálculo o decisiones de arquitectura, pero no es un conjuro: no elimina las alucinaciones y un razonamiento bien escrito puede estar equivocado.

Con los modelos de razonamiento, que piensan antes de responder por su cuenta, el consejo cambia. La documentación de Anthropic dice que, cuando el pensamiento está desactivado, puedes pedir razonamiento paso a paso como alternativa, pero en los modelos más recientes recomienda apoyarse en el pensamiento integrado y ajustar el parámetro effort; añade que un prompt que pida escribir el razonamiento (por ejemplo, en etiquetas <thinking>) puede ser rechazado. La guía de razonamiento de OpenAI aconseja dar la tarea, las restricciones y el formato deseado sin prescribir cada paso intermedio, y tratar el nivel de esfuerzo como una perilla de ajuste, no como la forma principal de recuperar calidad. Recomendación: si tu modelo ya razona, empieza sin “piensa paso a paso”, y añade el paso a paso solo si una prueba muestra que mejora.

Encadenar prompts

El encadenamiento de prompts divide una tarea en llamadas consecutivas. Un patrón típico: extraer (sacar los datos de un texto) → analizar (buscar anomalías, priorizar) → formatear (generar el informe final). Anthropic aclara que, con pensamiento adaptativo y subagentes, el modelo ya maneja internamente mucho razonamiento de varios pasos, y que el encadenamiento explícito sigue valiendo la pena cuando necesitas inspeccionar resultados intermedios o imponer una estructura de pipeline. Cada paso es una llamada aparte, así que puedes registrar, evaluar o bifurcar en cualquier punto, y cada paso puede tener su propio esquema y sus propias validaciones.

Autocrítica como paso separado

En la guía práctica vimos la autocrítica dentro del mismo prompt. En una aplicación, el patrón más común es hacerlo como paso aparte: borrador → revisión contra criterios → versión final, que Anthropic describe como el patrón de encadenamiento más frecuente (autocorrección). Según su guía, pedirle al modelo que verifique su respuesta contra criterios explícitos detecta errores de forma fiable, sobre todo en código y matemáticas, con la excepción de algunos modelos que ya verifican por sí solos y a los que esa instrucción solo les añade tokens y latencia. Como siempre: la prueba te dice si en tu caso aporta algo.

Evalúa antes de cambiar nada

Cambiar un prompt “porque suena mejor” es la forma más rápida de romper algo que ya funcionaba. Anthropic recomienda diseñar evaluaciones que reflejen la tarea real, incluir casos límite (datos irrelevantes o inexistentes, entradas muy largas, casos ambiguos) y automatizar la calificación cuando se pueda, por ejemplo con coincidencia exacta para respuestas categóricas. También sostiene que es mejor tener más casos con calificación automática, aunque den una señal algo menos fina, que pocos casos calificados a mano.

Para empezar sin montar nada grande: reúne entre 20 y 50 entradas reales (cantidad orientativa mía, no una cifra de la documentación) con la salida que esperas, incluye las incómodas, y ejecuta tu prompt actual contra todas antes y después de cada cambio. Si usas un modelo para calificar a otro, la documentación de Anthropic sugiere que sea un modelo distinto al que generó la respuesta.

Validar la salida en producción

Aunque uses esquemas, trata la respuesta del modelo como entrada externa: puede venir incompleta, rechazada o con la forma correcta y el contenido equivocado. Una pila de tres niveles lo cubre:

  1. Sintaxis: ¿es JSON que el parser puede leer?
  2. Esquema: ¿tiene los campos, tipos y valores permitidos? Si tu proveedor no soporta ciertas restricciones (como rangos numéricos), las compruebas tú aquí.
  3. Sentido: ¿tiene lógica? Que cada responsable aparezca de verdad en la transcripción, que la fecha no esté en el pasado, que la suma cuadre. Esto solo lo puede comprobar tu código y tus reglas de negocio.

Tabla 3: Cómo terminó la respuesta y qué hacer

SituaciónQué indicaQué hacer
stop_reason: "end_turn" (Anthropic)El modelo terminó con naturalidadPasa a validar (sintaxis, esquema, sentido)
stop_reason: "max_tokens" (Anthropic) o estado incomplete (OpenAI)Se alcanzó el límite; la salida puede estar cortada y no cumplir el esquemaNo la uses; sube el límite y reintenta (así lo indica la documentación de Anthropic)
stop_reason: "refusal" o campo refusalEl modelo declinó responder (llega como una respuesta normal, no como error HTTP)No reintentes a ciegas con el mismo prompt: revisa el motivo y decide; Anthropic sugiere un modelo alternativo
JSON inválido o fuera del esquemaFalló la sintaxis o el esquemaReintenta un número pequeño de veces, devolviendo el error al modelo, y luego cae al plan B
Pasa el esquema pero falla la regla de negocioRespuesta bien formada pero equivocadaMarca para revisión humana o reintenta una vez con la regla incumplida explícita

Cómo leerla: la primera columna es lo que te dice la API; la tercera es una política razonable para empezar, no una receta de ningún proveedor (excepto los puntos que indico). Lo importante es que cada fila tenga un camino definido en tu código, incluido el “plan B” final.

import re

from pydantic import BaseModel, ValidationError


class Tarea(BaseModel):
    descripcion: str
    responsable: str | None
    plazo: str | None


class Resultado(BaseModel):
    tareas: list[Tarea]


class Rechazo(Exception):
    """El modelo declinó responder: no se reintenta con el mismo prompt."""


def aparece(nombre: str, texto: str) -> bool:
    # Palabras completas, sin distinguir mayúsculas: "Ana" no coincide con "mañana".
    partes = nombre.split()
    if not partes:  # vacío o solo espacios: no cuenta como encontrado
        return False
    palabras = r"\s+".join(re.escape(p) for p in partes)
    return re.search(rf"\b{palabras}\b", texto, re.IGNORECASE) is not None


def interpretar(stop_reason: str, texto: str, transcripcion: str) -> Resultado:
    if stop_reason == "max_tokens":
        raise RuntimeError("Respuesta cortada: sube el límite y reintenta")
    if stop_reason == "refusal":
        raise Rechazo("El modelo declinó responder")
    try:
        resultado = Resultado.model_validate_json(texto)  # sintaxis + esquema
    except ValidationError as error:
        raise ValueError(f"Salida inválida: {error}") from error
    for tarea in resultado.tareas:  # sentido
        if tarea.responsable and not aparece(tarea.responsable, transcripcion):
            raise LookupError(f"Revisar a mano: {tarea.responsable}")
    return resultado

Dónde y cómo usarlo: en el backend o script que llama al modelo, justo después de recibir la respuesta. Necesitas Python 3.10 o superior y pip install pydantic. stop_reason y texto salen del objeto de respuesta de tu proveedor (aquí uso los nombres de Anthropic; en otros cambian). Reintenta solo los fallos de forma (RuntimeError por respuesta cortada, subiendo el límite, y ValueError por JSON inválido), con un máximo de 2 o 3 intentos. Un Rechazo no se reintenta igual: revisa el motivo, como indica la Tabla 3. El chequeo de sentido es una heurística simple: busca el nombre como palabras completas sin distinguir mayúsculas, así que “Equipo de QA” pasa y “Ana” no se confunde con “mañana”, pero un responsable reescrito con otras palabras no pasa. Por eso no lanza un reintento: lanza LookupError para que una persona lo revise.

Seguridad en aplicaciones: la prompt injection más allá del chat

La prompt injection básica ya está en la guía práctica. En una aplicación el riesgo crece: tu programa lee correos, páginas o archivos de terceros y, si el modelo tiene herramientas, puede actuar. Según OWASP, que la lista como el riesgo número uno para aplicaciones con modelos de lenguaje (LLM01), no está claro que exista un método infalible de prevención. Por eso su recomendación es de capas, que en una aplicación se traducen en:

  • Limita el comportamiento del modelo. Define en el system prompt su rol, su alcance y sus límites. Es una capa útil, pero es texto, no un control técnico.
  • Define y comprueba los formatos de salida. Exige formatos concretos y verifícalos con código determinista, justo la validación de la sección anterior.
  • Filtra entradas y salidas. Busca contenido sospechoso o sensible antes y después del modelo.
  • Mínimo privilegio. Da a la aplicación sus propias credenciales, con el acceso estrictamente necesario, y resuelve las funciones sensibles en código en vez de dejarlas al modelo. Con tool calling, recuerda que el modelo solo pide la acción: tu código decide si la ejecuta, con qué argumentos y con qué permisos.
  • Aprobación humana en acciones de alto riesgo. Enviar dinero, borrar datos o publicar no deberían ocurrir solo porque un modelo lo pidió.
  • Aísla y marca el contenido externo. Separa claramente lo que no controlas (con etiquetas, como en la guía práctica) para que influya menos. OWASP lo propone como mitigación, no como solución definitiva.
  • Pruébalo con ataques. Trata al modelo como un usuario no confiable y haz pruebas adversarias periódicas.

Dicho sin rodeos: separar el system prompt de los datos del usuario mejora el comportamiento del modelo, pero no previene la inyección. Cada capa reduce el riesgo; ninguna lo elimina.

Tu primer paso

Toma una tarea de texto que ya haces a mano y conviértela en una llamada a una API o en un script con un modelo local. Escribe el system prompt con etiquetas, define un esquema JSON o una gramática, junta 20 entradas reales con su salida esperada, y programa la validación de tres niveles. Antes de tocar nada, corre la prueba; después de cada cambio, vuelve a correrla. Esa rutina vale más que cualquier truco de redacción.

Fuentes y lecturas recomendadas

Documentación de proveedores y proyectos (consultada el 17 de octubre de 2026; verifica los detalles por versión):

Ideas, herramientas y lecturas de IA que merecen ser probadas

EXPLORAR SILO COMPLETO→