Arquitectura de sistemes LLM
Aquest document descriu com encaixar un LLM en una arquitectura de programari: quines propietats té com a component, com se serveix (Ollama, vLLM), quin estàndard d’API s’usa, quins requisits de maquinari imposen els pesos i la KV cache, i com triar el model. Per als patrons de programació, vegeu Disseny de prompts i Integració i execució.
Arquitectura d’un sistema LLM
LLMs com a components de programari
Els models de llenguatge que hem estudiat a Transformers i models de llenguatge són eines de gran capacitat, però una capacitat en aïllament no és una aplicació. Un LLM que rep un prompt i retorna text és, des del punt de vista del programari, un component, com ho és una base de dades, una cua de missatges o una API externa.
Com a component, té propietats que l’enginyer de sistemes ha de tenir presents:
- No determinista: la mateixa entrada pot produir sortides diferents en cada crida. Amb temperatura > 0, el model mostra distribucions de probabilitat, no funcions deterministes.
- Sense estat: el model no recorda res entre crides. Tota la “memòria” de la conversa ha de viatjar explícitament en cada petició: l’historial de missatges, el context recuperat, les instruccions del sistema.
- Lent i car: una crida a un LLM pot trigar entre 500 ms i diversos segons, i té un cost per token. No és l’operació adequada per a cada petició d’un sistema.
- Pot fallar de manera silenciosa: un LLM no llança una excepció si no sap la resposta: la genera igualment, amb confiança. Detectar errors requereix validació explícita de la sortida.
Aquestes propietats no fan que els LLMs siguin inferiors a altres components; simplement requereixen patrons d’enginyeria específics, igual que una base de dades requereix transaccions o una cua de missatges requereix idempotència.
On viu la crida al model en una arquitectura web
En una arquitectura frontend-backend clàssica, la crida al model viu al backend, mai al frontend directament. Hi ha tres raons principals:
- Seguretat: les credencials d’accés al model no s’han d’exposar al navegador.
- Control: el backend pot validar, enriquir i postprocessar la resposta abans d’enviar-la al client.
- Cost: el backend pot evitar crides redundants, aplicar cache i controlar el pressupost de tokens.
Usuari (navegador)
│ petició HTTP
▼
Backend
│
├── validació de l'entrada
├── recuperació de context
├── construcció del prompt
│
▼
Servei d'inferència (model)
│
▼
Backend
│
├── validació de la sortida
├── postprocessament
│
▼
Usuari (resposta)
La crida al model és un pas intern del backend, no un proxy directe. El backend és responsable de construir el prompt a partir de l’entrada de l’usuari i del context addicional, assegurar que la sortida té el format esperat, i gestionar errors i fallbacks.
Les capes d’un sistema LLM
Un sistema LLM combina diverses capes funcionals. No totes són necessàries en tots els sistemes: la inferència i el client d’inferència són sempre presents; la resta apareixen quan el sistema les necessita.
| Capa | Rol | Presència | Exemples OSS |
|---|---|---|---|
| Inferència | Servir el model i exposar una API | Sempre | Ollama, vLLM, llama.cpp |
| Client d’inferència | Fer crides al model i encapsular l’accés; pot ser un SDK estàndard o una abstracció pròpia de l’aplicació | Sempre | OpenAI SDK, Anthropic SDK, o capa pròpia |
| Routing / proxy | Abstraure múltiples proveïdors amb una API unificada | Si hi ha múltiples proveïdors | LiteLLM |
| Emmagatzematge vectorial | Indexar i cercar per similitud semàntica | Si cal RAG | Chroma, Qdrant, Weaviate |
| Orquestració | Coordinar passos, eines i flux del sistema | Si hi ha múltiples passos o eines | codi propi (bucle a l’aplicació), LangGraph, LlamaIndex |
| Observabilitat | Registrar, traçar i mesurar el comportament | Recomanat en producció | Langfuse, Helicone |
| Desplegament | Empaquetar i operar el sistema | En producció; trivial en dev | Docker Compose, Kubernetes |
L’orquestració no requereix un framework dedicat. El bucle de tool calling, la gestió d’historial o el routing entre passos es poden implementar com a codi Python normal sobre el client d’inferència; molts sistemes en producció no usen mai LangGraph ni LlamaIndex. Els frameworks d’orquestració es justifiquen quan apareix complexitat genuïna (grafs amb bifurcacions condicionals, checkpointing d’estat persistent, coordinació multi-agent), no com a punt de partida per defecte.
El client d’inferència és sempre necessari; el routing és opcional. L’estàndard de facto del sector és l’API d’OpenAI: un protocol HTTP que la majoria de serveis d’inferència implementen, tant locals (Ollama, vLLM) com comercials (OpenAI, Anthropic via LiteLLM, Google via LiteLLM). Això significa que el codi que interactua amb el model és independent del proveïdor: canviar de model local a API comercial és un detall de configuració, no un canvi de codi. Quan el sistema necessita suportar múltiples proveïdors, s’afegeix una capa de routing (LiteLLM) entre el client i els proveïdors.
El servei d’inferència
El rol del servidor de models
Un LLM no és una llibreria que s’importa en el codi de l’aplicació: és un servei que s’executa per separat i s’exposa via API. Aquesta separació és intencional: carregar els pesos d’un model a memòria és una operació costosa (pot trigar desenes de segons i ocupar desenes de GB) que no pot repetir-se a cada petició. El servidor de models fa aquesta càrrega una sola vegada i manté el model en memòria per atendre múltiples peticions.
Les responsabilitats del servidor d’inferència com a component arquitectònic són:
- Carregar i mantenir el model en memòria (GPU o CPU)
- Rebre peticions, processar-les i retornar respostes
- Gestionar la concurrència (múltiples peticions simultànies)
- Exposar una API estàndard que el backend pugui consumir
Hi ha dos patrons principals de desplegament d’un servidor d’inferència, i ambdós exposen la mateixa API estàndard:
Servidor local individual (Ollama): cada desenvolupador executa el servidor a la seva pròpia màquina. És l’opció més senzilla: una sola comanda descarrega el model i el serveix. Adequat per a desenvolupament i prototipatge.
Servidor d’inferència compartit (vLLM): una màquina amb GPU (al núvol o en un servidor de laboratori) executa el model i tot l’equip hi apunta. És el patró habitual en entorns institucionals i de producció. vLLM és la referència per a aquest cas: implementa PagedAttention (gestió eficient de la KV cache per a peticions concurrents) i batching continu (agrupa peticions de múltiples clients per maximitzar l’ús de la GPU), cosa que el fa significativament més eficient que Ollama sota càrrega.
Desenvolupament Producció / equip
───────────────── ──────────────────────────────
Màquina del dev Servidor GPU compartit
┌──────────────┐ ┌──────────────────────────┐
│ Ollama │ │ vLLM │
│ model petit │ │ model gran │
└──────┬───────┘ └────────────┬─────────────┘
│ localhost:11434 │ gpu-server:8000
▼ ▼
Backend local Backend (dev/prod)
Des del punt de vista del backend, les dues configuracions són intercanviables: la crida al model és idèntica, només canvia la URL del servidor.
L’estàndard de la interfície: l’API compatible amb OpenAI
L’estàndard de facto per comunicar-se amb un servidor de models és el protocol definit per OpenAI: una crida HTTP POST a /v1/chat/completions amb una llista de missatges i uns paràmetres de generació, que retorna el text generat. La majoria de servidors d’inferència implementen aquest protocol, tant locals com comercials.
Això permet que el codi del backend sigui independent del proveïdor. L’única diferència entre entorns és la configuració del client:
from openai import OpenAI
# Ollama en local (desenvolupament individual)
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
# vLLM en servidor compartit (laboratori / equip)
client = OpenAI(base_url="http://gpu-server:8000/v1", api_key="token-intern")
# API comercial (producció o prototipatge ràpid)
client = OpenAI(api_key="sk-...") # base_url per defecte apunta a OpenAI
La crida al model és idèntica en tots els casos:
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": "Explica en una frase què és un transformer."}]
)
print(response.choices[0].message.content)
Tasques del servidor d’inferència
Fins aquí el servidor d’inferència ha aparegut com un generador de text, però els servidors moderns serveixen més d’una família de models. Un pipeline RAG complet en necessita tres: un model generatiu que escriu la resposta, un model d’embeddings (bi-encoder) que converteix textos en vectors per indexar-los i cercar-los, i un reranker (cross-encoder) que reordena els candidats recuperats puntuant cada parella (consulta, fragment). Els dos últims són models encoder petits, no LLMs (la família que la nota de Variants modernes contraposa als generatius), però s’exposen amb el mateix mecanisme: una ruta HTTP per tasca.
| Tasca | Família de model | Ruta | Estàndard |
|---|---|---|---|
| Generació | decoder generatiu (LLM) | /v1/chat/completions, /v1/completions | OpenAI |
| Embeddings | bi-encoder | /v1/embeddings | OpenAI |
| Reranking | cross-encoder | /rerank, /v1/rerank, /v2/rerank | cap: convencions de Jina i de Cohere |
Per a la generació i els embeddings, la compatibilitat és sòlida: els servidors locals segueixen l’especificació d’OpenAI, almenys en els paràmetres bàsics, i les extensions de cada servidor (esquemes de sortida, opcions avançades) varien. El reranking no té ruta estàndard, perquè OpenAI no ofereix aquest producte: cada servidor imita l’API d’un proveïdor que sí que l’ofereix, i com que l’SDK d’OpenAI no té cap mètode de rerank, la crida es fa amb HTTP pla (httpx, requests).
Que els servidors hagin convergit en aquestes tres tasques no és casualitat: són els tres rols de model del patró RAG (vegeu Recuperació, embeddings i índexs), que és el cas d’ús que ha empès la infraestructura.
Model servit o model en procés
Imagina dues persones de l’equip que indexen documents al mateix índex vectorial des dels seus portàtils. Si cadascuna carrega el model d’embeddings dins del seu propi procés i una actualitza la versió de la llibreria, els seus vectors deixen de ser comparables amb els de l’altra i les cerques comencen a tornar resultats estranys sense que res no falli de manera visible. Un sol servidor d’embeddings al qual totes dues apunten elimina el problema d’arrel.
El principi que obre el rol del servidor de models, que el model és un servei i no una llibreria, és indiscutible per als LLMs generatius, on carregar desenes de GB per procés és inviable. Amb els models d’embeddings i els rerankers, en canvi, la tria és real: són encoders petits que corren bé en CPU i que es poden importar com a llibreria (sentence-transformers) dins del procés de l’aplicació.
La regla pràctica: prototipa en procés; serveix un model quan guanya un segon consumidor, quan no cap al maquinari del client o quan la seva versió comença a importar. Aplicat a les tres tasques: la generació es serveix gairebé sempre (maquinari); els embeddings es serveixen tan bon punt l’índex es comparteix (consistència); el reranker es queda en procés fins que l’escala justifiqui el contrari (utilització).
Abstraure múltiples proveïdors: la capa de routing
Quan un sistema necessita suportar diversos proveïdors (per fallback, per A/B testing de models, o per centralitzar el control de costos), afegir una capa de routing entre el backend i els proveïdors és el patró adequat. Aquesta capa exposa una única API estàndard cap al backend i gestiona la distribució cap als proveïdors de forma transparent.
LiteLLM és l’exemple de referència: actua com a proxy local que parla OpenAI cap al backend i tradueix les crides a qualsevol proveïdor (Anthropic, Google, servidors vLLM propis, etc.) segons la configuració.
Backend
│ API OpenAI estàndard
▼
LiteLLM (routing)
├──► Ollama (local)
├──► OpenAI API
└──► Anthropic API
Quan el sistema només usa un proveïdor, aquesta capa no és necessària: afegir-la prematurament és complexitat innecessària. Val la pena introduir-la quan el sistema necessita canviar de model sense modificar el codi del backend, o quan cal consolidar observabilitat i costos de múltiples proveïdors en un sol punt.
Requisits de maquinari
Un model amb \(N\) paràmetres en precisió FP16 ocupa aproximadament \(2N\) bytes de memòria GPU només per emmagatzemar els pesos:
| Classe de model | Paràmetres | Pesos (FP16) | Entrenament (estimat) |
|---|---|---|---|
| Encoder petit (un BERT) | 110M | ~0.2 GB | ~1 GB |
| LLM petit, executable en local | 7B | ~14 GB | ~56 GB |
| LLM gran dens, servidor amb GPU | 70B | ~140 GB | ~560 GB |
⚠️ Aquestes xifres són aproximacions dels pesos del model, no del consum real de memòria. Durant la inferència, cal afegir la KV cache (memòria que emmagatzema les keys i values calculades per a cada token generat), que creix amb la longitud de la seqüència i pot suposar diversos GB addicionals. Durant l’entrenament, els gradients, els estats de l’optimitzador i les activacions intermèdies poden multiplicar el consum per 4-6× respecte els pesos sols.
Per a més detalls sobre GPU i CUDA, vegeu la secció de càlcul eficient.
⚠️ Amb models MoE, la regla \(2N\) s’aplica als paràmetres totals, no als actius. Un model MoE que anuncia “30B actius de 200B totals” gasta per token el còmput d’un model de 30B, i és per això que és ràpid, però necessita els 200B carregats a memòria, perquè el següent token pot activar qualsevol altre expert. Per dimensionar el maquinari, doncs, compta sempre els paràmetres totals; la xifra d’actius només et diu com anirà de ràpid, no què hi cabrà.
La KV cache: els models decoder-only generen text un token a la vegada. A cada pas, el mecanisme d’atenció necessita les keys i values de tots els tokens anteriors. La KV cache evita recalcular-les: les emmagatzema i només calcula les del nou token. El cost de memòria creix linealment amb la longitud de la seqüència i amb el nombre de peticions simultànies, i per a seqüències llargues (32K-128K tokens) pot ocupar més VRAM que els propis pesos quantitzats. Ollama, llama.cpp i vLLM la implementen automàticament.
Aquesta és la memòria que fa dimensionar malament molts desplegaments, i també la raó de ser de la GQA: compartint cada parell key/value entre diversos caps d’atenció, els models actuals redueixen la KV cache diverses vegades respecte del que ocuparia amb un parell per cap. Traduït a decisions d’infraestructura: amb la mateixa GPU i el mateix model, la GQA és el que decideix si pots servir quatre converses llargues alhora o vint.
Quantització: reduir la precisió dels pesos (de FP16 a INT8 o INT4) permet executar models més grans en hardware limitat, amb una pèrdua mínima de qualitat. Els pesos d’un model de 7B en INT4 ocupen ~3.5 GB; el consum real de VRAM durant la inferència serà superior (KV cache, buffers d’activació).
Optimitzacions d’inferència
Els servidors d’inferència moderns implementen tècniques que expliquen per què vLLM es comporta molt millor que Ollama sota càrrega concurrent.
Flash Attention: reimplementació del mecanisme d’atenció que redueix el consum de VRAM, permetent processar seqüències molt més llargues amb el mateix hardware. Tots els servidors moderns l’apliquen per defecte: és transparent per al desenvolupador, però explica per què les finestres de context grans (128k+) són pràctiques avui.
Batching continu (continuous batching): en lloc d’esperar que totes les peticions d’un batch acabin per acceptar-ne de noves, el servidor afegeix noves peticions tan bon punt n’acaba una d’anterior. Elimina el temps mort de la GPU i és la raó principal per la qual vLLM serveix desenes de peticions concurrents eficientment. Ollama també n’atén unes quantes en paral·lel (per defecte, entre 1 i 4 per model segons la memòria, OLLAMA_NUM_PARALLEL) i posa la resta a la cua, però el seu rendiment sota càrrega queda molt per sota.
Prefix caching: si mil peticions comparteixen el mateix system prompt de 2.000 tokens, processar-lo mil vegades és feina repetida. El servidor guarda la KV cache dels prefixos que ja ha vist i, quan una petició nova comença igual que una d’anterior, se salta el càlcul i continua a partir d’on divergeixen. És l’optimització que més es nota a la factura i a la latència inicial, perquè els sistemes reals repeteixen molt de context: system prompts llargs, catàlegs d’eines, documents de referència. vLLM l’implementa de manera automàtica, i les APIs comercials ofereixen l’equivalent amb el nom de prompt caching (vegeu Control de costos). La conseqüència de disseny és que val la pena posar el que és estable al principi del prompt i el que canvia al final: si mous una sola paraula del començament, invalides tota la cache que ve després.
Selecció del model
Amb el servei, el maquinari i la memòria clars, queda la tria del model. És una decisió arquitectònica, no un detall d’implementació, i comença per les restriccions que imposa el sistema:
- Latència i throughput: si la resposta ha de ser interactiva, el model ha de ser prou ràpid i suportar la càrrega esperada.
- Context necessari: si la tasca processa documents llargs o històrics amplis, cal una finestra de context adequada.
- Privacitat i compliance: si les dades no poden sortir del perímetre propi, la inferència local passa a ser un requisit.
- Hardware disponible: la mida del model ha de cabre en la infraestructura que tens, inclosa la KV cache.
La regla pràctica és començar amb el model més petit que compleixi aquestes restriccions. Quines capacitats exigeix cada cas d’ús, i per on començar en cadascun, és a Capacitats i model per cas; com verificar que un model candidat les té, a Avaluar un model candidat.
Perfils de model
Els noms concrets canvien cada pocs mesos; els perfils són estables. Qualsevol model que avaluïs encaixarà aproximadament en un d’aquests quatre, i el perfil et diu quines capacitats pots donar per fetes i quines has de verificar.
✓ habitual · ○ variable, cal verificar · — poc habitual
| Perfil | Sortida estructurada | Tool use | Raonament estès | Context | Visió | Prompt caching | Local | Fine-tuning |
|---|---|---|---|---|---|---|---|---|
| Frontier (màxima capacitat, API) | ✓ | ✓ | ✓ | molt llarg | ✓ | ✓ | — | — |
| Mig (equilibri cost/qualitat, API) | ✓ | ✓ | ✓ | llarg | ✓ | ✓ | — | ○ |
| Petit / ràpid (alt volum, API) | ✓ | ✓ | ○ | mitjà | ✓ | ✓ | — | ○ |
| Open weights (autoallotjat) | ○ | ✓ | ○ | mitjà | ○ | ○ | ✓ | ✓ |
Open weights vol dir que el fabricant publica els pesos entrenats perquè qualsevol els pugui descarregar i executar. Les notes que importen per decidir:
- Sortida estructurada als models autoallotjats depèn del constrained decoding del servidor (
response_formatostructured_outputsa vLLM); sense, la fiabilitat és variable. Als models d’API sol ser nativa i fiable. - Tool use és la capacitat que més varia entre models, sobretot entre els autoallotjats: cal validar-la amb evals pròpies, amb el catàleg d’eines real.
- Prompt caching als models autoallotjats no depèn del model sinó del servidor d’inferència (prefix caching a vLLM).
- Context: el que importa no és la xifra anunciada sinó l’efectiva (vegeu Preentrenament).
API o model local
La tria entre API comercial i model local no és principalment de qualitat, sinó de privacitat, cost i control:
| Criteri | API comercial | Model local |
|---|---|---|
| Dades sensibles o regulades | Depèn del contracte del proveïdor | Les dades no surten de la infraestructura pròpia |
| Cost en volum alt | Lineal amb tokens, pot ser car | Cost fix (maquinari / núvol), zero marginal |
| Manteniment | Cap (el proveïdor actualitza el model) | Cal gestionar versions i infraestructura |
| Qualitat en tool use i sortida estructurada | Alta i consistent | Variable; cal constrained decoding i validació amb evals |
| Ús comercial | Cobert pel contracte de l’API | Depèn de la llicència dels pesos |
Open weights no vol dir open source: les llicències van des d’Apache 2.0 (ús comercial lliure) fins a llicències pròpies de cada fabricant, que poden obligar a atribució o limitar l’ús per sobre d’un llindar d’usuaris. Desplegar un model autoallotjat sense revisar-ne la llicència és un risc legal.
Punt de partida: mira primer si alguna restricció descarta una de les dues opcions. La privacitat, la regulació (RGPD, Reglament d’IA de la UE) o la política interna poden fer inviable l’API; el cost d’operació o la manca de capacitat per mantenir infraestructura poden fer inviable el model local. Si cap restricció ho decideix, l’API comercial sol ser el camí més ràpid per validar.
Mida del model
Els models grans costen més per crida però solen requerir menys reintents, produeixen menys errors d’esquema i gestionen millor els casos límit. Un model petit té sentit quan la tasca és simple i ben definida (classificació, extracció de camps fixos), quan el volum és molt alt, o com a worker dins d’un sistema multi-agent on el supervisor és el model gran.
No baixis per sota del que les evals demostren que funciona. Canviar de model gran a petit per estalviar sense mesurar l’impacte en la qualitat és un dels errors més habituals en producció.