Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Arquitectures per cas d’ús

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.


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 + Interfície unificada + 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)
CapaActivaNotes
InferènciaModel petit (3B–8B) sol ser suficient
Interfície unificadaOpenAI SDK
Emmagatzematge vectorialNo cal
OrquestracióCrida única, sense bucle
ObservabilitatRecomanatDetectar derives de qualitat
DesplegamentFastAPI + 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

def classificar_ticket(text: str) -> TicketClassificat:
    resposta = client.chat.completions.parse(
        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=TicketClassificat,
    )
    return resposta.choices[0].message.parsed

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 + Interfície unificada + 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)
CapaActivaNotes
InferènciaModel amb bona instrucció-seguiment
Interfície unificadaOpenAI SDK amb stream=True
Emmagatzematge vectorialOpcionalSi cal base de coneixement
OrquestracióEl bucle és la sessió de conversa
ObservabilitatTraces de sessió completes
DesplegamentGestió 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). Si el consumidor és un backend intern, el streaming és opcional i afegeix complexitat sense benefici. 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 — viu 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 Gestió de la memòria en converses multi-torn.

EstratègiaQuan usar
Finestra lliscant (últims N missatges)Converses on el context recent és el rellevant
Resum periòdicConverses llargues on cal preservar context acumulat
Límit de tokens explícitQuan 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, el streaming afegeix complexitat sense benefici.

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 + Interfície unificada + 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
CapaActivaNotes
InferènciaCal que respecti el context injectat
Interfície unificada
Emmagatzematge vectorialChroma (dev), Qdrant / Weaviate (prod)
OrquestracióEl flux és fix, no dinàmic
ObservabilitatRegistrar queries i fragments recuperats
DesplegamentPipeline 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 — la 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’inclouen als resultats de l’eina de cerca 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 Fragmentació.

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 Quan NO usar RAG.

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="nomic-embed-text", 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 + Interfície unificada + 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)
CapaActivaNotes
InferènciaPot necessitar temperatura > 0 per a varietat
Interfície unificada
Emmagatzematge vectorialEl perfil ve de la BD existent, no d’índex vectorial
OrquestracióCrida única o seqüència fixa
ObservabilitatTraçar perfil + resultat per a auditoria
DesplegamentPrompt 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 — no cal enviar tots els camps si n’hi ha d’irrellevants per a la tasca concreta.

Sortida: JSON estructurat validat per Pydantic. La sortida ha de superar dos nivells de validació: l’esquema (estructura i tipus) i les regles de negoci (valors en rang, consistència entre camps). Un model pot generar JSON estructuralment vàlid que viola una regla de negoci.

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 — només el bloc del perfil canvia. Marcar el system prompt com a cacheïtzable pot reduir el cost en un 80–90% per token. Vegeu Control de costos.

Dos nivells de validació: Pydantic valida l’estructura; les regles de negoci (restriccions contradictòries, valors fora de rang, consistència entre camps) es validen per separat al backend. Un model pot generar un JSON estructuralment vàlid que viola una regla de negoci.

Determinisme: per a alguns casos (dades de test, informes d’auditoria) cal que el mateix perfil produeixi sempre el mateix resultat — usar temperature=0 i cachejar el resultat. 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.parse(
        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=PlaSetmanal,
    )
    return resposta.choices[0].message.parsed  # la validació llança ValueError si falla

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 + Interfície unificada + 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
CapaActivaNotes
InferènciaModel amb bon tool use (Llama 3.1+, GPT-4o, Claude)
Interfície unificada
Emmagatzematge vectorialOpcionalSi una de les eines és cerca semàntica
OrquestracióOpcionalLangGraph per a fluxos amb bifurcacions o multi-agent
ObservabilitatTranscript complet obligatori per a depuració i evals
DesplegamentLí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 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 (i quan no).

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) + Cua de tasques + Emmagatzematge de resultats.

Dataset d'entrada (fitxer / BD)
    │
    ▼
Productor — encua ítems
    │
    ▼
Cua de tasques (Redis / Celery)
    │  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)
CapaActivaNotes
InferènciavLLM per throughput alt; batch API si s’usa proveïdor comercial
Interfície unificada
Emmagatzematge vectorialTípicament no necessari
OrquestracióLa cua gestiona el flux i la concurrència
ObservabilitatProgrés, errors per ítem, cost acumulat
DesplegamentWorkers 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 (OpenAI, Anthropic) 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 celery import Celery
from pydantic import BaseModel
from typing import Literal

app = Celery("batch_llm", broker="redis://localhost:6379/0")

class CorreuClassificat(BaseModel):
    categoria: Literal["facturació", "tècnic", "comercial", "spam"]
    urgència: Literal["alta", "baixa"]
    resum: str

@app.task(bind=True, max_retries=3)
def classificar_correu(self, correu_id: str, cos_correu: str):
    try:
        resposta = client.chat.completions.parse(
            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": cos_correu},
            ],
            response_format=CorreuClassificat,
        )
        desar_resultat(correu_id, resposta.choices[0].message.parsed)
        marcar_completat(correu_id)
    except Exception as exc:
        marcar_error(correu_id, str(exc))
        raise self.retry(exc=exc, countdown=60)

def classificar_safata(correus: list[dict]):
    for correu in correus:
        if estat_ítem(correu["id"]) != "completat":  # idempotència
            classificar_correu.delay(correu["id"], correu["cos"])

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 + Interfície unificada + 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 — aquest veu únicament 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.

CapaActivaNotes
InferènciaModel amb bon tool use i instrucció-seguiment
Interfície unificada
Emmagatzematge vectorialOpcionalSi una de les eines és cerca sobre base de coneixement
OrquestracióEl bucle d’eines és intern al torn; la sessió és el bucle exterior
ObservabilitatTraces de sessió incloent totes les crides a eines
DesplegamentGestió 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). Les crides a eines del torn són invisibles per a l’usuari — aquest veu només la resposta final.

Sortida interna: historial actualitzat que inclou els missatges de rol “assistant” (amb tool_calls) i “tool” en JSON. Aquests missatges augmenten el consum de tokens a cada torn: cal tenir-ho en compte al dissenyar la truncació de l’historial.

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 — cal tenir-ho en compte al 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 bufferitzar 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 — 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

def respondre_torn(sessió: list[dict], entrada: str) -> str:
    sessió.append({"role": "user", "content": entrada})
    historial = gestionar_historial(sessió)  # truncació si cal

    # bucle d'eines dins del torn (invisible per a l'usuari)
    missatges_torn = historial[:]
    while True:
        resposta = client.chat.completions.create(
            model=MODEL, messages=missatges_torn, tools=EINES
        )
        msg = resposta.choices[0].message
        missatges_torn.append(msg)

        if not msg.tool_calls:
            # resposta final del torn: desar a l'historial i retornar
            sessió.append({"role": "assistant", "content": msg.content})
            return msg.content

        for crida in msg.tool_calls:
            resultat = executar_eina(crida.function.name, crida.function.arguments)
            missatges_torn.append({
                "role": "tool", "tool_call_id": crida.id, "content": resultat
            })

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 + Interfície unificada + 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
CapaActivaNotes
InferènciaModel amb bon tool use
Interfície unificada
Emmagatzematge vectorialExposat com a eina, no com a pas fix del pipeline
OrquestracióOpcionalLangGraph si cal combinar múltiples agents especialitzats
ObservabilitatRegistrar queries de cerca i fragments recuperats per torn
DesplegamentBD 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="nomic-embed-text", 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.)
]

MAX_ITERACIONS = 15

def executar_agent_rag(tasca: str) -> str:
    missatges = [
        {"role": "system", "content":
         "Ets un agent de recerca. Usa l'eina cercar_docs per obtenir informació "
         "del corpus intern abans de respondre. Cita les fonts entre claudàtors."},
        {"role": "user", "content": tasca},
    ]

    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

        missatges.append(msg)
        for crida in msg.tool_calls:
            resultat = executar_eina(crida.function.name, 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."

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 — és que cada sub-tasca requereix un context i especialització diferent, 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 — 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 + Interfície unificada + 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) ─┘
CapaActivaNotes
InferènciaCada node és una crida independent al model
Interfície unificada
Emmagatzematge vectorialOpcionalSi els agents especialistes fan cerca
OrquestracióLangGraph — justificació principal d’aquest cas
ObservabilitatTraces per node + estat del graf en cada pas
DesplegamentLangGraph Server per a desplegament persistent

Entrades i sortides

Entrada: text pla (petició) + estat inicial del graf (TypedDict — JSON intern de LangGraph 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: l’estat del graf (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: un dispatcher amb if/else pot fer routing senzill, però no pot gestionar paral·lelisme, checkpointing d’estat, ni fluxos amb bucles condicionals (revisor que torna al redactor si la qualitat no és suficient). LangGraph expressa aquests patrons de forma declarativa.

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 — usar-lo sense necessitat és over-engineering.

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

from langgraph.graph import StateGraph, END
from typing import TypedDict, Literal

# Exemple: routing de tickets de suport a agents especialitzats
class EstatTicket(TypedDict):
    missatge: str
    categoria: str
    resposta: str

def supervisor(estat: EstatTicket) -> dict:
    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": estat["missatge"]},
        ],
    ).choices[0].message.content.strip()
    return {"categoria": categoria}

def agent_facturació(estat: EstatTicket) -> dict:
    resposta = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content":
             "Ets un especialista en facturació. Tens accés a la política de reemborsaments "
             "i pots consultar l'historial de pagaments. Resol el dubte del client."},
            {"role": "user", "content": estat["missatge"]},
        ],
    ).choices[0].message.content
    return {"resposta": resposta}

def agent_tècnic(estat: EstatTicket) -> dict:
    resposta = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content":
             "Ets un especialista tècnic. Diagnostica el problema i proposa solució pas a pas."},
            {"role": "user", "content": estat["missatge"]},
        ],
    ).choices[0].message.content
    return {"resposta": resposta}

def agent_comercial(estat: EstatTicket) -> dict:
    resposta = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content":
             "Ets un agent comercial. Informa sobre plans, preus i condicions contractuals."},
            {"role": "user", "content": estat["missatge"]},
        ],
    ).choices[0].message.content
    return {"resposta": resposta}

def ruta_supervisor(estat: EstatTicket) -> Literal["facturació", "tècnic", "comercial"]:
    return estat["categoria"]

graf = StateGraph(EstatTicket)
graf.add_node("supervisor", supervisor)
graf.add_node("facturació", agent_facturació)
graf.add_node("tècnic", agent_tècnic)
graf.add_node("comercial", agent_comercial)

graf.set_entry_point("supervisor")
graf.add_conditional_edges("supervisor", ruta_supervisor, {
    "facturació": "facturació",
    "tècnic": "tècnic",
    "comercial": "comercial",
})
graf.add_edge("facturació", END)
graf.add_edge("tècnic", END)
graf.add_edge("comercial", END)

app = graf.compile()

# ús
resultat = app.invoke({
    "missatge": "No he rebut la factura del mes passat.",
    "categoria": "",
    "resposta": "",
})
print(resultat["resposta"])
Last change: , commit: af60eb4