Flujos bancarios multiagente en Kubernetes: LangGraph, un registro y AgentGateway
Cree agentes especializados en FastAPI, un orquestador de LangGraph con un planificador basado en LLM, despliéguelos en un clúster con un registro, y luego agregue enrutamiento y límites de velocidad mediante AgentGateway.
MCP abarca el caso agente-herramienta: una función fija realiza una tarea específica y devuelve su resultado, sin autonomía ni capacidad de juicio por parte de la herramienta. La comunicación entre agentes es algo distinto; la parte del otro lado puede razonar por sí misma, posiblemente con su propio LLM, sus propias herramientas y sus propias decisiones, y un agente le asigna una subtarea esperando una respuesta fundamentada, no una simple búsqueda.
El proceso de apertura de cuentas bancarias ilustra la naturaleza del problema. Involucra varios aspectos independientes: verificación de identidad, historial del cliente, verificaciones de antecedentes y delictivos, capacidad financiera y registros públicos. Cada uno de ellos podría razonablemente pertenecer a un equipo diferente, desarrollarse por separado y ser analizado de forma independiente. Ese es el tipo de problema para el cual existe la arquitectura agente-a-agente. La estructura presentada a continuación evoluciona desde un único agente hasta un equipo de especialistas, un orquestador LangGraph con un planificador LLM, una implementación en Kubernetes, un registro y, finalmente, una pasarela de entrada.
Cinco especialistas, un patrón
El equipo encargado de abrir cuentas cuenta con cinco agentes especialistas. Cada uno es un servicio FastAPI autónomo en su propio puerto, y el agente de verificación de identidad establece el modelo base: un FastAPI sencillo con un único punto de conexión.
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)
Cada agente en el sistema sigue este mismo patrón: un modelo de solicitud de Pydantic, un modelo de respuesta de Pydantic, un endpoint /invoke y un endpoint /health. Una verificación rápida:
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"}'
Los servicios restantes se construyen de exactamente la misma manera, y al final existen ocho servicios.
Primer orquestador: llamadas en paralelo
La primera versión del orquestador llamó a los ocho agentes simultáneamente con asyncio.gather y sintetizó una decisión a partir de los resultados combinados:
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
La limitación impulsó el desarrollo del diseño. Se llamaba a cada agente en cada ocasión, incluso cuando una verificación previa ya había fallado. Algunas verificaciones costaban dinero real, ya que las búsquedas en fuentes externas y registros públicos suelen provenir de proveedores pagados, por lo que las llamadas repetidas eran una simple pérdida de recursos. El código tampoco separaba la razón interna de una decisión del mensaje que se mostraba al cliente. El manejo de errores se reducía a un único bloque try/except que envolvía todas las llamadas, en lugar de un sistema que permitiera auditar cada paso por separado.
La solución es un orquestador real de LangGraph, donde cada paso puede analizar lo que ocurrió anteriormente y redirigir la acción en consecuencia. El ayudante que llama a un agente nunca genera excepciones. Cada fallo se convierte en status='ERROR', de modo que el sistema redirige los fallos de manera uniforme en lugar de colapsar:
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()
Se trata de una solución sencilla de LangGraph con manejo de errores integrado en cada paso. Sin embargo, aún carece de la complejidad que exigen los flujos reales.
Un planificador de LLM para muchos productos
La apertura de cuentas es solo uno de los productos bancarios. Un banco también maneja tarjetas de crédito, tarjetas de débito y tarjetas regalo, y cada una requiere un conjunto diferente de verificaciones. Codificar manualmente condiciones if/else por tipo de producto no es escalable, por lo que un planificador de LLM (un modelo GPT a través de Azure OpenAI) toma una consulta en inglés sencillo junto con un catálogo de lo que ofrece cada agente, y luego selecciona los agentes relevantes y el orden en que deben actuar. Eso es razonamiento, no lógica de ramificación.
Tres agentes específicos para cada producto se añadieron al conjunto original, lo que genera tres flujos: abrir una cuenta bancaria, vincular una tarjeta de débito y comprar una tarjeta regalo.
# 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)
El ejecutor pasó a ser un nodo que recorre el plan generado por el LLM y ya no sigue una cadena fija de aristas del grafo. Se verificó directamente, evitando la llamada al LLM real ya que esta requiere credenciales activas de Azure, al alimentarlo con planes creados manualmente:
- Un plan de tarjeta regalo con un solo paso,
gift_card_compliance, fue aprobado dentro del umbral contra el lavado de dinero y rechazado si excedía dicho umbral, sin que se llamara a ningún otro paso. - Un plan de tarjeta de crédito de tres pasos,
identity,financial_capabilityycredit_limit, ejecutó los tres pasos en el orden correcto con los resultados adecuados.
Un pequeño script local inicia a cada agente al mismo tiempo; se guarda como 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
}
}
}
Con los agentes en ejecución, envíe una consulta de prueba:
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."
}
}
}
Una segunda solicitud activa otro flujo, y un archivo stop_agent.sh complementario detiene todo:
# 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]
Los agentes funcionan de forma local, pero aún queda trabajo por hacer. A continuación, deben operar detrás de AgentGateway, con un servicio de registro junto a él.
Despliegue en un clúster
Comience con un espacio de nombres para todos los agentes, lo que facilita su detección posterior. Se denomina banking-agents:
kubectl get namespace banking-agents 2>/dev/null || kubectl create namespace banking-agents
Un Dockerfile por cada agente
Existen dos Dockerfiles: uno para los agentes de la aplicación y otro para el registro y el orquestador. Dado que cada agente es una aplicación FastAPI sencilla con la misma estructura, un único archivo, Dockerfile.agent, sirve para todos ellos:
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}
Desplegando agentes uno por uno
Aquí se muestra el flujo completo para el primer agente, su identidad, seguido de los mismos tres comandos para cada uno de los demás:
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
El patrón se repite para los ocho agentes, que escuchan en los puertos del 9001 al 9008 y abarcan el historial del cliente, verificación de antecedentes, capacidad financiera, registros públicos, límite de crédito, vinculación de tarjetas y cumplimiento con las tarjetas regalo. Solo el nombre, la imagen y el puerto cambian cada vez. Luego confirme los 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
Todos los ocho deberían mostrar 1/1 Running.
Despliegue del registro
El servicio de registro hace referencia a todos los demás servicios del sistema. Hasta que esté activo, los agentes no cuentan con un mapa que muestre cómo conectarse. Más adelante, el acceso a él deberá ser restringido para que solo los agentes de las aplicaciones puedan acceder, lo cual mantiene seguros a los agentes y les proporciona una forma clara de comunicarse entre sí.
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 &
Verifique que pueda llegar a los otros ocho agentes mediante el nombre DNS. La respuesta es un mapa de todos los servicios de la aplicación, que funciona como un registro que contiene las ubicaciones y también descripciones, lo cual idealmente allana el camino para las habilidades necesarias:
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"
}
}
]
}
Despliegue del orquestador
Las credenciales de Azure OpenAI se ingresan como un Secreto y nunca se incluyen directamente en la imagen:
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"
Luego, construya e implemente la imagen siguiendo el mismo patrón que cualquier otro servicio:
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 &
Es hora de hacer una prueba:
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."
}
}
}
Actualmente, en el clúster funciona un sistema multiagente operativo con un orquestador, lo cual está muy lejos de ser un único punto de acceso.
Colocar el orquestador detrás del gateway
¿Por qué exponer el orquestador a través de un gateway? Hay cinco razones:
- Un punto de entrada estable. Una URL real como
https://your-domain/orchestratorreemplaza a un túnel al que solo puede acceder el administrador de Kubernetes. Cualquier cliente real, ya sea desde un backend móvil, otro servicio interno o una integración con un socio, puede llegar a él de forma normal. - Autenticación antes del código. Gracias a las políticas JWT y RBAC implementadas en el área de seguridad, los usuarios no autenticados o no autorizados son rechazados en el gateway y nunca llegan a tocar la lógica de aplicación del orquestador.
Los servidores MCP bancarios anteriores ya planteaban este mismo punto para las herramientas. El argumento se eleva a un nivel superior, ya que la entrada principal de todo un sistema multiagente merece la misma gobernanza que tenían los agentes individuales.
Un backend HTTP simple como el orquestador, sin traducción MCP ni configuración de proveedor de IA, no necesita un AgentgatewayBackend. Un HTTPRoute puede apuntar directamente a un Service de Kubernetes. Queda un problema: el Service del orquestador se encuentra en banking-agents mientras que el HTTPRoute está en agentgateway-system, un namespace diferente. La API de gateway exige un ReferenceGrant explícito en el namespace destino antes de permitir una referencia entre namespaces.
Primero, habilitar la referencia entre namespaces:
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
En segundo lugar, diríjase directamente al Servicio:
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
Luego verifique a través de la pasarela:
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"}
Barrieras integradas y límites de tasa
Los filtros integrados de AgentGateway se denominan barrieras o protectores de prompts. Se refieren al contenido del tráfico de LLM y no a las cargas útiles genéricas de REST, por lo que se aplican a la ruta /llm, a la conexión con Foundry y no al JSON simple del orquestador.
El candidato obvio para un banco es el enmascaramiento de PII en la ruta de LLM. El orquestador envía detalles del cliente como nombres, fechas de nacimiento, números de documentos, ingresos y direcciones al modelo de planificación dentro del prompt. Sin embargo, tal como está implementado, el código agente no utiliza /llm para todo el acceso a los modelos. La pasarela ofrece barrieras listas para usar en relación con datos sensibles en esa ruta, pero esta implementación llama al modelo a través de una ruta de estilo REST, por lo que esas protecciones nunca se activan. La solución adecuada es enviar las llamadas a LLM a través de /llm, un cambio que aún está por realizarse.
El límite de tasas es una política general de tráfico que se aplica directamente a la ruta del orquestador:
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 configuración burst: 10 es opcional pero útil. Permite picos breves por encima del límite constante de 60 solicitudes por minuto, lo cual ayuda a un cliente legítimo que envía varias solicitudes seguidas, sin cambiar el límite sostenido. Verifique qué política se está aplicando:
kubectl get agentgatewaypolicy banking-orchestrator-ratelimit -n agentgateway-system
NAME ACCEPTED ATTACHED AGE
banking-orchestrator-ratelimit True True 62s
Luego inténtelo:
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
Aproximadamente las primeras 70 solicitudes, más 60 de forma continua junto con el límite adicional para picos, deberían devolver 200; después de eso, se obtendrán respuestas 429 dentro de la misma ventana de un minuto. Si todas devuelven 200, o bien la política no se aplicó correctamente, en cuyo caso kubectl describe agentgatewaypolicy banking-orchestrator-ratelimit -n agentgateway-system mostrará cualquier error de resolución de targetRef, o bien 75 solicitudes simplemente no agotaron el límite adicional lo suficientemente rápido. Aumentar el número de solicitudes a 100 soluciona este problema.
Con esto se completa la implementación basada en agentes. Lo que queda es asegurar el acceso y establecer medidas de control entre los propios agentes.
Lecturas relacionadas
- Memory semántica de agente en LangGraph con Qdrant y recuperación entre hilos — Proporcionar a un agente de LangGraph memoria a largo plazo con Qdrant: incrustar datos del usuario, recuperarlos mediante IDs de hilo y agregar filtros de carga útil para la isolación multiinquilino.
- RAG agente en LangGraph: evaluadores, enrutamiento condicional y solución de respaldo mediante búsqueda web — Transferir un pipeline lineal RAG de LangChain a un StateGraph de LangGraph con evaluadores de LLM, entradas y bordes condicionales, pruebas y un endpoint FastAPI que indica el origen de los datos.