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

By Cyprian AaronsUpdated 2026-04-21
underwritinglangchainpythonbanking

An underwriting agent in banking takes borrower data, policy rules, and supporting documents, then produces a structured credit recommendation: approve, decline, or escalate for manual review. It matters because underwriting is where banks control credit risk, compliance exposure, and decision consistency; automating the first pass saves analyst time without removing human oversight.

Architecture

  • Document ingestion layer

    • Pulls application forms, bank statements, KYC records, income proofs, and internal policy docs.
    • Normalizes PDFs, text, and structured JSON into a single input format.
  • Policy retrieval layer

    • Uses langchain_community.vectorstores.Chroma or another retriever-backed store to fetch the relevant underwriting policy sections.
    • Keeps decisions grounded in current bank policy instead of model memory.
  • Reasoning and extraction layer

    • Uses an LLM chain to extract key fields like income stability, debt obligations, LTV, DTI, and red flags.
    • Produces structured output with PydanticOutputParser so downstream systems can validate it.
  • Decision engine

    • Applies deterministic rules for threshold checks: minimum income, max DTI, blacklist hits, missing documents.
    • Combines rule outcomes with LLM analysis to produce approve / refer / decline.
  • Audit and trace layer

    • Stores prompts, retrieved policy snippets, model outputs, and final decision payloads.
    • Required for model governance, explainability, and regulatory review.
  • Human review handoff

    • Routes borderline cases to an underwriter with a concise evidence pack.
    • Prevents the agent from making unsupported final decisions on high-risk files.

Implementation

1) Install the core dependencies

Use LangChain’s current split packages. For this pattern you need the core library plus OpenAI integration and a vector store for policy retrieval.

pip install langchain langchain-openai langchain-community chromadb pydantic

Set your API key before running anything:

export OPENAI_API_KEY="your-key"

2) Define a structured underwriting output

For banking workflows, free-form text is a bad interface. Use a Pydantic schema so every response has predictable fields for audit logs and downstream orchestration.

from typing import Literal, List
from pydantic import BaseModel, Field

class UnderwritingDecision(BaseModel):
    decision: Literal["approve", "refer", "decline"]
    risk_score: int = Field(ge=0, le=100)
    reasons: List[str]
    missing_documents: List[str]
    policy_references: List[str]

3) Build the LangChain pipeline

This example retrieves relevant policy text from Chroma, feeds it plus applicant data into an LLM chain built with ChatPromptTemplate, and parses the result with PydanticOutputParser.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

# Policy store setup (replace with your bank's approved corpus)
embeddings = OpenAIEmbeddings()
vectorstore = Chroma(
    collection_name="underwriting_policy",
    embedding_function=embeddings,
    persist_directory="./chroma_underwriting"
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

parser = PydanticOutputParser(pydantic_object=UnderwritingDecision)

prompt = ChatPromptTemplate.from_messages([
    ("system",
     "You are a banking underwriting assistant. "
     "Use only the provided policy context and applicant facts. "
     "If data is missing or policy is unclear, choose refer."),
    ("human",
     "Applicant facts:\n{application}\n\n"
     "Policy context:\n{policy_context}\n\n"
     "{format_instructions}")
])

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

def underwrite(application: str) -> UnderwritingDecision:
    docs = retriever.get_relevant_documents(application)
    policy_context = "\n\n".join([doc.page_content for doc in docs])

    chain_input = {
        "application": application,
        "policy_context": policy_context,
        "format_instructions": parser.get_format_instructions(),
    }

    messages = prompt.format_messages(**chain_input)
    response = llm.invoke(messages)
    return parser.parse(response.content)

result = underwrite(
    "Borrower: Jane Doe. Income: $120k salaried. Existing debt: $2k/month. "
    "Requested loan: $300k mortgage. Property value: $500k. Missing payslip."
)

print(result.model_dump())

4) Add deterministic guardrails before finalizing the decision

LLMs should not be the only control point. Add hard checks for missing mandatory documents and obvious ratio breaches before you accept any model recommendation.

def hard_rules(application_data: dict) -> list[str]:
    issues = []

    required_docs = ["id_document", "proof_of_income", "bank_statement"]
    for doc in required_docs:
        if not application_data.get(doc):
            issues.append(f"missing_{doc}")

    income = application_data.get("monthly_income", 0)
    debt = application_data.get("monthly_debt", 0)
    requested_payment = application_data.get("estimated_monthly_payment", 0)

    if income > 0:
        dti = (debt + requested_payment) / income
        if dti > 0.45:
            issues.append("dti_above_threshold")

    if application_data.get("sanctions_hit"):
        issues.append("sanctions_match")

    return issues

def finalize_decision(application_text: str, application_data: dict):
    issues = hard_rules(application_data)
    llm_result = underwrite(application_text)

    if "sanctions_match" in issues:
        return {"decision": "decline", "reason": "compliance_block"}

    if len(issues) > 0 or llm_result.decision == "refer":
        return {"decision": "refer", "issues": issues, "llm": llm_result.model_dump()}

    return {"decision": llm_result.decision, "llm": llm_result.model_dump()}

Production Considerations

  • Keep data residency explicit

    • Banking data often cannot leave approved regions.
    • Pin your model endpoint and vector store to compliant infrastructure in-region; do not send raw PII to uncontrolled services.
  • Log everything needed for audit

    • Store input hashes, retrieved policy document IDs, prompt versions, model version, output schema version, and final decision.
    • Regulators care about why a decision was made months later.
  • Use human-in-the-loop on borderline cases

    • Auto-decline or auto-refer based on deterministic thresholds.
    • Reserve approvals for low-risk files that pass both rules and model checks cleanly.
  • Monitor drift on underwriting features

    • Track approval rates by segment, missing-document frequency, false referrals, and override rates by analysts.
    • If override rates spike after a prompt or policy update, roll back fast.

Common Pitfalls

  1. Letting the LLM make final credit decisions

    • Avoid this by using the LLM for extraction and explanation only.
    • Final approval logic should stay in deterministic code owned by risk/compliance teams.
  2. Retrieving stale policy content

    • If your vector store contains old lending rules, your agent will cite outdated thresholds.
    • Version your policy corpus and rebuild embeddings whenever underwriting rules change.
  3. Ignoring explainability requirements

    • A plain-text answer like “looks good” is useless in banking.
    • Force structured output with reasons and cited policy references so analysts can review every recommendation quickly.
  4. Mixing sensitive data into prompts without controls

    • Strip unnecessary PII before sending text to the model.
    • Mask account numbers, national IDs, and full addresses unless they are required for the specific check being performed.

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