Módulo 1: Fundamentos de Prompt Engineering
3. Roles: system, user, assistant
Descripción
Las APIs de LLMs usan roles para estructurar conversaciones: system, user, y assistant. Cada rol tiene un propósito distinto y afecta cómo el modelo genera la respuesta. En esta cápsula aprenderás a usar el system prompt como contrato de comportamiento, el user prompt como input, y el assistant como formato de respuesta. También verás cómo diseñar conversaciones multi-turn con estado.
Por qué importa: El system prompt es la palanca más poderosa para controlar el comportamiento del modelo. La mayoría de usuarios solo envían mensajes user; los profesionales configuran system primero. La diferencia en consistencia y calidad es dramática.
Los Tres Roles
system
Propósito: Define el comportamiento global del asistente para toda la conversación. Es la "configuración" del modelo.
Características:
- Se procesa primero y establece la "persona" y reglas del modelo
- Típicamente un solo mensaje al inicio de la conversación
- No visible para el usuario final en la mayoría de UIs
- Alta prioridad: el modelo intenta seguir estas instrucciones en toda la conversación
- Permanente durante la sesión: no cambia entre turnos
Ejemplo:
system_prompt = """
Eres un asistente técnico especializado en Python.
- Responde en español
- Da ejemplos de código cuando sea relevante
- Si no sabes algo, dilo explícitamente
- No inventes información
"""
Qué incluir en system:
- Rol o persona del asistente
- Restricciones de comportamiento
- Formato de salida esperado
- Guardrails y casos edge
- Contexto de dominio permanente
user
Propósito: Representa el input del usuario (o del sistema que simula al usuario). Es el "prompt" que dispara cada respuesta.
Características:
- Puede haber múltiples mensajes user en una conversación (multi-turn)
- Contiene la pregunta, instrucción, o datos a procesar
- En aplicaciones programáticas, a veces el "user" lo genera tu sistema (ej: "Clasifica este ticket: ...")
- Es el input variable que cambia en cada llamada
Cuándo usar user para instrucciones adicionales:
# Si necesitas instrucciones que varían por llamada, ponlas en user
messages = [
{
"role": "system",
"content": "Eres un clasificador de sentimiento. Responde con JSON."
},
{
"role": "user",
"content": """
Clasifica el sentimiento con confidence score.
Texto: "Me encantó el producto, definitivamente lo recomiendo"
Formato: {"sentimiento": "POSITIVO|NEGATIVO|NEUTRO", "confidence": 0.0-1.0}
"""
}
]
assistant
Propósito: Representa las respuestas previas del modelo. Se usa en conversaciones multi-turn para dar contexto del historial.
Características:
- En la primera llamada, normalmente no hay mensajes assistant
- En llamadas subsiguientes, incluyes el historial completo
- El modelo "ve" sus propias respuestas anteriores y mantiene coherencia
- Puedes "prefill" el assistant para guiar la respuesta (técnica avanzada)
Prefilling con assistant (Anthropic):
# Técnica: forzar que la respuesta empiece de cierta forma
messages = [
{"role": "user", "content": "Clasifica: 'Gran producto'"},
{"role": "assistant", "content": "{"} # Fuerza a continuar con JSON
]
System Prompt como Contrato de Comportamiento
El system prompt funciona como un contrato: defines qué hará y qué no hará el modelo en toda la conversación.
Elementos de un system prompt efectivo
from openai import OpenAI
import json
client = OpenAI()
# System prompt bien estructurado como contrato
SYSTEM = """
# Rol
Eres un clasificador de tickets de soporte para una empresa de software B2B.
# Capacidades
- Clasificar tickets en exactamente una categoría
- Identificar urgencia (ALTA/MEDIA/BAJA)
- Detectar el idioma del ticket
# Restricciones
- Solo clasificas tickets; no resuelves problemas técnicos
- No das opiniones sobre la calidad del soporte
- Si el texto no parece un ticket, responde con categoria="OTRO"
# Formato de respuesta
Siempre JSON válido con esta estructura exacta:
{
"categoria": "TECNICO|BILLING|CUENTA|FEATURE_REQUEST|OTRO",
"urgencia": "ALTA|MEDIA|BAJA",
"idioma": "ES|EN|PT|FR|OTRO",
"resumen": "máximo 10 palabras"
}
# Definiciones de urgencia
- ALTA: sistema caído, pérdida de datos, imposibilidad de trabajar
- MEDIA: funcionalidad limitada, workaround disponible
- BAJA: consultas, mejoras, preguntas
"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": "No puedo acceder a la aplicación desde esta mañana y tengo una presentación en 2 horas"}
],
temperature=0
)
result = json.loads(response.choices[0].message.content)
print(result)
# {
# "categoria": "TECNICO",
# "urgencia": "ALTA",
# "idioma": "ES",
# "resumen": "Sin acceso a aplicación con presentación urgente"
# }
Multi-Turn Conversations
En conversaciones con múltiples intercambios, el historial se construye acumulando mensajes:
from openai import OpenAI
client = OpenAI()
# Simulación de una conversación multi-turn
def chat(messages: list, user_input: str) -> tuple[str, list]:
"""
Función que agrega el input del usuario, llama a la API,
y devuelve la respuesta + historial actualizado.
"""
messages.append({"role": "user", "content": user_input})
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
temperature=0.7
)
assistant_response = response.choices[0].message.content
messages.append({"role": "assistant", "content": assistant_response})
return assistant_response, messages
# Inicializar con system prompt
historial = [
{
"role": "system",
"content": "Eres un asistente de viajes. Responde en español. Sé conciso y práctico."
}
]
# Turno 1
respuesta, historial = chat(historial, "¿Qué ciudades recomiendas en Japón?")
print(f"Asistente: {respuesta}\n")
# Turno 2 — el modelo recuerda el contexto (Japón)
respuesta, historial = chat(historial, "¿Cuántos días necesito para cada una?")
print(f"Asistente: {respuesta}\n")
# Turno 3 — mantiene coherencia con la conversación
respuesta, historial = chat(historial, "¿Cuál es mejor para un primer viaje?")
print(f"Asistente: {respuesta}\n")
print(f"Total de mensajes en historial: {len(historial)}")
# Total: 7 (1 system + 3 user + 3 assistant)
Regla: Siempre incluye el historial completo (o un resumen si es muy largo) para que el modelo mantenga coherencia.
Gestión de Historial en Producción
El historial crece con cada turno. Debes manejarlo activamente:
from openai import OpenAI
client = OpenAI()
MAX_TOKENS_HISTORIAL = 4000 # Límite para el historial
SYSTEM_PROMPT = "Eres un asistente técnico de Python."
def truncar_historial(mensajes: list, max_chars: int = 8000) -> list:
"""
Mantiene el system prompt y los últimos N mensajes
para no exceder el límite de contexto.
"""
system = [m for m in mensajes if m["role"] == "system"]
conversacion = [m for m in mensajes if m["role"] != "system"]
# Calcular tamaño total
total_chars = sum(len(m["content"]) for m in conversacion)
# Si excede el límite, eliminar los más antiguos (no el system)
while total_chars > max_chars and len(conversacion) > 2:
# Eliminar el par más antiguo (user + assistant)
eliminado = conversacion.pop(0)
total_chars -= len(eliminado["content"])
return system + conversacion
# Ejemplo de uso
historial = [{"role": "system", "content": SYSTEM_PROMPT}]
# Simular múltiples turnos
for i in range(10):
historial.append({"role": "user", "content": f"Pregunta {i}: ¿Cómo funciona X?"})
historial.append({"role": "assistant", "content": f"Respuesta {i}: X funciona así..."})
# Truncar antes de cada llamada
historial = truncar_historial(historial)
print(f"Mensajes en historial (truncado): {len(historial)}")
Cómo Cada Rol Afecta el Output
| Rol | Efecto en el output | Cuándo modifica más |
|---|---|---|
| system | Define tono, restricciones, formato, persona | Siempre; es la base |
| user | Dispara la respuesta. El último user es el más determinante | Tareas específicas |
| assistant | Proporciona contexto histórico; el modelo mantiene coherencia | Conversaciones largas |
Experimento: Mismo user, diferentes system
from openai import OpenAI
client = OpenAI()
user_msg = "Explica qué es una API"
systems = {
"tecnico": "Eres un ingeniero senior. Explica de forma técnica y concisa. Usa términos como REST, HTTP, endpoints.",
"principiante": "Eres un profesor que explica a alguien sin experiencia técnica. Usa analogías del mundo real.",
"negocio": "Eres un consultor de negocios. Explica el valor de negocio, sin términos técnicos.",
}
for nombre, system in systems.items():
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user_msg}
],
temperature=0.3,
max_tokens=100
)
print(f"\n--- System: {nombre} ---")
print(response.choices[0].message.content[:200]) # Primeras 200 chars
Output esperado:
--- System: tecnico ---
Una API (Application Programming Interface) es un contrato de comunicación entre sistemas. Define endpoints HTTP que aceptan requests con parámetros específicos y devuelven respuestas estructuradas (JSON/XML)...
--- System: principiante ---
Imagina que eres en un restaurante. La carta es la lista de platos disponibles (lo que puedes pedir). El mesero es la API: toma tu pedido, va a la cocina, y te trae el resultado...
--- System: negocio ---
Una API es una "puerta de entrada" digital que permite que diferentes sistemas de software se comuniquen. Para tu empresa, significa que tu software puede conectarse con proveedores, clientes y partners automáticamente...
Diferencias por Proveedor
| Aspecto | OpenAI | Anthropic | Google Gemini |
|---|---|---|---|
| Roles disponibles | system, user, assistant | user, assistant (system como parámetro separado) | user, model (+ system instruction) |
| System prompt | En messages array con role "system" | Parámetro system separado | system_instruction separado |
| Prefilling | No oficial | Sí (último assistant message) | No oficial |
# OpenAI
client_oai = OpenAI()
response_oai = client_oai.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Responde en español."},
{"role": "user", "content": "Hello, how are you?"}
]
)
# Anthropic
import anthropic
client_ant = anthropic.Anthropic()
response_ant = client_ant.messages.create(
model="claude-3-5-sonnet-20241022",
system="Responde en español.", # System separado, no en messages
messages=[
{"role": "user", "content": "Hello, how are you?"}
],
max_tokens=200
)
Comparación: Con vs Sin System Prompt
| Aspecto | Sin system | Con system |
|---|---|---|
| Consistencia | Variable | Alta (si el system es claro) |
| Formato de output | Impredecible | Controlado |
| Restricciones | Ninguna | Explícitas y cumplidas |
| Persona | Genérica del modelo | Especializada para tu caso |
| Comportamiento en edge cases | Impredecible | Definido por guardrails |
Conexión con el Proyecto
En el Prompt Analyzer (cápsula 08) identificarás si un prompt usa roles correctamente:
- ¿Tiene system prompt cuando debería?
- ¿El system define restricciones y formato claros?
- ¿El user input está bien delimitado?
- ¿La conversación multi-turn mantiene coherencia?
Troubleshooting
Problema 1: El modelo ignora el system prompt
Causa: Algunos modelos (o versiones menos potentes) dan menos peso al system. O el user prompt es muy largo y "diluye" el system.
Solución:
# Opción A: Repite instrucciones clave en el user message
messages = [
{"role": "system", "content": "Responde SOLO en JSON. Nada más."},
{
"role": "user",
"content": """
Clasifica este ticket.
IMPORTANTE: Responde SOLO con JSON, sin texto adicional.
Ticket: "No puedo entrar a mi cuenta"
Formato: {"categoria": "...", "urgencia": "..."}
"""
}
]
Problema 2: En multi-turn, el modelo "olvida" restricciones
Causa: El historial crece y el system queda "lejos" en el contexto. Los mensajes recientes tienen más peso.
Solución:
# Reinyectar las instrucciones críticas en mensajes user periódicamente
def build_user_message(texto: str, es_primer_turno: bool) -> str:
if es_primer_turno:
return texto
# En turnos 5, 10, 15... reinyectar reglas
return f"""
[RECORDATORIO: Siempre responde en JSON. Nunca incluyas texto adicional.]
{texto}
"""
Problema 3: Anthropic no tiene "system" en messages array
Causa: Anthropic usa un parámetro system separado, no dentro del array messages.
Solución:
import anthropic
client = anthropic.Anthropic()
# ✅ Correcto para Anthropic
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
system="Eres un clasificador. Responde solo con: POSITIVO, NEGATIVO o NEUTRO.",
messages=[{"role": "user", "content": "¿Sentimiento de 'Excelente servicio'?"}],
max_tokens=10
)
# ❌ Incorrecto (system dentro de messages no funciona en Anthropic)
# messages=[{"role": "system", "content": "..."}]
Problema 4: Respuestas incoherentes en multi-turn
Causa: El historial no incluye todos los turnos o están en orden incorrecto.
Solución: Verifica que el historial esté en orden cronológico y alterno (user, assistant, user, assistant...):
# ✅ Orden correcto
messages = [
{"role": "system", "content": "..."},
{"role": "user", "content": "Pregunta 1"}, # Turno 1 user
{"role": "assistant", "content": "Respuesta 1"}, # Turno 1 assistant
{"role": "user", "content": "Pregunta 2"}, # Turno 2 user
# El modelo generará: Turno 2 assistant
]
# ❌ Incorrecto: dos user seguidos sin assistant
messages = [
{"role": "user", "content": "Pregunta 1"},
{"role": "user", "content": "Pregunta 2"}, # Error: no hay assistant entre ellos
]
Ejercicios
Ejercicio 1: Diseñar system prompt para extractor
Crea un system prompt para un sistema que extrae fechas de textos. Debe: (1) solo extraer fechas explícitas, (2) devolver JSON, (3) usar formato ISO cuando sea posible.
Ver solución
from openai import OpenAI
import json
client = OpenAI()
SYSTEM = """
# Rol
Eres un extractor de fechas a partir de texto en lenguaje natural.
# Tarea
Identificar y extraer todas las fechas explícitas en el texto.
# Reglas
- Solo fechas explícitas (no "ayer", "la semana pasada", ni fechas inferidas)
- Convertir a formato ISO YYYY-MM-DD cuando sea posible
- Si hay ambigüedad (ej: "03/04/2024" → 3 abril o 4 marzo), usa la interpretación más común en el contexto del texto
- Si no hay fechas, devuelve lista vacía
# Formato de respuesta
Siempre JSON válido, nada más:
{"fechas": ["YYYY-MM-DD", ...]}
"""
textos = [
"La reunión es el 15 de marzo de 2025 y el seguimiento el 22/03/2025",
"No tenemos fechas confirmadas aún",
"Vence el martes pero no sé cuándo"
]
for texto in textos:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": texto}
],
temperature=0
)
result = json.loads(response.choices[0].message.content)
print(f"Input: {texto}")
print(f"Output: {result}\n")
# Output esperado:
# Input: La reunión es el 15 de marzo de 2025 y el seguimiento el 22/03/2025
# Output: {'fechas': ['2025-03-15', '2025-03-22']}
#
# Input: No tenemos fechas confirmadas aún
# Output: {'fechas': []}
#
# Input: Vence el martes pero no sé cuándo
# Output: {'fechas': []}
Ejercicio 2: Multi-turn con contexto
Implementa una conversación de 3 turnos donde el usuario pregunta por una ciudad, luego por el clima, y finalmente por recomendaciones. El system debe establecer que eres un asistente de viajes que responde de forma concisa.
Ver solución
from openai import OpenAI
client = OpenAI()
SYSTEM = """
Eres un asistente de viajes experto. Responde en español.
- Respuestas concisas (máximo 3-4 oraciones)
- Solo información práctica y útil
- Si algo varía por temporada, menciona la mejor época
"""
def chat(messages, user_input):
messages.append({"role": "user", "content": user_input})
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
temperature=0.5
)
assistant_msg = response.choices[0].message.content
messages.append({"role": "assistant", "content": assistant_msg})
return assistant_msg
historial = [{"role": "system", "content": SYSTEM}]
# Turno 1: Ciudad
r = chat(historial, "¿Qué me recomiendas visitar en Tokio?")
print(f"[Turno 1] {r}\n")
# Turno 2: Clima (el modelo recuerda que hablamos de Tokio)
r = chat(historial, "¿Cuál es el mejor mes para ir?")
print(f"[Turno 2] {r}\n")
# Turno 3: Recomendaciones (mantiene contexto de Tokio + época)
r = chat(historial, "¿Qué debo llevar en la maleta?")
print(f"[Turno 3] {r}\n")
print(f"Historial total: {len(historial)} mensajes")
Ejercicio 3: Adapter pattern para OpenAI y Anthropic
Escribe una función call_llm(system, user, provider) que funcione con ambos proveedores usando la misma interfaz.
Ver solución
from openai import OpenAI
import anthropic
from typing import Literal
oai_client = OpenAI()
ant_client = anthropic.Anthropic()
def call_llm(
system: str,
user: str,
provider: Literal["openai", "anthropic"] = "openai",
temperature: float = 0,
max_tokens: int = 200
) -> str:
"""
Interfaz unificada para OpenAI y Anthropic.
Devuelve siempre el texto de la respuesta.
"""
if provider == "openai":
response = oai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user}
],
temperature=temperature,
max_tokens=max_tokens
)
return response.choices[0].message.content
elif provider == "anthropic":
response = ant_client.messages.create(
model="claude-3-5-haiku-20241022",
system=system, # Anthropic usa parámetro separado
messages=[{"role": "user", "content": user}],
temperature=temperature,
max_tokens=max_tokens
)
return response.content[0].text
else:
raise ValueError(f"Provider no soportado: {provider}")
# Mismo prompt, dos proveedores
SYSTEM = "Clasifica el sentimiento. Responde solo: POSITIVO, NEGATIVO o NEUTRO."
USER = "Este producto superó mis expectativas"
for provider in ["openai", "anthropic"]:
result = call_llm(SYSTEM, USER, provider=provider)
print(f"{provider}: {result}")
Ejercicio 4 (Avanzado): System prompt con guardrails
Diseña un system prompt para un chatbot de ayuda que: (a) responde sobre el producto, (b) rechaza preguntas off-topic, (c) detecta frustración del usuario y responde empáticamente.
Ver solución
SYSTEM_CON_GUARDRAILS = """
# Rol
Eres el asistente de soporte de TechApp, una aplicación de gestión de proyectos.
# Capacidades
- Responder preguntas sobre características de TechApp
- Guiar a usuarios en el uso de la aplicación
- Escalar problemas técnicos no resueltos
# Restricciones (Guardrails)
- Si la pregunta no está relacionada con TechApp, responde:
"Solo puedo ayudarte con preguntas sobre TechApp. ¿Hay algo específico de la app con lo que pueda ayudarte?"
- Si detectas frustración (palabras como "no funciona", "terrible", "horrible", "harto"),
empieza con: "Entiendo tu frustración. Voy a ayudarte a resolver esto."
- Nunca mentions competidores directamente
- Si no sabes algo, dí: "No tengo esa información. Te conectaré con un agente."
# Formato
- Respuestas concisas (máximo 3 párrafos)
- Usa tono amable y profesional
- Si hay pasos a seguir, usa lista numerada
"""
# Test de guardrails
test_cases = [
"¿Cómo creo un proyecto nuevo?", # Normal
"¿Cuál es la capital de Francia?", # Off-topic
"No funciona nada, esto es terrible", # Frustración
]
from openai import OpenAI
client = OpenAI()
for test in test_cases:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM_CON_GUARDRAILS},
{"role": "user", "content": test}
],
temperature=0.3
)
print(f"User: {test}")
print(f"Assistant: {response.choices[0].message.content[:150]}...\n")
Resumen
- system: Contrato de comportamiento para toda la conversación. Configura persona, restricciones, formato, guardrails.
- user: Input que dispara cada respuesta. Puede incluir instrucciones adicionales que varían por llamada.
- assistant: Historial de respuestas. Fundamental para multi-turn coherente.
- System prompt es la palanca más poderosa: mismo user message + distintos system = outputs radicalmente distintos.
- Multi-turn: Construir historial acumulativo (system + user/assistant alternados). Truncar si crece demasiado.
- Por proveedor: OpenAI incluye system en messages array; Anthropic lo tiene como parámetro separado.
- Guardrails: Restricciones explícitas en el system para comportamiento edge y seguridad.
Recursos adicionales
- OpenAI Chat Completions Guide — Documentación completa de roles y estructura de mensajes
- Anthropic Messages API — Formato de mensajes en Claude con system separado
- Anthropic System Prompts — Cómo usar system prompts en Claude efectivamente
- OpenAI Best Practices - System Messages — Estrategias para mensajes system efectivos
- tiktoken — Para calcular tokens del historial y evitar exceder límites
- Multi-turn Conversations (Anthropic) — Gestión de conversaciones multi-turn