Startseite / Artikel

Multi-Agenten-Banking-Flüsse auf Kubernetes: LangGraph, ein Registry und AgentGateway

Erstellen Sie FastAPI-Spezialisten-Agenten sowie einen LangGraph-Orchestrator mit einem LLM-Planer, deployen Sie sie in einen Cluster mitsamt Registry und fügen Sie anschließend mit AgentGateway Routing- sowie Rate-Limit-Funktionen hinzu.

Verlag: REACTAPP.TOP17 Min. Lesezeit3826 Wörter

Quellen: agentgateway in the world of Agentic developement

MCP bezieht sich auf den Fall Agent-zu-Tool: Eine feste Funktion erledigt eine spezifische Aufgabe und gibt ihr Ergebnis zurück, wobei die Tool-Seite keine Autonomie oder Urteilsfähigkeit besitzt. Die Kommunikation zwischen Agenten ist etwas anderes – die andere Seite kann für sich selbst entscheiden, möglicherweise mit ihrem eigenen LLM, ihren eigenen Tools und ihren eigenen Entscheidungen. Ein Agent überträgt ihr eine Unteraufgabe und erwartet eine begründete Antwort, nicht nur einen Suchergebnis.

Der Prozess der Eröffnung eines Bankkontos zeigt die Struktur des Problems. Er umfasst mehrere unabhängige Aspekte: Identitätsprüfung, Kundenhistorie, Hintergrund- und Kriminalüberprüfungen, finanzielle Leistungsfähigkeit sowie öffentliche Unterlagen. Jeder dieser Aspekte könnte durchaus einer anderen Abteilung zugeordnet werden, getrennt entwickelt und schließlich separat bewertet werden. Genau für solche Probleme existiert eine Agent-zu-Agent-Architektur. Die unten dargestellte Struktur entwickelt sich von einem einzelnen Agenten zu einem Team von Spezialisten – einem LangGraph-Orchestrator mit einem LLM-Planer, einer Kubernetes-Einsatzumgebung, einem Registry-System und schließlich einer vorgelagerten Gateway-Schicht.

Fünf Spezialisten, ein Muster

Das Team zur Eröffnung von Konten besteht aus fünf spezialisierten Agenten. Jeder ist ein eigenständiger FastAPI-Dienst auf seiner eigenen Portnummer, wobei der Agent für die Identitätsprüfung das Grundmuster vorgibt: ein einfacher FastAPI-Dienst mit einem einzigen Endpunkt.

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)

Jeder Agent im System folgt diesemselben Muster: ein Pydantic-Anfragemodell, ein Pydantic-Antwortmodell, eine /invoke-Endpunkt und eine /health-Endpunkt. Eine schnelle Überprüfung:

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"}'

Die verbleibenden Dienste werden genauso erstellt, sodass am Ende insgesamt acht Dienste vorhanden sind.

Erster Orchestrator: parallele Aufrufe

Die erste Version des Orchestrators rief alle acht Agenten gleichzeitig mit asyncio.gather auf und synthetisierte eine Entscheidung aus den kombinierten Ergebnissen:

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

Die Einschränkung trieb die Weiterentwicklung des Designs voran. Jeder Agent wurde jedes Mal aufgerufen, selbst wenn eine frühe Überprüfung bereits fehlgeschlagen war. Einige Überprüfungen kosteten tatsächlich Geld, da Abfragen in Hintergrundsystemen sowie öffentlichen Registern in der Regel von bezahlten externen Anbietern durchgeführt werden, wodurch die wiederholten Aufrufe reine Verschwendung darstellten. Zudem trennte der Code den internen Grund für eine Entscheidung nicht von der dem Kunden angezeigten Nachricht. Die Fehlerbehandlung beschränkte sich auf ein generisches try/except, das alle Aufrufe umschloss, anstatt eine für jeden Schritt nachvollziehbare Struktur zu verwenden.

Die Lösung besteht in einem echten LangGraph-Orchestrator, bei dem jeder Schritt prüfen kann, was zuvor geschah, und entsprechend weiterleiten kann. Der Hilfsprozess, der einen Agenten aufruft, wirft niemals Fehler aus. Jeder Fehler wird als status='ERROR' gekennzeichnet, sodass das System die Fehler einheitlich weiterleitet, anstatt abzustürzen:

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()

Es handelt sich um eine einfache LangGraph-Lösung, bei der Fehlerbehandlung in jedem Schritt integriert ist. Sie fehlt jedoch noch an der Komplexität, die reale Abläufe erfordern.

Ein LLM-Planer für viele Produkte

Die Eröffnung eines Kontos ist nur ein Bankprodukt. Eine Bank kümmert sich außerdem um Kreditkarten, Debitkarten und Geschenkkarten – jeweils mit unterschiedlichen Überprüfungen. Das Härten von if/else-Anweisungen pro Produkttyp ist nicht skalierbar; daher nutzt ein LLM-Planer (ein GPT-Modell über Azure OpenAI) eine Anfrage in einfachem Englisch zusammen mit einem Katalog dessen, was jede Agentur anbietet, und wählt anschließend die relevanten Agenturen sowie deren Reihenfolge aus. Das ist logisches Denken, keine verzweigte Logik.

Drei produktbezogene Agenturen wurden zur ursprünglichen Gruppe hinzugefügt, was drei Abläufe ergibt: Eröffnung eines Bankkontos, Verknüpfung einer Debitkarte und Kauf einer Geschenkkarte.

# 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)

Der Ausführende wurde zu einem Knoten, der durch jeden Plan läuft, den das LLM erzeugt, und folgt nicht mehr einer festen Kette von Graphenkanten. Es wurde direkt überprüft, wobei der eigentliche Aufruf des LLM umgangen wurde, da dieser aktuelle Azure-Anmeldeinformationen erfordert – stattdessen wurden handgefertigte Pläne eingegeben:

  • Ein Plan für Gutscheinkarten mit einem Schritt, gift_card_compliance, wurde unterhalb der Schwellenwert für die Geldwäscherkennung genehmigt und darüber hinaus abgelehnt, wobei keine weiteren Schritte ausgeführt wurden.
  • Ein dreistufiger Kreditkarten-Plan mit den Schritten identity, financial_capability und credit_limit führte alle drei Schritte in der richtigen Reihenfolge mit den richtigen Ergebnissen aus.

Ein kleines lokales Skript startet alle Agenten gleichzeitig und ist als start_agent.sh gespeichert:

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
        }
    }
}

Sobald die Agenten laufen, senden Sie eine Testanfrage:

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."
        }
    }
}

Eine zweite Anfrage aktiviert einen anderen Ablauf, und das zugehörige stop_agent.sh-Skript schaltet alles ab:

# 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]

Die Agenten arbeiten lokal, aber es gibt noch mehr zu tun. Als Nächstes müssen sie hinter AgentGateway laufen, zusammen mit einem Registry-Dienst.

Einsatz in einem Cluster

Fangen Sie mit einem Namespace für alle Agenten an, damit eine spätere Erkennung einfacher fällt. Er wird banking-agents genannt:

kubectl get namespace banking-agents 2>/dev/null || kubectl create namespace banking-agents

Ein Dockerfile pro Agent

Es gibt zwei Dockerfiles: eines für die Anwendungsagenten und eines für das Registry sowie den Orchestrator. Da jeder Agent eine einfache FastAPI-Anwendung mit derselben Struktur ist, dient ein einziger Datei, Dockerfile.agent, allen davon:

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}

Einzelne Agenten bereitstellen

Hier ist der vollständige Ablauf für den ersten Agenten, die Identität, gefolgt von denselben drei Befehlen für jeden der anderen:

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

Das Muster wiederholt sich für alle acht Agenten, die auf Ports 9001 bis 9008 lauschen und den Kundenverlauf, Hintergrundprüfungen, finanzielle Verhältnisse, öffentliche Unterlagen, Kreditlimit, Kartenverknüpfung sowie die Einhaltung von Gesetzen bezüglich Gutscheinkarten überprüfen. Jedes Mal ändern sich nur der Name, das Bild und der Port. Bestätigen Sie anschließend die 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

Sie alle acht sollten 1/1 Running anzeigen.

Einrichten des Registries

Dienste, die auf das Registry zugreifen, beziehen sich auf alle anderen Dienste im System. Solange das Registry nicht verfügbar ist, haben die Agenten keine Übersicht darüber, wie sie miteinander verbunden sind. Später sollte der Zugriff darauf eingeschränkt werden, sodass nur Anwendungsagenten darauf zugreifen können – dadurch bleiben die Agenten sicher und haben eine klare Möglichkeit, miteinander zu kommunizieren.

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 &

Überprüfen Sie, ob es über den DNS-Namen auf die anderen acht Agenten zugreifen kann. Die Antwort ist eine Karte aller Anwendungsdienste, die als Register dient, in dem sich Standorte sowie Beschreibungen befinden – idealerweise schafft dies die Voraussetzungen für weitere Funktionen:

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"
            }
        }
    ]
}

Orchestrierer bereitstellen

Azure OpenAI-Zugangsdaten werden als Geheimnis gespeichert und niemals in das Image eingebettet:

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"

Dann erstellen und bereitstellen Sie das Image nach dem gleichen Muster wie alle anderen Dienste:

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 &

Zeit für einen 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."
        }
    }
}

In dem Cluster läuft nun ein funktionsfähiges Mehr-Agenten-System mit einem Orchestrator – was schon weit entfernt von einem einzelnen Endpunkt ist.

Der Orchestrator hinter dem Gateway platzieren

Warum überhaupt den Orchestrator hinter einem Gateway verbergen? Es gibt fünf Gründe:

  • Ein stabiler Eingangspunkt. Eine echte URL wie https://your-domain/orchestrator ersetzt einen Tunnel, den nur ein Kubernetes-Admin nutzen kann. Jeder echte Client – sei es ein mobiler Backend, ein anderer interner Dienst oder eine Partnerintegration – kann normal darauf zugreifen.
  • Authentifizierung vor dem Code. Dank der JWT- und RBAC-Strategien aus den Sicherheitsmaßnahmen wird ein unauthentifizierter oder nicht autorisierter Aufrufer am Gateway abgewiesen und erreicht niemals die Anwendungslogik des Orchestrators.
  • Drosselung. Ein fehlerhaftes Client-Programm oder ein Fehler in einem vorangegangenen Aufrufer kann den Orchestrator mit Anfragen überfluten, wodurch jede einzelne Anfrage das Planungsmodell erreicht und zur Rechnung von Foundry beiträgt. Die Gateway-Instanz kann dies begrenzen.
  • Ein zentraler Ort zur Überwachung des gesamten Datenverkehrs. Die Protokollierung und Überwachung jeder Anfrage erfolgt über die Gateway-Instanz statt durch Instrumentierung innerhalb des Orchestrators.
  • Eine stabile Schnittstelle bei Änderungen. Selbst wenn Versionen des Orchestrators ausgetauscht, eine Replik hinzugefügt oder der Betrieb auf einen anderen Service verlagert wird, bemerken die Aufrufer nichts, da sie stets nur den Pfad zur Gateway-Instanz kannten.
  • Auch die früheren Banking-MCP-Server betonten diesen Punkt für Tools. Der Argumentationsansatz gilt nun auf einer höheren Ebene, da die Eingangsstelle eines gesamten mehragentigen Systems dieselbe Steuerung verdient wie die einzelnen Agenten.

    Ein einfacher HTTP-Backend wie der Orchestrator, ohne MCP-Umsetzung und ohne Konfiguration eines AI-Anbieters, benötigt keinen AgentgatewayBackend. Ein HTTPRoute kann direkt auf einen Kubernetes-Service verweisen. Es bleibt jedoch ein Problem: Der Service des Orchestrators befindet sich im Namespace banking-agents, während der HTTPRoute im Namespace agentgateway-system liegt, einem anderen Namespace. Die Gateway-API erfordert eine explizite ReferenceGrant im Ziel-Namespace, bevor ein Referenzverweis zwischen Namespace zugelassen wird.

    Zuerst muss der Referenzverweis zwischen den Namespace ermöglicht werden:

    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
    

    Zweitens: Leiten Sie direkt zum Service weiter:

    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
    

    Dann überprüfen Sie es über das Gateway:

    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"}
    

    Integrierte Schutzmechanismen und Rate Limits

    Die integrierten Filter von AgentGateway werden als Schutzmechanismen bezeichnet. Sie betreffen den Inhalt des LLM-Verkehrs und nicht allgemeine REST-Datenpakete, weshalb sie auf der /llm-Route sowie bei der Foundry-Verbindung Anwendung finden, aber nicht auf dem reinen JSON des Orchestriersystems.

    Ein offensichtlicher Ansatz für Banken ist das Maskieren personenbezogener Daten auf der LLM-Route. Das Orchestriersystem sendet Kundendaten wie Namen, Geburtsdaten, Dokumentnummern, Einkommen und Adressen zusammen mit Anweisungen an das Planungsmodell. Der derzeitige Code verwendet jedoch nicht immer /llm für den Zugriff auf die Modelle. Die Gateway-Lösung bietet zwar fertige Schutzmechanismen für sensible Daten auf dieser Route, doch die aktuelle Implementierung ruft das Modell über einen REST-ähnlichen Pfad auf, wodurch diese Schutzmaßnahmen nie aktiviert werden. Die richtige Lösung besteht darin, die LLM-Aufrufe über /llm zu senden – eine Änderung, die noch vorgenommen werden muss.

    Rate Limiting ist eine allgemeine Verkehrsrichtlinie und gilt direkt für die Orchestrator-Route:

    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
    

    Die Einstellung burst: 10 ist optional, aber nützlich. Sie ermöglicht kurze Anstiege über den konstanten Wert von 60 Anfragen pro Minute, was einem legitimen Client hilft, der mehrere Anfragen nacheinander sendet, ohne die dauerhafte Grenze zu ändern. Überprüfen Sie, welche Richtlinie angewendet wird:

    kubectl get agentgatewaypolicy banking-orchestrator-ratelimit -n agentgateway-system
    NAME                             ACCEPTED   ATTACHED   AGE
    banking-orchestrator-ratelimit   True       True       62s
    

    Dann versuchen Sie es:

    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
    

    Ungefähr die ersten 70 Anfragen, 60 konstante Anfragen plus der Puffer für plötzliche Spitzen, sollten 200 zurückgeben, wobei in demselben Zeitfenster danach 429-Antworten auftreten. Wenn alle Anfragen 200 zurückgeben, liegt entweder ein Problem bei der ordnungsgemäßen Anwendung der Richtlinie vor – in diesem Fall zeigt kubectl describe agentgatewaypolicy banking-orchestrator-ratelimit -n agentgateway-system Fehler bei der Auflösung des targetRef – oder 75 Anfragen haben den Puffer für plötzliche Spitzen einfach nicht schnell genug aufgebraucht. Eine Erhöhung der Anzahl der Anfragen auf 100 behebt dieses Problem.

    Dadurch ist die Implementierung mit Agenten abgeschlossen. Übrig bleibt noch der Schutz des Zugriffs sowie das Hinzufügen von Sicherheitsmaßnahmen zwischen den Agenten selbst.