In the insurance industry, the most critical events are often the rarest. Fraudulent claims, catastrophic natural disasters, or high-value complex litigation occur in less than 1% of cases. This is Class Imbalance. If you train a standard Machine Learning model on this data, it will achieve 99% accuracy by simply predicting "No Fraud" for every single claim. While the accuracy looks great, the model is useless—it misses every single fraud case.

In an enterprise setting, we don't just fix this with a line of code; we build a Multi-Agent System that combines statistical rebalancing, semantic RAG retrieval, and human-in-the-loop validation to ensure no high-risk event slips through the cracks. In this end-to-end guide, we will build an Insurance Risk Assessment Engine using LangGraph that specifically addresses class imbalance through a "Triangulation Strategy."

The Real-World Use Case: InsureTech’s "High-Severity Claim" Detector

Imagine you are building an AI system for InsureTech, a major property and casualty insurer. The system receives 100,000 claims daily.

  • 99,500 are routine (minor fender benders, small water damage).

  • 500 are "High-Severity" (potential arson, complex liability, or organized fraud rings).

The Imbalance Challenge

A standard LLM or RAG system will be biased toward the "routine" patterns found in its training data and vector database. It might look at a suspicious $50,000 fire claim and classify it as "Routine" because 99% of fire claims in its memory are routine.

The Solution: The Triangulation Agent

We will build a LangGraph workflow with three specialized agents designed to counteract this bias:

  1. The Statistical Agent: Uses traditional ML techniques (SMOTE/Undersampling logic) to flag anomalies based on numerical data.

  2. The Semantic Agent (RAG): Retrieves only rare, high-severity historical cases from a specialized "Rare Events" vector index, forcing the LLM to compare the current claim against other fraud cases, not routine ones.

  3. The Adversarial Agent: Acts as a "Devil’s Advocate," explicitly trying to find reasons why the claim might be fraudulent, countering the LLM's natural bias toward safety.

Technology Stack

ComponentTechnologyRole in Imbalance Management
OrchestrationLangGraphManages the multi-agent triangulation workflow.
LLM ProviderAzure OpenAI (GPT-4o)Powers the semantic reasoning and adversarial checks.
Vector Databasepgvector (PostgreSQL)Stores two separate indexes: "Routine Claims" and "Rare/Fraud Cases".
ML LibraryScikit-Learn / XGBoostHandles the statistical anomaly detection and SMOTE logic.
State ManagementPydanticDefines the strict schema for risk scores and agent findings.
ObservabilityLangSmithTraces how each agent contributes to the final risk score.
459

End-to-End Implementation

Let's build the LangGraph system that fights class imbalance by forcing the system to look for the "needle in the haystack."

Step 1: Define the Enterprise State

Our state needs to track the claim details, the findings from each specialized agent, and a consolidated risk score.

from typing import TypedDict, List, Annotated, Optionalimport operator
from langgraph.graph import StateGraph, END

class RiskAssessmentState(TypedDict):
    # Memory: Conversation/Action history
    messages: Annotated[List[str], operator.add]
    
    # Input Data
    claim_details: dict
    
    # Agent Findings
    statistical_anomaly_score: float # 0.0 to 1.0
    semantic_risk_context: List[str] # Retrieved rare cases
    adversarial_flags: List[str] # Reasons to suspect fraud
    
    # Final Output
    final_risk_category: str # "Low", "Medium", "High", "Critical"
    recommended_action: str

Step 2: Build the Triangulation Agents

Node 1: The Statistical Agent (Handling Numerical Imbalance)

This agent simulates a model trained with SMOTE (Synthetic Minority Over-sampling Technique) or Class Weights. It doesn't rely on language; it relies on mathematical deviation from the norm.

def statistical_agent(state: RiskAssessmentState) -> RiskAssessmentState:
    print("📊 [Statistical Agent] Running anomaly detection on numerical features...")
    
    # Simulating a model that has been re-balanced using SMOTE
    # In a real app, this would call an XGBoost model trained with scale_pos_weight
    claim_amount = state["claim_details"].get("amount", 0)
    
    # Simple heuristic for demonstration: High amount = Higher anomaly score
    if claim_amount > 20000:
        anomaly_score = 0.85
    elif claim_amount > 5000:
        anomaly_score = 0.45
    else:
        anomaly_score = 0.10
        
    return {
        "statistical_anomaly_score": anomaly_score,
        "messages": [f"Statistical Agent: Calculated anomaly score of {anomaly_score}."]
    }

Node 2: The Semantic Agent (Handling Contextual Imbalance via RAG)

This is the key to fixing LLM bias. Instead of searching the entire database, this agent queries a specialized "Rare Events" Index. This forces the LLM to see similar fraud cases, not similar routine cases.

def semantic_agent(state: RiskAssessmentState) -> RiskAssessmentState:
    print("🔍 [Semantic Agent] Querying 'Rare Events' Vector Index...")
    
    # Simulating RAG retrieval from a balanced index of only high-severity/fraud cases
    # This counters the "Majority Class Bias" by providing minority-class context
    rare_case_contexts = [
        "Case #9921: $25k fire claim where accelerant was found in the basement. Ruled as Arson.",
        "Case #8843: $22k water damage claim filed 2 days after policy inception. Ruled as Fraud."
    ]
    
    return {
        "semantic_risk_context": rare_case_contexts,
        "messages": ["Semantic Agent: Retrieved 2 similar high-severity historical cases."]
    }

Node 3: The Adversarial Agent (Countering Confirmation Bias)

LLMs are "people pleasers." They want to give a safe answer. This agent is prompted to be skeptical and look for red flags, effectively acting as a human underwriter's "gut check."

def adversarial_agent(state: RiskAssessmentState) -> RiskAssessmentState:
    print("😈 [Adversarial Agent] Playing Devil's Advocate...")
    
    flags = []
    details = state["claim_details"]
    
    # Simulating adversarial logic
    if details.get("amount", 0) > 20000:
        flags.append("Claim amount is in the top 1% of all policies.")
    if "fire" in details.get("type", "").lower():
        flags.append("Fire claims have a statistically higher fraud rate than other perils.")
        
    return {
        "adversarial_flags": flags,
        "messages": [f"Adversarial Agent: Identified {len(flags)} potential risk indicators."]
    }

Node 4: The Consolidator (Weighted Decision Making)

This agent combines the findings. Because we know the data is imbalanced, we weight the "High Risk" signals more heavily.

def consolidator_agent(state: RiskAssessmentState) -> RiskAssessmentState:
    print("⚖️ [Consolidator] Synthesizing triangulated risk assessment...")
    
    stat_score = state["statistical_anomaly_score"]
    has_rare_context = len(state["semantic_risk_context"]) > 0
    has_flags = len(state["adversarial_flags"]) > 0
    
    # Weighted Logic to counteract imbalance
    # If statistical score is high AND we found similar rare cases, risk is Critical
    if stat_score > 0.7 and has_rare_context:
        category = "Critical"
        action = "Immediate referral to Special Investigations Unit (SIU)."
    elif stat_score > 0.4 or has_flags:
        category = "High"
        action = "Manual review by Senior Underwriter required."
    else:
        category = "Low"
        action = "Auto-approve for payment."
        
    return {
        "final_risk_category": category,
        "recommended_action": action,
        "messages": ["Consolidator: Final risk categorization complete."]
    }

Step 3: Compile the Graph

def build_risk_graph():
    workflow = StateGraph(RiskAssessmentState)

    workflow.add_node("statistical", statistical_agent)
    workflow.add_node("semantic", semantic_agent)
    workflow.add_node("adversarial", adversarial_agent)
    workflow.add_node("consolidator", consolidator_agent)

    workflow.set_entry_point("statistical")
    
    # Parallel execution for speed
    workflow.add_edge("statistical", "semantic")
    workflow.add_edge("semantic", "adversarial")
    workflow.add_edge("adversarial", "consolidator")
    workflow.add_edge("consolidator", END)

    return workflow.compile()

app = build_risk_graph()

Running the System

Let's test a high-value, suspicious claim that would typically be misclassified by a naive model.

initial_state = {
    "messages": [],
    "claim_details": {"amount": 25000, "type": "Fire Damage", "policy_age_days": 15},
    "statistical_anomaly_score": 0.0,
    "semantic_risk_context": [],
    "adversarial_flags": [],
    "final_risk_category": "",
    "recommended_action": ""
}

result = app.invoke(initial_state)

print("\n--- Agent Triangulation Log ---")
for msg in result["messages"]:
    print(f"• {msg}")

print(f"\n--- Final Risk Assessment ---")
print(f"Category: {result['final_risk_category']}")
print(f"Action: {result['recommended_action']}")

Output Trace

📊 [Statistical Agent] Running anomaly detection on numerical features...
🔍 [Semantic Agent] Querying 'Rare Events' Vector Index...
😈 [Adversarial Agent] Playing Devil's Advocate...
⚖️ [Consolidator] Synthesizing triangulated risk assessment...

--- Agent Triangulation Log ---
• Statistical Agent: Calculated anomaly score of 0.85.
• Semantic Agent: Retrieved 2 similar high-severity historical cases.
• Adversarial Agent: Identified 2 potential risk indicators.
• Consolidator: Final risk categorization complete.

--- Final Risk Assessment ---
Category: Critical
Action: Immediate referral to Special Investigations Unit (SIU).

Notice how the system didn't just look at the amount. It used the Semantic Agent to find similar rare fraud cases, and the Adversarial Agent to highlight the risk, ensuring the "Critical" classification wasn't drowned out by the 99% of routine data.

Enterprise Best Practices for Class Imbalance

  1. Specialized Vector Indices: Don't just have one big vector database. Create a "Minority Class Index" containing only your rare, high-value, or fraudulent cases. Force your RAG agent to query this index when certain thresholds are met.

  2. Adversarial Prompting: Always include an agent or a prompt step that explicitly asks, "What are the reasons this might be a false negative?" This counteracts the LLM's tendency to default to the majority class.

  3. Human-in-the-Loop for Edge Cases: Use LangGraph's interrupt() feature. If the Statistical Agent and Semantic Agent disagree (e.g., low statistical score but high semantic similarity to fraud), pause the graph and route it to a human underwriter.

  4. Monitor Precision/Recall, Not Accuracy: In your LangSmith traces, track how many "Critical" flags actually result in confirmed fraud. In imbalanced datasets, a high recall (catching all fraud) is often more valuable than high precision, even if it means more manual reviews.

Conclusion

Managing class imbalance in enterprise AI isn't just about tweaking hyperparameters in a Python script; it's about architecting a system that respects the rarity of critical events. By using LangGraph to orchestrate a Triangulation Strategy—combining statistical anomaly detection, specialized "Rare Event" RAG, and adversarial reasoning—you ensure that your insurance risk model doesn't just follow the crowd. It identifies the outliers, protecting the company from the 1% of cases that cause 90% of the losses.