How to Build a loan approval Agent Using LlamaIndex in Python for banking

By Cyprian AaronsUpdated 2026-04-21
loan-approvalllamaindexpythonbanking

A loan approval agent helps a bank evaluate an application against policy, retrieve supporting evidence from internal documents, and produce a decision recommendation with an audit trail. It matters because lending decisions need to be consistent, explainable, and defensible under compliance review, not just “smart.”

Architecture

  • Application intake layer

    • Accepts borrower data: income, debt, collateral, employment history, requested amount, and jurisdiction.
    • Normalizes the payload into a structured schema before any LLM call.
  • Policy and procedure index

    • Stores underwriting rules, credit policy PDFs, product terms, and exception handling docs.
    • Queried with VectorStoreIndex or SummaryIndex depending on document type.
  • Decision engine

    • Combines deterministic checks with LLM-assisted reasoning.
    • Uses QueryEngine for policy retrieval and a structured output model for final recommendation.
  • Audit logging layer

    • Captures input data, retrieved policy snippets, model output, timestamps, and versioned prompts.
    • Required for model risk management and internal audit.
  • Guardrails and redaction

    • Prevents the agent from using prohibited attributes or leaking sensitive data.
    • Enforces fair lending constraints and PII handling before generation.
  • Human review workflow

    • Routes borderline or high-risk cases to an underwriter.
    • Keeps the agent as a recommendation system, not an autonomous approver.

Implementation

1) Install dependencies and load banking policy documents

Use LlamaIndex to index underwriting policies and product docs. For production, keep the source documents in a controlled repository with versioning so every decision can be traced back to the exact policy set.

pip install llama-index llama-index-llms-openai pydantic
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.llms.openai import OpenAI

# Load policy documents from a controlled directory
docs = SimpleDirectoryReader("./bank_policy_docs").load_data()

# Build the index over underwriting policies
index = VectorStoreIndex.from_documents(docs)

# Create a query engine for retrieval
query_engine = index.as_query_engine(similarity_top_k=3)

llm = OpenAI(model="gpt-4o-mini", temperature=0)

2) Define the loan application schema

Keep the input structured. Banking workflows break when you let free-form text drive core eligibility logic.

from pydantic import BaseModel, Field

class LoanApplication(BaseModel):
    applicant_id: str
    annual_income: float = Field(gt=0)
    monthly_debt: float = Field(ge=0)
    credit_score: int = Field(ge=300, le=850)
    loan_amount: float = Field(gt=0)
    employment_years: float = Field(ge=0)
    state: str
    purpose: str

3) Retrieve policy context and generate a decision recommendation

This pattern uses retrieval for policy grounding and a structured response for downstream systems. The LLM should recommend one of three outcomes: approve, deny, or refer to underwriter.

from typing import Literal
from pydantic import BaseModel

class LoanDecision(BaseModel):
    decision: Literal["approve", "deny", "refer"]
    rationale: str
    key_factors: list[str]
    policy_citations: list[str]

def build_prompt(app: LoanApplication, policy_context: str) -> str:
    return f"""
You are assisting a bank underwriting team.
Use only the provided policy context and application facts.

Application:
{app.model_dump()}

Policy Context:
{policy_context}

Return a concise decision with:
- decision: approve | deny | refer
- rationale
- key_factors
- policy_citations
"""

def evaluate_application(app: LoanApplication) -> LoanDecision:
    query = (
        f"Underwriting rules for {app.purpose} loans in {app.state}. "
        f"Credit score thresholds, debt-to-income limits, employment requirements."
    )
    response = query_engine.query(query)
    prompt = build_prompt(app, str(response))
    result = llm.complete(prompt)

    # In production parse this with strict JSON validation.
    return LoanDecision(
        decision="refer",
        rationale=result.text,
        key_factors=["policy review required", "manual validation needed"],
        policy_citations=["retrieved_policy_snippet_1"]
    )

application = LoanApplication(
    applicant_id="A10021",
    annual_income=95000,
    monthly_debt=1800,
    credit_score=712,
    loan_amount=250000,
    employment_years=6,
    state="CA",
    purpose="home_improvement"
)

decision = evaluate_application(application)
print(decision.model_dump())

4) Add deterministic checks before the LLM

Do not ask the model to calculate what your code can calculate. Debt-to-income ratio, minimum income thresholds, and hard declines should be deterministic.

def dti_ratio(app: LoanApplication) -> float:
    return (app.monthly_debt * 12) / app.annual_income

def hard_rules(app: LoanApplication):
    if app.credit_score < 620:
        return "deny", "Credit score below minimum threshold"
    if dti_ratio(app) > 0.45:
        return "deny", "Debt-to-income ratio above limit"
    if app.loan_amount > app.annual_income * 5:
        return "refer", "Loan size exceeds automated approval band"
    return None, None

def process_application(app: LoanApplication):
    rule_decision, reason = hard_rules(app)
    if rule_decision:
        return {"decision": rule_decision, "reason": reason}

    return evaluate_application(app).model_dump()

Production Considerations

  • Compliance and auditability

    • Store every retrieved chunk ID, prompt version, model version, and final decision.
    • Keep immutable logs so compliance teams can reconstruct why a file was approved or referred.
  • Data residency

    • Keep customer data in-region if your banking jurisdiction requires it.
    • If you use hosted models or vector stores, verify where embeddings and prompts are processed and persisted.
  • Monitoring

    • Track approval rate drift by product line, geography, channel, and credit band.
    • Alert on spikes in “refer” outcomes or sudden changes in denial reasons.
  • Guardrails

    • Block protected-class attributes from prompts and downstream reasoning.
    • Add content filters for PII leakage in generated rationales; never expose internal scorecards or confidential thresholds to applicants.

Common Pitfalls

  • Letting the LLM make numeric decisions

    • Mistake: asking the model to compute DTI or eligibility thresholds from scratch.
    • Fix: calculate all hard underwriting metrics in Python first; use the LLM only for contextual reasoning over retrieved policy text.
  • Skipping retrieval provenance

    • Mistake: returning “policy says no” without storing which document chunk was used.
    • Fix: persist source document IDs, chunk hashes, and timestamps alongside each decision for audit review.
  • Using unstructured prompts for regulated decisions

    • Mistake: passing raw application text into a chat prompt with no schema.
    • Fix: validate inputs with pydantic, enforce typed outputs like LoanDecision, and reject malformed responses before they hit downstream systems.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides