Publicat el 6 de novembre de 2025

per Osledy Bazo

Agents d'IA per a avaluació de crèdit hipotecari.

De regles estàtiques a decisions auditables amb RAG i tool-use. Com els agents d'IA poden transformar l'avaluació de crèdit hipotecari utilitzant RAG i tool-use per a decisions auditables i transparents.

El problema actual en l'avaluació hipotecària

L'avaluació de crèdit hipotecari no falla per la fórmula de PTI/DTI/LTV, sinó per com arriba cada equip a aquestes xifres: documents desiguals, criteris que canvien entre segments, càlculs amb supòsits diferents i poca traçabilitat per explicar una decisió a auditoria o al client. La següent radiografia condensa què fa mal i què necessita una solució moderna basada en agents d'IA + RAG + tool-use.

Documents i dades: més context útil

Un expedient típic barreja PDFs (nòmines, extractes), imatges escanejades (rebuts de lloguer), fulls de càlcul (RETA, llibres d'ingressos), contractes d'arras, tasacions i formularis interns. Sense una capa d'organització, l'analista:

  • Perd temps esbrinant on estan les dades (p. ex., el net està a la pàgina 2 o 3 de la nòmina?).

  • Comet errors en copiar/enganxar (net vs. brut, euros vs. milers).

  • No sap si el document és prou recent per ser vàlid (ex. nòmina de fa 5 mesos).

El que necessitem a la pràctica

És una normalització primerenca que faci tres coses senzilles però decisives:

  • Moneda i unitats unificades (p. ex., tot en EUR, "mes" com a període estàndard).

  • Periodització clara (ex.: payroll_2025_08.pdf → month=2025-08, net=2510), de manera que recència i tendències puguin validar-se automàticament.

  • Metadades de procedència: cada número porta amb si la seva font (arxiu, pàgina, camp), perquè qualsevol càlcul apunti a l'origen en un clic.

{
  "month": "2025-08",
  "net": 2510,
  "currency": "EUR",
  "source": "payroll_2025_08.pdf#page=1"
}
{
  "month": "2025-08",
  "incomes": 2580,
  "overdrafts": 0,
  "source": "extracto_bancoX_2025_08.pdf#page=2"
}

Amb això, un índex consultable (tipus RAG) ja no endevina on està la dada; la cita.

Mètriques clau però ben definides des del principi

Parlem de PTI, DTI, LTV i residual, però les diferències apareixen en els detalls:

  • PTI = quota hipotecària estressada / ingrés net mensual.

    L'error més comú és usar la quota sense stress (tipus actual) o dividir per l'ingrés brut.

  • DTI (post) = (altres deutes + quota estressada) / ingrés.

    Si la política permet que la hipoteca substitueixi el lloguer, no se suma el lloguer al post. Si no, sí.

  • LTV = import del préstec / min(preu, tasació).

    Usar només la tasació o només el preu esbiaixa la mesura del risc.

  • Residual = ingrés − (quota hipoteca + altres deutes).

    Es contrasta contra un mínim vital (ex., 900 € + 300 €/dependent).

def annuity_payment(principal, annual_rate, years):
    r = annual_rate / 12
    n = years * 12
    if r <= 0:
        return principal / n
    return principal * (r * (1 + r)**n) / ((1 + r)**n - 1)

Si un equip calcula PTI amb stress +3 pp i un altre sense stress, el mateix cas pot passar d'APTE a CONDICIONAT. La solució ha d'incloure un motor determinista (únic) que fixi fórmules i entrades, i deixi a l'agent la capa de "explicar i decidir" amb base en la política

La política no és un PDF: és policy-as-data (versionable i citable)

Les regles canvien per segment (resident/no resident), tipus d'activitat (empleat/autònom), producte (fix/variable). En un PDF, els matisos ("llindar conservador", "ingressos variables") s'interpreten diferent per cada analista, i és difícil saber quina versió es va aplicar.

Passar-ho a JSON resol tres coses:

  • Claredat: pti_max=0.35, ltv.primary_residence_max=0.80… sense ambigüitat.

  • Governança: versionat (v1.2, v1.3) i traçabilitat de quan va canviar.

  • Automatització: l'agent llegeix aquesta política (no la recorda) i aplica els llindars de forma consistent.

{
  "affordability": {
    "pti_max": 0.35,
    "dti_total_max": 0.45,
    "residual_income_min": {"base": 900, "per_dependent": 300},
    "rent_replacement_allowed": true
  },
  "ltv": {
    "primary_residence_max": 0.80,
    "take_lower_of_price_or_appraisal": true
  },
  "rate_stress": {
    "apply": true,
    "buffer_pp": 3.0,
    "min_rate_after_stress": 0.05
  }
}

Amb policy-as-data, canviar el límit de LTV o el buffer de stress és qüestió d'editar un JSON (i queda auditat).

Traçabilitat: explicar el "per què" amb cites, no amb opinions

A l'avaluador i control intern no els basta amb "CONDICIONAT": demanen proves. La sortida ha de dir:

  • Quina regla es va aplicar (p. ex., affordability.pti_max, rate_stress.buffer_pp).

  • Quins documents sustenten els números (nòmines de juny–agost, arras, tasació).

  • Quins canvis converteixen el cas en APTE (ex., "reduir principal a ≤ €149.700" o "afegir co-solicitant").

{
  "decision": "CONDICIONADO",
  "issues": ["PTI 42.1% > 35%", "DTI 46.9% > 45%", "LTV 83.7% > 80%"],
  "reason": "La cuota estresada es alta respecto al ingreso; el endeudamiento total y el LTV superan umbrales.",
  "conditions": ["Reducir principal a ≤ €149,700", "Entrada +€8,000 o co-solicitante"],
  "citations": {
    "policy": [
      "affordability.pti_max",
      "ltv.primary_residence_max",
      "rate_stress.buffer_pp"
    ],
    "case": [
      "payroll_2025_06.json",
      "payroll_2025_07.json",
      "payroll_2025_08.json",
      "property_appraisal.json",
      "property_purchase.json"
    ]
  }
}

Això canvia la conversa: ja no és opinar, és demostrar.

Qualitat i recència: checklist que evita reprocessos

Els desacords operatius solen venir de coses simples:

  • Una nòmina fora de recència (política demana 3 mesos i arriba una de fa 5).

  • Un net que no quadra amb l'abonament a l'extracte (tolerància, per exemple, ±5%).

  • Un préstec declarat que no apareix en moviments (o al revés).

Amb un checklist programàtic, el sistema marca abans de decidir: "falta la nòmina d'agost", "extracte incomplet", "DTI no considera targeta revolving".

Impacte i el que exigeix la solució

DolorImpacteQuè necessitem?
PDFs/imatges desigualsReprocessos, errorsNormalització (moneda, període, font) + índex consultable
Càlculs disparsDecisions inconsistentsMotor determinista de mètriques (font única)
Política ambiguaCriteris variablesPolicy-as-data (JSON versionat i citable)
Sense explicabilitatAuditoria costosaSortida JSON amb reason, issues, conditions i cites
Sense checklistNIGO, cicle llargValidadors de recència i coherència automàtics
Escalabilitat limitadaColls d'ampollaAgents amb tool-use i RAG (automatitzar el repetitiu)

Agents amb eines (tool-use) + RAG

  • Tool 1 — get_case_metrics: calcula mètriques en brut (entrades per a PTI/DTI/LTV/residual) des dels JSON normalitzats.

  • Tool 2 — policy_qe: llegeix la política JSON i retorna llindars amb cites (quina secció va aplicar).

  • Tool 3 — case_qe: recupera passatges de documents (p. ex., net a la nòmina o valor de tasació) perquè l'agent pugui citar.

  • Agent: orquestra aquestes eines i lliura una resposta estructurada: decision, issues, reason, conditions, citations.

  • Motor determinista (en paral·lel): assegura consistència numèrica i serveix per a tests A/B amb canvis de política.

Aquest repartiment evita que el LLM inventi números: calcula amb tools, llegeix llindars des de la política i cita documents.

Mini-cas en 3 línies

"Laura" sol·licita €180k. Amb stress +3 pb, la quota estressada dóna PTI 42%, DTI post 47% i LTV 84% — tots per sobre de la política. El sistema emet CONDICIONAT amb accions concretes: reduir principal a ~€149.7k o tenir co-solicitant; i cita tant les regles (PTI/DTI/LTV, stress) com les nòmines i la tasació usades.

Per què usar agents d'IA ara?

L'avaluació hipotecària exigeix tres coses a la vegada: consistència numèrica, capacitat de citar fonts i rapidesa per adaptar-se a canvis de política. Els agents actuals, models de llenguatge amb RAG i ús d'eines, encaixen perquè no substitueixen els càlculs ni les regles: els orquestren. Consulten la política en temps real, criden funcions per calcular i recuperen els passatges exactes dels documents. El resultat és una decisió explicable i repetible.

El que ha canviat

  • Ús d'eines sòlid. El model invoca funcions per nom i retorna sortides estructurades. Quan reporta un PTI, aquest número surt de la teva funció de càlcul.

  • RAG fiable. Indexar nòmines, extractes, arras o tasació permet recuperar i citar el fragment que respalda cada xifra.

  • Política com a dades. Els llindars viuen en JSON versionat. Canviar PTI màxim, LTV o el buffer d'estrès és editar dades; l'agent els llegeix al vol.

L'agent com a orquestador

L'agent no endevina xifres. Demana llindars a la política, calcula amb funcions i recupera evidències. Flux típic:

  • Extreu PTI, DTI, LTV, estrès i definicions aplicables des de la política, amb cita.

  • Calcula ingressos, deutes, quota estressada i base de LTV amb el motor determinista.

  • Recupera els passatges que sustenten xifres en els documents del cas.

  • Composa un JSON amb decisió, incidències, explicació, condicions i cites.

Aquest repartiment separa responsabilitats: càlcul determinista per als números, política com a font de límits i el model per integrar i explicar.

{
  "affordability": {
    "pti_max": 0.35,
    "dti_total_max": 0.45,
    "rent_replacement_allowed": true
  },
  "ltv": {
    "primary_residence_max": 0.80,
    "take_lower_of_price_or_appraisal": true
  },
  "rate_stress": {
    "apply": true,
    "buffer_pp": 3.0,
    "min_rate_after_stress": 0.05
  }
}

Beneficis tangibles

Avantatges directes: claredat, traçabilitat per versió i agilitat per reflectir canvis de llindars sense tocar el model.

  • Consistència. Mateixa entrada, mateixa sortida. S'eliminen discrepàncies entre fulls de càlcul.

  • Explicabilitat. Cada ràtio inclou cita a política i document, útil per a auditoria i per comunicar condicions al client.

  • Menys reprocessos. Validadors automàtics de recència i coherència redueixen NIGO i temps de cicle.

  • Evolució simple. Afegir un segment o ajustar llindars equival a estendre el JSON i, si cal, ajustar el càlcul amb canvis localitzats.

Mini-snippet orientatiu

# 1) Cálculo determinista (fuente de verdad de métricas)
def get_case_metrics(): ...

# 2) Lectura de política (RAG sobre JSON versionado)
policy_qe = VectorStoreIndex.from_documents([Document(text=open("policy.json").read())]).as_query_engine()

# 3) Consulta de documentos del caso (para citas)
case_qe = VectorStoreIndex.from_documents(load_case_docs()).as_query_engine()

# 4) El agente orquesta: política → métricas → evidencias → JSON explicable

Arquitectura de referència

La idea és separar amb claredat quatre responsabilitats: dades, regles, càlcul i decisió. Amb aquest disseny, pots canviar la política sense tocar el codi, rastrejar cada xifra fins al seu document i explicar per què una sol·licitud surt apta, condicionada o no apta.

Vista general

                         ┌─────────────────────────────┐
                         │   Registre de Política      │
                         │     (JSON versionat)        │
                         └──────────────┬──────────────┘
                                        │ llegeix/consulta
                                        ▼
┌─────────────────────────────┐   ┌───────────────────────────-──┐
│   Ingestió i Normalització  │   │     Índexs semàntics        │
│   (PDF/IMG/CSV → JSON)      │   │  • Cas (documents)          │
│   • moneda, període, font   │   │  • Política (llindars)      │
└──────────────┬──────────────┘   └───────────────┬──────────────┘
               │                                  │
               ▼                                  ▼
       ┌───────────────-─┐                ┌───────────────────────┐
       │ get_case_metrics│                │ policy_qe / case_qe   │
       │ Càlcul          │                │ RAG: porta passatges  │
       │ determinista    │                │ i llindars amb cites  │
       └────────┬────────┘                └────────────┬──────────┘
                │                                      │
                └──────────────┬───────────────────────┘
                               ▼
                      ┌───────────────────┐
                      │  Agent d'IA       │
                      │  Orquestra tools   │
                      │  i composa sortida│
                      └────────┬──────────┘
                               ▼
                 ┌─────────────────────────────┐
                 │ { decision, issues, reason, │
                 │   conditions, citations }   │
                 └─────────────┬───────────────┘
                               ▼
         Panell analista • CRM/Core • Bitàcola d'auditoria
  • La política viu com a dades versionades.

  • Els documents del cas es normalitzen en JSON amb moneda, període i referència de font.

  • Els índexs semàntics permeten preguntar i obtenir passatges que poden citar-se.

  • El motor de càlcul aplica fórmules exactes.

  • L'agent usa aquestes peces per decidir i explicar, no per inventar números.

Capa de dades: el just per treballar amb confiança

Abans de pensar en models, ens assegurem que cada número té context. Això s'aconsegueix afegint metadades mínimes en la transformació a JSON:

  • Període o data (per exemple, 2025-08).

  • Quantitat i moneda.

  • doc_type i applicant_id.

  • Referència de procedència (arxiu, pàgina, selector).

// payroll_2025_08.json
{
  "doc_type": "payroll",
  "month": "2025-08",
  "net": 2510,
  "currency": "EUR",
  "source_file": "payroll_2025_08.pdf",
  "page": 1
}
// property_appraisal.json
{
  "doc_type": "property_appraisal",
  "appraised_value": 225000,
  "date": "2025-08-20",
  "source_file": "tasacion_0820.pdf",
  "page": 3
}

Amb aquesta base, qualsevol xifra que el sistema utilitzi pot citar-se al detall.

Índexs i recuperació

  • Índex del cas: conté tots els documents normalitzats. S'utilitza per recuperar fragments que justifiquen ingressos, deutes, preu o avaluació.

  • Índex de política: conté el JSON de regles vigent (i versions anteriors, si es desitja). S'utilitza per recuperar llindars i definicions.

from llama_index.core import Document, VectorStoreIndex

policy_doc = Document(text=open("policy.json").read(), metadata={"doc_type": "policy", "version": "v1"})

policy_qe = VectorStoreIndex.from_documents([policy_doc]).as_query_engine(similarity_top_k=3)

Càlcul determinista

Aquesta capa és simple i crítica. Calcula la quota amb anualitat, PTI, DTI, LTV i residual. Aplica l'estrès de tipus que marqui la política. D'aquesta manera, el mateix cas sempre produeix les mateixes mètriques.

def annuity_payment(principal, annual_rate, years):
    r = annual_rate / 12
    n = years * 12
    if r <= 0:
        return principal / n
    return principal * (r * (1 + r)**n) / ((1 + r)**n - 1)
def get_case_metrics(case_dir) -> dict:
    # 1) Lee JSON normalizados (nóminas, préstamos, arras, tasación, solicitud)
    # 2) Deriva ingreso mensual, deudas, importe/years/rate y min(precio, tasación)
    # 3) Calcula cuota estresada según política actual
    # 4) Devuelve un diccionario de métricas
    return metrics

case_docs = [Document(text=json_to_markdown(p), metadata=meta_of(p)) for p in load_case_jsons()]

case_qe   = VectorStoreIndex.from_documents(case_docs).as_query_engine(similarity_top_k=5)

Eines de l'agent (tools)

  • get_case_metrics: lliura mètriques en brut.

  • policy_qe: porta llindars i definicions amb fragments citables de la política.

  • case_qe: porta passatges que justifiquen els números del cas.

from llama_index.core.tools import FunctionTool, QueryEngineTool

metrics_tool = FunctionTool.from_defaults(fn=lambda: get_case_metrics(CASE_DIR),name="get_case_metrics", description="Métricas del caso en JSON.")

policy_tool  = QueryEngineTool.from_defaults(query_engine=policy_qe, name="policy_qe", description="Umbrales y reglas desde la política.")

case_tool = QueryEngineTool.from_defaults(query_engine=case_qe, name="case_qe", description="Pasajes citables de los documentos del caso.")

Agent de decisió

L'agent segueix sempre la mateixa seqüència mental:

  • Llegeix llindars a la política.

  • Obté les mètriques calculades.

  • Recupera evidència en documents.

  • Lliura una resposta estructurada amb decisió, incidències, explicació, condicions i cites.

Suggerència pràctica: incloure en el prompt el criteri de mapatge de dictamen perquè sigui consistent amb Riscos. Per exemple, apte sense violacions; condicionat amb una o dues violacions o amb solucions clares; no apte quan hi ha diverses violacions crítiques o magnituds molt per sobre dels límits.

from llama_index.agent.openai import OpenAIAgent

SYSTEM = """

Eres analista hipotecario.

Usa policy_qe para umbrales, get_case_metrics para cifras y case_qe para evidencia.

Devuelve solo JSON con:

{ "decision": "APTO|CONDICIONADO|NO_APTO",

  "issues": ["..."],

  "reason": "...",

  "conditions": ["..."],

  "citations": { "policy": ["..."], "case": ["..."] } }

"""

agent = OpenAIAgent.from_tools(

    tools=[metrics_tool, policy_tool, case_tool],

    system_prompt=SYSTEM,

    verbose=True

)

verdict = agent.chat("Evalúa el caso y devuelve el JSON solicitado.")

Sortida i auditoria: l'explicació ve de sèrie

La sortida de l'agent un cop està llesta per integrar-se al panell de l'analista. Inclou el que Riscos i Auditoria pregunten amb més freqüència: quin llindar es va aplicar, quin document respalda la xifra i quins canvis convertiran el cas en apte.

Per assegurar format i evitar sorpreses, valida el JSON amb un esquema:

from pydantic import BaseModel

class Verdict(BaseModel):

    decision: str

    issues: list[str]

    reason: str

    conditions: list[str]

    citations: dict

validated = Verdict.model_validate_json(verdict.response)

A més, registra la versió de política aplicada i els logs d'eines invocades. És el fil d'Ariadna de cada decisió.

Un exemple de punta a punta en 20 segons

  • L'analista prem avaluar.

  • L'agent consulta la política vigent: PTI 35, DTI 45, LTV 80, estrès més tres punts amb mínim 5.

  • El motor calcula ingressos, deutes i quota estressada i retorna PTI, DTI i LTV.

  • L'agent porta els passatges de nòmines i avaluació que justifiquen aquestes xifres.

  • La resposta arriba amb decisió, motius, condicions concretes i cites precises.

Amb aquest esquema, la solució és previsible per a Riscos, còmoda per a l'analista i defensable davant Auditoria. I si demà canvia el límit de LTV o el buffer d'estrès, n'hi ha prou amb actualitzar el JSON de política: tot el demés s'ajusta sol.

Mètriques i regles clau

A la pràctica, l'aptitud d'una hipoteca es decideix amb quatre peces: la quota estressada, PTI, DTI, LTV i l'ingrés residual. El que importa no és només la fórmula, sinó quan aplicar-la, amb quines entrades i com justificar cada número. A continuació les expliquem de forma ordenada i amb un mini-exemple que les posa en context.

Quota estressada: el punt de partida

Abans de calcular ràtios, cal fixar una quota prudent. Per això s'aplica l'estrès de tipus que defineix la política —per exemple, sumar tres punts percentuals al tipus nominal, amb un sòl mínim del cinc per cent— i s'utilitza la fórmula d'anualitat.

  • Estrès de tipus:

    tasa_estresada = max(tasa_nominal + buffer_pp/100, tasa_minima)

  • Quota mensual (anualitat):

    quota = P * [ r * (1+r)^n ] / [ (1+r)^n − 1 ],

    on P és el principal, r és la taxa mensual i n el nombre de quotes.

Aquesta quota és la que condiciona la resta: si és alta davant de l'ingrés, puja PTI i DTI; si baixa el principal o puja l'ingrés, ambdós ràtios milloren immediatament.

def stressed_rate(nominal, buffer_pp, min_rate):
    return max(nominal + buffer_pp/100, min_rate)

def annuity_payment(P, annual_rate, years):
    r, n = annual_rate/12, years*12
    return P/n if r <= 0 else P * (r*(1+r)**n)/((1+r)**n - 1)

PTI: esforç d'habitatge

PTI mesura quant de l'ingrés mensual es destina a la quota estressada de la hipoteca.

PTI = quota_estresada / ingrés_net_mensual

Un límit típic és 35%. Un PTI per sobre indica que la càrrega d'habitatge és elevada per al nivell d'ingressos declarat.

Si PTI supera el llindar, hi ha tres palanques: reduir l'import del préstec, allargar termini (amb prudència) o acreditar més ingrés estable, per exemple amb un co-solicitant.

DTI: endeutament total

DTI integra tota la càrrega financera. Es calcula en dos moments:

  • DTI actual: (altres_deutes + lloguer) / ingrés

  • DTI post: (altres_deutes + quota_estresada) / ingrés

Si la política permet substitució de lloguer (compres primera vivenda), el lloguer no se suma al DTI post. El límit habitual és 45%. Aquest ràtio captura casos en què el PTI sembla acceptable però el client ja arrossega préstecs de consum o targetes.

LTV: prudència sobre el valor de l'immoble

LTV compara l'import del préstec amb el valor de referència de l'immoble, prenent el menor entre preu de compra i avaluació.

LTV = principal / min(preu, avaluació)

En primera vivenda el límit sol ser 80%. Un LTV alt assenyala poc coixí d'entrada; és típic resoldre-ho amb més aportació o ajustant l'import.

Ingrés residual: marge de seguretat

El que queda després de pagar hipoteca i deutes cada mes:

residual = ingrés − (quota_estresada + altres_deutes)

Es contrasta contra un mínim vital fixat en política —per exemple, 900 euros base més un suplement per dependent—. Aporta una visió de capacitat de pagament complementària a PTI i DTI.

Condicions que converteixen un no en un sí

Més enllà del dictamen, el sistema ha de proposar caminys clars per assolir apte. Per això convé calcular:

  • Principal màxim per PTI: pagament permès = PTI_max * ingrés; s'inverteix l'anualitat per recuperar el principal compatible.

  • Principal màxim per DTI: pagament permès = DTI_max * ingrés − altres_deutes.

  • Principal màxim per LTV: LTV_max * min(preu, avaluació).

  • Ingrés necessari per complir PTI o DTI amb l'import actual.

La recomanació pràctica és prendre el mínim d'aquests tres principals màxims i expressar-lo en condicions senzilles: reduir import fins a X, aportar Y d'entrada, o acreditar Z euros més al mes.

Exemple simplificat

Cas tipus empleat amb dades arrodonides:

  • Ingrés net mensual: 2.510 €

  • Import sol·licitat: 180.000 € a 30 anys

  • Tipus nominal: 2,8%; estrès: +3 pp amb mínim 5%

  • Altres deutes: 120 €/mes; lloguer actual: 900 €/mes

  • Preu: 215.000 €; avaluació: 225.000 €

  • Llindars de política: PTI 35%, DTI 45%, LTV 80%

Càlculs clau:

  • Tipus estressat: 5,0%

  • Quota estressada: 1.056 €/mes

  • PTI: 1.056 / 2.510 = 42,1% → per sobre de 35%

  • DTI post (amb substitució de lloguer): (120 + 1.056) / 2.510 = 46,9% → per sobre de 45%

  • LTV: 180.000 / 215.000 = 83,7% → per sobre de 80%

Consells:

Reduir principal a ≈ 149.700 € compleix PTI i arrossega DTI a la zona verda. Si es manté l'import, una entrada addicional ~8.000 € corregeix LTV, però PTI seguiria alt; caldria sumar ingrés (≈ 508 €/mes) o incorporar co-solicitant.

Implementació (mínima)

L'objectiu és muntar un MVP que converteixi documents dispersos en una decisió explicable. A continuació tens els passos essencials, amb fragments de codi llestos per enganxar i adaptar.

Índexs per a política i cas

def build_indexes(case_dir: Path, policy_path: Path):
    # índice de política
    policy_text = Path(policy_path).read_text()
    policy_doc  = Document(text=policy_text, metadata={"doc_type": "policy", "version": "v1"})
    policy_index = VectorStoreIndex.from_documents([policy_doc])
    policy_qe = policy_index.as_query_engine(similarity_top_k=3)
    
    # índice del caso
    case_docs = load_case_docs(case_dir)
    case_index = VectorStoreIndex.from_documents(case_docs)
    case_qe = case_index.as_query_engine(similarity_top_k=5)
    
    return policy_qe, case_qe

Motor determinista de mètriques

Centralitza aquí les fórmules. És la font única de veritat numèrica.

def stressed_rate(nominal: float, buffer_pp: float, min_rate: float) -> float:
    return max(nominal + buffer_pp/100.0, min_rate)

def annuity_payment(P: float, annual_rate: float, years: int) -> float:
    r, n = annual_rate/12.0, years*12
    if n <= 0: return 0.0
    if r <= 0: return P / n
    return P * (r*(1+r)**n) / ((1+r)**n - 1)

def load_policy(policy_path: Path) -> dict:
    return json.loads(Path(policy_path).read_text())

def get_case_metrics(case_dir: Path, policy: dict) -> dict:
    # carga entradas esenciales
    req   = json.loads((case_dir/"mortgage_request.json").read_text())
    price = json.loads((case_dir/"property_purchase.json").read_text())["price"]
    appr  = json.loads((case_dir/"property_appraisal.json").read_text())["appraised_value"]
    debts = json.loads((case_dir/"prior_loans.json").read_text())["other_debt_monthly"]
    rent  = json.loads((case_dir/"rent_receipts.json").read_text())["current_rent"]
    
    # ingreso mensual: ejemplo con nóminas de 3 meses
    p6 = json.loads((case_dir/"payroll_2025_06.json").read_text())["net"]
    p7 = json.loads((case_dir/"payroll_2025_07.json").read_text())["net"]
    p8 = json.loads((case_dir/"payroll_2025_08.json").read_text())["net"]
    income = (p6 + p7 + p8) / 3
    
    # stress y cuota
    stress = policy["rate_stress"]
    rate_stressed = stressed_rate(req["apr_hint"], stress["buffer_pp"], stress["min_rate_after_stress"])
    pay_stressed  = annuity_payment(req["amount"], rate_stressed, req["years"])
    
    # base de LTV
    base_value = min(price, appr)
    
    # ratios principales
    pti = pay_stressed / income
    
    # si la política permite sustitución de alquiler no lo sumes en post
    rent_replace = policy["affordability"].get("rent_replacement_allowed", True)
    dti_post = (debts + pay_stressed) / income
    dti_current = (debts + rent) / income
    ltv = req["amount"] / base_value
    
    residual_min = policy["affordability"].get("residual_income_min", {}).get("base", 900)
    residual = income - (debts + pay_stressed)
    
    return {
        "income": income,
        "other_debt": debts,
        "rent": rent,
        "loan": req,
        "price": price,
        "appraised": appr,
        "base_value": base_value,
        "rate_stressed": rate_stressed,
        "pay_stressed": pay_stressed,
        "pti": pti,
        "dti_current": dti_current,
        "dti_post": dti_post,
        "ltv": ltv,
        "residual": residual,
        "residual_min": residual_min,
        "rent_replacement_allowed": rent_replace
    }

Eines de l'agent

Tres eines basten: mètriques, política i documents del cas.

def make_metrics_tool(case_dir: Path, policy_path: Path):
    def _fn():
        pol = load_policy(policy_path)
        return get_case_metrics(case_dir, pol)
    return FunctionTool.from_defaults(
        fn=_fn, name="get_case_metrics",
        description="Métricas crudas calculadas del caso en JSON."
    )

def make_policy_tool(policy_qe):
    return QueryEngineTool.from_defaults(
        query_engine=policy_qe, name="policy_qe",
        description="Consulta de umbrales y definiciones desde la política vigente."
    )

def make_case_tool(case_qe):
    return QueryEngineTool.from_defaults(
        query_engine=case_qe, name="case_qe",
        description="Recupera pasajes de documentos del caso para citarlos."
    )

Agent de decisió amb sortida estructurada

Inclou en el sistema el format de sortida i el criteri de dictamen. Evita respostes lliures que siguin difícils d'integrar.

SYSTEM = """

Eres analista hipotecario.

1) Usa policy_qe para obtener umbrales (PTI, DTI, LTV, stress, residual) y cita la sección usada.

2) Usa get_case_metrics para cifras calculadas.

3) Usa case_qe para traer pasajes que justifiquen números de ingreso, precio y tasación.

4) Devuelve JSON con:

{ "decision": "APTO|CONDICIONADO|NO_APTO",

  "issues": ["..."],

  "reason": "...",

  "conditions": ["..."],

  "citations": { "policy": ["..."], "case": ["..."] } }

Reglas de dictamen:

- APTO: ninguna violación

- CONDICIONADO: 1–2 violaciones o ajustes viables claros

- NO_APTO: 3 o más violaciones críticas o magnitudes muy por encima del límite

"""

def build_agent(case_dir: Path, policy_path: Path):
    policy_qe, case_qe = build_indexes(case_dir, policy_path)
    tools = [
        make_metrics_tool(case_dir, policy_path),
        make_policy_tool(policy_qe),
        make_case_tool(case_qe),
    ]
    return OpenAIAgent.from_tools(tools=tools, system_prompt=SYSTEM, verbose=True)

if __name__ == "__main__":
    case_dir = Path("data/case_EMP_001")
    policy_path = Path("data/policy.json")
    agent = build_agent(case_dir, policy_path)
    verdict = agent.chat("Evalúa el caso y devuelve el JSON solicitado.")
    print(getattr(verdict, "response", verdict))

Validació de la sortida i control d'errors

Valida el JSON de l'agent perquè backend i panell el consumeixin sense sorpreses.

from pydantic import BaseModel, Field, ValidationError

class Verdict(BaseModel):
    decision: str
    issues: list[str]
    reason: str
    conditions: list[str]
    citations: dict

try:
    parsed = Verdict.model_validate_json(getattr(verdict, "response", "{}"))
except ValidationError as e:
    # registra el error y aplica fallback si procede
    print("Salida del agente inválida:", e)

Bones pràctiques addicionals:

  • Limitar tokens de sortida i exigir el bloc JSON com a resposta única.

  • Incloure versió de política aplicada i referències de documents en logs.

  • Reintentar una vegada si la validació falla, mantenint el mateix context.

Seguretat, compliment i qualitat

Aquesta secció aborda com protegir dades, governar canvis i garantir decisions consistents. La idea és simple: tractar la política com a dades versionades, minimitzar l'exposició de PII i auditar cada pas que l'agent fa amb eines.

Dades personals: el just, ben protegit

Principis operatius

  • Minimització. Només carregar el necessari per calcular ingressos, deutes, preu, avaluació i recència.

  • Separació. Guardar PII i característiques de risc en emmagatzematges diferents; treballar amb identificadors i referències.

  • Recència i retenció. Validar vigència i purgar informació fora de la finestra acordada amb Riscos i Legal.

  • Accés. Control per rols i registre d'accés; xifrat en trànsit i en repòs.

DadaÚs en decisióRetenció raonableObservacions
DNI i perfilKYC, segmentacióSegons política KYCEnmascarar en logs
Nòmines o RETAIngrés12–24 mesosGuardar net mensual i font
Extractes bancarisConducta i deutes3–12 mesosDerivar senyals; no emmagatzemar text complet
Arras, compra, avaluacióLTV i valorFins al tancamentReferenciar versió i data
Préstecs previsDTI actual i postVigència del préstecNormalitzar a mensual

Governança de política i canvis

  • Política en JSON amb semàntica clara i versionat. Exemple d'encapçalament:

{
  "policy_id": "mortgage_es_v1.3",
  "effective_date": "2025-08-01",
  "affordability": { "pti_max": 0.35, "dti_total_max": 0.45 },
  "ltv": { "primary_residence_max": 0.80 }
}
  • Control de canvis. Proposta, revisió de Riscos, aprovació, desplegament amb etiqueta de versió.

  • Trazabilitat. Cada avaluació guarda la versió de política aplicada.

Guardrails tècnics per a l'agent

  • Llista blanca d'eines. L'agent només pot invocar get_case_metrics, policy_qe i case_qe.

  • Esquema de sortida obligatori. Validar que la resposta compleix el JSON esperat; rebutjar si no s'ajusta.

  • Límit de context. Indexar i recuperar només el necessari, evitant que el LLM vegi PII sense motiu.

  • Defensa davant injecció en documents. Filtrar i netejar text d'entrada; mai executar instruccions embegudes en PDFs o notes.

  • Reintents controlats. Un únic reintent si la validació de l'esquema falla; registrar ambdós intents.

Auditoria i trazabilitat

Què registrar per cas

  • Versió de política aplicada i hash de l'arxiu de política.

  • Eines invocades per l'agent, paràmetres d'entrada i fragments citats.

  • Identificadors de documents font i les seves pàgines o selectors.

  • Valors numèrics deterministes calculats i decisió final generada.

Reproduïbilitat

  • Conservar els artefactes mínims: inputs normalitzats, versió de política i versió del codi del motor determinista.

  • Possibilitat de reexecutar un cas històric amb la política d'aquella data.

Qualitat: proves i mètriques

Proves automàtiques

  • Sanitat numèrica. Quota, PTI, DTI, LTV i residual amb valors coneguts.

  • Casuística límit. Estrès per sòl mínim, substitució de lloguer activada o no, LTV amb avaluació inferior al preu.

  • Regles de dictamen. Mapatge de violacions cap a apte, condicionat i no apte.

Avaluació contínua

  • Banc de casos de regressió per segment. Executar després de cada canvi de política o de model.

  • Mètriques operatives. Temps de decisió, taxa de NIGO, percentatge de decisions condicionades que passen a apte després d'ajustos.

  • Alineació determinista–agent. Alertes si la decisió de l'agent difereix del determinista per fora d'una tolerància acordada.

Equitat

  • Evitar atributs sensibles en dades d'entrada i en característiques derivades.

  • Auditories de sensibilitat amb dades sintètiques per detectar biaixos involuntaris en l'explicació o en les condicions proposades.

Evolució funcional

  • Simulador d'escenaris. Permet variar import, termini o entrada i veure com canvien PTI, DTI i LTV. És útil per a la conversa amb el client i per formar analistes.

  • Multi-solicitant. Combinar ingressos, deutes i polítiques específiques per a parelles o co-solicitants. Ajustar regles de tenència mínima i historial per sol·licitant.

  • Extracció avançada. Integrar OCR i parsers per a PDFs bancaris i nòmines, amb validadors de coherència entre documents. Reduir el temps manual de normalització.

  • Conducta bancària. Derivar senyals d'extractes: descobriments, ús de targeta revolving, percentatge d'ingressos en efectiu. Afegir llindars i alertes en política.

  • Buro i fonts externes. Connectar amb fitxers de morositat i verificació d'identitat. Registrar l'evidència com un altre document citables.

  • Explainability estesa. Afegir puntuacions per mètrica i una explicació més rica orientada a client, sense exposar PII innecessària.

Conclusió: arquitectura i beneficis

L'avaluació hipotecària no és només calcular ràtios. És coordinar documents heterogenis, aplicar polítiques que canvien i explicar decisions amb rigor. L'arquitectura proposada separa aquestes responsabilitats perquè el sistema sigui previsible per a Riscos, còmode per a l'analista i defensable davant Auditoria.

Resol:

  • Política com a dades: llindars clars i versionats, llestos per citar i ajustar sense tocar codi.

  • Càlcul determinista: una única font de veritat per a quota estressada, PTI, DTI, LTV i residual.

  • RAG sobre documents: evidència recuperable al detall amb referències a l'origen.

  • Agent amb eines: orquestra les peces, contrasta contra la política i lliura una sortida estructurada amb decisió, incidències, explicació, condicions i cites.

Resultats esperats

  • Consistència: mateixa entrada, mateixa sortida, independentment de l'analista.

  • Menys NIGO i reprocessos: checklist de recència i coherència abans de decidir.

  • Cicles més curts: menys cerca manual i menys anades i tornades amb el client.

  • Explicabilitat immediata: cada número apunta a la seva regla i al seu document.

  • Agilitat: canvis de PTI, DTI o LTV s'apliquen editant el JSON de política.

SOPs i operació diària

Si ja disposes de SOPs del procés hipotecari, aquesta solució els reforça i els torna executables. La idea és que cada pas del SOP tingui una traducció directa en eines, validadors i registres.

Com mapegem SOPs a l'arquitectura

  • SOP d'intake i completitud → checklist programàtic de documents i recència; estat de l'expedient visible abans de calcular res.

  • SOP de verificació d'ingressos → recuperació de nòmines o RETA amb RAG, regles de coherència entre nòmina i extracte, toleràncies definides en política.

  • SOP d'aplicació de política → policy-as-data; l'agent llegeix llindars vigents i els cita en la decisió.

  • SOP d'excepcions → flux d'override controlat: qui sol·licita l'excepció, per quina mètrica, quina evidència la respalda i qui l'aprova.

  • SOP de canvis de política → versionat del JSON, revisió per Riscos i auditoria, proves de regressió i desplegament amb canary.

  • SOP d'auditoria → registre d'eines invocades, fragments citats, versió de política i valors deterministes usats.

  • SOP d'incidents i rollback → possibilitat de fixar política i model per versió, revertir canvis i reexecutar casos històrics.

  • SOP de formació → banc de casos etiquetats, simulador d'escenaris i explicacions generades per l'agent llestes per a coaching.

Exemple de SOP operatiu expressat com a dades

Això permet que el panell i l'agent segueixin el mateix guió de manera consistent:

sop_id: mortgage_underwriting_es_v1
effective_date: 2025-09-01
steps:
  - name: Validar completitud y recencia
    checks:
      - require: payroll_last_3m
      - require: bank_statements_last_3m
      - require: property_appraisal <= 6m
    on_fail: request_missing_docs

  - name: Calcular métricas deterministas
    tool: get_case_metrics
    outputs: [pti, dti_post, ltv, residual]

  - name: Aplicar política
    tool: policy_qe
    compare:
      - pti <= affordability.pti_max
      - dti_post <= affordability.dti_total_max
      - ltv <= ltv.primary_residence_max

  - name: Evidencia
    tool: case_qe
    cite:
      - payroll_months: [2025-06, 2025-07, 2025-08]
      - appraisal: latest
      - purchase_price: current

  - name: Dictamen y condiciones
    decision_rules:
      - if: violations == 0
        decision: APTO
      - if: violations in [1,2]
        decision: CONDICIONADO
      - if: violations >= 3
        decision: NO_APTO
    include: issues, reason, conditions, citations

Construïm junts

Unim experiència i innovació per portar el teu projecte al següent nivell.

Contacta’ns
🏆Nominat 2025

AlamedaDev està nominada pels seus serveis d'Intel·ligència Artificial, Desenvolupament de Software a Mida i Desenvolupament d'Aplicacions Mòbils.

TechBehemoths 2025 Nominee
🏆Guanyador 2024
Award 2024
Award 2024
Partners:
DCA-IA Partner
AWS Partner