Configuración

Idioma

Crea un Chatbot de AI con una sola API Key: De cero a producción en 30 minutos

L
LemonData
·26 de febrero de 2026·562 vistas
Crea un Chatbot de AI con una sola API Key: De cero a producción en 30 minutos

Este tutorial construye un servicio de chatbot pequeño pero listo para producción con FastAPI, streaming SSE, memoria de conversación y cambio de modelos. El objetivo no es lanzar una demo de juguete. El objetivo es llegar a un backend que realmente puedas colocar detrás de la interfaz de un producto e iterar de forma segura.

Si ya has apuntado un SDK compatible con OpenAI a LemonData, este artículo continúa desde ahí. Si aún no has realizado el cambio de la base URL, lee primero la guía de migración. Si tu principal preocupación es el modelado de peticiones y el backoff bajo carga, combina esta guía con la guía de rate limiting de API de IA.

Qué vamos a construir

El servicio terminado tiene seis partes móviles:

  1. Un endpoint /chat síncrono para pruebas de humo (smoke tests).
  2. Un endpoint /chat/stream de streaming para la UI real.
  3. Estado de la conversación identificado por conversation_id.
  4. Una lista de permitidos (allowlist) de modelos para que el frontend no pueda solicitar IDs arbitrarios.
  5. Manejo de errores que no colapse ante el primer 429.
  6. Un camino claro desde un prototipo en memoria hacia Redis o PostgreSQL.

Eso es suficiente para alimentar un bot de soporte, un asistente interno o la primera versión de un widget de chat embebido.

Instala el stack mínimo

pip install fastapi uvicorn openai pydantic redis

Puedes omitir redis para la primera pasada, pero es útil configurar el import ahora para que el camino de actualización sea obvio.

Paso 1: Comienza con un endpoint de chat pequeño y sencillo

La forma más rápida de perderse en el desarrollo de chatbots es empezar con websockets, tool use y orquestación de agentes antes de que la ruta de petición básica sea estable. Comienza con un endpoint pequeño que demuestre que tu key, base URL y enrutamiento de modelos son correctos.

from fastapi import FastAPI
from openai import OpenAI
from pydantic import BaseModel

app = FastAPI()

client = OpenAI(
    api_key="sk-lemon-xxx",
    base_url="https://api.lemondata.cc/v1"
)

class ChatRequest(BaseModel):
    message: str
    model: str = "gpt-4.1-mini"
    conversation_id: str | None = None

@app.post("/chat")
async def chat(req: ChatRequest):
    response = client.chat.completions.create(
        model=req.model,
        messages=[{"role": "user", "content": req.message}]
    )
    return {"reply": response.choices[0].message.content}

Ejecuta una prueba de humo. Si esto falla, no sigas añadiendo capas de funcionalidades encima.

Paso 2: Añade streaming porque los usuarios sienten la latencia antes de medirla

La mayoría de los productos de chatbot se sienten lentos no porque el modelo sea lento, sino porque la UI permanece en blanco hasta que llega la respuesta completa. SSE es suficiente para muchos productos de chat y tiene una carga operativa menor que los websockets.

from fastapi.responses import StreamingResponse

@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
    def generate():
        stream = client.chat.completions.create(
            model=req.model,
            messages=[{"role": "user", "content": req.message}],
            stream=True
        )
        for chunk in stream:
            delta = chunk.choices[0].delta
            if delta.content:
                yield f"data: {delta.content}\n\n"
        yield "data: [DONE]\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")

En el frontend, el cliente de navegador más simple sigue siendo suficiente:

async function sendMessage(payload) {
  const response = await fetch('/chat/stream', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const chunk = decoder.decode(value, { stream: true });
    console.log(chunk);
  }
}

Si tu producto ya utiliza un cliente de navegador y HTTP estándar, SSE mantiene la arquitectura más simple.

Paso 3: Mueve el estado de la conversación fuera del cuerpo de la petición

La primera demo de un chatbot suele mantener la transcripción completa en el navegador y enviarla en cada turno. Eso funciona para prototipos. Se vuelve complicado en el momento en que necesitas reintentos, sesiones reanudables o herramientas del lado del servidor.

Un almacenamiento en memoria está bien para empezar:

from collections import defaultdict
import uuid

conversations: dict[str, list] = defaultdict(list)
SYSTEM_PROMPT = "You are a helpful assistant. Be concise and direct."

def build_messages(conv_id: str, user_msg: str) -> list:
    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
    history = conversations[conv_id][-20:]
    messages.extend(history)
    messages.append({"role": "user", "content": user_msg})
    conversations[conv_id].append({"role": "user", "content": user_msg})
    return messages

El camino de actualización a Redis es principalmente fontanería de almacenamiento:

import json
import redis

redis_client = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)

def load_history(conv_id: str) -> list:
    raw = redis_client.get(f"chat:{conv_id}")
    return json.loads(raw) if raw else []

def save_history(conv_id: str, history: list) -> None:
    redis_client.setex(f"chat:{conv_id}", 60 * 60 * 24, json.dumps(history))

Usa Redis si las conversaciones necesitan TTL, capacidad de reanudación o despliegue multi-instancia. Usa PostgreSQL si la propia transcripción son datos del producto.

Paso 4: Trata los errores como comportamiento del producto, no solo como excepciones

Si tu chatbot está de cara al cliente, la ruta de fallo importa tanto como la ruta de éxito. A un usuario no le importa si el fallo provino del rate limiting, del saldo o de una caída del modelo. Les importa si la UI se congela.

from openai import APIConnectionError, APIError, RateLimitError

@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
    conv_id = req.conversation_id or str(uuid.uuid4())
    messages = build_messages(conv_id, req.message)

    def generate():
        full_response = []
        try:
            stream = client.chat.completions.create(
                model=req.model,
                messages=messages,
                stream=True
            )
            for chunk in stream:
                delta = chunk.choices[0].delta
                if delta.content:
                    full_response.append(delta.content)
                    yield f"data: {delta.content}\n\n"
        except RateLimitError:
            yield "data: [ERROR] The model is busy. Please retry in a few seconds.\n\n"
        except APIConnectionError:
            yield "data: [ERROR] Temporary network issue. Please retry.\n\n"
        except APIError as error:
            yield f"data: [ERROR] {error.message}\n\n"
        else:
            conversations[conv_id].append(
                {"role": "assistant", "content": "".join(full_response)}
            )
        finally:
            yield "data: [DONE]\n\n"

    return StreamingResponse(
        generate(),
        media_type="text/event-stream",
        headers={"X-Conversation-ID": conv_id}
    )

Si estás sirviendo una carga significativa, también deberías modelar las peticiones antes de que lleguen al upstream. Los patrones detallados están en la guía de rate limiting, pero la versión corta es: usa reintentos limitados, usa jitter y nunca hagas except Exception: time.sleep(1).

Paso 5: El cambio de modelo necesita una lista de permitidos, no un cuadro de texto libre

Una sola API key puede acceder a cientos de modelos. Eso no significa que tu UI deba exponer cientos de modelos. El backend debería publicar una pequeña lista de permitidos (allowlist) adaptada a tu caso de uso.

AVAILABLE_MODELS = {
    "fast": "gpt-4.1-mini",
    "balanced": "claude-sonnet-4-6",
    "reasoning": "o3",
    "budget": "deepseek-chat",
}

@app.get("/models")
async def list_models():
    return {"models": AVAILABLE_MODELS}

Esto hace tres cosas útiles:

  • evita que el frontend solicite IDs de modelos inválidos o obsoletos
  • te permite remapear un nivel (tier) más tarde sin tener que volver a desplegar cada cliente
  • te da un lugar único para aplicar controles de costes

Si tu equipo aún está decidiendo en qué proveedores estandarizarse, la comparativa de precios y la comparativa OpenRouter vs LemonData son las dos páginas que vale la pena leer antes de cerrar la allowlist.

Paso 6: Añade los detalles de producción antes de que llegue el tráfico

Un backend de chatbot alcanza el nivel de producción cuando se gestionan los detalles periféricos, no cuando la llamada de chat principal es ingeniosa.

La lista de verificación es corta:

  • añade IDs de petición para que puedas conectar los fallos del frontend con los logs del backend
  • limita la concurrencia por usuario y el tamaño de la petición
  • recorta los historiales largos antes de que disparen tu presupuesto de tokens
  • registra el modelo, la latencia, el tamaño de la entrada y el motivo de finalización (finish reason)
  • separa los mensajes de error visibles para el usuario de los detalles técnicos internos
  • prueba un modelo alternativo para saber que el fallback funciona antes de la primera caída

El recorte del historial puede ser simple:

def trim_history(messages: list, max_tokens: int = 8000) -> list:
    system = messages[0]
    history = messages[1:]
    total_chars = len(system["content"])
    trimmed = []

    for msg in reversed(history):
        msg_chars = len(msg["content"])
        if total_chars + msg_chars > max_tokens * 4:
            break
        trimmed.insert(0, msg)
        total_chars += msg_chars

    return [system] + trimmed

El punto no es llevar una contabilidad de tokens perfecta. El punto es detener las explosiones obvias de contexto.

De la demo al producto

Una vez que este backend es estable, la siguiente actualización rara vez es "más IA". Suele ser infraestructura aburrida:

  • autenticación para que un usuario no pueda leer la conversación de otro
  • persistencia para que las sesiones sobrevivan a los despliegues
  • rate limiting para que un usuario ruidoso no agote tu cuota
  • facturación o atribución de uso si el chatbot está de cara al cliente
  • resumen en segundo plano si las conversaciones necesitan memoria a largo plazo

Por eso ayuda un gateway unificado. Una vez que has realizado la migración de la base URL, los cambios de modelo dejan de ser una reescritura de la plataforma y se convierten en configuración.

Prueba de humo (Smoke Test)

uvicorn main:app --reload --port 8000

curl -N -X POST http://localhost:8000/chat/stream \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello!", "model": "gpt-4.1-mini"}'

Si puedes transmitir un turno, preservar una conversación y devolver un error limpio en un fallo forzado, tienes la base adecuada.

Estimación de costes

Crea una API key en LemonData, apunta tu SDK de OpenAI a https://api.lemondata.cc/v1 y podrás lanzar la primera versión de producción de tu chatbot sin gestionar cuentas de proveedores por separado.

Modelo Coste diario Coste mensual
GPT-4.1-mini ~$2.40 ~$72
GPT-4.1 ~$12.00 ~$360
Claude Sonnet 4.6 ~$18.00 ~$540
DeepSeek V3 ~$1.68 ~$50

Usar GPT-4.1-mini para la mayoría de las conversaciones y actualizar a Claude Sonnet 4.6 solo cuando los usuarios lo soliciten mantiene los costes por debajo de los 100 $/mes para la mayoría de las aplicaciones.


Obtén tu API key: lemondata.cc proporciona más de 300 modelos a través de un único endpoint. 1 $ de crédito gratuito para empezar a construir.

Share: