How to Build a loan approval Agent Using LangChain in Python for investment banking
A loan approval agent in investment banking takes a borrower’s application, pulls in structured and unstructured data, checks policy constraints, scores risk, and produces a decision package for a human credit officer. It matters because banks need faster turnaround without losing control over compliance, auditability, and capital-risk discipline.
Architecture
- •
Application intake layer
- •Normalizes borrower data from CRM, LOS, PDFs, and internal APIs.
- •Validates required fields before any model call.
- •
Document retrieval layer
- •Pulls policy docs, term sheets, KYC notes, covenant history, and prior credit memos.
- •Uses
VectorStoreRetrieveror a retriever built fromChroma,FAISS, or your bank-approved store.
- •
Decisioning chain
- •Combines rules + LLM reasoning.
- •Produces a structured recommendation: approve, reject, or escalate.
- •
Risk and compliance guardrails
- •Enforces hard constraints like exposure limits, jurisdiction rules, sanctions flags, and missing-doc checks.
- •Keeps the LLM out of final authority on regulated decisions.
- •
Audit and traceability layer
- •Stores prompts, retrieved sources, outputs, timestamps, model version, and reviewer identity.
- •Required for model governance and internal audit.
- •
Human review interface
- •Routes borderline cases to a credit analyst or underwriter.
- •Captures overrides with reason codes.
Implementation
1) Install the right LangChain packages
Use the split packages. For most production builds you’ll want langchain, langchain-openai, and a vector store package such as langchain-chroma.
pip install langchain langchain-openai langchain-chroma pydantic
Set your model key through environment variables:
export OPENAI_API_KEY="your-key"
2) Define the decision schema and build the retrieval context
For investment banking workflows, don’t return free-form prose. Force a structured output that downstream systems can validate.
from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.documents import Document
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
class LoanDecision(BaseModel):
decision: Literal["approve", "reject", "escalate"] = Field(...)
risk_rating: Literal["low", "medium", "high"] = Field(...)
rationale: str = Field(...)
key_conditions: list[str] = Field(default_factory=list)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
docs = [
Document(page_content="Policy: Debt service coverage ratio must be >= 1.25 for unsecured corporate loans."),
Document(page_content="Policy: Any sanctioned counterparty requires immediate rejection."),
Document(page_content="Policy: Loans above $25M require escalation to senior credit committee."),
]
vectorstore = Chroma.from_documents(docs, embedding=embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
3) Create the LangChain pipeline with structured output
The pattern here is simple: retrieve policy context, feed it into a prompt, then force typed output with with_structured_output().
prompt = ChatPromptTemplate.from_messages([
("system", "You are a loan approval assistant for an investment bank. "
"Follow policy exactly. Never ignore sanctions or exposure limits."),
("human", """
Borrower profile:
- Name: {name}
- Amount requested: ${amount}
- DSCR: {dscr}
- Sanctions flag: {sanctions_flag}
- Existing exposure: ${existing_exposure}
Relevant policy:
{context}
Return a decision based only on the profile and policy.
""")
])
def retrieve_context(inputs):
query = f"loan amount {inputs['amount']} DSCR {inputs['dscr']} sanctions {inputs['sanctions_flag']}"
retrieved = retriever.invoke(query)
return {"context": "\n".join(doc.page_content for doc in retrieved), **inputs}
decision_chain = (
retrieve_context
| prompt
| llm.with_structured_output(LoanDecision)
)
result = decision_chain.invoke({
"name": "Acme Manufacturing",
"amount": 18000000,
"dscr": 1.31,
"sanctions_flag": False,
"existing_exposure": 4000000,
})
print(result.model_dump())
4) Add hard rules before the model makes any recommendation
LLMs should not decide obvious rejects. Put deterministic controls in front of the chain so compliance rules are enforced even if the model drifts.
def precheck(application):
if application["sanctions_flag"]:
return LoanDecision(
decision="reject",
risk_rating="high",
rationale="Counterparty sanctions flag detected.",
key_conditions=[]
)
if application["amount"] > 25000000:
return LoanDecision(
decision="escalate",
risk_rating="medium",
rationale="Exposure exceeds delegated authority threshold.",
key_conditions=["Senior credit committee review"]
)
return None
application = {
"name": "Acme Manufacturing",
"amount": 18000000,
"dscr": 1.31,
"sanctions_flag": False,
"existing_exposure": 4000000,
}
hard_stop = precheck(application)
if hard_stop:
print(hard_stop.model_dump())
else:
print(decision_chain.invoke(application).model_dump())
Production Considerations
- •
Audit logging
- •Persist every input payload, retrieved document IDs, model version, output schema version, and final human override.
- •In investment banking this is non-negotiable for model risk management and regulatory review.
- •
Data residency
- •Keep borrower data inside approved regions and approved vendors.
- •If your bank has jurisdictional restrictions, route requests through region-bound inference endpoints or private deployment.
- •
Guardrails
- •Add rule-based prechecks for sanctions, exposure caps, missing KYC artifacts, and restricted industries.
- •Use schema validation so malformed outputs never reach underwriting systems.
- •
Monitoring
- •Track approval rates by segment, escalation rates, override rates by analyst team, latency p95, and retrieval hit quality.
- •Watch for drift in policy citations; if the agent stops referencing current credit policy correctly, freeze deployment.
Common Pitfalls
- •
Letting the LLM make final credit decisions
- •Fix it by using deterministic policy checks first and treating the LLM as a recommendation engine.
- •Final approval should stay with an authorized human or a separate governed system.
- •
Returning unstructured text
- •Fix it with
with_structured_output()and a Pydantic schema likeLoanDecision. - •Free text is hard to validate, hard to audit, and dangerous in regulated workflows.
- •Fix it with
- •
Mixing public context with sensitive borrower data
- •Fix it by isolating embeddings stores per tenant or region and redacting PII before retrieval when possible.
- •Investment banking teams need strict controls around client confidentiality, data residency, and retention policies.
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