Trade finance is the backbone of global commerce, yet it remains one of the most document-heavy and friction-prone sectors in fintech. A single letter of credit (LC) transaction can involve dozens of documents, multiple jurisdictions, and complex UCP600 compliance rules. Traditional keyword-based search fails here; a query for "risk mitigation for agricultural exports to Brazil" won't match a document titled "Soybean Shipment Guarantee Structure." To solve this, modern fintechs are moving toward Semantic Recommendation Engines. However, a simple RAG (Retrieval-Augmented Generation) pipeline is insufficient for enterprise trade finance. These workflows require state management, regulatory compliance checks, and multi-step reasoning. This article demonstrates how to build an Enterprise Multi-Agent RAG System using LangGraph, ChromaDB, and Semantic Similarity. We will build a "Trade Structuring Advisor" that recommends optimal financing instruments based on unstructured client requests, maintaining conversation state and audit trails.

Architecture Overview

Our system uses a cyclic graph architecture rather than a linear chain. This allows agents to critique their own work, retrieve additional context, and maintain persistent memory across sessions.

The Agent Topology

  1. Intake & Memory Agent: Classifies the user request, loads historical client state, and determines if clarification is needed.

  2. Semantic Retrieval Agent: Queries ChromaDB using dense embeddings to find similar past deals, policy documents, and product sheets.

  3. Structuring Advisor Agent: Synthesizes retrieved context to recommend specific trade finance products (e.g., SBLC, Factoring, Forfaiting).

  4. Compliance Critic Agent: Validates recommendations against internal risk appetite and sanctions lists. If validation fails, the graph loops back to the Advisor.

  5. State Manager: Persists the interaction to Chroma’s metadata store and external DB for auditability.

Tech Stack

Real-Time Use Case: The Mid-Market Exporter

Scenario: A relationship manager at a fintech receives a vague email from a mid-market exporter:

"We have a new buyer in Vietnam for our machinery. They want 90-day terms but our bank won't extend more credit. We need to secure payment without hurting cash flow. What did we do for Apex Manufacturing last year?"

The Challenge:

  1. Retrieve the specific "Apex Manufacturing" deal structure from unstructured PDFs.

  2. Match the current Vietnam/machinery scenario to relevant products semantically.

  3. Ensure the recommendation complies with current Vietnam exposure limits.

  4. Remember this conversation for future follow-ups.

Implementation

1. Environment Setup

pip install langgraph langchain-chroma chromadb langchain-openai \
            langchain-community tiktoken python-dotenv

2. Defining the Enterprise State

In LangGraph, state is the single source of truth. For trade finance, we need more than just messages; we need structured deal parameters and compliance flags.

from typing import Annotated, TypedDict, List
from langgraph.graph.message import add_messages
from langchain_core.documents import Document

class TradeFinanceState(TypedDict):
    """State schema for the Trade Finance Multi-Agent Graph"""
    messages: Annotated[list, add_messages]
    
    # Structured extraction from user query
    client_name: str | None
    counterparty_country: str | None
    tenor_days: int | None
    product_type: str | None
    
    # RAG & Recommendation Context
    retrieved_docs: List[Document]
    recommended_products: List[str]
    
    # Compliance & Control Flow
    compliance_passed: bool | None
    critic_feedback: str | None
    iteration_count: int
    
    # Memory
    session_id: str
    previous_deals_referenced: List[str]

3. Semantic Indexing with Chroma

Before querying, we must ingest trade finance documents with rich metadata. Semantic similarity alone isn't enough; we need hybrid filtering.

import chromadb
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

def initialize_trade_vector_store():
    """Initialize Chroma with trade finance specific configuration"""
    embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
    
    vectorstore = Chroma(
        collection_name="trade_finance_knowledge_base",
        embedding_function=embeddings,
        persist_directory="./chroma_trade_db",
        # Enable HNSW index for enterprise-scale performance
        collection_metadata={"hnsw:space": "cosine"} 
    )
    return vectorstore

# Example ingestion with critical metadata
sample_docs = [
    Document(
        page_content="Structured forfaiting facility for Vietnamese machinery exports...",
        metadata={
            "doc_type": "deal_summary",
            "client": "Apex Manufacturing",
            "country": "Vietnam",
            "product": "Forfaiting",
            "year": 2025,
            "risk_rating": "B+"
        }
    ),
    # ... more documents
]

vectorstore = initialize_trade_vector_store()
vectorstore.add_documents(sample_docs)

4. Building the Agent Nodes

Semantic Retrieval Node

This node performs semantic search and metadata filtering based on extracted state.

async def retrieval_node(state: TradeFinanceState):
    """Retrieve relevant docs using semantic similarity + metadata filters"""
    query = state["messages"][-1].content
    
    # Build dynamic metadata filter from state
    where_filter = {}
    if state.get("counterparty_country"):
        where_filter["country"] = state["counterparty_country"]
    if state.get("client_name"):
        where_filter["client"] = state["client_name"]
    
    # Semantic search with enterprise filters
    docs = vectorstore.similarity_search(
        query=query,
        k=5,
        filter=where_filter if where_filter else None
    )
    
    return {
        "retrieved_docs": docs,
        "previous_deals_referenced": [
            d.metadata.get("client") for d in docs 
            if d.metadata.get("doc_type") == "deal_summary"
        ]
    }
410

Structuring Advisor Node

Generates recommendations grounded in retrieved context.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

advisor_llm = ChatOpenAI(model="gpt-4o", temperature=0.2)

ADVISOR_PROMPT = ChatPromptTemplate.from_messages([
    ("system", """You are a Senior Trade Finance Structurer at an enterprise fintech.
    
RETRIEVED CONTEXT:
{context}

CLIENT STATE:
- Country: {country}
- Tenor: {tenor} days
- Previous Deals: {previous_deals}

Recommend 1-3 specific trade finance products. For each provide:
1. Product name
2. Why it fits this scenario
3. Key risks to mitigate
4. Reference to similar past deals from context

Be precise. Cite document sources."""),
    ("human", "{query}")
])

async def advisor_node(state: TradeFinanceState):
    context = "\n\n".join([
        f"[{d.metadata.get('doc_type')}] {d.page_content}" 
        for d in state["retrieved_docs"]
    ])
    
    prompt = ADVISOR_PROMPT.format(
        context=context,
        country=state.get("counterparty_country", "Unknown"),
        tenor=state.get("tenor_days", "Unknown"),
        previous_deals=state.get("previous_deals_referenced", []),
        query=state["messages"][-1].content
    )
    
    response = await advisor_llm.ainvoke(prompt)
    
    return {
        "messages": [response],
        "recommended_products": ["Forfaiting", "Export Credit Insurance"], # Parse from response in production
        "iteration_count": state.get("iteration_count", 0) + 1
    }

Compliance Critic Node (The Loop Enabler)

This is what makes it enterprise. The critic can reject recommendations and force re-reasoning.

CRITIC_PROMPT = ChatPromptTemplate.from_messages([
    ("system", """You are a Trade Finance Compliance Officer. Review the advisor's recommendation.
    
Check against:
1. Country exposure limits (Vietnam max $50M per client)
2. Product suitability for stated tenor
3. Sanctions/AML red flags
4. Consistency with referenced past deals

If ANY issue exists, respond with REJECT and specific feedback.
If compliant, respond with APPROVE."""),
    ("human", "Recommendation:\n{recommendation}\n\nClient State: {state}")
])

async def critic_node(state: TradeFinanceState):
    last_message = state["messages"][-1].content
    
    response = await advisor_llm.ainvoke(
        CRITIC_PROMPT.format(
            recommendation=last_message,
            state=f"Country: {state.get('counterparty_country')}, Tenor: {state.get('tenor_days')}"
        )
    )
    
    approved = "APPROVE" in response.content.upper()
    
    return {
        "compliance_passed": approved,
        "critic_feedback": response.content if not approved else None,
        "messages": [response] if not approved else []
    }

5. Assembling the LangGraph Workflow

from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver

def build_trade_finance_graph():
    workflow = StateGraph(TradeFinanceState)
    
    # Add nodes
    workflow.add_node("retrieval", retrieval_node)
    workflow.add_node("advisor", advisor_node)
    workflow.add_node("critic", critic_node)
    
    # Define edges
    workflow.add_edge(START, "retrieval")
    workflow.add_edge("retrieval", "advisor")
    workflow.add_edge("advisor", "critic")
    
    # Conditional routing: loop back if compliance fails (max 3 iterations)
    def should_continue(state: TradeFinanceState):
        if state["compliance_passed"]:
            return "end"
        if state.get("iteration_count", 0) >= 3:
            return "end"  # Safety valve
        return "advisor"  # Loop back with critic feedback in messages
    
    workflow.add_conditional_edges("critic", should_continue, {
        "advisor": "advisor",
        "end": END
    })
    
    # Persistent memory for enterprise session continuity
    checkpointer = MemorySaver()
    
    return workflow.compile(checkpointer=checkpointer)

# Initialize
app = build_trade_finance_graph()

6. Running with Session Memory

import asyncio

async def main():
    config = {"configurable": {"thread_id": "client-vietnam-machinery-001"}}
    
    # First turn
    result = await app.ainvoke({
        "messages": [("human", 
            "New buyer in Vietnam for machinery. 90-day terms needed. "
            "What did we do for Apex Manufacturing?")],
        "session_id": "client-vietnam-machinery-001",
        "iteration_count": 0
    }, config=config)
    
    print("=== RECOMMENDATION ===")
    print(result["messages"][-1].content)
    print(f"\nCompliance Passed: {result['compliance_passed']}")
    print(f"Iterations: {result['iteration_count']}")
    print(f"Referenced Deals: {result['previous_deals_referenced']}")
    
    # Second turn - memory persists automatically via checkpointer
    follow_up = await app.ainvoke({
        "messages": [("human", "What were the pricing terms on that Apex deal?")]
    }, config=config)
    
    print("\n=== FOLLOW-UP RESPONSE ===")
    print(follow_up["messages"][-1].content)

asyncio.run(main())

Why This Architecture Wins in Enterprise Trade Finance

CapabilityNaive RAGLangGraph Multi-Agent + Chroma
Semantic Understanding✅ Basic✅ Deep (dense embeddings + metadata)
State Management❌ Stateless✅ Full deal lifecycle tracking
Compliance Validation❌ Post-hoc✅ Built-in critic loop
Session Memory❌ Manual✅ Automatic checkpointing
Audit Trail❌ Lost✅ Every state transition persisted
Self-Correction❌ Single pass✅ Cyclic refinement
Multi-Document Reasoning⚠️ Limited✅ Cross-referencing past deals + policies

Production Considerations

  1. Chroma at Scale: For >1M documents, deploy Chroma in client/server mode with persistent storage. Use partitioned collections by business unit or jurisdiction.

  2. Embedding Strategy: Trade finance has domain-specific jargon. Fine-tune embeddings on UCP600, ISBP, and internal deal summaries for better semantic alignment.

  3. Human-in-the-Loop: Add a human_approval node before final output for high-value transactions. LangGraph supports interrupt-and-resume natively.

  4. Observability: Integrate LangSmith or Arize Phoenix to trace every graph execution, retrieval score, and critic decision for regulatory audits.

  5. PII Redaction: Implement a pre-processing node that masks client names and account numbers before embedding. Store mappings securely separately.

Conclusion

Building recommendation engines for trade finance requires more than semantic search—it demands orchestrated intelligence. By combining LangGraph's stateful multi-agent patterns with Chroma's semantic retrieval, fintechs can create systems that don't just answer questions, but actively reason through complex structuring decisions while maintaining the compliance rigor and institutional memory that enterprise clients demand. The era of static document search in trade finance is over. The future is stateful, self-correcting, and semantically aware. Production trade finance systems require extensive security review, regulatory approval, and integration with core banking systems.