Aquesta lliçó salda un deute anunciat dues vegades: a 02-04, quan vam construir el conjunt de 30-50 preguntes reals i la rúbrica binària prometent automatitzar-ne l'execució, i a 04-05, quan la rúbrica va créixer fins a 9 criteris i vam dir que "06-03 automatitza això". DocuBot ja està protegit (06-01) i és respectuós amb les dades (06-02), però continua avaluant-se a mà: algú llança les preguntes, llegeix les respostes i omple un CSV. Això no escala, no s'executa abans de cada desplegament i depèn de l'humor de l'avaluador. Aquí muntem la piràmide completa de testing per a aplicacions amb LLMs: pytest clàssic per al teu codi determinista, tests de contracte sobre les sortides del model, un runner que executa el conjunt sencer i registra resultats versionats, i un LLM fent de jutge per als criteris que cap assert no pot verificar — calibrat contra la teva avaluació manual, que es converteix en patró or.
Contingut
- El problema: per què el testing clàssic no és suficient (i per què continua sent necessari)
- Els tres nivells de testing en una aplicació amb LLM
- Nivell 1: unit tests deterministes amb pytest
- Nivell 2: tests de contracte i propietats sobre sortides del LLM
- Nivell 3: el runner d'avaluació de qualitat
- LLM-as-judge: automatitzar els criteris no deterministes
- Regressions: avaluar abans de desplegar
- El cost d'avaluar
El problema: per què el testing clàssic no és suficient (i per què continua sent necessari)
El testing tradicional descansa en una premissa: mateixa entrada → mateixa sortida. Els LLMs la trenquen (el no determinisme va ser la limitació núm. 4 del mòdul 1): la mateixa pregunta pot produir respostes diferents, totes acceptables — o no. Un assert resposta == esperat sobre text lliure fallarà dimarts i passarà dijous.
La reacció equivocada és rendir-se ("no es pot testejar, provem a mà i llestos"). La correcta és adonar-se que una aplicació amb LLM té dos territoris:
- El teu codi:
netejar_json,validar_contracte, el chunking,executar_eina,redactar... Determinista al 100%. Es testeja amb el pytest de tota la vida, sense gastar ni un token. - Les sortides del model: no deterministes en la seva literalitat, però amb propietats que sí que s'han de complir sempre (el JSON és vàlid, les fonts existeixen, la frase anti-al·lucinació apareix quan toca) i amb qualitats que requereixen judici (és fidel a la documentació? és rellevant?).
Cada territori té la seva eina. Confondre'ls produeix els dos errors clàssics: gastar tokens testejant un parser (caríssim, lent, innecessari) o intentar verificar fidelitat amb un assert d'igualtat (impossible).
Els tres nivells de testing en una aplicació amb LLM
| Nivell | Què verifica | Determinista | Eina | Quan s'executa | Cost |
|---|---|---|---|---|---|
| 1. Unit tests del codi propi | Parsers, validadors, chunking, redacció, eines amb mocks | Sí | pytest normal | A cada commit (segons) | Zero tokens |
| 2. Tests de contracte/propietats | Propietats invariants de les sortides del LLM: JSON vàlid, contracte complert, frase anti-al·lucinació quan correspon, mai no inventa fonts | La verificació sí; la sortida no | pytest + crides reals al model | Abans de desplegar, amb pocs casos | Baix |
| 3. Avaluació de qualitat | Fidelitat, rellevància, correcció sobre el conjunt complet de casos | No | Runner + criteris automàtics + LLM-as-judge | Abans de cada canvi de prompt/model/chunking; periòdicament | El que cal gestionar |
La forma de piràmide no és casual: molts tests barats a baix, pocs i cars a dalt. La majoria dels bugs de DocuBot al llarg del curs (JSON amb text al voltant, chunks mal tallats, arguments invàlids) viuen al nivell 1, on trobar-los costa mil·lisegons.
Nivell 1: unit tests deterministes amb pytest
Res de nou sota el sol — i aquesta és la bona notícia: la meitat de DocuBot es testeja com qualsevol projecte Python. Exemples representatius:
# test_validacio.py
import pytest
from validacio import netejar_json, validar_contracte
def test_netejar_json_extreu_de_text_amb_soroll():
cru = 'És clar, aquí ho tens:\n{"categoria": "api", "resposta": "...", ' \
'"fonts": [], "confianca": 0.4}\nEspero que ajudi.'
assert netejar_json(cru)["categoria"] == "api"
def test_validar_contracte_rebutja_categoria_desconeguda():
sortida = {"categoria": "marciana", "resposta": "x",
"fonts": [], "confianca": 0.9}
assert not validar_contracte(sortida)
def test_validar_contracte_rebutja_confianca_fora_de_rang():
sortida = {"categoria": "api", "resposta": "x",
"fonts": [], "confianca": 1.7}
assert not validar_contracte(sortida)# test_chunking.py — el chunking és funció pura: entrada text, sortida chunks
from chunking import trossejar_markdown
def test_cap_chunk_no_supera_el_maxim():
text = "## Títol\n" + ("paràgraf de farciment. " * 500)
assert all(len(c["text"]) <= 1800 for c in trossejar_markdown(text))
def test_el_solapament_preserva_context():
# dos chunks consecutius comparteixen els ~300 caràcters de solapament
chunks = trossejar_markdown("## A\n" + "x" * 4000)
assert chunks[0]["text"][-100:] in chunks[1]["text"]I el patró clau per a les eines del mòdul 5 — mocks: es testeja la teva fontaneria sense cridar ni el LLM ni l'API real.
# test_eines.py
from eines import executar_eina
def test_projecte_inexistent_retorna_is_error(monkeypatch):
# api_nubelia ja és un mock, però en fixem l'estat per al test
execucio = executar_eina("consultar_projecte",
{"projecte": "inexistent"})
assert execucio["is_error"] is True # el patró de 05-01
def test_validacio_arguments_talla_abans_d_executar():
# l'allowlist de 06-01, testejada en mil·lisegons
execucio = executar_eina("crear_tiquet_suport",
{"prioritat": "apocaliptica",
"titol": "t", "descripcio": "d"})
assert execucio["is_error"] is TrueObserva el dividend de les decisions d'arquitectura del curs: com que el bucle agèntic és teu (05-03) i la lògica viu en mòduls separats, cada peça es testeja aïllada. La defensa de 06-01 (validar_arguments) i la redacció de 06-02 (redactar) són funcions pures: entren en aquest nivell amb tests trivials d'escriure.
Nivell 2: tests de contracte i propietats sobre sortides del LLM
Aquí sí que cridem el model, però no preguntem "és aquesta la resposta exacta?" sinó "compleix les propietats que tota resposta ha de complir?". És la idea del property-based testing adaptada: no coneixes la sortida, però en coneixes els invariants.
Les tres propietats canòniques de DocuBot:
# test_contracte_llm.py — s'executen amb crides reals: poques i triades
import json
import pytest
from rag import respondre_amb_rag, FRASE_SENSE_INFO
from validacio import netejar_json, validar_contracte
from vectordb import obtenir_fonts_existents # ids reals de docs_nubelia
# Propietat 1: la sortida sempre respecta el contracte JSON
@pytest.mark.llm # marca per poder excloure'ls del cicle ràpid: pytest -m "not llm"
def test_sortida_compleix_contracte():
sortida = netejar_json(respondre_amb_rag("Com canvio el meu pla de facturació?"))
assert validar_contracte(sortida)
# Propietat 2: sense documentació rellevant, apareix la frase anti-al·lucinació
@pytest.mark.llm
def test_pregunta_sense_resposta_activa_frase_sense_info():
sortida = netejar_json(respondre_amb_rag(
"Quina és la política de teletreball a Mart?")) # no existeix a la wiki
assert FRASE_SENSE_INFO in sortida["resposta"]
assert sortida["confianca"] <= 0.3
# Propietat 3: mai no inventa fonts — tota font citada existeix a la col·lecció
@pytest.mark.llm
def test_fonts_citades_existeixen():
sortida = netejar_json(respondre_amb_rag("Com creo un webhook?"))
existents = obtenir_fonts_existents()
assert all(f in existents for f in sortida["fonts"])Dos matisos d'enginyeria:
- El no determinisme no desapareix: una propietat pot complir-se en 2 de 3 execucions. Per això l'hàbit de 04-05 (3 execucions per cas) també aplica aquí per als tests inestables: o s'executa N vegades i s'exigeix N/N (propietats dures com el contracte), o s'accepta un llindar i aleshores el cas pertany al nivell 3, no a aquest.
- Pocs casos, molt triats: aquest nivell no recorre el conjunt sencer (això és el nivell 3); fa servir un grapat de casos sentinella — un per propietat, potser un per categoria del classificador — perquè es pugui executar sovint sense mal de pressupost.
Nivell 3: el runner d'avaluació de qualitat
Arribem a l'automatització promesa. El runner és un script que: (1) carrega el conjunt complet — els 30-50 casos de 02-04 amb els seus 4 tipus (fàcils ~40%, ambigus ~25%, sense resposta ~20%, adversarials suaus ~15%), els chunks anotats de 04-05 i els adversarials de seguretat de 06-01 —; (2) executa cada cas contra el sistema; (3) valida allò automatitzable; (4) delega en el jutge allò que no; (5) registra un CSV amb totes les versions.
# runner_eval.py
import csv, json, time, uuid
from datetime import date
from conjunt_rag import CONJUNT_RAG # casos amb chunks anotats (04-05)
from casos_adversarials import CASOS_ADVERSARIALS_SEGURETAT # 06-01
from rag import respondre_amb_rag, FRASE_SENSE_INFO
from agent import bucle_agent
from eines import EINES
from validacio import netejar_json, validar_contracte
from prompts import PROMPT_VERSION
from chunking import CHUNKING_VERSION
from client import MODEL
from preus import estimar_cost
EXECUCIONS_PER_CAS = 3 # l'hàbit de 04-05: el no determinisme es mostreja
def executar_cas(cas: dict) -> dict:
"""Executa un cas per la ruta que li correspon i avalua criteris automàtics."""
inici = time.time()
if cas.get("ruta") == "agent": # casos multi-pas o d'eines
crua = bucle_agent(cas["pregunta"], EINES, max_iteracions=8)
else: # ruta per defecte: pipeline RAG
crua = respondre_amb_rag(cas["pregunta"])
durada = time.time() - inici
sortida = netejar_json(crua)
resultats = {
"contracte_valid": validar_contracte(sortida) if sortida else False,
"durada_s": round(durada, 2),
}
if sortida:
# Criteris automàtics: deterministes, sense jutge
if cas["tipus"] == "sense_resposta":
resultats["frase_sense_info_ok"] = FRASE_SENSE_INFO in sortida["resposta"]
if "fonts_esperades" in cas: # gràcies als chunks anotats
citades = set(sortida["fonts"])
resultats["fonts_ok"] = citades <= set(cas["fonts_esperades"])
resultats["sortida"] = sortida
return resultats
def executar_conjunt():
id_execucio = f"{date.today()}-{uuid.uuid4().hex[:8]}"
casos = CONJUNT_RAG + CASOS_ADVERSARIALS_SEGURETAT
files = []
for cas in casos:
for n in range(EXECUCIONS_PER_CAS):
r = executar_cas(cas)
files.append({
"id_execucio": id_execucio,
"cas": cas["id"], "tipus": cas["tipus"], "repeticio": n + 1,
"prompt_version": PROMPT_VERSION,
"chunking_version": CHUNKING_VERSION, # "encapcalaments-v1-1800-300"
"model": MODEL,
**{k: v for k, v in r.items() if k != "sortida"},
})
with open(f"eval_{id_execucio}.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=files[0].keys())
w.writeheader(); w.writerows(files)
return filesPunts que convé interioritzar:
- Les versions van a cada fila.
PROMPT_VERSION,CHUNKING_VERSIONiMODELconverteixen el CSV en un experiment reproduïble: d'aquí a un mes sabràs exactament quina combinació va produir quins números. És el mateix principi del CSV de 02-04, ara automatitzat. - Cada cas viatja per la seva ruta real (
respondre_amb_ragobucle_agent), perquè avalues el sistema que despleguis, no una versió de laboratori. - Els criteris automàtics primer. Contracte vàlid, frase anti-al·lucinació als casos
sense_resposta, fonts dins de les esperades (possible gràcies a l'anotació de chunks de 04-05): tot això es resol ambasserts barats. El jutge només entra on l'assertno arriba. - Les mètriques de recuperació (recall@k, precision@k, MRR) no es recalculen aquí: viuen a
eval_rag.py(04-05) i avaluen el retriever aïllat. Aquest runner avalua el sistema de punta a punta; són diagnòstics complementaris (si el runner empitjora,eval_rag.pyet diu si el culpable és la recuperació o la generació).
LLM-as-judge: automatitzar els criteris no deterministes
Queden els criteris de la rúbrica de 9 punts de 04-05 que cap assert no pot comprovar: la resposta és fidel als chunks (no afegeix res que no hi sigui)? És rellevant per a la pregunta? La solució amb millor relació cost/utilitat és fer servir un altre LLM com a avaluador: LLM-as-judge (un LLM fent de jutge).
La idea: el jutge rep la pregunta, els chunks recuperats i la resposta, i aplica la rúbrica emetent veredictes binaris (sí/no) — la mateixa escala que vas fer servir a mà, perquè les escales de l'1 al 10 produeixen jutges inconsistents i números pseudoprecisos.
# jutge.py
import json
from client import client, MODEL_JUTGE, extreure_text # pot ser un model petit
from reintents import cridar_amb_reintents
PROMPT_JUTGE = """Ets un avaluador estricte d'un assistent de documentació.
Avalua la RESPOSTA únicament amb la DOCUMENTACIÓ proporcionada com a referència.
Criteris (respon exactament true o false per a cadascun):
- fidelitat: tota afirmació de la resposta està avalada per la documentació.
Si la resposta afegeix dades, passos o xifres que no hi apareixen, és false.
- rellevancia: la resposta aborda directament la pregunta de l'usuari.
Retorna NOMÉS un JSON: {"fidelitat": bool, "rellevancia": bool,
"justificacio": "una frase per criteri"}
PREGUNTA:
{pregunta}
DOCUMENTACIÓ (única font de veritat):
{documentacio}
RESPOSTA A AVALUAR:
{resposta}"""
def jutjar(pregunta: str, documentacio: str, resposta: str) -> dict:
prompt = PROMPT_JUTGE.format(pregunta=pregunta,
documentacio=documentacio,
resposta=resposta)
sortida = cridar_amb_reintents(client, MODEL_JUTGE,
[{"role": "user", "content": prompt}])
return json.loads(extreure_text(sortida))El detall que el prompt demani justificació no és cosmètic: obliga el jutge a "raonar" abans del veredicte (el CoT de 02-02 aplicat a l'avaluació) i et dona material per auditar els seus errors.
Calibració: no et refiïs del jutge sense mesurar-lo
Un jutge és un LLM més: també s'equivoca. Abans de delegar-li res, es calibra contra el patró or — les teves avaluacions manuals de 02-04 i 04-05, que deixen de ser una càrrega històrica per convertir-se en l'actiu que valida el jutge:
- Agafa 30-50 casos que ja vas avaluar a mà amb la rúbrica.
- Passa'ls pel jutge.
- Mesura el % d'acord per criteri (i, si vols rigor, un coeficient tipus kappa que descompti l'acord per atzar).
- Regla pràctica: amb acord ≥ 85-90% en un criteri, el jutge el pot automatitzar (revisant per mostreig els desacords); per sota, itera el prompt del jutge o mantén aquest criteri en manual.
Biaixos coneguts del jutge i mitigacions
| Biaix | Què fa | Mitigació |
|---|---|---|
| Posició | En comparacions A/B prefereix sistemàticament la primera (o l'última) opció presentada | Avaluar cada resposta aïllada contra la rúbrica (com fa el nostre jutge); si compares parells, avalua en tots dos ordres i descarta si no coincideix |
| Longitud | Afavoreix respostes més llargues i detallades encara que no siguin millors | Criteris binaris ancorats a la documentació ("avalada per", no "completa"); incloure a la calibració casos on la resposta curta és la correcta |
| Autopreferència | Puntua millor les sortides generades per la seva pròpia família de models | Fer servir com a jutge un model diferent de l'avaluat quan sigui possible; la calibració contra el patró or humà detecta el biaix encara que no el puguis evitar |
| Indulgència | Tendeix al "sí" en cas de dubte | Instrucció explícita de rigor ("si afegeix dades que no apareixen, és false") i exemples few-shot de veredictes negatius al prompt del jutge |
Regressions: avaluar abans de desplegar
Tot l'anterior cristal·litza en una disciplina: cap canvi de prompt, model o chunking no es desplega sense passar l'avaluació. És l'equivalent LLM de "no es fa merge amb tests en vermell", i el motiu pel qual ho versionem tot des de 02-04.
El flux, a nivell conceptual (un gate — una barrera de qualitat — a CI):
flowchart LR
A[Canvi: PROMPT_VERSION,<br>CHUNKING_VERSION o MODEL] --> B[Nivell 1: pytest<br>segons, zero tokens]
B -->|verd| C[Nivell 2: tests de contracte<br>casos sentinella]
C -->|verd| D[Nivell 3: runner + jutge<br>conjunt complet]
D --> E{Compleix llindars i<br>no empitjora la base?}
E -->|sí| F[Desplegar i arxivar CSV<br>com a nova línia base]
E -->|no| G[Revisar desacords<br>i iterar el canvi]Els llindars d'acceptació es defineixen per endavant (no després de veure els números, que és com es legalitzen les regressions). Un exemple raonable per a DocuBot:
- Contracte JSON vàlid: 100% (és codi + propietat dura; no hi ha excusa).
- Frase anti-al·lucinació en casos
sense_resposta: ≥ 95%. - Casos adversarials de 06-01 superats: 100% dels deterministes (l'allowlist no negocia), ≥ 90% dels jutjats.
- Fidelitat (jutge): ≥ 90% i mai més de 2 punts per sota de la línia base anterior.
Aquest segon tipus de llindar — comparar contra l'execució anterior, no només contra un mínim absolut — és el que detecta regressions: el CSV arxivat de l'última versió bona és la teva línia base, i les columnes de versió et diuen exactament què comparar amb què.
El cost d'avaluar
Una execució completa té un preu que ja saps estimar amb estimar_cost() (03-04): ~45 casos × 3 execucions × (1 crida al sistema + 1 al jutge) ≈ 270 crides. Palanques perquè avaluar no faci mal:
- Model petit com a jutge. Verificar contra una rúbrica és més fàcil que generar; els models petits rendeixen bé com a jutges de criteris binaris i costen una fracció. La calibració contra el patró or et diu si el petit és suficient — és una decisió mesurada, no una intuïció.
- Escalonar: els nivells 1 i 2 corren sempre (gratis o gairebé); el nivell 3 complet, en canvis de prompt/model/chunking i de manera periòdica.
- Mostreig: per al cicle ràpid d'iteració, un subconjunt estratificat (mantenint les proporcions 40/25/20/15 dels tipus de cas) dona senyal direccional barat; el conjunt complet es reserva per a la decisió de desplegament.
- Jutge només on cal: els criteris automàtics filtren primer; si el contracte ja és invàlid, no es gasta jutge a avaluar fidelitat.
Ordre de magnitud amb els preus de preus.py: una execució completa amb jutge petit queda típicament en cèntims-pocs euros — comparat amb els ~417 €/mes d'operació projectats a 03-04, l'assegurança antiregressions és de les partides més barates del sistema.
Errors Comuns i Consells
- Testejar el LLM amb asserts d'igualtat exacta.
assert resposta == "..."sobre text lliure és un test intermitent garantit. Sobre les sortides del model es verifiquen propietats (contracte, frase, fonts), no literals. - Gastar tokens a testejar el teu propi codi.
netejar_jsonno necessita un LLM per ser testejat; necessita 10 casos retorçats a pytest. Cada cosa al seu nivell de la piràmide. - Refiar-se del jutge sense calibrar-lo. Un jutge sense % d'acord mesurat contra el patró or és una altra font de números bonics i inservibles. Primer acord ≥ 85-90%, després confiança — i tot i així, mostreig periòdic dels seus veredictes.
- Avaluar amb 1 execució per cas. El no determinisme converteix una execució única en una moneda a l'aire. Tres execucions (l'hàbit de 04-05) distingeixen "falla sempre" de "falla de vegades", que són bugs diferents.
- Oblidar les versions al registre. Un CSV sense
PROMPT_VERSION/CHUNKING_VERSION/MODELés un número orfe: no pots comparar, no pots reproduir, no pots fer bisecció de regressions. - Definir els llindars després de veure els resultats. És la via ràpida per normalitzar regressions. Llindar primer, execució després — i la línia base anterior com a referència obligatòria.
- Confondre avaluació offline amb monitorització. Tot el d'aquesta lliçó passa abans de desplegar, amb casos coneguts. El que passa en producció amb preguntes reals d'usuaris reals és el territori de 06-04.
Exercicis
Exercici 1: classificar verificacions a la piràmide
Per a cada verificació, decideix a quin nivell pertany (1, 2 o 3) i amb quin mecanisme es comprova (assert determinista, propietat amb crida real, criteri automàtic del runner o LLM-as-judge): (a) estimar_cost() calcula bé el preu de 1.000 tokens d'entrada i 500 de sortida; (b) tota resposta de DocuBot és JSON parsejable que passa validar_contracte; (c) les respostes a preguntes sobre facturació són fidels a la documentació recuperada; (d) redactar() substitueix dos emails diferents del mateix text; (e) davant d'un cas adversarial amb instrucció intrusa al chunk, la resposta no reprodueix l'afirmació falsa.
Exercici 2: escriure un test de propietat nou
Escriu un test de nivell 2 per a aquesta propietat de DocuBot: "si la resposta té confianca ≥ 0.7, aleshores fonts no pot estar buida" (una resposta segura sense fonts és sospitosa d'al·lucinació). Fes servir respondre_amb_rag, netejar_json i una pregunta que sí que tingui resposta a la wiki.
Exercici 3: dissenyar la calibració del jutge
Tens 40 casos avaluats a mà per al criteri fidelitat (patró or: 31 "sí", 9 "no"). Passes el jutge i obtens: coincideix en 29 dels "sí" i en 5 dels "no". (a) Calcula el % d'acord global. (b) Quina proporció dels "no" humans detecta el jutge, i per què aquest número importa més que l'acord global en aquest criteri? (c) Automatitzaries ja aquest criteri? Què faries abans?
Solucions
Exercici 1:
- (a) Nivell 1, assert determinista:
estimar_costés aritmètica pura; pytest sense tokens. - (b) Nivell 2, propietat amb crides reals sobre casos sentinella (i a més criteri automàtic a cada fila del runner de nivell 3 — les propietats dures es comproven a tots dos llocs perquè al nivell 3 surten gratis).
- (c) Nivell 3, LLM-as-judge: la fidelitat requereix comparar semànticament resposta i documentació; cap assert no ho fa.
- (d) Nivell 1, assert determinista:
redactarés una funció pura de la 06-02. - (e) Nivell 3, criteri del runner: automatitzable en part amb cerca de l'afirmació falsa a la resposta (determinista) i completable amb jutge per als matisos — tal com es va dissenyar el
criteri_exita 06-01.
Exercici 2:
import pytest
from rag import respondre_amb_rag
from validacio import netejar_json
@pytest.mark.llm
def test_confianca_alta_implica_fonts():
"""Propietat: confianca >= 0.7 => fonts no buida.
Una resposta 'segura' sense fonts és el perfil típic d'una al·lucinació."""
sortida = netejar_json(respondre_amb_rag("Com creo un webhook a Nubelia?"))
assert sortida is not None, "la sortida no és JSON parsejable"
if sortida["confianca"] >= 0.7:
assert sortida["fonts"], (
f"confianca {sortida['confianca']} sense fonts: possible al·lucinació"
)Observa la forma lògica: la propietat és una implicació ("si confiança alta, aleshores fonts"), així que el test no falla si el model retorna confiança baixa — això seria un altre cas, no una violació. És un patró habitual en tests de propietats: verificar l'invariant només quan la seva precondició es dona (i tenir casos que garanteixin que la precondició es dona sovint).
Exercici 3:
- (a) Acords: 29 + 5 = 34 de 40 → 85% d'acord global.
- (b) Dels 9 "no" humans (respostes infidels), el jutge en detecta 5 → 55,6%. Importa més que el 85% global perquè els "no" són precisament allò que el jutge existeix per caçar: una resposta infidel que passa per bona és l'error car (al·lucinació en producció), mentre que l'acord global està inflat per la majoria de casos "sí" fàcils. És el clàssic problema de classes desequilibrades: mira el recall de la classe minoritària, no només l'exactitud.
- (c) Encara no. El 85% frega el llindar, però detectar només la meitat de les infidelitats és insuficient. Abans: llegir els 4 "no" que el jutge va deixar escapar (les justificacions del jutge hi ajuden), reforçar el prompt del jutge amb instruccions de rigor i 1-2 exemples few-shot de veredictes negatius semblants als fallats, potser provar un altre model com a jutge, i recalibrar. Automatitzar només quan la detecció de "no" sigui alta de manera estable — i fins i tot aleshores, continuar mostrejant desacords.
Conclusió
El deute de 02-04 i 04-05 està saldat: l'avaluació de DocuBot ja no depèn d'una tarda heroica amb un CSV, sinó d'una piràmide que corre sola — pytest per al codi determinista (parsers, chunking, validació d'arguments, redacció: la meitat del sistema es testeja gratis), tests de propietats per als invariants de les sortides del model (contracte JSON, frase anti-al·lucinació, fonts que existeixen), i el runner que executa el conjunt complet — fàcils, ambigus, sense resposta, adversarials suaus i els adversarials de seguretat de 06-01 — registrant cada fila amb PROMPT_VERSION, CHUNKING_VERSION i MODEL perquè cada número tingui nom i cognoms. On l'assert no arriba, un LLM-as-judge amb la rúbrica de 04-05 en escala binària emet els veredictes de fidelitat i rellevància — però només després de guanyar-se la confiança contra el teu patró or manual, i vigilat pels seus biaixos coneguts. Amb llindars definits per endavant i la línia base anterior com a vara de mesurar, "puc desplegar aquest canvi de prompt?" passa d'acte de fe a semàfor. Però aquest semàfor només veu les preguntes que tu vas imaginar: el dia del desplegament comencen les preguntes que ningú no va imaginar, els costos reals, les latències de la tarda del dilluns i els usuaris que se'n van sense dir per què. Veure què passa allà dins — convertir les traces que vam sembrar a 05-02 en observabilitat de debò, amb logs estructurats, mètriques, panells i alertes accionables — és la penúltima lliçó del curs.
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ó
