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.

Cinc casos són patrons bàsics i es descriuen sencers: quan s’usen, arquitectura, entrades i sortides, decisions de disseny i un exemple mínim de codi (1, 2, 3, 5 i 9). Els altres quatre són variants o combinacions d’aquests, i només se’n descriu el que canvia. El document acaba amb les capacitats de model que exigeix cada cas i per on començar a triar-lo, i amb els salts habituals d’un cas al següent.

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ó.

CasPatró d’orquestracióEl que el distingeixPatró de referència
1. Classificador / extractorCrida únicaSortida estructurada validadaTransformació semàntica
2. Assistent conversacionalCrida única per tornHistorial de sessió i streamingEstat, memòria i multi-torn
3. Q&A sobre base de coneixementWorkflow fixRecuperació abans de generarRAG
4. Generació personalitzadaCrida únicaPerfil injectat i prompt cachingPaquet de context per crida
5. Agent amb einesBucle d’agentEl model decideix les accionsBucle d’ús d’eines
6. Pipeline en batchCua de workersVolum i cost, sense latència críticaPatrons de comunicació
7. Assistent conversacional amb einesBucle d’agent dins de cada tornSessió i eines alhoraBucle d’ús d’eines
8. Agent amb RAGBucle d’agentLa cerca és una eina, no un pasAgentic RAG
9. Multi-agent orquestratGraf d’estatsContextos separats per especialitatSistemes multi-agent

Les capacitats de model que exigeix cadascun, i per on començar a triar-lo, són a Capacitats i model per cas.


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: classificar tickets de suport, extreure camps d’una factura, detectar el sentiment i els aspectes d’una ressenya, parsejar un CV, categoritzar despeses bancàries.

Arquitectura

Capes actives: inferència, client d’inferència i sortida estructurada. Un model petit (3B–8B) sol ser suficient.

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)

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: escalar a models més grans només si les evals mostren insuficiència. Vegeu Selecció del model.

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: 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 i, opcionalment, 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)

Entrades i sortides

Entrada: el torn actual de l’usuari, l’historial de missatges (llista de rols i continguts) i el 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è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, 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.

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: cerca sobre documentació tècnica interna, Q&A sobre normativa per a equips de compliance, navegació per articles 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 i, opcionalment, un 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

Entrades i sortides

Entrada (indexació): documents en qualsevol format (PDF, Markdown, HTML, text pla, registres de BD). El pipeline d’indexació els converteix tots a text pla abans de fragmentar-los i enviar-los al model d’embeddings.

Entrada (consulta): la pregunta de l’usuari en llenguatge natural.

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 o Qdrant: Chroma en mode encastat és adequat per a prototipatge i corpus petits. Per a producció amb múltiples processos concurrents o corpus grans, cal un servei independent.

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

És el cas 1 en sentit invers: en lloc d’analitzar una entrada existent, el model construeix contingut nou a partir d’un perfil d’usuari que el backend llegeix de la BD. No hi ha diàleg: el perfil entra, el resultat generat surt, sempre com a JSON validat.

Exemples: plans d’entrenament o de dieta, itineraris de viatge, camins d’aprenentatge, informes financers personalitzats, dades de test realistes per a un perfil definit.

El que canvia respecte del cas 1:

  • Injecció del perfil: selecciona només els camps rellevants per a la tasca. Injectar-ne d’irrellevants consumeix tokens sense millorar la qualitat i pot diluir l’atenció del model.
  • Prompt caching: les regles de generació, l’esquema i els exemples són idèntics per a tots els usuaris; només canvia el bloc del perfil, que va al final. Amb aquest ordre, el prefix estable es reutilitza de la memòria cau i el cost per crida baixa molt (vegeu Control de costos).
  • Regles de negoci a l’esquema: un JSON estructuralment vàlid pot violar una regla (una sessió de 90 minuts quan el màxim és 60). Pydantic les pot comprovar amb un field_validator al mateix model, de manera que una sola crida a model_validate_json valida forma i regles.
  • Variació o determinisme: per a plans i recomanacions, la variació entre crides és desitjable (temperature > 0). Per a dades de test o informes d’auditoria, el mateix perfil ha de donar sempre el mateix resultat: temperature=0 i resultat desat a la memòria cau.
  • Molts usuaris alhora: si cal generar per a tots els clients (informes mensuals), s’aplica el cas 6.

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: agent d’anàlisi de dades (consulta SQL i explica els resultats), agent de recerca (cerca, sintetitza i redacta), agent de revisió de codi (executa linter i tests, suggereix correccions).

Arquitectura

Capes actives: inferència, client d’inferència, eines i 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

Entrades i sortides

Entrada: la descripció de la tasca i les definicions d’eines (nom, descripció, esquema d’arguments). Les definicions es passen en un paràmetre propi de la crida, no com a text del missatge.

Sortida: la resposta final a l’usuari i el transcript (seqüència de crides amb arguments i resultats) per a auditoria i evals. El transcript és un producte secundari però imprescindible: sense ell, els errors d’agent són opacs. Vegeu Evals d’agents.

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.

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.

Sense framework per defecte: el bucle de l’exemple és suficient per a la majoria de casos. Un framework com LangGraph val la pena quan el flux té bifurcacions complexes, múltiples agents o condicions d’aturada que van més enllà d’un if not tool_calls (vegeu el cas 9).

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."

La descripció de l’eina diu “només lectura”, però això és una preferència: la garantia real és que la connexió db sigui d’un usuari de BD sense permisos d’escriptura (vegeu Capes de defensa reals).


6. Pipeline de processament en batch

És el cas 1 quan el volum puja: classificar 100.000 documents, anotar un dataset per entrenar un model propi, generar els informes mensuals de tots els clients. El prompt i l’esquema són els mateixos; el que canvia és com s’executa. No hi ha ningú esperant cada resposta, així que el cost per token passa per davant de la latència.

Abans de construir res, comprova si el proveïdor té una Batch API: processa els lots de manera asíncrona a meitat de preu i t’estalvia tota la maquinària d’aquesta secció. La cua pròpia té sentit amb models autoallotjats (vLLM) o quan cal control fi del flux.

Les decisions que importen:

  • Idempotència: el procés s’ha de poder reprendre si s’interromp, sense duplicar resultats. Cal guardar l’estat de cada ítem (pendent, completat, error) i consultar-lo abans de processar-lo.
  • Errors aïllats per ítem: un error en un ítem no pot aturar el lot. Es registra amb el missatge original en un log separat, de manera que els ítems fallits es poden reintentar sense reprocessar els completats.
  • Concurrència limitada: el nombre de workers s’ajusta al que aguanta el servidor d’inferència o al límit de peticions per minut del proveïdor, no al que aguanta la teva màquina.
  • Progrés i cost acumulat: registrar-los en temps real permet detectar anomalies de cost i estimar quan acabarà.
# 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 volums de milers d’ítems. Quan el procés ha de sobreviure a reinicis o repartir-se entre màquines, el pas següent és una cua de tasques de veritat (Celery o RQ amb Redis), que és el mateix disseny amb l’estat fora del procés.


7. Assistent conversacional amb eines

És la combinació dels casos 2 i 5: un diàleg multi-torn on l’assistent, dins de cada torn, pot cridar eines abans de respondre. La diferència amb l’agent és qui porta la iniciativa: aquí l’usuari guia la conversa torn a torn, mentre que l’agent resol una tasca sencera pel seu compte.

Exemples: bot de suport que consulta l’estat de comandes i inicia devolucions, assistent d’RRHH que reserva sales, assistent bancari que mostra saldos i moviments.

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

El que apareix en combinar-los:

  • Historial amb missatges d’eines: els missatges assistant amb tool_calls i els de rol tool formen part de l’historial per mantenir la coherència en torns posteriors. Fan créixer el consum de tokens, i la truncació els ha de tenir en compte: tallar entre una crida i el seu resultat deixa un historial invàlid.
  • Streaming amb eines: no es pot fer streaming d’una resposta que encara pot acabar en crides a eines. Es fa streaming de la resposta final, quan el model ja no en demana més.
  • Autorització a l’eina: les eines actuen en nom de l’usuari autenticat, i la comprovació (aquest usuari pot veure aquesta comanda?) viu a l’eina, no al prompt. Vegeu Autorització i control d’accés en agents.
  • Confirmació per a eines d’escriptura: processar una devolució o enviar un correu requereix confirmació explícita de l’usuari, perquè el model les pot cridar per error. Vegeu Control humà (HITL).

8. Agent amb RAG (agentic RAG)

És el cas 5 amb la cerca del cas 3 convertida en una eina més. A diferència del RAG fix, l’agent decideix si cerca, quan i amb quina query: pot reformular la cerca a partir del que ha trobat, fer-ne diverses sobre aspectes diferents, combinar-la amb altres eines o no cercar gens si ja té prou informació.

Exemples: assistent de recerca que cerca articles i consulta bases de citacions, agent legal que combina jurisprudència i normativa, agent de suport que cerca documentació i alhora comprova l’estat del sistema.

El bucle és exactament l’executar_agent del cas 5; només canvien el catàleg d’eines i el system prompt. La funció de cerca és a Agentic RAG i cerca iterativa. Les decisions pròpies:

  • Descripció de l’eina de cerca: el model decideix si la crida a partir del nom i la descripció, així que ha de dir quan usar-la i quan no.
  • Fonts al resultat de l’eina: la funció de cerca retorna cada fragment amb la seva referència (fitxer, secció, pàgina), i el system prompt demana citar-les a la resposta final.
  • Pressupost de cerques: cada reformulació és una crida més d’embeddings i de model. Cal un màxim de cerques per tasca, o un límit global d’iteracions, per evitar bucles de cerca redundants.
  • Índex independent de l’agent: la BD vectorial és un servei separat, i la seva re-indexació no depèn del desplegament de l’agent.

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 no és el nombre de crides al model, sinó que cada subtasca requereix un context i una especialització diferents.

Dos patrons concrets justifiquen l’orquestració:

Supervisor i especialistes: un agent supervisor classifica la petició i la delega a l’agent especialitzat corresponent. Exemple: 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 i síntesi: múltiples agents investiguen aspectes independents simultàniament i un agent final sintetitza els resultats. Exemple: una anàlisi de proveïdors que llança en paral·lel un agent de risc financer, un de legal i un de reputació, i després un agent redactor consolida l’informe.

Arquitectura

Capes actives: inferència, client d’inferència, orquestració i, opcionalment, eines per als especialistes.

Petició
    │
    ▼
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) ─┘

Entrades i sortides

Entrada: la 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.

Sortida: el resultat del node final i l’historial d’execució (nodes executats i estat a cada pas) per a auditoria.

Decisions de disseny clau

Estat compartit com a contracte: quan hi ha graf, el seu estat és la interfície entre nodes. Cada node llegeix el que necessita i escriu el que produeix, i dissenyar-lo bé evita que els nodes s’acoblin entre si.

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.

Quan no cal un framework: si el flux és “classifica i despatxa”, un if al backend ho resol, i és el que fa l’exemple següent. Un graf d’estats (LangGraph) es paga quan cal alguna d’aquestes tres coses: bucles condicionals, paral·lelisme amb síntesi o checkpointing de l’estat.

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 amb un graf d’estats és el que aquest if 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 de LangGraph, no un exemple complet
graf.add_conditional_edges("revisor", prou_bo, {True: END, False: "redactor"})
with SqliteSaver.from_conn_string("estat.db") as checkpointer:
    app = graf.compile(checkpointer=checkpointer)

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.


Capacitats i model per cas

Cada cas exigeix un subconjunt diferent de capacitats del model. La taula serveix per descartar candidats abans de provar res: una capacitat marcada amb ✓ no és negociable.

✓ requerit · ○ recomanat · — no necessari

Cas d’úsSortida estructuradaTool useRaonament estèsContext llargVisióStreamingPrompt cachingLocalFine-tuning
1. Classificador / 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✓✓✓○○—○——
  • Sortida estructurada cal sempre que el backend ha de llegir la sortida del model per programa; només els casos purament conversacionals en poden prescindir.
  • Raonament estès aporta valor clar on la planificació multi-pas és el coll d’ampolla (casos 5, 8 i 9); als altres, només afegeix latència i cost.
  • Prompt caching és una optimització de cost, no una capacitat: val la pena quan el system prompt és llarg i estable i hi ha moltes peticions (casos 4 i 6).
  • Fine-tuning és una optimització tardana: té sentit per als casos 1 i 6 quan hi ha milers d’exemples etiquetats i el prompting no arriba a la qualitat requerida.

Per on començar

La columna “primera opció” cobreix les capacitats crítiques amb bona qualitat sense verificació addicional; l’“alternativa econòmica” pot requerir validació amb evals pròpies. Els perfils (mig, petit, open weights) es descriuen a Selecció del model.

Cas d’úsPrimera opció (qualitat)Alternativa econòmica
1. Classificador / extractorModel mig + sortida estructuradaModel petit (8B local) + constrained decoding
2. Assistent conversacionalModel mig, bon seguiment d’instruccionsModel petit del mateix proveïdor
3. Q&A (RAG)Model mig, fidel al contextModel local (70B)
4. Generació personalitzadaModel mig + prompt cachingModel petit + caching
5. Agent amb einesModel mig amb bon tool useModel local (70B) + validació amb evals
6. BatchBatch API del proveïdor actualvLLM + model local
7. Assistent conversacional amb einesModel mig amb bon tool useModel petit del mateix proveïdor
8. Agentic RAGModel mig o gran amb raonamentModel local (70B) + validació
9. Multi-agent orquestratModel gran amb raonament (supervisor) + model mig (workers)Model mig amb raonament (supervisor)

Regla general: comença amb el model mig del proveïdor actual. Escala al model frontier si les evals mostren que no arriba; passa a un model open weights autoallotjat si la privacitat o el cost en producció ho exigeixen, i verifica-hi les capacitats crítiques.

Un exemple aplicat

Suposem el cas 5, per a un servei intern que consulta la base de dades de comandes i pot iniciar devolucions.

1. Quines capacitats són obligatòries. A la fila del cas 5, tool use i sortida estructurada porten ✓: sense la primera l’agent no pot actuar, i sense la segona el backend no pot llegir el que retorna. El raonament estès i el context llarg hi surten amb ○: ajudaran si el flux té molts passos, però no descarten cap candidat.

2. Quines restriccions imposa l’entorn. Les comandes porten dades de clients i la política interna diu que no poden sortir de la xarxa de l’organització. Aquesta restricció resol la tria entre API o model local abans de mirar cap capacitat: cal un model open weights autoallotjat.

3. Quin perfil encaixa. Als perfils de model, la fila open weights dona el tool use com a habitual però la sortida estructurada com a variable, i el tool use és precisament la capacitat que més varia entre aquests models. Encara no has provat res i ja saps què hauràs de comprovar.

4. Què verifiques. De la taula d’Avaluar un model candidat, només les dues files obligatòries. Per a la sortida estructurada: si el servidor admet response_format amb json_schema, i de 100 crides quantes tornen un esquema invàlid. Per al tool use: quin percentatge de crides tria l’eina correcta amb el teu catàleg real, no amb un d’exemple.

5. Què fas si no hi arriba. Si el model més gran que et cap a la GPU falla el tool use amb les dotze eines del catàleg, encara tens dues palanques abans de replantejar la restricció de privacitat: reduir el catàleg que veu el model a cada ruta, o partir la feina en agents especialitzats (cas 9) perquè cadascun en vegi menys. Si ni així, tens un requisit de privacitat i un de capacitat que no es poden satisfer alhora, i això ja és una conversa de producte, no una decisió tècnica.


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.

Amb el sistema dissenyat i avaluat, queda portar-lo a producció: desplegar-lo, observar-lo i mantenir-lo. Això és el tema de Desplegament i operació, i el que és específic dels LLMs es recull a Sistemes LLM en producció.

Last change: , commit: 2280835