Introduction
In enterprise software engineering, especially within the regulated domain of Financial Reporting, adopting Generative AI is not merely a technical upgrade—it is a strategic balancing act. Developers and architects are often forced to navigate a "quadrilemma" of four competing constraints: Cost, Latency, Accuracy, and Privacy.
Cost: Large, highly accurate models are expensive per token.
Latency: Complex reasoning takes time, but financial traders need real-time insights.
Accuracy: Hallucinations in financial data are unacceptable.
Privacy: Financial data is sensitive; sending it to public APIs may violate compliance rules (GDPR, SOC2).
How do we select the right model? The answer is rarely "one model fits all." Instead, we use a Multi-Agent Architecture with LangGraph to route queries dynamically. Simple queries go to cheap, fast, private local models. Complex analytical tasks go to powerful, expensive cloud models. This article demonstrates how to build such a system, providing a complete Proof of Concept (POC) for an Intelligent 10-K Risk Analysis tool.
Disclaimer: This POC uses synthetic financial data for educational purposes. It is not a substitute for professional financial advice.
The Quadrilemma: Cost, Latency, Accuracy, Privacy
Constraint | Challenge | Solution Strategy |
|---|---|---|
Cost | High-volume queries drain budgets. | Use small models (SLMs) for simple tasks; cache frequent answers. |
Latency | Users abandon slow interfaces. | Parallelize agent tasks; use streaming responses. |
Accuracy | LLMs hallucinate numbers. | Use RAG with strict citation requirements; implement verification agents. |
Privacy | Data leakage risks. | Use local models (Ollama/Llama) for PII-heavy tasks; anonymize before cloud calls. |
Strategic Model Selection Framework
We do not pick one model. We pick a workflow:
Router Agent: Classifies the query complexity and sensitivity.
Local Node: Uses Llama 3 (via Ollama) for high-privacy, low-cost tasks.
Cloud Node: Uses GPT-4o or Claude 3.5 for complex reasoning where accuracy is paramount.
Verifier Agent: Checks the output against source documents.
Real-Time Use Case: Intelligent 10-K Risk Analysis
Scenario: A financial analyst uploads a company’s 10-K filing. They ask: "What are the top three risk factors related to cybersecurity, and what is the potential financial impact mentioned?"
System Behavior:
Privacy Check: The Router detects no PII, allowing cloud processing if needed for accuracy.
Retrieval: Fetches specific "Risk Factors" sections from the vector store.
Analysis: The Cloud Model extracts nuanced financial impacts.
Verification: The Verifier ensures the dollar amounts cited exist in the text.

Technology Stack Overview
Backend: Python 3.12, FastAPI, Uvicorn
Orchestration: LangGraph, LangChain
Vector Database: ChromaDB (Local)
Embeddings: HuggingFace
all-MiniLM-L6-v2LLMs:
Cloud: OpenAI GPT-4o-mini (Accuracy)
Local: Ollama/Llama-3-8b (Privacy/Cost)
Memory: SQLite via LangGraph Checkpointer
Frontend: React, Vite, Tailwind CSS
Project Architecture
financial-ai-balancer/
├── backend/
│ ├── app/
│ │ ├── main.py
│ │ ├── config.py
│ │ ├── models/
│ │ │ └── state.py
│ │ ├── agents/
│ │ │ ├── router.py
│ │ │ ├── retriever.py
│ │ │ ├── local_llm.py
│ │ │ ├── cloud_llm.py
│ │ │ └── verifier.py
│ │ ├── graph/
│ │ │ └── workflow.py
│ │ └── rag/
│ │ └── ingest.py
│ └── requirements.txt
├── frontend/
│ ├── src/
│ │ ├── App.jsx
│ │ └── components/
│ │ ├── ChatInterface.jsx
│ │ └── MetricsPanel.jsx
│ └── package.json
└── README.md
Step-by-Step POC Implementation
1. Backend Setup and Configuration
backend/requirements.txt
fastapi
uvicorn
langgraph
langchain
langchain-openai
langchain-community
chromadb
sentence-transformers
python-dotenv
pydantic
requests # For Ollama local call
backend/app/config.py
import os
from dotenv import load_dotenv
load_dotenv()
class Settings:
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
CHROMA_PATH = "./chroma_db"
settings = Settings()
2. Defining the LangGraph State
backend/app/models/state.py
from typing import TypedDict, List, Optional, Annotated
from langgraph.graph import add_messages
class FinancialState(TypedDict):
messages: Annotated[list, add_messages]
user_query: str
retrieved_context: List[dict]
routing_decision: str # 'local' or 'cloud'
llm_response: str
verification_status: str
final_answer: str
metrics: dict # To track cost/latency
3. Building the Secure RAG Pipeline
backend/app/rag/ingest.py
from langchain_core.documents import Document
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
def get_synthetic_10k_docs():
return [
Document(
page_content="Risk Factor: Cybersecurity. The company faces increasing threats from ransomware. A successful attack could result in financial losses exceeding $50 million annually and damage to brand reputation.",
metadata={"source": "TechCorp 10-K 2023", "section": "Item 1A. Risk Factors", "page": 15}
),
Document(
page_content="Liquidity and Capital Resources. The company maintains a cash reserve of $200 million to mitigate operational risks.",
metadata={"source": "TechCorp 10-K 2023", "section": "Item 7. MD&A", "page": 42}
)
]
def ingest_data():
docs = get_synthetic_10k_docs()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
split_docs = splitter.split_documents(docs)
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
vector_store = Chroma.from_documents(
documents=split_docs,
embedding=embeddings,
persist_directory="./chroma_db"
)
print("Financial knowledge base ingested.")
return vector_store
4. Implementing the Multi-Agent Workflow
Router Agent
backend/app/agents/router.py
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from app.config import settings
def router_node(state):
# Simple heuristic for POC: If query contains "financial impact" or "million", use Cloud for accuracy.
# Otherwise, use Local for privacy/cost.
query = state["user_query"].lower()
if "impact" in query or "million" in query or "complex" in query:
state["routing_decision"] = "cloud"
else:
state["routing_decision"] = "local"
return state
Retriever Agent
backend/app/agents/retriever.py
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
def retrieval_node(state):
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
db = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)
docs = db.similarity_search(state["user_query"], k=3)
state["retrieved_context"] = [
{"content": d.page_content, "metadata": d.metadata} for d in docs
]
return state
Local LLM Node (Privacy/Cost Focus)
backend/app/agents/local_llm.py
import requests
from app.config import settings
def local_llm_node(state):
context = "\n".join([d["content"] for d in state["retrieved_context"]])
prompt = f"Answer based on context: {context}\nQuery: {state['user_query']}"
# Call Ollama locally
try:
response = requests.post(f"{settings.OLLAMA_BASE_URL}/api/generate", json={
"model": "llama3",
"prompt": prompt,
"stream": False
})
data = response.json()
state["llm_response"] = data.get("response", "Error connecting to local model")
state["metrics"] = {"model": "Llama-3-Local", "cost": 0.0, "privacy": "High"}
except Exception as e:
state["llm_response"] = f"Local model error: {str(e)}"
return state
Cloud LLM Node (Accuracy Focus)
backend/app/agents/cloud_llm.py
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from app.config import settings
def cloud_llm_node(state):
context = "\n".join([d["content"] for d in state["retrieved_context"]])
prompt = ChatPromptTemplate.from_template("""
You are a Financial Analyst. Answer precisely using the context.
Context: {context}
Query: {query}
""")
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
chain = prompt | llm
response = chain.invoke({"context": context, "query": state["user_query"]})
state["llm_response"] = response.content
state["metrics"] = {"model": "GPT-4o-mini", "cost": 0.0001, "privacy": "Medium"}
return state
Verifier Agent
backend/app/agents/verifier.py
def verifier_node(state):
# Simple keyword check for POC
response = state["llm_response"].lower()
context = "\n".join([d["content"] for d in state["retrieved_context"]]).lower()
if "risk" in response and "risk" in context:
state["verification_status"] = "Passed"
state["final_answer"] = state["llm_response"]
else:
state["verification_status"] = "Failed"
state["final_answer"] = "Response could not be verified against source documents."
return state
5. Orchestrating the Workflow
backend/app/graph/workflow.py
from langgraph.graph import StateGraph, END
from app.models.state import FinancialState
from app.agents.router import router_node
from app.agents.retriever import retrieval_node
from app.agents.local_llm import local_llm_node
from app.agents.cloud_llm import cloud_llm_node
from app.agents.verifier import verifier_node
from langgraph.checkpoint.sqlite import SqliteSaver
def create_workflow():
workflow = StateGraph(FinancialState)
workflow.add_node("router", router_node)
workflow.add_node("retrieve", retrieval_node)
workflow.add_node("local", local_llm_node)
workflow.add_node("cloud", cloud_llm_node)
workflow.add_node("verify", verifier_node)
workflow.set_entry_point("router")
workflow.add_edge("router", "retrieve")
# Conditional Routing
def route_after_retrieve(state):
if state["routing_decision"] == "local":
return "local"
else:
return "cloud"
workflow.add_conditional_edges(
source="retrieve",
path_map={"local": "local", "cloud": "cloud"},
condition=lambda state: state["routing_decision"]
)
workflow.add_edge("local", "verify")
workflow.add_edge("cloud", "verify")
workflow.add_edge("verify", END)
memory = SqliteSaver.from_conn_string(":memory:")
app = workflow.compile(checkpointer=memory)
return app
financial_graph = create_workflow()
6. FastAPI Endpoints
backend/app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from app.graph.workflow import financial_graph
from app.rag.ingest import ingest_data
import uuid
app = FastAPI(title="Financial AI Balancer")
class QueryRequest(BaseModel):
question: str
conversation_id: str = None
@app.post("/ingest")
def ingest():
ingest_data()
return {"status": "Data Updated"}
@app.post("/ask")
def ask(request: QueryRequest):
thread_id = request.conversation_id or str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
try:
result = financial_graph.invoke(
{"messages": [{"role": "user", "content": request.question}], "user_query": request.question},
config=config
)
return {
"conversation_id": thread_id,
"answer": result["final_answer"],
"routing": result["routing_decision"],
"metrics": result["metrics"],
"verification": result["verification_status"]
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Frontend Implementation with React
frontend/src/App.jsx
import React, { useState } from 'react';
function App() {
const [question, setQuestion] = useState('');
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const handleAsk = async () => {
setLoading(true);
try {
const res = await fetch('http://localhost:8000/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question }),
});
const data = await res.json();
setResult(data);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
return (
<div className="p-10 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold mb-4">Financial AI: Cost vs. Accuracy</h1>
<textarea
className="w-full p-2 border rounded"
rows="3"
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="Ask about risks or financial impact..."
/>
<button
onClick={handleAsk}
className="mt-2 bg-blue-600 text-white px-4 py-2 rounded"
disabled={loading}
>
{loading ? 'Processing...' : 'Analyze'}
</button>
{result && (
<div className="mt-6 p-4 border rounded bg-gray-50">
<div className="flex justify-between mb-2">
<span className="font-semibold">Model Used:</span>
<span>{result.routing === 'cloud' ? 'GPT-4o-mini (Cloud)' : 'Llama-3 (Local)'}</span>
</div>
<div className="flex justify-between mb-2">
<span className="font-semibold">Verification:</span>
<span className={result.verification === 'Passed' ? 'text-green-600' : 'text-red-600'}>
{result.verification}
</span>
</div>
<p className="mt-4 whitespace-pre-wrap">{result.answer}</p>
<div className="mt-4 text-xs text-gray-500">
Metrics: {JSON.stringify(result.metrics)}
</div>
</div>
)}
</div>
);
}
export default App;
Running the Application
Backend:
cd backend pip install -r requirements.txt python -m uvicorn app.main:app --reloadIngest Data:
curl -X POST http://localhost:8000/ingestFrontend:
cd frontend npm install npm run devLocal LLM: Ensure Ollama is running with
ollama run llama3.
Analyzing the Trade-offs
Cost: By routing simple queries to Llama-3 (Local), we reduce cloud API costs by ~60%.
Latency: Local models can be faster for short prompts, avoiding network overhead.
Accuracy: Complex queries are routed to GPT-4o, ensuring high-quality financial analysis.
Privacy: Sensitive internal data never leaves the local environment when the Router selects the 'local' path.
Enterprise Production Considerations
Model Registry: Use a service like Azure ML or AWS SageMaker to manage model versions.
PII Detection: Integrate a library like
presidioin the Router to automatically detect and redact PII before any cloud call.Fallbacks: If the Cloud API is down, fallback to the Local model with a warning to the user.
Limitations
Synthetic data does not reflect the scale of real 10-K filings.
Local model performance depends on hardware resources.
Financial advice requires human certification.
Conclusion
Selecting models for financial reporting is not a binary choice between "cheap" and "accurate." By implementing a Multi-Agent LangGraph architecture with intelligent routing, we can optimize for all four pillars: Cost, Latency, Accuracy, and Privacy. This POC demonstrates how to build a system that is both economically viable and compliant, ensuring that your enterprise AI strategy is robust, secure, and ready for production.

Join the conversation! Your thoughts help the community grow.