Fins ara, tot el que DocuBot "ha respost" ho hem escrit nosaltres sobre el paper: prompts dissenyats al mòdul 2 i respostes esperades contra les quals avaluar. En aquesta lliçó el sistema pren vida: prepararem un entorn Python net, farem la primera crida real a l'API d'un LLM enviant els rols system/user que vam dissenyar a 02-01, disseccionarem la resposta que retorna el proveïdor (contingut, motiu d'aturada, consum de tokens) i connectarem el parser JSON defensiu de 02-03 a una resposta de debò. Ho exemplificarem amb l'SDK d'Anthropic, però els conceptes —missatges amb rols, límit de sortida, objecte d'ús— són equivalents en qualsevol proveïdor, i tancarem amb una taula que ho demostra.
Contingut
- Del paper a l'API: què construirem
- Preparar l'entorn: venv, SDK i clau d'API
- La primera crida: system + user amb els artefactes de
prompts.py - Anatomia de la resposta: contingut,
stop_reasoniusage - Paràmetres principals:
model,max_tokensitemperature - Connectar el parser JSON de 02-03 a una resposta real
- Equivalència de conceptes entre proveïdors
Del paper a l'API: què construirem
A 01-03 vam prendre una decisió d'arquitectura que ara es materialitza: API gestionada, model de gamma mitjana i temperature baixa. Això vol dir que no instal·larem ni servirem cap model; enviarem peticions HTTP a un proveïdor i rebrem respostes. L'SDK del proveïdor s'encarrega del transport, l'autenticació i la serialització, i nosaltres ens concentrem en el que ja tenim: SYSTEM_DOCUBOT, les plantilles de prompts.py (versió 1.2) i el contracte JSON amb la seva validació defensiva.
L'objectiu d'aquesta lliçó és un mòdul client.py que, donada una pregunta i un bloc de documentació, retorni el diccionari validat {"categoria", "resposta", "fonts", "confianca"}. És la primera peça "viva" de DocuBot.
Preparar l'entorn: venv, SDK i clau d'API
Treballarem en un entorn virtual per aïllar dependències de la resta del sistema:
# Crear i activar l'entorn virtual python -m venv .venv # Linux / macOS source .venv/bin/activate # Windows (PowerShell) .venv\Scripts\Activate.ps1 # Instal·lar l'SDK del proveïdor (exemple: Anthropic) pip install anthropic # Congelar dependències per a reproduïbilitat pip freeze > requirements.txt
La clau d'API mai no s'escriu al codi ni es puja al repositori. Es defineix com a variable d'entorn, que és on l'SDK la busca automàticament:
# Linux / macOS export ANTHROPIC_API_KEY="la-teva-clau-aqui" # Windows (PowerShell) $env:ANTHROPIC_API_KEY = "la-teva-clau-aqui"
Bones pràctiques des del primer dia:
- Variable d'entorn o gestor de secrets, mai una cadena al codi. Una clau filtrada en un commit és un incident de seguretat (i de facturació: qui la tingui gasta la teva quota).
- Afegeix
.venv/i qualsevol fitxer.enval.gitignore. - Fes servir una clau diferent per a desenvolupament i per a producció; així en pots revocar una sense afectar l'altra.
La primera crida: system + user
Creem client.py reutilitzant els artefactes del mòdul 2 tal qual. Fixa't que el system prompt viatja en un paràmetre propi, separat de la llista de missatges — exactament la separació de rols que vam estudiar a 02-01:
# client.py
import os
import anthropic
from prompts import SYSTEM_DOCUBOT, prompt_resposta, PROMPT_VERSION
# L'identificador exacte del model depèn del proveïdor i del moment;
# el llegim d'una variable d'entorn amb un placeholder com a valor per defecte.
MODEL = os.environ.get("DOCUBOT_MODELO", "claude-...")
# El client llegeix ANTHROPIC_API_KEY de l'entorn: no passem la clau a mà.
client = anthropic.Anthropic()
def preguntar_docubot_brut(pregunta: str, documentacio: str):
"""Envia una pregunta a l'API i retorna la resposta completa (sense processar)."""
return client.messages.create(
model=MODEL,
max_tokens=1024,
temperature=0.2, # baixa: respostes consistents (decisió de 01-03)
system=SYSTEM_DOCUBOT, # el system prompt v1.2 dissenyat a 02-01
messages=[
{
"role": "user",
"content": prompt_resposta(pregunta, documentacio),
}
],
)Desglossament línia a línia:
anthropic.Anthropic()crea el client. No li passem la clau: la resol des d'ANTHROPIC_API_KEY. Si no existeix, la crida fallarà amb un error d'autenticació — millor això que una clau escrita al fitxer.model=MODELindica quin model respon. Fem servir un placeholder tipus"claude-..."perquè els identificadors concrets canvien amb el temps; al teu projecte fixa el que vas triar a 01-03.system=SYSTEM_DOCUBOTenvia la identitat, les regles anti-al·lucinació i el to de DocuBot. En aquest proveïdor és un paràmetre de primer nivell; en d'altres va com un missatge més amb rolsystem(ho veurem a la taula final).messagesés la llista de torns de conversa. De moment un únic tornuser, el contingut del qual generaprompt_resposta(): la pregunta de l'empleat més el bloc<documentacio>que exigeix la regla 2 del system prompt.max_tokens=1024és el topall de tokens de sortida: la resposta mai no serà més llarga que això. És obligatori en aquesta API i és el teu primer fre de cost.
Ho provem amb dades fictícies de Nubelia:
if __name__ == "__main__":
documentacio = """## Exportar un projecte
Per exportar un projecte a CSV: Projecte > Configuració > Exportar > CSV.
L'exportació inclou tasques, responsables i dates. Disponible als
plans Pro i Business."""
pregunta = "Com exporto el meu projecte a CSV?"
resposta = preguntar_docubot_brut(pregunta, documentacio)
print(resposta)Si tot està ben configurat, en executar python client.py veuràs per primera vegada una resposta generada pel model amb el teu system prompt. Val la pena aturar-s'hi un segon: les regles que vas escriure a 02-01 estan governant, ara mateix, la sortida d'un model real.
Anatomia de la resposta
L'objecte que retorna l'API conté molt més que el text. Els seus camps importants:
resposta = preguntar_docubot_brut(pregunta, documentacio)
# 1. El contingut: una LLISTA de blocs, no un string directe
for bloc in resposta.content:
if bloc.type == "text":
print(bloc.text)
# 2. Per què va deixar de generar
print(resposta.stop_reason) # p. ex. "end_turn"
# 3. Quants tokens ha consumit (això és la factura!)
print(resposta.usage.input_tokens) # tokens d'entrada (system + user)
print(resposta.usage.output_tokens) # tokens generats
# 4. Metadades
print(resposta.model) # el model que ha respost realment
print(resposta.id) # identificador únic de la peticióTres detalls que convé interioritzar:
contentés una llista de blocs. Avui només conté blocs de text, però el mateix camp transporta altres tipus (crides a eines al mòdul 5, per exemple). Acostuma't des d'ara a filtrar perbloc.type == "text"en comptes d'accedir acontent[0].texta cegues.stop_reasonet diu per què va acabar la generació. Els valors principals:
stop_reason |
Significat | Què fer |
|---|---|---|
end_turn |
El model ha acabat de manera natural | Cas normal, processar la resposta |
max_tokens |
S'ha esgotat el topall de sortida | La resposta està tallada: puja'l o demana sortides més breus |
stop_sequence |
Ha aparegut una seqüència d'aturada definida per tu | Processar fins a aquell punt |
tool_use |
El model vol cridar una eina | Ho veurem al mòdul 5 |
Per a DocuBot, max_tokens és especialment perillós: un JSON tallat a mitges mai no validarà. Comprovar stop_reason abans de parsejar evita perdre temps depurant "JSON invàlid" quan el problema real era el topall de sortida.
usageés el teu comptador de cost. Entrada i sortida es facturen per separat i a preus diferents. A 03-04 construirem l'estimació de costos sobre aquests dos números; de moment, imprimeix-los a cada prova per anar desenvolupant intuïció de quant "pesa" cada crida.
Una funció auxiliar que farem servir durant tot el curs:
def extreure_text(resposta) -> str:
"""Concatena els blocs de text d'una resposta de l'API."""
return "".join(
bloc.text for bloc in resposta.content if bloc.type == "text"
)Paràmetres principals
Els tres paràmetres que ajustaràs constantment:
| Paràmetre | Què controla | Guia per a DocuBot |
|---|---|---|
model |
Quin model respon (capacitat, velocitat, preu) | Gamma mitjana per a respostes; a 03-04 veurem que el classificador pot fer servir un de menor |
max_tokens |
Topall dur de tokens de sortida | 1024 sobra per a respostes de <150 paraules + JSON; és fre de cost i de descontrol |
temperature |
Aleatorietat del mostreig (0 = molt determinista, 1 = molt variat) | Baixa (0.0–0.3): DocuBot ha de ser consistent, no creatiu |
Sobre temperature, dos matisos honestos:
- Temperature baixa no és determinisme total. Com vam veure a 01-04 (no determinisme), dues crides idèntiques poden diferir lleugerament fins i tot a temperature 0. Redueix la variància; no l'elimina.
- Alguns models recents d'alguns proveïdors fixen el mostreig internament i rebutgen el paràmetre
temperatureamb un error de petició invàlida. Consulta la documentació del model concret que facis servir; si el rebutja, simplement omet el paràmetre i controla l'estil mitjançant el prompt.
Connectar el parser JSON de 02-03 a una resposta real
A 02-03 vam dissenyar el contracte {"categoria", "resposta", "fonts", "confianca"} i la validació defensiva (netejar_json, json.loads amb try/except, defaults conservadors, RESPOSTA_FALLBACK). Tot allò es va escriure contra respostes hipotètiques; ara ho endollem a la sortida real:
import json
from prompts import RESPOSTA_FALLBACK
from validacio import netejar_json, validar_contracte # el mòdul escrit a 02-03
def preguntar_docubot(pregunta: str, documentacio: str) -> dict:
"""Crida completa: API -> text -> JSON validat amb el contracte de 02-03."""
resposta = preguntar_docubot_brut(pregunta, documentacio)
# Un JSON tallat per max_tokens mai no parsejarà: detectar-ho aviat i clar.
if resposta.stop_reason == "max_tokens":
return RESPOSTA_FALLBACK
text = extreure_text(resposta)
try:
dades = json.loads(netejar_json(text)) # treu ```json ... ``` i soroll
except (json.JSONDecodeError, TypeError):
return RESPOSTA_FALLBACK
# Defaults conservadors per a camps absents o invàlids (02-03)
return validar_contracte(dades)La primera vegada que executis això contra el model real és probable que descobreixis coses que el paper no mostrava: respostes embolcallades amb ```json, un camp confianca amb un valor fora de l'enumerat, una llista fonts que arriba com a string. Exactament per a això vam construir la validació defensiva — i per això netejar_json i els defaults no eren paranoia, sinó enginyeria.
Sortida estructurada nativa. Com vam anunciar a 02-03, els proveïdors ofereixen modes que garanteixen que la sortida compleix un esquema JSON: a Anthropic es declara un esquema a la petició (output_config amb json_schema), a OpenAI existeix l'equivalent amb response_format/esquemes estrictes i a Gemini amb response_schema. Quan el teu proveïdor i model ho suportin, activa-ho: converteix el "gairebé sempre és JSON vàlid" en "sempre ho és". Tot i així, mantén la validació defensiva: l'esquema garanteix la forma, no el contingut (una confianca: "alta" en una resposta inventada continua sent un problema d'avaluació, no de format), i el teu codi continuarà sent portable entre proveïdors.
Equivalència de conceptes entre proveïdors
Tot l'anterior existeix, amb altres noms, en qualsevol proveïdor seriós. A nivell de conceptes (no de xifres ni d'identificadors concrets):
| Concepte | Anthropic | OpenAI | Google Gemini |
|---|---|---|---|
| SDK Python | anthropic |
openai |
google-genai |
| Clau d'API | ANTHROPIC_API_KEY (env) |
OPENAI_API_KEY (env) |
GOOGLE_API_KEY (env) |
| System prompt | Paràmetre system separat |
Missatge amb rol system/developer a la llista |
system_instruction a la configuració |
| Torns de conversa | messages amb rols user/assistant |
messages amb rols user/assistant |
contents amb rols user/model |
| Topall de sortida | max_tokens |
Paràmetre de límit de tokens de sortida | max_output_tokens |
| Consum de tokens | usage.input_tokens / usage.output_tokens |
Objecte usage (prompt / completion) |
usage_metadata |
| Motiu d'aturada | stop_reason |
finish_reason |
finish_reason |
| Sortida estructurada | Esquema JSON a la petició | response_format / esquemes estrictes |
response_schema |
La lliçó d'aquesta taula: aprèn els conceptes, no els noms. Si demà Nubelia canvia de proveïdor, el 90% del teu codi (plantilles, validació, avaluació, gestió d'errors) no canvia; només la fina capa que parla amb l'SDK. Per això concentrem tota la interacció amb l'API a client.py — un únic fitxer per reescriure si arriba aquell dia.
El que no hem tocat avui, deliberadament: la resposta arriba de cop al cap d'uns quants segons (streaming, 03-02), si la xarxa falla l'excepció explota sense control (errors i reintents, 03-02), cada crida comença de zero sense recordar res (converses, 03-03) i no sabem quant costa això al mes (costos, 03-04).
Errors Comuns i Consells
- Hardcodejar la clau d'API "només per provar". Aquell "només per provar" acaba en un commit. Variable d'entorn des del minut u, sense excepcions.
- Accedir a
resposta.content[0].textsense comprovar el tipus de bloc. Funciona avui; es trenca tan bon punt el model retorni un altre tipus de bloc. Fes servir sempre el filtratge perbloc.type. - Ignorar
stop_reason. El símptoma típic: "el parser JSON falla aleatòriament". La causa real: respostes tallades permax_tokens. Comprova el motiu d'aturada abans de parsejar. - Oblidar que
max_tokenslimita la sortida, no l'entrada. L'entrada està limitada per la finestra de context del model (01-04); són límits diferents amb errors diferents. - Reinventar el parser a cada script. Els artefactes de 02-03 (
netejar_json,validar_contracte,RESPOSTA_FALLBACK) són mòduls importables. Un únic punt de validació, un únic lloc per corregir. - Consell: imprimeix
usagea cada prova durant el desenvolupament. En dos dies tindràs una intuïció sòlida de quants tokens consumeix cada tipus de pregunta — i 03-04 et resultarà molt més natural. - Consell: executa contra l'API el conjunt d'avaluació de 02-04 (o una mostra de 10 preguntes) tan bon punt tinguis
preguntar_docubot()funcionant. És la teva primera avaluació amb respostes reals, i la base de comparació per a tot el que optimitzis després.
Exercicis
Exercici 1
Escriu una funció diagnostic_crida(resposta) que rebi l'objecte de resposta de l'API i retorni un diccionari amb: text (contingut concatenat), tallada (booleà: True si stop_reason == "max_tokens"), tokens_entrada, tokens_sortida i model. Prova-la amb una crida real i amb max_tokens=20 per forçar el tall.
Exercici 2
Modifica preguntar_docubot_brut() perquè accepti un paràmetre opcional temperature (per defecte 0.2) i fes 3 crides amb la mateixa pregunta a temperature 0.0 i 3 més a 0.9. Compara les sortides: es compleix el que vam estudiar a 02-04 sobre la necessitat de diverses execucions per cas en comparar?
Exercici 3
El classificador de suport de 02-02 (prompt_classificacio() amb EXEMPLES_CLASSIFICACIO) encara no ha tocat l'API. Escriu classificar_consulta(text: str) -> str que l'enviï amb max_tokens=20 i temperature 0.0, i retorni una de les 6 categories (facturacio, permisos, integracions, api, incidencies, altres). Si la sortida del model no coincideix exactament amb cap categoria, retorna "altres" (default conservador, com a 02-03).
Solucions
Solució 1:
def diagnostic_crida(resposta) -> dict:
return {
"text": "".join(
b.text for b in resposta.content if b.type == "text"
),
"tallada": resposta.stop_reason == "max_tokens",
"tokens_entrada": resposta.usage.input_tokens,
"tokens_sortida": resposta.usage.output_tokens,
"model": resposta.model,
}Amb max_tokens=20 veuràs tallada: True i un text interromput a mitja frase: exactament el cas que preguntar_docubot() intercepta abans de parsejar.
Solució 2:
def preguntar_docubot_brut(pregunta, documentacio, temperature=0.2):
return client.messages.create(
model=MODEL,
max_tokens=1024,
temperature=temperature,
system=SYSTEM_DOCUBOT,
messages=[{"role": "user", "content": prompt_resposta(pregunta, documentacio)}],
)
for t in (0.0, 0.9):
print(f"--- temperature={t} ---")
for i in range(3):
r = preguntar_docubot_brut(pregunta, documentacio, temperature=t)
print(f"[{i+1}]", extreure_text(r)[:120])A 0.0 les tres sortides seran gairebé idèntiques (gairebé: no determinisme de 01-04); a 0.9 variaran visiblement. Conclusió pràctica: comparar prompts amb una sola execució per cas és soroll, no mesura — per això 02-04 va fixar 3 execucions per cas.
Solució 3:
from prompts import prompt_classificacio
CATEGORIES = {"facturacio", "permisos", "integracions", "api", "incidencies", "altres"}
def classificar_consulta(text: str) -> str:
resposta = client.messages.create(
model=MODEL,
max_tokens=20,
temperature=0.0,
messages=[{"role": "user", "content": prompt_classificacio(text)}],
)
sortida = extreure_text(resposta).strip().lower()
return sortida if sortida in CATEGORIES else "altres"Dues decisions heretades del mòdul 2: temperature 0.0 perquè classificar exigeix consistència, i default "altres" perquè davant del dubte és millor una categoria genèrica que una d'equivocada amb aparença de segura.
Conclusió
DocuBot ja no és paper: tens un entorn reproduïble amb l'SDK instal·lat i la clau fora del codi, una primera crida real que envia el SYSTEM_DOCUBOT de 02-01 i les plantilles de prompts.py, i el criteri per llegir el que torna — el contingut com a llista de blocs, stop_reason com a sentinella (especialment max_tokens abans de parsejar) i usage com a comptador de la factura. El parser defensiu de 02-03 processa ara respostes de debò, reforçat amb els modes de sortida estructurada natius quan el proveïdor els ofereixi, i la taula d'equivalències et garanteix que res d'això no et lliga a un proveïdor concret. Però la nostra integració és ingènua en un aspecte que l'usuari nota a l'instant i en un altre que notarà el pitjor dia de l'any: la resposta triga uns quants segons a aparèixer de cop, i si l'API retorna un error 429 o un 500, DocuBot simplement explota. A la propera lliçó resolem tots dos: streaming perquè les paraules flueixin a mesura que es generen, i una estratègia seriosa d'errors i reintents perquè DocuBot es degradi amb elegància en comptes de trencar-se.
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ó
