Publicado el 6 de noviembre de 2025

por Osledy Bazo

Agentes de IA para evaluación de crédito hipotecario.

De reglas estáticas a decisiones auditables con RAG y tool-use. Cómo los agentes de IA pueden transformar la evaluación de crédito hipotecario utilizando RAG y tool-use para decisiones auditables y transparentes.

El problema actual en la evaluación hipotecaria

La evaluación de crédito hipotecario no falla por la fórmula de PTI/DTI/LTV, sino por cómo llega cada equipo a esas cifras: documentos desparejos, criterios que cambian entre segmentos, cálculos con supuestos distintos y poca trazabilidad para explicar una decisión a auditoría o al cliente. La siguiente radiografía condensa qué duele y qué necesita una solución moderna basada en agentes de IA + RAG + tool-use.

Documentos y datos: más contexto útil

Un expediente típico mezcla PDFs (nóminas, extractos), imágenes escaneadas (recibos de alquiler), hojas de cálculo (RETA, libros de ingresos), contratos de arras, tasaciones y formularios internos. Sin una capa de organización, el analista:

  • Pierde tiempo averiguando dónde están los datos (p. ej., ¿el neto está en la página 2 o 3 de la nómina?).

  • Comete errores al copiar/pegar (neto vs. bruto, euros vs. miles).

  • No sabe si el documento es lo bastante reciente para ser válido (ej. nómina de hace 5 meses).

Lo que necesitamos en la práctica

Es una normalización temprana que haga tres cosas sencillas pero decisivas:

  • Moneda y unidades unificadas (p. ej., todo en EUR, "mes" como período estándar).

  • Periodización clara (ej.: payroll_2025_08.pdf → month=2025-08, net=2510), de modo que recencia y tendencias puedan validarse automáticamente.

  • Metadatos de procedencia: cada número lleva consigo su fuente (archivo, página, campo), para que cualquier cálculo apunte al 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"
}

Con esto, un índice consultable (tipo RAG) ya no adivina dónde está el dato; lo cita.

Métricas clave pero bien definidas desde el inicio

Hablamos de PTI, DTI, LTV y residual, pero las diferencias aparecen en los detalles:

  • PTI = cuota hipotecaria estresada / ingreso neto mensual.

    El error más común es usar la cuota sin stress (tipo actual) o dividir por el ingreso bruto.

  • DTI (post) = (otras deudas + cuota estresada) / ingreso.

    Si la política permite que la hipoteca sustituya el alquiler, no se suma el alquiler al post. Si no, sí.

  • LTV = importe del préstamo / min(precio, tasación).

    Usar solo la tasación o solo el precio sesga la medición del riesgo.

  • Residual = ingreso − (cuota hipoteca + otras deudas).

    Se contrasta contra un mínimo vital (ej., 900 € + 300 €/dependiente).

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 equipo calcula PTI con stress +3 pp y otro sin stress, el mismo caso puede pasar de APTO a CONDICIONADO. La solución debe incluir un motor determinista (único) que fije fórmulas y entradas, y deje al agente la capa de "explicar y decidir" con base en la política

La política no es un PDF: es policy-as-data (versionable y citable)

Las reglas cambian por segmento (residente/no residente), tipo de actividad (empleado/autónomo), producto (fijo/variable). En un PDF, los matices ("umbral conservador", "ingresos variables") se interpretan distinto por cada analista, y es difícil saber qué versión se aplicó.

Pasarlo a JSON resuelve tres cosas:

  • Claridad: pti_max=0.35, ltv.primary_residence_max=0.80… sin ambigüedad.

  • Gobernanza: versionado (v1.2, v1.3) y trazabilidad de cuándo cambió.

  • Automatización: el agente lee esa política (no la recuerda) y aplica los umbrales de forma consistente.

{
  "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
  }
}

Con policy-as-data, cambiar el límite de LTV o el buffer de stress es cuestión de editar un JSON (y queda auditado).

Trazabilidad: explicar el "por qué" con citas, no con opiniones

Al evaluador y control interno no les basta con "CONDICIONADO": piden pruebas. La salida debe decir:

  • Qué regla se aplicó (p. ej., affordability.pti_max, rate_stress.buffer_pp).

  • Qué documentos sustentan los números (nóminas de junio–agosto, arras, tasación).

  • Qué cambios convierten el caso en APTO (ej., "reducir principal a ≤ €149.700" o "añadir co-solicitante").

{
  "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"
    ]
  }
}

Esto cambia la conversación: ya no es opinar, es demostrar.

Calidad y recencia: checklist que evita reprocesos

Los desacuerdos operativos suelen venir de cosas simples:

  • Una nómina fuera de recencia (política pide 3 meses y llega una de hace 5).

  • Un neto que no cuadra con el abono en extracto (tolerancia, por ejemplo, ±5%).

  • Un préstamo declarado que no aparece en movimientos (o al revés).

Con un checklist programático, el sistema marca antes de decidir: "falta la nómina de agosto", "extracto incompleto", "DTI no considera tarjeta revolving".

Impacto y lo que exige la solución

DolorImpacto¿Qué necesitamos?
PDFs/imágenes desparejosReprocesos, erroresNormalización (moneda, período, fuente) + índice consultable
Cálculos disparesDecisiones inconsistentesMotor determinista de métricas (fuente única)
Política ambiguaCriterios variablesPolicy-as-data (JSON versionado y citable)
Sin explicabilidadAuditoría costosaSalida JSON con reason, issues, conditions y citas
Sin checklistNIGO, ciclo largoValidadores de recencia y coherencia automáticos
Escalabilidad limitadaCuellos de botellaAgentes con tool-use y RAG (automatizar lo repetitivo)

Agentes con herramientas (tool-use) + RAG

  • Tool 1 — get_case_metrics: calcula métricas crudas (inputs para PTI/DTI/LTV/residual) desde los JSON normalizados.

  • Tool 2 — policy_qe: lee la política JSON y devuelve umbrales con citas (qué sección aplicó).

  • Tool 3 — case_qe: recupera pasajes de documentos (p. ej., neto en nómina o valor de tasación) para que el agente pueda citar.

  • Agente: orquesta esas herramientas y entrega una respuesta estructurada: decision, issues, reason, conditions, citations.

  • Motor determinista (en paralelo): asegura consistencia numérica y sirve para tests A/B con cambios de política.

Este reparto evita que el LLM invente números: calcula con tools, lee umbrales desde la política y cita documentos.

Mini-caso en 3 líneas

"Laura" solicita €180k. Con stress +3 pb, la cuota estresada da PTI 42%, DTI post 47% y LTV 84% — todos por encima de la política. El sistema emite CONDICIONADO con acciones concretas: reducir principal a ~€149.7k o tener co-solicitante; y cita tanto las reglas (PTI/DTI/LTV, stress) como las nóminas y la tasación usadas.

¿Por qué usar agentes de IA ahora?

La evaluación hipotecaria exige tres cosas a la vez: consistencia numérica, capacidad de citar fuentes y rapidez para adaptarse a cambios de política. Los agentes actuales, modelos de lenguaje con RAG y uso de herramientas, encajan porque no sustituyen los cálculos ni las reglas: los orquestan. Consultan la política en tiempo real, llaman funciones para calcular y recuperan los pasajes exactos de los documentos. El resultado es una decisión explicable y repetible.

Lo que ha cambiado

  • Uso de herramientas sólido. El modelo invoca funciones por nombre y devuelve salidas estructuradas. Cuando reporta un PTI, ese número sale de tu función de cálculo.

  • RAG fiable. Indexar nóminas, extractos, arras o tasación permite recuperar y citar el fragmento que respalda cada cifra.

  • Política como datos. Los umbrales viven en JSON versionado. Cambiar PTI máximo, LTV o el buffer de estrés es editar datos; el agente los lee al vuelo.

El agente como orquestador

El agente no adivina cifras. Pide umbrales a la política, calcula con funciones y recupera evidencias. Flujo típico:

  • Extrae PTI, DTI, LTV, estrés y definiciones aplicables desde la política, con cita.

  • Calcula ingreso, deudas, cuota estresada y base de LTV con el motor determinista.

  • Recupera los pasajes que sustentan cifras en los documentos del caso.

  • Compone un JSON con decisión, incidencias, explicación, condiciones y citas.

Este reparto separa responsabilidades: cálculo determinista para los números, política como fuente de límites y el modelo para integrar y 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
  }
}

Beneficios tangibles

Ventajas directas: claridad, trazabilidad por versión y agilidad para reflejar cambios de umbrales sin tocar el modelo.

  • Consistencia. Misma entrada, misma salida. Se eliminan discrepancias entre hojas de cálculo.

  • Explicabilidad. Cada ratio incluye cita a política y documento, útil para auditoría y para comunicar condiciones al cliente.

  • Menos reprocesos. Validadores automáticos de recencia y coherencia reducen NIGO y tiempos de ciclo.

  • Evolución simple. Añadir un segmento o ajustar umbrales equivale a extender el JSON y, si hace falta, ajustar el cálculo con cambios localizados.

Mini-snippet orientativo

# 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 referencia

La idea es separar con claridad cuatro responsabilidades: datos, reglas, cálculo y decisión. Con ese diseño, puedes cambiar la política sin tocar el código, rastrear cada cifra hasta su documento y explicar por qué una solicitud sale apta, condicionada o no apta.

Vista general

                         ┌─────────────────────────────┐
                         │   Registro de Política      │
                         │     (JSON versionado)       │
                         └──────────────┬──────────────┘
                                        │ lee/consulta
                                        ▼
┌─────────────────────────────┐   ┌───────────────────────────-──┐
│   Ingesta y Normalización   │   │     Índices semánticos       │
│   (PDF/IMG/CSV → JSON)      │   │  • Caso (documentos)         │
│   • moneda, periodo, fuente │   │  • Política (umbrales)       │
└──────────────┬──────────────┘   └───────────────┬──────────────┘
               │                                  │
               ▼                                  ▼
       ┌───────────────-─┐                ┌───────────────────────┐
       │ get_case_metrics│                │ policy_qe / case_qe   │
       │ Cálculo         │                │ RAG: trae pasajes y   │
       │ determinista    │                │ umbrales con citas    │
       └────────┬────────┘                └────────────┬──────────┘
                │                                      │
                └──────────────┬───────────────────────┘
                               ▼
                      ┌───────────────────┐
                      │  Agente de IA     │
                      │  Orquesta tools   │
                      │  y compone salida │
                      └────────┬──────────┘
                               ▼
                 ┌─────────────────────────────┐
                 │ { decision, issues, reason, │
                 │   conditions, citations }   │
                 └─────────────┬───────────────┘
                               ▼
         Panel analista • CRM/Core • Bitácora de auditoría
  • La política vive como datos versionados.

  • Los documentos del caso se normalizan en JSON con moneda, período y referencia de fuente.

  • Los índices semánticos permiten preguntar y obtener pasajes que pueden citarse.

  • El motor de cálculo aplica fórmulas exactas.

  • El agente usa esas piezas para decidir y explicar, no para inventar números.

Capa de datos: lo justo para trabajar con confianza

Antes de pensar en modelos, nos aseguramos de que cada número tiene contexto. Eso se logra añadiendo metadatos mínimos en la transformación a JSON:

  • Período o fecha (por ejemplo, 2025-08).

  • Cantidad y moneda.

  • doc_type y applicant_id.

  • Referencia de procedencia (archivo, 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
}

Con esa base, cualquier cifra que el sistema utilice puede citarse al detalle.

Índices y recuperación

  • Índice del caso: contiene todos los documentos normalizados. Se usa para recuperar fragmentos que justifican ingresos, deudas, precio o tasación.

  • Índice de política: contiene el JSON de reglas vigente (y versiones anteriores, si se desea). Se usa para recuperar umbrales y definiciones.

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álculo determinista

Esta capa es simple y crítica. Calcula la cuota con anualidad, PTI, DTI, LTV y residual. Aplica el stress de tipo que marque la política. De esta forma, el mismo caso siempre produce las mismas métricas.

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)

Herramientas del agente (tools)

  • get_case_metrics: entrega métricas crudas.

  • policy_qe: trae umbrales y definiciones con fragmentos citables de la política.

  • case_qe: trae pasajes que justifican los números del caso.

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.")

Agente de decisión

El agente sigue siempre la misma secuencia mental:

  • Lee umbrales en la política.

  • Obtiene las métricas calculadas.

  • Recupera evidencia en documentos.

  • Entrega una respuesta estructurada con decisión, incidencias, explicación, condiciones y citas.

Sugerencia práctica: incluir en el prompt el criterio de mapeo de dictamen para que sea consistente con Riesgos. Por ejemplo, apto sin violaciones; condicionado con una o dos violaciones o con soluciones claras; no apto cuando hay varias violaciones críticas o magnitudes muy por encima de los límites.

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.")

Salida y auditoría: la explicación viene de serie

La salida del agente una vez está lista para integrarse en el panel del analista. Incluye lo que Riesgos y Auditoría preguntan con más frecuencia: qué umbral se aplicó, qué documento respalda la cifra y qué cambios convertirían el caso en apto.

Para asegurar formato y evitar sorpresas, valida el JSON con 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)

Además, registra la versión de política aplicada y los logs de herramientas invocadas. Es el hilo de Ariadna de cada decisión.

Un ejemplo de punta a punta en 20 segundos

  • El analista pulsa evaluar.

  • El agente consulta la política vigente: PTI 35, DTI 45, LTV 80, stress más tres puntos con mínimo 5.

  • El motor calcula ingreso, deudas y cuota estresada y devuelve PTI, DTI y LTV.

  • El agente trae los pasajes de nóminas y tasación que justifican esas cifras.

  • La respuesta llega con decisión, motivos, condiciones concretas y citas precisas.

Con este esquema, la solución es predecible para Riesgos, cómoda para el analista y defendible ante Auditoría. Y si mañana cambia el límite de LTV o el buffer de estrés, basta con actualizar el JSON de política: todo lo demás se ajusta solo.

Métricas y reglas clave

En la práctica, la aptitud de una hipoteca se decide con cuatro piezas: la cuota estresada, PTI, DTI, LTV y el ingreso residual. Lo importante no es solo la fórmula, sino cuándo aplicarla, con qué entradas y cómo justificar cada número. A continuación las explicamos de forma ordenada y con un mini-ejemplo que las pone en contexto.

Cuota estresada: el punto de partida

Antes de calcular ratios, hay que fijar una cuota prudente. Para eso se aplica el estrés de tipo que define la política —por ejemplo, sumar tres puntos porcentuales al tipo nominal, con un suelo mínimo del cinco por ciento— y se usa la fórmula de anualidad.

  • Estrés de tipo:

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

  • Cuota mensual (anualidad):

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

    donde P es el principal, r es la tasa mensual y n el número de cuotas.

Esta cuota es la que condiciona el resto: si es alta frente al ingreso, sube PTI y DTI; si baja el principal o sube el ingreso, ambos ratios mejoran de inmediato.

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: esfuerzo de vivienda

PTI mide cuánto del ingreso mensual se destina a la cuota estresada de la hipoteca.

PTI = cuota_estresada / ingreso_neto_mensual

Un límite típico es 35%. Un PTI por encima indica que la carga de vivienda es elevada para el nivel de ingresos declarado.

Si PTI rebasa el umbral, hay tres palancas: reducir el importe del préstamo, alargar plazo (con prudencia) o acreditar más ingreso estable, por ejemplo con un co-solicitante.

DTI: endeudamiento total

DTI integra toda la carga financiera. Se calcula en dos momentos:

  • DTI actual: (otras_deudas + alquiler) / ingreso

  • DTI post: (otras_deudas + cuota_estresada) / ingreso

Si la política permite sustitución de alquiler (compras primera vivienda), el alquiler no se suma en el DTI post. El límite habitual es 45%. Este ratio captura casos en los que el PTI parece aceptable pero el cliente ya arrastra préstamos de consumo o tarjetas.

LTV: prudencia sobre el valor del inmueble

LTV compara el importe del préstamo con el valor de referencia del inmueble, tomando el menor entre precio de compra y tasación.

LTV = principal / min(precio, tasación)

En primera vivienda el límite suele ser 80%. Un LTV alto señala poco colchón de entrada; es típico resolverlo con más aportación o ajustando el importe.

Ingreso residual: margen de seguridad

Lo que queda tras pagar hipoteca y deudas cada mes:

residual = ingreso − (cuota_estresada + otras_deudas)

Se contrasta contra un mínimo vital fijado en política —por ejemplo, 900 euros base más un suplemento por dependiente—. Aporta una visión de capacidad de pago complementaria a PTI y DTI.

Condiciones que convierten un no en un sí

Más allá del dictamen, el sistema debe proponer caminos claros para alcanzar apto. Para eso conviene calcular:

  • Principal máximo por PTI: pago permitido = PTI_max * ingreso; se invierte la anualidad para recuperar el principal compatible.

  • Principal máximo por DTI: pago permitido = DTI_max * ingreso − otras_deudas.

  • Principal máximo por LTV: LTV_max * min(precio, tasación).

  • Ingreso necesario para cumplir PTI o DTI con el importe actual.

La recomendación práctica es tomar el mínimo de esos tres principales máximos y expresarlo en condiciones sencillas: reducir importe hasta X, aportar Y de entrada, o acreditar Z euros más al mes.

Ejemplo simplificado

Caso tipo empleado con datos redondeados:

  • Ingreso neto mensual: 2.510 €

  • Importe solicitado: 180.000 € a 30 años

  • Tipo nominal: 2,8%; estrés: +3 pp con mínimo 5%

  • Otras deudas: 120 €/mes; alquiler actual: 900 €/mes

  • Precio: 215.000 €; tasación: 225.000 €

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

Cálculos clave:

  • Tipo estresado: 5,0%

  • Cuota estresada: 1.056 €/mes

  • PTI: 1.056 / 2.510 = 42,1% → por encima de 35%

  • DTI post (con sustitución de alquiler): (120 + 1.056) / 2.510 = 46,9% → por encima de 45%

  • LTV: 180.000 / 215.000 = 83,7% → por encima de 80%

Consejos:

Reducir principal a ≈ 149.700 € cumple PTI y arrastra DTI a la zona verde. Si se mantiene el importe, una entrada adicional ~8.000 € corrige LTV, pero PTI seguiría alto; habría que sumar ingreso (≈ 508 €/mes) o incorporar co-solicitante.

Implementación (mínima)

El objetivo es montar un MVP que convierta documentos dispersos en una decisión explicable. A continuación tienes los pasos esenciales, con fragmentos de código listos para pegar y adaptar.

Índices para política y caso

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étricas

Centraliza aquí las fórmulas. Es la fuente única de verdad 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
    }

Tools del agente

Tres herramientas bastan: métricas, política y documentos del caso.

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."
    )

Agente de decisión con salida estructurada

Incluye en el sistema el formato de salida y el criterio de dictamen. Evita respuestas libres que sean difíciles de 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ón de la salida y control de errores

Valida el JSON del agente para que backend y panel lo consuman sin sorpresas.

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)

Buenas prácticas adicionales:

  • Limitar tokens de salida y exigir el bloque JSON como respuesta única.

  • Incluir versión de política aplicada y referencias de documentos en logs.

  • Reintentar una vez si la validación falla, manteniendo el mismo contexto.

Seguridad, cumplimiento y calidad

Esta sección aterriza cómo proteger datos, gobernar cambios y garantizar decisiones consistentes. La idea es sencilla: tratar la política como dato versionado, reducir al mínimo la exposición de PII y auditar cada paso que el agente da con herramientas.

Datos personales: lo justo, bien protegido

Principios operativos

  • Minimización. Solo cargar lo necesario para calcular ingresos, deudas, precio, tasación y recencia.

  • Separación. Guardar PII y features de riesgo en almacenes distintos; trabajar con identificadores y referencias.

  • Recencia y retención. Validar vigencia y purgar información fuera de la ventana acordada con Riesgos y Legal.

  • Acceso. Control por roles y registro de accesos; cifrado en tránsito y en reposo.

DatoUso en decisiónRetención razonableObservaciones
DNI y perfilKYC, segmentaciónSegún política KYCEnmascarar en logs
Nóminas o RETAIngreso12–24 mesesGuardar neto mensual y fuente
Extractos bancariosConducta y deudas3–12 mesesDerivar señales; no almacenar texto completo
Arras, compra, tasaciónLTV y valorHasta cierreReferenciar versión y fecha
Préstamos previosDTI actual y postVigencia del préstamoNormalizar a mensual

Gobierno de política y cambios

  • Política en JSON con semántica clara y versionado. Ejemplo de encabezado:

{
  "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 cambios. Propuesta, revisión de Riesgos, aprobación, despliegue con etiqueta de versión.

  • Trazabilidad. Cada evaluación guarda la versión de política aplicada.

Guardrails técnicos para el agente

  • Lista blanca de herramientas. El agente solo puede invocar get_case_metrics, policy_qe y case_qe.

  • Esquema de salida obligatorio. Validar que la respuesta cumple el JSON esperado; rechazar si no se ajusta.

  • Límite de contexto. Indexar y recuperar solo lo necesario, evitando que el LLM vea PII sin motivo.

  • Defensa frente a inyección en documentos. Filtrar y sanear texto de entrada; nunca ejecutar instrucciones embebidas en PDFs o notas.

  • Reintentos controlados. Un único reintento si la validación del esquema falla; registrar ambos intentos.

Auditoría y trazabilidad

Qué registrar por caso

  • Versión de política aplicada y hash del archivo de política.

  • Herramientas invocadas por el agente, parámetros de entrada y fragmentos citados.

  • Identificadores de documentos fuente y sus páginas o selectores.

  • Valores numéricos deterministas calculados y decisión final generada.

Reproducibilidad

  • Conservar los artefactos mínimos: inputs normalizados, versión de política y versión del código del motor determinista.

  • Posibilidad de reejecutar un caso histórico con la política de aquella fecha.

Calidad: pruebas y métricas

Pruebas automáticas

  • Sanidad numérica. Cuota, PTI, DTI, LTV y residual con valores conocidos.

  • Casuística límite. Stress por suelo mínimo, sustitución de alquiler activada o no, LTV con tasación inferior al precio.

  • Reglas de dictamen. Mapeo de violaciones hacia apto, condicionado y no apto.

Evaluación continua

  • Banco de casos de regresión por segmento. Ejecutar tras cada cambio de política o de modelo.

  • Métricas operativas. Tiempo de decisión, tasa de NIGO, porcentaje de decisiones condicionadas que pasan a apto tras ajustes.

  • Alineación determinista–agente. Alertas si la decisión del agente difiere del determinista por fuera de una tolerancia acordada.

Equidad

  • Evitar atributos sensibles en datos de entrada y en características derivadas.

  • Auditorías de sensibilidad con datos sintéticos para detectar sesgos involuntarios en la explicación o en las condiciones propuestas.

Evolución funcional

  • Simulador de escenarios. Permite variar importe, plazo o entrada y ver cómo cambian PTI, DTI y LTV. Es útil para la conversación con el cliente y para formar analistas.

  • Multi-solicitante. Combinar ingresos, deudas y políticas específicas para parejas o co-solicitantes. Ajustar reglas de tenencia mínima e historial por solicitante.

  • Extracción avanzada. Integrar OCR y parsers para PDFs bancarios y nóminas, con validadores de coherencia entre documentos. Reducir el tiempo manual de normalización.

  • Conducta bancaria. Derivar señales de extractos: descubiertos, uso de tarjeta revolving, porcentaje de ingresos en efectivo. Añadir umbrales y alertas en política.

  • Buro y fuentes externas. Conectar con ficheros de morosidad y verificación de identidad. Registrar la evidencia como otro documento citables.

  • Explainability extendida. Agregar puntuaciones por métrica y una explicación más rica orientada a cliente, sin exponer PII innecesaria.

Conclusión: arquitectura y beneficios

La evaluación hipotecaria no es solo calcular ratios. Es coordinar documentos heterogéneos, aplicar políticas que cambian y explicar decisiones con rigor. La arquitectura propuesta separa esas responsabilidades para que el sistema sea predecible para Riesgos, cómodo para el analista y defendible ante Auditoría.

Resuelve:

  • Política como datos: umbrales claros y versionados, listos para citar y ajustar sin tocar código.

  • Cálculo determinista: una única fuente de verdad para cuota estresada, PTI, DTI, LTV y residual.

  • RAG sobre documentos: evidencia recuperable al detalle con referencias al origen.

  • Agente con herramientas: orquesta las piezas, contrasta contra la política y entrega una salida estructurada con decisión, incidencias, explicación, condiciones y citas.

Resultados esperados

  • Consistencia: misma entrada, misma salida, independientemente del analista.

  • Menos NIGO y reprocesos: checklist de recencia y coherencia antes de decidir.

  • Ciclos más cortos: menos búsqueda manual y menos idas y vueltas con el cliente.

  • Explicabilidad inmediata: cada número apunta a su regla y a su documento.

  • Agilidad: cambios de PTI, DTI o LTV se aplican editando el JSON de política.

SOPs y operación diaria

Si ya dispones de SOPs del proceso hipotecario, esta solución los refuerza y los vuelve ejecutables. La idea es que cada paso del SOP tenga una traducción directa en herramientas, validadores y registros.

Cómo mapeamos SOPs a la arquitectura

  • SOP de intake y completitud → checklist programático de documentos y recencia; estado del expediente visible antes de calcular nada.

  • SOP de verificación de ingresos → recuperación de nóminas o RETA con RAG, reglas de coherencia entre nómina y extracto, tolerancias definidas en política.

  • SOP de aplicación de política → policy-as-data; el agente lee umbrales vigentes y los cita en la decisión.

  • SOP de excepciones → flujo de override controlado: quién solicita la excepción, por qué métrica, qué evidencia la respalda y quién la aprueba.

  • SOP de cambios de política → versionado del JSON, revisión por Riesgos y auditoría, pruebas de regresión y despliegue con canary.

  • SOP de auditoría → registro de herramientas invocadas, fragmentos citados, versión de política y valores deterministas usados.

  • SOP de incidentes y rollback → posibilidad de fijar política y modelo por versión, revertir cambios y reejecutar casos históricos.

  • SOP de formación → banco de casos etiquetados, simulador de escenarios y explicaciones generadas por el agente listas para coaching.

Ejemplo de SOP operativo expresado como datos

Esto permite que el panel y el agente sigan el mismo guion de manera consistente:

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

Iniciemos tu proyecto

Te acompañamos con soluciones a medida, desde la idea hasta la implementación.

Contactar
🏆Nominado 2025

AlamedaDev está nominada por sus servicios de Inteligencia Artificial, Desarrollo de Software a Medida y Desarrollo de Aplicaciones Móviles.

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