Les lliçons anteriors acabaven sempre en text: una resposta redactada, una etiqueta de categoria. Suficient mentre el lector sigui un humà. Però DocuBot no viurà en una terminal: anirà incrustat al web de suport de Nubelia, i aquest web és codi — necessita saber en quina categoria ha caigut la pregunta per pintar la icona correcta, quines fonts citar com a enllaços, i quina confiança té la resposta per decidir si la mostra o la deriva a un humà. És a dir: el consumidor de la sortida de l'LLM serà un programa, i els programes no parsegen prosa. Aquesta lliçó ensenya a aconseguir sortides estructurades i parsejables — sobretot JSON — d'un sistema que, com sabem des de 01-04, és no determinista per naturalesa: com demanar-les, com validar-les en Python, com robustir-les i com controlar altres aspectes del format (markdown, longitud, idioma). Seguim sense cridar cap API: treballem amb el text dels prompts, les respostes esperades i el codi de validació que les rebrà.
Contingut
- Per què un desenvolupador necessita sortides parsejables
- Demanar JSON amb un esquema al prompt
- Validar en Python: json.loads i programació defensiva
- Tècniques per robustir la sortida
- Modes JSON natius: què t'espera al mòdul 3
- Control d'altres formats: markdown, longitud i idioma
Per què un desenvolupador necessita sortides parsejables
Imagina la integració de DocuBot al web de suport de Nubelia amb sortida en text lliure:
La pregunta sembla de permisos. Per convidar un usuari extern necessites rol d'Administrador... Font: secció "Rols i permisos". Estic força segur d'aquesta resposta.
Com n'extreu el codi la categoria? Amb una expressió regular que busqui "sembla de X"? I si demà el model escriu "Es tracta d'una qüestió de permisos"? Cada resposta redactada de manera diferent (no determinisme, 01-04) trencaria el parser. La solució és invertir el contracte: en lloc d'adaptar el codi a la prosa del model, obliguem el model a parlar l'idioma del codi. El contracte de DocuBot amb el web de suport serà:
{
"categoria": "permisos",
"resposta": "Per convidar un usuari extern necessites rol d'Administrador. Ves a Espai de treball → Membres → Convidar.",
"fonts": ["Rols i permisos"],
"confianca": "alta"
}Quatre camps, cadascun amb un consumidor clar al web:
| Camp | Tipus | Qui el consumeix |
|---|---|---|
categoria |
string (enum de 6 valors) | Enrutament i icona a la interfície |
resposta |
string | Es mostra a l'empleat |
fonts |
llista de strings | Es pinten com a enllaços a la wiki |
confianca |
"alta" | "mitjana" | "baixa" |
Si és "baixa", el web ofereix escalar a un humà en lloc de fiar-se'n |
El camp confianca mereix una pausa: és la versió estructurada de la regla 3 del system prompt de 02-01 ("digues que no ho saps"). Un text lliure que admet ignorància és útil; un camp "confianca": "baixa" és accionable: el codi hi pot ramificar a sobre. Estructurar la sortida no és només comoditat de parseig — és convertir els comportaments de l'assistent en dades sobre les quals programar. (Que la decisió final d'escalar la prengui un flux amb humans, i no el model pel seu compte, és un principi de disseny al qual tornarem al mòdul 6.)
Demanar JSON amb un esquema al prompt
La tècnica base és descriure l'esquema al prompt i — recordant el few-shot de 02-02 — mostrar un exemple, perquè el format s'ensenya millor amb exemples que amb descripcions. Afegim a les instruccions de DocuBot:
FORMAT DE RESPOSTA:
Respon ÚNICAMENT amb un objecte JSON vàlid, sense text abans ni
després, sense cometes de bloc de codi. L'esquema és:
{
"categoria": string, una de: "facturacio", "permisos",
"integracions", "api", "incidencies", "altres",
"resposta": string, la resposta a l'usuari en l'idioma de la
pregunta, màxim 150 paraules,
"fonts": array de strings, títols de les seccions de
<documentacio> usades; [] si no n'has usat cap,
"confianca": "alta" | "mitjana" | "baixa". Usa "baixa" si la
documentació no cobreix la pregunta; en aquest cas
"resposta" ha de ser "No trobo aquesta informació a
la documentació disponible." i "fonts" ha de ser [].
Exemple de resposta vàlida:
{"categoria": "permisos", "resposta": "Només els Administradors poden convidar usuaris externs. Ves a Espai de treball → Membres.", "fonts": ["Rols i permisos"], "confianca": "alta"}Decisions de disseny que convé interioritzar:
- "ÚNICAMENT… sense text abans ni després". Sense això, els models tendeixen a embolcallar el JSON amb cortesia ("I tant! Aquí el tens: …") o en un bloc markdown
```json. Totes dues coses trenquen unjson.loadsingenu. - Cada camp porta tipus, valors permesos i criteri d'ús. No només "un camp confianca": quan és baixa i què implica per a la resta de camps. Els camps es defineixen junts perquè s'omplen junts.
- Els valors tancats s'enumeren (
"alta" | "mitjana" | "baixa"). Hi tornarem a la secció de robustesa. - L'exemple va en una sola línia deliberadament: també ensenya que no cal pretty-printing (menys tokens de sortida = menys cost i latència, 01-04).
- El cas "no ho sé" està integrat a l'esquema. La regla anti-al·lucinació de 02-01 ara produeix dades coherents:
confianca: "baixa"+ resposta fixa + fonts buides. Els tres camps expliquen la mateixa història i el codi ho pot verificar.
Amb la pregunta "Nubelia permet exportar un projecte a PDF?" (sense cobertura a la documentació), la resposta esperada del DocuBot estructurat és:
{"categoria": "altres", "resposta": "No trobo aquesta informació a la documentació disponible.", "fonts": [], "confianca": "baixa"}Compara-la amb el DocuBot ingenu de 01-04, que s'inventava l'exportació a PDF amb tota fluïdesa. El mateix model; el prompt i el contracte de sortida marquen la diferència.
Validar en Python: json.loads i programació defensiva
Regla d'or: la sortida d'un LLM és entrada no fiable, exactament igual que un formulari web. El model gairebé sempre retornarà JSON vàlid amb aquest prompt… i "gairebé sempre" en producció vol dir "de vegades no". El teu codi ho ha d'assumir. Validació defensiva pas a pas:
import json
CATEGORIES_VALIDES = {"facturacio", "permisos", "integracions",
"api", "incidencies", "altres"}
NIVELLS_CONFIANCA = {"alta", "mitjana", "baixa"}
RESPOSTA_FALLBACK = {
"categoria": "altres",
"resposta": "No he pogut processar la resposta. "
"Un agent humà revisarà la teva consulta.",
"fonts": [],
"confianca": "baixa",
}
def netejar_json(text: str) -> str:
"""Elimina embolcalls habituals: blocs markdown i text extra."""
text = text.strip()
if text.startswith("```"):
# Treu la primera línia (``` o ```json) i el tancament final
text = text.split("\n", 1)[1]
text = text.rsplit("```", 1)[0]
# Si encara hi ha prosa al voltant, queda't amb el primer {...} complet
inici, fi = text.find("{"), text.rfind("}")
if inici != -1 and fi > inici:
text = text[inici:fi + 1]
return text.strip()
def parsejar_resposta_docubot(text_model: str) -> dict:
"""Converteix la sortida del model en un dict segur per al web."""
# Pas 1: parsejar (pot fallar → JSON mal format)
try:
dades = json.loads(netejar_json(text_model))
except json.JSONDecodeError:
return RESPOSTA_FALLBACK
# Pas 2: és un objecte? (el model podria retornar una llista o un string)
if not isinstance(dades, dict):
return RESPOSTA_FALLBACK
# Pas 3: camps amb valors per defecte segurs
resultat = {
"categoria": dades.get("categoria", "altres"),
"resposta": dades.get("resposta", ""),
"fonts": dades.get("fonts", []),
"confianca": dades.get("confianca", "baixa"),
}
# Pas 4: normalitzar valors fora de l'enum
if resultat["categoria"] not in CATEGORIES_VALIDES:
resultat["categoria"] = "altres"
if resultat["confianca"] not in NIVELLS_CONFIANCA:
resultat["confianca"] = "baixa"
if not isinstance(resultat["fonts"], list):
resultat["fonts"] = []
# Pas 5: regles de coherència del domini
if not resultat["resposta"].strip():
return RESPOSTA_FALLBACK
if resultat["confianca"] == "baixa":
resultat["fonts"] = [] # sense confiança no hi ha fonts a citar
return resultatExplicació de cada capa de defensa:
netejar_jsonataca les dues fallades més freqüents a la pràctica: l'embolcall```json … ```i la frase de cortesia abans de l'objecte. Buscar del primer{a l'últim}és un truc pragmàtic que rescata la majoria de casos (no tots: un JSON truncat a mitja cadena continuarà fallant, i està bé que falli — per a això hi ha el pas 1).try/except json.JSONDecodeErrorés innegociable: és l'única manera segura de saber si un text és JSON vàlid. No validis mai "a ull" ambstartswith("{").dades.get(camp, valor_per_defecte)en lloc dedades["camp"]: si el model omet un camp, obtens un valor segur en lloc d'unKeyErrora les tres de la matinada. Fixa't que cada valor per defecte és el conservador: categoriaaltres, confiançabaixa— davant del dubte, el sistema desconfia.- Normalització d'enums: si el model retorna
"Permisos"(majúscula) o"billing"(se li ha creuat l'anglès), el valor cau aaltres/baixaen lloc de propagar-se com a categoria fantasma per la teva base de dades. (Podries ser més indulgent i normalitzar a minúscules abans de comparar; decideix-ho i documenta-ho.) - Regles de coherència: la validació estructural (és JSON?, tipus correctes?) no garanteix coherència de domini (confiança baixa amb tres fonts citades?). Aquesta darrera capa és teva.
RESPOSTA_FALLBACKtanca el sistema: passi el que passi, el web de suport rep un dict amb la forma correcta i un missatge digne. La fallada del model es degrada amb elegància en lloc de convertir-se en una excepció per a l'usuari. Al mòdul 3 hi afegirem una altra opció: reintentar la crida quan el parseig falla.
En projectes reals, aquesta validació manual sol delegar-se en biblioteques d'esquemes com Pydantic (defineixes la classe, ella valida tipus i enums); el principi és idèntic i convé haver escrit la versió manual una vegada per entendre què et regala la biblioteca.
Tècniques per robustir la sortida
Validar és la xarxa de seguretat; aquestes tècniques redueixen quantes vegades cal usar-la:
- Instrucció "només JSON", explícita i repetida. "Respon ÚNICAMENT amb l'objecte JSON, sense explicacions, sense markdown" — i repeteix-la al final del prompt si el context és llarg (el "lost in the middle" de 01-04 també afecta les instruccions de format).
- Esquema + exemple, sempre junts. La descripció defineix el contracte; l'exemple el demostra. Si observes errors de format recurrents, afegeix un segon exemple que cobreixi el cas conflictiu (few-shot aplicat al format): per exemple, un exemple amb
confianca: "baixa"perquè el model vegi també la forma del cas "no ho sé". - Camps enum millor que camps lliures.
"confianca": "alta"|"mitjana"|"baixa"és robust;"confianca": 0.87convida el model a inventar una precisió decimal que no té (en què es diferencia 0.87 de 0.79? En res mesurable). Regla general: com menys graus de llibertat tingui un camp, més fiable serà. Els enums, a més, es validen amb un simplein. - Estructures planes millor que niuaments profunds. Un objecte de 4 camps falla poc; un amb tres nivells de niuament i arrays d'objectes falla més. Si necessites molta estructura, planteja't si són diverses crides encadenades (02-02) amb sortides simples.
- Demana els camps "difícils" en un ordre que ajudi. Col·locar
respostaabans queconfiancadeixa que l'autoavaluació es recolzi en la resposta ja generada — és el principi de la cadena de raonament (02-02) aplicat dins del JSON: primer la feina, després el judici sobre la feina. - Temperatura baixa. Per a sortides estructurades vols l'extrem determinista de la taula de temperatures de 01-02. La creativitat és enemiga del parser.
I la bona notícia: no hi estaràs sol. Els principals proveïdors ofereixen modes JSON i structured outputs natius — paràmetres de l'API amb què el mateix motor garanteix JSON sintàcticament vàlid o fins i tot conforme a un esquema formal que tu declares. Els farem servir tan bon punt arribem a l'API a 03-01. Per què aprendre, doncs, la versió "artesanal"? Tres raons: no tots els models/proveïdors l'ofereixen; els modes natius garanteixen sintaxi, no coherència de domini (la validació dels passos 4-5 continua sent teva); i entendre el mecanisme et permet depurar quan alguna cosa falla. Més endavant veuràs que el function calling (05-01) és l'evolució natural d'aquesta mateixa idea: en lloc de demanar JSON per les bones, l'API formalitza l'esquema com la signatura d'una funció que el model "omple".
Control d'altres formats: markdown, longitud i idioma
JSON és el cas estrella, però el control de format és més ampli. Tres fronts que DocuBot necessita:
Markdown. El camp resposta es renderitzarà al web de suport, que admet markdown bàsic. Convé dir exactament quin:
El camp "resposta" pot usar markdown bàsic: **negreta**, llistes amb "-" i passos numerats. No usis encapçalaments (#), taules ni blocs de codi llevat que la resposta inclogui un exemple de l'API.
Sense aquesta acotació, el model pot retornar encapçalaments ## que en un panell petit del web es veuen desproporcionats. El format de sortida s'especifica per al consumidor real: un panell de suport, un correu, una cel·la de taula admeten coses diferents.
Longitud. Ja ho vam veure a 02-01: els models no compten paraules amb exactitud, però les instruccions de longitud desplacen la mitjana de manera fiable. Funciona millor especificar unitats estructurals que paraules: "màxim 3 passos", "una frase per font", "2 paràgrafs". Les unitats discretes es respecten millor que els recomptes ("màxim 147 paraules" no passarà de guia). Si la longitud és un límit dur (una columna de base de dades, un SMS), trunca-la en codi: la instrucció redueix els casos, la validació garanteix el límit.
Idioma. Nubelia opera a Espanya i té equips que escriuen en castellà, català i anglès. La regla de DocuBot ja ho preveu ("respon en l'idioma de la pregunta"), però amb sortida JSON hi ha una subtilesa que sorprèn la primera vegada: cal separar l'idioma del contingut de l'idioma de l'estructura:
El camp "resposta" ha d'estar en l'idioma de la pregunta de l'usuari. Les claus del JSON i els valors de "categoria" i "confianca" es mantenen SEMPRE en els valors exactes de l'esquema, sense traduir.
Sense la segona frase, una pregunta en anglès pot produir "confianca": "high" — JSON perfectament vàlid que el teu enum rebutjarà. L'esquema és codi; el contingut és llenguatge. (I fixa't que la validació de l'apartat anterior ja et protegia: "high" hauria caigut a "baixa". Les capes es recolzen entre si.)
Errors Comuns i Consells
- Parsejar amb
eval()en lloc dejson.loads. Mai:evalexecuta codi arbitrari, i estàs processant text generat per un sistema no fiable. És la diferència entre parsejar i executar. - Assumir que "el model gairebé sempre retorna JSON vàlid" equival a "sempre". L'1-2 % de fallades de format, amb milers de peticions, són desenes d'errors diaris. El
try/excepti el fallback no són paranoia: són el cost d'admissió. - Validar només la sintaxi i no el domini.
{"categoria": "receptes", "confianca": "altissima"}és JSON impecable i escombraries per al teu sistema. Enums i regles de coherència sempre. - Demanar números de confiança amb decimals. Conviden a una falsa precisió. Enums de 3 nivells: menys expressius, molt més honestos i validables.
- Esquemes quilomètrics amb 15 camps niuats. Cada camp extra és una oportunitat d'error i més tokens. Demana el que l'aplicació consumeix avui; afegeix camps quan n'existeixi el consumidor.
- Oblidar el cas d'error a l'esquema. Si l'esquema només contempla respostes reeixides, el model hi ficarà el "no ho sé" amb calçador en camps pensats per a una altra cosa. Dissenya l'esquema de la fallada (la nostra combinació
confianca: "baixa"+ resposta fixa +fonts: []) amb la mateixa cura que el de l'èxit. - Consell: desa les sortides del model que trenquin el teu parser. Cadascuna és un cas de prova gratuït per a la lliçó 02-04 i un candidat a exemple few-shot de format.
Exercicis
Exercici 1. L'equip de suport de Nubelia vol que DocuBot detecti a més la urgència de cada pregunta per prioritzar la cua. Amplia l'esquema JSON del prompt de DocuBot amb un camp urgencia ben dissenyat (valors, criteri d'assignació, exemple) i justifica les teves decisions davant de l'alternativa "urgencia": 7 (escala numèrica 1-10).
Exercici 2. Escriu què retorna parsejar_resposta_docubot (la funció de la lliçó) per a cadascuna d'aquestes tres sortides del model, i quin pas de la funció actua en cada cas:
a) ```json\n{"categoria": "api", "resposta": "El límit és de 100 peticions/min.", "fonts": ["Límits de l'API"], "confianca": "alta"}\n```
b) {"categoria": "Facturació", "resposta": "Pots descarregar-la a Configuració → Facturació.", "confianca": "mitjana"}
c) Ho sento, ara mateix no puc generar la resposta.
Exercici 3. Escriu la instrucció de format (el bloc FORMAT DE RESPOSTA del prompt) per a un altre cas de Nubelia: un prompt que rep el text d'un tiquet i ha de retornar JSON amb resum (string, màxim 2 frases, en l'idioma del tiquet), producte_afectat (enum: "taulers", "api", "informes", "altres") i requereix_enginyer (booleà true/false amb criteri explícit). Inclou la instrucció "només JSON" i un exemple vàlid.
Solucions
Solució 1. Ampliació possible de l'esquema:
"urgencia": "critica" | "alta" | "normal". Usa "critica" només si
l'usuari descriu un servei caigut o una pèrdua de dades en
curs; "alta" si hi ha un bloqueig de feina sense
alternativa; "normal" en la resta de casos (dubtes,
millores, consultes).
Exemple: {"categoria": "incidencies", "resposta": "...", "fonts":
["Estat del servei"], "confianca": "alta", "urgencia": "critica"}Justificació: un enum de 3 nivells amb criteris observables ("servei caigut", "bloqueig sense alternativa") és assignable de manera consistent i validable amb un in. Una escala 1-10 obliga el model a inventar distincions que ningú no ha definit (què separa un 6 d'un 7?), produeix valors incomparables entre execucions (no determinisme, 01-04) i complica el codi consumidor (el llindar de prioritat és ≥6 o ≥7?). Regla: la granularitat del camp ha de ser la que el consumidor realment fa servir — i una cua de suport distingeix tres nivells, no deu.
Solució 2.
a) netejar_json elimina l'embolcall ```json … ```, el parseig del pas 1 funciona i tots els valors passen les validacions: es retorna el dict complet tal qual, amb categoria: "api" i confianca: "alta".
b) El JSON parseja bé (passos 1-2), però: falta fonts → el pas 3 l'omple amb []; "Facturació" no és a l'enum (majúscula i accent) → el pas 4 el normalitza a "altres". Resultat: {"categoria": "altres", "resposta": "Pots descarregar-la a Configuració → Facturació.", "fonts": [], "confianca": "mitjana"}. (Il·lustra el cost d'una normalització estricta: una variant trivial de majúscules perd la categoria real; seria raonable aplicar .lower() i treure accents abans de comparar.)
c) No hi ha ni { ni }: netejar_json retorna el text tal qual, json.loads llança JSONDecodeError i el pas 1 retorna RESPOSTA_FALLBACK. El web mostra el missatge neutre i ofereix l'escalat humà: degradació elegant.
Solució 3. Una solució possible:
FORMAT DE RESPOSTA:
Respon ÚNICAMENT amb un objecte JSON vàlid, sense text abans ni
després i sense blocs de codi. Esquema:
{
"resum": string, màxim 2 frases, en l'idioma del tiquet,
"producte_afectat": "taulers" | "api" | "informes" | "altres",
"requereix_enginyer": booleà. true només si el tiquet descriu un
error reproduïble del producte (fallada, dada incorrecta,
caiguda); false si és un dubte d'ús, una petició de millora o un
problema de configuració del mateix usuari.
}
Les claus i els valors de "producte_afectat" no es tradueixen mai.
Exemple de resposta vàlida:
{"resum": "L'usuari no pot exportar l'informe setmanal; la descàrrega falla amb error.", "producte_afectat": "informes", "requereix_enginyer": true}Claus: instrucció "només JSON" al principi, enum tancat, booleà amb criteri explícit d'assignació (sense ell, requereix_enginyer seria cara o creu), nota de no-traducció i exemple en una línia que demostra el contracte.
Conclusió
DocuBot ja parla dos idiomes: el de les persones (el camp resposta, en l'idioma de l'usuari, amb el seu markdown acotat) i el dels programes (el contracte JSON categoria/resposta/fonts/confianca, amb enums tancats i el seu cas de fallada dissenyat). Has après les dues meitats inseparables de la disciplina: demanar bé (esquema + exemple, "només JSON", camps amb pocs graus de llibertat) i validar sempre (json.loads protegit, valors per defecte conservadors, coherència de domini i un fallback digne), sabent que els modes structured outputs natius que veurem a 03-01 et trauran feina sintàctica però mai la validació de domini, i que el function calling de 05-01 portarà aquesta idea un pas més enllà. Però queda una pregunta incòmoda que aquest mòdul encara no ha respost: el prompt de DocuBot ja té identitat, regles, exemples few-shot i un esquema de sortida… com sabem que funciona? I com sabrem que la propera "millora" del prompt no empitjora el que ja funcionava? L'última lliçó del mòdul converteix el prompting en enginyeria de debò: versionat, conjunts de casos de prova i avaluació sistemàtica de prompts.
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ó
