Tot el que hem construït en aquest mòdul consumeix dos recursos que no apareixen al codi: diners i temps. Cada token del SYSTEM_DOCUBOT, dels few-shot del classificador i de l'historial de 03-03 es factura a cada crida; i cada crida triga un temps que l'empleat de Nubelia percep. En aquesta lliçó posem números a tots dos: entendrem el model de preus per tokens, escriurem una funció d'estimació de cost i la projecció mensual de DocuBot (refinant aquella estimació de ~400 €/mes que vam fer a ull a 01-04), repassarem les palanques per abaixar-la, mesurarem la latència amb les mètriques correctes i acabarem amb la palanca que ataca cost i latència alhora: el caching, tant el de prompts del proveïdor com un de respostes fet per nosaltres.

Contingut

  1. El model de preus: tokens d'entrada i de sortida
  2. Estimar el cost per crida en Python
  3. La projecció mensual de DocuBot (refinant la xifra de 01-04)
  4. Palanques de cost
  5. Latència: TTFT, temps total i com mesurar-los
  6. Prompt caching del proveïdor: el prefix estable
  7. Caching de respostes propi: hash, TTL i validesa
  8. Pressupostos i alertes: disseny

El model de preus: tokens d'entrada i de sortida

Les APIs de LLM facturen per tokens processats, amb dues tarifes diferents expressades normalment en preu per milió de tokens:

  • Tokens d'entrada (input): tot el que envies — system prompt, few-shot, documentació, historial, pregunta.
  • Tokens de sortida (output): tot el que el model genera. Sol costar entre 3 i 5 vegades més que l'entrada, perquè generar és computacionalment més car que llegir.

Les xifres concretes canvien amb freqüència i depenen del model i del proveïdor, així que en aquest curs treballarem sempre amb variables il·lustratives — substitueix-les per les tarifes vigents del teu proveïdor el dia que calculis seriosament:

# preus.py — valors IL·LUSTRATIUS, no tarifes reals.
# Consulta la pàgina de preus del teu proveïdor i actualitza aquí.

PREU_ENTRADA = 3.0        # € per milió de tokens d'entrada (model gamma mitjana)
PREU_SORTIDA = 15.0       # € per milió de tokens de sortida

PREU_ENTRADA_MINI = 0.5   # € per milió — model petit (per al classificador)
PREU_SORTIDA_MINI = 2.5

# Factors típics del prompt caching del proveïdor (vegeu secció 6):
FACTOR_CACHE_LECTURA = 0.1      # llegir del cache ≈ 10% del preu d'entrada
FACTOR_CACHE_ESCRIPTURA = 1.25  # escriure al cache ≈ 25% de recàrrec

Dues conseqüències estructurals d'aquest model que ja hem anat intuint:

  • L'entrada domina en sistemes tipus DocuBot. Les nostres crides envien milers de tokens (system + few-shot + documentació) per generar-ne uns centenars. Optimitzar l'entrada rendeix més que escurçar respostes.
  • Tot allò repetit es re-paga. El SYSTEM_DOCUBOT viatja íntegre en cadascuna de les milers de crides del mes. Aquesta repetició és exactament el que el prompt caching ataca.

Estimar el cost per crida en Python

L'objecte usage de 03-01 ens dona les dades exactes; només falta posar-los preu:

# costos.py
from preus import PREU_ENTRADA, PREU_SORTIDA

def estimar_cost(usage, preu_entrada: float = PREU_ENTRADA,
                 preu_sortida: float = PREU_SORTIDA) -> float:
    """Cost en euros d'una crida a partir del seu objecte usage."""
    cost_entrada = usage.input_tokens / 1_000_000 * preu_entrada
    cost_sortida = usage.output_tokens / 1_000_000 * preu_sortida
    return cost_entrada + cost_sortida

# Ús després de qualsevol crida:
# resposta = client.messages.create(...)
# print(f"Aquesta crida: {estimar_cost(resposta.usage):.4f} €")

Per estimar abans de cridar (per exemple, per pressupostar un lot), els proveïdors ofereixen un endpoint de recompte de tokens que tokenitza sense generar (a l'SDK d'Anthropic, client.messages.count_tokens(...), que retorna input_tokens); si no en tens, la regla aproximada de 01-02 (1 token ≈ 0,75 paraules en català/anglès) serveix per a ordres de magnitud.

Connecta estimar_cost a la comptabilitat de la classe Conversa de 03-03 (que ja acumula tokens_entrada_total i tokens_sortida_total) i tindràs el cost per conversa sense esforç extra.

La projecció mensual de DocuBot

A 01-04 vam estimar "~400 €/mes" gairebé a ull, com a ordre de magnitud per decidir si el projecte tenia sentit. Ara tenim dades reals d'usage i una arquitectura concreta (classificador + resposta, l'encadenat de 02-02). Refinem-ho amb números ficticis però coherents amb el que hem mesurat al mòdul:

Perfil d'una consulta a DocuBot (tokens, mesurats/estimats):

Component Entrada Sortida
Crida de classificació (few-shot d'EXEMPLES_CLASSIFICACIO) ~900 ~10
Crida de resposta: SYSTEM_DOCUBOT (~600) + documentació injectada (~2.000) + pregunta (~100) + plantilla JSON (~150) ~2.850 ~300

Volum fictici: 250 empleats, ~1.200 consultes per dia laborable, 22 dies/mes → 26.400 consultes/mes.

# projeccio.py
from preus import (PREU_ENTRADA, PREU_SORTIDA,
                   PREU_ENTRADA_MINI, PREU_SORTIDA_MINI)

def cost_crida(t_entrada: int, t_sortida: int,
               p_entrada: float, p_sortida: float) -> float:
    return t_entrada / 1e6 * p_entrada + t_sortida / 1e6 * p_sortida

def projeccio_mensual(consultes_mes: int = 26_400,
                      classificador_en_mini: bool = False) -> float:
    """Projecció mensual de DocuBot en euros (xifres il·lustratives)."""
    p_ent_cls = PREU_ENTRADA_MINI if classificador_en_mini else PREU_ENTRADA
    p_sor_cls = PREU_SORTIDA_MINI if classificador_en_mini else PREU_SORTIDA

    classificacio = cost_crida(900, 10, p_ent_cls, p_sor_cls)
    resposta = cost_crida(2_850, 300, PREU_ENTRADA, PREU_SORTIDA)

    return consultes_mes * (classificacio + resposta)

print(f"Base (tot en gamma mitjana): {projeccio_mensual():.0f} €/mes")
print(f"Classificador en model petit: {projeccio_mensual(classificador_en_mini=True):.0f} €/mes")

Amb les tarifes il·lustratives: la crida de classificació costa ~0,0029 € i la de resposta ~0,0130 €, és a dir ~0,016 € per consulta i ~417 €/mes. L'estimació de tovalló de 01-04 (~400 €) era, per variar, raonable — però ara sabem d'on surt cada cèntim, i això és el que converteix una xifra en una palanca: el 82% del cost és la crida de resposta, i dins d'ella el 78% és entrada. Ja sabem on prémer.

Palanques de cost

Ordenades per relació esforç/impacte per a DocuBot, totes amb el que hem après al mòdul:

  1. Model més petit on sobra capacitat. El classificador tria entre 6 etiquetes amb few-shot; no necessita el model de respostes. Moure'l al model petit (classificador_en_mini=True) abaixa el seu cost de ~0,0029 € a ~0,00047 € per consulta: ~62 €/mes estalviats canviant un paràmetre. La regla general: cada tasca del pipeline al model més barat que superi la teva avaluació de 02-04 — la rúbrica decideix, no la intuïció.
  2. Prompt caching del proveïdor (secció 6): el prefix estable (SYSTEM_DOCUBOT + few-shot) passa a facturar-se a ~10% a les crides amb encert de cache.
  3. max_tokens ajustat i format concís. El system prompt ja demana <150 paraules (02-01); max_tokens=1024 és xarxa de seguretat, no invitació. La sortida es paga a preu car: cada paraula de farciment evitada és estalvi directe.
  4. Retallar l'entrada repetida: els 6 exemples few-shot són necessaris o l'avaluació de 02-04 dona igual amb 4? La documentació injectada pot ser més selectiva? (Aquesta última pregunta és literalment el mòdul 4: RAG és, entre altres coses, una palanca de cost — injectar 2 fragments rellevants en comptes de 10 de genèrics.)
  5. Gestió de l'historial (03-03). En converses, truncament o finestra lliscant converteixen el cost quadràtic acumulat en lineal. El resum amb LLM costa una crida, però s'amortitza si evita reenviar milers de tokens per torn.
  6. Caching de respostes propi (secció 7): la consulta repetida no crida l'API en absolut — cost zero i latència zero.

Latència: TTFT, temps total i com mesurar-los

La segona moneda. Dues mètriques diferents que ja vam fregar a 03-02, ara amb nom propi:

  • TTFT (time to first token): temps des que envies la petició fins que arriba el primer token. És el que l'usuari percep com a "reacció" del bot. Amb streaming, és LA mètrica d'experiència.
  • Temps total: fins a l'últim token. Governa el throughput de processos per lots i els timeouts.

Què influeix en cadascuna:

Factor Afecta el TTFT Afecta el total És a les teves mans
Mida de l'entrada (prompt + historial) ✅ (cal processar-la sencera abans del primer token) Sí: palanques 4 i 5
Longitud de la sortida ✅ (token a token) Sí: max_tokens, format concís
Model triat (mida) Sí: palanca 1
Càrrega del proveïdor / hora del dia No: només mesurar (i reintents de 03-02)
Prompt caching del proveïdor ✅ (el prefix en cache no es reprocessa) Sí: secció 6

Mesura amb l'streaming de 03-02 — l'exercici 1 d'aquella lliçó era exactament això; ho consolidem com a utilitat:

# latencia.py
import time

def mesurar_crida(fn_stream, *args, **kwargs) -> dict:
    """Executa una funció d'streaming i retorna TTFT i temps total."""
    inici = time.perf_counter()
    ttft = None
    fragments = []

    for text in fn_stream(*args, **kwargs):     # fn_stream: generador de fragments
        if ttft is None:
            ttft = time.perf_counter() - inici
        fragments.append(text)

    return {
        "ttft_s": round(ttft, 2),
        "total_s": round(time.perf_counter() - inici, 2),
        "text": "".join(fragments),
    }

Dos consells de mesura: (1) mesura percentils, no mitjanes — la latència de les APIs té cua llarga, i el p95 (el pitjor cas que veu 1 de cada 20 usuaris) explica la veritat que la mitjana amaga; (2) mesura sobre el conjunt d'avaluació de 02-04, que ja representa la distribució real de preguntes — així cost, qualitat i latència s'avaluen sobre els mateixos casos.

Prompt caching del proveïdor: el prefix estable

Els proveïdors ofereixen prompt caching: si el començament de la teva petició (el prefix) és byte a byte idèntic al d'una petició recent, el proveïdor reutilitza el processament ja fet i et cobra aquella part a una fracció del preu (lectura de cache ≈ 10% del preu d'entrada, amb un recàrrec la primera vegada que s'escriu ≈ +25%; el cache expira al cap de pocs minuts sense ús).

La paraula clau és prefix: el cache encaixa des del principi de la petició i qualsevol byte diferent invalida tot el que vingui després. D'aquí la regla d'or de disseny: allò estable primer, allò variable al final.

Mira la nostra crida de resposta amb aquests ulls:

[system: SYSTEM_DOCUBOT]                 ← idèntic a TOTES les crides ✅
[few-shot / plantilla fixa]              ← idèntic a totes ✅
[documentació injectada]                 ← varia per pregunta ❌
[pregunta de l'usuari]                   ← varia sempre ❌

SYSTEM_DOCUBOT i els EXEMPLES_CLASSIFICACIO són el candidat perfecte: milers de tokens idèntics repetits en cadascuna de les 26.400 crides mensuals. A l'SDK d'Anthropic es marca el final del prefix cachejable amb cache_control; altres proveïdors l'apliquen automàticament sobre prefixos repetits — el concepte és el mateix:

resposta = client.messages.create(
    model=MODEL,
    max_tokens=1024,
    system=[{
        "type": "text",
        "text": SYSTEM_DOCUBOT,
        "cache_control": {"type": "ephemeral"},   # "cachejar fins aquí"
    }],
    messages=[{"role": "user", "content": prompt_resposta(pregunta, documentacio)}],
)

# Verificació: està funcionant?
print(resposta.usage.cache_creation_input_tokens)  # tokens escrits al cache
print(resposta.usage.cache_read_input_tokens)      # tokens LLEGITS del cache (l'estalvi)

Si cache_read_input_tokens és 0 crida rere crida, alguna cosa trenca el prefix. Els sabotejadors silenciosos habituals:

  • Un timestamp o dada variable interpolada al system prompt ("Avui és {data}..."): cada crida té un prefix diferent. Allò variable, al final del missatge d'usuari.
  • Prefix massa curt: hi ha un mínim cachejable (de l'ordre de mil tokens segons el model); prefixos menors no es cachegen, sense error.
  • Versions barrejades: si mitja flota envia PROMPT_VERSION 1.2 i l'altra meitat la 1.1, cada variant competeix pel seu propi cache. Un altre motiu per al versionament disciplinat de 02-04.

Quant val a DocuBot: els ~600 tokens del system (més few-shot si comparteixen prefix) al 10% en comptes del 100%, sobre 26.400 crides/mes, són de l'ordre de 40–50 €/mes amb les nostres tarifes il·lustratives — i una millora de TTFT gratis, perquè el prefix en cache no es reprocessa. Al mòdul 4, quan la documentació injectada creixi, l'ordre "estable primer" serà encara més rendible.

Caching de respostes propi: hash, TTL i validesa

El prompt caching abarateix la crida; un cache de respostes l'elimina. Si vint empleats pregunten "com exporto a CSV?" la mateixa setmana, per què generar vint vegades la mateixa resposta?

La implementació clàssica: clau = hash de la pregunta normalitzada, valor = la resposta validada, amb un TTL (temps de vida) després del qual l'entrada caduca:

# cache_respostes.py
import hashlib
import time
import unicodedata

def normalitzar(text: str) -> str:
    """Redueix variants trivials a una mateixa clau de cache."""
    text = text.lower().strip()
    # treure accents: "què" i "que" han de donar la mateixa clau
    text = unicodedata.normalize("NFKD", text)
    text = "".join(c for c in text if not unicodedata.combining(c))
    return " ".join(text.split())      # espais repetits -> un

class CacheRespostes:
    def __init__(self, ttl_segons: int = 24 * 3600):
        self.ttl = ttl_segons
        self.dades: dict[str, tuple[float, dict]] = {}   # clau -> (timestamp, resposta)

    def _clau(self, pregunta: str) -> str:
        return hashlib.sha256(normalitzar(pregunta).encode("utf-8")).hexdigest()

    def obtenir(self, pregunta: str) -> "dict | None":
        entrada = self.dades.get(self._clau(pregunta))
        if entrada is None:
            return None
        timestamp, resposta = entrada
        if time.time() - timestamp > self.ttl:
            del self.dades[self._clau(pregunta)]         # caducada
            return None
        return resposta

    def desar(self, pregunta: str, resposta: dict):
        self.dades[self._clau(pregunta)] = (time.time(), resposta)

Integració davant del pipeline de 03-02:

cache = CacheRespostes(ttl_segons=24 * 3600)

def respondre_amb_cache(pregunta: str, documentacio: str) -> dict:
    if (desada := cache.obtenir(pregunta)) is not None:
        return desada                         # 0 €, ~0 ms
    resultat = respondre(pregunta, documentacio)     # pipeline complet de 03-02
    if resultat["confianca"] != "baixa":      # no fossilitzar respostes dubtoses
        cache.desar(pregunta, resultat)
    return resultat

Quan és vàlid cachejar — i quan no. Aquest cache només és correcte si es compleixen tres condicions:

  1. Mateixa pregunta ⇒ mateixa resposta desitjada. Cert per a preguntes de documentació ("com exporto?"); fals per a tot allò que depengui de l'usuari o del seu estat ("quin pla tinc jo?"). A DocuBot convé cachejar només consultes sense context de sessió — de fet, integrar el cache abans de l'historial de 03-03: si la pregunta arriba dins d'una conversa amb context, no aplica.
  2. La font no ha canviat. Si la documentació de Nubelia s'actualitza, les respostes en cache queden obsoletes. El TTL és la defensa passiva (24h acota el desfasament); l'activa és invalidar el cache en publicar documentació — un hook al procés editorial que faci cache.dades.clear() o invalidi per secció.
  3. La resposta mereixia ser conservada. D'aquí el filtre per confianca: cachejar un fallback o una resposta de baixa confiança multiplica una fallada puntual per tots els usuaris que heretin l'entrada.

La normalització té un límit: "com exporto a CSV?" i "de quina manera trec un CSV del projecte?" són la mateixa pregunta per a un humà i claus diferents per al hash. El cache exacte només captura repeticions literals (que en suport intern són més freqüents del que sembla: la gent copia i enganxa). La versió que agrupa per significat fa servir embeddings — la tècnica que obre el mòdul 4 i que ens regalarà, de passada, el cache semàntic.

Com amb les sessions de 03-03: el dict en memòria serveix per a un procés; amb diversos, el cache viu en un magatzem compartit (Redis és l'habitual) — el patró no canvia.

Pressupostos i alertes: disseny

Última peça, a nivell de disseny (la instrumentació fina — taulers, traces — és de 06-04). Un sistema que factura per ús necessita tres números acordats abans de producció:

  1. Pressupost mensual (p. ex. 500 €): la xifra que direcció ha aprovat. D'ell es deriva el diari (~23 €/dia laborable) — el senyal primerenc.
  2. Llindar d'alerta (p. ex. 80%): en creuar-lo, avís a l'equip. No talla el servei; dona temps a investigar (pic legítim d'adopció? bucle de reintents descontrolat? algú enganxant documents enormes?).
  3. Sostre dur (p. ex. 150%): en creuar-lo, el sistema degrada — per exemple, activa RESPOSTA_FALLBACK per a consultes noves o restringeix a cache — en comptes de gastar sense límit. Fa mal, però és la diferència entre un mal dia i una mala factura.

En el disseny de DocuBot això es concreta en poc codi i molta disciplina: un comptador de cost per crida (estimar_cost bolcat a un registre, al costat del CSV d'avaluació de 02-04 que ja coneixem), agregat per dia, i comparat contra els llindars. A més, els proveïdors ofereixen límits de despesa i alertes a la seva consola de facturació: activa'ls com a xarxa externa — el teu comptador pot tenir bugs; el seu és el que factura. I una mètrica de disseny més valuosa que el total: cost per consulta resolta. Si baixa mes a mes mentre la qualitat (rúbrica de 02-04) es manté, estàs optimitzant de debò i no simplement gastant menys per donar pitjors respostes.

Errors Comuns i Consells

  • Pressupostar només la sortida (o només "les respostes"). A DocuBot l'entrada és ~80% del cost: system, few-shot, documentació i historial pesen més que el que es genera.
  • Copiar tarifes d'un tutorial (inclòs aquest). Els preus canvien; les variables PREU_* existeixen per actualitzar-se. Fixa la data de la tarifa al costat del número a la teva documentació.
  • Posar la data/hora dins del system prompt i preguntar-se per què el prompt caching no encerta mai. Allò variable trenca el prefix: sempre al final, al missatge d'usuari.
  • Cachejar respostes sense TTL ni invalidació. El dia que canviï la documentació de Nubelia, DocuBot repetirà instruccions obsoletes amb total seguretat en si mateix — una al·lucinació fabricada per la teva pròpia infraestructura.
  • Cachejar també els fallbacks. Un error transitori de l'API queda fossilitzat i servit a tothom. Filtra què mereix entrar al cache (confiança, validesa del contracte).
  • Mesurar latència amb mitjanes. La mitjana amaga la cua: informa p50 i p95. Un p50 d'1,2s amb p95 de 9s és un problema real que la mitjana de 2s dissimula.
  • Consell: apunta les tres xifres de cada optimització — cost, latència (p95) i qualitat (rúbrica de 02-04) — abans i després. Una palanca que estalvia el 30% de cost però suspèn l'avaluació no és una palanca: és una retallada.
  • Consell: revisa usage.cache_read_input_tokens al teu registre la primera setmana després d'activar el prompt caching. És la manera més barata de descobrir un sabotejador de prefix silenciós.

Exercicis

Exercici 1

Amplia projeccio_mensual() amb dos paràmetres: taxa_encert_cache: float (fracció de consultes servides pel cache de respostes propi, que costen 0 €) i prompt_caching: bool (si és True, els ~600 tokens del system de la crida de resposta es facturen al 10%). Calcula l'escenari combinat: classificador en mini + 20% d'encert de cache + prompt caching. Quant queda dels ~417 €/mes base?

Exercici 2

La normalització de CacheRespostes decideix quines preguntes comparteixen clau. Per a cada parell, indica si comparteixen clau amb la implementació actual i si haurien de compartir-la: (a) "Com exporto a CSV?" / "com exporto a csv"; (b) "Com exporto a CSV?" / "Com exporto a PDF?"; (c) "Com exporto a CSV?" / "De quina manera es pot treure un CSV?"; (d) "Quin pla tinc?" / "quin pla tinc?". Justifica (d) amb especial cura.

Exercici 3

Dissenya la funció registrar_crida(usage, tipus_crida: str) que alimenti el control de pressupost: ha d'acumular el cost del dia (fes servir estimar_cost) en un fitxer o estructura simple, i retornar un de tres senyals: "ok", "alerta" (superat el 80% del pressupost diari) o "sostre" (superat el 150%). Indica en quin punt del pipeline de 03-02 la integraries i què hauria de fer respondre() amb cada senyal.

Solucions

Solució 1:

def projeccio_mensual(consultes_mes=26_400, classificador_en_mini=False,
                      taxa_encert_cache=0.0, prompt_caching=False):
    p_ent_cls = PREU_ENTRADA_MINI if classificador_en_mini else PREU_ENTRADA
    p_sor_cls = PREU_SORTIDA_MINI if classificador_en_mini else PREU_SORTIDA

    classificacio = cost_crida(900, 10, p_ent_cls, p_sor_cls)

    entrada_resposta = 2_850
    if prompt_caching:
        # ~600 tokens de system al 10% del preu d'entrada
        entrada_resposta = 2_850 - 600
        extra_system = 600 * FACTOR_CACHE_LECTURA / 1e6 * PREU_ENTRADA * 1e6 / 1e6
        # més llegible: cost del system en cache calculat a part
        cost_system = 600 / 1e6 * PREU_ENTRADA * FACTOR_CACHE_LECTURA
    else:
        cost_system = 0.0
        entrada_resposta = 2_850

    resposta = cost_crida(entrada_resposta, 300, PREU_ENTRADA, PREU_SORTIDA) + cost_system

    consultes_api = consultes_mes * (1 - taxa_encert_cache)   # la resta: cache propi, 0 €
    return consultes_api * (classificacio + resposta)

print(f"{projeccio_mensual(classificador_en_mini=True, taxa_encert_cache=0.2, prompt_caching=True):.0f} €/mes")

Resultat amb les tarifes il·lustratives: ~270 €/mes — un ~35% menys que la base de ~417 €, sense tocar la qualitat (el classificador canvia de model només si supera l'avaluació de 02-04, el cache només serveix respostes ja validades i el prompt caching és neutre en qualitat). Nota didàctica: el càlcul omet el recàrrec d'escriptura de cache (~25% en errors de cache) per simplicitat; inclou-lo si busques la xifra fina.

Solució 2:

  • (a) Comparteixen clau (minúscules + accents + espais ho igualen; els signes d'interrogació convé eliminar-los també a normalitzar — millora raonable a la implementació). Haurien de fer-ho: ✅ coherent.
  • (b) No comparteixen (difereixen en "CSV"/"PDF") i no haurien de fer-ho: són preguntes diferents amb respostes diferents. La normalització mai no ha d'esborrar contingut semàntic.
  • (c) No comparteixen, però un humà diria que haurien de fer-ho. És el límit del cache exacte: capturar aquesta equivalència exigeix comparar significats, no bytes — cache semàntic amb embeddings, mòdul 4.
  • (d) Comparteixen clau... i no haurien de ser al cache en absolut. La resposta depèn de qui pregunta: cachejar-la serviria el pla d'un empleat a un altre — fuga d'informació entre usuaris, la mateixa fallada de l'exercici 3 de 03-03 però fabricada pel cache. La defensa és abans de la normalització: excloure del cache tota consulta dependent de sessió/usuari (per exemple, cachejant només la ruta sense historial, o filtrant per categoria del classificador).

Solució 3:

import datetime, json, pathlib

PRESSUPOST_DIARI = 23.0   # € (≈ 500 €/mes entre 22 dies laborables)
FITXER = pathlib.Path("despesa_diaria.json")

def registrar_crida(usage, tipus_crida: str) -> str:
    avui = datetime.date.today().isoformat()
    dades = json.loads(FITXER.read_text()) if FITXER.exists() else {}
    dades.setdefault(avui, 0.0)
    dades[avui] += estimar_cost(usage)
    FITXER.write_text(json.dumps(dades))

    despesa = dades[avui]
    if despesa >= PRESSUPOST_DIARI * 1.5:
        return "sostre"
    if despesa >= PRESSUPOST_DIARI * 0.8:
        return "alerta"
    return "ok"

Integració: a respondre() (03-02), just després de cada crida reeixida — és on existeix usage. Amb "ok" no passa res; amb "alerta" es notifica l'equip (log de nivell warning avui; canal d'avisos quan existeixi l'observabilitat de 06-04) però se segueix servint; amb "sostre" les consultes noves passen a mode degradat (cache propi + RESPOSTA_FALLBACK per al que no és en cache) fins a revisió humana. Fixa't en la coherència amb tot el curs: la decisió de tallar o continuar gastant és humana; el sistema només la prepara i l'acota.

Conclusió

Amb aquesta lliçó, el mòdul d'integració queda complet — i DocuBot deixa de ser només un sistema que funciona per ser un sistema que sabem operar: coneix el preu de cada token que envia i rep (estimar_cost, sobre l'usage de 03-01), té una projecció mensual amb nom i cognoms que confirma i refina la xifra de 01-04 (~417 €/mes base, ~270 € després d'aplicar les palanques), distingeix TTFT de temps total i els mesura en percentils sobre el conjunt d'avaluació de 02-04, explota el prompt caching del proveïdor gràcies a una decisió de disseny que ara es revela premonitòria (el SYSTEM_DOCUBOT i els few-shot de 02-01/02-02 són un prefix estable perfecte), evita crides senceres amb el cache de respostes propi (hash normalitzat, TTL, invalidació en publicar documentació) i opera dins d'un pressupost amb alerta i sostre l'última paraula del qual és humana. Mirant enrere, el mòdul 3 va convertir els artefactes de paper del mòdul 2 en un sistema viu: la primera crida real amb rols system/user (03-01), streaming i una estratègia seriosa d'errors, reintents i degradació (03-02), converses amb memòria fabricada i sessions aïllades (03-03) i, avui, l'economia i la física del conjunt. Però el taló d'Aquil·les continua intacte: DocuBot respon bé quan li donem la documentació correcta a la mà — i fins ara l'hi hem donat nosaltres, a mà, a cada exemple. Qui troba, entre centenars de pàgines de documentació de Nubelia, els dos fragments que responen cada pregunta? Aquest és exactament el problema que resol la generació augmentada per recuperació. Al mòdul 4 construïm aquesta peça: començant pels embeddings i la cerca semàntica (04-01) — la tècnica que compara significats en comptes de paraules i que, de regal, resoldrà la limitació del nostre cache exacte —, seguint per les bases de dades vectorials, la ingesta i el chunking de la documentació, i culminant amb el pipeline RAG complet que convertirà DocuBot en el que vam prometre a 01-01: un assistent que de debò coneix Nubelia.

© Copyright 2026. Tots els drets reservats