How to Build a underwriting Agent Using LangChain in Python for retail banking

By Cyprian AaronsUpdated 2026-04-21
underwritinglangchainpythonretail-banking

A underwriting agent for retail banking takes a loan application, pulls the relevant customer and product data, evaluates policy rules, and produces a recommendation with an audit trail. It matters because loan decisions need to be fast, consistent, explainable, and compliant with internal credit policy and regulatory controls.

Architecture

  • Application intake layer

    • Accepts structured inputs like income, employment status, existing obligations, requested amount, and product type.
    • Normalizes messy application data before it reaches the model.
  • Policy retrieval layer

    • Uses LangChain retrieval to fetch current underwriting policy, risk thresholds, and exceptions.
    • Keeps decisions aligned with approved bank policy instead of hardcoding rules in prompts.
  • Decision engine

    • Combines deterministic checks with LLM reasoning for narrative explanation.
    • Produces outcomes like approve, decline, or refer_to_human.
  • Audit and evidence store

    • Persists input features, retrieved policy snippets, model output, and final decision.
    • Required for compliance review, dispute handling, and model governance.
  • Guardrail layer

    • Prevents the agent from inventing facts or making unsupported recommendations.
    • Enforces refusal paths when required data is missing or policy is ambiguous.
  • Human review workflow

    • Routes borderline cases to an underwriter or credit officer.
    • Keeps final authority where policy demands it.

Implementation

1. Install LangChain and set up a structured decision schema

Use a Pydantic model so the agent returns a predictable underwriting result. That makes downstream logging and compliance review much easier.

from typing import Literal
from pydantic import BaseModel, Field

class UnderwritingDecision(BaseModel):
    decision: Literal["approve", "decline", "refer_to_human"]
    risk_band: Literal["low", "medium", "high"]
    reason: str = Field(description="Short explanation grounded in policy and application facts")
    missing_info: list[str] = Field(default_factory=list)

2. Load underwriting policy into a retriever

In retail banking you should not rely on the model’s memory for policy. Put current underwriting guidance into documents and retrieve only the relevant sections at runtime.

from langchain_core.documents import Document
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

policy_docs = [
    Document(
        page_content=(
            "Personal loan policy: maximum debt-to-income ratio is 40%. "
            "Applicants with recent delinquencies in the last 12 months require manual review. "
            "Minimum stable employment history is 6 months."
        ),
        metadata={"policy_id": "PL-001", "jurisdiction": "UK"}
    ),
    Document(
        page_content=(
            "If income documentation is missing or inconsistent, refer to human review. "
            "Do not auto-approve when affordability cannot be verified."
        ),
        metadata={"policy_id": "PL-002", "jurisdiction": "UK"}
    ),
]

embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(policy_docs, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})

3. Build the LangChain prompt + structured output chain

This pattern uses ChatPromptTemplate, create_retrieval_chain, and .with_structured_output() so the response stays machine-readable.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains.retrieval import create_retrieval_chain

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

prompt = ChatPromptTemplate.from_messages([
    ("system",
     "You are a retail banking underwriting assistant. "
     "Use only the provided policy context and application facts. "
     "If key information is missing, recommend refer_to_human."),
    ("human",
     """Application:
Applicant: {applicant_name}
Income: {income}
Monthly debt: {monthly_debt}
Employment months: {employment_months}
Recent delinquencies: {recent_delinquencies}
Requested amount: {requested_amount}

Policy context:
{context}

Return a decision that follows policy exactly.""")
])

structured_llm = llm.with_structured_output(UnderwritingDecision)
combine_docs_chain = create_stuff_documents_chain(structured_llm, prompt)
underwriting_chain = create_retrieval_chain(retriever, combine_docs_chain)

4. Run an application through the chain and persist the result

This is where you keep the audit trail. In production you would write this to your case management system or immutable log store.

application = {
    "applicant_name": "Amina Patel",
    "income": "$5,000/month",
    "monthly_debt": "$1,600/month",
    "employment_months": 10,
    "recent_delinquencies": False,
    "requested_amount": "$12,000"
}

result = underwriting_chain.invoke(application)

decision = result["answer"]
print(decision.model_dump())

audit_record = {
    "application": application,
    "retrieved_policy": [doc.page_content for doc in result["context"]],
    "decision": decision.model_dump(),
}

Production Considerations

  • Deploy in-region

    • Keep customer data and retrieved policy documents inside approved regions.
    • Retail banking often has strict data residency requirements; do not ship PII across uncontrolled endpoints.
  • Log everything needed for audit

    • Store input fields, retrieved document IDs, model version, prompt version, and final output.
    • If a decision is challenged later, you need to reconstruct exactly what happened.
  • Add deterministic pre-checks before the LLM

    • Validate required fields like income, debt obligations, employment length, and consent flags.
    • If affordability cannot be calculated reliably, route directly to human review.
  • Monitor drift and override rates

    • Track approval/decline rates by product segment, region, channel, and model version.
    • A spike in overrides usually means policy drift or bad prompt behavior.

Common Pitfalls

  • Using the LLM as the source of truth for credit policy

    • Don’t ask the model to “remember” lending rules.
    • Put policies in retrievable documents or deterministic rule engines so changes are controlled and auditable.
  • Returning free-form text instead of structured output

    • Natural language answers are hard to validate downstream.
    • Use with_structured_output() so your system can enforce schema validation before any decision is stored or acted on.
  • Skipping human review for borderline cases

    • Retail banking underwriting has edge cases that models should not finalize alone.
    • Define explicit referral thresholds for missing documents, thin-file applicants, affordability exceptions, and recent delinquencies.

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