How to Build a underwriting Agent Using LlamaIndex in Python for fintech
A underwriting agent for fintech takes borrower data, policy rules, and supporting documents, then turns them into a consistent credit decision package: approve, reject, or route to manual review. It matters because underwriting is where you control loss rates, compliance exposure, and turnaround time; if the agent is sloppy, you either approve bad risk or slow down good customers.
Architecture
- •
Data ingestion layer
- •Pulls applicant data from KYC, bank statements, payroll, bureau reports, and internal CRM.
- •Normalizes structured fields and extracts text from PDFs or statements.
- •
Policy and underwriting knowledge base
- •Stores lending policy, risk rules, exception handling, and product eligibility criteria.
- •Indexed with
VectorStoreIndexso the agent can retrieve the right policy clauses.
- •
Risk scoring workflow
- •Combines deterministic checks with LLM-assisted reasoning.
- •Keeps hard rules separate from narrative analysis.
- •
Decision engine
- •Produces one of three outputs:
approve,reject,manual_review. - •Returns a structured rationale for audit and compliance.
- •Produces one of three outputs:
- •
Audit trail store
- •Persists inputs, retrieved policy chunks, model output, and final decision.
- •Required for explainability, dispute handling, and regulator review.
- •
Guardrails layer
- •Enforces PII redaction, residency constraints, and prompt boundaries.
- •Prevents the model from inventing policy or overriding hard rules.
Implementation
1) Install dependencies and load your underwriting documents
Use LlamaIndex to index your lending policy docs and applicant files. In fintech, keep policy documents versioned so every decision can be traced back to the exact rule set in force at decision time.
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.core.settings import Settings
from llama_index.llms.openai import OpenAI
# Configure your LLM
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
# Load underwriting policy docs from a controlled directory
policy_docs = SimpleDirectoryReader(
input_dir="./underwriting_policy_docs"
).load_data()
policy_index = VectorStoreIndex.from_documents(policy_docs)
policy_query_engine = policy_index.as_query_engine(similarity_top_k=3)
2) Define a strict decision schema
Do not let the model return free-form prose as the final artifact. Use a structured response model so downstream systems can validate it before a decision is stored or acted on.
from pydantic import BaseModel
from typing import Literal
class UnderwritingDecision(BaseModel):
decision: Literal["approve", "reject", "manual_review"]
risk_grade: Literal["A", "B", "C", "D"]
rationale: str
key_factors: list[str]
3) Build the underwriting agent with retrieval + structured output
The pattern here is simple: retrieve relevant policy text first, then ask the LLM to score the application against that context. This keeps decisions grounded in current policy instead of model memory.
from llama_index.core.agent.workflow import FunctionAgent
def get_policy_context(applicant_summary: str) -> str:
response = policy_query_engine.query(
f"Retrieve underwriting rules relevant to this applicant:\n{applicant_summary}"
)
return str(response)
def underwrite_application(applicant_summary: str) -> UnderwritingDecision:
context = get_policy_context(applicant_summary)
prompt = f"""
You are an underwriting assistant for a fintech lender.
Use only the provided policy context. Do not invent rules.
If information is insufficient or conflicts with policy, choose manual_review.
POLICY CONTEXT:
{context}
APPLICANT SUMMARY:
{applicant_summary}
Return a decision with:
- decision
- risk_grade
- rationale
- key_factors
"""
result = Settings.llm.predict(prompt)
return UnderwritingDecision.model_validate_json(result)
agent = FunctionAgent(
name="underwriting_agent",
description="Underwrites fintech applications using retrieved policy context.",
tools=[get_policy_context],
)
4) Add an audit record before returning the result
For regulated workflows, every decision needs an immutable trace. Store the applicant snapshot, retrieved context, model output, timestamp, and policy version in your audit store.
import json
from datetime import datetime
def save_audit_record(applicant_id: str,
applicant_summary: str,
context: str,
decision: UnderwritingDecision):
record = {
"applicant_id": applicant_id,
"timestamp_utc": datetime.utcnow().isoformat(),
"applicant_summary": applicant_summary,
"retrieved_policy_context": context,
"decision": decision.model_dump(),
"policy_version": "2026-04",
}
with open(f"./audit/{applicant_id}.json", "w") as f:
json.dump(record, f, indent=2)
# Example usage
summary = """
Applicant has 14 months employment history,
monthly income $6,500,
DTI 31%,
no prior delinquencies,
requested loan amount $8,000.
"""
context = get_policy_context(summary)
decision = underwrite_application(summary)
save_audit_record("app_12345", summary, context, decision)
print(decision.model_dump())
Production Considerations
- •
Keep hard rules outside the LLM
- •DTI thresholds, sanctions hits, minimum income requirements, and residency restrictions should be deterministic code.
- •The LLM should explain outcomes and handle edge cases, not override policy.
- •
Log everything needed for audit
- •Persist retrieved chunks, model version, prompt template version, and final output.
- •Regulators care about reproducibility more than clever prompting.
- •
Control data residency
- •If borrower data must stay in-region, use local vector stores and region-bound inference endpoints.
- •Do not ship raw PII to external services without explicit legal approval and encryption controls.
- •
Add monitoring for drift and exception rates
- •Track approval rate by segment, manual review rate, false positives on rejects, and missing-data frequency.
- •A sudden shift usually means upstream data changed or your retrieval layer is pulling weak context.
Common Pitfalls
- •
Letting the model make final credit decisions from memory
- •Avoid this by grounding every call in retrieved underwriting policies using
VectorStoreIndex. - •The agent should reason over current documents only.
- •Avoid this by grounding every call in retrieved underwriting policies using
- •
Returning unstructured text
- •Free-form responses break downstream automation and auditability.
- •Use a Pydantic schema like
UnderwritingDecisionso validation fails fast when output is malformed.
- •
Mixing compliance logic into prompts
- •Prompts are not controls.
- •Put sanctions screening, affordability thresholds, jurisdiction checks, and PII handling in application code before the LLM runs.
A production underwriting agent is not just an LLM wrapper. It is a controlled workflow that retrieves policy evidence, applies deterministic guardrails where required by regulation, and emits decisions that can survive audit six months later when someone asks why an application was approved or rejected.
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