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ó
| Dolor | Impacte | Què necessitem? |
|---|---|---|
| PDFs/imatges desiguals | Reprocessos, errors | Normalització (moneda, període, font) + índex consultable |
| Càlculs dispars | Decisions inconsistents | Motor determinista de mètriques (font única) |
| Política ambigua | Criteris variables | Policy-as-data (JSON versionat i citable) |
| Sense explicabilitat | Auditoria costosa | Sortida JSON amb reason, issues, conditions i cites |
| Sense checklist | NIGO, cicle llarg | Validadors de recència i coherència automàtics |
| Escalabilitat limitada | Colls d'ampolla | Agents 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 explicableArquitectura 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'auditoriaLa 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_qeMotor 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ó raonable | Observacions |
|---|---|---|---|
| DNI i perfil | KYC, segmentació | Segons política KYC | Enmascarar en logs |
| Nòmines o RETA | Ingrés | 12–24 mesos | Guardar net mensual i font |
| Extractes bancaris | Conducta i deutes | 3–12 mesos | Derivar senyals; no emmagatzemar text complet |
| Arras, compra, avaluació | LTV i valor | Fins al tancament | Referenciar versió i data |
| Préstecs previs | DTI actual i post | Vigència del préstec | Normalitzar 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, citationsConstruïm junts
Unim experiència i innovació per portar el teu projecte al següent nivell.


