Strona główna / Artykuły

Przepływy bankowe z wieloma agentami na Kubernetes: LangGraph, rejestr i AgentGateway

Stwórz specjalistycznych agentów FastAPI, orkiestratora LangGraph z planerem LLM, zaimplementuj je w klastrze z rejestratorem, a następnie dodaj routowanie i limity przepustowości za pomocą AgentGateway.

Wydawca: REACTAPP.TOP17 min czytania3826 słów

Źródła: agentgateway in the world of Agentic developement

MCP obejmuje przypadek komunikacji agent–narzędzie: stała funkcja wykonywa jedno konkretne zadanie i zwraca jego wynik, bez żadnej autonomii czy zdolności oceny ze strony narzędzia. Komunikacja między agentami to coś zupełnie innego – strona po drugiej stronie może samodzielnie rozumować, korzystając ewentualnie ze swojego własnego LLM, narzędzi i decyzji, a jeden agent przekazuje jej podzadanie i oczekuje uzasadnionej odpowiedzi, a nie tylko szybkiego wyszukiwania informacji.

Proces otwierania konta bankowego pokazuje charakter tego problemu. Obejmuje on kilka niezależnych aspektów: weryfikację tożsamości, historię klienta, sprawdzenia pochodzenia i przestępczości, zdolności finansowe oraz dane publiczne. Każdy z tych elementów mógłby rozsądnie należeć do innej grupy, być rozwijany oddzielnie i analizowany osobno. Właśnie do takich problemów służy architektura agentów. Przedstawiony poniżej przykład rozwija się od jednego agenta do zespołu specjalistów, obejmującego orkiestratora LangGraph z planerem LLM, implementację w Kubernetes, rejestr oraz ostatecznie bramkę wejściową.

Pięciu specjalistów, jeden wzorzec

Zespół zajmujący się otwieraniem kont ma pięciu specjalistycznych agentów. Każdy z nich to samodzielna usługa FastAPI działająca na własnym porcie, przy czym agent odpowiedzialny za weryfikację tożsamości ustala szablon: zwykły FastAPI z jednym punktem końcowym.

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)

Każdy agent w systemie stosuje ten sam schemat: model żądania Pydantic, model odpowiedzi Pydantic, jeden endpoint /invoke oraz jeden endpoint /health. Szybka weryfikacja:

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

Pozostałe usługi są budowane w dokładnie ten sam sposób, a na końcu istnieje osiem usług.

Pierwszy orkiestrator: połączenia równoległe

Pierwsza wersja orkiestratora wywoływała wszystkie osiem agentów jednocześnie za pomocą asyncio.gather i syntetyzowała decyzję na podstawie połączonych wyników:

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

To ograniczenie przyspieszyło rozwój projektu. Każdy agent był wywoływany za każdym razem, nawet gdy wcześniejsza weryfikacja już się nie powiodła. Niektóre sprawdzenia wiązały się z rzeczywistymi kosztami, ponieważ wyszukiwanie informacji w bazach tła i publicznych rejestrach zazwyczaj odbywa się za pośrednictwem płatnych usługodawców z zewnątrz, więc te powtarzane wywołania stanowiły czystą stratę. Kod również nie oddzielał wewnętrznego powodu podjętej decyzji od komunikatu pokazywanego klientowi. Obsługa błędów polegała na jednym ogólnym bloku try/except otaczającym wszystkie wywołania, zamiast na rozwiązaniach umożliwiających audyt każdego kroku.

Rozwiązaniem jest prawdziwy orkiestrator LangGraph, w którym każdy krok może sprawdzić to, co nastąpiło wcześniej, i odpowiednio skierować działanie. Funkcja pomocnicza wywołująca agenta nigdy nie powoduje błędów. Każda awaria oznaczana jest jako status='ERROR', dzięki czemu graf umożliwia jednolite przekierowanie błędów zamiast doprowadzenia do awarii całego systemu:

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

To proste rozwiązanie LangGraph z obsługą błędów włączoną we każdy krok. Nadal brakuje mu złożoności wymaganej przez rzeczywiste procesy.

Jeden planer LLM dla wielu produktów

Otwieranie konta to zaledwie jeden produkt bankowy. Bank zajmuje się również kartami kredytowymi, debetowymi i prezentowymi, a każdy z nich wymaga innego zestawu weryfikacji. Kodowanie warunkowe if/else dla każdego typu produktu nie jest skalowalne, dlatego planer LLM (model GPT przez Azure OpenAI) bierze zapytanie napisane prostym językiem angielskim wraz z katalogiem ofert poszczególnych agentów, a następnie wybiera odpowiednie agenty i ich kolejność działania. To jest rozumowanie, a nie logika rozgałęziania.

Dо pierwotnego zestawu dołączyły trzy agenty specyficzne dla poszczególnych produktów, co daje trzy procesy: otwieranie konta bankowego, łączenie karty debetowej oraz zakup karty prezentowej.

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

Egzekutor stał się jednym węzłem, który przetwarza plan wygenerowany przez LLM i nie kieruje się już ustaloną łańcuchem krawędzi grafu. Został sprawdzony bezpośrednio, omijając faktyczne wezwanie LLM, które wymaga aktualnych uprawnień Azure, poprzez podawanie mu ręcznie stworzonych planów:

  • Plan z kartą prezentową składający się z jednego kroku, gift_card_compliance, został zatwierdzony poniżej progu przeciwdziałania praniu pieniędzy, natomiast powyżej tego progu został odrzucony, bez uruchamiania żadnych innych kroków.
  • Plan z trzema krokami dotyczący karty kredytowej, obejmujący identity, financial_capability i credit_limit, wykonał wszystkie trzy kroki we właściwej kolejności i z odpowiednimi wynikami.

Niewielki lokalny skrypt uruchamia wszystkie agenty jednocześnie, zapisany jako 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
        }
    }
}

Gdy agenci są uruchomieni, wyślij zapytanie testowe:

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

Druga prośba uruchamia inny proces, a towarzyszący jej plik stop_agent.sh wyłącza wszystko:

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

Agenty działają lokalnie, ale jest jeszcze wiele do zrobienia. Następnie muszą działać za AgentGateway, przy użyciu usługi rejestru.

Rozwijanie w klastrze

Zacznij od nazwy przestrzeni dla wszystkich agentów, co ułatwi późniejsze ich znajdowanie. Nazywa się ona banking-agents:

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

Jeden plik Dockerfile na każdego agenta

Istnieją dwa pliki Dockerfile: jeden dla agentów aplikacji, a drugi dla rejestru i orkiestratora. Ponieważ każdy agent to zwykła aplikacja FastAPI o tej samej strukturze, jeden plik, Dockerfile.agent, służy im wszystkim:

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}

Rozwijanie agentów po jednym

Oto pełny proces dla pierwszego agenta, jego identyfikacji, a następnie te same trzy polecenia dla każdego z pozostałych:

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

Ten wzorzec powtarza się dla wszystkich ośmiu agentów, które przysłuchują się na portach od 9001 do 9008 i obejmują historię klienta, weryfikację pochodzenia, zdolności finansowe, dane publiczne, limit kredytowy, powiązanie kart oraz zgodność z zasadami kart prezentowych. Za każdym razem zmieniają się tylko nazwa, obraz i port. Następnie potwierdź kontenery:

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

Wszystkie osiem powinno pokazywać 1/1 Running.

Rozwijanie rejestru

Służba rejestru odwołuje się do każdej innej usługi w systemie. Dopóki nie będzie działać, agenty nie mają żadnej mapy pokazującej, jak się ze sobą łączą. Później dostęp do niego powinien zostać ograniczony, tak aby tylko agenty aplikacji mogły do niego uzyskać dostęp – to zapewnia bezpieczeństwo agentom i daje im jasny sposób komunikacji między sobą.

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 &

Sprawdź, czy może nawiązać połączenie z pozostałymi ośmioma agentami za pomocą nazwy DNS. Odpowiedzią jest mapa wszystkich usług aplikacyjnych, która pełni rolę rejestru zawierającego lokalizacje oraz opisy, co idealnie stwarza podstawy dla dalszych działań:

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

Rozwijanie orkiestratora

Udostępnienia Azure OpenAI są umieszczane jako tajemnica i nigdy nie są włączane bezpośrednio do obrazu:

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"

Następnie utwórz i wdroż obraz, stosując ten sam schemat co we wszystkich innych usługach:

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 &

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

W klastrze działa teraz funkcjonalny system wielu agentów z orkiestratorem, co oznacza duży krok naprzód w porównaniu z pojedynczym punktem końcowym.

Umieszczenie orkiestratora za bramką

Dlaczego w ogóle umieszczać orkiestratora za bramką? Istnieje pięć powodów:

  • Jeden stabilny punkt wejścia. Prawdziwa adresa URL, taka jak https://twoja-domena/orchestrator, zastępuje tunel, którego może używać tylko administrator Kubernetes. Każdy prawdziwy klient – od backendu mobilnego po inny usługę wewnętrzną lub integrację z partnerem – może do niego normalnie uzyskać dostęp.
  • Autoryzacja przed wykonywaniem kodu. Dzięki polityce JWT i RBAC z działań bezpieczeństwa, nieautoryzowany lub nieuprawniony użytkownik jest odrzucany przy bramce i w ogóle nie ma dostępu do logiki aplikacji orkiestratora.
  • Ograniczanie przepustowości. Klient, który zachowuje się niewłaściwie, lub błąd w jakimś komponentie wejściowym, może zasypać orchestrator żądaniami, przy czym każde z nich trafia do modelu planowania i powoduje wzrost rachunku Foundry. Brama może to ograniczyć.
  • Jedno miejsce do przeglądania całego ruchu. Logowanie i możliwość obserwacji każdego połączenia pochodzą z bramy, a nie z narzędzi wbudowanych w orchestratora.
  • Stabilna struktura przy zmianach. Można zamienić wersje orchestratora, dodać replikę lub przenieść go do innego usługi, a użytkownicy niczego nie zauważą, ponieważ znali tylko ścieżkę do bramy.
  • Wcześniejsze serwery MCP w bankowości podkreślały ten sam argument dotyczący narzędzi. Argument ten ma teraz wyższy stopień znaczenia, ponieważ „wejście” całego systemu wieloagentowego zasługuje na takie zarządzanie, jakie miały poszczególne agenty.

    Zwykły backend HTTP, taki jak orchestrator, bez konwersji MCP i bez konfiguracji dostawcy AI, nie wymaga AgentgatewayBackend. HTTPRoute może bezpośrednio wskazywać na usługę Kubernetes Service. Pozostaje jednak jeden problem: usługa orchestratora znajduje się w przestrzeni nazw banking-agents, podczas gdy HTTPRoute znajduje się w przestrzeni nazw agentgateway-system, czyli w innej przestrzeni. API bramy wymaga wyraźnego ReferenceGrant w docelowej przestrzeni nazw, zanim pozwoli na odwołanie międzyprzestrzeniowe.

    Najpierw należy zezwolić na odwołanie międzyprzestrzeniowe:

    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
    

    Po drugie, skieruj bezpośrednio do usługi:

    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
    

    Następnie sprawdź przez bramkę:

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

    Wbudowane zasady bezpieczeństwa i ograniczenia szybkości

    Wbudowane filtry AgentGateway nazywane są zasadami bezpieczeństwa lub zabezpieczeniami promptów. Dotyczą one treści ruchu związanych z LLM, a nie ogólnych danych przekazywanych w formacie REST, więc mają zastosowanie do ścieżki /llm oraz połączenia z Foundry, ale nie do zwykłych plików JSON używanych przez orkiestratora.

    Oczywistym rozwiązaniem dla banków jest maskowanie danych osobowych w ramach ścieżki LLM. Orkiestrator wysyła informacje o klientach, takie jak imiona, daty urodzenia, numery dokumentów, dochód i adresy, do modelu planowania zawartego w promptzie. Jednak obecny kod nie wykorzystuje ścieżki /llm do dostępu do wszystkich modeli. Gateway oferuje gotowe zasady bezpieczeństwa dla danych wrażliwych na tej ścieżce, ale ta implementacja wywołuje model za pomocą ścieżki w stylu REST, więc te zabezpieczenia nigdy się nie aktywują. Prawidłowym rozwiązaniem jest wysyłanie żądań do LLM przez ścieżkę /llm, co jednak jeszcze nie zostało zrealizowane.

    Ograniczanie szybkości to ogólna polityka ruchu, która ma bezpośrednie zastosowanie do ścieżki orkiestratora:

    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
    

    Ustawienie burst: 10 jest opcjonalne, ale przydatne. Pozwala na krótkotrwałe wzrosty liczby żądań powyżej stałych 60 na minutę, co pomaga prawomocnemu klientowi wysyłającemu kilka żądań w krótkim czasie, bez zmiany stałego limitu. Sprawdź, jaka polityka jest stosowana:

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

    Następnie spróbuj tego:

    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
    

    Mniej więcej pierwsze 70 żądań, 60 regularnych plus dopuszczalna ilość żądań jednorazowych, powinno zwracać wartość 200, a w tym samym oknie czasowym po nich powinny pojawić się odpowiedzi 429. Jeśli wszystko zwraca wartość 200, albo polityka nie została prawidłowo przyłączona – w takim przypadku polecenie kubectl describe agentgatewaypolicy banking-orchestrator-ratelimit -n agentgateway-system pokaże błąd w rozwiązywaniu targetRef, albo 75 żądań po prostu nie wyczerpało dostępnej ilości żądań jednorazowych wystarczająco szybko. Zwiększenie liczby żądań do 100 rozwiązuje ten problem.

    To zamyka implementację opartą na agentach. Pozostaje już tylko zabezpieczenie dostępu oraz dodanie ograniczeń pomiędzy samymi agentami.

    Literatura pokrewna