首页 / 文章

Kubernetes上的多智能体银行流程:注册中心与AgentGateway——LangGraph

构建 FastAPI 专用代理以及带有 LLM 规划器的 LangGraph 编排器,将其通过注册表部署到集群中,再通过 AgentGateway 添加路由和速率限制功能。

发布者:REACTAPP.TOP阅读约 17 分钟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规划器(通过Azure OpenAI提供的GPT模型),它接收用普通英语表述的请求以及各服务节点所能提供的功能列表,进而选择相应的服务节点及其执行顺序。这是推理过程,而非分支逻辑。

在原有的服务节点基础上又增加了三个针对特定产品的节点,从而形成了三种流程:开立银行账户、绑定借记卡以及购买礼品卡。

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

集群中现已运行着一个带有协调器的功能完备的多智能体系统,这已远远超越了单一端点的范畴。

将协调器置于网关之后

为何要特意在协调器前添加网关?原因有五点:

  • 统一的稳定入口。使用如 https://your-domain/orchestrator 这样的真实URL,取代了只有Kubernetes管理员才能使用的隧道。无论是移动后端、其他内部服务还是合作伙伴的集成,任何真实客户端都能正常访问它。
  • 先进行身份验证再处理代码。借助安全措施中实现的JWT和RBAC策略,未认证或未经授权的请求者会在网关处被拒绝,从而无法接触到协调器的应用逻辑。
  • 流量控制。行为异常的客户端或上游调用方中的漏洞可能会向协调器发送大量请求,这些请求都会到达规划模型并增加Foundry的账单费用。网关可以对此进行限制。
  • 统一查看所有流量。每次调用的日志记录与监控功能都来自网关,而非协调器内部的监控机制。
  • 环境变更时依然稳定。即使更换协调器版本、添加副本或迁移到其他服务,调用方也不会察觉任何变化,因为他们始终只知道网关的路径。
  • 早期的银行MCP服务器也为类似工具提出了相同观点。这一论点更上一层楼,因为整个多智能体系统的入口同样需要具备各个智能体所拥有的管理机制。

    像调度器这样的普通 HTTP 后端,既没有 MCP 转换功能,也没有 AI 提供商配置,因此不需要 AgentgatewayBackend。HTTPRoute 可以直接指向 Kubernetes 的 Service。不过仍存在一个问题:调度器的 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 的内置过滤器被称为防护机制或提示词保护装置。它们针对的是大语言模型请求的内容,而非通用的 REST 请求载荷,因此仅适用于 /llm 路由以及 Foundry 连接,而不适用于协调器发送的普通 JSON 数据。

    对于银行而言,最显而易见的防护措施是在 /llm 路由上对个人信息进行掩码处理。协调器会将客户的姓名、出生日期、文件编号、收入状况和地址等信息嵌入提示词中,发送给其中的规划模型。但目前,代理代码并非通过 /llm 路由来访问所有模型。虽然网关为该路由提供了针对敏感数据的内置防护机制,但由于当前实现是通过 REST 风格的路径来调用模型,这些防护措施始终无法生效。正确的解决方案是通过 /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即可解决该问题。

    至此,代理实现已完成。接下来需要做的是保障访问安全,并在各个代理之间设置防护机制。