Галоўная / Артыкулы

Шматагентныя банкаўскія працэсы на Kubernetes: LangGraph, рэестр і AgentGateway

Будуем спецыялізаваных агентаў на FastAPI і аркестратар LangGraph з LLM-планавальнікам, разгортваем іх у кластары з рэестрам і дадаём маршрутызацыю і ліміты праз AgentGateway.

Выдавец: REACTAPP.TOP17 хв чытання3826 слоў

Крыніцы: agentgateway in the world of Agentic developement

MCP адаптаваны для сітуацыі, калі агент взаімадзейвае з інструментам: фіксаваная функцыя выкаанае адзін конкрэтны завод і вяртае свой рэзультат, без якога-лібо автонаміі чыраку інструмента. Комунікацыя межа агентамі — гэта ўжо іншая справа. Агент, які выступае на протыяжэнні лініі, можа сам ствараць выводы, возможна, за дапамою сябеўскага LLM, сябеўскіх інструментоў і сябеўскіх рашэнняў, а першы агент дае яму падзаданне і чакае обоснаванага адказу, а не проста рэзультата пошуку.

Працэс адміністрування рахунку паказвае сутнасць гэтай проблемы. Ён включае калькольныя незалежныя аспекты: парабяранне ідэнтычнасці, історыю кліента, пераканальнэ следчасць і перагляд крымінальных дакументаў, фінансавую спроможнасць і дакументы з гасударствавых баз. Кожны з гэтых аспектаў можа належаць да разных каманд, будавацца окрэма і в падсумку рассматрывацца адначасова. Самэ гэтая проблема ў тым, для чаго існуе архітэктура агентаў. Практычны прыклад, паказаны ніжэй, дазваляе перайсці ад аднаго агента да каманды спецялістаў: організатора на базе 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)

Экзэкютар стаў аднам узлом, які пераглядае будзь-які план, створаны LLM, і больш не следуе фіксаванай лямбэце графа. Ёго пераканалі безпасабліва, прычымляючыся да рэальнага вызову LLM, калі тое трэбуе актуальных крантактаў 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 і без налашоўкаў прадаўця AI, не патрэбуе AgentgatewayBackend. HTTPRoute можа быць наставлены безпосерэдна на Service Kubernetes. Жыўе ўсё ж адна проблема: Service оркестрацыянта знаходзіцца ў 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 рашае гэтыя проблемы.

    Гэтам выканана рэалізацыя на аднойчасных агентах. Што застаецца — гэта забезпечыць доступ і дадаць правіла керавання межу самымі агентамі.