Python y LLMs en 2026: construir aplicaciones con modelos de lenguaje

Si trabajas con modelos de lenguaje y no usas Python, antes o después acabas en Python. Las SDKs oficiales de OpenAI, Anthropic, Google (Gemini) y Mistral tienen Python como primer ciudadano: ejemplos en los docs, clientes mantenidos por los propios equipos y soporte inmediato de las novedades. Las de otros lenguajes van siempre un paso por detrás.

A eso súmale que Python ya era el lenguaje de data science. NumPy, pandas y Jupyter son herramientas que los equipos de ML llevan años usando, y encajan de forma natural con el procesamiento de texto que necesita cualquier aplicación con LLMs. No hace falta cambiar de entorno ni de mentalidad.

En producción, la combinación más habitual es FastAPI como servidor. Es async por defecto, tiene validación de datos con Pydantic integrada y genera documentación OpenAPI automáticamente. Para una API que llama a un LLM y devuelve resultados, no hay nada más cómodo ahora mismo.

La API de OpenAI en Python

Instala el cliente oficial:

pip install openai

La llamada básica tiene esta forma:

from openai import OpenAI

client = OpenAI(api_key="sk-...")

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "¿Qué es Python?"}]
)

print(response.choices[0].message.content)

response.choices[0].message.content es el texto de la respuesta. El modelo, los mensajes y los parámetros van en el mismo objeto. Nada más.

Streaming con OpenAI

Para mostrar la respuesta conforme se genera, en vez de esperar a que termine:

with client.chat.completions.stream(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Explícame los decoradores de Python"}]
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

El usuario ve los tokens llegar en tiempo real, lo que mejora mucho la percepción de velocidad aunque el tiempo total sea el mismo.

La API de Anthropic en Python

Instala el cliente:

pip install anthropic

La llamada básica:

from anthropic import Anthropic

client = Anthropic(api_key="sk-ant-...")

message = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hola, ¿cómo funciona async/await en Python?"}]
)

print(message.content[0].text)

La diferencia respecto a OpenAI es que aquí max_tokens es obligatorio y la respuesta viene en message.content[0].text en vez de choices[0].message.content. Una vez que lo tienes claro, el cambio entre una SDK y otra es trivial.

Los modelos disponibles en 2026 son claude-opus-4-8 (el más capaz), claude-sonnet-4-6 (buen equilibrio entre velocidad y calidad) y claude-haiku-4-5 (el más rápido y barato para tareas sencillas).

Streaming con Anthropic

with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Escribe una función para parsear CSV en Python"}]
) as stream:
    for text in stream.text_stream:
        print(text, end="")

El patrón es casi idéntico al de OpenAI. Si ya sabes usar uno, el otro no te cuesta nada.

Tool use y function calling

Los LLMs no solo generan texto: también pueden decidir cuándo llamar a una función que tú defines. Útil para buscar en una base de datos, consultar una API externa o ejecutar código. El flujo es:

  • Le dices al modelo qué herramientas tiene disponibles y qué parámetros acepta cada una.
  • El modelo devuelve una llamada a herramienta con los argumentos que ha extraído del mensaje del usuario.
  • Tu código ejecuta la función y devuelve el resultado al modelo.
  • El modelo genera la respuesta final usando ese resultado.

Tool use en OpenAI

tools = [
    {
        "type": "function",
        "function": {
            "name": "buscar_producto",
            "description": "Busca un producto en la base de datos por nombre",
            "parameters": {
                "type": "object",
                "properties": {
                    "nombre": {"type": "string", "description": "Nombre del producto"}
                },
                "required": ["nombre"]
            }
        }
    }
]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "¿Tienes el libro 'Python Crash Course'?"}],
    tools=tools
)

# Si el modelo quiere usar una herramienta:
tool_call = response.choices[0].message.tool_calls[0]
print(tool_call.function.name)       # buscar_producto
print(tool_call.function.arguments)  # {"nombre": "Python Crash Course"}

Tool use en Anthropic

tools = [
    {
        "name": "buscar_producto",
        "description": "Busca un producto en la base de datos por nombre",
        "input_schema": {
            "type": "object",
            "properties": {
                "nombre": {"type": "string", "description": "Nombre del producto"}
            },
            "required": ["nombre"]
        }
    }
]

message = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "¿Tienes el libro 'Python Crash Course'?"}]
)

# Si el modelo quiere usar una herramienta:
tool_use = next(b for b in message.content if b.type == "tool_use")
print(tool_use.name)   # buscar_producto
print(tool_use.input)  # {"nombre": "Python Crash Course"}

La diferencia principal está en la clave del schema: OpenAI usa "parameters" y Anthropic usa "input_schema". El resto es equivalente.

Si usas Pydantic, puedes definir el schema de la herramienta directamente desde un modelo y generar el JSON Schema con MiModelo.model_json_schema(). Así no repites la definición en dos sitios y tienes validación automática del output.

Outputs estructurados

Pedir al modelo que devuelva JSON funciona, pero no garantiza que el JSON sea válido ni que tenga los campos que necesitas. Para eso hay varias opciones.

OpenAI Structured Outputs: Pasa el schema directamente en la llamada y el modelo lo respeta:

response = client.chat.completions.create(
    model="gpt-4o",
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "producto",
            "schema": {
                "type": "object",
                "properties": {
                    "nombre": {"type": "string"},
                    "precio": {"type": "number"},
                    "disponible": {"type": "boolean"}
                },
                "required": ["nombre", "precio", "disponible"]
            }
        }
    },
    messages=[{"role": "user", "content": "Dame los datos del producto 42"}]
)

Anthropic: No tiene structured outputs nativo al estilo de OpenAI, pero pidiéndolo en el system prompt con un ejemplo claro funciona bien. Luego validas con Pydantic.

La librería instructor: Envuelve cualquier SDK y devuelve objetos Pydantic directamente, sea OpenAI o Anthropic:

import instructor
from pydantic import BaseModel

class Producto(BaseModel):
    nombre: str
    precio: float
    disponible: bool

# Con OpenAI
client_patched = instructor.from_openai(OpenAI())
producto = client_patched.chat.completions.create(
    model="gpt-4o",
    response_model=Producto,
    messages=[{"role": "user", "content": "Dame los datos del producto 42"}]
)
print(producto.nombre, producto.precio)

instructor maneja los reintentos automáticamente si el modelo devuelve algo que no encaja con el modelo Pydantic. Para aplicaciones donde la estructura del output es crítica, es la opción más cómoda.

Streaming en FastAPI

Cuando integras un LLM en una API web y quieres que el cliente reciba los tokens en tiempo real, StreamingResponse de FastAPI es lo que necesitas:

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from anthropic import Anthropic

app = FastAPI()
client = Anthropic()

async def generar_stream(pregunta: str):
    with client.messages.stream(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{"role": "user", "content": pregunta}]
    ) as stream:
        for text in stream.text_stream:
            yield text

@app.get("/chat")
async def chat(q: str):
    return StreamingResponse(generar_stream(q), media_type="text/plain")

El cliente (un frontend, curl, lo que sea) recibe los tokens conforme el modelo los genera. La conexión se mantiene abierta hasta que el stream termina.

Gestión de contexto y costes

Los LLMs cobran por tokens, tanto de entrada como de salida. Mandar un contexto enorme en cada petición sale caro y además tiene límites: los modelos actuales tienen ventanas de contexto de 128k o 200k tokens, pero cuanto más llenas están, más lentas y caras son las llamadas.

Para contar tokens antes de enviar (y evitar errores por exceder el límite), OpenAI tiene tiktoken:

import tiktoken

enc = tiktoken.encoding_for_model("gpt-4o")
tokens = enc.encode("Tu texto aquí")
print(f"Tokens: {len(tokens)}")

Anthropic tiene su propio método en la SDK:

token_count = client.messages.count_tokens(
    model="claude-sonnet-4-6",
    messages=[{"role": "user", "content": "Tu texto aquí"}]
)
print(token_count.input_tokens)

Prompt caching: Tanto Anthropic como OpenAI cachean los prefijos de los mensajes que se repiten. Si tienes un system prompt largo que usas en todas las llamadas, con la primera petición se cachea y las siguientes pagan mucho menos por esa parte. En Anthropic hay que marcarlo explícitamente con "cache_control": {"type": "ephemeral"}; en OpenAI es automático.

RAG (Retrieval-Augmented Generation): En vez de meter todo el contenido en el contexto, indexas los documentos en una base de datos vectorial (pgvector, Chroma, Pinecone...) y en cada petición buscas solo los fragmentos relevantes para esa pregunta. Así el contexto que mandas al modelo es pequeño y concreto. Si quieres profundizar en esto, echa un vistazo a cómo reducir el coste de los LLMs en aplicaciones RAG con Python.

De prototipo a producción

Pasar de un script que funciona en local a una aplicación en producción requiere unos cuantos cambios que conviene tener claros desde el principio.

API keys y variables de entorno

Nunca pongas las claves en el código. Usa variables de entorno y cárgalas con python-dotenv:

from dotenv import load_dotenv
import os

load_dotenv()

openai_key = os.environ["OPENAI_API_KEY"]
anthropic_key = os.environ["ANTHROPIC_API_KEY"]

El archivo .env va en .gitignore siempre. En producción, las claves van en las variables de entorno del servidor o en un gestor de secretos.

Rate limits y reintentos

Las APIs de LLMs devuelven errores 429 (rate limit) cuando superas los límites de peticiones por minuto o de tokens por minuto. Para manejarlo bien, usa tenacity:

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=4, max=10)
)
def llamar_llm(mensaje: str) -> str:
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": mensaje}]
    )
    return response.choices[0].message.content

Con esto, si la API devuelve un 429, el código espera y reintenta automáticamente hasta tres veces con backoff exponencial.

Logging de llamadas

En producción conviene registrar al menos: qué se envía, qué se recibe, cuánto tarda la llamada y cuántos tokens se consumen. Los tokens de la respuesta te los da el objeto de respuesta (response.usage.total_tokens en OpenAI, message.usage.input_tokens + output_tokens en Anthropic).

Con esos datos puedes detectar llamadas lentas, controlar el gasto por usuario o endpoint y depurar cuando el modelo devuelve algo inesperado.

Testing sin llamadas reales

Para tests unitarios que no dependan de la API (más rápidos, sin coste y sin flakiness por red), mockea el cliente:

from unittest.mock import MagicMock, patch

def test_mi_funcion():
    mock_response = MagicMock()
    mock_response.choices[0].message.content = "Respuesta de prueba"

    with patch("openai.OpenAI") as mock_client:
        mock_client.return_value.chat.completions.create.return_value = mock_response
        resultado = mi_funcion_que_llama_al_llm("pregunta")
        assert resultado == "Respuesta de prueba"

Si quieres tests de integración que graben las respuestas reales y las reproduzcan después sin volver a llamar a la API, pytest-recording con VCR es una buena opción.

Para llevar esto más lejos y darle memoria persistente a tus agentes entre sesiones, hay una guía sobre memoria episódica para agentes IA: LLMs con contexto persistente que cubre LanceDB y PostgreSQL como backends de memoria.

Imagen: Pexels / Tara Winstead

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP