Casi todas las herramientas de la ingeniería de software (tipos, unit tests, contratos de API) se apoyan en una suposición: con la misma entrada y el mismo estado, una función devuelve el mismo resultado. Un rompe esa suposición, y además falla de una forma incómoda: devuelve algo que parece correcto.
En esta lección tratamos al LLM como lo que es para quien lo integra: un componente con un contrato particular. Veamos qué entra (, contexto, mensajes), qué perillas existen () y cómo convertir su salida en datos confiables. Los casos de estudio son docket, que consume modelos, y Adapta, que los sirve.
Un contrato distinto
| Propiedad | Software tradicional | LLM |
|---|---|---|
| Determinismo | misma entrada, misma salida | salidas parecidas, no idénticas |
| Especificación | el código | texto en lenguaje natural |
| Dominio de entrada | tipado, acotado | cualquier texto, incluido el de un atacante |
| Modos de falla | excepciones, códigos de error | respuestas plausibles pero incorrectas |
Una llamada típica usa el formato chat completions:
{
"model": "some-model",
"messages": [
{"role": "system", "content": "Extract contact data."},
{"role": "user", "content": "Ana's email is ana at example dot com"}
],
"temperature": 0.2
}
El protocolo HTTP sigue siendo exacto. Lo que no es exacto es el contenido. Un 200 OK ahora significa "el modelo respondió", no "la respuesta es correcta".
Tokens y ventana de contexto
Los modelos no leen caracteres ni palabras, sino tokens: fragmentos de texto de tamaño variable. En inglés, una aproximación común es unos 4 caracteres por token; en español suele haber algo más de tokens por palabra. Los tokens importan porque:
- se cobran, normalmente con precios distintos para la entrada y la salida;
- limitan: cada modelo tiene una máxima que incluye entrada y salida;
- cuestan tiempo: cada token de salida es un paso más de generación.
Sin memoria entre llamadas
El modelo no recuerda nada entre una llamada y la siguiente. Si una aplicación "recuerda" una conversación, es porque reenvía el historial completo cada vez. De ahí salen tres consecuencias: el costo crece con cada turno, cuando el historial no cabe alguien tiene que decidir qué descartar, y lo que no está en el contexto no existe para el modelo.
El historial se organiza en mensajes con rol: system (instrucciones del desarrollador), user, assistant y, en APIs con herramientas, tool. Pero al final todos se convierten en un solo texto:
def _render_chatml(system: Optional[str], messages: Sequence[_Turn]) -> str:
"""ChatML (``<|im_start|>role\\n…<|im_end|>``) — Qwen2.5, and the default."""
parts = []
if system:
parts.append(f"<|im_start|>system\n{system}<|im_end|>")
for msg in messages:
parts.append(f"<|im_start|>{msg.role}\n{msg.content}<|im_end|>")
parts.append("<|im_start|>assistant\n")
return "\n".join(parts)
Esa función es real (adapta/core/chat_templates.py). Muestra algo importante para la seguridad: no hay una barrera física entre instrucciones y datos. El rol system pesa más solo porque el modelo fue entrenado para obedecerlo.
Temperature no es determinismo
La temperature controla cuánta aleatoriedad hay al elegir el siguiente token. Cerca de 0, el modelo elige casi siempre el token más probable; con valores altos, explora alternativas.
Quién fija el valor por defecto también es una decisión de diseño:
Salida estructurada y validación
Para una persona, "Nombre: Ana. Email: ana@example.com" es perfecto. Para un programa que debe guardar ese email, es texto libre que puede cambiar de forma. Hay tres niveles de defensa:
- Pedirlo en el prompt ("responde solo con JSON"). Funciona casi siempre.
- Structured output: el proveedor restringe la generación a JSON válido o a un JSON Schema.
- Validar siempre en tu programa, porque un email inventado cumple
"type": "string".
# Illustrative
from pydantic import BaseModel, EmailStr, ValidationError
class Contact(BaseModel):
name: str
email: EmailStr
try:
contact = Contact.model_validate_json(model_output)
except ValidationError:
... # retry, ask for a fix, or escalate to a human
El except resume el AI Engineering en miniatura: el modelo falló de una forma esperable, ¿qué hace el sistema?
Fallas que parecen correctas
- Truncamiento: si el endpoint corta por longitud (
finish_reason: "length"), el JSON o la llamada a herramienta pueden quedar a medias. En docket,ChatResponse.truncateddetecta ese caso y el loop (src/docket/core/agent_loop.py) termina el turno coninvalid_outputsin ejecutar ninguna herramienta. - Esquema válido, contenido inventado: pasa cualquier validación de tipos.
- Instrucción ignorada en silencio: responde en otro idioma o agrega un saludo.
- Salida vacía que no es error: en Adapta, si
_extract_pairsno logra parsear, devuelve una lista vacía; solo las excepciones cuentan para la tasa de error de la síntesis. Ese fragmento no aporta pares y tampoco suma como falla.
Para CTOs
Integrar un LLM obliga a decidir qué pasa cuando la respuesta tiene la forma correcta pero está mal. Antes de ponerlo en producción, pide a tu equipo:
- validación de esquema en el código, no solo en el prompt;
- una política explícita para cada falla esperable: reintentar, pedir corrección, escalar o rechazar;
- métricas que distingan "el modelo respondió" de "la respuesta fue útil" (ver Evaluar modelos).
Si el modelo va a pedir acciones y no solo producir texto, el siguiente paso es De la aplicación con LLM al agente.
Para llevar
- Un
200 OKde un modelo solo dice que respondió; la corrección hay que verificarla aparte. - Los tokens cuestan dinero, espacio de contexto y latencia; distingue los tokens medidos de los estimados.
- El modelo no tiene memoria: el historial se reenvía, y todos los roles terminan en un solo texto.
temperature=0no garantiza determinismo, y el determinismo no garantiza que la respuesta sea correcta.- Valida siempre la salida en tu programa y define qué hace el sistema cuando la validación falla.
Comprueba lo aprendido
¿Por qué una conversación larga cuesta más por turno que una corta?
Porque el modelo no tiene memoria y la aplicación reenvía todo el historial en cada llamada. Los tokens de entrada crecen con cada turno.
El proveedor garantiza que la salida cumple tu JSON Schema. ¿Sigues necesitando validar?
Sí. El esquema garantiza la forma, no el contenido: un valor inventado puede ser un string perfectamente válido. Además, conviene validar reglas de negocio que el esquema no expresa.
¿Qué riesgo tiene una respuesta con `finish_reason: "length"` que incluye una llamada a herramienta?
La llamada puede estar incompleta o mal formada. docket la trata como salida inválida y no la ejecuta.