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
Intake & Memory Agent: Classifies the user request, loads historical client state, and determines if clarification is needed.
Semantic Retrieval Agent: Queries ChromaDB using dense embeddings to find similar past deals, policy documents, and product sheets.
Structuring Advisor Agent: Synthesizes retrieved context to recommend specific trade finance products (e.g., SBLC, Factoring, Forfaiting).
Compliance Critic Agent: Validates recommendations against internal risk appetite and sanctions lists. If validation fails, the graph loops back to the Advisor.
State Manager: Persists the interaction to Chroma’s metadata store and external DB for auditability.
Tech Stack
Orchestration: LangGraph (Stateful, cyclic agent workflows)
Vector Store: ChromaDB (Semantic similarity + metadata filtering)
Embeddings:
text-embedding-3-large(High-dimensional semantic capture)LLM: GPT-4o / Claude 3.5 Sonnet (Reasoning & Synthesis)
Memory: LangGraph Checkpointer + Chroma Metadata
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:
Retrieve the specific "Apex Manufacturing" deal structure from unstructured PDFs.
Match the current Vietnam/machinery scenario to relevant products semantically.
Ensure the recommendation complies with current Vietnam exposure limits.
Remember this conversation for future follow-ups.
Implementation
1. Environment Setup
pip install langgraph langchain-chroma chromadb langchain-openai \
langchain-community tiktoken python-dotenv2. 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"
]
}
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
| Capability | Naive RAG | LangGraph 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
Chroma at Scale: For >1M documents, deploy Chroma in client/server mode with persistent storage. Use partitioned collections by business unit or jurisdiction.
Embedding Strategy: Trade finance has domain-specific jargon. Fine-tune embeddings on UCP600, ISBP, and internal deal summaries for better semantic alignment.
Human-in-the-Loop: Add a
human_approvalnode before final output for high-value transactions. LangGraph supports interrupt-and-resume natively.Observability: Integrate LangSmith or Arize Phoenix to trace every graph execution, retrieval score, and critic decision for regulatory audits.
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.

Join the conversation! Your thoughts help the community grow.