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.
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_capabilityicredit_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.
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
- Pamięć agentów semantycznych w LangGraph z Qdrant i przywoływanie danych między wątkami — Nadaj agentowi LangGraph pamięć długoterminową za pomocą Qdrant: umieść tam fakty użytkowników, przywołuj je przy różnych identyfikatorach wątków oraz dodaj filtry ładunku danych dla izolacji wielu użytkowników.
- RAG agentowy w LangGraph: oceniające elementy, warunkowe kierowanie i fallback do wyszukiwania w sieci — Przenieś liniową architekturę RAG z LangChain do StateGraph w LangGraph, wykorzystując oceniające elementy LLM, warunkowe połączenia i krawędzie, testy oraz endpoint FastAPI, który informuje o pochodzeniu danych.