Flux bancaires multi-agents sur Kubernetes : LangGraph, un registre et AgentGateway
Créez des agents spécialisés FastAPI, un orchestrateur LangGraph accompagné d’un planificateur LLM, déployez-les dans un cluster doté d’un registre, puis ajoutez du routage et des limites de débit à l’aide d’AgentGateway.
MCP concerne le cas agent-outil : une fonction fixe effectue une tâche précise et renvoie son résultat, sans aucune autonomie ni capacité de jugement de la part de l’outil. La communication agent-agent est une autre affaire ; la partie à l’autre bout peut raisonner par elle-même, éventuellement avec son propre LLM, ses propres outils et ses propres décisions. Un agent lui confie alors une sous-tâche en s’attendant à une réponse fondée sur la réflexion, et non simplement à une recherche.
Le processus d’ouverture de compte met en évidence la nature du problème. Il implique plusieurs aspects indépendants : vérification de l’identité, historique du client, vérifications de son passé et de ses antécédents criminels, capacités financières et archives publiques. Chacun de ces aspects pourrait raisonnablement relever d’une équipe différente, être développé séparément et traité indépendamment. C’est précisément pour ce type de problème que l’architecture agent-à-agent a été conçue. La structure présentée ci-dessous évolue d’un seul agent vers une équipe de spécialistes, comprenant un orchestrateur LangGraph associé à un planificateur LLM, une déploiement Kubernetes, un registre, et enfin une passerelle d’accès.
Cinq spécialistes, un modèle
L’équipe chargée de l’ouverture de compte compte cinq agents spécialisés. Chacun d’eux correspond à un service FastAPI autonome fonctionnant sur sa propre porte, et l’agent de vérification de l’identité sert de modèle de base : un simple FastAPI disposant d’une seule interface.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(title="Identity Verification Agent")
class VerifyRequest(BaseModel):
customer_id: str
document_type: str
document_number: str
class VerifyResponse(BaseModel):
customer_id: str
status: str
confidence: float
notes: str
@app.get("/health")
async def health():
return {"agent": "identity-verification", "status": "ok"}
@app.post("/invoke", response_model=VerifyResponse)
async def invoke(req: VerifyRequest):
return VerifyResponse(
customer_id=req.customer_id,
status="VERIFIED",
confidence=0.97,
notes=f"{req.document_type} {req.document_number} matched on file.",
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=9001)
Chaque agent du système suit ce même schéma : un modèle de demande Pydantic, un modèle de réponse Pydantic, une extrémité /invoke et une extrémité /health. Une vérification rapide :
curl -s http://localhost:9001/health
curl -s -X POST http://localhost:9001/invoke \
-H "Content-Type: application/json" \
-d '{"customer_id":"CUST-5567","document_type":"passport","document_number":"X1234567"}'
Les services restants sont construits de la même manière, et huit services existent à la fin.
Premier orchestrateur : appels parallèles
La première version de l’orchestrateur a appelé les huit agents simultanément à l’aide de asyncio.gather et a synthétisé une décision à partir des résultats combinés :
async def call_agent(client, name, payload):
try:
resp = await client.post(f"{AGENTS[name]}/invoke", json=payload, timeout=10.0)
resp.raise_for_status()
return resp.json()
except httpx.HTTPError as e:
return {"status": "ERROR", "error": str(e)}
@app.post("/open-account")
async def open_account(app_data: AccountApplication):
async with httpx.AsyncClient() as client:
identity, history, background, financial, public_records = await asyncio.gather(
call_agent(client, "identity", {...}),
call_agent(client, "customer_history", {...}),
call_agent(client, "background_check", {...}),
call_agent(client, "financial_capability", {...}),
call_agent(client, "public_records", {...}),
............
............
)
# decision synthesized from all five results
Cette contrainte a poussé à améliorer la conception. Chaque agent était appelé à chaque fois, même lorsque une vérification préalable avait déjà échoué. Certaines vérifications coûtaient de l’argent réel, car les recherches dans les bases de données en arrière-plan et les registres publics provenaient généralement de fournisseurs externes payants, ce qui rendait ces appels répétés purement inutiles. Le code ne séparait pas non plus la raison interne d’une décision du message affiché au client. La gestion des erreurs se résumait à un seul bloc try/except englobant toutes les appels, au lieu d’un mécanisme permettant un suivi précis à chaque étape.
La solution consiste en un véritable orchestrateur LangGraph, où chaque étape peut examiner ce qui a précédé et rediriger le flux en conséquence. L’outil d’appel des agents ne génère jamais d’erreur. Chaque échec se traduit par un status='ERROR', ce qui permet au système de gérer uniformément les échecs sans planter :
async def call_agent(agent_name: str, payload: dict) -> dict:
"""Never raises -- every failure mode becomes status='ERROR' so the
graph can route uniformly instead of crashing."""
url = f"{AGENTS[agent_name]}/invoke"
try:
async with httpx.AsyncClient(timeout=TIMEOUT_SECONDS) as client:
resp = await client.post(url, json=payload)
resp.raise_for_status()
return resp.json()
except httpx.TimeoutException:
return {"status": "ERROR", "error": f"{agent_name} agent timed out after {TIMEOUT_SECONDS}s"}
except httpx.ConnectError:
return {"status": "ERROR", "error": f"{agent_name} agent is unreachable"}
except httpx.HTTPStatusError as e:
return {"status": "ERROR", "error": f"{agent_name} agent returned {e.response.status_code}"}
except Exception as e:
return {"status": "ERROR", "error": f"{agent_name} agent call failed: {e}"}
def route_after_identity(state) -> str:
result = state.get("identity_result") or {}
if result.get("status") == "ERROR":
state.setdefault("reasons", []).append(f"Identity check failed: {result.get('error')}")
return "decline"
if result.get("status") != "VERIFIED":
state.setdefault("reasons", []).append("Identity could not be verified.")
return "decline"
return "continue"
# ... one routing function per check, same shape ...
builder = StateGraph(ApplicationState)
builder.add_node("identity_check", identity_check)
builder.add_node("customer_history_check", customer_history_check)
builder.add_node("background_check", background_check)
builder.add_node("financial_capability_check", financial_capability_check)
builder.add_node("public_records_check", public_records_check)
builder.add_node("decline", decline_node)
builder.add_node("approve", approve_node)
builder.add_edge(START, "identity_check")
builder.add_conditional_edges("identity_check", route_after_identity,
{"continue": "customer_history_check", "decline": "decline"})
# ... same pattern chained through all five checks ...
builder.add_edge("decline", END)
builder.add_edge("approve", END)
graph = builder.compile()
C’est une solution LangGraph simple, avec un traitement des erreurs intégré à chaque étape. Elle manque cependant de la complexité requise par des flux réels.
Un planificateur LLM pour de nombreux produits
L’ouverture de compte n’est qu’un seul produit bancaire. Une banque gère également des cartes de crédit, des cartes de débit et des cartes-cadeaux, chacune nécessitant un ensemble différent de vérifications. Le codage statique d’instructions if/else par type de produit n’est pas scalable ; c’est pourquoi un planificateur LLM (un modèle GPT via Azure OpenAI) prend en charge une demande rédigée en langue anglaise simple, accompagnée d’un catalogue de ce que chaque agent propose, puis sélectionne les agents pertinents ainsi que l’ordre à suivre. Il s’agit de raisonnement, et non de logique de branchement.
Trois agents spécifiques à des produits ont été ajoutés au ensemble initial, ce qui permet trois flux : ouverture d’un compte bancaire, liaison d’une carte de débit et achat d’une carte-cadeau.
# A simple starter for all agents
uv run agents/background_check_agent.py &
uv run agents/card_linking_agent.py &
uv run agents/identity_agent.py &
uv run agents/customer_history_agent.py &
uv run agents/public_records_agent.py &
uv run agents/financial_capability_agent.py &
uv run agents/gift_card_compliance_agent.py &
uv run orchestrator_langgraph_llm.py &
chmod a+x ./start_agent.sh
# Run it
./start_agents.sh
(account-opening-agents) krishnansriram@Krishnans-MacBook-Pro account-opening-agents % INFO: Started server process [22183]
INFO: Started server process [22184]
INFO: Started server process [22185]
INFO: Started server process [22182]
INFO: Started server process [22186]
INFO: Started server process [22187]
INFO: Started server process [22188]
INFO: Waiting for application startup.
INFO: Waiting for application startup.
INFO: Waiting for application startup.
INFO: Waiting for application startup.
INFO: Waiting for application startup.
INFO: Waiting for application startup.
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Application startup complete.
INFO: Application startup complete.
INFO: Application startup complete.
INFO: Application startup complete.
INFO: Application startup complete.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:9007 (Press CTRL+C to quit)
INFO: Uvicorn running on http://0.0.0.0:9001 (Press CTRL+C to quit)
INFO: Uvicorn running on http://0.0.0.0:9005 (Press CTRL+C to quit)
INFO: Uvicorn running on http://0.0.0.0:9008 (Press CTRL+C to quit)
INFO: Uvicorn running on http://0.0.0.0:9003 (Press CTRL+C to quit)
INFO: Uvicorn running on http://0.0.0.0:9002 (Press CTRL+C to quit)
INFO: Uvicorn running on http://0.0.0.0:9004 (Press CTRL+C to quit)
INFO: Started server process [22190]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:9000 (Press CTRL+C to quit)
L’exécuteur est devenu un nœud qui parcourt le plan généré par le LLM sans suivre plus une chaîne fixe d’arêtes de graphe. Il a été testé directement, en contournant l’appel au véritable LLM qui nécessite des identifiants Azure en temps réel, en lui fournissant des plans conçus manuellement :
- Un plan de carte-cadeau composé d’une seule étape,
gift_card_compliance, a été approuvé en dessous du seuil anti-blanchiment d’argent et rejeté au-delà de ce seuil, sans qu’aucune autre étape ne soit exécutée. - Un plan de carte de crédit en trois étapes, correspondant aux étapes
identity,financial_capabilityetcredit_limit, a exécuté les trois étapes dans le bon ordre avec les bons résultats.
Un petit script local lance tous les agents en même temps, sauvegardé sous le nom de start_agent.sh :
curl -s -X POST http://localhost:9000/process-enquiry \
-H "Content-Type: application/json" \
-d '{
"enquiry_text": "I would like to open a new checking account",
"customer_id": "CUST-9003",
"full_name": "Jordan Alex Smith",
"date_of_birth": "1990-04-12",
"document_type": "passport",
"document_number": "X1234567",
"declared_annual_income": 95000,
"address": "123 Main St, Columbus, OH"
}' | python3 -m json.tool
Handling connection for 9000
{
"customer_id": "CUST-9003",
"product_type": "account_opening",
"planned_steps": [
"identity",
"financial_capability",
"background_check",
"public_records"
],
"planner_reasoning": "This is a new deposit account opening request. Identity should be verified first for the new customer, followed by financial capability checks needed for account opening. Background screening and public records checks are also required for opening a new deposit account.",
"decision": "APPROVED",
"customer_message": "Your account opening request has been approved.",
"reasons": [
"All required checks passed."
],
"step_results": {
"identity": {
"customer_id": "CUST-9003",
"status": "VERIFIED",
"confidence": 0.97,
"notes": "passport X1234567 matched on file."
},
"financial_capability": {
"customer_id": "CUST-9003",
"status": "PASS",
"income_verified": true,
"estimated_credit_score": 742,
"affordability_status": "ADEQUATE"
},
"background_check": {
"customer_id": "CUST-9003",
"status": "PASS",
"criminal_record_found": false,
"sanctions_hit": false,
"watchlist_hit": false,
"notes": "No adverse findings for Jordan Alex Smith."
},
"public_records": {
"customer_id": "CUST-9003",
"status": "PASS",
"address_verified": true,
"bankruptcy_history": false,
"litigation_history": false
}
}
}
Avec les agents en cours d’exécution, envoyez une demande de test :
curl -s -X POST http://localhost:9000/process-enquiry \
-H "Content-Type: application/json" \
-d '{
"enquiry_text": "I would like to link a debit card to my checking account",
"customer_id": "CUST-9002",
"full_name": "Priya Nair",
"date_of_birth": "1994-03-08",
"document_type": "passport",
"document_number": "X5566778",
"declared_annual_income": 65000,
"address": "12 Maple Rd, Dublin, OH",
"linked_account_number": "ACC-99887766"
}' | python3 -m json.tool
Handling connection for 9000
{
"customer_id": "CUST-9002",
"product_type": "debit_card",
"planned_steps": [
"customer_history",
"card_linking"
],
"planner_reasoning": "This is a debit card request. First check customer history to confirm the customer relationship and whether identity/KYC is already established, then perform card linking to verify the checking account and link the debit card.",
"decision": "APPROVED",
"customer_message": "Your debit card request has been approved.",
"reasons": [
"All required checks passed."
],
"step_results": {
"customer_history": {
"customer_id": "CUST-9002",
"status": "PASS",
"existing_customer": true,
"relationship_years": 3.5,
"prior_accounts": 1,
"kyc_status": "CURRENT"
},
"card_linking": {
"customer_id": "CUST-9002",
"status": "PASS",
"account_verified": true,
"notes": "Account ACC-99887766 verified and eligible for debit card linking."
}
}
}
Une deuxième demande déclenche un autre flux, et le script stop_agent.sh associé arrête tout :
# This all that we need to kill our agents - stop_agent.sh
pkill -f "uv run agents/"
pkill -f "orchestrator_langgraph_llm.py"
chmod a+x ./stop_agent.sh
# Execute stop agent
./stop_agents.sh
INFO: Shutting down
INFO: Shutting down
INFO: Shutting down
INFO: Shutting down
INFO: Shutting down
INFO: Shutting down
INFO: Shutting down
(account-opening-agents) krishnansriram@Krishnans-MacBook-Pro account-opening-agents % INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [22185]
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Waiting for application shutdown.
INFO: Finished server process [22186]
INFO: Application shutdown complete.
INFO: Finished server process [22190]
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [22182]
INFO: Waiting for application shutdown.
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Application shutdown complete.
INFO: Finished server process [22184]
INFO: Finished server process [22187]
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [22188]
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [22183]
Les agents fonctionnent de manière locale, mais il reste encore du travail à faire. Ensuite, ils doivent être exécutés derrière AgentGateway, avec un service de registre en parallèle.
Déploiement dans un cluster
Commencez par un espace de noms pour tous les agents, ce qui facilite leur découverte ultérieure. Il s’appelle banking-agents :
kubectl get namespace banking-agents 2>/dev/null || kubectl create namespace banking-agents
Un Dockerfile par agent
Il existe deux Dockerfiles : un pour les agents d’application, et un pour le registre ainsi que l’orchestrateur. Comme chaque agent est une application FastAPI simple ayant la même structure, un seul fichier, Dockerfile.agent, les sert tous :
FROM python:3.13-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
COPY agents ./agents
ARG APP_MODULE
ENV APP_MODULE=${APP_MODULE}
CMD uv run python ${APP_MODULE}
Déploiement des agents un par un
Voici le flux complet pour le premier agent, son identité, suivi des mêmes trois commandes pour chacun des autres :
docker build -t identity-agent:v1 \
--build-arg APP_MODULE=agents/identity_agent.py \
-f Dockerfile.agent .
kind load docker-image identity-agent:v1 --name agentgateway
kubectl apply -f- <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: identity-agent
namespace: banking-agents
spec:
replicas: 1
selector:
matchLabels: {app: identity-agent}
template:
metadata:
labels: {app: identity-agent}
spec:
containers:
- name: identity-agent
image: identity-agent:v1
imagePullPolicy: IfNotPresent
ports: [{containerPort: 9001}]
---
apiVersion: v1
kind: Service
metadata:
name: identity-agent
namespace: banking-agents
spec:
selector: {app: identity-agent}
ports: [{port: 80, targetPort: 9001}]
type: ClusterIP
EOF
kubectl rollout status deploy/identity-agent -n banking-agents --timeout=60s
Le schéma se répète pour les huit agents, qui écoutent sur les ports 9001 à 9008 et couvrent l’historique du client, la vérification des antécédents, les capacités financières, les archives publiques, le plafond de crédit, la liaison des cartes ainsi que la conformité des cartes-cadeaux. Seul le nom, l’image et le port changent à chaque fois. Veuillez ensuite confirmer les pods :
kubectl get pods -n banking-agents
NAME READY STATUS RESTARTS AGE
background-check-agent-566d595b77-h69zg 1/1 Running 0 19h
card-linking-agent-786f4899-96s6c 1/1 Running 0 19h
credit-limit-agent-7fb6db7b76-b8zzf 1/1 Running 0 19h
customer-history-agent-b765d95fd-2pzh5 1/1 Running 0 19h
financial-capability-agent-5c879c8879-98sgp 1/1 Running 0 19h
gift-card-compliance-agent-75c55c74d4-qzpx4 1/1 Running 0 19h
identity-agent-668c66fff8-stgxt 1/1 Running 0 19h
public-records-agent-66dfd78c8b-qrcvh 1/1 Running 0 19h
Tous les huit devraient afficher 1/1 En cours d’exécution.
Déploiement du registre
Le service de registre fait référence à tous les autres services du système. Tant qu’il n’est pas opérationnel, les agents ne disposent d’aucune carte indiquant comment se connecter. Par la suite, l’accès à ce service doit être restreint de manière à ce que seuls les agents des applications puissent y accéder, ce qui assure la sécurité des agents et leur permet de communiquer entre eux de manière claire.
docker build -t agent-registry:v1 --build-arg APP_MODULE=agent_registry_service.py -f Dockerfile .
kind load docker-image agent-registry:v1 --name agentgateway
kubectl apply -f- <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: agent-registry
namespace: banking-agents
spec:
replicas: 1
selector:
matchLabels: {app: agent-registry}
template:
metadata:
labels: {app: agent-registry}
spec:
containers:
- name: agent-registry
image: agent-registry:v1
imagePullPolicy: IfNotPresent
ports: [{containerPort: 9100}]
---
apiVersion: v1
kind: Service
metadata:
name: agent-registry
namespace: banking-agents
spec:
selector: {app: agent-registry}
ports: [{port: 80, targetPort: 9100}]
type: ClusterIP
EOF
kubectl rollout status deploy/agent-registry -n banking-agents --timeout=60s
kubectl port-forward -n banking-agents svc/agent-registry 9100:80 &
Vérifiez qu’il peut atteindre les huit autres agents via leur nom DNS. La réponse est une carte de tous les services d’application, servant de registre qui indique à la fois les emplacements et des descriptions, ce qui ouvre idéalement la voie aux compétences requises :
curl -s http://localhost:9100/agents | python3 -m json.tool
#Execution results
Handling connection for 9100
{
"agents": [
{
"name": "identity",
"url": "http://identity-agent.banking-agents.svc.cluster.local:80",
"description": "Verifies a customer's identity documents (passport, license, etc). Needed any time a NEW customer's identity hasn't already been established.",
"input_schema": {
"customer_id": "customer_id",
"document_type": "document_type",
"document_number": "document_number"
}
},
{
"name": "customer_history",
"url": "http://customer-history-agent.banking-agents.svc.cluster.local:80",
"description": "Looks up existing relationship, prior accounts, and KYC status for a customer. Useful to check whether identity verification can be skipped for an existing customer.",
"input_schema": {
"customer_id": "customer_id"
}
},
{
"name": "background_check",
"url": "http://background-check-agent.banking-agents.svc.cluster.local:80",
"description": "Criminal record, sanctions, and watchlist screening. Required for opening a new deposit account; usually not required for issuing a card to an already-verified customer.",
"input_schema": {
"customer_id": "customer_id",
"full_name": "full_name",
"date_of_birth": "date_of_birth"
}
},
{
"name": "financial_capability",
"url": "http://financial-capability-agent.banking-agents.svc.cluster.local:80",
"description": "Verifies income and estimates a credit score. Required for account opening and for credit card applications; not required for debit or gift cards.",
"input_schema": {
"customer_id": "customer_id",
"declared_annual_income": "declared_annual_income"
}
},
{
"name": "public_records",
"url": "http://public-records-agent.banking-agents.svc.cluster.local:80",
"description": "Checks address verification, litigation, and bankruptcy history. Required for opening a new deposit account.",
"input_schema": {
"customer_id": "customer_id",
"address": "address"
}
},
{
"name": "credit_limit",
"url": "http://credit-limit-agent.banking-agents.svc.cluster.local:80",
"description": "Determines an approved credit limit based on income and the requested limit. Required ONLY for credit card applications.",
"input_schema": {
"customer_id": "customer_id",
"declared_annual_income": "declared_annual_income",
"requested_credit_limit": "requested_credit_limit"
}
},
{
"name": "card_linking",
"url": "http://card-linking-agent.banking-agents.svc.cluster.local:80",
"description": "Verifies a bank account to link a debit card to. Required ONLY for debit card applications.",
"input_schema": {
"customer_id": "customer_id",
"linked_account_number": "linked_account_number"
}
},
{
"name": "gift_card_compliance",
"url": "http://gift-card-compliance-agent.banking-agents.svc.cluster.local:80",
"description": "Checks a gift card purchase amount against AML limits. Required ONLY for gift card purchases.",
"input_schema": {
"customer_id": "customer_id",
"purchase_amount": "purchase_amount"
}
}
]
}
Déploiement de l’orchestrateur
Les identifiants Azure OpenAI sont stockés en tant que secret et ne sont jamais intégrés à l’image :
kubectl create secret generic azure-openai-secret \
-n banking-agents \
--from-literal=AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com" \
--from-literal=AZURE_OPENAI_DEPLOYMENT="gpt-5-1-chat" \
--from-literal=AZURE_OPENAI_API_KEY="your-key" \
--from-literal=AZURE_OPENAI_API_VERSION="2025-04-14"
Ensuite, construisez et déployez l’image en suivant le même schéma que pour tous les autres services :
docker build -t orchestrator:v1 --build-arg APP_MODULE=orchestrator_dynamic.py -f Dockerfile .
kind load docker-image orchestrator:v1 --name agentgateway
kubectl apply -f- <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: orchestrator
namespace: banking-agents
spec:
replicas: 1
selector:
matchLabels: {app: orchestrator}
template:
metadata:
labels: {app: orchestrator}
spec:
containers:
- name: orchestrator
image: orchestrator:v1
imagePullPolicy: IfNotPresent
ports: [{containerPort: 9000}]
env:
- name: AGENT_REGISTRY_URL
value: "http://agent-registry.banking-agents.svc.cluster.local:80"
envFrom:
- secretRef:
name: azure-openai-secret
---
apiVersion: v1
kind: Service
metadata:
name: orchestrator
namespace: banking-agents
spec:
selector: {app: orchestrator}
ports: [{port: 80, targetPort: 9000}]
type: ClusterIP
EOF
kubectl rollout status deploy/orchestrator -n banking-agents --timeout=60s
kubectl port-forward -n banking-agents svc/orchestrator 9000:80 &
Moment de faire un test :
curl -s -X POST http://localhost:9000/process-enquiry \
-H "Content-Type: application/json" \
-d '{
"enquiry_text": "I would like to purchase a $500 gift card",
"customer_id": "CUST-7001", "full_name": "Jordan Smith", "date_of_birth": "1990-04-12",
"document_type": "passport", "document_number": "X1234567",
"declared_annual_income": 95000, "address": "123 Main St, Columbus, OH",
"purchase_amount": 500
}' | python3 -m json.tool
Handling connection for 9000
{
"customer_id": "CUST-7001",
"product_type": "gift_card",
"planned_steps": [
"gift_card_compliance"
],
"planner_reasoning": "This is a gift card purchase, so the only relevant specialist is gift_card_compliance to check the amount against AML limits. No identity, credit, account, or background screening steps are needed for this request.",
"decision": "APPROVED",
"customer_message": "Your gift card request has been approved.",
"reasons": [
"All required checks passed."
],
"step_results": {
"gift_card_compliance": {
"customer_id": "CUST-7001",
"status": "PASS",
"within_aml_limit": true,
"notes": "Purchase amount 500.0 vs AML threshold 2000.0."
}
}
}
Un système multi-agents fonctionnel avec un orchestrateur s’exécute désormais dans le cluster, ce qui est bien différent d’un simple point de terminaison.
Placer l’orchestrateur derrière le gateway
Pourquoi placer un gateway en tête de l’orchestrateur ? Il y a cinq raisons :
- Un point d’entrée stable. Une URL réelle telle que
https://your-domain/orchestratorremplace un tunnel réservé uniquement aux administrateurs Kubernetes. Tout client réel, qu’il s’agisse d’un backend mobile, d’un autre service interne ou d’une intégration avec un partenaire, peut y accéder normalement. - Authentification avant exécution du code. Grâce aux politiques JWT et RBAC mises en place pour la sécurité, un appelant non authentifié ou non autorisé est rejeté au niveau du gateway et n’a jamais accès à la logique d’application de l’orchestrateur.
Les anciens serveurs MCP bancaires soulignaient déjà ce point pour les outils. Ce raisonnement s’applique désormais à un niveau supérieur, car l’entrée principale d’un système multi-agents mérite la même gouvernance que celle dont disposaient les agents individuels.
Un backend HTTP simple comme l’orchestrateur, sans traduction MCP et sans configuration de fournisseur d’IA, n’a pas besoin d’un AgentgatewayBackend. Un HTTPRoute peut pointer directement vers un Service Kubernetes. Il reste cependant un problème : le Service de l’orchestrateur se trouve dans le namespace banking-agents tandis que le HTTPRoute se trouve dans agentgateway-system, un namespace différent. L’API Gateway exige une ReferenceGrant explicite dans le namespace cible avant d’autoriser une référence inter-namespace.
Tout d’abord, autorisez la référence inter-namespace :
kubectl apply -f- <<EOF
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-agentgateway-to-orchestrator
namespace: banking-agents
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: agentgateway-system
to:
- group: ""
kind: Service
name: orchestrator
EOF
Deuxièmement, redirigez directement vers le Service :
kubectl apply -f- <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: banking-orchestrator-route
namespace: agentgateway-system
spec:
parentRefs:
- name: agentgateway-proxy
rules:
- matches:
- path: {type: PathPrefix, value: /orchestrator}
backendRefs:
- name: orchestrator
namespace: banking-agents
port: 80
EOF
kubectl get httproute banking-orchestrator-route -n agentgateway-system
Vérifiez ensuite via le passerelle :
curl -s -X POST http://localhost:8080/orchestrator/process-enquiry \
-H "Content-Type: application/json" \
-d '{
"enquiry_text": "I would like to purchase a $500 gift card",
"customer_id": "CUST-7001", "full_name": "Jordan Smith", "date_of_birth": "1990-04-12",
"document_type": "passport", "document_number": "X1234567",
"declared_annual_income": 95000, "address": "123 Main St, Columbus, OH",
"purchase_amount": 500
}' | python3 -m json.tool
# Execution Results
Handling connection for 8080
{
"customer_id": "CUST-7001",
"product_type": "gift_card",
"planned_steps": [
"gift_card_compliance"
],
"planner_reasoning": "This enquiry is for a gift card purchase. The only required specialist agent is gift_card_compliance to check the $500 amount against AML limits; no identity, background, or account-linking checks are needed for a straightforward gift card purchase.",
"decision": "APPROVED",
"customer_message": "Your gift card request has been approved.",
"reasons": [
"All required checks passed."
],
"step_results": {
"gift_card_compliance": {
"customer_id": "CUST-7001",
"status": "PASS",
"within_aml_limit": true,
"notes": "Purchase amount 500.0 vs AML threshold 2000.0."
}
}
}
# 2nd Execution
curl -s http://localhost:8080/orchestrator/health
Handling connection for 8080
# Execution results
{"agent":"dynamic-banking-orchestrator","status":"ok","registry_url":"http://agent-registry.banking-agents.svc.cluster.local:80"}
Barrages intégrés et limites de débit
Les filtres intégrés d’AgentGateway sont appelés barrages ou protections de prompt. Ils concernent le contenu du trafic lié aux LLM et non les en-têtes REST génériques, ce qui signifie qu’ils s’appliquent à une route /llm, à la connexion Foundry, mais pas au JSON brut de l’orchestrateur.
Le candidat évident pour une banque est le masquage des PII sur la route LLM. L’orchestrateur envoie des détails clients tels que les noms, dates de naissance, numéros de documents, revenus et adresses au modèle de planification via le prompt. Cependant, dans sa version actuelle, le code agentic n’utilise pas /llm pour tous les accès au modèle. Le gateway propose des barrages prêts à l’emploi pour les données sensibles sur cette route, mais cette implémentation appelle le modèle via une voie de type REST, ce qui empêche l’activation de ces protections. La solution appropriée consiste à envoyer les appels aux LLM par l’intermédiaire de /llm, ce qui reste à mettre en œuvre.
La limitation des débits est une politique de trafic générale qui s’applique directement à la route de l’orchestrateur :
kubectl apply -f- <<EOF
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
name: banking-orchestrator-ratelimit
namespace: agentgateway-system
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: banking-orchestrator-route
traffic:
rateLimit:
local:
- requests: 60
unit: Minutes
burst: 10
EOF
La valeur burst: 10 est optionnelle mais utile. Elle permet des pics temporaires au-dessus du niveau constant de 60 requêtes par minute, ce qui aide un client légitime envoyant plusieurs requêtes rapidement, sans modifier la limite continue. Vérifiez la politique appliquée :
kubectl get agentgatewaypolicy banking-orchestrator-ratelimit -n agentgateway-system
NAME ACCEPTED ATTACHED AGE
banking-orchestrator-ratelimit True True 62s
Essayez-le alors :
for i in $(seq 1 75); do
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/orchestrator/health
done | sort | uniq -c
# Execution results
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
............................
............................
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
70 200
5 429
Au total, les premières 70 requêtes, soit 60 de manière continue plus la marge autorisée pour les pics, devraient retourner 200, suivies de réponses 429 dans la même fenêtre de minute. Si toutes les requêtes retournent 200, soit la politique n’a pas été attachée correctement, auquel cas l’instruction kubectl describe agentgatewaypolicy banking-orchestrator-ratelimit -n agentgateway-system affichera tout erreur de résolution de targetRef, soit 75 requêtes n’ont tout simplement pas épuisé suffisamment rapidement la marge autorisée pour les pics. Augmenter le nombre de requêtes à 100 résout ce problème.
Cela conclut la mise en œuvre basée sur des agents. Il ne reste plus qu’à sécuriser l’accès et à mettre en place des contrôles entre les agents eux-mêmes.