A 04-01 vam deixar funcionant una cerca semàntica de força bruta amb dues vergonyes confessades: cada consulta compara la pregunta contra tots els vectors del corpus, i aquests vectors viuen en una llista de Python que s'evapora en reiniciar el procés, obligant a re-embeddir (i re-pagar) tota la documentació a cada arrencada. Per al mini-corpus de sis fragments era igual; per als centenars de pàgines de la wiki de Nubelia, no. Aquesta lliçó presenta la peça d'infraestructura que resol tots dos problemes: la base de dades vectorial. Veurem què aporta exactament (persistència, índexs aproximats, metadades amb filtratge), muntarem ChromaDB en local pas a pas —serà la base vectorial de DocuBot durant la resta del curs—, entendrem a nivell conceptual el trade-off exactitud/velocitat dels índexs ANN, i tancarem amb un mapa d'alternatives perquè sàpigues triar amb criteri quan el projecte no sigui DocuBot.
Contingut
- Per què la força bruta no escala
- Què aporta una base de dades vectorial
- Índexs ANN: canviar una engruna d'exactitud per molta velocitat
- ChromaDB pas a pas: la col·lecció de DocuBot
- Metadades i filtratge: cercar només on té sentit
- Panorama d'alternatives: com triar
Per què la força bruta no escala
Posem números al problema. La nostra funció cercar() de 04-01 fa, per cada consulta, una similitud cosinus contra cada vector del corpus — cost O(n): creix linealment amb el nombre de fragments.
| Corpus | Vectors (després del chunking) | Comparacions per consulta | Viable en força bruta? |
|---|---|---|---|
| Mini-demo de 04-01 | 6 | 6 | Sí, trivial |
| Wiki de Nubelia avui | ~300 pàgines → ~2.000 chunks | 2.000 | Sí, encara aguanta (mil·lisegons) |
| Nubelia + repos + històric de tiquets | ~50.000 chunks | 50.000 | Comença a fer mal en latència |
| Un corpus corporatiu gran | milions | milions | No |
I la latència no és l'únic problema. Amb la llista en memòria:
- Cada arrencada re-embedeix tot el corpus: minuts d'espera i una factura d'embeddings repetida sense necessitat — just el tipus de despesa evitable que vam aprendre a perseguir a 03-04.
- No hi ha actualització incremental: si Nubelia publica una pàgina nova, o cal re-embeddir-ho tot o cal inventar-se una gestió manual de què està embeddit i què no.
- No hi ha filtres: si l'usuari pregunta per la versió 2026.2 del producte, la força bruta li pot retornar alegrement documentació de la 2024, perquè només mira proximitat de significat.
- Un sol procés: dues instàncies de DocuBot no poden compartir la llista.
Sonarà familiar: són els mateixos motius pels quals les aplicacions no desen les seves dades en diccionaris de Python sinó en una base de dades. Els vectors no en són una excepció — només necessiten una base de dades que sàpiga fer l'operació "dona'm els k veïns més propers" de manera eficient.
Què aporta una base de dades vectorial
Una base de dades vectorial és un magatzem especialitzat en vectors + el seu text + les seves metadades, amb tres superpoders:
- Persistència. Els vectors es desen a disc. Embeddir el corpus es paga una vegada (a la ingesta, que construirem a 04-03) i les consultes ho reutilitzen per sempre. Reiniciar el servei costa zero.
- Índexs ANN (Approximate Nearest Neighbors, veïns més propers aproximats). En comptes de comparar contra tot, l'índex organitza els vectors per trobar els més propers visitant només una petita fracció — la cerca passa d'O(n) a alguna cosa propera a logarítmica. La paraula "aproximat" té lletra petita; la desgranem a l'apartat següent.
- Metadades i filtratge. Cada vector porta adjunt un diccionari de metadades (categoria, versió, URL d'origen, data...) i les consultes poden combinar proximitat semàntica amb filtres exactes: "els 4 chunks més semblants a aquesta pregunta que siguin de la categoria
permisosi de la versió 2026.2". Per a DocuBot això connecta directament amb el classificador de 6 categories de 02-02: classificar la pregunta primer permet cercar només a la secció rellevant de la documentació.
A més, segons el producte: API d'upsert (inserir-o-actualitzar, clau per a re-ingestes), esborrat per id, rèpliques, control d'accés... L'essencial és entendre que la base vectorial no calcula embeddings (això continua sent feina del nostre embeddings.py, encara que algunes ofereixen fer-ho per comoditat) ni genera respostes (això és el LLM): és la peça intermèdia que recorda i troba.
Índexs ANN: canviar una engruna d'exactitud per molta velocitat
Com troba el veí més proper sense mirar tots els vectors? La idea general dels índexs ANN és preorganitzar els vectors en una estructura que permeti descartar regions senceres de l'espai sense visitar-les.
L'índex més popular avui és HNSW (Hierarchical Navigable Small World), que fan servir Chroma, Qdrant, Weaviate o pgvector. Sense entrar en matemàtica, la intuïció:
- Imagina els vectors com a ciutats al mapa de significats, connectades amb carreteres a les seves veïnes properes.
- HNSW afegeix a sobre capes d'autopistes: una capa superior amb poques ciutats molt separades i connexions de llarga distància, capes intermèdies més denses, i a baix la xarxa local completa.
- Una cerca entra per l'autopista (salts llargs cap a la regió correcta), va baixant de capa (salts cada cop més fins) i acaba explorant només el veïnatge local de la resposta.
- Resultat: en comptes de visitar els 50.000 vectors, en visita uns centenars.
La lletra petita és la paraula aproximat: l'índex no garanteix trobar exactament els k veïns més propers; garanteix trobar gairebé sempre gairebé tots. La mètrica que ho mesura es diu recall de l'índex (percentatge dels veritables k veïns que l'índex retorna — no el confonguis encara amb el recall de recuperació de 04-05, que mesura una altra cosa a un altre nivell).
| Configuració de l'índex | Velocitat | Exactitud (recall de l'índex) | Memòria |
|---|---|---|---|
| Índex més "dens" (més connexions, més exploració) | Menor | Major (99%+) | Major |
| Índex més "lleuger" (menys connexions, menys exploració) | Major | Menor (90–95%) | Menor |
| Força bruta (sense índex) | La pitjor | 100% exacta | Mínima |
Dues conseqüències pràctiques per no obsessionar-s'hi:
- A l'escala de DocuBot (milers de chunks), els valors per defecte de qualsevol base vectorial donen un recall altíssim amb latència de mil·lisegons. No toquis els paràmetres de l'índex fins a tenir un problema mesurable.
- Perdre ocasionalment el 4t veí més proper gairebé mai no importa en RAG: si la teva resposta depèn de recuperar exactament aquell chunk i cap altre de semblant, el problema real sol ser al chunking o a la redundància del corpus, no a l'índex.
ChromaDB pas a pas: la col·lecció de DocuBot
ChromaDB és una base vectorial open source que corre en local (embeguda al teu procés Python o com a servidor), persisteix a disc i té una API mínima — ideal per a desenvolupament i per a projectes de la mida de DocuBot. Instal·lació:
Creem el mòdul que centralitzarà l'accés (els mòduls 4, 5 i 6 l'importaran tal qual):
# vectordb.py — accés a la base vectorial de DocuBot
import os
import chromadb
RUTA_DB = os.environ.get("DOCUBOT_VECTORDB", "./docubot_db")
def obtenir_colleccio():
"""Retorna la col·lecció de documentació de Nubelia (la crea si no existeix)."""
client = chromadb.PersistentClient(path=RUTA_DB)
return client.get_or_create_collection(
name="docs_nubelia",
metadata={"hnsw:space": "cosine"}, # mesurar amb distància cosinus
)Desglossament:
PersistentClient(path=...)ho desa tot al directori indicat. Aquí hi ha la persistència: mata el procés, torna a arrencar, i els vectors continuen. (Existeix tambéchromadb.Client()purament en memòria, útil només per a tests.)get_or_create_collection: una col·lecció és l'equivalent a una taula — un conjunt de vectors amb el mateix model d'embeddings al darrere.docs_nubeliaserà la nostra col·lecció canònica.{"hnsw:space": "cosine"}: li diem a l'índex HNSW que mesuri amb cosinus, coherent amb 04-01. Compte amb un matís que causa confusió: Chroma retorna distàncies, no similituds, i amb espai cosinus la relació éssimilitud = 1 - distancia. Distància 0.2 ⇒ similitud 0.8.
Afegim documents. Un punt de disseny deliberat: calculem els embeddings nosaltres, amb l'embed() de 04-01, i els passem ja fets a Chroma. Chroma pot calcular-los pel seu compte amb un model local per defecte, però perdríem el control sobre quin model es fa servir — i ja sabem de 04-01 que barrejar models d'embeddings és un error silenciós i letal.
# poblar_demo.py — càrrega manual de prova (la ingesta real arriba a 04-03)
from embeddings import embed
from vectordb import obtenir_colleccio
colleccio = obtenir_colleccio()
documents = [
"Per restablir la teva contrasenya, ves a Configuració > Seguretat i prem 'Restablir'.",
"Els informes es poden exportar a CSV i Excel. L'exportació a PDF no està suportada.",
"El pla Pro permet fins a 500 tasques per projecte i 25 membres per espai.",
"L'API de Nubelia fa servir autenticació per token Bearer, generat a Configuració > Desenvolupadors.",
]
metadades = [
{"categoria": "permisos", "versio": "2026.2", "url": "wiki/seguretat/restablir-password"},
{"categoria": "altres", "versio": "2026.2", "url": "wiki/informes/exportacio"},
{"categoria": "facturacio", "versio": "2026.2", "url": "wiki/plans/limits-pro"},
{"categoria": "api", "versio": "2026.2", "url": "wiki/api/autenticacio"},
]
ids = ["seguretat-password-01", "informes-export-01", "plans-pro-01", "api-auth-01"]
colleccio.upsert(
ids=ids, # id estable i únic per chunk
documents=documents, # el text original (recuperable!)
embeddings=embed(documents, tipus="document"),
metadatas=metadades,
)
print(colleccio.count()) # 4Tres detalls importants:
upserten comptes d'add: si l'id ja existeix, actualitza en lloc de fallar o duplicar. És el que farà re-executable la ingesta de 04-03.- Els
idssón responsabilitat nostra i convé que siguin deterministes (derivats de l'origen del chunk), no aleatoris: així la mateixa pàgina re-ingerida sobreescriu els seus propis chunks. - Chroma desa el text al costat del vector (
documents): en consultar recuperem directament el fragment per al bloc<documentacio>, sense una segona base de dades.
I la consulta:
from embeddings import embed
from vectordb import obtenir_colleccio
colleccio = obtenir_colleccio()
resultat = colleccio.query(
query_embeddings=embed(["no puc entrar al meu compte"], tipus="query"),
n_results=2,
)
for doc, meta, dist in zip(resultat["documents"][0],
resultat["metadatas"][0],
resultat["distances"][0]):
print(f"similitud={1 - dist:.3f} [{meta['categoria']}] {doc[:55]}...")similitud=0.781 [permisos] Per restablir la teva contrasenya, ves a Configuració >... similitud=0.412 [api] L'API de Nubelia fa servir autenticació per token Beare...
Fixa't en la forma del resultat: cada camp és una llista de llistes (resultat["documents"][0]), perquè query accepta diverses consultes alhora i retorna una llista de resultats per consulta. Com que enviem una sola pregunta, sempre indexem amb [0]. És l'ensopegada número u amb l'API de Chroma.
Metadades i filtratge: cercar només on té sentit
El paràmetre where combina la cerca semàntica amb filtres exactes sobre les metadades:
# Només documentació de la categoria 'permisos'
resultat = colleccio.query(
query_embeddings=embed(["no puc entrar al meu compte"], tipus="query"),
n_results=2,
where={"categoria": "permisos"},
)
# Combinant condicions: categoria 'api' I versió actual del producte
resultat = colleccio.query(
query_embeddings=embed(["com m'autentico contra l'API"], tipus="query"),
n_results=4,
where={"$and": [
{"categoria": "api"},
{"versio": "2026.2"},
]},
)Quan filtrar a DocuBot? Dos casos amb recorregut:
- Per categoria, reutilitzant el classificador de 02-02: si
prompt_classificacio()diu que la pregunta és defacturacio, cercar només en aquesta categoria elimina d'arrel que un chunk de l'API "es coli" per semblança superficial. (A 04-04 decidirem quan compensa activar aquest filtre i quan és millor cercar a tot arreu.) - Per versió del producte: quan conviuen documentació de la versió actual i de versions antigues, el filtre evita respondre amb instruccions obsoletes — un tipus d'error especialment danyós en suport.
Una advertència simètrica: cada filtre és una aposta. Si el classificador s'equivoca de categoria, el filtre amaga justament els chunks correctes i la recuperació fracassa sense soroll. Filtra quan la metadada sigui fiable (la versió ho és; una classificació automàtica, només a vegades), i mesura l'efecte amb l'avaluació de 04-05 abans de deixar-lo fix.
Panorama d'alternatives: com triar
Chroma és la nostra elecció didàctica i perfectament vàlida per a DocuBot, però l'ecosistema és ampli. El que és honest no és un rànquing sinó un mapa de criteris:
| Opció | Tipus | Tret distintiu | Quan encaixa bé |
|---|---|---|---|
| Chroma | Local (embeguda o servidor), open source | API mínima, zero fricció per començar | Prototips, projectes petits/mitjans, desenvolupament local |
| pgvector | Extensió de PostgreSQL | Els vectors viuen al costat de les teves dades relacionals, amb SQL, transaccions i backups de sempre | Equips que ja operen PostgreSQL i no volen una peça nova d'infraestructura |
| Qdrant | Servidor dedicat, open source (amb núvol gestionat) | Filtratge sobre metadades molt potent i bon rendiment | Càrregues mitjanes/grans amb filtres complexos, control del desplegament |
| Weaviate | Servidor dedicat, open source (amb núvol gestionat) | Mòduls integrats (embeddings, híbrida keyword+vector) | Qui vol cerca híbrida i funcionalitat "amb piles incloses" |
| Pinecone | Servei gestionat (SaaS) | Zero operació: escala, rèpliques i disponibilitat són problema del proveïdor | Producció a escala sense equip d'infraestructura, si la dada pot sortir a un tercer |
| FAISS | Biblioteca (no base de dades) | Índexs vectorials d'altíssim rendiment, sense persistència ni metadades "de sèrie" | Recerca, pipelines a mida on tu construeixes tota la resta al voltant |
Els criteris de decisió, en ordre raonable d'importància:
- Local o gestionat? La mateixa disjuntiva de 01-03 amb els LLMs: control i privadesa de la dada davant de comoditat operativa. Per a documentació interna, on viuen els vectors importa (els embeddings no són xifratge: d'un vector se'n pot reconstruir molt del contingut).
- Què opera ja el teu equip? Si hi ha PostgreSQL en producció, pgvector sol guanyar per simplicitat operativa encara que una altra opció sigui "millor" sobre el paper.
- Escala real (no aspiracional): milers de vectors → qualsevol val, comença simple; desenes de milions → servidor dedicat o gestionat.
- Filtratge i funcions: si les teves consultes depenen de filtres rics o cerca híbrida, mira Qdrant/Weaviate abans.
La bona notícia: com que el nostre codi aïlla l'accés a vectordb.py i els embeddings a embeddings.py, migrar de base vectorial més endavant és un canvi localitzat, no una reescriptura.
Errors Comuns i Consells
- Confondre distància amb similitud. Chroma retorna distàncies; amb espai cosinus,
similitud = 1 - distancia. Si les teves "similituds" surten millors com més baixes, estàs llegint distàncies. Converteix en un sol lloc i treballa sempre en similitud. - Oblidar
metadata={"hnsw:space": "cosine"}en crear la col·lecció: l'espai per defecte pot no ser cosinus, i els números deixaran de casar amb els de 04-01. Es fixa en crear la col·lecció — canviar-ho després implica recrear-la. - Deixar que la base calculi els embeddings amb el seu model per defecte. Tindràs un corpus embeddit amb un model i preguntes embeddides amb un altre: resultats dolents sense cap error visible. Passa sempre
embeddings=calculats amb el teuembed(). - Ids aleatoris (
uuid4) a la càrrega. Cada re-ingesta duplicarà el corpus sencer. Ids deterministes +upsertés la parella correcta. - Oblidar el
[0]en llegir els resultats dequery(són llistes de llistes, una per consulta). - Optimitzar l'índex ANN prematurament. A escala DocuBot, els defaults sobren. Mesura primer (04-05), ajusta després.
- Consell: desa a les metadades tot el que puguis necessitar per filtrar o citar (categoria, versió, URL, data). Afegir una metadada a posteriori significa re-processar tot el corpus.
Exercicis
- Persistència demostrada. Executa
poblar_demo.py, tanca l'intèrpret de Python, obre'l de nou i, sense tornar a afegir res, executa una consulta sobreobtenir_colleccio(). Comprova ambcolleccio.count()que els 4 documents continuen allà. Què hauria passat amb la llistaVECTORSde 04-01? - El filtre que ajuda i el filtre que fa nosa. Amb els 4 documents de la demo, consulta "quantes tasques puc tenir en un projecte?" primer sense filtre i després amb
where={"categoria": "api"}. Compara resultats i explica quina lliçó pràctica se n'extreu sobre els filtres per classificació automàtica. - Upsert idempotent. Executa
poblar_demo.pytres vegades seguides i comprovacolleccio.count(). Després, canvia el text del documentinformes-export-01(per exemple, afegeix-hi "L'exportació a PDF està prevista per a 2027."), re-executa i consulta "puc exportar a PDF?" per verificar que recuperes la versió nova. Quines dues decisions del codi ho fan possible?
Solucions
-
count()retorna 4 i la consulta funciona:PersistentClientva escriure els vectors a./docubot_dbi els recarrega del disc. La llistaVECTORSde 04-01 hauria desaparegut amb el procés, i reconstruir-la hauria exigit tornar a cridar l'API d'embeddings per a tot el corpus — pagant en temps i diners. Aquesta és exactament la diferència entre una estructura en memòria i una base de dades. -
Sense filtre, el chunk de "El pla Pro permet fins a 500 tasques per projecte" surt primer amb similitud clarament superior — recuperació correcta. Amb
where={"categoria": "api"}, aquest chunk queda exclòs d'arrel (la seva categoria ésfacturacio) i la consulta retorna el chunk d'autenticació de l'API, que és el més semblant dins del filtre però irrellevant per a la pregunta. Lliçó: un filtre basat en una classificació equivocada no degrada la cerca — la saboteja, i a més amb similituds baixes que semblen "resultats normals". Per això a 04-04 tractarem el filtre per categoria com a opcional i condicionat a la confiança del classificador, i per això el llindar mínim de similitud de l'exercici 1 de 04-01 serà la nostra xarxa de seguretat. -
count()continua sent 4 després de tres execucions:upsertamb els mateixos ids sobreescriu en lloc de duplicar (si haguéssim fet serviraddamb ids nous cada vegada, en tindríem 12). En canviar el text d'informes-export-01, la re-execució recalcula el seu embedding i sobreescriu vector, text i metadades sota el mateix id, així que la consulta recupera la versió actualitzada. Les dues decisions: ids deterministes (el document "sap" quin és el seu id) i upsert. Són la base de l'script d'ingesta re-executable de la propera lliçó — juntament amb un tercer pas que aquí vam fer mentalment: invalidar els caches de respostes de 03-04/04-01, perquè la documentació acaba de canviar.
Conclusió
DocuBot ja té memòria de llarg termini: la col·lecció docs_nubelia de ChromaDB persisteix els vectors a disc (s'embedeix una vegada, es consulta sempre), els troba en mil·lisegons gràcies a un índex HNSW que canvia una engruna d'exactitud per ordres de magnitud de velocitat, i desa al costat de cada vector el text original i unes metadades (categoria, versió, URL) que permeten filtrar la cerca i que aviat alimentaran el camp fonts del contracte JSON. Hem encapsulat l'accés a vectordb.py::obtenir_colleccio(), après a convertir les distàncies de Chroma en les similituds de 04-01, i situat Chroma en un mapa d'alternatives (pgvector, Qdrant, Weaviate, Pinecone, FAISS) on l'elecció correcta depèn de criteris —dada local o gestionada, què opera ja l'equip, escala real— i no de rànquings. Però la nostra col·lecció continua poblada amb quatre fragments escrits a mà en un script de demo. La documentació real de Nubelia són centenars de pàgines markdown a la wiki i els repos, amb encapçalaments, taules i seccions de longituds salvatges. Convertir aquest material brut en chunks ben tallats, amb metadades riques i mitjançant un script d'ingesta re-executable és l'ofici que aprenem a la lliçó següent: ingesta i chunking de documents (04-03).
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ó
