A 06-01 vam construir un motor d'encadenament de deu línies i vam deixar dos problemes oberts: dues regles que xoquen (la garantia davant de "usat i més de 14 dies") i un raspall d'higiene obert que el motor reemborsava i denegava alhora. Resoldre'ls exigeix més que lògica: cal decidir quina regla guanya, recordar què se sap del cas, explicar cada conclusió, preguntar a l'usuari el que falta i, abans de tot això, treure les regles del cap del Diego i comprovar-les. Aquest conjunt de peces és un sistema expert, la tecnologia que als anys 80 va portar la IA a la indústria (01-01) i que avui sobreviu, rebatejada, en els motors de regles de negoci de bancs, asseguradores i botigues en línia. En aquesta lliçó veurem què és un sistema expert i d'on ve, la seva arquitectura peça a peça (base de coneixement, motor d'inferència amb resolució de conflictes, memòria de treball, explicació, adquisició del coneixement i interfície), com se'n construeix un (enginyeria del coneixement amb l'expert, que a NovaMarket és el Diego), els seus avantatges i límits, i com es compara amb un model d'ML. I construirem en Python pur un sistema expert de devolucions i garanties (cas 8) amb prioritats, traça d'explicació i una interfície que pregunta el que necessita. És important perquè és el patró que faràs servir cada vegada que una decisió hagi de ser explicable, auditable i modificable sense reentrenar.

Contingut

  1. Què és un sistema expert i d'on ve
  2. Arquitectura d'un sistema expert
  3. El motor d'inferència: encadenament i resolució de conflictes
  4. Explicació: per què? i com?
  5. Construir un sistema expert: enginyeria del coneixement
  6. Avantatges i desavantatges; comparació amb un model d'ML
  7. Codi: sistema expert de devolucions i garanties de NovaMarket
  8. Eines reals
  9. Errors Comuns i Consells
  10. Exercicis
  11. Conclusió

  1. Què és un sistema expert i d'on ve

Un sistema expert és un programa que resol problemes d'un domini concret al nivell d'un expert humà, fent servir coneixement explícit (fets i regles escrits per persones) i un mecanisme de raonament separat d'aquest coneixement. La separació és la idea clau: el motor no sap res de devolucions; les regles no saben res de com s'executen. Canviar la política és canviar regles, no codi.

Les tres fites històriques que ja vam citar a 01-01, amb el que cadascuna va aportar:

  • DENDRAL (Stanford, 1965-): identificava estructures moleculars a partir d'espectres de masses. Va demostrar que el coneixement específic del domini, i no un raonador general, era el que donava resultats: "en el coneixement hi ha el poder" (Feigenbaum).
  • MYCIN (Stanford, anys 70): diagnòstic d'infeccions bacterianes i recomanació d'antibiòtics amb unes 600 regles SI-ALESHORES, encadenament cap enrere que preguntava al metge, un mòdul que explicava "per què" preguntava i "com" havia arribat a la conclusió, i factors de certesa per a la incertesa (06-03). El seu motor buit, EMYCIN, va ser la primera shell reutilitzable.
  • XCON / R1 (Digital Equipment Corporation, 1980): configurava comandes d'ordinadors VAX amb milers de regles d'encadenament cap endavant; va estalviar desenes de milions de dòlars l'any i va disparar l'auge comercial. Els seus problemes de manteniment (més de 10.000 regles cap al 1988, amb equips sencers dedicats a mantenir-les) van anticipar l'hivern.

El que NovaMarket vol per a les devolucions és més a prop de XCON (regles de negoci aplicades a cada comanda) que de MYCIN, però prendrà de MYCIN l'explicació i les preguntes a l'usuari.

  1. Arquitectura d'un sistema expert

flowchart LR
    subgraph SE[Sistema expert de devolucions]
        BC[(Base de coneixement<br/>fets generals + regles R1..R8)]
        MT[(Memòria de treball<br/>fets del cas: comanda 47102)]
        MI[Motor d'inferència<br/>encadenament + resolució de conflictes]
        EX[Mòdul d'explicació<br/>per què? / com?]
        AQ[Mòdul d'adquisició<br/>del coneixement]
    end
    UI[Interfície d'usuari<br/>agent d'atenció al client]
    D[Diego, expert<br/>en devolucions]
    IC[Enginyer/a del coneixement<br/>Marta]
    UI <--> MI
    UI <--> EX
    MI <--> BC
    MI <--> MT
    EX --> MT
    EX --> BC
    D --> IC --> AQ --> BC

Les peces, una a una:

Component Què conté / fa A NovaMarket
Base de coneixement Coneixement permanent del domini: regles SI-ALESHORES i fets generals. No canvia d'un cas a l'altre Les regles de devolucions i garanties: termini 14 dies, garantia 3 anys, excepcions d'higiene/precintats, enviament gratuït si és defectuós
Memòria de treball Fets del cas actual: les dades d'entrada, les respostes de l'usuari i les conclusions intermèdies. Es buida a cada consulta Comanda 47102: 40 dies, usat, defectuós; després decisio = garantia
Motor d'inferència Aplica les regles a la memòria de treball (encadenament cap endavant o enrere, 06-01) i decideix quina regla disparar quan diverses són aplicables (resolució de conflictes) La nostra classe MotorInferencia
Mòdul d'explicació Respon "per què em preguntes això?" (quina regla intenta aplicar) i "com has arribat a aquesta conclusió?" (cadena de regles i fets) La traça "s'ha aplicat R3 perquè..."
Mòdul d'adquisició del coneixement Eines per introduir, editar i validar regles sense programar (editors, comprovació de consistència, proves amb casos) Un full de càlcul o formulari on el Diego edita regles i veu quins casos de prova canvien
Interfície d'usuari Recull les dades, formula preguntes, mostra la decisió i l'explicació La pantalla de l'agent d'atenció al client o l'assistent del cas 7

Fixa't en la línia que separa la base de coneixement de la memòria de treball: és la mateixa que a 06-01 separava REGLES (general) de fets_de(comanda) (particular). I en el fet que l'expert (el Diego) no toca directament la base: ho fa a través de l'enginyera del coneixement (la Marta) i del mòdul d'adquisició.

  1. El motor d'inferència: encadenament i resolució de conflictes

El motor repeteix un cicle de reconeixement-acció:

  1. Aparellar: trobar totes les regles les condicions de les quals es compleixen amb la memòria de treball actual. El resultat és el conjunt de conflicte.
  2. Resoldre el conflicte: triar una regla del conjunt (si n'hi ha diverses).
  3. Actuar: disparar la regla triada: afegir les seves conclusions a la memòria de treball (o preguntar, o executar una acció).
  4. Tornar a 1 fins que no hi hagi regles aplicables o s'assoleixi l'objectiu.

A 06-01 el pas 2 no existia: les disparàvem totes en ordre d'escriptura, i d'aquí venien les incoherències. Les estratègies clàssiques de resolució de conflictes, que se solen combinar en aquest ordre:

Estratègia Criteri Exemple en devolucions
Refractarietat Una regla no es dispara dues vegades sobre els mateixos fets Evita bucles infinits
Prioritat (salience) Guanya la regla amb més prioritat, assignada per l'expert La garantia (R1, prioritat 10) guanya la regla del Diego (R8, prioritat 2): un producte defectuós s'atén encara que hagin passat 14 dies
Especificitat Guanya la regla amb més condicions (més específica): les excepcions són més específiques que la regla general "Dins del termini, sense usar, amb etiqueta i amb 5+ devolucions prèvies → revisió humana" (R5) guanya "dins del termini, sense usar, amb etiqueta → reemborsament" (R4)
Recència Guanya la regla que fa servir els fets més recents: segueix el fil del raonament Després que l'usuari respongui una pregunta, es prefereixen les regles que la fan servir
Ordre La primera escrita Últim recurs; fràgil

A més hi ha una decisió de disseny sobre quan aturar-se: derivar tot el que és derivable (útil per calcular totes les conseqüències d'una comanda) o aturar-se en fixar l'objectiu (decisio), que és el que farem: un cop la regla guanyadora ha fixat la decisió, les de menys prioritat ja no la poden contradir. És la manera més simple que "guanyi" la regla correcta sense negacions a les premisses.

  1. Explicació: per què? i com?

És la funcionalitat que distingeix un sistema expert d'un simple if gegant i la raó per la qual aquest mòdul existeix després de 05-05. MYCIN responia a dues preguntes, i nosaltres implementarem totes dues:

  • Per què? (durant la consulta): "per què em preguntes si el producte mostra senyals d'ús?" → "perquè intento aplicar R5, que necessita saber si està usat". Dona context a l'usuari i li permet detectar preguntes absurdes (símptoma de regles mal escrites).
  • Com? (al final): "com has decidit que procedeix garantia?" → "s'ha aplicat R1 perquè defectuós = True, producte nou = True i 40 dies ≤ 1095; R1 diu: garantia legal de 3 anys; l'enviament el paga NovaMarket". És la justificació que exigeix el client, l'auditor i, a 02-04, el RGPD per a decisions automatitzades.

Tècnicament, l'explicació s'obté gairebé gratis si el motor registra per a cada fet de la memòria de treball qui l'hi va posar (dada d'entrada, resposta de l'usuari o nom de regla) i desa una traça de les regles disparades amb els valors concrets que en satisfeien les condicions. Amb això, "per què?" és seguir la cadena d'orígens cap enrere.

  1. Construir un sistema expert: enginyeria del coneixement

Un sistema expert no s'entrena: es construeix, i el procés s'anomena enginyeria del coneixement. Amb NovaMarket com a exemple:

  1. Triar el domini i l'abast. Estret, ben definit, amb un expert disponible i decisions repetitives: les devolucions i garanties (cas 8) ho compleixen; "atenció al client en general" no.
  2. Adquisició del coneixement: entrevistes a l'expert. La Marta s'asseu amb el Diego. Tècniques: preguntes obertes ("com decideixes si procedeix?"), casos concrets ("i aquesta comanda de la setmana passada?"), think aloud (el Diego resol casos en veu alta), revisió de documents (política de devolucions, normativa de consum, la qual, hi insistim, ha de revisar un professional legal). És la fase més lenta: els experts saben més del que saben dir (coll d'ampolla de l'adquisició), obliden les excepcions fins que veuen un cas, i de vegades es contradiuen entre sessions.
  3. Formalitzar: extreure regles. Convertir el que s'ha dit en condicions i conclusions precises, amb noms d'atributs definits en un diccionari ("usat" vol dir "amb senyals d'ús" o "desprecintat"? El Diego distingeix les dues coses i el sistema també ho ha de fer). Assignar prioritats i justificacions. Detectar regles incompletes ("i si està usat però és defectuós?").
  4. Validar amb casos. Un conjunt de casos de prova amb la decisió que el Diego considera correcta (20-50 casos reals de l'històric de devolucions), que s'executa cada vegada que canvia una regla: l'equivalent del conjunt de test de 04-05. Buscar regles que no es disparen mai, casos sense decisió, i decisions que el Diego no signaria.
  5. Mantenir. Les polítiques canvien (una campanya amb 30 dies de devolució per Nadal, una categoria nova), la normativa canvia i apareixen casos nous. Cada canvi passa pel mòdul d'adquisició i per la bateria de casos. XCON va morir d'això: milers de regles sense propietari clar. La recepta: regles poques, anomenades, amb justificació escrita i propietari (el Diego és el propietari de les regles de devolucions, com vam acordar a 02-03 per a les dades).

Una observació que connecta amb el mòdul 4: el pas 4 s'assembla a avaluar un model, i el pas 3 s'assembla al que fa un arbre de decisió, però a l'inrevés: allà l'algorisme troba les regles a comandes.csv; aquí les dicta l'expert. I hi ha un pont evident: l'export_text de l'arbre de 04-04 produeix regles llegibles que poden servir d'esborrany per a l'entrevista amb el Diego ("l'arbre diu que les devolucions amb més de 3 prèvies i menys de 5 dies són sospitoses; et quadra?").

  1. Avantatges i desavantatges; comparació amb un model d'ML

Avantatges Desavantatges
Explicable per construcció: cada decisió té la seva cadena de regles Coll d'ampolla de l'adquisició: extreure i formalitzar el coneixement és lent i car
Modificable sense reentrenar: canviar una regla és un canvi de política, no de codi Fràgil fora del domini: no sap res del que no és a les regles; no degrada amb elegància
No necessita dades històriques: funciona des del primer dia i amb casos rars Manteniment difícil en créixer: interaccions entre centenars de regles, prioritats embolicades
Consistent: la mateixa entrada, la mateixa sortida, sempre Incertesa mal suportada en la seva forma pura (06-03)
Auditable i conforme amb la normativa (AI Act, RGPD, 02-04) No aprèn de l'experiència llevat que algú reescrigui regles
Conserva i difon el coneixement de l'expert (si el Diego se'n va, les regles es queden) Codifica també els biaixos i errors de l'expert

Davant d'un model d'ML (l'arbre o la logística de 04-04):

Aspecte Sistema expert (regles escrites) Model d'ML (regles apreses)
Origen del coneixement Un expert i la normativa Dades històriques etiquetades
Necessita dades No (sí casos de prova) Sí, moltes i representatives
Explicabilitat Total De l'arbre sí; de la xarxa no
Canviar la política Editar una regla Recollir dades noves i reentrenar
Casos rars i excepcions S'escriuen una vegada Mal coberts si són rars a les dades
Patrons subtils en moltes dades No els veu El seu punt fort
Incertesa Afegida (06-03) Nativa: probabilitats
Exemple NovaMarket Devolucions i garanties (cas 8), polítiques fixades per normativa Risc de frau (cas 3), demanda (cas 2), ressenyes (cas 4)

Quan cadascun: regles quan la decisió està definida per normativa o política, s'ha de justificar una a una, hi ha poques dades o els casos límit importen; ML quan la relació entrada-sortida no la sap escriure ningú però és a les dades. I molt sovint tots dos: l'ML estima (probabilitat de frau, de defecte) i les regles decideixen amb aquesta estimació com un fet més. És la combinació neurosimbòlica que va anunciar 05-05 i que tancarà el mòdul a 06-04.

  1. Codi: sistema expert de devolucions i garanties de NovaMarket

Construïm les peces de la secció 2 en Python pur. Els fets de la memòria de treball són parells atribut-valor (dies_des_lliurament = 40, usat = True), i les condicions d'una regla són tuples (atribut, operador, valor), llegibles i per tant explicables. Dades de negoci: desistiment 14 dies; garantia legal 3 anys (1095 dies) per a producte nou; higiene i precintats no retornables si s'obren; etiqueta original; enviament de devolució gratuït si és defectuós.

7.1 Base de coneixement, regles i memòria de treball

import operator

OPERADORS = {"==": operator.eq, "!=": operator.ne, "<=": operator.le, "<": operator.lt,
             ">=": operator.ge, ">": operator.gt, "in": lambda a, b: a in b}

class Regla:
    """Regla SI condicions ALESHORES conclusions, amb prioritat i text de justificació."""
    def __init__(self, nom, condicions, conclusions, prioritat=0, justificacio=""):
        self.nom = nom
        self.condicions = condicions            # llista de (atribut, operador, valor)
        self.conclusions = conclusions          # dict atribut -> valor
        self.prioritat = prioritat
        self.justificacio = justificacio

    def avaluar(self, memoria):
        """True si totes les condicions es compleixen, False si alguna falla,
        None si falta algun fet per decidir."""
        resultat = True
        for atribut, op, valor in self.condicions:
            if atribut not in memoria.fets:
                resultat = None                 # encara no ho sabem: continuem per si una altra falla
                continue
            if not OPERADORS[op](memoria.fets[atribut], valor):
                return False
        return resultat

    def fets_que_falten(self, memoria):
        return [a for a, _, _ in self.condicions if a not in memoria.fets]

    def __repr__(self):
        cond = " ∧ ".join(f"{a} {op} {v!r}" for a, op, v in self.condicions)
        return f"{self.nom} [prio {self.prioritat}]: SI {cond} ALESHORES {self.conclusions}"


class BaseConeixement:
    def __init__(self):
        self.regles = []
    def afegir(self, regla):
        self.regles.append(regla)
    def regles_que_conclouen(self, atribut):
        return [r for r in self.regles if atribut in r.conclusions]


class MemoriaTreball:
    """Fets del cas actual + registre de qui els ha posat i quan."""
    def __init__(self, fets_inicials=None):
        self.fets = {}
        self.origen = {}         # atribut -> "entrada" | "usuari" | nom de regla
        self.rellotge = 0
        self.temps = {}          # atribut -> instant en què s'ha afirmat (per a la recència)
        for k, v in (fets_inicials or {}).items():
            self.afirmar(k, v, "entrada")
    def afirmar(self, atribut, valor, origen):
        self.rellotge += 1
        self.fets[atribut] = valor
        self.origen[atribut] = origen
        self.temps[atribut] = self.rellotge
    def recencia(self, regla):
        return max((self.temps.get(a, 0) for a, _, _ in regla.condicions), default=0)

Explicació: Regla.avaluar distingeix tres resultats, no dos: vertader, fals i "no se sap" (None) quan falta un fet; aquest tercer valor és el que permetrà a la interfície preguntar exactament el que falta. MemoriaTreball desa, a més del valor de cada fet, el seu origen (dada d'entrada, resposta de l'usuari o regla que l'ha deduït) i l'instant en què s'ha afirmat: l'origen alimenta l'explicació i l'instant, la recència.

7.2 Motor d'inferència amb resolució de conflictes i explicació

class MotorInferencia:
    def __init__(self, base, memoria):
        self.base = base
        self.memoria = memoria
        self.disparades = []     # noms de regles ja aplicades (refractarietat)
        self.traca = []          # explicació pas a pas

    # ---- encadenament cap endavant amb resolució de conflictes ----
    def conjunt_conflicte(self):
        return [r for r in self.base.regles
                if r.nom not in self.disparades and r.avaluar(self.memoria) is True]

    def triar(self, candidates):
        """Prioritat > especificitat (nre. de condicions) > recència dels fets usats."""
        return max(candidates, key=lambda r: (r.prioritat, len(r.condicions),
                                              self.memoria.recencia(r)))

    def disparar(self, regla):
        detall = ", ".join(f"{a}={self.memoria.fets[a]!r} ({op} {v!r})"
                           for a, op, v in regla.condicions)
        for atribut, valor in regla.conclusions.items():
            self.memoria.afirmar(atribut, valor, regla.nom)
        self.disparades.append(regla.nom)
        self.traca.append(f"S'ha aplicat {regla.nom} perquè {detall} "
                          f"→ {regla.conclusions}. Justificació: {regla.justificacio}")

    def executar(self, objectiu="decisio"):
        while objectiu not in self.memoria.fets:
            candidates = self.conjunt_conflicte()
            if not candidates:
                self.traca.append(f"Cap regla aplicable i '{objectiu}' sense fixar")
                return None
            if len(candidates) > 1:
                self.traca.append("Conflicte entre " + ", ".join(r.nom for r in candidates)
                                  + f"; guanya {self.triar(candidates).nom}")
            self.disparar(self.triar(candidates))
        return self.memoria.fets[objectiu]

    # ---- mòdul d'explicació ----
    def per_que(self, atribut):
        origen = self.memoria.origen.get(atribut)
        if origen is None:
            return f"'{atribut}' no és a la memòria de treball"
        if origen in ("entrada", "usuari"):
            return f"'{atribut}' = {self.memoria.fets[atribut]!r} és una dada d'{origen}"
        regla = next(r for r in self.base.regles if r.nom == origen)
        return (f"'{atribut}' = {self.memoria.fets[atribut]!r} l'ha fixat {regla.nom} "
                f"({regla.justificacio}); les seves condicions: "
                + "; ".join(self.per_que(a) for a, _, _ in regla.condicions))
    def com(self):
        return "\n".join(f"  {i+1}. {pas}" for i, pas in enumerate(self.traca))

    # ---- interfície de consulta dirigida per objectiu (cap enrere simplificat) ----
    def consultar(self, objectiu, preguntes, respondre):
        """Recorre les regles que conclouen l'objectiu per prioritat; si a una li
        falten fets preguntables, els demana; dispara la primera que es compleix."""
        for regla in sorted(self.base.regles_que_conclouen(objectiu),
                            key=lambda r: (-r.prioritat, -len(r.condicions))):
            for atribut in regla.fets_que_falten(self.memoria):
                if regla.avaluar(self.memoria) is False:
                    break                                   # ja sabem que no aplica: no preguntar més
                if atribut in preguntes:
                    valor = respondre(preguntes[atribut], regla)
                    self.memoria.afirmar(atribut, valor, "usuari")
            if regla.avaluar(self.memoria) is True:
                self.disparar(regla)
                return self.memoria.fets[objectiu]
        return None

Explicació:

  • conjunt_conflicte és el pas "aparellar" del cicle: regles no disparades encara (refractarietat) les condicions de les quals són totes certes. triar és la resolució de conflictes en una línia: max amb una clau composta (prioritat, nombre de condicions, recència), que aplica els criteris en aquest ordre.
  • disparar afirma les conclusions a la memòria (amb la regla com a origen) i escriu a la traça una frase completa amb els valors reals que satisfeien cada condició: la matèria primera del "com?".
  • executar repeteix el cicle fins a fixar l'objectiu (decisio) o quedar-se sense regles. Quan hi ha més d'una candidata, ho deixa anotat a la traça: veurem "Conflicte entre R3, R4; guanya R3".
  • per_que segueix la cadena d'orígens: si el fet l'ha fixat una regla, explica la regla i, recursivament, cadascuna de les seves condicions. com retorna la traça numerada.
  • consultar és l'encadenament cap enrere simplificat dirigit per objectiu: pren les regles que conclouen decisio ordenades com les ordenaria el motor (prioritat, especificitat), i per a cadascuna pregunta només els fets que li falten, aturant-se així que una condició ja coneguda falla (per no preguntar coses inútils). La primera regla que resulta certa es dispara. respondre(pregunta, regla) és una funció que se li passa: en producció llegirà del teclat o d'un formulari; a la lliçó la simularem per poder executar sense teclat, i li passem la regla perquè pugui contestar "per què?".

7.3 Les regles de devolucions i garanties

def base_devolucions():
    kb = BaseConeixement()
    kb.afegir(Regla("R1", [("defectuos", "==", True), ("producte_nou", "==", True),
                           ("dies_des_lliurament", "<=", 1095)],
                    {"decisio": "garantia", "enviament_devolucio": "gratuit"}, prioritat=10,
                    justificacio="garantia legal de 3 anys del producte nou; l'enviament el paga NovaMarket"))
    kb.afegir(Regla("R2", [("defectuos", "==", True), ("dies_des_lliurament", ">", 1095)],
                    {"decisio": "denegada", "motiu": "fora del termini de garantia legal"}, prioritat=9,
                    justificacio="passats 3 anys s'ofereix servei tècnic de pagament"))
    kb.afegir(Regla("R3", [("categoria", "in", ("higiene", "precintat")), ("obert", "==", True)],
                    {"decisio": "denegada", "motiu": "producte d'higiene o precintat obert"}, prioritat=8,
                    justificacio="excepció al desistiment: precintats i higiene no es retornen oberts"))
    kb.afegir(Regla("R4", [("dies_des_lliurament", "<=", 14), ("usat", "==", False),
                           ("etiqueta_original", "==", True)],
                    {"decisio": "reemborsament_desistiment", "enviament_devolucio": "a carrec del client"}, prioritat=5,
                    justificacio="dret de desistiment de 14 dies amb producte sense usar i etiquetat"))
    kb.afegir(Regla("R5", [("dies_des_lliurament", "<=", 14), ("usat", "==", False),
                           ("etiqueta_original", "==", True), ("devolucions_previes", ">=", 5)],
                    {"decisio": "revisio_humana", "motiu": "patró de devolucions freqüents"}, prioritat=5,
                    justificacio="més específica que R4: el cas 3 (frau) demana una mirada humana"))
    kb.afegir(Regla("R6", [("dies_des_lliurament", "<=", 14), ("usat", "==", True)],
                    {"decisio": "revisio_humana", "motiu": "producte usat dins del termini: valorar depreciació"},
                    prioritat=4, justificacio="una persona decideix si procedeix reemborsament total o parcial"))
    kb.afegir(Regla("R7", [("dies_des_lliurament", "<=", 14), ("etiqueta_original", "==", False)],
                    {"decisio": "revisio_humana", "motiu": "falta l'etiqueta original"}, prioritat=4,
                    justificacio="la política exigeix etiqueta; una persona valora l'excepció"))
    kb.afegir(Regla("R8", [("dies_des_lliurament", ">", 14), ("defectuos", "==", False)],
                    {"decisio": "denegada", "motiu": "fora del termini de desistiment i sense defecte"}, prioritat=2,
                    justificacio="la regla del Diego: usat o no, passats 14 dies sense defecte no procedeix"))
    return kb

Llegeix les prioritats com la jerarquia de la política: primer la garantia (R1, R2: un defecte s'atén amb independència del desistiment, i així es resol el xoc de 06-01), després les excepcions al desistiment (R3), després el desistiment ordinari (R4) i les seves variants que demanen revisió humana (R5-R7, en línia amb el human-in-the-loop de 02-04: la denegació o el reemborsament parcial d'un producte usat no s'automatitzen), i per últim la regla del Diego (R8). Nota que R5 i R4 tenen la mateixa prioritat: la decidirà l'especificitat.

7.4 Casos de prova

CASOS = {
    "A cafetera NovaBrew":    dict(dies_des_lliurament=9,  usat=False, etiqueta_original=True,  defectuos=False, producte_nou=True, categoria="general", obert=True,  devolucions_previes=0),
    "B aspirador NovaClean":  dict(dies_des_lliurament=40, usat=True,  etiqueta_original=False, defectuos=True,  producte_nou=True, categoria="general", obert=True,  devolucions_previes=1),
    "C raspall NovaSmile":    dict(dies_des_lliurament=5,  usat=False, etiqueta_original=True,  defectuos=False, producte_nou=True, categoria="higiene", obert=True,  devolucions_previes=0),
    "D auriculars NovaSound": dict(dies_des_lliurament=20, usat=True,  etiqueta_original=True,  defectuos=False, producte_nou=True, categoria="general", obert=True,  devolucions_previes=2),
    "E monitor NovaView":     dict(dies_des_lliurament=3,  usat=False, etiqueta_original=True,  defectuos=False, producte_nou=True, categoria="general", obert=False, devolucions_previes=7),
}
for nom, dades in CASOS.items():
    motor = MotorInferencia(base_devolucions(), MemoriaTreball(dades))
    decisio = motor.executar("decisio")
    print(f"{nom}: decisio = {decisio}, {motor.memoria.fets.get('enviament_devolucio') or motor.memoria.fets.get('motiu')}")
    print(motor.com())

motor = MotorInferencia(base_devolucions(), MemoriaTreball(CASOS["C raspall NovaSmile"]))
motor.executar("decisio")
print("Per què decisio?\n ", motor.per_que("decisio"))

Sortida:

A cafetera NovaBrew: decisio = reemborsament_desistiment, a carrec del client
  1. S'ha aplicat R4 perquè dies_des_lliurament=9 (<= 14), usat=False (== False), etiqueta_original=True (== True) → {'decisio': 'reemborsament_desistiment', 'enviament_devolucio': 'a carrec del client'}. Justificació: dret de desistiment de 14 dies amb producte sense usar i etiquetat
B aspirador NovaClean: decisio = garantia, gratuit
  1. S'ha aplicat R1 perquè defectuos=True (== True), producte_nou=True (== True), dies_des_lliurament=40 (<= 1095) → {'decisio': 'garantia', 'enviament_devolucio': 'gratuit'}. Justificació: garantia legal de 3 anys del producte nou; l'enviament el paga NovaMarket
C raspall NovaSmile: decisio = denegada, producte d'higiene o precintat obert
  1. Conflicte entre R3, R4; guanya R3
  2. S'ha aplicat R3 perquè categoria='higiene' (in ('higiene', 'precintat')), obert=True (== True) → {'decisio': 'denegada', 'motiu': "producte d'higiene o precintat obert"}. Justificació: excepció al desistiment: precintats i higiene no es retornen oberts
D auriculars NovaSound: decisio = denegada, fora del termini de desistiment i sense defecte
  1. S'ha aplicat R8 perquè dies_des_lliurament=20 (> 14), defectuos=False (== False) → {'decisio': 'denegada', 'motiu': 'fora del termini de desistiment i sense defecte'}. Justificació: la regla del Diego: usat o no, passats 14 dies sense defecte no procedeix
E monitor NovaView: decisio = revisio_humana, patró de devolucions freqüents
  1. Conflicte entre R4, R5; guanya R5
  2. S'ha aplicat R5 perquè dies_des_lliurament=3 (<= 14), usat=False (== False), etiqueta_original=True (== True), devolucions_previes=7 (>= 5) → {'decisio': 'revisio_humana', 'motiu': 'patró de devolucions freqüents'}. Justificació: més específica que R4: el cas 3 (frau) demana una mirada humana

Per què decisio?
  'decisio' = 'denegada' l'ha fixat R3 (excepció al desistiment: precintats i higiene no es retornen oberts); les seves condicions: 'categoria' = 'higiene' és una dada d'entrada; 'obert' = True és una dada d'entrada

Els cinc casos cobreixen la política: A, desistiment net (el client paga l'enviament de tornada); B, l'aspirador defectuós al cap de 40 dies, usat i sense etiqueta, va per garantia amb enviament gratuït: R1 guanya tota la resta, i el xoc de 06-01 ha desaparegut per prioritat; C, el raspall d'higiene obert: R3 i R4 són totes dues aplicables (conflicte) i guanya R3 per prioritat, amb la qual cosa la incoherència de l'exercici 3 de 06-01 queda resolta i documentada a la traça; D, la regla del Diego; E, el mateix cas que A però amb 7 devolucions prèvies: R4 i R5 empaten en prioritat i guanya R5 per especificitat, derivant a revisió humana. La crida a per_que mostra l'explicació encadenada fins a les dades d'entrada.

7.5 Interfície de preguntes: consulta dirigida per l'objectiu

PREGUNTES = {
    "defectuos": "El producte presenta un defecte de fabricació o funcionament? (s/n)",
    "producte_nou": "Es va vendre com a producte nou (no recondicionat)? (s/n)",
    "dies_des_lliurament": "Quants dies han passat des del lliurament?",
    "categoria": "Categoria del producte (general/higiene/precintat)",
    "obert": "Està obert o desprecintat? (s/n)",
    "usat": "Mostra senyals d'ús? (s/n)",
    "etiqueta_original": "Conserva l'etiqueta original? (s/n)",
    "devolucions_previes": "Quantes devolucions prèvies té el client?",
}
def convertir(text):
    t = text.strip().lower()
    if t in ("s", "si", "sí"): return True
    if t in ("n", "no"): return False
    try: return int(t)
    except ValueError: return t

def respondre_simulat(respostes):
    """Retorna una funció 'respondre' que llegeix d'un dict en lloc d'input()
    (per poder executar la sessió sense teclat). Amb '?' explica per què pregunta."""
    def respondre(pregunta, regla):
        atribut = next(k for k, v in PREGUNTES.items() if v == pregunta)
        print(f"  Sistema: {pregunta}")
        if respostes.get(atribut + "?"):
            print(f"  Usuari: per què?\n  Sistema: perquè intento aplicar {regla.nom}: {regla}")
        text = respostes[atribut]
        print(f"  Usuari: {text}")
        return convertir(text)
    return respondre

# Portàtil NovaBook retornat amb dades parcials: el sistema pregunta el que necessita
sessio = {"defectuos": "n", "dies_des_lliurament": "10", "categoria": "general", "usat": "s", "usat?": True}
motor = MotorInferencia(base_devolucions(), MemoriaTreball({}))
decisio = motor.consultar("decisio", PREGUNTES, respondre_simulat(sessio))
print(f"  Decisió: {decisio} ({motor.memoria.fets.get('motiu', '')})")
print("  Com?\n" + motor.com())

Sortida:

  Sistema: El producte presenta un defecte de fabricació o funcionament? (s/n)
  Usuari: n
  Sistema: Categoria del producte (general/higiene/precintat)
  Usuari: general
  Sistema: Quants dies han passat des del lliurament?
  Usuari: 10
  Sistema: Mostra senyals d'ús? (s/n)
  Usuari: per què?
  Sistema: perquè intento aplicar R5: R5 [prio 5]: SI dies_des_lliurament <= 14 ∧ usat == False ∧ etiqueta_original == True ∧ devolucions_previes >= 5 ALESHORES {'decisio': 'revisio_humana', 'motiu': 'patró de devolucions freqüents'}
  Usuari: s
  Decisió: revisio_humana (producte usat dins del termini: valorar depreciació)
  Com?
  1. S'ha aplicat R6 perquè dies_des_lliurament=10 (<= 14), usat=True (== True) → {'decisio': 'revisio_humana', 'motiu': 'producte usat dins del termini: valorar depreciació'}. Justificació: una persona decideix si procedeix reemborsament total o parcial

Segueix el fil: el sistema comença per la regla de més prioritat (R1) i pregunta si és defectuós; amb "no", R1 i R2 queden descartades sense més preguntes. Passa a R3 i pregunta la categoria; "general" la descarta, i no pregunta si està obert (la condició ja coneguda falla). Passa a R5 (la mateixa prioritat que R4 però més específica): pregunta els dies i si està usat; l'usuari demana "per què?" i el mòdul d'explicació respon amb la regla que està intentant; "sí, usat" descarta R5 i R4, i R6 es compleix: revisió humana per producte usat dins del termini. En total 4 preguntes de 8 possibles: l'encadenament dirigit per objectiu pregunta només el que és rellevant, com MYCIN. Per fer-lo servir amb teclat n'hi ha prou de passar respondre=lambda pregunta, regla: convertir(input(pregunta + " ")).

  1. Eines reals

No cal escriure el motor cada vegada. En producció es fan servir:

  • CLIPS (NASA, anys 80, en C): la shell de regles clàssica amb encadenament cap endavant i l'algorisme Rete, que aparella regles i fets de manera eficient quan n'hi ha milers de totes dues; sintaxi tipus Lisp.
  • Drools (Java, codi obert): motor de regles de negoci (BRMS) molt usat en banca i assegurances, amb taules de decisió i l'estàndard DMN que veurem a 06-04.
  • experta (Python, hereva de PyKnow): regles com a mètodes decorats amb @Rule, fets com a objectes Fact, motor amb salience (prioritat). No és a l'entorn d'aquest curs, però el nostre codi es tradueix gairebé línia a línia als seus conceptes: Regla@Rule, MemoriaTreballdeclare, prioritatsalience.

Amb el que has après aquí, la documentació de qualsevol d'elles et resultarà familiar; 07-03 les situa en l'ecosistema.

Errors Comuns i Consells

  • Regles sense prioritat ni ordre pensat. És l'error de 06-01: dues regles aplicables i el resultat depèn de l'ordre d'escriptura. Decideix explícitament la jerarquia (garantia > excepcions > desistiment > regla general) i escriu-la a la justificació.
  • Regles que "saben" massa. Una regla que barreja vuit condicions és il·legible i fràgil. Descompon-la en fets intermedis (en_garantia, dins_termini) com vam fer a 06-01, i en regles de dues o tres condicions.
  • Atributs ambigus. obert és "desprecintat" o "usat"? El Diego distingeix; el diccionari de dades ha de distingir. Els sistemes experts moren de vocabulari imprecís abans que de lògica.
  • Oblidar el cas "cap regla aplicable". El nostre motor retorna None; en producció això s'ha de traduir en revisió humana, no en denegació per defecte (món tancat, 06-01) ni en un error.
  • No mantenir la bateria de casos. Cada regla nova pot canviar decisions antigues. Els casos A-E són el conjunt de test; afegeix-hi cada cas real conflictiu que aparegui.
  • Confiar la política a la màquina. El sistema aplica regles; el responsable de les regles és el Diego, i de la normativa, un professional legal. Que la traça digui sempre quina regla i quina versió han decidit.
  • Automatitzar el que exigeix una persona. Reemborsaments parcials, denegacions dubtoses i patrons de frau van a revisio_humana; és disseny, no covardia (02-04).

Exercicis

Exercici 1. El Diego recorda una excepció: durant la campanya de Nadal el termini de desistiment s'amplia a 30 dies per a les comandes lliurades al desembre. Afegeix a la memòria de treball un atribut campanya_nadal (True/False) i escriu la regla o regles necessàries sense trencar les existents. Quina prioritat els dones i per què? Comprova-ho amb el cas D (auriculars, 20 dies) amb campanya_nadal=True i amb False.

Exercici 2. Executa consultar amb la sessió {"defectuos": "s", "producte_nou": "s", "dies_des_lliurament": "400"} i anota quantes preguntes fa i què decideix. Després canvia dies_des_lliurament a "1200". Explica, mirant l'ordre de les regles a consultar, per què en tots dos casos el sistema no pregunta res sobre categoria, ús ni etiqueta.

Exercici 3. Dissenya, sense codi, la sessió d'adquisició del coneixement amb el Diego per afegir al sistema les garanties comercials ampliades que NovaMarket ven amb alguns productes (per exemple, 2 anys addicionals per a portàtils). Indica: (a) tres preguntes que li faries, (b) quins atributs nous necessitaria la memòria de treball, (c) quins casos de prova afegiries a la bateria A-E i (d) què podria sortir malament en la interacció amb R1 i R2.

Solucions

Solució 1. Una manera neta: R4n, prioritat 5, condicions campanya_nadal == True, dies_des_lliurament <= 30, usat == False, etiqueta_original == Truereemborsament_desistiment; i perquè la regla del Diego (R8) no denegui entre els dies 15 i 30 en campanya, o bé se li afegeix la condició campanya_nadal == False (i una R8n amb campanya_nadal == True i dies_des_lliurament > 30), o bé, més simple, es puja R4n per sobre de R8, cosa que ja passa (prioritat 5 davant de 2), perquè el motor s'atura en fixar decisio. Amb campanya_nadal=True el cas D (usat) continua sense entrar a R4n (exigeix sense usar) i necessita una R6n anàloga a R6 amb 30 dies (revisió humana); amb False, R8 denega com abans. La lliçó: cada excepció temporal toca diverses regles; convé agrupar-les i provar-les juntes.

Solució 2. Amb 400 dies: pregunta defectuos (sí), producte_nou (sí) i dies_des_lliurament (400): tres preguntes, R1 es compleix → garantia, enviament gratuït. Amb 1200 dies: les mateixes tres preguntes; R1 falla en la tercera condició, passa a R2 (defectuos == True, dies > 1095), que ja té tots els fets i es compleix → denegada, "fora del termini de garantia legal". consultar recorre les regles per prioritat descendent i dispara la primera que es compleix: com que R1 i R2 (prioritats 10 i 9) es resolen amb aquests tres fets, mai no arriba a R3-R8, les preguntes de les quals (categoria, ús, etiqueta) són irrellevants per a un producte defectuós.

Solució 3. (a) "Quins productes porten garantia ampliada i com se sap a la comanda?"; "l'ampliada cobreix el mateix que la legal (defectes) o també accidents?"; "qui paga l'enviament i qui repara, NovaMarket o el fabricant?". (b) garantia_ampliada_mesos (o fi_garantia_ampliada), tipus_cobertura, potser causa_defecte (fabricació/accident). (c) Portàtil NovaBook defectuós al cap de 4 anys amb ampliada de 2 anys → garantia (ampliada); el mateix sense ampliada → denegada per R2; un dany accidental amb ampliada que no el cobreix → denegada o revisió humana. (d) R2 denega a partir de 1095 dies abans de considerar l'ampliada: caldria donar a la nova regla una prioritat entre R1 i R2 (per exemple 9,5) o afegir a R2 la condició que no hi hagi ampliada vigent; i cal aclarir al diccionari que producte_nou i garantia_ampliada són atributs diferents.

Conclusió

En aquesta lliçó hem convertit el motor de 06-01 en un sistema expert. Hem vist què és (coneixement explícit separat del raonament) i d'on ve (DENDRAL, MYCIN, XCON), i n'hem recorregut l'arquitectura: base de coneixement, memòria de treball, motor d'inferència amb resolució de conflictes (refractarietat, prioritat, especificitat, recència), mòdul d'explicació (per què? i com?), adquisició del coneixement i interfície. Hem descrit el procés d'enginyeria del coneixement (entrevistes al Diego, extracció i formalització de regles, validació amb casos, manteniment amb propietari), i hem comparat avantatges i inconvenients i el sistema expert amb un model d'ML: regles escrites davant de regles apreses, amb l'arbre de 04-04 com a pont i la combinació de tots dos com a destí. En codi, Regla, BaseConeixement, MemoriaTreball i MotorInferencia han decidit cinc casos de devolucions i garanties de NovaMarket amb traça d'explicació (la garantia del NovaClean per prioritat, el raspall NovaSmile per prioritat sobre el desistiment, el monitor NovaView per especificitat), i consultar ha preguntat només el que calia, amb "per què?" inclòs.

Però fixa't en com hem tractat la incertesa: no l'hem tractat. defectuos és vertader o fals; revisio_humana és la sortida d'escapament quan no volem decidir. En les devolucions això és acceptable, perquè la política és nítida. En el diagnòstic d'incidències del cas 9 no ho és: un paquet abonyegat suggereix dany en transport però també un error de picking amb mal embalatge; un retard apunta a Getafe però també a la missatgeria; i incidencies.csv té la meitat de les causes sense emplenar. Aquí les regles nítides fallen i cal raonar amb graus de creença: els factors de certesa que va inventar MYCIN, la lògica difusa i, sobretot, la probabilitat, el teorema de Bayes i les xarxes bayesianes que van rellançar la IA als 90 (01-01). És el tema de la propera lliçó, 06-03.

Fonaments d'Intel·ligència Artificial (IA)

Mòdul 1: Introducció a la Intel·ligència Artificial

Mòdul 2: Principis Bàsics de la IA

Mòdul 3: Algorismes en IA

Mòdul 4: Aprenentatge Automàtic (Machine Learning)

Mòdul 5: Xarxes Neuronals i Deep Learning

Mòdul 6: Lògica i Sistemes Experts

Mòdul 7: Eines i Llenguatges de Programació en IA

Mòdul 8: Projectes i Casos d'Estudi

Mòdul 9: Exercicis i Pràctiques

Mòdul 10: Recursos Addicionals

© Copyright 2026. Tots els drets reservats