Arquitectures per cas d’ús
- Els nou casos d’un cop d’ull
- 1. Classificador i extractor
- 2. Assistent conversacional
- 3. Q&A sobre base de coneixement (RAG)
- 4. Generació personalitzada
- 5. Agent amb eines
- 6. Pipeline de processament en batch
- 7. Assistent conversacional amb eines
- 8. Agent amb RAG (Agentic RAG)
- 9. Sistema multi-agent orquestrat
- Com es passa d’un cas al següent
Els documents anteriors han descrit les peces: capes d’un sistema LLM, patrons de programació, eines i avaluació. Aquest document aplica aquestes peces a nou casos d’ús concrets, mostrant quines capes s’activen en cada situació i per quins motius.
Cada cas inclou: quan s’aplica el patró, arquitectura (capes actives i diagrama), entrades i sortides, decisions de disseny clau, exemples de projectes reals i un exemple mínim de codi.
Els nou casos d’un cop d’ull
Els casos estan ordenats de menys a més maquinària. Els quatre primers són crides que el codi controla de dalt a baix; a partir del cinquè, el model comença a decidir coses en temps d’execució.
| Cas | Patró d’orquestració | El que el distingeix | Patró de referència |
|---|---|---|---|
| 1. Classificador / extractor | Crida única | Sortida estructurada validada | Transformació semàntica |
| 2. Assistent conversacional | Crida única per torn | Historial de sessió i streaming | Estat, memòria i multi-torn |
| 3. Q&A sobre base de coneixement | Workflow fix | Recuperació abans de generar | RAG |
| 4. Generació personalitzada | Crida única | Perfil injectat i prompt caching | Paquet de context per crida |
| 5. Agent amb eines | Bucle d’agent | El model decideix les accions | Bucle d’ús d’eines |
| 6. Pipeline en batch | Cua de workers | Volum i cost, sense latència crítica | Patrons de comunicació |
| 7. Assistent conversacional amb eines | Bucle d’agent dins de cada torn | Sessió i eines alhora | Bucle d’ús d’eines |
| 8. Agent amb RAG | Bucle d’agent | La cerca és una eina, no un pas | Agentic RAG |
| 9. Multi-agent orquestrat | Graf d’estats | Contextos separats per especialitat | Sistemes multi-agent |
Els requisits de capacitats de model que imposa cadascun són a Selecció de model per cas d’ús.
1. Classificador i extractor
Quan s’usa
El cas d’ús més senzill: una entrada de text entra, dades estructurades surten. No hi ha historial de conversa, no cal recuperació externa ni presa de decisions dinàmica. El model fa una sola tasca: entendre l’entrada i mapar-la a un esquema conegut.
Exemples de tasca: classificar tickets de suport, extreure camps d’una factura, detectar el sentiment i els aspectes d’una ressenya, parsejar un CV.
Arquitectura
Capes actives: Inferència + Client d’inferència + Sortida estructurada.
Entrada (text)
│
▼
Backend
├── construcció del prompt (system + few-shot + entrada)
▼
Servei d'inferència
▼
Backend
├── validació d'esquema (Pydantic)
├── validació de regles de negoci
▼
Sortida (JSON estructurat)
| Capa | Activa | Notes |
|---|---|---|
| Inferència | ✓ | Model petit (3B–8B) sol ser suficient |
| Client d’inferència | ✓ | OpenAI SDK |
| Emmagatzematge vectorial | — | No cal |
| Orquestració | — | Crida única, sense bucle |
| Observabilitat | ✓ | Detectar derives de qualitat |
| Desplegament | ✓ | FastAPI + Docker |
Entrades i sortides
Entrada: text pla o markdown (document, correu, formulari). El format original del document no importa: el que arriba al model és sempre text extret. L’esquema de sortida es defineix al system prompt o via response_format.
Sortida: JSON estructurat validat per Pydantic, mai text lliure. Si la validació falla, el sistema ha de gestionar l’error explícitament (reintent o rebuig), no propagar dades malformades.
Decisions de disseny clau
Sortida estructurada: sempre usar response_format o parse() del SDK. Mai parsejar JSON de text lliure manualment: és fràgil i innecessari. Vegeu Sortida estructurada.
Few-shot al prompt: per a tasques amb casos límit o format molt específic, afegir 2–3 exemples al prompt sol ser més efectiu que instruccions en prosa.
Dos nivells de validació: l’esquema Pydantic valida l’estructura; les regles de negoci (valors en rang, camps condicionalment obligatoris, consistència entre camps) es validen per separat al backend.
Cache de respostes: si la mateixa entrada pot arribar diverses vegades, una cache a nivell de hash de l’entrada elimina crides redundants.
Model petit primer: models de 3B–8B paràmetres solen ser suficients per a classificació i extracció. Escalar a models més grans només si les evals mostren insuficiència. Vegeu Selecció del model.
Exemples de projectes
- Classificació automàtica de tickets de suport al client per categoria i urgència
- Extracció de dades de factures i albarans (proveïdor, import, data, línies)
- Parser de CVs per a sistemes de selecció de personal
- Detecció de sentiment i aspectes en ressenyes de producte
- Categorització automàtica de despeses bancàries
Exemple
from pydantic import BaseModel
from typing import Literal
class TicketClassificat(BaseModel):
categoria: Literal["facturació", "tècnic", "general", "queixes"]
urgència: Literal["alta", "mitjana", "baixa"]
resum: str
ESQUEMA = {
"name": "ticket_classificat",
"schema": TicketClassificat.model_json_schema(),
"strict": True,
}
def classificar_ticket(text: str) -> TicketClassificat:
resposta = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content":
"Classifica el ticket de suport al client. "
"Urgència alta: servei caigut o pèrdua de dades."},
{"role": "user", "content": text},
],
response_format={"type": "json_schema", "json_schema": ESQUEMA},
temperature=0,
)
return TicketClassificat.model_validate_json(resposta.choices[0].message.content)
Les dues peces van juntes i cap no substitueix l’altra: response_format restringeix el que el servidor pot generar, i model_validate_json comprova a la frontera que el que ha arribat és el que esperaves. Vegeu Sortida estructurada i validació per a les alternatives quan el servidor no admet aquesta forma.
2. Assistent conversacional
Quan s’usa
L’usuari manté un diàleg multi-torn on el context s’acumula: les respostes anteriors influeixen les següents. El sistema ha de recordar el que s’ha dit dins de la sessió i pot necessitar accés a una base de coneixement per respondre amb precisió.
Exemples de tasca: chatbot de suport intern, assistent d’incorporació de nous empleats, tutor educatiu, assistent de codi en un IDE.
Arquitectura
Capes actives: Inferència + Client d’inferència + Gestió d’historial + (opcional) Emmagatzematge vectorial.
Usuari (navegador / app)
│ petició HTTP
▼
Backend
├── recupera historial de sessió (Redis / BD)
├── (opcional) recuperació RAG si la consulta requereix context extern
├── construeix missatges [system, ...historial, user]
├── gestiona truncació si l'historial supera el límit
▼
Servei d'inferència
│ stream de tokens
▼
Backend
├── reenvia tokens al client (Server-Sent Events)
├── desa la resposta a l'historial de sessió
▼
Usuari (resposta incrementalment visible)
| Capa | Activa | Notes |
|---|---|---|
| Inferència | ✓ | Model amb bona instrucció-seguiment |
| Client d’inferència | ✓ | OpenAI SDK amb stream=True |
| Emmagatzematge vectorial | Opcional | Si cal base de coneixement |
| Orquestració | — | El bucle és la sessió de conversa |
| Observabilitat | ✓ | Traces de sessió completes |
| Desplegament | ✓ | Gestió de sessions (Redis / BD) |
Entrades i sortides
Entrada: text pla (torn actual de l’usuari) + historial de missatges serialitzat com a llista de rols i continguts (text pla o markdown) + system prompt.
Sortida: text pla o markdown en streaming (SSE al client web). L’historial actualitzat es desa al backend, no al model.
La gestió de l’historial és la peça central d’aquest patró: no viu al model, sinó al backend, i es passa explícitament a cada crida.
Decisions de disseny clau
Estratègia de memòria: la tria de com gestionar l’historial quan creix determina el comportament en converses llargues. Vegeu Estat, memòria i multi-torn.
| Estratègia | Quan usar |
|---|---|
| Finestra lliscant (últims N missatges) | Converses on el context recent és el rellevant |
| Resum periòdic | Converses llargues on cal preservar context acumulat |
| Límit de tokens explícit | Quan cal control precís del cost |
RAG opcional: si l’assistent necessita respondre preguntes sobre documentació interna, un pipeline RAG s’activa per a les consultes que ho requereixin. El context recuperat s’insereix al prompt, no substitueix l’historial.
Streaming: rellevant quan el frontend ha de mostrar la resposta mentre es genera. Si el consumidor és un altre sistema backend, afegeix complexitat sense benefici (vegeu Patrons de comunicació amb el model).
Gestió de sessions: l’historial ha de persistir entre peticions HTTP. Redis és adequat per a sessions d’alta freqüència; una base de dades relacional quan cal auditoria persistent.
Exemples de projectes
- Chatbot de suport al client amb accés a la documentació del producte
- Assistent intern d’incorporació per a nous empleats
- Tutor personalitzat per a plataformes d’aprenentatge en línia
- Assistent de codi integrat a l’entorn de desenvolupament
Exemple
MAX_MISSATGES = 20
def respondre(sessió: list[dict], entrada: str):
sessió.append({"role": "user", "content": entrada})
# truncar mantenint sempre el system prompt al principi
historial = [sessió[0]] + sessió[-MAX_MISSATGES:] if len(sessió) > MAX_MISSATGES else sessió
resposta_text = ""
for chunk in client.chat.completions.create(
model=MODEL, messages=historial, stream=True
):
delta = chunk.choices[0].delta.content or ""
resposta_text += delta
yield delta # SSE al frontend
sessió.append({"role": "assistant", "content": resposta_text})
3. Q&A sobre base de coneixement (RAG)
Quan s’usa
Els usuaris fan preguntes sobre un corpus de documents que el model no coneix: documentació interna, normativa, informes propietaris. El coneixement és privat, canvia sovint, o és massa extens per cabre al context del model.
Exemples de tasca: cerca sobre documentació tècnica interna, Q&A sobre normativa legal, navegació per papers de recerca, base de coneixement de producte per a equips de suport.
Arquitectura
Capes actives: Inferència + Client d’inferència + Model d’embeddings + Emmagatzematge vectorial + (opcional) Reranker. Vegeu Embeddings i cerca vectorial per als fonaments dels models d’embeddings i la cerca ANN.
FASE D'INDEXACIÓ (offline, una vegada o periòdicament)
──────────────────────────────────────────────────────
Documents
│
▼
Fragmentació (chunking)
│
▼
Model d'embeddings → vectors
│
▼
Base de dades vectorial (índex persistent)
FASE DE CONSULTA (online, per a cada petició)
──────────────────────────────────────────────
Consulta de l'usuari
│
▼
Model d'embeddings → vector de consulta
│
▼
Cerca híbrida (vector + BM25) a la BD vectorial
│
▼
(opcional) Reranker: millora precisió dels fragments recuperats
│
▼
Backend: construcció del prompt [system + context + consulta]
│
▼
Servei d'inferència
│
▼
Resposta fonamentada + referències a les fonts
| Capa | Activa | Notes |
|---|---|---|
| Inferència | ✓ | Cal que respecti el context injectat |
| Client d’inferència | ✓ | |
| Emmagatzematge vectorial | ✓ | Chroma (dev), Qdrant / Weaviate (prod) |
| Orquestració | — | El flux és fix, no dinàmic |
| Observabilitat | ✓ | Registrar queries i fragments recuperats |
| Desplegament | ✓ | Pipeline d’indexació + servei de consulta |
Entrades i sortides
Entrada (indexació): documents en qualsevol format (PDF, Markdown, HTML, text pla, registres de BD). Tots es converteixen a text pla abans de fragmentar-los i enviar-los al model d’embeddings, perquè el pipeline d’indexació fa l’extracció de text.
Entrada (consulta): text pla (pregunta en LN).
Sortida: text pla o markdown amb citacions de font integrades. Les referències (fitxer, secció) s’injecten com a marcadors al context recuperat (per exemple [Font: ...]) i el model les incorpora a la resposta.
Decisions de disseny clau
Estratègia de fragmentació: la qualitat de la recuperació depèn de com es tallen els documents. Fragments massa grans recuperen contingut irrellevant; massa petits perden el context necessari per entendre el contingut. Vegeu Recuperació, embeddings i índexs.
Cerca híbrida: combinar cerca vectorial (semàntica) amb BM25 (lèxica) millora el recall per a termes tècnics o noms propis que la similitud semàntica no captura bé. Qdrant i Weaviate ho suporten nativament.
Instrucció explícita al model: el system prompt ha d’indicar que el model es basi exclusivament en el context recuperat i declari quan la informació no hi és. Sense aquesta instrucció, el model combina context recuperat amb coneixement propi i pot al·lucinar.
Re-indexació: quan els documents s’actualitzen, cal actualitzar l’índex. La freqüència (per trigger, nocturnament, en temps real) determina la frescor de les respostes.
Chroma vs. Qdrant: Chroma en mode embegut és adequat per a prototipatge i corpus petits. Per a producció amb múltiples processos concurrents o corpus grans, cal un servei independent. Vegeu Recuperació, embeddings i índexs.
Exemples de projectes
- Cercador sobre documentació interna d’una empresa (wikis, manuals, procediments)
- Q&A sobre normativa legal o regulatòria per a equips de compliance
- Navigator de papers científics per a grups de recerca
- Base de coneixement de producte per a equips de suport tècnic
Exemple
def respondre_amb_rag(consulta: str) -> str:
# recuperar fragments rellevants
vec = client.embeddings.create(
model=EMBED_MODEL, input=[consulta]
).data[0].embedding
resultats = col.query(query_embeddings=[vec], n_results=4)
context = "\n\n".join(
f"[Font: {m['font']}]\n{doc}"
for doc, m in zip(resultats["documents"][0], resultats["metadatas"][0])
)
return client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content":
"Respon basant-te exclusivament en el context proporcionat. "
"Cita la font entre claudàtors. "
"Si la informació no hi és, indica-ho explícitament."},
{"role": "user", "content": f"Context:\n{context}\n\nPregunta: {consulta}"},
],
).choices[0].message.content
4. Generació personalitzada
Quan s’usa
El sistema genera dades o contingut estructurat adaptat a un perfil d’usuari concret. A diferència del classificador, que analitza una entrada existent, aquí el model construeix contingut nou a partir de les característiques de l’usuari. A diferència de l’assistent, no hi ha diàleg: el perfil entra, el resultat generat surt.
Exemples de tasca: plans d’entrenament o de dieta personalitzats, itineraris de viatge, camins d’aprenentatge adaptats, informes financers personalitzats, dades de test realistes per a un perfil definit.
Arquitectura
Capes actives: Inferència + Client d’inferència + Sortida estructurada + Prompt caching.
Base de dades (perfil d'usuari)
│
▼
Backend
├── selecció dels camps del perfil rellevants per a la generació
├── (opcional) comprovació de cache per a perfils idèntics
├── construcció del prompt [system cacheïtzat + perfil + esquema]
▼
Servei d'inferència
▼
Backend
├── validació d'esquema (Pydantic)
├── validació de regles de negoci
├── (opcional) desar resultat a cache
▼
Contingut generat (JSON estructurat)
| Capa | Activa | Notes |
|---|---|---|
| Inferència | ✓ | Pot necessitar temperatura > 0 per a varietat |
| Client d’inferència | ✓ | |
| Emmagatzematge vectorial | — | El perfil ve de la BD existent, no d’índex vectorial |
| Orquestració | — | Crida única o seqüència fixa |
| Observabilitat | ✓ | Traçar perfil + resultat per a auditoria |
| Desplegament | ✓ | Prompt caching per reduir cost |
Entrades i sortides
Entrada: JSON (perfil d’usuari seleccionat de la BD: preferències, restriccions, historial, objectius). El perfil es serialitza com a text estructurat per injectar-lo al prompt, i no cal enviar tots els camps si n’hi ha d’irrellevants per a la tasca concreta.
Sortida: JSON estructurat que ha de superar dos nivells de validació, l’esquema i les regles de negoci (vegeu les decisions de disseny).
Decisions de disseny clau
Injecció del perfil: un perfil complet pot ser molt gran. Cal seleccionar els camps rellevants per a cada tasca de generació: injectar camps irrellevants consumeix tokens sense millorar la qualitat i pot diluir l’atenció del model.
Prompt caching: el system prompt (regles de generació, esquema, exemples) és estable entre usuaris i només canvia el bloc del perfil. Marcar el system prompt perquè es guardi a la memòria cau redueix substancialment el cost per token. Vegeu Control de costos.
Dos nivells de validació: com al classificador, Pydantic valida l’estructura i les regles de negoci es validen per separat al backend. Un model pot generar un JSON estructuralment vàlid que viola una regla de negoci (restriccions contradictòries, valors fora de rang, inconsistència entre camps).
Determinisme: per a alguns casos (dades de test, informes d’auditoria) cal que el mateix perfil produeixi sempre el mateix resultat: usa temperature=0 i desa el resultat a la memòria cau. Per a casos creatius (plans de viatge, recomanacions), la variació és desitjable.
Generació en batch: si cal generar per a molts usuaris alhora (p.ex., informes mensuals), el patró del Pipeline de processament en batch s’aplica directament.
Exemples de projectes
- Generador de plans d’entrenament setmanals personalitzats (apps de fitness)
- Generador de plans de dieta basats en restriccions i objectius nutricionals
- Creador d’itineraris de viatge a partir de preferències, pressupost i durada
- Generador de camins d’aprenentatge per a plataformes educatives
- Generació de dades de test realistes per a un perfil d’usuari definit (QA)
Exemple
from pydantic import BaseModel, field_validator
class PlaSetmanal(BaseModel):
objectiu: str
sessions: list[dict] # [{dia, exercicis, durada_min}]
notes: str
@field_validator("sessions")
def durades_en_rang(cls, sessions):
for s in sessions:
if not (30 <= s["durada_min"] <= 60):
raise ValueError(f"Durada fora de rang: {s['durada_min']} min")
return sessions
def generar_pla(perfil: dict) -> PlaSetmanal:
perfil_text = (
f"Nivell: {perfil['nivell']} | "
f"Dies disponibles: {', '.join(perfil['dies'])} | "
f"Objectiu: {perfil['objectiu']} | "
f"Restriccions: {perfil.get('restriccions', 'cap')}"
)
resposta = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content":
"Ets un entrenador personal. Genera un pla d'entrenament setmanal "
"adaptat al perfil. Cada sessió ha de durar entre 30 i 60 minuts."},
{"role": "user", "content": perfil_text},
],
response_format={"type": "json_schema", "json_schema": {
"name": "pla_setmanal", "schema": PlaSetmanal.model_json_schema(), "strict": True,
}},
temperature=0.7, # aquí la variació entre crides és desitjada
)
# el `field_validator` llança ValueError si alguna durada surt del rang:
# l'esquema fixa la forma, la regla de negoci la comprova Pydantic
return PlaSetmanal.model_validate_json(resposta.choices[0].message.content)
5. Agent amb eines
Quan s’usa
La tasca requereix múltiples passos on el nombre i l’ordre depenen de la informació obtinguda durant l’execució. El model actua com a planificador i el sistema com a executor: el model decideix quines eines cridar i amb quins arguments; el codi les executa i retorna els resultats.
Exemples de tasca: agent de recerca (cerca + sintetitza + redacta), agent d’anàlisi de dades (consulta BD + explica), automatització de tasques de desenvolupament (executa tests + diagnostica + proposa correcció).
Arquitectura
Capes actives: Inferència + Client d’inferència + Eines + Bucle d’agent.
Tasca de l'usuari
│
▼
Backend (bucle d'agent, màx. N iteracions)
├── construeix missatges + definicions d'eines
▼
Servei d'inferència
├── retorna text ──────────────────────► fi (resposta final)
└── retorna tool_call(nom, arguments)
│
▼
Backend executa l'eina
(BD, API externa, sistema de fitxers, cerca, etc.)
│
▼
Resultat afegit als missatges com a rol "tool"
│
└──────────────────────────────► torna al servei d'inferència
| Capa | Activa | Notes |
|---|---|---|
| Inferència | ✓ | Cal model amb bon suport de tool use |
| Client d’inferència | ✓ | |
| Emmagatzematge vectorial | Opcional | Si una de les eines és cerca semàntica |
| Orquestració | Opcional | LangGraph per a fluxos amb bifurcacions o multi-agent |
| Observabilitat | ✓ | Transcript complet obligatori per a depuració i evals |
| Desplegament | ✓ | Límit d’iteracions i timeouts crítics |
Entrades i sortides
Entrada: text pla (descripció de la tasca) + JSON (definicions d’eines: nom, descripció, esquema d’arguments). Les definicions d’eines es passen fora del prompt: el model les rep com a metadades, no com a text del missatge.
Sortida: text pla o markdown (resposta final a l’usuari) + transcript JSON (seqüència de crides amb arguments i resultats) per a auditoria i evals. El transcript és el producte secundari però imprescindible: sense ell, els errors d’agent són opacs.
Decisions de disseny clau
Límit màxim d’iteracions: sempre obligatori. Un agent pot entrar en bucle o no convergir. Definir un màxim (p.ex., 15 iteracions) i gestionar el cas de no convergència com un error explícit.
Disseny d’eines atòmiques: cada eina fa una sola cosa. Eines amb massa responsabilitat produeixen crides ambigües i errors difícils de depurar. Vegeu Disseny i contracte d’eines.
Transcript com a unitat de depuració: el registre complet de totes les crides és la unitat d’anàlisi per a evals i diagnosi. Sense transcript, els errors d’agent són opacs. Vegeu Evals d’agents.
Human-in-the-loop per a accions destructives: si l’agent pot escriure a bases de dades, enviar correus o modificar fitxers, cal un punt de confirmació humana abans d’executar accions irreversibles.
Quan usar LangGraph: el bucle while simple és suficient per a la majoria de casos. LangGraph val la pena quan el flux té bifurcacions complexes, múltiples agents especialitzats, o condicions d’aturada que van més enllà d’un if not tool_calls. Vegeu Quan usar agents.
Exemples de projectes
- Agent d’anàlisi de dades (consulta SQL + visualització + explicació en llenguatge natural)
- Agent de recerca (cerca web + sintetitza fonts + genera informe estructurat)
- Agent de revisió de codi (executa linter + tests + suggereix correccions)
- Agent de suport tècnic (diagnostica el problema + consulta documentació + proposa solució)
Exemple
import json
# Exemple: agent d'anàlisi de vendes que consulta la BD i explica els resultats
def executar_sql(query: str) -> str:
resultats = db.execute(query).fetchall()
return json.dumps([dict(r) for r in resultats], default=str)
EINES = [
{
"type": "function",
"function": {
"name": "executar_sql",
"description": "Executa una consulta SQL de només lectura a la BD de vendes. "
"Usa-la per obtenir dades de comandes, productes i clients. "
"No usar per a INSERT, UPDATE ni DELETE.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Consulta SQL SELECT"}
},
"required": ["query"],
},
},
},
]
MAX_ITERACIONS = 15
def executar_agent(pregunta: str) -> str:
missatges = [
{"role": "system", "content":
"Ets un analista de dades de vendes. Tens accés a la BD via SQL (només lectura). "
"Executa les consultes necessàries i explica els resultats en llenguatge natural."},
{"role": "user", "content": pregunta},
]
for _ in range(MAX_ITERACIONS):
resposta = client.chat.completions.create(
model=MODEL, messages=missatges, tools=EINES
)
msg = resposta.choices[0].message
if not msg.tool_calls:
return msg.content # anàlisi completada
missatges.append(msg)
for crida in msg.tool_calls:
resultat = executar_sql(**json.loads(crida.function.arguments))
missatges.append({
"role": "tool",
"tool_call_id": crida.id,
"content": resultat,
})
return "Error: l'agent no ha convergit en el límit d'iteracions."
6. Pipeline de processament en batch
Quan s’usa
Cal processar un gran volum d’ítems (documents, registres, usuaris) sense requisits de latència en temps real. El cost per token és prioritari sobre la velocitat de resposta individual. El patró és adequat per a enriquiment massiu de dades, anotació de datasets o generació periòdica de contingut.
Exemples de tasca: classificar 100.000 documents, anotar un dataset per a entrenament d’un model propi, generar informes personalitzats per a tots els clients del mes.
Arquitectura
Capes actives: Inferència (vLLM / batch API del proveïdor) + Pool de workers + Emmagatzematge de resultats.
Dataset d'entrada (fitxer / BD)
│
▼
Productor: encua ítems
│
▼
Pool de workers (fils del procés, o una cua Redis si cal repartir-ho entre màquines)
│ els workers consumeixen ítems en paral·lel
▼
Worker
├── crida al servei d'inferència
▼
Servei d'inferència (vLLM per throughput, o batch API del proveïdor)
▼
Worker
├── validació del resultat
├── desa a BD de resultats
└── gestió d'errors (reintent o descart amb registre)
▼
Dataset de sortida (resultats processats + log d'errors)
| Capa | Activa | Notes |
|---|---|---|
| Inferència | ✓ | vLLM per throughput alt; batch API si s’usa proveïdor comercial |
| Client d’inferència | ✓ | |
| Emmagatzematge vectorial | — | Típicament no necessari |
| Orquestració | — | El pool de workers gestiona el flux i la concurrència |
| Observabilitat | ✓ | Progrés, errors per ítem, cost acumulat |
| Desplegament | ✓ | Workers escalables horitzontalment |
Entrades i sortides
Entrada: dataset d’ítems (CSV, JSON o taula de BD). Cada ítem es converteix a text pla per construir el prompt individual, típicament un document o una fila per crida.
Sortida per ítem: JSON estructurat validat per Pydantic. Sortida del batch: dataset enriquit (JSON o CSV) amb el resultat per a cada ítem + log d’errors en JSON (ítem, missatge, codi d’error) dels ítems fallits. Separar el log d’errors del dataset de resultats permet reintentar els ítems fallits sense reprocessar els completats.
Decisions de disseny clau
Batch API vs. cua pròpia: els proveïdors comercials ofereixen una API de batch que processa asíncronament a un cost per token reduït (típicament 50%). És la millor opció quan s’usa API comercial i no hi ha restriccions de temps. Per a models auto-allotjats o quan cal control fi del flux, una cua pròpia amb vLLM és l’alternativa adequada.
Idempotència: els workers han de poder reprendre processos interromputs sense duplicar resultats. Cal emmagatzemar l’estat per ítem (pendent / en procés / completat / error) i consultar-lo abans de processar.
Gestió d’errors per ítem: un error en un ítem no ha d’aturar el batch. Cal gestionar cada ítem de forma independent, registrar els errors amb el missatge original i permetre reintentar els ítems fallits per separat.
Límit de taxa: les APIs comercials tenen límits de peticions per minut. Cal implementar rate limiting als workers per no superar-los i evitar costos addicionals per respostes d’error.
Monitoratge de progrés i cost: registrar el cost acumulat i el progrés (ítems processats / total) en temps real permet detectar anomalies de cost i estimar el temps de finalització.
Exemples de projectes
- Classificació massiva de correu electrònic corporatiu per categoria i prioritat
- Anotació de datasets d’entrenament per a models propis
- Generació mensual d’informes personalitzats per a tots els clients
- Enriquiment d’un catàleg de productes amb descripcions i etiquetes generades
Exemple
# Exemple: classificació massiva de correu electrònic corporatiu
from concurrent.futures import ThreadPoolExecutor, as_completed
from pydantic import BaseModel
from typing import Literal
class CorreuClassificat(BaseModel):
categoria: Literal["facturació", "tècnic", "comercial", "spam"]
urgència: Literal["alta", "baixa"]
resum: str
ESQUEMA = {
"name": "correu_classificat",
"schema": CorreuClassificat.model_json_schema(),
"strict": True,
}
def classificar_correu(correu: dict) -> None:
"""Processa un ítem. No llança mai: un error d'un correu no pot tombar el batch."""
try:
resposta = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content":
"Classifica el correu electrònic corporatiu per categoria i urgència. "
"Urgència alta: incidències crítiques o sol·licituds de direcció."},
{"role": "user", "content": correu["cos"]},
],
response_format={"type": "json_schema", "json_schema": ESQUEMA},
temperature=0,
)
resultat = CorreuClassificat.model_validate_json(resposta.choices[0].message.content)
desar_resultat(correu["id"], resultat)
marcar_completat(correu["id"])
except Exception as exc: # noqa: BLE001 — es registra i es continua
marcar_error(correu["id"], str(exc))
def classificar_safata(correus: list[dict], workers: int = 8) -> None:
# idempotència: els que ja s'han completat en una execució anterior no es tornen a fer
pendents = [c for c in correus if estat_ítem(c["id"]) != "completat"]
# el nombre de workers s'ajusta a la concurrència que aguanta el servidor
# d'inferència, no a la que aguanta la teva màquina
with ThreadPoolExecutor(max_workers=workers) as executor:
futurs = {executor.submit(classificar_correu, c): c["id"] for c in pendents}
for fet, futur in enumerate(as_completed(futurs), start=1):
futur.result()
if fet % 100 == 0:
print(f"processats {fet}/{len(pendents)}")
Aquest esquelet cobreix el que fa falta per a volums de milers d’ítems: concurrència limitada, idempotència i errors aïllats per ítem. Quan el volum creix fins al punt que el procés ha de sobreviure a reinicis, repartir-se entre màquines o reprendre’s al mig, el següent pas és una cua de tasques de veritat (Celery o RQ amb Redis), que és el mateix disseny amb l’estat fora del procés. I si treballes contra una API comercial, comprova abans si té una Batch API: sol costar la meitat per token i t’estalvia tota aquesta maquinària.
7. Assistent conversacional amb eines
Quan s’usa
Combinació dels casos 2 i 5: un diàleg multi-torn on l’assistent, a més de generar text, pot invocar eines per obtenir dades en temps real o executar accions. El fil conductor és la conversa (iniciativa de l’usuari, context acumulat), però dins de cada torn l’assistent pot cridar eines abans de respondre.
La distinció respecte al cas 5 (agent) és important: aquí l’usuari guia el diàleg torn a torn; l’agent és autònom i resol una tasca sencera en un sol cop. La distinció respecte al cas 2 (assistent pur) és que les respostes no són només text generat: poden incloure dades reals consultades en el moment.
Exemples de tasca: bot de suport que consulta l’estat de comandes i processa devolucions, assistent d’RRHH que reserva sales i consulta dades d’empleats, assistent bancari que mostra saldos i moviments recents.
Arquitectura
Capes actives: Inferència + Client d’inferència + Gestió d’historial + Eines.
Usuari
│ missatge
▼
Backend
├── recupera historial de sessió
├── construeix [system, ...historial, user] + definicions d'eines
▼
Servei d'inferència
├── retorna text ──────────────────────────────► desa a historial, envia al client
└── retorna tool_call(nom, arguments)
│
▼
Backend executa l'eina (BD, API externa, etc.)
│
▼
Resultat afegit als missatges com a rol "tool"
│
└──► torna al servei d'inferència (dins del mateix torn)
│
▼
retorna text ──────────────────────► desa a historial, envia al client
Les crides a eines succeeixen dins d’un sol torn i són invisibles per a l’usuari, que només veu la resposta final de text. L’historial que es desa inclou els missatges d’eines per mantenir la coherència en torns posteriors, però penalitza el comptador de tokens.
| Capa | Activa | Notes |
|---|---|---|
| Inferència | ✓ | Model amb bon tool use i instrucció-seguiment |
| Client d’inferència | ✓ | |
| Emmagatzematge vectorial | Opcional | Si una de les eines és cerca sobre base de coneixement |
| Orquestració | Opcional | LangGraph si el bucle d’eines del torn té bifurcacions o cal estat persistent |
| Observabilitat | ✓ | Traces de sessió incloent totes les crides a eines |
| Desplegament | ✓ | Gestió de sessions amb eines exposades |
Entrades i sortides
Entrada: text pla (missatge del torn) + historial de missatges (que inclou missatges de rol “tool” en JSON dels torns anteriors) + system prompt + JSON (definicions d’eines).
Sortida visible a l’usuari: text pla o markdown (pot incloure dades consultades en temps real).
Sortida interna: historial actualitzat que inclou els missatges de rol “assistant” (amb tool_calls) i “tool” en JSON.
Decisions de disseny clau
Les eines s’executen dins del torn, no entre torns: totes les crides a eines per generar una resposta passen en un sol cicle de model → eines → model. L’usuari no intervé durant aquest cicle.
Gestió de l’historial amb missatges d’eines: els missatges de tipus tool i assistant amb tool_calls han de formar part de l’historial per mantenir la coherència. Això augmenta el consum de tokens, cosa que cal tenir en compte en truncar l’historial.
Autorització per eines: les eines actuen en nom de l’usuari autenticat. La lògica d’autorització (aquest usuari pot consultar aquesta comanda?) ha de viure a l’eina, no al prompt. No confiar que el model apliqui restriccions d’accés.
Streaming amb eines: si cal fer streaming de la resposta final, cal retenir-la en memòria intermèdia fins que el model acabi de cridar totes les eines, executar-les, i llavors fer stream de la resposta final. No es pot fer stream de la primera crida si encara hi ha tool calls pendents.
Eines de lectura vs. d’escriptura: eines que modifiquen estat (processar devolució, enviar correu) han de tenir confirmació explícita de l’usuari al prompt o una pantalla de confirmació al frontend, perquè el model pot cridar-les per error.
Exemples de projectes
- Bot de suport de comerç electrònic (consulta comandes, inicia devolucions, comprova estoc)
- Assistent intern d’RRHH (consulta vacances, reserva sales, gestiona sol·licituds)
- Assistent bancari (saldos, moviments recents, transferències amb confirmació)
- Assistent de gestió de projectes (crea tasques, consulta estat, assigna membres)
Exemple
El bucle d’eines és el del cas 5; la novetat és embolcallar-lo en la gestió de sessió:
def respondre_torn(sessió: list[dict], entrada: str) -> str:
sessió.append({"role": "user", "content": entrada})
missatges_torn = gestionar_historial(sessió) # truncació si cal
# dins del torn s'executa el bucle d'eines del cas 5, invisible per a
# l'usuari: model → tool_calls → executar → afegir com a rol "tool" → model
text_final = bucle_eines(missatges_torn, EINES)
sessió.append({"role": "assistant", "content": text_final})
return text_final
8. Agent amb RAG (Agentic RAG)
Quan s’usa
Un agent que exposa la base de coneixement com una eina de cerca que pot cridar quan decideix que li cal. A diferència del cas 3 (RAG pur), la recuperació no és un pas fix del pipeline: l’agent decideix si cerca, quan cerca i amb quina query, i pot combinar els resultats amb altres eines.
La diferència pràctica és significativa: en RAG pur, sempre es recuperen fragments per a cada consulta, amb una query derivada directament de la pregunta de l’usuari. En agentic RAG, l’agent pot reformular la query basant-se en el que ha trobat en una cerca anterior, fer múltiples cerques sobre aspectes diferents, o decidir que no cal cercar si ja té prou informació al context.
Exemples de tasca: assistent de recerca que cerca papers i consulta bases de citacions, agent legal que combina cerca de jurisprudència amb consulta de normativa específica, agent de suport tècnic que cerca documentació i comprova l’estat del sistema en paral·lel.
Arquitectura
Capes actives: Inferència + Client d’inferència + Emmagatzematge vectorial + Eines (inclosa cerca) + Bucle d’agent.
Tasca de l'usuari
│
▼
Backend (bucle d'agent, màx. N iteracions)
├── eines: [cercar_docs(query), consultar_bd(sql), ...]
▼
Servei d'inferència
├── retorna text ─────────────────────────────► resposta final
└── retorna tool_call
│
├── cercar_docs(query) ──► BD vectorial ──► fragments + fonts
├── consultar_bd(sql) ──► base de dades ──► dades estructurades
└── ...
│
▼
Resultat afegit als missatges com a rol "tool"
│
└──────────────────────────────────────► torna al servei d'inferència
| Capa | Activa | Notes |
|---|---|---|
| Inferència | ✓ | Model amb bon tool use |
| Client d’inferència | ✓ | |
| Emmagatzematge vectorial | ✓ | Exposat com a eina, no com a pas fix del pipeline |
| Orquestració | Opcional | LangGraph si cal combinar múltiples agents especialitzats |
| Observabilitat | ✓ | Registrar queries de cerca i fragments recuperats per torn |
| Desplegament | ✓ | BD vectorial com a servei independent (Qdrant / Weaviate) |
Entrades i sortides
Entrada: text pla (tasca) + corpus indexat accessible via eina de cerca (la BD vectorial no s’injecta directament al prompt: l’agent la consulta quan crida l’eina) + JSON (definicions d’eines).
Sortida: text pla o markdown amb citacions de font integrades (perquè la funció de cerca les inclou explícitament al resultat de l’eina) + transcript JSON (queries fetes, fragments recuperats, altres crides) per a auditoria.
Decisions de disseny clau
La cerca com a eina, no com a pas: el pipeline de recuperació s’embolcalla en una funció amb nom i descripció clars. La descripció ha d’especificar quan usar-la i quan no, perquè el model decideix si cridar-la.
L’agent reformula les queries: a diferència del RAG pur, l’agent pot fer una primera cerca, llegir els resultats, i fer una segona cerca més específica basada en el que ha trobat. Això millora la qualitat de recuperació en tasques complexes, però incrementa el cost (múltiples crides d’embeddings i al model).
Cites explícites als resultats de l’eina: la funció de cerca ha de retornar els fragments amb les referències de font (fitxer, secció, pàgina). L’agent pot incloure-les a la resposta final, cosa que el RAG pur fa difícilment de forma consistent.
Pressupost de cerques: definir un màxim de cerques per tasca (o un límit d’iteracions global) per evitar bucles de cerca redundants.
Indexació independent del flux d’agent: la BD vectorial és un servei separat. La seva actualització (re-indexació quan els documents canvien) és independent del desplegament de l’agent.
Exemples de projectes
- Agent d’investigació que cerca papers, extreu dades clau i redacta resums comparatius
- Agent legal que combina cerca de jurisprudència amb consulta d’articles de llei concrets
- Agent de suport tècnic avançat que cerca documentació i pot obrir tickets o comprovar logs
- Agent de due diligence que analitza documents financers i contrasta amb dades de mercat
Exemple
def cercar_docs(query: str, n: int = 4) -> str:
vec = client.embeddings.create(
model=EMBED_MODEL, input=[query]
).data[0].embedding
resultats = col.query(query_embeddings=[vec], n_results=n)
return "\n\n".join(
f"[Font: {m['font']}]\n{doc}"
for doc, m in zip(resultats["documents"][0], resultats["metadatas"][0])
)
EINES = [
{
"type": "function",
"function": {
"name": "cercar_docs",
"description": "Cerca fragments rellevants a la base de coneixement interna. "
"Usa-la quan necessitis informació del corpus. "
"Pots cridar-la múltiples vegades amb queries diferents.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Consulta de cerca en llenguatge natural"}
},
"required": ["query"],
},
},
},
# ... altres eines (consultar_bd, comprovar_estat, etc.)
]
El bucle és exactament l’executar_agent del cas 5: només canvien el catàleg d’eines i el system prompt, que aquí demana usar cercar_docs abans de respondre i citar les fonts entre claudàtors.
9. Sistema multi-agent orquestrat
Quan s’usa
Quan la tasca requereix múltiples agents especialitzats que col·laboren, i el flux entre ells té bifurcacions condicionals o passos paral·lels que un bucle while simple no pot modelar. La diferència respecte al cas 5 (agent amb eines) no és el nombre de crides al model, sinó que cada subtasca requereix un context i una especialització diferents, i el flux té lògica que va més enllà d’un if not tool_calls.
Dos patrons concrets justifiquen l’orquestració:
Supervisor + especialistes: un agent supervisor classifica la petició i la delega a l’agent especialitzat corresponent. El supervisor és un node del graf; cada especialista és un altre node; les arestes condicionals defineixen el routing. Exemple: sistema de suport al client amb agents de facturació, tècnic i comercial, on el supervisor llegeix el ticket i decideix qui el gestiona.
Agents en paral·lel + síntesi: múltiples agents investiguen aspectes independents simultàniament i un agent final sintetitza els resultats. Exemple: pipeline de due diligence que llança en paral·lel un agent de risc financer, un de risc legal i un de reputació, i després un agent redactor consolida l’informe final.
Exemples de tasca: routing de tickets de suport a agents especialitzats, pipeline de generació de contingut (planificador → redactor → revisor), due diligence automatitzat amb agents en paral·lel, sistemes de codi amb agent de disseny + agent d’implementació + agent de revisió.
Arquitectura
Capes actives: Inferència + Client d’inferència + Orquestració (LangGraph) + (opcional) Eines per als agents especialistes.
Petició
│
▼
Graf LangGraph (estat compartit entre nodes)
│
▼
Node supervisor (LLM): classifica i decideix routing
│
├── categoria = "facturació" ──► Node agent facturació (LLM) ──► END
├── categoria = "tècnic" ──► Node agent tècnic (LLM) ──► END
└── categoria = "comercial" ──► Node agent comercial (LLM) ──► END
Variant amb agents en paral·lel:
Node disparador
├──► Node agent risc financer (LLM) ─┐
├──► Node agent risc legal (LLM) ─┼──► Node agent síntesi (LLM) ──► END
└──► Node agent risc reputació (LLM) ─┘
| Capa | Activa | Notes |
|---|---|---|
| Inferència | ✓ | Cada node és una crida independent al model |
| Client d’inferència | ✓ | |
| Emmagatzematge vectorial | Opcional | Si els agents especialistes fan cerca |
| Orquestració | ✓ | LangGraph: justificació principal d’aquest cas |
| Observabilitat | ✓ | Traces per node + estat del graf en cada pas |
| Desplegament | ✓ | LangGraph Server per a desplegament persistent |
Entrades i sortides
Entrada: text pla (petició) i, si el flux es modela com un graf d’estats, l’estat inicial (un TypedDict que cada node pot llegir i modificar). L’estat és el mecanisme de comunicació entre nodes: cada node llegeix el que necessita i escriu el que produeix.
Sortida: text pla o markdown (resultat del node final) + historial d’execució del graf en JSON (seqüència de nodes executats, estat en cada pas) per a auditoria. Si s’activa el checkpointing, l’estat persisteix a la BD entre execucions.
Decisions de disseny clau
Estat compartit com a contracte: quan hi ha graf, el seu estat (TypedDict) és la interfície entre nodes. Cada node llegeix el que necessita i escriu el que produeix. Dissenyar bé l’estat evita que els nodes s’acoplin entre si.
Per què no un if/else en el backend: sovint sí, i l’exemple d’aquesta secció ho és. Un dispatcher amb if/else fa routing senzill perfectament; el que no fa és paral·lelisme amb síntesi, checkpointing d’estat ni bucles condicionals (un revisor que torna la feina al redactor si la qualitat no és suficient). Un graf d’estats expressa aquests tres patrons de forma declarativa, i és quan n’has de menester algun que el framework es paga.
Agents especialitzats, no eines especialitzades: la raó per tenir múltiples agents és que cada especialista necessita un context i instruccions radicalment diferents. Un agent de facturació ha de tenir accés a la BD de factures i conèixer la política de reemborsaments; un agent tècnic necessita documentació tècnica i eines de diagnosi. Barrejar-ho en un sol agent amb moltes eines degrada la qualitat i complica el control.
Checkpointing: LangGraph permet pausar l’execució del graf i reprendre-la més tard, útil per a fluxos llargs o amb intervenció humana intermèdia. Cal una base de dades de checkpoints (SQLite per a dev, PostgreSQL per a producció).
Quan NO usar LangGraph: si el flux és seqüencial sense bifurcacions ni paral·lelisme, el bucle while del cas 5 és suficient i molt més simple. LangGraph afegeix complexitat real, i usar-lo sense necessitat és sobreenginyeria.
Exemples de projectes
- Sistema de suport al client amb routing a agents especialitzats per domini (facturació, tècnic, comercial)
- Pipeline de generació d’articles: agent planificador → agent redactor per secció → agent revisor amb retorn condicional
- Due diligence automatitzat: agents de risc financer, legal i reputació en paral·lel → agent redactor d’informe
- Sistema de desenvolupament de software: agent de disseny d’arquitectura → agent implementador → agent revisor de codi
Exemple
El nucli del patró no és cap llibreria: és un classificador que tria, i un especialista per branca amb el seu propi system prompt i les seves pròpies eines.
# Exemple: routing de tickets de suport a agents especialitzats
ESPECIALISTES = {
"facturació": "Ets un especialista en facturació. Tens accés a la política de "
"reemborsaments i pots consultar l'historial de pagaments.",
"tècnic": "Ets un especialista tècnic. Diagnostica el problema i proposa una "
"solució pas a pas.",
"comercial": "Ets un agent comercial. Informa sobre plans, preus i condicions "
"contractuals.",
}
def classificar(missatge: str) -> str:
categoria = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content":
"Classifica el ticket de suport. Respon únicament amb una paraula: "
"'facturació', 'tècnic' o 'comercial'."},
{"role": "user", "content": missatge},
],
temperature=0,
).choices[0].message.content.strip()
return categoria if categoria in ESPECIALISTES else "tècnic" # ruta per defecte
def atendre(missatge: str) -> str:
categoria = classificar(missatge)
return client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": ESPECIALISTES[categoria]},
{"role": "user", "content": missatge},
],
).choices[0].message.content
print(atendre("No he rebut la factura del mes passat."))
Cada especialista podria ser al seu torn un bucle d’agent amb el seu catàleg d’eines, i el dict seguiria sent el mateix. Fixa’t que el supervisor no comparteix el seu system prompt amb els especialistes: entre agents només hi viatja el resultat.
Què hi afegeix LangGraph. Mentre el flux sigui “classifica i despatxa”, el codi de sobre ja és la solució i un framework només hi posa vocabulari. El que compres quan te’n vas a un graf d’estats és el que aquest if implícit no sap fer: arestes condicionals que tornen enrere (un revisor que retorna la feina al redactor mentre no arribi al llistó), execució en paral·lel de branques independents amb un node de síntesi al final, i checkpointing de l’estat perquè una execució llarga es pugui aturar, esperar una aprovació humana i reprendre’s des d’on era.
# Fragment il·lustratiu: LangGraph no forma part de l'entorn de treball
graf.add_conditional_edges("revisor", prou_bo, {True: END, False: "redactor"})
app = graf.compile(checkpointer=SqliteSaver.from_conn_string("estat.db"))
Aquestes tres coses són la frontera real del cas 9. Si el teu flux no en necessita cap, el que tens és el cas 5 amb diversos prompts.
Com es passa d’un cas al següent
Cap sistema real es queda quiet al cas amb què va començar. Els salts més habituals són tres, i cadascun té un desencadenant concret:
Del cas 1 al cas 6, quan puja el volum. Un classificador que atén peticions d’una en una segueix sent el mateix prompt i el mateix esquema, però amb desenes de milers de documents al dia la crida síncrona deixa de tenir sentit. El que canvia no és la lògica sinó com s’executa: cua, workers i, si el proveïdor l’ofereix, API de batch.
Del cas 3 al cas 8, quan una sola recuperació no basta. Mentre les preguntes es responen amb els fragments que recupera la consulta original, el RAG fix és més simple, més barat i més fàcil de depurar. Quan comences a veure preguntes que necessiten dues cerques encadenades, o que la primera query no encerta i caldria reformular-la, val la pena exposar la cerca com a eina i deixar que el model iteri.
Del cas 5 al cas 9, quan el context d’un sol agent es contradiu. El senyal no és el nombre d’eines sinó el system prompt: quan s’omple d’instruccions que només valen per a un tipus de petició i que confonen la resta, la tasca demana contextos separats. Abans d’arribar-hi, esgota el que pot fer un sol agent ben acotat, que sol ser més del que sembla.
En tots tres casos, el criteri per moure’s és el mateix que recorre aquests materials: no puges d’esglaó perquè el cas següent sigui més interessant, sinó perquè les evals demostren que l’actual no arriba.