English · Español
Teoría 01 — Arquitectura del grammar tutor: un recorrido C4¶
La arquitectura del grammar tutor es deliberadamente pequeña: un proceso (miniserve), una observabilidad detrás (Prometheus + Grafana + Tempo), un registry (MLflow + DVC) que vive aparte y solo se consulta al arrancar, y un sandbox (Fase 31 MCP) para herramientas auditadas. C4 nos da un vocabulario de cuatro niveles (sistema, contenedor, componente, código) para verla sin perdernos.
Por qué C4¶
Existen muchas notaciones para diagramas de arquitectura. La mayoría son demasiado informales ("dibuja cajas y flechas") o demasiado formales (UML 2.x con una especificación de 200 páginas). C4 (de Simon Brown) se sitúa en el punto adecuado: cuatro niveles de zoom (Sistema → Contenedor → Componente → Código), tres notaciones (cajas, flechas, etiquetas), una regla (cada flecha lleva etiqueta con la tecnología y los datos que la cruzan). Para una demo de un solo proceso como la nuestra, dos niveles bastan: System Context y Container. Añadimos un diagrama de secuencia para una petición completa para hacer visible la historia dinámica. Tres diagramas. En total.
Nivel 1 — System Context¶
En el nivel System Context, el grammar tutor es una caja con flechas hacia/desde actores externos:
┌──────────────────────────────┐
│ │
┌────────────────┐ HTTP │ lynx-cortex │
│ Learner │────────►│ grammar tutor │
│ (Borja) │◄────────│ (single Python process) │
└────────────────┘ JSON │ │
│ │
└──────┬───────────┬───────────┘
│ │
MLflow │ │ DVC remote
tracking │ (local FS in demo;
server (HTTP) │ S3/GCS in prod)
▼ ▼
┌────────────┐ ┌────────────┐
│ artifacts │ │ corpus + │
│ + run logs │ │ eval sets │
└────────────┘ └────────────┘
El sistema tiene un usuario humano (Borja, o el desconocido de la demo), dos sistemas externos (MLflow tracking, DVC remoto — ambos técnicamente internos en la demo local pero lógicamente externos), y un protocolo de entrada/salida (HTTP+JSON). Los sumideros de telemetría (Prometheus, Tempo, opcionalmente Langfuse) están en el nivel Contenedor — están dentro del límite del sistema porque la demo los levanta vía docker-compose.
Renderiza esto en docs/phase-39-capstone/diagrams/c4-context.mmd usando sintaxis flowchart de mermaid.
Nivel 2 — Container¶
En el nivel Container, "contenedor" significa un proceso que corre por separado. Para la demo tenemos:
miniserve(el propio grammar tutor) — proceso Python 3.11, FastAPI, un único event loop de asyncio. Aloja:- Los handlers HTTP (
/v1/grammar/correct,/v1/grammar/explain,/health). - El router de la Fase 38 (
src/miniserve/traffic.py) — decide qué arm del modelo sirve la petición. - El bundle del modelo (grammar tutor con LoRA de la Fase 28, cargado una vez al arrancar).
- El bucle de inferencia (clase
Serverde la Fase 33, con la KV cache de la Fase 22). - El emisor de coste (Fase 34,
src/miniobserve/cost_emitter.py). - Los spans de observabilidad (Fase 34, OpenTelemetry).
- Prometheus — hace scrape de
/metricsdeminiservecada 15s. Almacena 7 días de métricas localmente. - Grafana — un único dashboard en
http://localhost:3000/d/capstone. Lee de Prometheus + Tempo. - Tempo — recibe trazas OTLP del SDK de OpenTelemetry de
miniserve. Almacena trazas por trace_id. - Servidor de tracking de MLflow —
http://localhost:5000. Almacena artefactos del modelo bajo./mlruns/. Lo consultaminiserveal arrancar para obtener el bundle de LoRA promovido. - OTel-Collector — distribuye trazas OTLP desde
miniservehacia Tempo. Opcional: mismo camino hacia Langfuse si está habilitado. - (Opcional) Langfuse — UX de trazas LLM. Apagado por default en la demo por portabilidad.
- Sandbox MCP — no es un contenedor de larga vida. Lo lanza
miniservecomo subproceso solo cuando se despacha una llamada a tool de A13 (conjugate,lookup_irregular_verb,lookup_spanish,check_subject_verb_agreement) (Fase 31). Vive durante la llamada, luego sale.
Los contratos en cada frontera de contenedor:
| De | A | Protocolo | Datos | Ubicación del schema |
|---|---|---|---|---|
| Learner | miniserve |
HTTP POST | {sentence: str, request_id?: str} |
src/miniserve/schemas/correct.py |
miniserve |
Learner | HTTP 200 | {correction: str, per_token: [...], request_id: str, model_sha: str} |
mismo |
miniserve |
Prometheus | scrape | formato texto de Prometheus | src/miniobserve/metrics.py |
miniserve |
OTel-Collector | OTLP/gRPC | spans OTel | src/miniobserve/tracing.py |
| OTel-Collector | Tempo | OTLP | spans | (config del collector) |
miniserve |
MLflow | HTTP (solo al arrancar) | descarga de artefactos | (cliente mlflow) |
miniserve |
sandbox MCP | subproceso + JSON-RPC stdio | llamada a tool | src/miniserve/mcp_client.py |
| sandbox MCP | miniserve |
stdio del subproceso | resultado del tool | mismo |
Renderiza en docs/phase-39-capstone/diagrams/c4-container.mmd. Mermaid flowchart LR, una caja por contenedor, cada flecha etiquetada como [protocolo] forma-de-datos.
Nivel 3 — Componentes dentro de miniserve¶
miniserve es el único contenedor con estructura interna no trivial. Sus componentes (cada uno un módulo o subpaquete de Python):
miniserve/
├── handlers.py # rutas FastAPI
├── traffic.py # Fase 38: router (shadow/A/B/canary)
├── pipeline.py # tokenize → embed → forward → sample → detokenize
├── model_loader.py # cliente MLflow + verificación del SHA canónico
├── mcp_client.py # despacho de tools de la Fase 31
└── schemas/ # modelos Pydantic de request/response
Cada componente lee sus inputs del anterior y emite hacia el siguiente. El componente pipeline completo es el que recorre una frase en inglés desde su llegada hasta la respuesta. Lo documentamos en detalle en theory/02-end-to-end-data-flow.md.
La secuencia: una petición de corrección gramatical¶
El tercer diagrama (docs/phase-39-capstone/diagrams/sequence-request.mmd) es un sequenceDiagram de mermaid que muestra exactamente lo que ocurre para una petición:
Learner → miniserve.HTTP : POST /v1/grammar/correct {"sentence": "Yesterday I goed to the store"}
miniserve.HTTP → security : rate-limit check (Phase 33), body-size check, injection-filter (Phase 37)
security → traffic : router.assign(request_id) → "production" arm
traffic → pipeline : pipeline.run(sentence, arm="production")
pipeline → BPE.encode : tokenize → [42, 17, 9001, ...]
pipeline → Embedding.forward : (batch=1, seq_len=8) → (1, 8, 128)
pipeline → Mini-GPT.forward : prefill, populate KV-cache (Phase 22)
pipeline → sampler.decode : structured generation (Phase 30) for the correction template
pipeline → BPE.decode : token ids → "Yesterday I went to the store"
pipeline → cost_emitter : record per-stage wall times → cost histogram
pipeline → tracing : emit OTel spans with model_sha, request_id, latency, cost
miniserve.HTTP → Learner : 200 OK {"correction": "Yesterday I went to the store", "per_token": [...], "model_sha": "abc123...", "request_id": "..."}
Cada flecha corresponde a una Fase: Fase 33 (HTTP + rate-limit), Fase 37 (filtro de inyección), Fase 38 (router), Fase 11 (BPE), Fase 13 (embedding), Fase 17 (Mini-GPT), Fase 22 (KV-cache), Fase 30 (structured gen), Fase 34 (coste + tracing). Un diagrama de secuencia, diez Fases visibles.
Las ocho Fases que contribuyen (y las cuatro de solo lectura)¶
La ruta de la demo ejecuta activamente código de diez Fases (33, 37, 38, 11, 13, 17, 22, 30, 34, adapter LoRA de 28). Cuatro Fases más contribuyen configuración o datos sin que su código corra en la ruta de la petición:
- Fase 12 (diseño del corpus) — el corpus de verbos lo descarga
dvc pullal inicio de la demo; su hash se verifica contramanifest.json. El corpus en sí no está en la ruta de la petición (artefacto solo de entrenamiento). - Fase 18 (training loop) — el checkpoint base FP32 sobre el que se asienta el adapter LoRA se produjo aquí. Cargado en memoria al arrancar; los pesos quedan luego congelados.
- Fase 20 (eval harness) — corre después del flujo de peticiones de la demo, generando
experiments/39-end-to-end/eval-YYYY-MM-DD.json. No está en la ruta de la petición. - Fase 26 (cuantización INT8) — ruta opcional de carga de pesos descuantizados. Apagada por default; la demo corre FP32 + LoRA.
Las 26 Fases restantes aportaron fundamentos (representación numérica, álgebra lineal, cálculo, entrenamiento de BPE, etc.) cuyos resultados están horneados en los artefactos; su código no se invoca directamente en la demo. La tabla de mapeo en PHASE_39_REPORT.md hace esto totalmente explícito.
Qué contratos refuerza la arquitectura¶
- Ningún módulo src nuevo. La lista de contenedores de arriba no introduce ninguno. Cada componente vive en un
src/<module>/existente. - Sin fan-out. Un único proceso
miniservesirve la demo. Ningún pool de workers, ningún Redis, ningún Kafka. El diagrama de arquitectura tiene menos cajas que el típico de producción. Ese es el punto: la complejidad se gana, no se hereda. - La telemetría es unidireccional.
miniserveemite a Prometheus/OTel; no los consulta en tiempo de petición. (Comparar la latencia en vivo con un baseline es solo offline, en el análisis de drift de la Fase 38. La demo no cambia comportamiento en función de la telemetría.) - MLflow es de solo lectura tras el arranque. El bundle se carga una vez. La demo no hace hot-swap de modelos. (El hot-swap es un ítem de lectura de la Fase 40.)
- MCP es opt-in por petición. Una petición de corrección gramatical puede disparar el tool de auditoría (p. ej., si la petición incluye un flag explícito
audit=true). La demo demuestra esto exactamente una vez.
Escollos al dibujar los diagramas¶
- Sobre-dibujar. Resiste la tentación de mostrar cada función de Python. Los contenedores son procesos; los componentes son módulos Python mayores; lo demás es nivel
code(Nivel 4), que omitimos. - Flechas sin etiqueta. Cada flecha lleva un protocolo y una forma de datos. Una flecha sin etiqueta es pista de que el contrato no es real.
- Flechas bidireccionales. Úsalas con parsimonia. Oscurecen la dirección de la dependencia. Prefiere dos flechas unidireccionales.
- Confusión estático vs dinámico. El diagrama de Contenedor muestra qué está corriendo. El de Secuencia muestra qué ocurre para una petición. No los mezcles — responden a preguntas distintas.
- La telemetría como hub. Es tentador dibujar Grafana como un hub central. No lo es — Grafana es un visor de solo lectura para la demo. El hub es
miniserve(el único emisor en la ruta de la petición). - Olvidar el sandbox MCP. Solo se lanza a veces, pero es una frontera de seguridad que merece dibujarse explícitamente. Muéstralo punteado (ciclo de vida: spawn y muerte por llamada).
Una discrepancia trabajada: el contrato entre pipeline.py y tracing.py¶
El pipeline emite spans OTel. El módulo de tracing promete adjuntar trace_id + span_id a la línea de log estructurado. El contrato: cada línea de log emitida en la ruta de la petición incluye el trace_id correcto. Si no, el panel "Logs para esta traza" de Grafana no se rellenará; la demo fallará de forma visible.
La auditoría: el lab 01 corre la demo con el logging de trace_id habilitado y verifica vía grep que cada línea de log de una petición comparte el mismo trace_id. Si incluso una línea de log no tiene el campo, el contrato está roto y el diagrama mentía. Esto es lo que queremos decir con "los diagramas de arquitectura están testeados": la auditoría atrapa diagramas que sobre-prometen la realidad.
Qué NO cubre esta teoría¶
- Por qué FastAPI en concreto. Hecho en la Fase 33.
- Por qué OpenTelemetry en concreto. Hecho en la teoría de la Fase 34.
- Por qué MCP en concreto. Hecho en la teoría de la Fase 31.
- El entrenamiento del adapter LoRA. Hecho en la Fase 28.
- Arquitectura multi-región o HA. Fuera de alcance. Lectura de la Fase 40.
- El debate microservicios vs monolito. La demo es un monolito; defender esa elección es un ítem de reflexión de la Fase 40.
Siguiente: theory/02-end-to-end-data-flow.md — una petición, todas las capas, con conteo de bytes y el latency budget.