Tenim una base vectorial (docs_nubelia, 04-02) i sabem embeddir i comparar significats (04-01), però la col·lecció continua poblada amb quatre fragments de joguina escrits a mà. La documentació real de Nubelia és una altra cosa: centenars de pàgines markdown repartides entre la wiki i els repos, amb guies pas a pas, referència d'API, notes de versió, encapçalaments imbricats i seccions que van de dues línies a dues mil paraules. Aquesta lliçó cobreix l'ofici que converteix aquest material brut en un corpus consultable: per què cal trossejar els documents (no és opcional), com trossejar-los bé (les estratègies de chunking i els seus trade-offs), quines metadades ha de portar cada tros (spoiler: són les que ompliran el camp fonts del contracte JSON), i com empaquetar-ho tot en un script d'ingesta re-executable — perquè la documentació canvia cada setmana i el corpus ha de poder seguir-li el ritme sense drames. La qualitat de tot el pipeline RAG que muntarem a 04-04 es decideix, en gran part, aquí.
Contingut
- El corpus de Nubelia: què tenim entre mans
- Per què trossejar: context, precisió i cost
- Estratègies de chunking: comparativa
- Implementació: chunking per encapçalaments amb solapament
- Metadades per chunk: l'origen de les
fonts - El pipeline d'ingesta:
ingesta.py - Re-ingestes: upsert i invalidació de caches
El corpus de Nubelia: què tenim entre mans
Abans d'escriure codi, inventari. La documentació de Nubelia —dispersa, com vam confessar a 01-01— s'ha consolidat en un directori de fitxers markdown exportats de la wiki i els repos:
docs_nubelia/
├── guies/
│ ├── primers-passos.md
│ ├── gestio-de-tasques.md
│ └── informes-i-exportacio.md
├── administracio/
│ ├── seguretat-i-acces.md
│ ├── membres-i-permisos.md
│ └── plans-i-facturacio.md
├── api/
│ ├── autenticacio.md
│ ├── endpoints-projectes.md
│ └── webhooks.md
└── integracions/
├── slack.md
└── calendari.mdCada fitxer segueix una convenció raonable (no perfecta — és documentació real): un # Títol de pàgina, seccions ## i subseccions ###, i una primera línia de comentari amb metadades de publicació que la wiki afegeix en exportar:
<!-- url: wiki/administracio/seguretat-i-acces | versio: 2026.2 | actualitzat: 2026-06-15 --> # Seguretat i accés ## Restablir contrasenya Per restablir la teva contrasenya, ves a **Configuració > Seguretat**... ## Bloqueig de compte Si el teu compte queda bloquejat després de diversos intents fallits...
Treballem amb markdown perquè és el cas més habitual (wikis, READMEs, portals de docs) i perquè la seva estructura d'encapçalaments és or pur per al chunking. Si el teu corpus real inclou PDF, HTML o Word, el patró és idèntic amb un pas previ de conversió a text/markdown (biblioteques com pypandoc, beautifulsoup4 o extractors de PDF); no ho desenvolupem aquí per no desviar el focus.
Per què trossejar: context, precisió i cost
La temptació inicial és no trossejar: embeddir cada pàgina sencera com un sol vector. Tres raons ho descarten:
- Límits de context. La pàgina sencera pot no cabre a la finestra del model d'embeddings (04-01), i encara que hi càpiga, diverses pàgines senceres concatenades al bloc
<documentacio>poden desbordar —o degradar— la finestra del LLM: recorda "lost in the middle" de 01-04. Volem donar-li al LLM poc i bo, no molt i diluït. - Precisió de la recuperació. L'embedding d'una pàgina que parla de contrasenyes, bloquejos, sessions i auditoria és una mitjana de tots aquests temes: no és especialment a prop de cap pregunta concreta. L'embedding de la secció "Restablir contrasenya" apunta com un làser a les preguntes sobre contrasenyes. Chunks temàticament enfocats ⇒ similituds més nítides ⇒ millor top-k.
- Cost. Cada token del bloc
<documentacio>es paga a cada crida (ho vam mesurar ambestimar_costa 03-04). Recuperar 4 chunks de ~300 tokens en comptes de 4 pàgines de ~3.000 divideix per deu aquesta part de la factura — i és la part que es repeteix a cada pregunta.
El trossejament també té un cost propi: tallar malament pot separar una instrucció de la seva condició ("...prem Restablir." / [tall] / "Nota: aquesta opció requereix ser administrador."). Tot el disseny d'aquest capítol persegueix minimitzar aquest dany.
Estratègies de chunking: comparativa
Les tres famílies que cobreixen la pràctica totalitat dels casos:
| Estratègia | Com talla | Avantatges | Inconvenients | Quan fer-la servir |
|---|---|---|---|---|
| Mida fixa amb solapament | Cada N tokens/caràcters, repetint els últims M al chunk següent | Trivial d'implementar; chunks de mida uniforme i predictible | Talla a cegues: parteix frases, barreja temes; el solapament mitiga però no cura | Text sense estructura (transcripcions, bolcats) o com a primera versió ràpida |
| Per estructura (encapçalaments markdown) | Als límits de secció ##/### |
Respecta les fronteres temàtiques que el mateix autor va marcar; chunks amb sentit complet; el títol de secció és una metadada gratis | Seccions de mida molt dispar: algunes enormes (cal sub-trossejar), altres minúscules | Documentació amb estructura — el nostre cas, i el més comú en corpus corporatius |
| Per paràgrafs / semàntic | Als salts de paràgraf; la variant "semàntica" talla on l'embedding entre frases consecutives canvia bruscament | Granularitat fina; la semàntica detecta canvis de tema sense dependre del format | Chunks de vegades massa petits (perden context); la variant semàntica embedeix frase a frase: més cost i complexitat | Textos llargs sense encapçalaments; refinament quan l'estructura no basta |
Dos paràmetres transversals a qualsevol estratègia:
- Mida objectiu. El compromís habitual és entre 200 i 600 tokens per chunk: suficient perquè el fragment s'entengui sol, prou petit per apuntar a un tema. No hi ha número màgic — a 04-05 el tractarem com a variable experimental.
- Solapament (overlap). Repetir el final d'un chunk al principi del següent (10–20% de la mida) assegura que una frase partida per un tall existeixi completa en almenys un dels dos chunks. Preu: una mica d'emmagatzematge duplicat i el risc de recuperar dos chunks gairebé iguals al mateix top-k.
Per a Nubelia triem l'estratègia que aprofita el que ja tenim: per encapçalaments, amb sub-trossejament amb solapament per a les seccions llargues. El millor de dues famílies.
Implementació: chunking per encapçalaments amb solapament
El pla de la funció: separar el document per encapçalaments ##/###, i si una secció excedeix la mida màxima, sub-trossejar-la per paràgrafs amb solapament. Cada chunk surt amb el seu text i les seves metadades.
# chunking.py — trossejament de markdown per encapçalaments amb solapament
import re
TAM_MAX = 1800 # mida màxima del chunk en caràcters (~450 tokens)
SOLAPAMENT = 300 # caràcters repetits entre sub-chunks consecutius
def trossejar_markdown(text: str, url: str, versio: str, data: str) -> list[dict]:
"""Converteix un document markdown en una llista de chunks amb metadades."""
titol_pagina, seccions = _separar_per_encapcalaments(text)
chunks = []
for titol_seccio, cos in seccions:
for index, tros in enumerate(_sub_trossejar(cos)):
chunks.append({
# Context explícit: el chunk "sap" de quina pàgina i secció ve
"text": f"[{titol_pagina} > {titol_seccio}]\n{tros}",
"metadades": {
"titol_seccio": titol_seccio,
"titol_pagina": titol_pagina,
"url": url,
"versio": versio,
"data": data,
},
# Id determinista: mateixa secció => mateix id a cada re-ingesta
"id": f"{url.replace('/', '-')}--{_slug(titol_seccio)}--{index:02d}",
})
return chunks
def _separar_per_encapcalaments(text: str):
"""Divideix el markdown en (titol_pagina, [(titol_seccio, cos), ...])."""
titol_pagina = "Sense títol"
m = re.search(r"^# (.+)$", text, flags=re.MULTILINE)
if m:
titol_pagina = m.group(1).strip()
# Partim per encapçalaments de nivell 2 o 3, conservant el títol de cada secció
parts = re.split(r"^(#{2,3} .+)$", text, flags=re.MULTILINE)
seccions, titol_actual = [], "Introducció" # text abans del primer ##
for part in parts:
if re.match(r"^#{2,3} ", part):
titol_actual = part.lstrip("#").strip()
elif part.strip():
seccions.append((titol_actual, part.strip()))
return titol_pagina, seccions
def _sub_trossejar(cos: str) -> list[str]:
"""Si la secció hi cap, un sol chunk; si no, talla per paràgrafs amb solapament."""
if len(cos) <= TAM_MAX:
return [cos]
trossos, actual = [], ""
for paragraf in cos.split("\n\n"): # mai no partim un paràgraf per dins
if len(actual) + len(paragraf) > TAM_MAX and actual:
trossos.append(actual.strip())
actual = actual[-SOLAPAMENT:] # el solapament: arrossega el final anterior
actual += "\n\n" + paragraf
if actual.strip():
trossos.append(actual.strip())
return trossos
def _slug(text: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")Decisions que convé entendre (són les que marquen la diferència entre un chunking correcte i un de mediocre):
- El prefix
[Pàgina > Secció]dins del text del chunk. Un chunk que comença per "Per restablir la teva contrasenya..." és bo; un que comença per "[Seguretat i accés > Restablir contrasenya]\nPer restablir..." és millor: el context entra a l'embedding (millora la similitud amb preguntes que mencionen el tema general) i el LLM el veurà en redactar, reduint ambigüitat. Tècnica barata amb retorn alt. - Mai no partim un paràgraf per dins. El sub-trossejament acumula paràgrafs complets; el pitjor cas és un chunk una mica més llarg que
TAM_MAX, no una frase decapitada. - El solapament s'arrossega entre sub-chunks de la mateixa secció (
actual[-SOLAPAMENT:]), no entre seccions diferents: creuar la frontera d'un encapçalament barrejaria temes, just el que l'estratègia evita. - Ids deterministes (
url--seccio--index): la peça que a 04-02 vam identificar com a requisit de l'upsertidempotent.
Metadades per chunk: l'origen de les fonts
Mereix el seu propi apartat perquè les metadades no són decoració — cada camp té un consumidor concret ja construït o ja anunciat:
| Metadada | Qui la consumeix | Per a què |
|---|---|---|
url |
Camp fonts del contracte JSON (02-03) |
Que la resposta de DocuBot citi la pàgina real d'on va sortir — verificable per l'usuari |
titol_seccio, titol_pagina |
Bloc <documentacio> (04-04) i fonts |
Cites llegibles ("Seguretat i accés > Restablir contrasenya") en lloc d'URLs crues |
versio |
Filtres where de 04-02 |
No respondre amb documentació d'una versió que l'usuari no fa servir |
data |
Filtres i diagnòstic | Detectar documentació rància; desempatar entre chunks duplicats |
Fins ara, el camp fonts del contracte JSON era el que el LLM deia haver fet servir — amb la documentació passada a mà, poc verificable. A partir de 04-04, fonts s'omplirà des d'aquestes metadades, és a dir, des de fets: quins chunks es van recuperar realment. És un salt de fiabilitat que es decideix aquí, a la ingesta, desant bé l'origen de cada tros.
El pipeline d'ingesta: ingesta.py
Ho assemblem tot a l'script que s'executarà cada vegada que la documentació canviï:
flowchart LR
A[Llegir fitxers .md] --> B[Netejar i extreure metadades]
B --> C[Trossejar amb chunking.py]
C --> D[Embeddir per lots amb embeddings.py]
D --> E[Upsert a docs_nubelia]
E --> F[Invalidar caches de respostes]# ingesta.py — pipeline d'ingesta re-executable de la documentació de Nubelia
import os
import re
import pathlib
from chunking import trossejar_markdown
from embeddings import embed
from vectordb import obtenir_colleccio
DIR_DOCS = os.environ.get("DOCUBOT_DOCS", "./docs_nubelia")
TAM_LOT = 64 # quants chunks embedim per crida a l'API
def netejar(text: str) -> str:
"""Normalitza el markdown abans de trossejar."""
text = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL) # comentaris HTML fora
text = re.sub(r"\n{3,}", "\n\n", text) # col·lapsar línies en blanc
return text.strip()
def extreure_metadades_publicacio(text: str, ruta: pathlib.Path) -> dict:
"""Llegeix la línia '<!-- url: ... | versio: ... | actualitzat: ... -->' de la wiki."""
m = re.search(r"<!--\s*url:\s*(\S+)\s*\|\s*versio:\s*(\S+)\s*\|\s*actualitzat:\s*(\S+)\s*-->", text)
if m:
return {"url": m.group(1), "versio": m.group(2), "data": m.group(3)}
# Valors de recanvi si la pàgina no porta la línia (documentació real = imperfecta)
return {"url": str(ruta.with_suffix("")).replace("\\", "/"), "versio": "desconeguda", "data": "desconeguda"}
def ingerir():
colleccio = obtenir_colleccio()
tots = []
for ruta in sorted(pathlib.Path(DIR_DOCS).rglob("*.md")):
cru = ruta.read_text(encoding="utf-8")
meta = extreure_metadades_publicacio(cru, ruta.relative_to(DIR_DOCS))
chunks = trossejar_markdown(netejar(cru), **meta)
tots.extend(chunks)
print(f" {ruta.name}: {len(chunks)} chunks")
# Embeddir i desar PER LOTS: menys crides, menys latència, mateix cost per token
for i in range(0, len(tots), TAM_LOT):
lot = tots[i : i + TAM_LOT]
vectors = embed([c["text"] for c in lot], tipus="document")
colleccio.upsert(
ids=[c["id"] for c in lot],
documents=[c["text"] for c in lot],
embeddings=vectors,
metadatas=[c["metadades"] for c in lot],
)
print(f"Ingesta completa: {len(tots)} chunks a '{colleccio.name}' ({colleccio.count()} totals).")
# La documentació ha canviat => les respostes cachejades poden estar obsoletes
invalidar_caches()
def invalidar_caches():
"""Regla de 03-04: publicar documentació invalida les respostes cachejades."""
from cache_semantic import CacheSemantic # i el CacheRespostes exacte de 03-04
# En un desplegament real, aquí es buidaria el magatzem compartit de tots dos caches.
print("Caches de respostes invalidats (CacheRespostes + CacheSemantic).")
if __name__ == "__main__":
ingerir()Lectura guiada de l'script:
netejar()és mínima a propòsit: treu comentaris HTML (un cop extretes les seves metadades) i col·lapsa espais. La neteja agressiva (treure taules, codi, emojis) sol destruir informació útil; neteja només allò que demostri fer nosa.- Lots de 64: el patró de 04-01 en acció. Amb ~2.000 chunks són ~32 crides a l'API d'embeddings en lloc de 2.000. El cost per token és el mateix; la latència total i la fiabilitat, incomparables. (Si l'API d'embeddings retorna errors transitoris, embolcalla
embedamb elcridar_amb_reintentsdereintents.py— els patrons de 03-02 serveixen per a qualsevol API, no només la del LLM.) upsertper lots amb ids deterministes: executaringesta.pydues vegades deixa la col·lecció exactament igual; executar-la després d'editar una pàgina sobreescriu només els chunks d'aquella pàgina.- La impressió per fitxer no és cosmètica: "gestio-de-tasques.md: 47 chunks" quan la resta en té 8-12 és el senyal d'una pàgina monstruosa que potser mereix reestructurar-se — la ingesta és també un espieta de qualitat de la documentació.
Re-ingestes: upsert i invalidació de caches
La documentació de Nubelia canvia cada setmana; el corpus ha de poder actualitzar-se amb una sola ordre i sense efectes secundaris sorpresa. El nostre disseny ho garanteix amb tres propietats:
- Idempotència: re-executar sense canvis no duplica res (ids deterministes + upsert, de 04-02).
- Actualització real: una pàgina editada regenera els seus chunks sota els mateixos ids, amb embeddings i metadades frescos.
- Coherència amb els caches: la regla que vam fixar a 03-04 —publicar documentació invalida
CacheRespostes— s'estén ara aCacheSemantic(04-01), iingesta.pyés el lloc natural per disparar-la: és l'únic punt del sistema que sap que la documentació acaba de canviar. Un cache que sobreviu a una re-ingesta pot servir respostes basades en documentació que ja no existeix — el pitjor bug possible en un assistent de suport, perquè ningú no el veu.
Queda una aresta honesta: si una secció s'elimina d'una pàgina (o una pàgina desapareix), els seus chunks antics continuen a la col·lecció amb els seus ids orfes — l'upsert actualitza i afegeix, però no esborra allò que ja no es genera. La solució robusta és comparar els ids generats a la ingesta amb els existents (colleccio.get()) i esborrar la diferència; ho deixem assenyalat com a exercici, perquè és exactament el tipus de detall que separa un script de demo d'una peça operable.
Errors Comuns i Consells
- Chunks sense context propi. Un chunk que diu "Després, prem Acceptar i el canvi s'aplica" no el recupera cap pregunta ni ajuda el LLM. El prefix
[Pàgina > Secció]i el tall per encapçalaments existeixen per evitar-ho; revisa una mostra de chunks a ull després de cada canvi d'estratègia. - Trossejar per mida fixa tenint estructura disponible. Si el document té encapçalaments, fes-los servir: tallar "cada 1.000 caràcters" un markdown ben estructurat és llençar informació de disseny que l'autor ja et va donar.
- Chunks enormes "per no perdre res". Recuperaràs fragments que contenen la resposta... diluïda entre tres temes més, amb similituds planes i el bloc
<documentacio>inflat (i pagat, 03-04). Si dubtes entre gran i petit, comença petit-mitjà i deixa que l'avaluació de 04-05 decideixi. - Ids no deterministes o derivats d'un comptador global. Un comptador (
chunk-0001,chunk-0002...) canvia d'assignació així que s'afegeix una pàgina al principi del recorregut: els upserts sobreescriuran chunks equivocats. L'id ha de derivar del contingut estable (url + secció + índex). - Oblidar la invalidació de caches a la re-ingesta. El símptoma és diabòlic: la col·lecció té la doc nova, les proves directes al pipeline funcionen, però els usuaris continuen rebent respostes velles (cachejades). Invalidar a
ingesta.pytanca la porta. - Consell: versiona
chunking.pyamb cura i anota (com ambPROMPT_VERSIONa 02-04) quina versió del chunking va generar el corpus actual. Canviar l'estratègia de chunking canvia els ids i el contingut dels chunks: exigeix re-ingesta completa i re-avaluació.
Exercicis
- Auditoria de chunks. Executa
trossejar_markdown()sobre la pàginaseguretat-i-acces.mdd'exemple i imprimeix, per a cada chunk: id, primers 80 caràcters i longitud. Verifica que (a) cap chunk no superaTAM_MAXen més d'un paràgraf, (b) els ids són únics, i (c) el prefix[Pàgina > Secció]hi és present. Després, duplica el contingut d'una secció fins a fer-la superarTAM_MAXi comprova que apareix el sub-trossejament amb solapament. - Esborrat d'orfes. Implementa
purgar_orfes(colleccio, ids_vigents)que esborri de la col·lecció els chunks l'id dels quals no sigui a la ingesta actual, i crida-la al final d'ingerir(). Pista:colleccio.get()sense filtres retorna els ids existents, icolleccio.delete(ids=[...])esborra per id. - El chunking com a variable. Sense executar res: per a la pregunta "quant dura l'enllaç de restablir contrasenya?", la resposta de la qual ("24 hores") és a l'última frase d'una secció llarga, raona què passaria amb (a) chunks per pàgina sencera, (b) mida fixa de 300 caràcters sense solapament, (c) la nostra estratègia. Quina recupera millor i per què?
Solucions
- Amb la pàgina d'exemple, la sortida esperada és de l'estil:
wiki-administracio-seguretat-i-acces--restablir-contrasenya--00 | [Seguretat i accés > Restablir contrasenya]\nPer restabl... | 412 wiki-administracio-seguretat-i-acces--bloqueig-de-compte--00 | [Seguretat i accés > Bloqueig de compte]\nSi el teu compt... | 388
En inflar una secció per sobre de TAM_MAX, apareixen ids --00, --01... de la mateixa secció, i l'inici del chunk --01 repeteix els últims ~300 caràcters del --00 (el solapament). Si a més observes que el chunk --01 comença a mitja idea, acabes de veure en viu per què el solapament existeix: la idea completa està garantida al --00.
- Una implementació directa:
def purgar_orfes(colleccio, ids_vigents: set[str]):
existents = set(colleccio.get()["ids"])
orfes = list(existents - ids_vigents)
if orfes:
colleccio.delete(ids=orfes)
print(f"Purgats {len(orfes)} chunks orfes.")I a ingerir(), després del bucle d'upserts: purgar_orfes(colleccio, {c['id'] for c in tots}). Amb això la ingesta passa d'"afegeix i actualitza" a mirall fidel del directori de documentació. Matís d'operació: si la ingesta corre contra un directori incomplet per error (p. ex. un checkout parcial), la purga esborraria mig corpus — un guarda-rail raonable és avortar si ids_vigents és sospitosament petit respecte dels existents. Les decisions destructives mereixen cinturó i tirants, com el sostre de pressupost de 03-04.
- (a) Pàgina sencera: l'embedding de la pàgina fa la mitjana de contrasenyes, bloquejos i auditoria; la similitud amb la pregunta serà mediocre i, encara que es recuperi, el LLM rebrà 3.000 tokens on la dada clau està enterrada al final — risc "lost in the middle" (01-04) i cost màxim. (b) Mida fixa 300 sense solapament: alta probabilitat que el tall caigui entre "rebràs un correu amb un enllaç" i "vàlid durant 24 hores" — el chunk recuperat conté la meitat de la resposta i DocuBot, fidel a la regla 2 de
SYSTEM_DOCUBOT, no podrà respondre amb precisió. (c) La nostra estratègia: la secció completa "Restablir contrasenya" és un chunk (o dos amb solapament), temàticament pur, amb la frase de les 24 hores intacta en almenys un chunk, i amb el prefix de secció reforçant la similitud. Recupera millor perquè cada decisió (tallar en fronteres temàtiques, no partir paràgrafs, solapar) protegeix exactament el cas que les altres dues trenquen.
Conclusió
La col·lecció docs_nubelia ja no conté quatre frases de demo sinó la documentació real de Nubelia, convertida en chunks amb ofici: trossejats per encapçalaments (respectant les fronteres temàtiques que va marcar l'autor), sub-trossejats amb solapament quan una secció s'allarga, prefixats amb el seu [Pàgina > Secció] perquè el context viatgi dins de l'embedding, i carregats amb les metadades —url, titol_seccio, versio, data— que alimentaran els filtres de 04-02 i, sobretot, el camp fonts del contracte JSON amb fets en lloc de declaracions. Tot orquestrat per ingesta.py: llegir → netejar → trossejar → embeddir per lots → upsert, re-executable a voluntat gràcies als ids deterministes, i amb la disciplina d'invalidar CacheRespostes i CacheSemantic a cada publicació, perquè un cache dessincronitzat de la documentació és un generador silenciós de respostes obsoletes. Ja tenim les tres peces sobre la taula: sabem embeddir i comparar (04-01), sabem emmagatzemar i recuperar amb filtres (04-02) i sabem poblar el magatzem amb material de qualitat (04-03). El que falta és unir-les amb els artefactes dels mòduls anteriors —SYSTEM_DOCUBOT i el seu bloc <documentacio> que per fi s'omplirà sol, prompt_resposta(), el respondre() robust de 03-02— en una sola funció que rebi una pregunta i retorni una resposta fonamentada amb fonts reals. Aquest és el moment culminant del mòdul: la construcció del pipeline RAG complet (04-04).
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ó
