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

Orquestració amb workflows i agents

Aquest document cobreix els dos patrons fonamentals per construir sistemes amb LLMs: workflows, on l’orquestrador controla el flux i el model omple tasques concretes; i agents, on el model decideix dinàmicament quines accions executar. Per a la capa anterior, com es dissenyen els prompts i com es crida l’API d’un model, vegeu Prompts i integració. Per a la infraestructura (servidors, maquinari, models), vegeu Arquitectura de sistemes LLM.

El document s’estructura en tres blocs:

Workflows deterministes

Un workflow és un patró d’orquestració on el flux de control és determinista: el programador defineix la seqüència de passos en temps de disseny, i el LLM omple tasques específiques dins d’un pipeline que el codi controla. El nombre i l’ordre dels passos és fix i conegut per avançada. RAG n’és l’exemple canònic: el pipeline sempre fa els mateixos passos (recuperar, injectar, generar), i és el codi qui decideix quan i com executar-los.

Paquet de context per crida

En un workflow, l’orquestrador té el control: decideix quina informació es posa davant del model a cada crida. El model no busca dades pel seu compte, no escull què recuperar, no decideix si necessita més context: rep el paquet que el codi ha preparat i produeix una resposta. La pregunta de disseny és, per tant, què s’inclou en aquest paquet.

Les opcions, ordenades de la més simple a la més rica:

OpcióQuan s’usa
Cap dada addicionalTasques purament semàntiques sobre l’entrada: resum, traducció, classificació, reescriptura. Tot el que el model necessita ja és a la pròpia entrada.
Context estàtic al system promptGlossaris, polítiques, esquemes o regles de negoci petits i estables. S’escriu una vegada i no canvia entre crides.
Dades pre-recuperades per reglaL’orquestrador consulta una BD o una API segons regles deterministes (per ID, per data, per usuari autenticat) i serialitza el resultat al missatge user.
Recuperació semàntica (RAG)El corpus és massa gran per cabre al context i la consulta determina dinàmicament quins fragments són rellevants.
Exemples few-shotL’esquema de sortida o el to tenen ambigüitats que cal resoldre mostrant casos concrets (vegeu Exemples com a especificació (few-shot)).
Adjunts multimodalsImatges, PDFs o fitxers que el model parseja directament, sense que l’orquestrador hagi d’extreure’n el text (vegeu Formats d’entrada i sortida).

Aquestes opcions no són exclusives: un workflow real sovint en combina diverses (per exemple, context estàtic al system prompt + dades pre-recuperades + un parell d’exemples few-shot). El que importa és que totes les decisions sobre què veu el model les pren el codi, no el model.

Les seccions següents desenvolupen els patrons més habituals: la transformació semàntica (input a transformar, sense dades externes), l’encadenament de prompts (pipelines de crides seqüencials o paral·leles), la cascada de models (escalat del mètode barat al car segons la confiança), i RAG (recuperació semàntica dinàmica).

Transformació semàntica i extracció

El patró d’extracció és el workflow LLM més habitual. En el seu nucli, el LLM actua com a processador semàntic dins d’un pipeline determinista: rep una entrada, aplica comprensió del llenguatge o inferència semàntica, i retorna una sortida estructurada. Les transformacions segueixen quatre formes:

TransformacióCasos típics
Text → JSONExtracció d’entitats, normalització de formularis, estructuració de transcripcions
JSON → JSONClassificació, inferència semàntica, adaptació entre esquemes
Text → TextResum, traducció, reescriptura amb restriccions
JSON → TextGeneració de missatges personalitzats, explicació de dades

El cas menys obvi és JSON → JSON: el LLM rep dades estructurades d’un sistema i retorna dades estructurades per a un altre, sense que hi hagi un usuari a la cadena. El model substitueix regles de classificació o d’inferència que serien costoses de mantenir. Per exemple, un sistema de ticketing pot enviar {"ticket": "El pagament s'ha acceptat però no apareix la comanda"} i rebre {"categoria": "pagaments", "prioritat": "alta"}.

El flux és el mateix en tots els casos: construir el prompt amb l’entrada, cridar el model, validar la sortida.

entrada (text o dades estructurades)
    │
    ▼
construcció del prompt (system + input)
    │
    ▼
LLM (amb sortida estructurada)
    │
    ▼
validació (Pydantic)
    │
    ▼
sortida validada

Formats d’entrada per a dades estructurades: quan l’entrada és estructurada, cal serialitzar-la a text abans d’injectar-la al prompt. El format preferit és JSON: els models l’han vist extensivament en entrenament i el parsegen de forma fiable, és compacte i és fàcil de validar programàticament. Per a dades tabulars o relacionals, les taules Markdown (o CSV amb capçalera) representen millor l’estructura fila-columna que el JSON imbricat. YAML és una alternativa llegible per a estructures profundament niades, amb menys soroll sintàctic. Els parells clau: valor en línies separades funcionen bé per a registres plans. Evita XML llevat que les dades ja siguin en aquest format. En tots els casos, inclou sempre les capçaleres o una descripció breu dels camps: el model necessita saber què representa cada valor, no només el valor.

Delimitadors: quan el prompt conté prosa al voltant de les dades, o múltiples blocs de dades, cal delimitar-los explícitament per evitar ambigüitats sobre on comença i acaba cada input. La convenció habitual són etiquetes d’estil XML (<order>...</order>, <document>...</document>), no com a format de dades sinó com a marcadors estructurals del prompt. Els models actuals les gestionen bé i la majoria de proveïdors les recomanen explícitament per a aquest ús. Si les dades són l’únic contingut del missatge i el format ja és autodelimitador (com JSON), els delimitadors no calen.

Capacitats del model: alguns models van més enllà de la serialització de text. Els resultats d’eines (tool results) arriben en un slot dedicat del missatge, separat del prompt de l’usuari, la qual cosa redueix el risc d’injecció de prompt des del contingut de les dades. Algunes APIs permeten adjuntar fitxers (CSV, documents) de forma nativa. Per a taules grans on injectar totes les files seria prohibitiu, els models amb execució de codi poden escriure i executar codi per consultar les dades en lloc de llegir-les totes al context.

Quan usar-lo: quan cal una transformació semàntica dins d’un pipeline. Text→JSON per estructurar entrada no estructurada (classificació de sentiment, extracció d’entitats, normalització de formularis, anàlisi de contractes). JSON→JSON per classificar o inferir dins de sistemes existents (categorització de tickets, enrutament, adaptació entre esquemes). JSON→Text per generar missatges o explicacions contextuals. En tots els casos, el criteri subjacent és el mateix: el LLM substitueix lògica que seria costosa o fràgil d’implementar amb regles.

Building blocks rellevants: Sortida estructurada per a la implementació tècnica (Pydantic, validació de schema, gestió de reintents); Exemples com a especificació (few-shot) per a casos on l’esquema té ambigüitats que cal resoldre amb exemples.

Cadenes de crides

L’encadenament de prompts (prompt chaining) és el patró compositiu fonamental dels workflows: descomposar una tasca complexa en una seqüència de crides al model, on la sortida de cada pas és l’entrada del següent. El codi decideix l’ordre, els criteris per avançar i com gestionar els errors de cada pas.

entrada
    │
    ▼
LLM · pas 1 (extracció / simplificació)
    │
    ▼
LLM · pas 2 (transformació / enriquiment)
    │
    ▼
LLM · pas 3 (generació / formatació)
    │
    ▼
sortida

El motiu principal per encadenar és que cap model resol bé tasques molt llargues en una sola crida: la qualitat degrada a mesura que el context creix i la tasca es complica. Descomposar redueix la dificultat de cada crida i permet especialitzar el prompt per a cada etapa, que és testable de forma independent.

Un segon motiu és la injecció de lògica determinista entre passos: el codi pot validar la sortida del pas anterior (Pydantic, assertions, serveis externs), filtrar-la o enriquir-la amb dades addicionals abans de passar-la al model següent. El pipeline és controlable perquè el codi manté el control en cada transició.

Paral·lelització: quan els passos d’una cadena són independents entre si, es poden executar de forma concurrent. El patró és el fan-out i fan-in habitual de la programació concurrent: llançar totes les crides en paral·lel i combinar els resultats en un pas final.

from concurrent.futures import ThreadPoolExecutor

def processar_chunk(chunk: str) -> str:
    return client.chat.completions.create(
        model=MODEL,
        messages=[{"role": "user", "content": f"Resumeix: {chunk}"}]
    ).choices[0].message.content

with ThreadPoolExecutor() as executor:
    resums = list(executor.map(processar_chunk, chunks))

resum_final = client.chat.completions.create(
    model=MODEL,
    messages=[{"role": "user", "content": "Integra aquests resums:\n\n" + "\n\n".join(resums)}]
).choices[0].message.content

Una variant de la paral·lelització és la votació per majoria (ensemble): executar el mateix pas N vegades en paral·lel amb temperatures diverses i triar la resposta més freqüent o que superi un criteri de validació. Adequada per a classificacions on la confiança és crítica i el cost de N crides és assumible.

Quan usar-lo: quan una tasca és massa complexa per a una sola crida, quan cal validar o transformar resultats intermedis, o quan parts del pipeline són independents i es poden paral·lelitzar. Si tots els passos es poden fer en una sola crida amb qualitat acceptable, l’encadenament afegeix latència i complexitat innecessàries.

Cascada de models i escalat per confiança

L’encadenament posa crides en seqüència perquè cada pas depèn de l’anterior. La cascada és un patró diferent: col·loca diversos mètodes que resolen la mateixa tasca en ordre de cost creixent. Cada nivell intenta resoldre el cas, i només si no ho aconsegueix amb prou confiança escala al nivell següent, més car i més capaç. Els nivells barats despatxen la majoria fàcil; el nivell car (típicament un LLM gran) només s’executa sobre el residu difícil. La indústria ho anomena model cascade o confidence-gated escalation, i és l’aplicació directa del principi “comença pel més simple” que ja apareix a la guia de baseline (regles, després embedding, després LLM) de Quan usar un LLM i a l’escala de prompting (zero-shot, few-shot, chain-of-thought, fine-tuning) de Prompts i integració.

cas d'entrada
    │
    ▼
Nivell 1  (regla / keyword, ~5 ms)
    ├── confiança alta → accepta el resultat
    └── incert        → escala
            │
            ▼
Nivell 2  (embedding / model petit, ~30 ms)
    ├── confiança alta → accepta el resultat
    └── incert        → escala
            │
            ▼
Nivell 3  (LLM gran, ~1,5 s, amb timeout)
    └── resultat (si exhaureix el termini, es manté la millor resposta dels nivells previs)

El que justifica el patró és el triangle cost / precisió / latència: cap mètode únic els optimitza tots tres. Una regla és instantània i gratuïta però fràgil; un LLM és precís però lent i car. La cascada compra un punt d’operació millor que qualsevol mètode aïllat, perquè reserva el mètode car només per als casos que el necessiten.

Tres peces el fan funcionar:

Confiança com a sortida de primera classe. Un classificador no hauria de retornar només l’etiqueta més probable (argmax), sinó també una mesura de com de segur n’està. Les dues senyals habituals són el llindar (la probabilitat de la millor opció supera un mínim) i el marge (la distància entre la primera i la segona opció: si van empatades, el cas és ambigu encara que la primera tingui una probabilitat alta). Un classificador que sap quan no sap és el que fa possible escalar de manera selectiva. El nivell barat sol ser un model encoder (un classificador o un zero-shot, vegeu Variants modernes), no un LLM generatiu: és precisament el seu cost baix i la seva predictibilitat el que fa que valgui la pena posar-lo al davant.

La porta de decisió (decision gate). A cada nivell, el codi decideix de manera determinista: accepta el resultat (confiança per sobre del llindar i marge suficient) o escala al següent. Aquesta lògica viu al codi, no al model.

Degradació controlada sota timeout. El nivell car sol tenir un timeout estricte: si l’LLM no respon a temps, es conserva la millor resposta dels nivells previs en lloc de fer esperar l’usuari. Així la cascada es degrada de manera controlada en comptes de quedar bloquejada.

L’economia, amb números. Suposem una tasca on un filtre barat (~5 ms) resol amb confiança el 80% dels casos i el 20% restant escala a un LLM (~1.500 ms). La latència mitjana és 0,8 × 5 + 0,2 × 1.500 ≈ 304 ms, davant dels 1.500 ms de cridar sempre l’LLM, i amb una cinquena part de les crides de pagament. Aquest és tot el sentit del patró: les xifres publicades a la indústria situen l’estalvi de cost entre el 45% i el 85% mantenint al voltant del 95% de la qualitat, precisament perquè el gros del tràfic no arriba mai al model car. El llindar de confiança per escalar sol rondar 0,8 en les implementacions de referència.

Modes de fallada. El patró té un cost propi:

  • Pèrdua de recall als filtres primerencs: si un nivell barat descarta un cas amb massa agressivitat, el nivell intel·ligent no el veu mai. Els errors dels primers nivells es propaguen cap avall sense remei.
  • Superfície d’afinament: cada llindar i cada marge és un paràmetre a calibrar, i en una cascada de molts nivells aquests paràmetres interactuen.
  • Avaluació més difícil: el comportament és escalonat, de manera que cal avaluar el sistema sencer i no cada nivell per separat.

Quan usar-la: quan la mateixa tasca es pot resoldre amb mètodes de cost molt diferent i la majoria de casos són fàcils, de manera que pagar el mètode car per a tots seria malbaratament. És especialment natural en classificació i enrutament d’alt volum. Si gairebé tots els casos necessiten igualment el model car, la cascada només afegeix complexitat i latència sense filtrar res.

RAG com a patró de recuperació

El coneixement d’un LLM és estàtic: queda fixat en el moment de l’entrenament. El model no sap res de la documentació interna de l’organització, dels canvis recents al producte, ni de cap dada privada. La solució intuïtiva, posar tota la informació rellevant al prompt, té un límit: la finestra de context del model és finita, i omplir-la amb documents irrellevants degrada la qualitat de la resposta.

RAG (Retrieval-Augmented Generation) resol aquest problema amb un principi simple: en lloc d’enviar tot el corpus al model, recuperar només els fragments rellevants per a cada consulta concreta i injectar-los al prompt.

Quan el coneixement paramètric és suficient

Abans d’afegir la infraestructura de recuperació, val la pena preguntar-se si el model ja sap el que necessita. Els LLMs moderns cobreixen de forma fiable els principis establerts de dominis ben representats al corpus d’entrenament: salut general, nutrició, esports, dret comú, cuina, educació, finances personals, ciències. La densitat de textos de divulgació, manuals, fòrums especialitzats i documentació acadèmica fa que el model pugui respondre amb solvència preguntes que no requereixin informació recent ni context individual. El que la literatura anomena closed-book QA: el model actua com a oracle de domini, sense accés a cap corpus extern.

Com avaluar si el model és suficient per al domini:

CriteriFavorable al model solRisc
Representació a l’entrenamentDomini popular amb literatura abundantSubdisciplina nínxol o molt especialitzada
Estabilitat del coneixementPrincipis establerts i establesRecerca recent o normativa en evolució
Context individualPregunta genèrica de dominiRequereix dades de l’usuari (historial, biomecànica, medicació)
Necessitat de citacióResposta orientativaCal font autoritzativa citable
Cost d’errorBaixa conseqüència si és incorrecteAlta conseqüència (decisió mèdica, legal, de seguretat)

Criteri pràctic: testa els marges, no el centre. El model respondrà bé les preguntes típiques del domini. La qüestió és si les preguntes atípiques (un cas nínxol, un canvi recent, una situació individual complexa) estan dins del rang acceptable per al teu cas d’ús.

Quan cal RAG: si les consultes necessiten informació posterior al tall d’entrenament, fonts citables, o un corpus específic de domini (metodologia pròpia, normativa interna, base de coneixement privada). RAG no millora el que el model ja sap: hi afegeix accés a fonts que no són als seus pesos. Les dades específiques de l’usuari (historial, registres, mesures) no són un cas de RAG: són dades estructurades que s’obtenen per consulta determinista a una BD i s’injecten al prompt per regla (vegeu Dades pre-recuperades per regla a la taula anterior). Els detalls d’indexació, embeddings, chunking i vector databases es tracten a Integració i execució; l’avaluació del pipeline, a Evals per a RAG.

Quan afegir fine-tuning: útil quan el problema no és d’accés a dades sinó de comportament: classificació, consistència de format, to, seguiment d’instruccions o patrons de sortida repetibles. Si el model ja coneix el domini però falla de manera sistemàtica en com respon, el fine-tuning pot ser més adequat que RAG. Si el problema és falta d’informació específica o recent, RAG continua sent la millor opció.

Quan el RAG vectorial no és suficient

Per a preguntes que requereixen raonament multi-hop sobre relacions estructurals, o quan el corpus és molt interconnectat, una variant anomenada GraphRAG substitueix l’índex vectorial per un graf de coneixement i recupera per travessia en lloc de per similitud. És significativament més car de construir i mantenir, així que només s’usa quan el RAG vectorial falla sistemàticament en preguntes relacionals. Implementació de referència: Microsoft GraphRAG.

Tool calling dins de workflows

Fins ara les opcions de Paquet de context per crida assumien que totes les dades les prepara l’orquestrador abans de la crida. El tool calling introdueix una variant: el model pot emetre una crida estructurada que l’orquestrador intercepta i executa. Això s’associa habitualment amb agents, però existeixen usos perfectament deterministes del mecanisme que encaixen en un workflow, sempre que el codi controli el bucle i no el model.

Dos patrons cobreixen la majoria de casos:

1. Enrutament i despatx: el model rep un menú reduït d’eines i ha de triar-ne exactament una (tool_choice="required"). L’orquestrador llegeix la decisió i executa la branca corresponent. El model decideix què, el codi decideix què passa després. És el patró natural per a classificació amb acció: triar a quin servei enviar un tiquet, quina plantilla aplicar a un document, quina API consultar segons la intenció de l’usuari.

EINES = [
    {"type": "function", "function": {"name": "facturacio", "parameters": {...}}},
    {"type": "function", "function": {"name": "suport_tecnic", "parameters": {...}}},
    {"type": "function", "function": {"name": "resposta_automatica", "parameters": {...}}},
]

resposta = client.chat.completions.create(
    model=MODEL, messages=messages, tools=EINES, tool_choice="required",
)
crida = resposta.choices[0].message.tool_calls[0]
HANDLERS[crida.function.name](json.loads(crida.function.arguments))

2. Recuperació amb iteracions acotades: el model rep un catàleg d’eines de només lectura, en pot cridar diverses en paral·lel en una mateixa resposta, i l’orquestrador imposa un límit dur d’iteracions (típicament 1 o 2). Mentre el límit és baix i les eines no tenen efectes laterals, el flux és predictible: el model fa fan-out de consultes, agrega els resultats i respon. És el patró habitual per a respostes fonamentades en múltiples fonts (consultar simultàniament la BD de clients, l’inventari i l’històric de comandes per a una pregunta de suport).

for _ in range(MAX_ITERACIONS):
    msg = client.chat.completions.create(
        model=MODEL, messages=messages, tools=EINES
    ).choices[0].message
    messages.append(msg)
    if not msg.tool_calls:
        break
    for crida in msg.tool_calls:
        resultat = EXECUTORS[crida.function.name](**json.loads(crida.function.arguments))
        messages.append({"role": "tool", "tool_call_id": crida.id, "content": resultat})

El límit dur (MAX_ITERACIONS) és el que separa això d’un agent: per construcció, el codi garanteix que el bucle acaba. Els frameworks d’agents i les APIs de tool use dels proveïdors encapsulen aquest patró, però la mecànica subjacent és la mateixa.

Les crides paral·leles són un cas habitual i barat: el model emet diverses crides en un mateix torn, l’orquestrador les executa concurrentment i les retorna juntes. Els models actuals principals suporten paral·lelisme natiu.

La coerció d’esquema, és a dir, declarar una eina fictícia només per forçar JSON vàlid, és un tercer ús del mecanisme, però avui és residual: la sortida estructurada (response_format) està disponible a tots els proveïdors principals i a la majoria de servidors locals. Queda com a fallback per a models antics i com a solució de contingència quan el backend no implementa la validació de schema de forma nativa, tal com es descriu a Sortida estructurada i validació.

Cost i cache del catàleg d’eines

Exposar 20 o 50 eines és car si la definició es retransmet a cada crida, però molt més barat si el proveïdor pot cachejar el prefix del context. Els proveïdors principals tenen mecanismes de prompt caching que redueixen molt el cost dels prefixos repetits, i els servidors d’inferència locals implementen prefix caching equivalent. La conseqüència pràctica: el consell tradicional de “menú reduït” ja no és una restricció dura de cost, però continua sent una bona decisió de qualitat (massa eines confon el model). Per a la reutilització d’eines entre agents i la integració amb catàlegs de tercers, vegeu L’estàndard d’integració d’eines: MCP.

Línia divisòria amb els agents

Si el límit d’iteracions és alt, si el model decideix quan parar, o si les eines tenen efectes laterals que el model pot encadenar, ja no és un workflow amb tool calling. El control de flux ha passat del codi al model, i això és un agent. Aquest és el tema de la secció següent.

Agents i tool use

Un agent és un patró d’orquestració on el flux de control és dinàmic: el model decideix en temps d’execució quines eines cridar, en quin ordre, i si el resultat és suficient per continuar o cal iterar. El programador defineix les eines disponibles i els límits del sistema; el model decideix com usar-les per arribar a un objectiu.

Un LLM pot raonar sobre un problema i decidir quins passos cal fer, però no pot executar res per si mateix: no pot llegir fitxers, consultar bases de dades ni cridar APIs. El patró d’ús d’eines (tool use o function calling) separa clarament aquests dos rols: el model decideix quan cal cridar una eina i amb quins arguments; el sistema executa la crida i retorna el resultat. Totes les accions que el sistema fa en nom del model són auditables i controlades; si són reversibles depèn de l’eina i del flux de confirmació, no del LLM.

Qualsevol sistema amb ús d’eines executa el bucle d’agent: tècnicament, tot LLM amb tool use és un agent. En la pràctica, hi ha un espectre: en un extrem, un bucle simple on el model crida gairebé sempre la mateixa eina i acaba en 1–2 iteracions (predictible, proper a un workflow); a l’altre extrem, un agent que raona sobre cada resultat, decideix si calen més crides i adapta el camí a la informació obtinguda. La distinció pràctica: si es pot substituir el bucle per una seqüència fixa de crides directes i obtenir el mateix resultat, és un workflow que usa el mecanisme d’agent. Si no es pot, és un agent real.

Bucle d’ús d’eines

La interacció entre model i eines segueix un bucle fins que el model considera que té prou informació per respondre:

missatges
    │
    ▼
LLM
    ├── retorna text → fi (resposta final)
    └── retorna tool_call(nom, arguments)
            │
            ▼
        execució local de la funció
            │
            ▼
        resultat afegit als missatges
            │
            └──► torna al LLM

El LLM proposa; el sistema disposa. El model retorna una proposta estructurada (nom de l’eina, arguments); el codi valida, executa i retorna el resultat. En Python pur:

while True:
    resposta = client.chat.completions.create(model=MODEL, messages=missatges, tools=eines)
    msg = resposta.choices[0].message

    if not msg.tool_calls:
        return msg.content  # el model ha acabat

    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})

No cal cap framework per implementar aquest patró. El bucle while, la funció executar_eina i el registre d’eines són tot el que es necessita per a la majoria de casos.

El model pot retornar múltiples tool_calls en una sola resposta quan les crides són independents entre si, per exemple consultar dos productes alhora. El bucle les gestiona seqüencialment per defecte, però es poden executar en paral·lel per reduir la latència:

from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor() as executor:
    futures = {
        executor.submit(executar_eina, c.function.name, c.function.arguments): c
        for c in msg.tool_calls
    }
    for future, crida in futures.items():
        missatges.append({
            "role": "tool", "tool_call_id": crida.id, "content": future.result()
        })

On és l’estat de l’agent

Quan veiem un agent treballant, sembla lògic pensar que hi ha alguna estructura que guarda com va la feina:

estat = {"objectiu": ..., "fet": ..., "pendent": ..., "tests": ...}

Normalment no hi és. L’única cosa que es manté entre iteracions del bucle és la llista missatges. És a dir: la trajectòria és l’estat. L’objectiu inicial, les eines cridades, els resultats, els errors que van sortir i els que ja s’han resolt hi són com a text acumulat, no com a camps d’un registre.

Cada cop que ha de decidir què fa, el model torna a llegir tota la seqüència. L’atenció li permet connectar l’objectiu del primer missatge amb un resultat d’eina de fa deu iteracions.

Això explica una cosa que sorprèn. Pots donar una instrucció que no diu cap algorisme, com ara “queda alguna cosa pendent?”, i l’agent fa passos concrets: mira l’estat del repositori, el compara amb l’objectiu inicial, busca què no s’ha verificat. No hi ha cap funció calcular_estat_sessio(). La resposta surt del mateix càlcul que produeix qualsevol altra continuació.

Dir que el model “prediu el següent token” descriu què calcula, no com de simple és el càlcul. Per encertar la continuació d’una trajectòria de feina, el model ha de representar-se internament què es volia fer, què s’ha fet i què falta.

La conseqüència pràctica és directa: gestionar el context d’un agent és gestionar el seu estat.

En un chatbot, retallar l’historial fa perdre matisos de la conversa. En un agent, retallar-lo malament fa perdre l’objectiu, o una opció que ja s’havia descartat, o el fet que un test ja havia fallat abans. Llavors l’agent repeteix feina o es contradiu. Per això la compactació, que guarda explícitament objectius, decisions i pendents, no és el mateix que un resum genèric, i per això les eines han de retornar el mínim útil i no l’output cru. Vegeu Estat, memòria i multi-torn.

Un avís per acabar: aquest estat implícit no serveix per a tot. És una representació que el model construeix per predir, no un objecte que puguem consultar ni garantir. Si una dada ha de ser exacta i auditable (un identificador de comanda, un import, el llindar d’una política), ha de viure en estat explícit del sistema (una variable, una base de dades) i arribar al model com a resultat d’eina.

El model i el harness

Ja hem dit que el model proposa i el sistema disposa. Val la pena mirar bé aquest “sistema”, perquè quan diem “agent” és fàcil atribuir-ho tot a l’LLM. El codi que envolta el model sol anomenar-se harness, i és força més que un while:

                 AGENT
┌─────────────────────────────────────────┐
│   ┌───────────┐       ┌──────────────┐  │
│   │    LLM    │◄─────►│   context    │  │
│   └─────┬─────┘       └──────────────┘  │
│         │ tool call                     │
│         ▼                               │
│   ┌───────────┐                         │
│   │  harness  │                         │
│   └─────┬─────┘                         │
└─────────┼───────────────────────────────┘
          │ resultat
          ▼
┌─────────────────────────────────────────┐
│                ENTORN                   │
│   fitxers · Git · shell · tests · APIs  │
└─────────────────────────────────────────┘

El harness és qui defineix quines eines existeixen, valida els arguments abans d’executar res, aplica els permisos, decideix què necessita confirmació humana, imposa el límit d’iteracions, gestiona el context quan s’apropa al límit i registra què ha passat. Dit d’una altra manera:

Un LLM no és un agent. Un agent és un sistema que fa servir un LLM dins d’un bucle amb un entorn.

Això és útil quan un agent no funciona bé. La reacció habitual és reescriure el prompt, però moltes fallades no són del model: eines mal descrites, un resultat massa llarg que embruta el context, cap límit d’iteracions, permisos massa amplis. Són problemes del harness i cap reescriptura del prompt els arregla (vegeu Evals d’agents).

Les seccions que segueixen són, de fet, el disseny d’aquesta capa: les eines, els permisos, el control humà i els límits. I l’operació en producció, a Sistemes LLM en producció.

📝 La paraula harness (o arnès) s’usa amb dos sentits propers que convé no barrejar. Aquí és el runtime que executa les crides del model: el bucle, les eines, els permisos. A Harness engineering és el conjunt de portes de verificació que envolten un agent de programació (linters, tipus, tests, CI) i que atrapen els seus errors. Un executa, l’altre comprova; tots dos són codi determinista al voltant d’un model probabilístic, i és per això que comparteixen el nom.

Disseny i contracte d’eines

Una eina és la interfície entre el model i el món extern. El seu disseny determina quanta autonomia efectiva té l’agent i la fiabilitat del sistema. Alguns principis:

Atomicitat: cada eina fa una sola cosa. cercar_producte(id) i actualitzar_estoc(id, quantitat) són millors que una eina gestionar_producte(acció, id, ...). Les eines atòmiques són més fàcils de testar, auditar i reutilitzar.

Noms i descripcions precisos: el model usa el nom i la descripció de l’eina per decidir quan cridar-la, perquè no en veu la implementació. Una descripció ambigua provoca crides incorrectes; una descripció que no especifica els estats de fallada possibles pot fer que el model cridi en bucle una eina que retorna errors sense entendre per què. La descripció ha d’especificar quan s’ha d’usar, quan no, i quins errors pot retornar.

Contracte d’errors explícit: les eines han de retornar errors de forma estructurada, no llançar excepcions que el bucle no gestiona. El model pot llegir un missatge d’error i decidir com continuar; una excepció no capturada trenca el sistema.

def consultar_comanda(comanda_id: str) -> str:
    comanda = db.get(comanda_id)
    if not comanda:
        return json.dumps({"error": f"Comanda {comanda_id} no trobada"})
    return json.dumps({"estat": comanda.estat, "data": comanda.data_lliurament})

Concisió de la sortida: una eina ha de retornar el mínim útil per al raonament del model, no l’output cru del sistema subjacent. Un grep que retorna 500 línies, una crida HTTP que retorna headers més body sencer, o un traceback de 200 línies omplen el context d’informació que el model no necessita i degraden la qualitat dels torns següents (vegeu Estat, memòria i multi-torn). Els resultats verbosos s’han de filtrar, resumir o paginar abans de retornar-los. Si l’agent necessita explorar més detall, ho pot demanar amb una crida addicional més específica.

Principi del mínim privilegi: exposa només les eines que l’agent necessita per a la tasca concreta. Un agent de suport al client no ha de tenir accés a eines d’administració del sistema. Cada eina exposada amplia la superfície d’atac si el model és manipulat per injecció de prompt.

Eines deterministes com a tools: quan una operació té una resposta correcta verificable (validar un esquema, consultar una BD, executar un linter), envolta-la com a eina en lloc de deixar que el model raoni sobre el resultat. El model obtindrà un fet, no una estimació. La mateixa lògica s’aplica a la cerca semàntica local: si tens un índex RAG, exposa’l com a tool perquè l’agent recuperi fragments precisos en lloc d’al·lucinar contingut del corpus.

Recuperació d’eines per a conjunts grans: passar tots els schemas de totes les eines a cada crida és inviable quan l’agent té 50+ eines: la precisió degrada i el context s’omple. El patró és tractar les eines com un corpus: vectoritzar les seves descripcions, recuperar les top-k rellevants per a la consulta actual, i passar al model només aquelles.

# indexació única de les descripcions d'eines
eines_vectors = {nom: embedding(desc) for nom, desc in eines.items()}

# per a cada torn: recuperar les eines rellevants
query_vec = embedding(missatge_usuari)
eines_rellevants = top_k(eines_vectors, query_vec, k=10)
# passar només eines_rellevants al model

Agentic RAG i cerca iterativa

El RAG clàssic fa una recuperació fixa per consulta. L’Agentic RAG exposa el pipeline de recuperació com a eina dins del catàleg que rep el bucle d’agent: l’agent l’invoca quan ho decideix, amb la query que considera adequada, i pot tornar a cercar si el resultat no és suficient.

consulta
  │
  ▼
agent
  ├── [prou context] → genera resposta
  └── [cal cercar]   → tool: search(query)
                              │
                              ▼
                        fragments recuperats → agent → [avalua] → ...
def search(query: str, k: int = 5) -> str:
    """Cerca al corpus i retorna els fragments més rellevants.
    Usa-la quan necessitis informació factual que no tinguis al context."""
    vec = embed(query)
    fragments = col.query(query_embeddings=[vec], n_results=k)["documents"][0]
    return "\n\n".join(fragments)

EINES = [{"type": "function", "function": {"name": "search", "parameters": {...}}}]
# la resta és el bucle d'agent estàndard

La diferència respecte al RAG clàssic no és el codi de recuperació, és qui controla quan executar-lo: aquí el model, allà el pipeline. El RAG clàssic recupera sempre, independentment de si cal; l’agentic recupera quan té sentit i pot iterar. Els errors de recuperació es detecten i es corregeixen dins del bucle en lloc de propagar-se silenciosament a la resposta.

Autorització i control d’accés en agents

En sistemes multi-usuari, el rol de l’usuari autenticat (per exemple: estudiant, professor, administrador) pot influir el comportament del model, però no és el mecanisme que autoritza l’accés. Cal separar clarament dues coses: personalització del comportament i control d’accés real.

1. El prompt només adapta el comportament. El rol s’injecta al system prompt a partir de la sessió autenticada (mai des de l’entrada de l’usuari) i serveix per ajustar el to, el nivell de detall i les prioritats de resposta.

def construir_system_prompt(usuari: UsuariAutenticat) -> str:
    return (
        f"Ets un assistent de l'escola. L'usuari és {usuari.rol} ({usuari.nom}). "
        "Respon de forma adequada al seu rol."
    )

El model pot ser manipulat per injecció de prompt perquè ignori aquest rol declarat. El prompt defineix el comportament esperat; no concedeix permisos.

2. Les eines enforcen l’autorització real. Cada crida d’eina ha de rebre l’usuari autenticat des de la sessió i verificar-lo abans d’executar res. Si l’usuari no té permís, l’eina ha de retornar un error estructurat.

def consultar_horari(usuari: UsuariAutenticat, alumne_id: str) -> str:
    if usuari.rol == "estudiant" and usuari.id != alumne_id:
        return json.dumps({"error": "Accés no autoritzat: només pots consultar el teu propi horari"})
    if usuari.rol == "professor" and alumne_id not in usuari.alumnes:
        return json.dumps({"error": "Accés no autoritzat: aquest alumne no és del teu grup"})
    horari = db.get_horari(alumne_id)
    return json.dumps({"horari": horari})

Retornar l’error com a string estructurat, en lloc de llançar una excepció, permet al model llegir-lo i decidir com continuar sense trencar el bucle.

3. El RAG filtra abans de recuperar. Per als documents indexats, el control d’accés s’implementa com a filtre per metadades a la cerca vectorial. Cada fragment porta atributs que indiquen a quins rols és accessible, i el filtre s’aplica a la base de dades vectorial abans que el model vegi res.

# indexació: cada fragment porta metadades de rol
col.add(
    documents=fragments,
    embeddings=[...],
    ids=[...],
    metadatas=[{"font": doc.nom, "rol_acces": doc.rol_acces} for _ in fragments]
    # rol_acces pot ser "estudiant", "professor", "tots", "admin"
)

# cerca: filtre aplicat a la BD vectorial, abans que el model vegi res
def cercar_documents(usuari: UsuariAutenticat, query: str) -> str:
    filtres = {"rol_acces": {"$in": [usuari.rol, "tots"]}}
    vec = embedding(query)
    fragments = col.query(query_embeddings=[vec], n_results=5, where=filtres)
    return json.dumps({"fragments": fragments["documents"][0]})

Sense aquest filtre, un estudiant podria recuperar materials marcats com a exclusius per a professors si el model decidís cercar-los. Aplica tant al RAG clàssic com a l’Agentic RAG.

La regla: el rol ve de la sessió autenticada, no del prompt de l’usuari ni de cap declaració del model. La sessió és la font de veritat; el model no ha de poder elevar privilegis per cap via.

Quan usar agents

Un agent amb eines és la solució adequada quan el flux de treball és dinàmic: el nombre i l’ordre dels passos depèn de la informació obtinguda durant l’execució. Si el flux és fix i conegut d’avançada, una seqüència de crides directes és més simple, més ràpida i més fàcil de depurar. Com a criteri addicional: com més crític és el sistema, menys autònom hauria de ser l’agent. Els sistemes productius d’alta fiabilitat tendeixen cap a workflows deterministes, reservant l’autonomia agèntica per a tasques exploratòries o on els errors tenen poc cost.

Principi: comença amb un sol agent. Abans d’afegir múltiples agents especialitzats, esgota el que pots fer amb un sol agent ben dissenyat: un system prompt precís, les eines estrictament necessàries i un límit d’iteracions. La majoria de tasques que semblen requerir un sistema multi-agent es poden resoldre amb un agent sol si el disseny és prou concret. La complexitat dels sistemes multi-agent (coordinació, pas d’estat entre agents, propagació d’errors) es justifica quan el sol agent no convergeix o quan la tasca té subtasques genuïnament paral·lelitzables.

Execució sandboxada: els agents que generen i executen codi, manipulen fitxers o fan crides a APIs externes han d’executar-se en un entorn aïllat (contenidor, sandbox de sistema operatiu, entorn efímer al núvol). Un agent que executa codi arbitrari sense sandboxing és un risc de seguretat i de pèrdua de dades. La regla és que l’agent no ha de tenir accés a res que no necessiti per a la tasca concreta, i les accions irreversibles han de requerir confirmació explícita (vegeu Control humà (HITL)).

La taula que resumeix quin patró correspon a cada situació és a Workflow o agent, al final del document.

⚠️ Els agents cometen errors i poden entrar en bucles. Cal sempre definir un límit màxim d’iteracions i gestionar el cas en què l’agent no convergeixi a una resposta. Un criteri pràctic de readiness: si no pots dibuixar la màquina d’estats de l’agent (quins estats té, quines transicions, on para), el disseny no està prou concret per desplegar-lo.

📝 La pregunta d’aquesta secció, quan usar un agent dins d’un sistema LLM, és un nivell per sota d’una pregunta prèvia: quan té sentit involucrar un LLM en absolut. Per al marc de decisió complet (eines deterministes → eines semàntiques locals → agent), vegeu Codi, eina o agent.

Control humà (HITL)

El control humà (human-in-the-loop, HITL) és un patró de primera classe en sistemes agents: inserir un punt de confirmació explícit al bucle de l’agent per a accions que siguin irreversibles, d’alt risc o que requereixin judici que el sistema automatitzat no pot fer amb prou fiabilitat.

agent
  ├── acció reversible → executa directament
  └── acció irreversible (eliminar, enviar, desplegar...)
          │
          ▼
      presenta proposta a l'humà
          │
          ├── aprovat → executa
          └── rebutjat / modificat → continua el bucle amb feedback

HITL no és un fallback d’error: és un disseny deliberat per a les fronteres on l’autonomia del model no és suficient. Un agent de suport pot respondre preguntes autònomament però ha de demanar confirmació abans d’iniciar un reembossament. Un agent de desplegament pot preparar els canvis però requereix aprovació humana abans d’aplicar-los a producció.

Com implementar-lo: la manera més simple és una eina demanar_confirmació(acció, descripció) -> bool que el model invoca explícitament quan detecta que l’acció és significativa. El system prompt ha d’indicar quines accions requereixen confirmació.

def demanar_confirmacio(accio: str, descripcio: str) -> str:
    """Demana confirmació humana abans d'executar una acció irreversible.
    Cal cridar-la SEMPRE abans d'enviar correus, esborrar dades, fer transaccions
    o desplegar canvis. Retorna 'aprovat' o 'rebutjat: <motiu>'."""
    print(f"\n[Confirmació requerida] {accio}\n{descripcio}")
    resposta = input("Aprovar? [s/N/motiu]: ").strip()
    if resposta.lower() == "s":
        return "aprovat"
    return f"rebutjat: {resposta or 'sense motiu'}"

SYSTEM = """Ets un agent de suport. Abans de qualsevol acció irreversible
(reembossaments, cancel·lacions, enviaments) HAS de cridar demanar_confirmacio
i només procedir si retorna 'aprovat'."""

El patró funciona perquè el model tracta la confirmació com una eina més: si és rebutjada, el resultat ("rebutjat: ...") torna al context i l’agent pot reformular o abandonar. En entorns no interactius, input() se substitueix per una cua de revisió (Slack, email, dashboard) que bloqueja el bucle fins a rebre la decisió. Per a sistemes amb estat persistent i múltiples agents, LangGraph ofereix punts de control (checkpoints) on l’execució se suspèn fins a rebre input humà.

Regla: qualsevol acció que no es pugui desfer automàticament (enviar un missatge, esborrar dades, fer una transacció, desplegar codi) ha de tenir un punt HITL explícit en el disseny, no com a patch afegit posteriorment.

Sistemes multi-agent i coordinació

Un sol agent generalista és difícil d’optimitzar: el context s’omple, les instruccions entren en conflicte i els errors es propaguen. El patró de sistemes multi-agent divideix la tasca entre agents especialitzats que col·laboren, cadascun amb el seu propi context, eines i rol definit. Cada agent té el seu propi system prompt, configurat de forma independent. L’orquestrador no comparteix el seu system prompt amb els workers ni viceversa: el que passa entre agents és el resultat de les crides (missatges de rol tool o assistant), no les instruccions internes de cada agent.

Orquestrador (planifica i delega)
    ├── Agent investigador  (cerca, recupera, resumeix)
    ├── Agent redactor      (genera contingut estructurat)
    └── Agent verificador   (valida resultats, detecta errors)

Els patrons principals:

Orquestrador-worker: un agent central descompon la tasca i delega subtasques a agents especialitzats. Els workers retornen resultats que l’orquestrador integra. És el patró més comú i el més fàcil de depurar.

Pipeline seqüencial: cada agent transforma la sortida de l’anterior. Adequat quan les etapes són fixes i independents (extracció → validació → formatació).

Paral·lelització: subtasques independents s’assignen a agents concurrents per reduir la latència total. Equivalent a ThreadPoolExecutor però a nivell d’agent.

Handoffs: en lloc d’una orquestració centralitzada, un agent pot transferir el control a un altre directament quan detecta que la subtasca és fora del seu àmbit. L’agent origen passa l’estat acumulat (missatges, resultats d’eines, context) a l’agent destinació, que continua des d’on l’altre ha deixat. El patró és simple però requereix definir clarament quines condicions disparen el handoff i quin context cal transferir per a una transició sense pèrdua d’informació. Alguns SDKs d’agents formalitzen aquest patró com a primer tipus de ciutadà; en sistemes propis es pot implementar com una eina transferir_a_agent(agent_id, context).

LangGraph és la referència per a sistemes multi-agent en producció: modela l’execució com un graf dirigit d’estats amb nodes per agent, arestes condicionals i punts de control humans. La seva complexitat és justificada quan el bucle while simple ja no és suficient.

MCP per a integració d’eines

El Model Context Protocol (MCP) és el protocol obert i estàndard per exposar eines, recursos i dades als models de forma interoperable. On el function calling de l’API defineix com un model invoca una eina en una crida concreta, MCP defineix com un servidor exposa un conjunt d’eines de manera que qualsevol client compatible les pugui descobrir i usar sense integració ad hoc. L’especificació es versiona per data de publicació, i els SDK oficials indiquen quina versió implementen.

MCP requereix que el model tingui capacitat de tool calling: és una capa d’infraestructura per damunt d’aquesta capacitat, no una alternativa. El model no veu MCP: veu eines en el format estàndard que ja coneix i fa tool_call com de costum. El client MCP intercepta la crida, l’enruta al servidor corresponent via JSON-RPC, obté el resultat i el retorna al model com a missatge de rol tool. Des del punt de vista del bucle d’agent, una eina MCP és indistingible d’una funció local: el bucle rep un string de resultat i continua igual. La diferència és operacional: una eina local l’escrius i la mantens tu, s’executa al mateix procés; una eina MCP l’escriu i la manté un tercer, s’executa en un procés separat. MCP és, en essència, una manera d’empaquetar i compartir eines perquè no s’hagin de reimplementar per cada agent o cada projecte.

Client MCP (backend, IDE, agent)
    │  JSON-RPC sobre stdio (local) o HTTP (remot)
    ▼
Servidor MCP
    ├── tools     → funcions que el model pot cridar
    ├── resources → dades que el client pot llegir
    └── prompts   → plantilles reutilitzables

El nucli del protocol és sense estat. Cada petició és autocontinguda i es respon per si sola, com qualsevol API HTTP: no cal mantenir una sessió oberta entre client i servidor. La conseqüència pràctica és operativa més que conceptual: un servidor MCP es pot desplegar, balancejar i escalar amb la mateixa infraestructura HTTP que qualsevol altre servei, sense connexions de llarga durada ni afinitat de sessió.

Sobre aquest nucli, el protocol defineix un mecanisme d’extensions que el client i el servidor negocien en connectar-se. Dues especialment rellevants per a agents:

  • Tasks: per a feina llarga. En lloc de bloquejar la crida fins que acaba, el servidor pot respondre amb un identificador de tasca, i el client en consulta l’estat, l’actualitza o la cancel·la. És el que permet exposar com a eina una operació que triga minuts sense mantenir res obert.
  • Apps: permet que el servidor retorni interfície renderitzable, no només text, per a eines que necessiten que l’usuari vegi o triï alguna cosa.

L’autorització dels servidors remots s’articula sobre OAuth 2.0 i OpenID Connect, de manera que un servidor MCP es protegeix amb les mateixes eines que qualsevol altra API de l’organització.

Hi ha milers de servidors MCP de codi obert publicats per la comunitat que donen accés a sistemes habituals (bases de dades, sistemes de fitxers, APIs externes, eines de desenvolupament). Consumir-los permet integrar capacitats sense implementar la integració des de zero.

MCP aporta valor quan el sistema necessita reutilitzar eines entre múltiples agents o clients, quan es volen consumir servidors de tercers, o quan l’organització vol un punt únic de govern (autenticació, auditoria, configuració) per a totes les integracions d’eines.

Servidor MCP propi

Quan les eines que necessita l’agent són específiques del domini (una BD interna, una API privada, una operació de negoci), cal escriure un servidor propi. La interfície d’alt nivell del SDK oficial de Python redueix això a decorar funcions: el servidor deriva l’esquema (nom, paràmetres, tipus, descripció) de les anotacions de tipus i del docstring, i és aquest esquema el que el client exposarà al model.

from mcp.server import MCPServer

mcp = MCPServer("inventari")

@mcp.tool()
def stock(sku: str) -> int:
    """Retorna les unitats disponibles d'un SKU al magatzem."""
    return db.query("SELECT qty FROM stock WHERE sku = ?", sku).scalar()

@mcp.resource("politica://devolucions")
def politica_devolucions() -> str:
    """Text vigent de la política de devolucions."""
    return open("docs/devolucions.md").read()

if __name__ == "__main__":
    mcp.run()   # transport stdio per defecte

Amb transport stdio, qui llança el servidor és el client, com a subprocés, i li parla per l’entrada i la sortida estàndard. La conseqüència pràctica sorprèn el primer dia: dins d’un servidor stdio no hi pot haver cap print(), perquè stdout és el canal del protocol. Els missatges de registre van a stderr.

Per provar les eines a mà abans de connectar-hi cap agent, l’ecosistema té un inspector web. Aquesta és l’ordre que funciona, des de la carpeta del servidor i amb l’entorn actiu:

npx @modelcontextprotocol/inspector --cli python server.py \
    --connect-timeout 90000 --method tools/list

⚠️ No facis servir mcp dev server.py: llança el servidor en un entorn aïllat que només conté el paquet mcp, sense les dependències del teu projecte, i es queda a mig connectar. El mode web, sense --cli, es rendeix als 15 segons, que en màquines lentes és menys del que triga el servidor a arrencar.

Fixa’t que l’inspector no és imprescindible: tools/list és una crida del protocol com qualsevol altra, i un script de vint línies amb el client del SDK et dona la mateixa informació sense dependre de Node.

Decisions clau de disseny:

  • Granularitat de les eines: una eina per operació de negoci, no una per endpoint HTTP. El model raona millor amb stock(sku) que amb http_get(url).
  • Descripcions com a contracte: el docstring és el que veu el model per decidir si crida l’eina. Ha de dir què fa i quan usar-la, no com s’implementa.
  • Tools vs. resources: tools són accions que el model decideix invocar (poden tenir efectes); resources són dades que el client llegeix proactivament i injecta al context.
  • Transport: stdio per a servidors locals (un per usuari, llançats pel client); HTTP quan el servidor és compartit i necessita autenticació, auditoria i escalat independent.
  • Operacions llargues: si una eina pot trigar més que un timeout HTTP raonable, exposa-la amb l’extensió Tasks en lloc de fer esperar la crida.

Control de GUI (Computer Use)

Una extensió del patró d’eines és la capacitat que alguns models han adquirit de controlar interfícies gràfiques: prendre captures de pantalla, identificar elements visuals, i generar accions de ratolí i teclat. Això s’anomena computer use o GUI automation, present en alguns models comercials.

Com funciona: l’entorn exposa eines de captura i acció. El model observa la captura, raona sobre l’estat de la interfície i decideix l’acció.

screenshot → model → acció (click / type / scroll) → screenshot → model → ...

Quan té sentit: automatitzar tasques en aplicacions sense API (formularis web, aplicacions d’escriptori heretades), navegació web autònoma, scripts d’acceptació sobre interfícies gràfiques que no exposen cap endpoint.

Limitacions importants:

  • Lent i car: cada pas és una crida multimodal completa. No és adequat per a tasques que es puguin fer amb una API.
  • Fràgil davant canvis de UI: un canvi de layout o de color pot confondre el model sense cap avís.
  • Alt risc si no s’aïlla: un agent amb accés a una GUI pot fer accions irreversibles (enviar correus, esborrar fitxers, fer transaccions). Cal sandboxing estricte i punts de confirmació humana explícits per a accions destructives.

Regla: sempre que existeixi una API, és el patró correcte. Computer use és el recurs quan no n’hi ha cap altra alternativa.

Guia de decisió de patró

Aquest document acaba amb les dues decisions que has de prendre, en ordre: primer qui controla el flux, i després com arriba al model la informació que no té als pesos.

Workflow o agent

La primera decisió és si el flux el fixa el codi o el decideix el model en temps d’execució. El criteri de fons és quantes coses saps abans d’executar: si saps quins passos calen i en quin ordre, no deleguis aquesta decisió al model.

SituacióPatró recomanat
Passos fixos, ordre conegutCrides seqüencials directes al model (cadenes de crides)
La mateixa tasca, resolta per mètodes de cost molt diferentCascada de models
El model tria entre poques opcions i el codi executaTool calling dins de workflows amb límit dur d’iteracions
Passos variables, l’agent decideix quan pararBucle d’agent amb eines, començant per un sol agent
Accions irreversibles o d’alt riscBucle d’agent amb HITL explícit
Subtasques genuïnament paral·lelitzablesSistemes multi-agent
Flux amb bifurcacions condicionals i estat persistentGraf d’estats (LangGraph)

En cas de dubte, el graó més baix de la taula que resolgui el problema és el correcte: cada esglaó cap avall afegeix autonomia al model i, amb ella, més maneres de fallar que has de preveure.

Com arriba la informació al model

Establerta la primera tria, en queda una de transversal: com donar al model accés a informació que no té als pesos. Un workflow amb RAG recupera context abans que el model raoni, amb el pipeline decidint què; un agent amb eines de cerca deixa que el model decideixi durant el raonament si cal recuperar i amb quina query; la injecció directa del corpus sencer al system prompt evita la recuperació quan els tokens hi caben.

La tria depèn del corpus, les consultes i el tipus de raonament necessari:

  • Si el corpus cap sencer a la finestra de context, injectar-lo directament al system prompt és més simple i fiable. La finestra dels models actuals (de centenars de milers fins a milions de tokens) cobreix casos que fa dos anys requerien RAG obligatòriament: una base de coneixement de 50.000 paraules, tota la documentació d’una API, o el codi d’un projecte mitjà.
  • Si el corpus és gran, injectar-lo tot no és gratuït: el cost per crida escala linealment amb els tokens d’entrada, i la qualitat es degrada pel context rot i el lost in the middle (vegeu Estat, memòria i multi-torn). RAG continua sent millor perquè recupera selectivament el rellevant per a cada consulta, reduint alhora cost i soroll.
  • Si les consultes requereixen raonament relacional entre entitats (qui depèn de qui, quines decisions estan connectades), la cerca vectorial no és suficient perquè la similitud semàntica no captura relacions estructurals. GraphRAG recupera per travessia, no per similitud.
  • Si les dades canvien en temps real o la recuperació ha de ser condicional, el patró d’eines és més adequat: el model decideix quan i com cercar, pot iterar sobre els resultats, i pot accedir a fonts que no estan indexades en cap corpus estàtic.
  • Si el model produeix respostes pobres fins i tot quan la informació rellevant és al context, el problema no és accés a dades sinó comprensió del domini: terminologia especialitzada, patrons de raonament específics, o un estil de resposta que el model no ha vist prou durant l’entrenament. En aquests casos el fine-tuning és més adequat. RAG proporciona dades en temps d’inferència; fine-tuning internalitza patrons en temps d’entrenament.
  • Si les consultes requereixen agregar o comparar informació a través de tot el corpus (comptar ocurrències, detectar contradiccions, identificar patrons globals), RAG no ajuda: recuperar els fragments més similars no és suficient quan la resposta depèn de llegir-ho tot. Cal injecció completa o fine-tuning.

📝 Per a la comparació entre RAG i fine-tuning com a estratègies d’adaptació, vegeu Fine-tuning i adaptació.

Referències

Last change: , commit: f13bff8