Arquitectura y setup
"Antes de escribir la primera línea, entendé qué estás construyendo. Un chatbot es 20% modelo y 80% lo que vos armás alrededor."
Qué vas a aprender en este capítulo
Cuando chateás con Claude o ChatGPT, se siente como hablar con algo que te recuerda, que tiene personalidad, que a veces busca información o ejecuta acciones. Nada de eso vive dentro del modelo. El modelo es una función: texto entra, texto sale. Todo lo demás — la memoria, la personalidad, las acciones, el conocimiento del negocio — lo construye el programador. O sea, vos.
En este capítulo vas a:
- Ver el plano completo del chatbot que vas a construir para La Esquina (y entender cada caja del diagrama).
- Entender por qué la API es stateless y qué te obliga a hacer.
- Dejar tu entorno listo: venv, SDK, API key segura.
- Escribir tu primer programa con la API de Claude, explicado línea por línea.
- Calcular cuánto cuesta cada mensaje — porque un bot que no sabés cuánto cuesta no está listo para producción.
1.1 Qué es realmente un chatbot moderno
💡 Intuición
Pensá en la mejor empleada de La Esquina, Marta. Cuando un cliente le escribe por WhatsApp, Marta:
- Sabe quién es y cómo hablar — es cordial, conoce el negocio, no inventa precios (eso será el system prompt).
- Recuerda la conversación — si el cliente dijo "quiero 3 de queso" hace dos mensajes, no le pregunta de nuevo (el historial).
- Puede hacer cosas — consulta la pizarra de precios, anota el pedido en el cuaderno (las herramientas).
- Conoce las reglas del negocio — sabe la política de entregas y qué hacer con un reclamo (el conocimiento, que daremos vía RAG).
El LLM por sí solo no tiene ninguna de las cuatro. Es un motor de lenguaje potentísimo pero amnésico, sin identidad y sin manos. Un chatbot es ese motor más las cuatro piezas, que vos programás.
Este es el plano del sistema completo que vas a tener funcionando al final del libro. No te preocupés si todavía no entendés cada caja — para eso están los capítulos:
Mermaid
flowchart TB
U["👤 Cliente
(navegador / consola)"] -->|mensaje| APP["Tu aplicación
(Python — Flask en cap. 5)"]
subgraph APP_INT["Lo que construís VOS"]
SP["System prompt<br/>(personalidad + reglas)<br/>cap. 2"]
H["Historial de mensajes<br/>(la 'memoria')<br/>cap. 2"]
T["Herramientas<br/>consultar_menu, registrar_pedido...<br/>cap. 3"]
K["Conocimiento (RAG)<br/>políticas + FAQ indexadas<br/>cap. 4"]
end
APP --> SP
APP --> H
APP --> K
APP -->|"request: system + historial<br/>+ tools + contexto"| API["API de Claude<br/>(claude-opus-4-8)"]
API -->|"texto o tool_use"| APP
APP <-->|ejecutar| T
APP -->|respuesta| U
La flecha clave es la del medio: en cada turno, tu aplicación arma un request con TODO (system prompt, historial completo, herramientas disponibles, contexto recuperado) y la API devuelve la siguiente respuesta. La API no guarda nada entre llamadas.
📐 Fundamento
La API es stateless
Stateless significa que el servidor no guarda estado entre requests. Cada llamada a la API de Claude es independiente y autocontenida: el modelo solo "sabe" lo que viene en ese request. Cuando la respuesta sale, el servidor olvida que exististe.
Consecuencias directas para tu código:
- La memoria es tu problema. Si querés que el bot recuerde que el cliente pidió 3 pupusas de queso, tenés que guardar ese intercambio en una lista y reenviarlo completo en la siguiente llamada. El "chat con memoria" es una ilusión que tu código fabrica (capítulo 2).
- Pagás por la memoria. Cada token del historial que reenviás cuenta como entrada y se factura. Conversaciones largas = requests cada vez más caros. Por eso existen el recorte de historial (cap. 2) y el prompt caching (cap. 5).
- Escalar es fácil. Como el servidor no guarda nada, dos requests tuyos pueden caer en máquinas distintas de Anthropic sin problema. Y tu propia app puede correr en varias instancias, siempre que el historial viva en un lugar compartido.
El formato del estado que reenviás es la lista messages: una alternancia de turnos user y assistant, donde el primero siempre es user:
messages = [
{"role": "user", "content": "¿Tienen pupusas de ayote?"},
{"role": "assistant", "content": "¡Sí! La de ayote cuesta 0.85..."},
{"role": "user", "content": "Dame 3 entonces"}, # ← el turno nuevo
]
Sin las dos primeras entradas, el modelo no tiene idea de qué son "3". Esa lista es la conversación.
📜 Historia
Los chatbots de los 2000-2010 (los de "presione 1 para saldo") eran árboles de decisión escritos a mano: miles de reglas si el usuario dice X, respondé Y. Frágiles, carísimos de mantener, e inútiles ante cualquier frase no prevista. La revolución de 2022-2023 (ChatGPT, Claude) no fue inventar el chatbot — fue reemplazar el árbol de reglas por un LLM que entiende lenguaje. Lo curioso: la arquitectura alrededor (estado, acciones, conocimiento) sigue siendo trabajo de ingeniería clásica. Por eso este libro existe.
1.2 El caso de negocio: el bot de La Esquina
Doña Carmen, dueña de La Esquina, tiene tres sucursales en San Miguel y un problema: entre las 11:30 y la 1:00 el teléfono no para, y las muchachas no dan abasto anotando pedidos mientras atienden mesas. Te contrató para construir un bot que atienda el chat del sitio web. Estos son los requisitos, levantados en una reunión con ella:
| # | Requisito | ¿Cómo lo resolvemos? | Capítulo |
|---|---|---|---|
| R1 | Responder preguntas sobre el menú y precios sin inventar | Herramienta consultar_menu sobre datos reales |
3 |
| R2 | Calcular el total de un pedido con precios exactos | Herramienta calcular_pedido (el LLM no hace aritmética confiable) |
3 |
| R3 | Registrar pedidos con nombre del cliente y sucursal | Herramienta registrar_pedido |
3 |
| R4 | Saber horarios por sucursal (y si está abierta ahora) | Herramienta verificar_horario |
3 |
| R5 | Conocer políticas: entregas, pagos, reclamos, encargos grandes | RAG sobre el documento de políticas | 4 |
| R6 | Tono cordial salvadoreño, nunca grosero, no salirse del tema | System prompt | 2 |
| R7 | Recordar lo que el cliente dijo en la conversación | Historial | 2 |
| R8 | Funcionar en el navegador, rápido y fluido | Flask + streaming + SSE | 5 |
| R9 | Costar menos de $25 al mes en API | Control de costos, caching, elección de modelo | 5 |
Fijate en algo: casi ningún requisito se resuelve "con el modelo". El modelo es el mismo en todos. Lo que cambia es lo que construís alrededor. Guardá esta tabla — vamos a volver a ella al final de cada capítulo para marcar qué ya cumplimos.
1.3 Los modelos y sus precios
📐 Fundamento
Anthropic ofrece varios modelos. Difieren en capacidad, ventana de contexto (cuánto texto aceptan por request) y precio. Los precios se cobran por millón de tokens (MTok), separando entrada (lo que mandás) y salida (lo que el modelo genera):
| Modelo | ID exacto | Contexto | Entrada / MTok | Salida / MTok |
|---|---|---|---|---|
| Claude Fable 5 | claude-fable-5 |
1M | $10 | $50 |
| Claude Opus 4.8 | claude-opus-4-8 |
1M | $5 | $25 |
| Claude Sonnet 4.6 | claude-sonnet-4-6 |
1M | $3 | $15 |
| Claude Haiku 4.5 | claude-haiku-4-5 |
200K | $1 | $5 |
(Precios verificados en junio 2026 — confirmalos en la documentación oficial antes de presupuestar.)
En este libro usamos Claude Opus 4.8 (claude-opus-4-8), el modelo recomendado por defecto: el mejor balance entre capacidad y precio para un bot que tiene que entender pedidos enredados, usar herramientas con criterio y no equivocarse con el dinero de los clientes. En el capítulo 5 discutimos cuándo conviene bajar a Sonnet o Haiku.
¿Qué es un token? La unidad en la que el modelo procesa texto: fragmentos de palabras. En español, una palabra ≈ 1.3 tokens. "Quiero tres pupusas de queso con curtido" ≈ 10 tokens. No adivines conteos: la API tiene un endpoint exacto, client.messages.count_tokens(...), que usaremos más abajo. (Nunca uses tiktoken para Claude — es el tokenizador de OpenAI y cuenta mal.)
💡 Intuición
Para dimensionar: un mensaje típico del bot de La Esquina son ~500 tokens de entrada (system prompt + historial corto + pregunta) y ~150 de salida. Con Opus 4.8:
- Entrada: 500 × $5 / 1,000,000 = $0.0025
- Salida: 150 × $25 / 1,000,000 = $0.00375
- Total: ~$0.006 por mensaje. Más de 150 mensajes por dólar.
El costo no te va a doler por mensaje — te va a doler por acumulación de historial y por volumen. De eso nos ocupamos en los capítulos 2 y 5.
1.4 Setup del entorno
Vamos a dejar todo listo. Necesitás Python 3.10 o superior (python3 --version para verificar).
Paso 1: carpeta y entorno virtual
mkdir bot-la-esquina
cd bot-la-esquina
python3 -m venv .venv
source .venv/bin/activate # En Windows: .venv\Scripts\activate
El entorno virtual aísla las dependencias del proyecto. Si el prompt de tu terminal ahora muestra (.venv), vas bien.
Paso 2: instalar dependencias
pip install anthropic python-dotenv
anthropic: el SDK oficial de Python para la API de Claude.python-dotenv: para cargar la API key desde un archivo.env(ahora vas a ver por qué).
Paso 3: obtener y guardar la API key
- Creá una cuenta en la consola de Anthropic (platform.claude.com).
- Cargá un mínimo de crédito ($5 alcanza de sobra para todo este libro).
- Generá una API key. Es un string largo que empieza con
sk-ant-.... Tratala como una contraseña: quien la tenga puede gastar tu crédito.
Guardala en un archivo llamado .env en la raíz del proyecto:
# Archivo: .env
ANTHROPIC_API_KEY=sk-ant-tu-key-aqui
Y creá inmediatamente un .gitignore para que jamás llegue a un repositorio:
# Archivo: .gitignore
.env
.venv/
__pycache__/
*.pyc
conversaciones/
pedidos.json
⚠️ Trampa común
La API key en el código. El error más caro que podés cometer en este libro:
client = anthropic.Anthropic(api_key="sk-ant-abc123...") # ❌ NUNCA
Ese archivo termina en GitHub, un bot la encuentra en minutos (hay scrapers escaneando repos públicos 24/7), y tu crédito se esfuma esa misma noche. Pasa constantemente, también a gente con experiencia.
La regla es una sola: la key vive en una variable de entorno, jamás en el código ni en el repositorio. El SDK lee ANTHROPIC_API_KEY del entorno automáticamente, así que anthropic.Anthropic() sin argumentos es todo lo que necesitás. Si una key se te filtra, revocala de inmediato en la consola.
1.5 Hola, Claude — tu primera llamada, línea por línea
Creá 01_hola_claude.py:
"""Primera llamada a la API de Claude — el 'hola mundo' del libro."""
from dotenv import load_dotenv
import anthropic
load_dotenv() # carga ANTHROPIC_API_KEY desde .env al entorno
client = anthropic.Anthropic() # lee la key del entorno; NUNCA la pongas acá
respuesta = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system="Sos el asistente de La Esquina, una pupusería de San Miguel, El Salvador.",
messages=[
{"role": "user", "content": "¡Hola! ¿Qué venden ustedes?"},
],
)
print(respuesta.content[0].text)
Corrélo:
python 01_hola_claude.py
Vas a recibir algo como: "¡Hola! Bienvenido a La Esquina 😊 Somos una pupusería..." — el modelo ya adoptó el rol, solo con esa línea de system prompt.
📐 Fundamento
Desarmemos el programa pieza por pieza:
-
load_dotenv()— lee el archivo.envy vuelca sus variables al entorno del proceso. Sin esto, el SDK no encuentra la key (salvo que la hayas exportado en la terminal). -
anthropic.Anthropic()— construye el cliente. Sin argumentos, buscaANTHROPIC_API_KEYen el entorno. El cliente maneja por vos la conexión HTTPS, los reintentos automáticos ante errores temporales y la serialización JSON. -
client.messages.create(...)— la llamada al endpoint Messages, el corazón de toda la API. Sus parámetros:model— el ID exacto del modelo. Sin sufijos de fecha, tal cual la tabla de la sección 1.3.max_tokens— tope de tokens de salida. Es obligatorio y es tu freno de mano económico: el modelo nunca generará más que esto. Si la respuesta lo alcanza, queda cortada (lo detectás constop_reason, ya vemos).system— el system prompt: instrucciones de rol que el modelo trata con más autoridad que los mensajes del usuario. Acá vive la personalidad del bot (capítulo 2 entero sobre esto).messages— la conversación: lista de turnos{"role": ..., "content": ...}. Roles válidos:useryassistant, alternados, empezando poruser. En este primer programa hay un solo turno.
-
respuesta— un objetoMessagecon varios campos que importan:respuesta.content— lista de bloques de contenido. Para una respuesta de texto simple es un solo bloque con.type == "text"y.textadentro. ¿Por qué una lista y no un string? Porque en el capítulo 3 el modelo va a devolver bloquestool_usemezclados con texto — el formato ya está preparado para eso.respuesta.stop_reason— por qué terminó de generar:"end_turn"(terminó naturalmente),"max_tokens"(lo cortó tu tope — la respuesta está incompleta),"tool_use"(quiere ejecutar una herramienta, cap. 3) o"refusal"(declinó responder por seguridad).respuesta.usage— el medidor de la luz:input_tokensyoutput_tokensconsumidos en esta llamada. Lo usamos ya mismo para calcular costos.
⚠️ Trampa común
Parámetros de muestreo que ya no existen. Si venís de tutoriales viejos (o de la API de OpenAI), vas a querer pasar temperature, top_p o top_k para controlar la "creatividad". En Claude Opus 4.8 esos parámetros fueron eliminados y devuelven error 400. El comportamiento se guía con el prompt, no con perillas de muestreo. Tampoco existe el prefill (pre-llenar el turno del assistant para forzar el formato): si necesitás controlar el formato de salida, pedilo en el system prompt. Y si algún día necesitás razonamiento extendido para problemas difíciles, el parámetro es thinking={"type": "adaptive"} — no lo vamos a necesitar para el bot.
1.6 ¿Cuánto costó ese mensaje?
Un bot en producción sin medición de costos es un cheque en blanco. Vamos a construir desde el día uno el hábito de medir. Creá 02_costo.py:
"""Calcular el costo exacto de cada llamada usando response.usage."""
from dotenv import load_dotenv
import anthropic
load_dotenv()
client = anthropic.Anthropic()
# Precios de claude-opus-4-8 por MILLON de tokens (junio 2026)
PRECIO_ENTRADA = 5.00 # USD por 1M tokens de entrada
PRECIO_SALIDA = 25.00 # USD por 1M tokens de salida
def costo_usd(usage) -> float:
"""Convierte el usage de una respuesta en dolares."""
entrada = usage.input_tokens * PRECIO_ENTRADA / 1_000_000
salida = usage.output_tokens * PRECIO_SALIDA / 1_000_000
return entrada + salida
respuesta = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system="Sos el asistente de La Esquina, una pupusería de San Miguel.",
messages=[
{"role": "user", "content": "¿Qué es una pupusa? Explicámelo en 2 oraciones."},
],
)
print(respuesta.content[0].text)
print("-" * 50)
print(f"stop_reason: {respuesta.stop_reason}")
print(f"Tokens entrada: {respuesta.usage.input_tokens}")
print(f"Tokens salida: {respuesta.usage.output_tokens}")
print(f"Costo: {costo_usd(respuesta.usage):.6f} USD")
Salida típica:
Una pupusa es una tortilla gruesa de maíz rellena de queso, chicharrón o
frijoles, asada en comal. Es el platillo nacional de El Salvador y se sirve
con curtido y salsa de tomate.
--------------------------------------------------
stop_reason: end_turn
Tokens entrada: 41
Tokens salida: 68
Costo: 0.001905 USD
Menos de un quinto de centavo. Pero ese número va a crecer cuando el historial crezca — y por eso lo medimos siempre.
🛠️ En la práctica
Contar tokens ANTES de mandar. A veces querés saber el tamaño de un prompt sin pagar por una respuesta (por ejemplo, para decidir si recortar el historial). El endpoint count_tokens cuenta gratis:
conteo = client.messages.count_tokens(
model="claude-opus-4-8",
system="Sos el asistente de La Esquina, una pupusería de San Miguel.",
messages=[{"role": "user", "content": "¿Qué es una pupusa?"}],
)
print(conteo.input_tokens) # p. ej. 38
Dos reglas de oro del equipo que mantiene bots en producción:
- Logueá
usageen cada llamada desde el primer día. Cuando el costo mensual te sorprenda (te va a sorprender), vas a tener los datos para saber por qué. - El conteo es por modelo. El mismo texto puede tokenizar distinto en modelos distintos. Contá siempre con el modelo que vas a usar.
1.7 Manejo de errores desde el día uno
La red falla, los servidores se saturan, los límites de tasa existen. El SDK trae excepciones tipadas y reintenta automáticamente los errores temporales (429 y 5xx) con backoff exponencial — pero tu código igual debe estar preparado para cuando los reintentos se agotan. Creá 03_errores.py:
"""Llamada con manejo de errores tipado — la plantilla que usaremos siempre."""
from dotenv import load_dotenv
import anthropic
load_dotenv()
client = anthropic.Anthropic()
def preguntar(texto: str) -> str:
"""Hace una pregunta al modelo y devuelve el texto, manejando errores."""
try:
respuesta = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system="Sos el asistente de La Esquina, pupusería de San Miguel.",
messages=[{"role": "user", "content": texto}],
)
return respuesta.content[0].text
except anthropic.RateLimitError:
# 429: demasiados requests. El SDK ya reintento; si llegamos aca, esperar.
return "⏳ Estamos recibiendo muchos mensajes. Probá de nuevo en un minuto."
except anthropic.APIConnectionError:
# No se pudo conectar: red caida, DNS, timeout.
return "🔌 No hay conexión con el servidor. Revisá tu internet."
except anthropic.APIStatusError as e:
# Cualquier otro error HTTP. 529 = API sobrecargada (temporal).
if e.status_code == 529:
return "🔥 El servicio está sobrecargado. Reintentá en unos segundos."
return f"⚠️ Error del API ({e.status_code}): {e.message}"
if __name__ == "__main__":
print(preguntar("¿A qué hora abren?"))
El orden de los except importa: de lo más específico (RateLimitError) a lo más general (APIStatusError). Esta plantilla — try, excepciones tipadas, mensajes amables al usuario — la vamos a arrastrar a todos los capítulos.
Resumen visual
| Concepto | Qué es | Quién lo provee |
|---|---|---|
| LLM | Motor de lenguaje: texto entra, texto sale | Anthropic (API) |
| System prompt | Personalidad y reglas del bot | Vos (cap. 2) |
Historial (messages) |
La "memoria": lista user/assistant que reenviás completa | Vos (cap. 2) |
| Herramientas | Acciones que el bot puede pedir ejecutar | Vos (cap. 3) |
| Conocimiento (RAG) | Documentos del negocio inyectados al prompt | Vos (cap. 4) |
max_tokens |
Tope de salida — freno económico obligatorio | Vos, en cada llamada |
usage |
Tokens consumidos → costo exacto | La respuesta del API |
stop_reason |
Por qué terminó: end_turn, max_tokens, tool_use, refusal |
La respuesta del API |
Estado del proyecto — requisitos de La Esquina cumplidos: ninguno todavía 😅, pero ya tenés el entorno seguro, sabés llamar a la API, leer la respuesta y medir el costo. La base de todo lo que viene.
Ejercicios
✏️ Ejercicio 1 — Tu primer diálogo a mano
Modificá 01_hola_claude.py para que messages contenga tres turnos escritos a mano: un user que pregunta si hay pupusas de chicharrón, un assistant que responde que sí, y un user final que dice "dame 2 entonces". Corrélo. ¿La respuesta demuestra que el modelo "entendió" los turnos anteriores? ¿Qué pasa si borrás los dos primeros turnos y dejás solo "dame 2 entonces"?
✅ Solución
messages=[
{"role": "user", "content": "¿Tienen pupusas de chicharrón?"},
{"role": "assistant", "content": "¡Claro! Las de chicharrón son de las más pedidas."},
{"role": "user", "content": "Dame 2 entonces."},
]
Con los tres turnos, el modelo responde confirmando 2 pupusas de chicharrón: usó el contexto. Si dejás solo "dame 2 entonces", el modelo no sabe 2 de qué — típicamente pregunta "¿2 de qué te gustaría?". Acabás de demostrar empíricamente qué significa stateless: el modelo solo sabe lo que va en messages. La "memoria" del capítulo 2 será exactamente esto, automatizado.
✏️ Ejercicio 2 — El freno de mano
Pedile al modelo "Contame la historia de las pupusas con todo detalle" con max_tokens=50. Imprimí stop_reason y la respuesta. ¿Qué observás? ¿Por qué max_tokens es un parámetro de seguridad económica y no solo un detalle técnico?
✅ Solución
La respuesta queda cortada a media frase y stop_reason es "max_tokens" en vez de "end_turn". El modelo habría generado quizás 800 tokens; tu tope lo frenó en 50.
Es seguridad económica porque la salida es lo caro ($25/MTok vs $5/MTok en Opus 4.8 — 5× más). Sin tope, un prompt malicioso o un bug ("repetí esto 1000 veces") puede generar miles de tokens por request. En producción, siempre poné el max_tokens más bajo que tu caso de uso tolere, y revisá stop_reason para detectar respuestas truncadas.
✏️ Ejercicio 3 — Comparador de precios
Escribí comparar_costos.py: una función que reciba input_tokens y output_tokens y devuelva el costo en los cuatro modelos de la tabla 1.3. Usala para estimar el costo mensual del bot de La Esquina con: 2,000 conversaciones/mes, 8 mensajes por conversación, ~600 tokens de entrada y ~150 de salida por mensaje. ¿Cuál cumple el requisito R9 (< $25/mes)?
✅ Solución
PRECIOS = { # (entrada, salida) USD por MTok
"claude-fable-5": (10, 50),
"claude-opus-4-8": (5, 25),
"claude-sonnet-4-6": (3, 15),
"claude-haiku-4-5": (1, 5),
}
def costo(modelo, t_in, t_out):
e, s = PRECIOS[modelo]
return t_in * e / 1e6 + t_out * s / 1e6
msgs = 2000 * 8 # 16,000 mensajes/mes
for m in PRECIOS:
mensual = msgs * costo(m, 600, 150)
print(f"{m:20s} ${mensual:8.2f}/mes")
Resultados aproximados: Fable 5 → $216, Opus 4.8 → $108, Sonnet 4.6 → $64.80, Haiku 4.5 → $21.60. Solo Haiku cumple R9 a este volumen y con este tamaño de prompt. ¿Significa que hay que usar Haiku? No necesariamente: en el capítulo 5 vas a ver que el prompt caching reduce el costo de entrada ~10× en la parte cacheada, lo que cambia la cuenta por completo. Moraleja: el costo se diseña, no solo se elige.
✏️ Ejercicio 4 — Detective de errores
Provocá a propósito dos errores y anotá qué excepción lanza el SDK: (a) usá un modelo inexistente como "claude-opus-99"; (b) renombrá temporalmente tu .env y corré el programa. ¿Qué tipo de excepción aparece en cada caso? ¿Cuál de los dos reintentaría el SDK automáticamente?
✅ Solución
(a) anthropic.NotFoundError (HTTP 404) — el modelo no existe. (b) anthropic.AuthenticationError (HTTP 401) — no hay key.
Ninguno de los dos se reintenta: son errores permanentes (4xx, excepto 429); reintentar no los arregla. El SDK solo reintenta automáticamente 429 (rate limit) y errores 5xx/529 (problemas temporales del servidor). Esta distinción — errores permanentes vs temporales — es central para el manejo robusto del capítulo 5.
Para profundizar
- Documentación oficial del API — platform.claude.com/docs: la referencia del endpoint Messages, modelos vigentes y precios actualizados. Marcala en favoritos; los precios de este libro caducan, esa página no.
- SDK de Python — github.com/anthropics/anthropic-sdk-python: el README cubre todos los parámetros y las excepciones tipadas.
- Capítulo 6 del libro de IA de esta colección — LLMs y transformers: si querés entender qué pasa dentro del modelo (tokens, atención, entrenamiento), ese es el lugar.
- python-dotenv — pypi.org/project/python-dotenv: patrones para manejar configuración por entorno (dev, staging, producción).