Inicio / Artículos

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.

Editor: REACTAPP.TOP17 min de lectura3826 palabras

Fuentes: agentgateway in the world of Agentic developement

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_capability y credit_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/orchestrator reemplaza 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.
  • Regulación de la carga. Un cliente que se comporta de manera inadecuada, o un error en algún llamante anterior, podría saturar al orquestador con solicitudes; cada una de ellas llega al modelo de planificación y aumenta la factura de Foundry. La pasarela puede controlar eso.
  • Un único lugar para ver todo el tráfico. El registro y la capacidad de observación de cada llamada provienen de la pasarela, en lugar de de las herramientas integradas dentro del orquestador.
  • Una interfaz estable cuando hay cambios. Al cambiar las versiones del orquestador, agregar una réplica o migrar a otro servicio, los llamantes no se dan cuenta de nada, ya que solo conocen la ruta de la pasarela.
  • 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