La integració de 03-01 funciona, però té dues debilitats que separen un prototip d'un sistema usable. La primera la nota qualsevol empleat de Nubelia a la seva primera pregunta: DocuBot es queda en silenci uns quants segons i deixa anar la resposta de cop. La segona la notarà l'equip el pitjor dia possible: quan l'API retorni un 429 per rate limit (límit de peticions) o un error de servidor, el bot simplement explotarà amb un traceback. En aquesta lliçó resolem totes dues: streaming perquè la resposta flueixi a mesura que es genera, i una estratègia completa d'errors, reintents amb backoff i degradació elegant perquè DocuBot falli bé quan alguna cosa falli — perquè alguna cosa, tard o d'hora, fallarà.
Contingut
- Per què streaming: la percepció de latència
- Consumir un stream en Python
- Streaming i sortides JSON: validar al final
- Taxonomia d'errors de l'API
- Reintents amb backoff exponencial i jitter
- Timeouts, idempotència i què NO reintentar
- Circuit breaker: deixar d'insistir (concepte)
- Degradació elegant a DocuBot
Per què streaming: la percepció de latència
Un LLM genera la resposta token a token, però la crida de 03-01 espera que tots els tokens existeixin abans de retornar res. Si la resposta triga 6 segons a generar-se, l'usuari mira una pantalla buida 6 segons.
L'streaming canvia el contracte: el servidor envia cada fragment tan bon punt el genera i el client el consumeix sobre la marxa. El temps total és el mateix (els tokens es generen al mateix ritme), però la percepció és radicalment diferent:
| Mètrica | Sense streaming | Amb streaming |
|---|---|---|
| Temps fins al primer text visible | 6 s (tot o res) | ~0,5–1,5 s (primer token) |
| Temps fins a la resposta completa | 6 s | 6 s (igual) |
| Sensació de l'usuari | "Està penjat" | "Està escrivint" |
| Possibilitat d'avortar una resposta que va malament | No | Sí, a mitges |
Per al xat de DocuBot, l'streaming és obligatori: un agent de suport amb un client al telèfon no tolera mirar una rodeta. La regla general: interfície conversacional → streaming; procés per lots (classificar 500 tiquets de nit) → sense streaming, perquè allà ningú no mira la pantalla.
Hi ha un segon motiu, més tècnic: en respostes llargues, mantenir una connexió HTTP oberta esperant la resposta completa acaba topant amb timeouts de xarxa intermedis. El degoteig constant de l'streaming manté la connexió viva.
Consumir un stream en Python
L'SDK exposa un context manager que itera fragments de text. La forma més directa:
# streaming.py
from client import client, MODEL, extreure_text
from prompts import SYSTEM_DOCUBOT, prompt_resposta
def stream_resposta(pregunta: str, documentacio: str) -> str:
"""Imprimeix la resposta a mesura que es genera i retorna el text complet."""
with client.messages.stream(
model=MODEL,
max_tokens=1024,
system=SYSTEM_DOCUBOT,
messages=[{"role": "user", "content": prompt_resposta(pregunta, documentacio)}],
) as stream:
fragments = []
for text in stream.text_stream:
print(text, end="", flush=True) # mostrar a l'usuari JA
fragments.append(text) # i acumular per processar després
# En esgotar l'stream podem demanar el missatge final complet,
# amb usage i stop_reason com en una crida normal.
final = stream.get_final_message()
print() # salt de línia en acabar
print(f"[tokens sortida: {final.usage.output_tokens}, aturada: {final.stop_reason}]")
return "".join(fragments)Punts clau del codi:
client.messages.stream(...)accepta els mateixos paràmetres quemessages.create(); només canvia com es consumeix la resposta.stream.text_streamés un iterador de fragments de text. Cada iteració lliura un o pocs tokens;flush=Trueforça que es pintin immediatament a la consola (sense això, Python pot retenir la sortida en un buffer i arruïnar l'efecte).- Acumulem els fragments en una llista i els unim al final: mostrar i processar són dues necessitats diferents i el patró "imprimeix i acumula" cobreix totes dues.
stream.get_final_message()retorna, quan l'stream ha acabat, el mateix objecte de resposta que ja coneixes de 03-01:usage,stop_reason,content. L'streaming no et fa perdre la telemetria.
Per sota, el proveïdor envia esdeveniments tipats (inici de missatge, inici de bloc, delta de text, fi de bloc, fi de missatge) sobre una connexió d'esdeveniments de servidor (SSE). text_stream és la drecera còmoda que filtra només els deltes de text; si algun dia necessites els esdeveniments crus (per exemple, per distingir tipus de bloc), el mateix stream permet iterar-los un a un. El concepte és idèntic en qualsevol proveïdor: OpenAI i Gemini també emeten fragments incrementals que es consumeixen en un bucle.
Streaming i sortides JSON: validar al final
Aquí apareix una limitació pràctica important per a DocuBot: el nostre contracte de 02-03 exigeix un JSON complet, i un JSON parcial no és parsejable. {"categoria": "facturacio", "respo no es pot validar; només el document sencer.
La conseqüència de disseny:
- Pots mostrar els fragments a mesura que arriben (percepció de latència).
- Has de validar només al final, amb el text complet acumulat i el parser de 02-03.
import json
from prompts import RESPOSTA_FALLBACK
from validacio import netejar_json, validar_contracte
def stream_docubot(pregunta: str, documentacio: str) -> dict:
text = stream_resposta(pregunta, documentacio) # mostra en viu i acumula
try:
return validar_contracte(json.loads(netejar_json(text)))
except (json.JSONDecodeError, TypeError):
return RESPOSTA_FALLBACKAixò crea una tensió d'experiència d'usuari: si la sortida és el JSON cru, l'usuari veuria claus i cometes fluint per pantalla. Les solucions habituals, de més simple a més elaborada:
- No fer streaming a les rutes JSON i reservar-lo per a respostes de text lliure. Simple i suficient per a molts casos.
- Streaming intern, render al final: consumeixes l'stream (la connexió es manté viva, pots mesurar el primer token) però només mostres a l'usuari un indicador de progrés fins a validar.
- Extreure el camp
respostaen viu amb un parser incremental que detecta quan és "dins" del valor d'aquell camp i emet només això. És fràgil i rarament compensa; menciona-ho en una revisió de disseny abans d'intentar-ho.
Per a DocuBot adoptem l'opció 2: és la que manté el contracte intacte sense renunciar a la connexió en viu.
Taxonomia d'errors de l'API
Tota API de LLM retorna errors HTTP amb la mateixa lògica de fons: 4xx = el problema és teu, 5xx = el problema és seu. Els que veuràs a la pràctica:
| Codi | Nom | Causa típica | Reintentar? |
|---|---|---|---|
| 400 | Petició invàlida | Paràmetre mal format; entrada que excedeix la finestra de context | ❌ No: fallarà igual. Corregeix la petició (o retalla el context, 03-03) |
| 401 | Autenticació | Clau absent, mal escrita o revocada | ❌ No: revisa la variable d'entorn |
| 403 | Permisos | La clau no té accés a aquell model o funció | ❌ No: revisa el pla/permisos |
| 404 | No trobat | Identificador de model inexistent (errada) | ❌ No: corregeix el nom |
| 413 | Petició massa gran | El cos de la petició excedeix límits | ❌ No: redueix l'entrada |
| 429 | Rate limit | Massa peticions o tokens per minut | ✅ Sí, amb espera (respectant retry-after si arriba) |
| 500 | Error intern | Fallada transitòria del proveïdor | ✅ Sí, amb backoff |
| 529 | Sobrecàrrega | El servei està saturat (equivalent a "torna d'aquí a una estona") | ✅ Sí, amb backoff més generós |
I una fila més que no és HTTP: els errors de connexió (DNS, timeout de xarxa, connexió tallada a mig stream). No hi ha codi perquè la petició no va arribar o no va tornar; també són reintentables.
L'SDK tradueix aquests codis a excepcions tipades, cosa que permet un try/except llegible en comptes de comparar números:
import anthropic
try:
resposta = client.messages.create(...)
except anthropic.BadRequestError as e: # 400: context excedit, paràmetre invàlid
print(f"Petició invàlida: {e.message}")
except anthropic.AuthenticationError: # 401
print("Clau d'API invàlida: revisa ANTHROPIC_API_KEY")
except anthropic.RateLimitError as e: # 429
espera = int(e.response.headers.get("retry-after", "60"))
print(f"Límit assolit; el servidor suggereix esperar {espera}s")
except anthropic.APIStatusError as e: # resta de codis HTTP
if e.status_code >= 500:
print(f"Error del proveïdor ({e.status_code}): reintentable")
else:
print(f"Error de client ({e.status_code}): {e.message}")
except anthropic.APIConnectionError: # la xarxa, no l'API
print("Error de xarxa: reintentable")Fixa't en l'ordre: de l'excepció més específica a la més general. RateLimitError abans que APIStatusError, perquè la primera és subtipus conceptual de la segona i un except genèric col·locat abans se l'empassaria. Aquest patró de jerarquia d'excepcions existeix a tots els SDKs de tots els proveïdors amb noms diferents; la taxonomia 4xx/5xx/xarxa és universal.
El 400 per context excedit mereix menció a part: avui és improbable (enviem poca cosa), però a 03-03, quan acumulem historial de conversa, es convertirà en un risc estructural. Recorda-ho.
Reintents amb backoff exponencial i jitter
Davant d'un 429 o un 5xx, reintentar immediatament és contraproduent: si el servidor està saturat, mil clients reintentant alhora el saturen més. L'estratègia estàndard té dos ingredients:
- Backoff exponencial: esperar cada vegada més entre intents (1s, 2s, 4s, 8s...). Dona temps que el problema transitori es resolgui.
- Jitter (aleatorietat): sumar un component aleatori a cada espera. Sense això, tots els clients que van fallar alhora reintenten alhora, creant onades sincronitzades de trànsit (el problema del "ramat atronador").
Implementació completa per a DocuBot:
# reintents.py
import random
import time
import anthropic
from client import client
def cridar_amb_reintents(
max_reintents: int = 4,
espera_base: float = 1.0,
espera_maxima: float = 30.0,
**kwargs,
):
"""Envia una petició a l'API amb backoff exponencial + jitter.
Reintenta NOMÉS errors transitoris (429, 5xx, xarxa). Els errors
de client (400, 401, 403, 404...) es propaguen immediatament.
"""
ultima_excepcio = None
for intent in range(max_reintents):
try:
return client.messages.create(**kwargs)
except anthropic.RateLimitError as e:
ultima_excepcio = e
# Si el servidor dicta quant esperar, obeir
retry_after = e.response.headers.get("retry-after")
if retry_after is not None:
espera = min(float(retry_after), espera_maxima)
else:
espera = None # calcular amb backoff a sota
except anthropic.APIStatusError as e:
if e.status_code < 500:
raise # 4xx diferent de 429: reintentar no ho arreglarà
ultima_excepcio = e
espera = None
except anthropic.APIConnectionError as e:
ultima_excepcio = e
espera = None
if espera is None:
# backoff exponencial: base * 2^intent, acotat, + jitter
espera = min(espera_base * (2 ** intent), espera_maxima)
espera += random.uniform(0, espera_base)
print(f"[intent {intent + 1}/{max_reintents} fallit; espero {espera:.1f}s]")
time.sleep(espera)
# Esgotats els intents: propagar l'últim error perquè el caller decideixi
raise ultima_excepcioDecisions de disseny que has de poder justificar:
- Només es reintenta el transitori. Un 401 reintentat 4 vegades són 4 fracassos idèntics més lents. El
raiseprimerenc als 4xx no-429 és tan important com el bucle. retry-aftermana. Quan el servidor et diu quant esperar, la teva heurística sobra.- L'espera està acotada (
espera_maxima): sense topall, l'intent 6 esperaria 32s, el 8 més de 2 minuts, i l'usuari ja haurà marxat. - L'últim error es propaga. La funció de reintents no decideix què mostrar a l'usuari; això és de la capa de degradació (última secció).
Important: els SDKs oficials ja porten reintents automàtics configurables — al d'Anthropic, anthropic.Anthropic(max_retries=4) reintenta 429/5xx/xarxa amb backoff per tu, i altres proveïdors ofereixen l'equivalent. En producció, fes servir primer el de l'SDK i escriu el teu només si necessites un comportament que aquell no cobreix (logging propi, mètriques, polítiques per tipus de consulta). L'hem implementat a mà perquè entendre què fa l'SDK per dins és el que et permetrà configurar-lo amb criteri i depurar-lo quan alguna cosa no quadri.
Timeouts, idempotència i què NO reintentar
Timeouts. Un timeout limita quant esperes una resposta abans de rendir-te. Sense això, una connexió penjada bloqueja el teu procés indefinidament. El client permet configurar-lo globalment o per crida:
client = anthropic.Anthropic(timeout=30.0) # segons, per a totes les crides # o puntualment, sense tocar el client global: client.with_options(timeout=10.0).messages.create(...)
Criteri: el timeout ha de ser coherent amb el cas d'ús. El classificador (20 tokens de sortida) no hauria de trigar més d'uns segons: timeout curt. Una resposta llarga amb streaming pot trigar legítimament més: timeout més generós o streaming, que reparteix l'espera. Tingues en compte que un timeout esgotat sol comptar-se com a error reintentable, així que el temps total en el pitjor cas és aproximadament timeout × (reintents + 1) — dimensiona tots dos junts.
Idempotència. Una operació és idempotent si executar-la dues vegades produeix el mateix efecte que una. Les crides de DocuBot (pregunta → resposta) ho són: reintentar és segur, com a molt pagues dues vegades la generació. Però compte amb el patró general: si la teva crida al LLM està acoblada a un efecte secundari ("genera el text I envia el correu", "respon I descompta crèdit al client"), un reintent pot duplicar l'efecte. Regla de disseny: separa la generació (reintentable) de l'efecte secundari (executar una sola vegada, després de validar). Al mòdul 5, quan el LLM cridi eines que modifiquen estat, aquesta regla serà crítica.
Què no reintentar, resumit:
- Errors 4xx que no siguin 429 (fallaran igual).
- Peticions el contingut de les quals ha violat una política del proveïdor (fallarà igual i embruta les teves mètriques).
- Operacions no idempotents sense protecció (risc d'efecte duplicat).
- I no reintentar infinitament: tot bucle de reintents necessita un límit i una sortida cap a la degradació.
Circuit breaker: deixar d'insistir
Els reintents resolen fallades puntuals. Però si el proveïdor porta 10 minuts caigut, cada petició de cada usuari recorrerà el seu cicle complet de 4 intents amb esperes — multiplicant latència i trànsit inútil just quan el sistema aliè està pitjor.
El patró circuit breaker (tallacircuits) afegeix memòria compartida sobre la salut del servei. Conceptualment, tres estats:
- Tancat (normal): les peticions passen. Es compten les fallades recents.
- Obert: en superar un llindar de fallades (p. ex. 5 fallades en 60 segons), el circuit "salta": durant un temps de refredament, les peticions ni s'intenten — van directes a la resposta degradada. Fallar ràpid és l'objectiu.
- Semiobert: esgotat el refredament, es deixa passar una petició de prova. Si funciona, el circuit es tanca (retorn a la normalitat); si falla, es reobre.
No l'implementarem aquí (biblioteques com pybreaker el porten fet, i l'observabilitat que l'alimenta es veu a 06-04); el que has de retenir és la divisió de responsabilitats: reintents per a la fallada puntual, circuit breaker per a la caiguda sostinguda. Un sistema madur té tots dos.
Degradació elegant a DocuBot
Tot l'anterior convergeix en una pregunta de producte: què veu l'empleat de Nubelia quan, malgrat tot, la crida falla? Un traceback és inacceptable; un silenci, gairebé pitjor. La resposta ja la vam dissenyar a 02-03: RESPOSTA_FALLBACK, un objecte que compleix el contracte JSON amb valors conservadors. Ara li donem el seu paper definitiu com a última baula de la cadena:
# docubot.py — la capa que uneix totes les peces del mòdul
import json
import anthropic
from client import MODEL, extreure_text
from reintents import cridar_amb_reintents
from prompts import SYSTEM_DOCUBOT, prompt_resposta, RESPOSTA_FALLBACK
from validacio import netejar_json, validar_contracte
def respondre(pregunta: str, documentacio: str) -> dict:
"""Pipeline complet: crida amb reintents -> validació -> fallback.
Passi el que passi, SEMPRE retorna un dict que compleix el contracte.
"""
try:
resposta = cridar_amb_reintents(
model=MODEL,
max_tokens=1024,
system=SYSTEM_DOCUBOT,
messages=[{"role": "user", "content": prompt_resposta(pregunta, documentacio)}],
)
except anthropic.APIError:
# Reintents esgotats o error no reintentable: degradar, no explotar.
# (A 06-04 hi afegirem registre i mètriques.)
return RESPOSTA_FALLBACK
if resposta.stop_reason == "max_tokens":
return RESPOSTA_FALLBACK
try:
return validar_contracte(json.loads(netejar_json(extreure_text(resposta))))
except (json.JSONDecodeError, TypeError):
return RESPOSTA_FALLBACKLa garantia que ofereix aquesta funció és la que defineix un sistema robust: el seu tipus de retorn no depèn que el món funcioni. Xarxa caiguda, proveïdor saturat, JSON corrupte, resposta tallada — el caller sempre rep un contracte vàlid, i la interfície sempre té alguna cosa digna per mostrar ("Ara mateix no puc consultar la documentació; torna-ho a provar d'aquí a uns minuts"). Els detalls de quantes vegades es va degradar i per què pertanyen a l'observabilitat (06-04); el fet que mai no exploti pertany a aquesta lliçó.
Errors Comuns i Consells
- Reintentar els 400. És l'error de novell més car: un bucle reintentant una petició invàlida 4 vegades amb esperes de 30s converteix un bug d'un segon en minuts de latència. 4xx (llevat de 429) → corregir, no insistir.
- Backoff sense jitter. Funciona a la teva prova amb un sol client i fracassa en producció amb cent: tots reintenten sincronitzats. El
random.uniformno és decoratiu. - Ignorar
retry-after. El servidor t'està dient exactament quant esperar; el teu exponencial és una suposició. - Validar JSON fragment a fragment en streaming. Un JSON parcial mai no parseja; acumula i valida al final. Si veus
JSONDecodeErrorconstants a la ruta d'streaming, gairebé segur que és això. - Oblidar
flush=Trueen imprimir l'stream: el buffer de sortida reté els fragments i l'"efecte màquina d'escriure" desapareix, amb la qual cosa l'streaming perd la seva raó de ser visual. - Duplicar reintents (els de l'SDK activats I els teus al voltant): 4 × 4 = 16 intents reals. Decideix una capa i desactiva o limita l'altra (
max_retries=0si el control el porta la teva funció). - Consell: provoca els errors en desenvolupament, no esperis a producció. Una clau falsa et dona el 401;
max_tokens=10et dona el tall; untimeout=0.001et dona l'error de connexió. Cada branca de l'excepts'ha d'haver executat almenys una vegada davant dels teus ulls. - Consell: registra sempre l'identificador de petició (
resposta.ido el de l'excepció) en reportar errors al proveïdor: és el que els permet rastrejar el teu cas.
Exercicis
Exercici 1
Escriu stream_amb_metriques(pregunta, documentacio) que consumeixi l'stream i retorni un diccionari amb: text (complet), t_primer_token (segons des de l'enviament fins al primer fragment) i t_total (segons fins al final). Fes servir time.perf_counter(). Executa la mateixa pregunta amb i sense streaming i compara quina mètrica millora i quina no.
Exercici 2
Modifica cridar_amb_reintents() perquè accepti un paràmetre en_fallar (callable opcional) que s'invoqui a cada intent fallit amb (intent, excepcio, espera). Fes-lo servir per imprimir una traça llegible. Després provoca un 401 amb una clau falsa i comprova que no es reintenta (la traça no ha d'aparèixer).
Exercici 3
Dissenya (en pseudocodi o Python) un circuit breaker mínim per a DocuBot: classe Tallacircuits amb llindar_fallades=5, finestra_segons=60 i refredament_segons=120, i mètodes permetre() -> bool, registrar_exit() i registrar_fallada(). Integra'l conceptualment a respondre(): on es consulta i on es registra?
Solucions
Solució 1:
import time
def stream_amb_metriques(pregunta, documentacio):
inici = time.perf_counter()
t_primer_token = None
fragments = []
with client.messages.stream(
model=MODEL, max_tokens=1024, system=SYSTEM_DOCUBOT,
messages=[{"role": "user", "content": prompt_resposta(pregunta, documentacio)}],
) as stream:
for text in stream.text_stream:
if t_primer_token is None:
t_primer_token = time.perf_counter() - inici
fragments.append(text)
return {
"text": "".join(fragments),
"t_primer_token": t_primer_token,
"t_total": time.perf_counter() - inici,
}Observaràs que t_total és similar amb i sense streaming, però t_primer_token n'és una fracció petita. Aquesta diferència és la millora de percepció — i a 03-04 l'anomenarem pel seu nom (TTFT) i la mesurarem sistemàticament.
Solució 2:
def cridar_amb_reintents(max_reintents=4, espera_base=1.0,
espera_maxima=30.0, en_fallar=None, **kwargs):
ultima_excepcio = None
for intent in range(max_reintents):
try:
return client.messages.create(**kwargs)
except anthropic.RateLimitError as e:
ultima_excepcio = e
except anthropic.APIStatusError as e:
if e.status_code < 500:
raise # aquí mor el 401: sense reintent
ultima_excepcio = e
except anthropic.APIConnectionError as e:
ultima_excepcio = e
espera = min(espera_base * (2 ** intent), espera_maxima)
espera += random.uniform(0, espera_base)
if en_fallar:
en_fallar(intent + 1, ultima_excepcio, espera)
time.sleep(espera)
raise ultima_excepcio
# Ús:
# cridar_amb_reintents(en_fallar=lambda i, e, s: print(f"[{i}] {type(e).__name__}, espero {s:.1f}s"), ...)Amb la clau falsa, AuthenticationError (que hereta de l'error d'estat amb codi < 500 a la pràctica de l'SDK; si la teva versió l'exposa com a classe pròpia, afegeix el seu except amb raise) es propaga al primer intent: zero traces, zero esperes. Exactament el comportament correcte.
Solució 3:
import time
class Tallacircuits:
def __init__(self, llindar_fallades=5, finestra_segons=60, refredament_segons=120):
self.llindar = llindar_fallades
self.finestra = finestra_segons
self.refredament = refredament_segons
self.fallades = [] # timestamps de fallades recents
self.obert_fins = 0.0 # instant en què acaba el refredament
def permetre(self) -> bool:
return time.monotonic() >= self.obert_fins
def registrar_exit(self):
self.fallades.clear()
def registrar_fallada(self):
ara = time.monotonic()
self.fallades = [t for t in self.fallades if ara - t < self.finestra]
self.fallades.append(ara)
if len(self.fallades) >= self.llindar:
self.obert_fins = ara + self.refredament
self.fallades.clear()Integració a respondre(): es consulta al principi (if not breaker.permetre(): return RESPOSTA_FALLBACK — fallar ràpid, sense tocar la xarxa) i es registra al final de la crida (èxit després del create, fallada a l'except). L'estat semiobert sorgeix sol: passat el refredament, permetre() torna a donar True i la petició següent actua de prova.
Conclusió
DocuBot ja es comporta com un sistema i no com un script: l'streaming fa visible la generació des del primer token (validant el JSON només al final, amb el parser de 02-03 intacte), la taxonomia d'errors separa el que es corregeix (4xx) del que s'espera (429, 5xx, xarxa), els reintents amb backoff exponencial i jitter absorbeixen les fallades puntuals —sabent que l'SDK porta els seus configurables—, els timeouts i la idempotència marquen els límits del que és reintentable, el circuit breaker queda apuntat per a les caigudes sostingudes, i RESPOSTA_FALLBACK garanteix que, passi el que passi, l'empleat de Nubelia rep una resposta digna i no un traceback. Però el nostre DocuBot continua tenint la memòria d'un peix: cada crida comença de zero, i si un agent pregunta "i això com es configura?", el bot no sap què és "això". A la propera lliçó ataquem la conversa: per què els LLMs no recorden res entre crides, com es construeix la il·lusió de memòria enviant l'historial complet, i quines estratègies existeixen perquè aquest historial no devori la finestra de context ni el pressupost.
Curs d'IA Generativa i LLMs per a Desenvolupadors
Mòdul 1: Fonaments de la IA generativa
- Què és la IA generativa i per què importa als desenvolupadors
- Com funciona un LLM: tokens, embeddings i atenció
- Panorama de models i proveïdors
- Limitacions i riscos: al·lucinacions, context i costos
Mòdul 2: Prompt engineering
- Anatomia del prompt: rols, instruccions i context
- Tècniques de prompting: few-shot, cadena de raonament i plantilles
- Sortides estructurades: JSON i control de format
- Iteració i avaluació de prompts
Mòdul 3: Integració de LLMs en aplicacions
- Primera integració amb l'API d'un LLM
- Streaming, errors i reintents
- Converses i gestió del context
- Costos, latència i caching
Mòdul 4: RAG - Generació augmentada per recuperació
- Embeddings i cerca semàntica
- Bases de dades vectorials
- Ingesta i chunking de documents
- Construcció d'un pipeline RAG complet
- Avaluació de sistemes RAG
Mòdul 5: Function calling i agents
- Function calling: connectar el LLM amb el teu codi
- De LLM a agent: el bucle de raonament i acció
- Frameworks d'orquestració
