How to Build a compliance checking Agent Using LangChain in Python for fintech
A compliance checking agent reviews fintech content, transactions, or customer communications against policy rules before anything goes live. It matters because one missed sanction screen, one prohibited phrase in a loan message, or one data residency violation can turn into regulatory exposure, fines, and audit pain.
Architecture
- •Input adapter
- •Accepts the artifact to check: payment memo, customer email, onboarding form text, KYC summary, or support reply.
- •Policy retriever
- •Pulls the relevant controls from a versioned policy store: AML, KYC, sanctions, marketing disclaimers, privacy rules.
- •Compliance analyzer
- •Uses an LLM chain to classify issues and explain why something violates policy.
- •Deterministic rule layer
- •Handles hard checks like country restrictions, prohibited terms, PII leakage patterns, and required disclaimer presence.
- •Audit logger
- •Stores input hash, policy version, model output, decision reason, timestamp, and reviewer override.
- •Decision API
- •Returns
approved,needs_review, orrejectedwith structured reasons for downstream systems.
- •Returns
Implementation
1) Install and define the policy schema
Use LangChain’s structured output so your agent returns machine-readable compliance decisions. For fintech workflows, that is non-negotiable because auditors do not want free-form prose.
from typing import List
from pydantic import BaseModel, Field
class ComplianceFinding(BaseModel):
rule_id: str = Field(description="Policy rule identifier")
severity: str = Field(description="low | medium | high")
explanation: str = Field(description="Why this is a problem")
remediation: str = Field(description="How to fix it")
class ComplianceResult(BaseModel):
decision: str = Field(description="approved | needs_review | rejected")
findings: List[ComplianceFinding]
summary: str
2) Build the LangChain chain with structured output
This example uses ChatOpenAI, ChatPromptTemplate, and RunnableLambda. The deterministic check runs first, then the LLM evaluates policy context and produces structured output.
import re
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda
POLICY_TEXT = """
You are a compliance reviewer for a fintech company.
Flag:
- Sanctions-risk countries
- Promises of guaranteed returns
- Missing risk disclosures in investment language
- PII leakage such as full card numbers or SSNs
Return only structured findings.
"""
def deterministic_checks(text: str):
issues = []
if re.search(r"\b\d{16}\b", text):
issues.append({"rule_id": "PII-CARD", "severity": "high",
"explanation": "Possible full card number detected",
"remediation": "Mask card data before processing"})
if re.search(r"\b(guaranteed return|risk-free profit)\b", text.lower()):
issues.append({"rule_id": "MKT-GUARANTEE", "severity": "high",
"explanation": "Prohibited investment guarantee language",
"remediation": "Remove performance guarantees"})
return issues
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
structured_llm = llm.with_structured_output(ComplianceResult)
prompt = ChatPromptTemplate.from_messages([
("system", POLICY_TEXT),
("user", "Review this content for compliance:\n\n{text}\n\nKnown deterministic issues: {issues}")
])
chain = (
RunnableLambda(lambda x: {
"text": x["text"],
"issues": deterministic_checks(x["text"])
})
| prompt
| structured_llm
)
result = chain.invoke({
"text": "We guarantee risk-free profit on this investment. Call us with your card number 4242424242424242."
})
print(result.model_dump())
3) Add retrieval for versioned policies
In production you should not hardcode policy text. Use a retriever backed by your internal policy docs so legal can update rules without redeploying code. RetrievalQA exists in older stacks, but the cleaner pattern in current LangChain is composing a retriever into your chain with RunnablePassthrough or direct retrieval calls.
A practical pattern is:
- •store policies in a vector store with metadata like
jurisdiction,product,version - •retrieve only the applicable policy set for the request
- •pass those snippets into the prompt as grounded context
That keeps decisions tied to the exact policy version used at runtime.
4) Return an auditable decision object
Your API should persist every decision with enough context for reconstruction later. That means request ID, policy version, model name, retrieved documents, deterministic flags, and final result.
from datetime import datetime
import hashlib
import json
def audit_record(input_text: str, result: ComplianceResult):
payload = {
"input_hash": hashlib.sha256(input_text.encode()).hexdigest(),
"timestamp": datetime.utcnow().isoformat(),
"model": "gpt-4o-mini",
"decision": result.decision,
"summary": result.summary,
"findings": [f.model_dump() for f in result.findings],
}
return json.dumps(payload)
record = audit_record("sample fintech text", result)
print(record)
Production Considerations
- •Deploy with jurisdiction routing
- •If you operate across EU, UK, and US markets, route requests to region-specific inference endpoints and keep logs inside the correct data residency boundary.
- •Version every policy pack
- •Store the exact policy document version used for each decision. That is what makes audits reproducible when regulations change.
- •Monitor false positives and reviewer overrides
- •Track how often analysts override
needs_revieworrejected. High override rates usually mean your prompts are too strict or your rules are stale.
- •Track how often analysts override
- •Add hard guardrails before the LLM
- •Block obvious violations like raw PANs, SSNs, sanctioned geographies, and unsupported languages before sending data to the model.
Common Pitfalls
- •
Using the LLM as the only control
- •Do not ask the model to catch everything. Use regexes and deterministic validators for known forbidden patterns like card numbers and regulated claims.
- •
Skipping policy versioning
- •If you cannot answer “which rule set produced this decision?”, your audit trail is weak. Always log retrieved documents or at least their IDs and versions.
- •
Sending unnecessary sensitive data to the model
- •Mask account numbers, card data, SSNs, and personal identifiers before inference. For fintech compliance workflows this is both a privacy issue and a data residency issue.
- •
Returning unstructured text
- •Free-form responses break downstream automation. Use
with_structured_output()so your app can route decisions consistently into case management or approval systems.
- •Free-form responses break downstream automation. Use
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