Orquestració amb workflows i agents
- Workflows deterministes
- Agents i tool use
- Guia de decisió de patró
- Referències
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, consulta Crides a un LLM: disseny de prompts i integració. Per a la infraestructura (servidors, maquinari, models), consulta Arquitectura de sistemes LLM.
El document s’estructura en tres blocs:
- Workflows deterministes — flux determinista controlat pel codi, amb el model com a processador semàntic
- Agents i tool use — flux dinàmic controlat pel model, amb eines i bucles de raonament
- Guia de decisió de patró — guia de decisió per orientar la tria
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 addicional | Tasques 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 prompt | Glossaris, polítiques, esquemes o regles de negoci petits i estables. S’escriu una vegada i no canvia entre crides. |
| Dades pre-recuperades per regla | L’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-shot | L’esquema de sortida o el to tenen ambigüitats que cal resoldre mostrant casos concrets — vegeu Exemples com a especificació (few-shot). |
| Adjunts multimodals | Imatges, 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 → JSON | Extracció d’entitats, normalització de formularis, estructuració de transcripcions |
| JSON → JSON | Classificació, inferència semàntica, adaptació entre esquemes |
| Text → Text | Resum, traducció, reescriptura amb restriccions |
| JSON → Text | Generació 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 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 anidiat. 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 els proveïdors principals (Anthropic inclòs) 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 (instructor, Pydantic, 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 Crides a un LLM.
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 irrelevants 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:
| Criteri | Favorable al model sol | Risc |
|---|---|---|
| Representació a l’entrenament | Domini popular amb literatura abundant | Subdisciplina nínxol o molt especialitzada |
| Estabilitat del coneixement | Principis establerts i estables | Recerca recent o normativa en evolució |
| Context individual | Pregunta genèrica de domini | Requereix dades de l’usuari (historial, biomecànica, medicació) |
| Necessitat de citació | Resposta orientativa | Cal font autoritzativa citable |
| Cost d’error | Baixa conseqüència si és incorrecte | Alta 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 — afegeix accés a fonts que no estan 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, vector databases i avaluació es tracten a Integració i execució.
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 predible: 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 moderns (OpenAI Agents SDK, LangGraph, l’API de tool use d’Anthropic) 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.
Coerció d’esquema — declarar una eina fictícia només per forçar JSON vàlid — és un tercer ús del mecanisme, però en 2026 ja é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, descrit a Quan instructor no és disponible.
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. Anthropic i OpenAI tenen mecanismes de prompt caching que redueixen molt el cost dels prefixos repetits. 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 — é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 (predible, 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()
})
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 — no 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).
| Situació | Patró recomanat |
|---|---|
| Passos fixes, ordre conegut | Crides seqüencials directes al model |
| Passos variables, l’agent decideix | Bucle d’agent amb eines (sol agent primer) |
| Accions irreversibles o d’alt risc | Bucle d’agent + HITL explícit |
| Subtasques genuïnament paral·lelitzables | Sistemes multi-agent (vegeu secció següent) |
| Flux amb bifurcacions i estat persistent | Graf d’estats (LangGraph) |
⚠️ 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), consulta 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’orchestrador 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.
Orchestrador (planifica i delega)
├── Agent investigador (cerca, recupera, resumeix)
├── Agent redactor (genera contingut estructurat)
└── Agent verificador (valida resultats, detecta errors)
Els patrons principals:
Orchestrador-worker: un agent central descompon la tasca i delega subtasques a agents especialitzats. Els workers retornen resultats que l’orchestrador 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ó).
Parallelització: 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), publicat per Anthropic el novembre de 2024 i adoptat pels principals proveïdors (OpenAI, Google, Microsoft) al llarg de 2025, és el protocol 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.
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 la escrius i mantens tu, s’executa al mateix procés; una eina MCP la escriu i 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 / SSE / HTTP
▼
Servidor MCP
├── tools → funcions que el model pot cridar
├── resources → dades que el client pot llegir
└── prompts → plantilles reutilitzables
Hi ha milers de servidors MCP OSS 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 FastMCP, inclosa al SDK oficial de Python, redueix això a decorar funcions: el servidor en deriva l’esquema (nom, paràmetres, tipus, descripció) que el client exposarà al model.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("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
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 ambhttp_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:
toolssón accions que el model decideix invocar (poden tenir efectes);resourcessón dades que el client llegeix proactivament i injecta al context. - Transport:
stdioper a servidors locals (un per usuari, llançats pel client);HTTP/SSEquan el servidor és compartit i necessita autenticació, auditoria i escalat independent.
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 llegàcies), 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ó
Workflow vs agent és la primera decisió: el codi controla el flux o el model el decideix dinàmicament. Establerta aquesta tria, sovint en queda una de transversal: com donar al model accés a informació que no té als pesos. 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 del patró d’accés a la informació 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ó, consulta Fine-tuning i adaptació.