Skip to content

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:

  1. miniserve (el propio grammar tutor) — proceso Python 3.11, FastAPI, un único event loop de asyncio. Aloja:
  2. Los handlers HTTP (/v1/grammar/correct, /v1/grammar/explain, /health).
  3. El router de la Fase 38 (src/miniserve/traffic.py) — decide qué arm del modelo sirve la petición.
  4. El bundle del modelo (grammar tutor con LoRA de la Fase 28, cargado una vez al arrancar).
  5. El bucle de inferencia (clase Server de la Fase 33, con la KV cache de la Fase 22).
  6. El emisor de coste (Fase 34, src/miniobserve/cost_emitter.py).
  7. Los spans de observabilidad (Fase 34, OpenTelemetry).
  8. Prometheus — hace scrape de /metrics de miniserve cada 15s. Almacena 7 días de métricas localmente.
  9. Grafana — un único dashboard en http://localhost:3000/d/capstone. Lee de Prometheus + Tempo.
  10. Tempo — recibe trazas OTLP del SDK de OpenTelemetry de miniserve. Almacena trazas por trace_id.
  11. Servidor de tracking de MLflowhttp://localhost:5000. Almacena artefactos del modelo bajo ./mlruns/. Lo consulta miniserve al arrancar para obtener el bundle de LoRA promovido.
  12. OTel-Collector — distribuye trazas OTLP desde miniserve hacia Tempo. Opcional: mismo camino hacia Langfuse si está habilitado.
  13. (Opcional) Langfuse — UX de trazas LLM. Apagado por default en la demo por portabilidad.
  14. Sandbox MCPno es un contenedor de larga vida. Lo lanza miniserve como 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 pull al inicio de la demo; su hash se verifica contra manifest.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

  1. Ningún módulo src nuevo. La lista de contenedores de arriba no introduce ninguno. Cada componente vive en un src/<module>/ existente.
  2. Sin fan-out. Un único proceso miniserve sirve 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.
  3. La telemetría es unidireccional. miniserve emite 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.)
  4. 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.)
  5. 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

  1. 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.
  2. 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.
  3. Flechas bidireccionales. Úsalas con parsimonia. Oscurecen la dirección de la dependencia. Prefiere dos flechas unidireccionales.
  4. 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.
  5. 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).
  6. 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.