How to Build a loan approval Agent Using LlamaIndex in Python for investment banking
A loan approval agent in investment banking takes a credit package, extracts the relevant facts, checks them against policy and risk rules, and produces a structured recommendation with evidence. It matters because bankers need faster turnaround on deal flow without losing auditability, compliance, or control over who approved what and why.
Architecture
- •
Document ingestion layer
- •Pulls borrower financials, term sheets, KYC files, covenants, and internal policy docs.
- •Normalizes PDFs, DOCX, and text into indexed chunks.
- •
Retrieval layer
- •Uses
VectorStoreIndexto retrieve relevant policy clauses, prior approvals, and credit memo references. - •Keeps the agent grounded in bank-approved source material.
- •Uses
- •
Decision engine
- •Combines retrieved context with a structured prompt that asks for approval, decline, or escalate.
- •Forces output into a schema so downstream systems can process it.
- •
Risk and compliance guardrails
- •Applies hard checks for missing KYC, sanctions flags, concentration limits, LTV/DSCR thresholds, and policy exceptions.
- •Prevents the model from making unsupported credit decisions.
- •
Audit logging layer
- •Stores input documents, retrieved nodes, model output, timestamps, and reviewer actions.
- •Supports model risk management and internal audit.
Implementation
1) Load policy documents and borrower files
Use LlamaIndex to ingest the bank’s credit policy and the borrower package. In production you would point this at an approved data store with residency controls; here we keep it local for clarity.
from llama_index.core import SimpleDirectoryReader
policy_docs = SimpleDirectoryReader(
input_dir="./data/policy",
required_exts=[".pdf", ".docx", ".txt"]
).load_data()
borrower_docs = SimpleDirectoryReader(
input_dir="./data/borrower",
required_exts=[".pdf", ".docx", ".txt"]
).load_data()
2) Build indexes for retrieval
For loan approval you want retrieval over both policy and deal materials. A single index works for small setups; in practice I usually keep policy and deal indexes separate so I can trace which source drove each answer.
from llama_index.core import VectorStoreIndex
policy_index = VectorStoreIndex.from_documents(policy_docs)
borrower_index = VectorStoreIndex.from_documents(borrower_docs)
policy_retriever = policy_index.as_retriever(similarity_top_k=3)
borrower_retriever = borrower_index.as_retriever(similarity_top_k=5)
3) Add a structured decision function
The agent should not freewheel into a narrative response. Force it to return a clear recommendation with rationale tied to evidence from retrieved nodes.
from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
from pydantic import BaseModel, Field
from typing import Literal
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
class LoanDecision(BaseModel):
recommendation: Literal["approve", "decline", "escalate"] = Field(...)
rationale: str = Field(...)
key_risks: list[str] = Field(default_factory=list)
missing_items: list[str] = Field(default_factory=list)
def build_loan_prompt(query: str, policy_ctx: str, borrower_ctx: str) -> str:
return f"""
You are a credit analyst for an investment bank.
Use only the provided context.
Return a decision for the loan request as approve, decline, or escalate.
Loan request:
{query}
Policy context:
{policy_ctx}
Borrower context:
{borrower_ctx}
Rules:
- If KYC/sanctions data is missing, escalate.
- If any key financial metric violates policy thresholds, decline or escalate.
- Cite concrete evidence from the context.
"""
4) Retrieve context and generate the final decision
This is the core pattern: retrieve grounded context first, then pass that context into the LLM. The model should never infer from memory when making a credit recommendation.
def run_loan_review(query: str) -> LoanDecision:
policy_nodes = policy_retriever.retrieve(query)
borrower_nodes = borrower_retriever.retrieve(query)
policy_ctx = "\n\n".join([n.node.get_content() for n in policy_nodes])
borrower_ctx = "\n\n".join([n.node.get_content() for n in borrower_nodes])
prompt = build_loan_prompt(query=query,
policy_ctx=policy_ctx,
borrower_ctx=borrower_ctx)
llm = Settings.llm
response = llm.complete(prompt)
# Parse manually or with your own validation layer
text = response.text.lower()
if "approve" in text:
rec = "approve"
elif "decline" in text:
rec = "decline"
else:
rec = "escalate"
return LoanDecision(
recommendation=rec,
rationale=response.text,
key_risks=[],
missing_items=[]
)
decision = run_loan_review(
"Review this senior secured term loan for a mid-market sponsor-backed borrower."
)
print(decision.model_dump())
If you want a stronger production pattern, wrap this with explicit pre-checks before calling the LLM:
- •sanctions screening status present
- •KYC complete
- •financial statements within approved date range
- •covenant calculations available
- •exposure limits not exceeded
That keeps obvious rejects out of the model path.
Production Considerations
- •
Deploy inside your bank’s controlled environment
- •Keep document storage and vector stores in-region to satisfy data residency requirements.
- •Avoid sending borrower PII or confidential deal terms to unmanaged external services.
- •
Log every decision path
- •Persist retrieved node IDs, source document names, prompt version, model version, and final recommendation.
- •This is mandatory for auditability and model risk review.
- •
Add hard guardrails before generation
- •Enforce deterministic checks for KYC completion, sanctions clearance, exposure caps, leverage ratios, DSCR/LTV thresholds.
- •The agent should escalate when inputs are incomplete instead of guessing.
- •
Monitor drift by deal type
- •Track approval rates by sector, geography, sponsor type, and ticket size.
- •A sudden shift often means retrieval quality degraded or policy documents changed without reindexing.
Common Pitfalls
- •
Letting the LLM make ungrounded credit calls
- •Fix it by retrieving from approved sources first and constraining outputs to structured decisions only.
- •
Mixing public embeddings with confidential banking data
- •Fix it by using an approved embedding/model stack with clear data handling terms and regional controls.
- •
Skipping exception handling
- •Fix it by routing missing KYC, stale financials, sanctions ambiguity, or conflicting covenants to human review instead of auto-decisioning.
- •
Ignoring source traceability
- •Fix it by storing which retrieved chunks influenced each recommendation so compliance can reconstruct the decision later.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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