Главная / Статьи

Многопроцессные банковские потоки на Kubernetes: LangGraph, реестр и AgentGateway

Создайте специализированных агентов FastAPI, оркестратор LangGraph с планировщиком на основе LLM, разверните их в кластере с реестром, а затем добавьте маршрутизацию и ограничения по скорости с помощью AgentGateway.

Издатель: REACTAPP.TOP17 мин чтения3826 слов

Источники: agentgateway in the world of Agentic developement

MCP охватывает сценарий взаимодействия агента и инструмента: фиксированная функция выполняет одну конкретную задачу и возвращает результат, при этом инструмент не обладает автономией и способностью к принятию решений. Коммуникация между агентами — это совершенно другое явление. Сторона на другом конце может самостоятельно рассуждать, возможно, с использованием собственного большого языкового моделя, собственных инструментов и собственных решений; один агент передаёт ей подзадачу и ожидает обоснованного ответа, а не просто результат поиска.

Процесс открытия банковского счёта демонстрирует суть проблемы. Он включает несколько независимых аспектов: проверку личности, историю клиента, проверку биографических данных и криминального прошлого, финансовую состоятельность и государственные записи. Каждый из этих аспектов разумно может относиться к разной команде, разрабатываться отдельно и рассматриваться по отдельности. Именно для решения подобных проблем существует архитектура агентов. Приведённая ниже структура развивается от одного агента до команды специалистов: оркестратора LangGraph с планировщиком на основе LLM, развертывания в Kubernetes, реестра и, наконец, шлюза на входе.

Пять специалистов, одна схема

Команда по открытию счёта состоит из пяти специализированных агентов. Каждый из них — самостоятельный сервис FastAPI на своём порту, причём агент для проверки личности задаёт шаблон: простой FastAPI с одним эндпоинтом.

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)

Каждый агент в системе следует одинаковой структуре: модель запроса Pydantic, модель ответа Pydantic, один конечный пункт /invoke и один конечный пункт /health. Краткая проверка:

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

Остальные сервисы создаются точно таким же образом, и в итоге получается восемь сервисов.

Первый оркестратор: параллельные вызовы

Первая версия оркестратора одновременно вызывала все восемь агентов с помощью asyncio.gather и формировала решение на основе совокупных результатов:

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

Это ограничение стимулировало разработку. Каждый агент вызывался при каждой попытке, даже когда предварительная проверка уже провалилась. Некоторые проверки стоили реальных денег, поскольку поиск в базах данных и публичных реестрах обычно осуществлялся через платных внешних поставщиков, поэтому повторные вызовы были просто пустой тратой ресурсов. Код также не разделял внутренние причины принятия решения и сообщение, отображаемое клиенту. Обработка ошибок сводилась к одному общему блоку try/except, охватывающему все вызовы, вместо отдельной обработки каждого шага.

Решением является настоящий оркестратор LangGraph, позволяющий на каждом этапе анализировать предыдущие действия и направлять работу соответственно. Вспомогательный модуль, вызывающий агента, никогда не вызывает исключений. Каждая ошибка приводит к значению status='ERROR', благодаря чему система равномерно обрабатывает сбои вместо того, чтобы выходить из строя:

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

Это простое решение на LangGraph с обработкой ошибок, встроенной в каждый шаг. Однако оно всё ещё не обладает сложностью, требуемой для реальных процессов.

Один планировщик на основе LLM для множества продуктов

Открытие счета — это лишь один из банковских продуктов. Банк также обслуживает кредитные карты, дебетовые карты и подарочные карты, причем для каждого из них требуется свой набор проверок. Жесткое задание условий if/else для каждого типа продукта не позволяет масштабироваться, поэтому планировщик на основе LLM (модель GPT через Azure OpenAI) принимает запрос на простом английском языке вместе с каталогом того, что предлагает каждый агент, затем выбирает соответствующих агентов и порядок их действий. Это рассуждение, а не логика ветвления.

К первоначальному набору добавились три агента, специализирующихся на конкретных продуктах, что позволяет реализовать три процесса: открытие банковского счета, привязка дебетовой карты и покупка подарочной карты.

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

Экзекутор превратился в один узел, который проходит по любому плану, сгенерированному большой языковой моделью, и более не следует фиксированной цепи рёбер графа. Его тестировали напрямую, обходя реальный вызов большой языковой модели, поскольку этот требует актуальных учетных данных Azure, путём подачи в него вручную созданных планов:

  • План с подарочной картой, состоящий из одного шага gift_card_compliance, был одобрен при уровне, не превышающем порог для борьбы с отмыванием денег, и отклонён при превышении этого уровня; больше никакие шаги не выполнялись.
  • Трёхшаговый план с использованием кредитной карты, включающий шаги identity, financial_capability и credit_limit, выполнил все три шага в правильном порядке с соответствующими результатами.

Небольшой локальный скрипт запускает каждого агента одновременно; он сохранён как 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
        }
    }
}

При запущенных агентах отправьте тестовый запрос:

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

Второй запрос запускает другой алгоритм обработки, а сопутствующий файл stop_agent.sh останавливает всё:

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

Агенты работают локально, но дел ещё много. Далее им необходимо запуститься через AgentGateway с сопутствующей службой реестра.

Развертывание в кластере

Начните с пространства имён для всех агентов, что облегчит их последующее обнаружение. Оно называется banking-agents:

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

Один Dockerfile на каждого агента

Существует два Dockerfile: один для агентов приложений, а другой — для реестра и оркестратора. Поскольку каждый агент представляет собой обычное приложение FastAPI с одинаковой структурой, один файл Dockerfile.agent подходит для всех них:

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}

Развертывание агентов по одному

Вот полный алгоритм для первого агента — определение его идентичности, после чего для каждого последующего агента выполняются те же три команды:

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

Эта схема повторяется для всех восьми агентов, которые прослушивают порты с 9001 по 9008 и обрабатывают информацию о истории клиента, проверке биографических данных, финансовых возможностях, государственных записях, лимите кредита, связке карт и соответствии правилам использования подарочных карт. Каждый раз меняются только имя, изображение и порт. Затем необходимо подтвердить поды:

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

У всех восьми должно отображаться 1/1 Running.

Развертывание реестра

Служба реестра использует ссылки на все остальные службы в системе. Пока она не запущена, у агентов нет информации о том, как с ней подключаться. Позже доступ к ней следует ограничить, чтобы к ней могли обращаться только агенты приложений, что обеспечит их безопасность и создаст четкий механизм взаимодействия между ними.

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 &

Проверьте, может ли он обращаться к остальным восьми агентам по имену DNS. Ответ представляет собой карту всех прикладных сервисов, которая служит реестром с указанием местоположений и описаниями, что в идеале создаёт основу для определения необходимых навыков:

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

Развертывание оркестратора

Учетные данные Azure OpenAI вводятся как секрет и никогда не встраиваются в образ:

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"

Затем соберите и разверните изображение, следуя тому же подходу, что и для всех остальных сервисов:

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 &

Время для теста:

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

В кластере теперь работает функциональная многопроцессная система с оркестратором, что значительно отличается от использования единственной точки входа.

Размещение оркестратора за шлюзом

Зачем вообще размещать оркестратор за шлюзом? Есть пять причин:

  • Один стабильный пункт входа. Настоящий URL, такой как https://your-domain/orchestrator, заменяет туннель, доступный только администратору Kubernetes. Любой реальный клиент — от мобильного бэкенда до другого внутреннего сервиса или интеграции с партнером — может обращаться к нему в обычном режиме.
  • Аутентификация перед выполнением кода. Благодаря механизмам JWT и политике RBAC, реализованным в рамках мер безопасности, непроверенные или неуполномоченные пользователи отклоняются на уровне шлюза и не могут добраться до логики приложения оркестратора.
  • Ограничение скорости запросов. Некорректно работающий клиент или ошибка в каком-либо внешнем компоненте могут заполнить систему оркестратора запросами, причем каждый из них доходит до модели планирования и увеличивает счета в Foundry. Шлюз позволяет ограничить это.
  • Единое место для отслеживания всего трафика. Логирование и возможности мониторинга для каждого запроса поступают от шлюза, а не от встроенных инструментов внутри оркестратора.
  • Стабильная структура при изменениях. При смене версий оркестратора, добавлении реплики или переходе на другую службу пользователи ничего не замечают, поскольку они всегда использовали только путь к шлюзу.
  • Ранние серверы MCP в банковской сфере уже поднимали ту же проблему в отношении инструментов. Аргумент усиливается на еще один уровень, поскольку входная точка всей многоагентной системы заслуживает такого же управления, какое имели отдельные агенты.

    Простой HTTP-бэкенд, такой как оркестратор, без поддержки MCP и настроек поставщика ИИ, не требует использования AgentgatewayBackend. HTTPRoute может напрямую указывать на Kubernetes-сервис. Остается одна проблема: сервис оркестратора находится в пространстве имён banking-agents, в то время как HTTPRoute — в пространстве имён agentgateway-system, что является разными пространствами имён. API-шлюз требует явного ReferenceGrant в целевом пространстве имён перед тем, как разрешить обращение между пространствами имён.

    Сначала необходимо разрешить обращение между пространствами имён:

    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
    

    Во-вторых, направьте запрос непосредственно к сервису:

    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
    

    Затем проверьте через шлюз:

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

    Встроенные механизмы защиты и ограничения скорости

    Встроенные фильтры AgentGateway называются механизмами защиты или фильтрами запросов. Они касаются содержимого трафика LLM, а не обычных данных REST, поэтому применяются к маршруту /llm и подключению Foundry, но не к обычному JSON-формату от оркестратора.

    Очевидным решением для банков является маскировка персональных данных на маршруте LLM. Оркестратор отправляет в модель планирования такие данные клиентов, как имена, даты рождения, номера документов, доход и адреса, в виде текста запроса. Однако в текущей реализации код агентов не использует маршрут /llm для всех запросов к модели. Гейтвей предлагает готовые механизмы защиты для конфиденциальных данных на этом маршруте, но из-за того, что модель вызывается через путь в стиле REST, эти защиты так и не активируются. Правильным решением является отправка запросов к LLM через маршрут /llm, однако это изменение ещё предстоит внести.

    Ограничение скорости — это общая политика трафика, которая применяется непосредственно к маршруту оркестратора:

    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
    

    Параметр burst: 10 является необязательным, но полезным. Он позволяет временно увеличить количество запросов выше постоянного значения в 60 за минуту, что помогает законному клиенту, отправляющему несколько запросов подряд, при этом не меняется постоянный лимит. Проверьте применяемую политику:

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

    Тогда попробуйте это:

    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
    

    Примерно первые 70 запросов, 60 постоянных запросов плюс допускаемое количество всплесковых запросов должны возвращать код 200, а после этого в том же временном окне — код 429. Если все запросы возвращают код 200, либо политика не была правильно применена, в таком случае команда kubectl describe agentgatewaypolicy banking-orchestrator-ratelimit -n agentgateway-system покажет ошибки разрешения targetRef, либо 75 запросов просто не исчерпали допускаемое количество всплесковых запросов достаточно быстро. Увеличение количества запросов до 100 решает эту проблему.

    Этим завершается реализация с использованием агентов. Остаётся обеспечить безопасный доступ и добавить механизмы контроля между самими агентами.