Tác nhân ở quy mô lớn: Cấu trúc đa tác nhân có giao thức A2A trên Thời gian chạy tác nhân và tích hợp ADK

1. Giới thiệu

Khi các tác nhân AI đảm nhận nhiều trách nhiệm hơn, việc duy trì, mở rộng và phát triển một tác nhân duy nhất làm mọi việc sẽ trở nên khó khăn. Các chức năng khác nhau thường cần các chiến lược triển khai, chu kỳ cập nhật khác nhau, hoặc thậm chí là các nhóm khác nhau sở hữu chúng.

  • Giao thức A2A (Agent2Agent) giải quyết vấn đề giao tiếp – chuẩn hoá cách các tác nhân khám phá khả năng của nhau và cộng tác trên nhiều khung và tổ chức.
  • Thời gian chạy nền tảng tác nhân Gemini Enterprise giải quyết phía triển khai – một nền tảng không máy chủ, được quản lý hoàn toàn, lưu trữ các tác nhân của bạn với tính năng hỗ trợ A2A tích hợp, khả năng tự động cấp tài nguyên bổ sung, các điểm cuối bảo mật, phiên liên tục và không cần quản lý cơ sở hạ tầng.

Nhờ đó, bạn có thể tạo các tác nhân chuyên biệt, triển khai chúng dưới dạng các dịch vụ A2A có thể phát hiện và kết hợp chúng thành các hệ thống đa tác nhân.

Sản phẩm bạn sẽ tạo ra

Một Tác nhân đặt chỗ quản lý việc đặt bàn tại nhà hàng (tạo, kiểm tra và huỷ) bằng cách sử dụng trạng thái phiên ADK do Gemini Enterprise Agent Platform Sessions quản lý. Bạn triển khai tác nhân này vào Thời gian chạy Nền tảng tác nhân Gemini Enterprise, nơi tác nhân này có thể được phát hiện thông qua thẻ tác nhân của giao thức A2A. Sau đó, bạn nâng cấp tác nhân hỗ trợ nhà hàng Foodie Finds (từ lớp học lập trình tiên quyết, đừng lo lắng nếu bạn chưa truy cập vào lớp học lập trình này – chúng tôi đã chuẩn bị một kho lưu trữ khởi đầu cho bạn) để sử dụng Reservation Agent làm tác nhân phụ A2A từ xa. Kết quả: một hệ thống đa tác nhân, trong đó trình điều phối định tuyến các truy vấn về trình đơn đến MCP Toolbox và các yêu cầu đặt trước đến tác nhân A2A từ xa.

143fadef342e67a6.jpeg

Kiến thức bạn sẽ học được

  • Tạo một tác nhân ADK sử dụng dịch vụ phiên được quản lý để quản lý dữ liệu đặt phòng
  • Hiển thị một tác nhân ADK dưới dạng máy chủ A2A có thẻ và kỹ năng của tác nhân
  • Triển khai tác nhân A2A vào Thời gian chạy tác nhân Gemini Enterprise
  • Sử dụng tác nhân A2A từ xa từ một tác nhân ADK khác bằng cách dùng RemoteA2aAgent và xử lý yêu cầu đã xác thực
  • Kiểm thử hệ thống nhiều tác nhân theo từng bước: A2A cục bộ, A2A đã triển khai, tích hợp một phần, triển khai đầy đủ

Điều kiện tiên quyết

2. Thiết lập môi trường – Tiếp tục từ lớp học lập trình trước

Nội dung mà chúng tôi cung cấp trong lớp học lập trình này thực sự là phần tiếp theo của lớp học lập trình tiên quyết này: Agentic RAG bằng ADK, Bộ công cụ MCP và Cloud SQL . Bạn có thể tiếp tục công việc từ lớp học lập trình trước

Chúng ta có thể bắt đầu tạo trong thư mục làm việc của lớp học lập trình trước ( thư mục làm việc phải là build-agent-adk-toolbox-cloudsql). Để tránh nhầm lẫn, hãy đổi tên thư mục thành tên thư mục mà chúng ta dùng khi bắt đầu từ đầu

mv ~/build-agent-adk-toolbox-cloudsql ~/adk-a2a-agent-runtime-starter
cloudshell workspace ~/adk-a2a-agent-runtime-starter && cd ~/adk-a2a-agent-runtime-starter
source .env

Xác minh rằng các tệp khoá trong lớp học lập trình trước đã được đặt đúng vị trí:

echo "--- Restaurant Agent ---"
cat restaurant_agent/agent.py | head -5
echo ""
echo "--- Toolbox Config ---"
cat tools.yaml | head -5

Bạn sẽ thấy tệp restaurant_agent/agent.py có chế độ nhập LlmAgent và tệp tools.yaml có cấu hình Toolbox.

Tiếp theo, hãy khởi động lại môi trường Python

rm -rf .venv
uv sync

Ngoài ra, hãy xác minh rằng cơ sở dữ liệu đã được gieo hạt và sẵn sàng:

uv run python scripts/verify_seed.py

Nếu làm theo từng chi tiết kiểm thử trong lớp học lập trình trước, bạn có thể thấy kết quả như sau

Menu Items: 16/15
Embeddings: 16/15

✗ Database not ready

Không sao cả! Quy trình kiểm tra cơ sở dữ liệu không tính đến dữ liệu bổ sung mà bạn nhập từ quy trình kiểm tra nhập dữ liệu. Miễn là bạn có từ 15 dữ liệu trở lên, mọi thứ đều ổn!

Kích hoạt API bắt buộc

Tiếp theo, bạn cần đảm bảo rằng bạn đã bật API cần thiết để tương tác với Nền tảng tác nhân Gemini Enterprise

gcloud services enable \
  cloudresourcemanager.googleapis.com

Bạn đã có các tệp và cơ sở hạ tầng cần thiết để tiếp tục chuyển sang phần tiếp theo: A2A Protocol and Gemini Enterprise Agent Runtime!

3. Thiết lập môi trường – Bắt đầu từ đầu với kho lưu trữ khởi động

Bước này chuẩn bị môi trường Cloud Shell, định cấu hình dự án trên đám mây Google Cloud và sao chép kho lưu trữ ban đầu.

Mở Cloud Shell

Mở Cloud Shell trong trình duyệt. Cloud Shell cung cấp một môi trường được định cấu hình sẵn với tất cả các công cụ bạn cần cho lớp học lập trình này. Nhấp vào Uỷ quyền khi được nhắc

Sau đó, nhấp vào "View" (Xem) -> "Terminal" (Thiết bị đầu cuối) để mở thiết bị đầu cuối.Giao diện của bạn sẽ trông tương tự như thế này

86307fac5da2f077.png

Đây sẽ là giao diện chính của chúng ta, IDE ở trên cùng, thiết bị đầu cuối ở dưới cùng

Thiết lập thư mục làm việc

Sao chép kho lưu trữ khởi đầu. Tất cả mã bạn viết trong lớp học lập trình này sẽ nằm ở đây:

rm -rf ~/adk-a2a-agent-runtime-starter
git clone https://github.com/alphinside/adk-a2a-agent-runtime-starter.git
cloudshell workspace ~/adk-a2a-agent-runtime-starter && cd ~/adk-a2a-agent-runtime-starter

Tạo tệp .env từ mẫu được cung cấp:

cp .env.example .env

Để đơn giản hoá việc thiết lập dự án trong thiết bị đầu cuối, hãy tải tập lệnh thiết lập dự án này xuống thư mục làm việc của bạn:

curl -sL https://raw.githubusercontent.com/alphinside/cloud-trial-project-setup/main/setup_verify_trial_project.sh -o setup_verify_trial_project.sh

Chạy tập lệnh. Lệnh này xác minh tài khoản thanh toán dùng thử của bạn, tạo một dự án mới (hoặc xác thực một dự án hiện có), lưu mã dự án vào một tệp .env trong thư mục hiện tại và đặt dự án đang hoạt động trong gcloud.

bash setup_verify_trial_project.sh && source .env

Tập lệnh sẽ:

  1. Xác minh rằng bạn có một tài khoản thanh toán dùng thử đang hoạt động
  2. Kiểm tra xem có dự án nào trong .env hay không (nếu có)
  3. Tạo dự án mới hoặc sử dụng lại dự án hiện có
  4. Liên kết tài khoản thanh toán dùng thử với dự án của bạn
  5. Lưu mã dự án vào .env
  6. Đặt dự án làm dự án gcloud đang hoạt động

Xác minh rằng dự án được thiết lập đúng cách bằng cách kiểm tra văn bản màu vàng bên cạnh thư mục đang hoạt động trong dấu nhắc của thiết bị đầu cuối Cloud Shell. Mã này sẽ hiển thị mã dự án của bạn.

5c515e235ee1179f.png

Kích hoạt API bắt buộc

Tiếp theo, bạn cần đảm bảo rằng bạn đã bật API cần thiết để tương tác với Nền tảng tác nhân Gemini Enterprise

gcloud services enable \
  aiplatform.googleapis.com \
  cloudresourcemanager.googleapis.com

Thiết lập cơ sở hạ tầng cơ bản

Trước tiên, chúng ta cần cài đặt các phần phụ thuộc Python bằng uv. Đây là một trình quản lý dự án và gói Python nhanh được viết bằng Rust ( tài liệu về uv). Lớp học lập trình này sử dụng uv để duy trì dự án Python một cách nhanh chóng và đơn giản

uv sync

Sau đó, hãy chạy tập lệnh thiết lập đầy đủ để tạo phiên bản Cloud SQL, gieo dữ liệu và triển khai dịch vụ Hộp công cụ. Dịch vụ này sẽ đóng vai trò là trạng thái ban đầu của tác nhân nhà hàng

bash scripts/full_setup.sh > logs/full_setup.log 2>&1 &

4. Khái niệm: Giao thức Agent2Agent (A2A) và thời gian chạy tác nhân Gemini Enterprise

Trước khi bắt đầu xây dựng, hãy dành một chút thời gian để tìm hiểu 2 công nghệ chính được trình bày trong lớp học lập trình này nhằm mở rộng quy mô ứng dụng dựa trên tác nhân của chúng ta.

Giao thức Agent2Agent (A2A)

Giao thức Agent2Agent (A2A) là một tiêu chuẩn mở được thiết kế để cho phép giao tiếp và cộng tác liền mạch giữa các tác nhân AI. Trong đó, MCP (Giao thức bối cảnh mô hình) kết nối các tác nhân với các công cụ và dữ liệu, A2A kết nối các tác nhân với các tác nhân khác – cho phép các tác nhân khám phá khả năng của nhau, uỷ quyền nhiệm vụ và cộng tác trên nhiều khung và tổ chức.

5586b67d0437d79f.png

Điểm khác biệt chính giữa việc bao bọc một tác nhân dưới dạng một công cụ (thông qua MCP) so với việc hiển thị tác nhân đó thông qua A2A: các công cụ không có trạng thái và thực hiện các chức năng đơn lẻ, trong khi các tác nhân A2A có thể suy luận, duy trì trạng thái và xử lý các lượt tương tác nhiều lượt như thương lượng hoặc làm rõ. Một tác nhân được hiển thị thông qua A2A vẫn giữ nguyên toàn bộ khả năng của mình thay vì bị giảm xuống thành một lệnh gọi hàm.

A2A xác định 3 khái niệm cốt lõi:

  1. Thẻ tác nhân – một tài liệu JSON mô tả những việc mà tác nhân thực hiện, các kỹ năng và điểm cuối của tác nhân. Các tác nhân khác tìm nạp thẻ này để khám phá các chức năng.
  2. Tin nhắn – yêu cầu của người dùng hoặc tác nhân phần mềm được gửi đến một điểm cuối A2A, kích hoạt một tác vụ.
  3. Tác vụ – một đơn vị công việc có vòng đời (đã gửi → đang hoạt động → đã hoàn thành/thất bại) và cấu phần phần mềm chứa kết quả.

e7e3224d05b725f0.jpeg

Để tìm hiểu kỹ hơn, hãy xem bài viết A2A là gì?

Thời gian chạy Nền tảng tác nhân Gemini Enterprise

Thời gian chạy tác nhân là một dịch vụ được quản lý hoàn toàn trên Google Cloud để triển khai, mở rộng quy mô và quản lý các tác nhân AI trong quá trình sản xuất bằng các tính năng bảo mật cấp doanh nghiệp (ví dụ: VPC Service Controls, CMEK). Dịch vụ này xử lý cơ sở hạ tầng để bạn có thể tập trung vào logic của tác nhân.

8ecbfbce8f0b9557.png

Agent Runtime cung cấp:

  • Triển khai được quản lý – triển khai các tác nhân được tạo bằng ADK, LangGraph hoặc bất kỳ khung Python nào bằng một lệnh gọi SDK duy nhất
  • Lưu trữ A2A – triển khai các tác nhân dưới dạng điểm cuối tuân thủ A2A với tính năng phân phát thẻ tác nhân tự động và quyền truy cập đã xác thực
  • Phiên liên tụcVertexAiSessionService lưu trữ nhật ký cuộc trò chuyện và trạng thái trên các yêu cầu
  • Tự động cấp tài nguyên bổ sung – cấp tài nguyên bổ sung từ 0 để xử lý lưu lượng truy cập mà không cần quản lý cơ sở hạ tầng
  • Khả năng ghi nhận – tính năng theo dõi, ghi nhật ký và giám sát tích hợp thông qua ngăn xếp khả năng ghi nhận của Google Cloud
  • và nhiều tính năng khác, hãy xem tài liệu này để biết thông tin chi tiết

Trong lớp học lập trình này, bạn sẽ triển khai tác nhân đặt phòng cho Agent Runtime. Quy trình triển khai sẽ chuyển đổi mã tác nhân của bạn thành dữ liệu nối tiếp (pickle) rồi tải mã đó lên. Agent Runtime cung cấp một điểm cuối không máy chủ để phân phát giao thức A2A – các tác nhân (hoặc ứng dụng) khác tương tác với điểm cuối này thông qua các lệnh gọi HTTP tiêu chuẩn, được xác thực bằng thông tin đăng nhập Google Cloud.

5. Tạo tác nhân đặt phòng

Bước này sẽ tạo một tác nhân ADK mới xử lý việc đặt chỗ tại nhà hàng bằng cách sử dụng trạng thái phiên. Tác nhân này hỗ trợ 3 thao tác (tạo, kiểm tra và huỷ) với số điện thoại là khoá tra cứu. Tất cả dữ liệu đặt phòng đều nằm trong trạng thái phiên của ADK

Tạo cấu trúc cơ bản cho nhân viên hỗ trợ

Sử dụng adk create để tạo cấu trúc thư mục tác nhân với cấu hình dự án và mô hình chính xác:

source .env
uv run adk create reservation_agent \
    --model gemini-2.5-flash \
    --project ${GOOGLE_CLOUD_PROJECT} \
    --region ${GOOGLE_CLOUD_LOCATION}

Thao tác này sẽ tạo một thư mục reservation_agent/__init__.py, agent.py.env được định cấu hình sẵn cho mô hình Gemini trên Nền tảng tác nhân.

adk-a2a-agent-runtime-starter/
├── reservation_agent/
│   ├── __init__.py
│   ├── agent.py
│   └── .env
├── logs
├── scripts
└── ...

Tiếp theo, hãy cập nhật mã tác nhân

Viết mã cho tác nhân

Mở tệp tác nhân đã tạo:

cloudshell edit reservation_agent/agent.py

Sau đó, hãy thay thế nội dung bằng nội dung sau:

# reservation_agent/agent.py
from google.adk.agents import LlmAgent
from google.adk.tools import ToolContext

# App-scoped state prefix ensures reservations persist across all sessions.
# See https://adk.dev/sessions/state/ for state scope details.
STATE_PREFIX = "app:reservation:"


def create_reservation(
    phone_number: str,
    name: str,
    party_size: int,
    date: str,
    time: str,
    tool_context: ToolContext,
) -> dict:
    """Create a new restaurant reservation.

    Args:
        phone_number: Customer's phone number, used as the reservation ID.
        name: Name for the reservation.
        party_size: Number of guests.
        date: Reservation date (e.g., '2025-07-15' or 'this Friday').
        time: Reservation time (e.g., '7:00 PM').

    Returns:
        Confirmation of the reservation.
    """
    reservation = {
        "name": name,
        "party_size": party_size,
        "date": date,
        "time": time,
        "status": "confirmed",
    }
    tool_context.state[f"{STATE_PREFIX}{phone_number}"] = reservation
    return {
        "status": "confirmed",
        "message": f"Reservation created for {name}, party of {party_size} on {date} at {time}. Phone: {phone_number}.",
    }


def check_reservation(phone_number: str, tool_context: ToolContext) -> dict:
    """Look up an existing reservation by phone number.

    Args:
        phone_number: The phone number used when the reservation was created.
        tool_context: ADK tool context for state access.

    Returns:
        The reservation details, or a message if not found.
    """
    reservation = tool_context.state.get(f"{STATE_PREFIX}{phone_number}")
    if reservation:
        return {"found": True, "reservation": reservation}
    return {"found": False, "message": f"No reservation found for {phone_number}."}


def cancel_reservation(phone_number: str, tool_context: ToolContext) -> dict:
    """Cancel an existing reservation by phone number.

    Args:
        phone_number: The phone number used when the reservation was created.
        tool_context: ADK tool context for state access.

    Returns:
        Confirmation of cancellation, or a message if not found.
    """
    key = f"{STATE_PREFIX}{phone_number}"
    reservation = tool_context.state.get(key)
    if not reservation:
        return {"success": False, "message": f"No reservation found for {phone_number}."}
    if reservation.get("status") == "cancelled":
        return {"success": False, "message": f"Reservation for {phone_number} is already cancelled."}
    reservation["status"] = "cancelled"
    tool_context.state[key] = reservation
    return {"success": True, "message": f"Reservation for {reservation['name']} ({phone_number}) has been cancelled."}


root_agent = LlmAgent(
    name="reservation_agent",
    model="gemini-2.5-flash",
    instruction="""You are a friendly reservation assistant for "Foodie Finds" restaurant.
You help diners create, check, and cancel table reservations.

When a diner wants to make a reservation, collect these details:
- Name for the reservation
- Phone number (used as the reservation ID)
- Party size (number of guests)
- Date
- Time

Always confirm the details before creating the reservation.
When checking or cancelling, ask for the phone number if not provided.
Be concise and professional.""",
    tools=[create_reservation, check_reservation, cancel_reservation],
)

6. Chuẩn bị cấu hình máy chủ A2A

Xác định thẻ nhân viên hỗ trợ A2A

Thẻ tác nhân là nội dung mô tả có cấu trúc về các chức năng của tác nhân. Các tác nhân và ứng dụng khác sử dụng thẻ này để khám phá những việc mà tác nhân của bạn làm. Tạo cấu hình thẻ:

cloudshell edit reservation_agent/a2a_config.py

Sao chép nội dung sau vào reservation_agent/a2a_config.py:

# reservation_agent/a2a_config.py
from a2a.types import AgentSkill
from vertexai.preview.reasoning_engines.templates.a2a import create_agent_card

reservation_skill = AgentSkill(
    id="manage_reservations",
    name="Restaurant Reservations",
    description="Create, check, and cancel table reservations at Foodie Finds restaurant",
    tags=["reservations", "restaurant", "booking"],
    examples=[
        "Book a table for 4 on Friday at 7pm",
        "Check reservation for 555-0101",
        "Cancel my reservation, phone number 555-0101",
    ],
    input_modes=["text/plain"],
    output_modes=["text/plain"],
)

agent_card = create_agent_card(
    agent_name="Reservation Agent",
    description="Handles restaurant table reservations — create, check, and cancel bookings for Foodie Finds restaurant.",
    skills=[reservation_skill],
)

Tạo trình thực thi A2A

Trình thực thi kết nối giao thức A2A và tác nhân ADK. Nó nhận các yêu cầu A2A, chạy các yêu cầu đó thông qua tác nhân ADK và trả về kết quả dưới dạng các tác vụ A2A:

cloudshell edit reservation_agent/executor.py

Sao chép nội dung sau vào reservation_agent/executor.py:

# reservation_agent/executor.py
import os
from typing import NoReturn

import vertexai
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types import TaskState, TextPart, UnsupportedOperationError
from a2a.utils import new_agent_text_message
from a2a.utils.errors import ServerError
from google.adk.artifacts import InMemoryArtifactService
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService, VertexAiSessionService
from google.genai import types

from reservation_agent.agent import root_agent as reservation_agent


class ReservationAgentExecutor(AgentExecutor):
    """Bridge between the A2A protocol and the ADK reservation agent.

    Uses InMemorySessionService for local testing, VertexAiSessionService
    when deployed to Agent Runtime (detected via GOOGLE_CLOUD_AGENT_ENGINE_ID).
    """

    def __init__(self) -> None:
        self.agent = None
        self.runner = None

    def _init_agent(self) -> None:
        if self.agent is not None:
            return

        self.agent = reservation_agent
        engine_id = os.environ.get("GOOGLE_CLOUD_AGENT_ENGINE_ID")

        if engine_id:
            project = os.environ.get("GOOGLE_CLOUD_PROJECT")
            location = os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1")
            vertexai.init(project=project, location=location)
            session_service = VertexAiSessionService(
                project=project, location=location, agent_engine_id=engine_id,
            )
            app_name = engine_id
        else:
            session_service = InMemorySessionService()
            app_name = self.agent.name

        self.runner = Runner(
            app_name=app_name,
            agent=self.agent,
            artifact_service=InMemoryArtifactService(),
            session_service=session_service,
            memory_service=InMemoryMemoryService(),
        )

    async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
        if self.agent is None:
            self._init_agent()

        query = context.get_user_input()
        updater = TaskUpdater(event_queue, context.task_id, context.context_id)
        user_id = context.message.metadata.get("user_id", "a2a-user") if context.message.metadata else "a2a-user"

        if not context.current_task:
            await updater.submit()
        await updater.start_work()

        try:
            session = await self._get_or_create_session(context.context_id, user_id)
            content = types.Content(role="user", parts=[types.Part(text=query)])

            async for event in self.runner.run_async(
                session_id=session.id, user_id=user_id, new_message=content,
            ):
                if event.is_final_response():
                    parts = event.content.parts
                    answer = " ".join(p.text for p in parts if p.text) or "No response."
                    await updater.add_artifact([TextPart(text=answer)], name="answer")
                    await updater.complete()
                    break
        except Exception as e:
            await updater.update_status(
                TaskState.failed, message=new_agent_text_message(f"Error: {e!s}"),
            )
            raise

    async def _get_or_create_session(self, context_id: str, user_id: str):
        app_name = self.runner.app_name
        if context_id:
            session = await self.runner.session_service.get_session(
                app_name=app_name, session_id=context_id, user_id=user_id,
            )
            if session:
                return session
        session = await self.runner.session_service.create_session(
            app_name=app_name, user_id=user_id, session_id=context_id,
        )
        return session

    async def cancel(self, context: RequestContext, event_queue: EventQueue) -> NoReturn:
        raise ServerError(error=UnsupportedOperationError())

Trình thực thi tự động phát hiện môi trường của nó: khi GOOGLE_CLOUD_AGENT_ENGINE_ID được đặt (Agent Runtime sẽ chèn giá trị này tại thời điểm triển khai), trình thực thi sẽ sử dụng VertexAiSessionService cho các phiên liên tục. Ở cấp cục bộ, giá trị này sẽ quay lại InMemorySessionService.

Thư mục reservation_agent của bạn hiện sẽ chứa:

reservation_agent/
├── __init__.py
├── agent.py
├── a2a_config.py
├── executor.py
└── .env

7. Chuẩn bị A2A Agent bằng Agent Platform SDK và Kiểm thử cục bộ

Bước này bao bọc tác nhân đặt phòng dưới dạng một tác nhân tuân thủ A2A bằng cách sử dụng lớp A2aAgent của Agent Platform SDK ( tên SDK vẫn sử dụng thuật ngữ vertex để đảm bảo khả năng tương thích ngược), sau đó kiểm thử toàn bộ quy trình giao thức A2A cục bộ – truy xuất thẻ tác nhân, gửi thông báo và truy xuất tác vụ. Đây cũng là đối tượng A2aAgent mà bạn triển khai cho Agent Runtime ở bước tiếp theo.

Thêm phần phụ thuộc

Cài đặt Agent Platform SDK có hỗ trợ Agent Runtime và ADK, cùng với A2A SDK:

uv add "google-cloud-aiplatform[agent_engines,adk]==1.149.0" "a2a-sdk==0.3.26"

Tìm hiểu các thành phần A2A

Để bao bọc một tác nhân ADK cho A2A, bạn cần có 3 thành phần:

  1. Thẻ của trợ lý ảo – "danh thiếp" mô tả các chức năng, kỹ năng và URL điểm cuối của trợ lý ảo. Các tác nhân khác sử dụng thông tin này để khám phá những việc mà tác nhân của bạn làm.
  2. Agent Executor – cầu nối giữa giao thức A2A và logic của tác nhân ADK. Nó nhận các yêu cầu A2A, chạy các yêu cầu đó thông qua tác nhân ADK và trả về kết quả dưới dạng các tác vụ A2A.
  3. A2aAgent – lớp Agent Platform SDK kết hợp thẻ và trình thực thi thành một đơn vị có thể triển khai.

Tạo kịch bản kiểm tra

Tạo tập lệnh sau để kiểm thử cục bộ

cloudshell edit scripts/test_a2a_agent_local.py

Sao chép nội dung sau vào scripts/test_a2a_agent_local.py:

# scripts/test_a2a_agent_local.py
import asyncio
import json
import os
from pprint import pprint

from dotenv import load_dotenv
from starlette.requests import Request
from vertexai.preview.reasoning_engines import A2aAgent

from reservation_agent.a2a_config import agent_card
from reservation_agent.executor import ReservationAgentExecutor

load_dotenv()


# --- Helper functions for building mock requests ---

def receive_wrapper(data: dict):
    async def receive():
        byte_data = json.dumps(data).encode("utf-8")
        return {"type": "http.request", "body": byte_data, "more_body": False}
    return receive

def build_post_request(data: dict = None, path_params: dict = None) -> Request:
    scope = {"type": "http", "http_version": "1.1", "headers": [(b"content-type", b"application/json")], "app": None}
    if path_params:
        scope["path_params"] = path_params
    return Request(scope, receive_wrapper(data))

def build_get_request(path_params: dict) -> Request:
    scope = {"type": "http", "http_version": "1.1", "query_string": b"", "app": None}
    if path_params:
        scope["path_params"] = path_params
    async def receive():
        return {"type": "http.disconnect"}
    return Request(scope, receive)


# --- Helper: poll for task completion ---

async def wait_for_task(a2a_agent, task_id, max_retries=30):
    """Poll on_get_task until the task reaches a terminal state."""
    for _ in range(max_retries):
        request = build_get_request({"id": task_id})
        result = await a2a_agent.on_get_task(request=request, context=None)
        state = result.get("status", {}).get("state", "")
        if state in ["completed", "failed"]:
            return result
        await asyncio.sleep(1)
    return result


def print_task_answer(result):
    """Extract and print the answer from task artifacts."""
    print(f"Status: {result.get('status', {}).get('state')}")
    for artifact in result.get("artifacts", []):
        if artifact.get("parts") and "text" in artifact["parts"][0]:
            print(f"Answer: {artifact['parts'][0]['text']}")


# --- Local test ---

async def main():
    # Create and set up the A2A agent locally
    a2a_agent = A2aAgent(agent_card=agent_card, agent_executor_builder=ReservationAgentExecutor)
    a2a_agent.set_up()

    # 1. Get agent card
    print("=" * 50)
    print("1. Retrieving agent card...")
    print("=" * 50)
    request = build_get_request(None)
    card_response = await a2a_agent.handle_authenticated_agent_card(request=request, context=None)
    print(f"Agent: {card_response.get('name')}")
    print(f"Skills: {[s.get('name') for s in card_response.get('skills', [])]}")

    # 2. Create a reservation
    print("\n" + "=" * 50)
    print("2. Creating a reservation...")
    print("=" * 50)
    message_data = {
        "message": {
            "messageId": f"msg-{os.urandom(4).hex()}",
            "content": [{"text": "Book a table for 2 on Saturday at 6pm. Name: Bob, Phone: 555-0202"}],
            "role": "ROLE_USER",
        },
    }
    request = build_post_request(message_data)
    response = await a2a_agent.on_message_send(request=request, context=None)
    task_id = response["task"]["id"]
    context_id = response["task"].get("contextId")
    print(f"Task ID: {task_id}")

    # 3. Wait for result
    print("\n" + "=" * 50)
    print("3. Waiting for task result...")
    print("=" * 50)
    result = await wait_for_task(a2a_agent, task_id)
    print_task_answer(result)

    # 4. Check the reservation (same context for session continuity)
    print("\n" + "=" * 50)
    print("4. Checking the reservation...")
    print("=" * 50)
    check_data = {
        "message": {
            "messageId": f"msg-{os.urandom(4).hex()}",
            "content": [{"text": "Check the reservation for 555-0202"}],
            "role": "ROLE_USER",
            "contextId": context_id,
        },
    }
    request = build_post_request(check_data)
    check_response = await a2a_agent.on_message_send(request=request, context=None)
    check_result = await wait_for_task(a2a_agent, check_response["task"]["id"])
    print_task_answer(check_result)

    # 5. Cancel the reservation
    print("\n" + "=" * 50)
    print("5. Cancelling the reservation...")
    print("=" * 50)
    cancel_data = {
        "message": {
            "messageId": f"msg-{os.urandom(4).hex()}",
            "content": [{"text": "Cancel the reservation for 555-0202"}],
            "role": "ROLE_USER",
            "contextId": context_id,
        },
    }
    request = build_post_request(cancel_data)
    cancel_response = await a2a_agent.on_message_send(request=request, context=None)
    cancel_result = await wait_for_task(a2a_agent, cancel_response["task"]["id"])
    print_task_answer(cancel_result)

    print("\n" + "=" * 50)
    print("All tests passed!")
    print("=" * 50)


if __name__ == "__main__":
    asyncio.run(main())

Kịch bản kiểm tra nhập thẻ tác nhân và trình thực thi mà bạn đã tạo ở bước trước – không có nội dung trùng lặp. Thao tác này sẽ tạo một A2aAgent cục bộ, mô phỏng các lệnh gọi giao thức A2A thông qua các yêu cầu HTTP mô phỏng và xác minh cả 3 thao tác đặt phòng.

Vì không có GOOGLE_CLOUD_AGENT_ENGINE_ID nào được đặt cục bộ, nên trình thực thi sẽ dùng InMemorySessionService. Khi được triển khai cho Agent Runtime, cùng một trình thực thi sẽ tự động chuyển sang VertexAiSessionService cho các phiên liên tục.

Chạy kiểm thử

PYTHONPATH=. uv run python scripts/test_a2a_agent_local.py

Kết quả sẽ trải qua 5 giai đoạn:

  1. Thẻ trợ lý AI – truy xuất các chức năng và kỹ năng của trợ lý AI
  2. Tạo lượt đặt chỗ – đặt bàn và trả về một tác vụ kèm theo thông tin xác nhận
  3. Nhận kết quả của tác vụ – truy xuất tác vụ đã hoàn thành cùng với câu trả lời
  4. Kiểm tra thông tin đặt chỗ – tìm thông tin đặt chỗ theo số điện thoại
  5. Huỷ yêu cầu đặt chỗ – huỷ yêu cầu đặt chỗ và xác nhận

Ví dụ về kết quả đầu ra như minh hoạ dưới đây

==================================================
1. Retrieving agent card...
==================================================
Agent: Reservation Agent
Skills: ['Restaurant Reservations']

==================================================
2. Creating a reservation...
==================================================
Task ID: f7f7004d-cfea-49c2-b57d-5bca9959e193

==================================================
3. Waiting for task result...
==================================================
Status: TASK_STATE_COMPLETED
Answer: Your reservation for Bob, party of 2, on Saturday at 6:00 PM has been confirmed. The phone number associated is 555-0202.

==================================================
4. Checking the reservation...
==================================================
Status: TASK_STATE_COMPLETED
Answer: I found a reservation for Bob, party of 2, on Saturday at 6:00 PM. The reservation status is confirmed.

==================================================
5. Cancelling the reservation...
==================================================
Status: TASK_STATE_COMPLETED
Answer: Your reservation for Bob (555-0202) has been cancelled.

==================================================
All tests passed!
==================================================

Đến đây, bạn đã xác minh: thẻ tác nhân phần mềm A2A mô tả các kỹ năng chính xác, cả 3 thao tác đặt trước đều hoạt động thông qua luồng thông báo/tác vụ của giao thức A2A và trạng thái vẫn duy trì trên các thông báo trong cùng một bối cảnh.

8. Triển khai Reservation Agent vào Agent Runtime

Bước này triển khai tác nhân đặt phòng vào Thời gian chạy Nền tảng tác nhân Gemini Enterprise – một nền tảng không máy chủ, được quản lý toàn diện, lưu trữ tác nhân của bạn và hiển thị tác nhân đó dưới dạng một điểm cuối A2A bảo mật. Sau khi triển khai, mọi ứng dụng khách được uỷ quyền đều có thể khám phá và tương tác với tác nhân thông qua các điểm cuối HTTP A2A tiêu chuẩn.

Tạo vùng lưu trữ tạm thời

Tạo một bộ chứa Cloud Storage để dàn xếp Thời gian chạy của tác nhân. Agent Runtime sử dụng vùng chứa này để tải mã và các phần phụ thuộc của tác nhân lên trong quá trình triển khai:

STAGING_BUCKET="${GOOGLE_CLOUD_PROJECT}-adk-a2a-agent-runtime"
gsutil mb -l $REGION -p $GOOGLE_CLOUD_PROJECT gs://$STAGING_BUCKET 2>/dev/null || echo "Bucket already exists"
echo "STAGING_BUCKET=$STAGING_BUCKET" >> .env
source .env

Tạo tập lệnh triển khai

Tiếp theo, chúng ta sẽ cần chuẩn bị tập lệnh triển khai

cloudshell edit scripts/deploy_a2a_agent_runtime.py

Sao chép nội dung sau vào scripts/deploy_a2a_agent_runtime.py:

# scripts/deploy_a2a_agent_runtime.py
import os
from pathlib import Path

import vertexai
from dotenv import load_dotenv
from google.genai import types
from vertexai.preview.reasoning_engines import A2aAgent

from reservation_agent.a2a_config import agent_card
from reservation_agent.executor import ReservationAgentExecutor

load_dotenv()

PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
REGION = os.environ["REGION"]
STAGING_BUCKET = os.environ.get("STAGING_BUCKET", f"{PROJECT_ID}-adk-a2a-agent-runtime")
BUCKET_URI = f"gs://{STAGING_BUCKET}"

a2a_agent = A2aAgent(
    agent_card=agent_card,
    agent_executor_builder=ReservationAgentExecutor,
)


def main():
    vertexai.init(project=PROJECT_ID, location=REGION, staging_bucket=BUCKET_URI)
    client = vertexai.Client(
        project=PROJECT_ID,
        location=REGION,
        http_options=types.HttpOptions(api_version="v1beta1"),
    )

    print("Deploying Reservation Agent to Agent Runtime...")
    print("This may take 3-5 minutes.")

    remote_agent = client.agent_engines.create(
        agent=a2a_agent,
        config={
            "display_name": agent_card.name,
            "description": agent_card.description,
            "requirements": [
                "google-cloud-aiplatform[agent_engines,adk]==1.149.0",
                "a2a-sdk==0.3.26",
                "google-adk==1.29.0",
                "cloudpickle",
                "pydantic"
            ],
            "extra_packages": [
                "./reservation_agent",
            ],
            "http_options": {
                "api_version": "v1beta1",
            },
            "staging_bucket": BUCKET_URI,
        },
    )

    resource_name = remote_agent.api_resource.name
    print(f"\nDeployment complete!")
    print(f"Resource name: {resource_name}")

    env_path = Path(".env")
    lines = env_path.read_text().splitlines() if env_path.exists() else []
    lines = [l for l in lines if not l.startswith("RESERVATION_AGENT_RESOURCE_NAME=")]
    lines.append(f"RESERVATION_AGENT_RESOURCE_NAME={resource_name}")
    env_path.write_text("\n".join(lines) + "\n")
    print("Written RESERVATION_AGENT_RESOURCE_NAME to .env")


if __name__ == "__main__":
    main()

Tập lệnh triển khai nhập cùng agent_cardReservationAgentExecutor được dùng trong kiểm thử cục bộ – không cần mã trùng lặp. Agent Runtime sẽ chuyển đổi đối tượng A2aAgent cùng với các phần phụ thuộc của đối tượng đó thành chuỗi để triển khai. Ở cuối tập lệnh triển khai, tập lệnh này sẽ ghi giá trị RESERVATION_AGENT_RESOURCE_NAME vào tệp .env

Triển khai cho Thời gian chạy của tác nhân

Chạy tập lệnh triển khai:

PYTHONPATH=. uv run python scripts/deploy_a2a_agent_runtime.py

Quá trình triển khai mất từ 3 đến 5 phút. Tập lệnh này cung cấp một điểm cuối không máy chủ trên Agent Runtime để lưu trữ tác nhân đặt phòng. Sau khi triển khai thành công, bạn sẽ thấy kết quả tương tự như dưới đây

Deploying Reservation Agent to Agent Runtime...
This may take 3-5 minutes.

Deployment complete!
Resource name: projects/your-project-number/locations/us-central1/reasoningEngines/your-agent-deployment-unique-id
Written RESERVATION_AGENT_RESOURCE_NAME to .env

Bạn có thể xem tác nhân đã triển khai trong bảng điều khiển đám mây. Tìm Agent Platform trong thanh tìm kiếm của bảng điều khiển

af3751f461e4708c.png

Sau đó, trên thẻ bên trái, hãy di chuột đến Agents rồi chọn Deployments

8a9c7fd127e60aca.png

Bạn sẽ thấy Reservation Agent xuất hiện trong danh sách triển khai như hình dưới đây

a38b46bcb6c8e4db.png

Kiểm thử nhân viên hỗ trợ đã triển khai

Bây giờ, chúng ta đã sẵn sàng kiểm thử tác nhân đã triển khai, hãy tạo một kịch bản kiểm tra cho tác nhân đã triển khai:

cloudshell edit scripts/test_a2a_agent_runtime.py

Sao chép nội dung sau vào scripts/test_a2a_agent_runtime.py:

# scripts/test_a2a_agent_runtime.py
import asyncio
import os
import time

import vertexai
from a2a.types import TaskState
from dotenv import load_dotenv
from google.genai import types

load_dotenv()

PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
REGION = os.environ["REGION"]
RESOURCE_NAME = os.environ["RESERVATION_AGENT_RESOURCE_NAME"]


async def main():
    vertexai.init(project=PROJECT_ID, location=REGION)
    client = vertexai.Client(
        project=PROJECT_ID, location=REGION,
        http_options=types.HttpOptions(api_version="v1beta1"),
    )

    agent = client.agent_engines.get(name=RESOURCE_NAME)

    # 1. Get agent card
    print("=" * 50)
    print("1. Retrieving agent card...")
    print("=" * 50)
    card = await agent.handle_authenticated_agent_card()
    print(f"Agent: {card.name}")
    print(f"URL: {card.url}")
    print(f"Skills: {[s.name for s in card.skills]}")

    # 2. Send a reservation request
    print("\n" + "=" * 50)
    print("2. Sending reservation request...")
    print("=" * 50)
    message_data = {
        "messageId": "msg-remote-001",
        "role": "user",
        "parts": [{"kind": "text", "text": "Book a table for 3 on Sunday at noon. Name: Carol, Phone: 555-0303"}],
    }
    response = await agent.on_message_send(**message_data)

    task_object = None
    for chunk in response:
        if isinstance(chunk, tuple) and len(chunk) > 0 and hasattr(chunk[0], "id"):
            task_object = chunk[0]
            break

    task_id = task_object.id
    print(f"Task ID: {task_id}")
    print(f"Status: {task_object.status.state}")

    # 3. Poll for result
    print("\n" + "=" * 50)
    print("3. Waiting for result...")
    print("=" * 50)
    result = None
    for _ in range(30):
        try:
            result = await agent.on_get_task(id=task_id)
            if result.status.state in [TaskState.completed, TaskState.failed]:
                break
        except Exception:
            pass
        time.sleep(1)

    print(f"Final status: {result.status.state}")
    if result.artifacts:
        for artifact in result.artifacts:
            if artifact.parts and hasattr(artifact.parts[0], "root") and hasattr(artifact.parts[0].root, "text"):
                print(f"Answer: {artifact.parts[0].root.text}")

    print("\n" + "=" * 50)
    print("Remote agent test passed!")
    print("=" * 50)


if __name__ == "__main__":
    asyncio.run(main())

Sau đó, hãy chạy kiểm thử

source .env
uv run python scripts/test_a2a_agent_runtime.py

Đầu ra cho thấy thẻ tác nhân có kỹ năng "Đặt chỗ nhà hàng", sau đó là tác vụ hoàn tất với thông tin xác nhận đặt chỗ.

==================================================
1. Retrieving agent card...
==================================================
Agent: Reservation Agent
URL: https://us-central1-aiplatform.googleapis.com/v1beta1/projects/your-project-id/locations/us-central1/reasoningEngines/your-agent-unique-id/a2a
Skills: ['Restaurant Reservations']

==================================================
2. Sending reservation request...
==================================================
Task ID: b34585d0-5f03-4cb0-85a3-40710a0d224d
Status: TaskState.completed

==================================================
3. Waiting for result...
==================================================
Final status: TaskState.completed
Answer: Your reservation for Carol, party of 3 on Sunday at noon with phone number 555-0303 is confirmed.

==================================================
Remote agent test passed!
==================================================

Giờ đây, tác nhân đặt phòng đang chạy thành công dưới dạng một điểm cuối A2A được quản lý trên Agent Runtime.

9. Tích hợp tác nhân đặt phòng A2A với tác nhân nhà hàng gốc

Bước này nâng cấp tác nhân nhà hàng để sử dụng tác nhân đặt phòng đã triển khai làm tác nhân phụ A2A từ xa. Trình điều phối chạy cục bộ trong khi tác nhân đặt phòng chạy trên Agent Runtime – một quy trình tích hợp một phần giúp xác thực kết nối A2A trước khi triển khai đầy đủ.

Phân giải URL thẻ nhân viên hỗ trợ A2A

RemoteA2aAgent cần URL thẻ của tác nhân đặt phòng đã triển khai để khám phá các chức năng của tác nhân đó. Tạo một tập lệnh tìm nạp URL này từ Thời gian chạy của tác nhân và ghi URL đó vào .env của tác nhân nhà hàng:

cloudshell edit scripts/resolve_agent_card_url.py

Sao chép nội dung sau vào scripts/resolve_agent_card_url.py:

# scripts/resolve_agent_card_url.py
import asyncio
import os
from pathlib import Path

import vertexai
from dotenv import load_dotenv
from google.genai import types

load_dotenv()

PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
REGION = os.environ["REGION"]
RESOURCE_NAME = os.environ["RESERVATION_AGENT_RESOURCE_NAME"]


async def main():
    vertexai.init(project=PROJECT_ID, location=REGION)
    client = vertexai.Client(
        project=PROJECT_ID, location=REGION,
        http_options=types.HttpOptions(api_version="v1beta1"),
    )

    agent = client.agent_engines.get(name=RESOURCE_NAME)
    card = await agent.handle_authenticated_agent_card()
    card_url = f"{card.url}/v1/card"

    print(f"Agent: {card.name}")
    print(f"Card URL: {card_url}")

    # Write to restaurant_agent/.env
    # Write to both restaurant_agent/.env (for adk web) and root .env (for Cloud Run deploy)
    for env_path in [Path("restaurant_agent/.env"), Path(".env")]:
        lines = env_path.read_text().splitlines() if env_path.exists() else []
        lines = [l for l in lines if not l.startswith("RESERVATION_AGENT_CARD_URL=")]
        lines.append(f"RESERVATION_AGENT_CARD_URL={card_url}")
        env_path.write_text("\n".join(lines) + "\n")
        print(f"Written RESERVATION_AGENT_CARD_URL to {env_path}")


if __name__ == "__main__":
    asyncio.run(main())

Chạy tập lệnh để điền URL thẻ đại lý vào tệp .env

uv run python scripts/resolve_agent_card_url.py
source .env

Cập nhật tác nhân nhà hàng

Mở tệp tác nhân nhà hàng:

cloudshell edit restaurant_agent/agent.py

Sau đó, hãy thay thế nội dung bằng phiên bản mới có bao gồm tác nhân đặt trước từ xa dưới dạng tác nhân phụ:

# restaurant_agent/agent.py
import os

import httpx
from google.adk.agents import LlmAgent
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.auth import default
from google.auth.transport.requests import Request as AuthRequest
from toolbox_adk import ToolboxToolset

TOOLBOX_URL = os.environ.get("TOOLBOX_URL", "http://127.0.0.1:5000")
RESERVATION_AGENT_CARD_URL = os.environ.get("RESERVATION_AGENT_CARD_URL", "")

toolbox = ToolboxToolset(TOOLBOX_URL)


class GoogleCloudAuth(httpx.Auth):
    """Auto-refreshing Google Cloud authentication for httpx.

    Refreshes the access token before each request if expired,
    so long-running agents never hit 401 errors.
    """

    def __init__(self):
        self.credentials, _ = default(
            scopes=["https://www.googleapis.com/auth/cloud-platform"]
        )

    def auth_flow(self, request):
        # Refresh the token if it is expired or missing
        if not self.credentials.valid:
            self.credentials.refresh(AuthRequest())
            
        request.headers["Authorization"] = f"Bearer {self.credentials.token}"
        yield request


reservation_remote_agent = RemoteA2aAgent(
    name="reservation_agent",
    description="Handles restaurant table reservations — create, check, and cancel bookings. Delegate to this agent when the user wants to book a table, check a reservation, or cancel a reservation.",
    agent_card=RESERVATION_AGENT_CARD_URL,
    httpx_client=httpx.AsyncClient(auth=GoogleCloudAuth(), timeout=60),
)

root_agent = LlmAgent(
    name="restaurant_agent",
    model="gemini-2.5-flash",
    instruction="""You are a friendly and knowledgeable concierge at "Foodie Finds," a restaurant. Your job:
- Help diners browse the menu by category or cuisine type.
- Provide full details about specific dishes, including ingredients, price, and dietary information.
- Recommend dishes based on natural language descriptions of what the diner is craving.
- Add new menu items when asked.
- For reservation requests (booking, checking, or cancelling tables), delegate to the reservation_agent.

When a diner asks about a specific dish by name or cuisine, use the get-item-details tool.
When a diner asks for a specific category or cuisine type, use the search-menu tool.
When a diner describes what kind of food they want — by flavor, texture, dietary needs, or cravings — use the search-menu-by-description tool for semantic search.

When in doubt between search-menu and search-menu-by-description, prefer search-menu-by-description — it searches dish descriptions and finds more relevant matches.
If a dish is not available (available is false), let the diner know and suggest similar alternatives from the search results.
Be conversational, knowledgeable, and concise.""",
    tools=[toolbox],
    sub_agents=[reservation_remote_agent],
)

Sau đây là những thay đổi chính so với phiên bản trước:

  • GoogleCloudAuth – một trình xử lý httpx.Auth tuỳ chỉnh giúp làm mới mã truy cập Google Cloud trước mỗi yêu cầu. Agent Runtime yêu cầu các lệnh gọi A2A đã xác thực và mã thông báo sẽ hết hạn sau một khoảng thời gian.
  • RemoteA2aAgent đọc RESERVATION_AGENT_CARD_URL từ .env (do tập lệnh phân giải ghi) và sử dụng httpx_client đã xác thực
  • Được đăng ký làm tác nhân phụ – Trình điều phối của ADK sẽ tự động uỷ quyền các yêu cầu đặt phòng cho tác nhân phụ này
  • Đã cập nhật hướng dẫn để đề cập đến tính năng uỷ quyền đặt phòng

Kiểm thử tác nhân tích hợp cục bộ

Nhân viên hỗ trợ khởi đầu cần tích hợp với MCP Toolbox. Tệp bắt buộc phải được cung cấp từ lớp học lập trình trước hoặc từ kho lưu trữ khởi đầu. Chúng ta chỉ cần đảm bảo rằng quy trình hộp công cụ chạy đúng cách.

Nếu TOOLBOX_URL trong .env của bạn đã trỏ đến một dịch vụ Cloud Run (từ lớp học lập trình trước hoặc có thể từ full_setup.sh của kho lưu trữ khởi động), thì bạn có thể bỏ qua bước này – tác nhân sẽ kết nối với Toolbox đã triển khai.

Nếu bạn cần một Toolbox cục bộ, hãy kiểm tra xem có Toolbox nào đang chạy hay chưa trước khi bắt đầu một phiên bản mới:

if curl -s http://127.0.0.1:5000/api/toolsets > /dev/null 2>&1; then
  echo "Toolbox already running on port 5000"
else
  set -a; source .env; set +a
  ./toolbox --config=tools.yaml > logs/toolbox.log 2>&1 &
  echo "Toolbox started"
fi

Sau đó, chúng ta có thể thử tương tác với tác nhân nhà hàng thông qua giao diện người dùng dành cho nhà phát triển web ADK

uv run adk web --allow_origins "regex:https://.*\.cloudshell\.dev" --port 8080

Mở giao diện người dùng web ADK bằng Cloud Shell Web Preview (nhấp vào nút Web Preview, thay đổi cổng thành 8080), sau đó chọn restaurant_agent

65a055b70ab52aa8.png

Thử nghiệm một cuộc trò chuyện kết hợp:

Truy vấn trình đơn

What Italian dishes do you have?

Yêu cầu đặt chỗ

I want to create reservation under name Bob, phone number 123456

Kiểm tra thông tin đặt chỗ

Tạo phiên mới ( bắt đầu cuộc trò chuyện mới):

Check the reservation for 123456

92cef3bc7671129a.png

16bfd60f202dcaa7.png

c5326bbf6fa778e2.png

Dừng quy trình adk web bằng cách nhấn tổ hợp phím Ctrl+C hai lần. Tiếp theo, hãy hoàn tất hệ thống bằng cách triển khai đầy đủ tác nhân

10. Triển khai Restaurant Agent mới cập nhật lên Cloud Run

Bước này triển khai lại tác nhân nhà hàng vào Cloud Run bằng cách tích hợp A2A, hoàn tất hệ thống đa tác nhân được triển khai đầy đủ.

Cấp quyền truy cập vào Thời gian chạy của tác nhân

Tài khoản dịch vụ Cloud Run cần có quyền gọi Thời gian chạy của tác nhân. Cấp vai trò roles/aiplatform.user cho tài khoản dịch vụ Compute Engine mặc định:

PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
  --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
  --role="roles/aiplatform.user"

Triển khai lên Cloud Run

Trong quá trình thiết lập này, chúng tôi giả định rằng dịch vụ tác nhân nhà hàng đã tồn tại từ lớp học lập trình trước hoặc bằng cách chạy scripts/full_setup.sh nếu bạn bắt đầu từ đầu. Thao tác này sẽ triển khai lại bằng mã đã cập nhật (tích hợp RemoteA2aAgent mới) và thêm URL thẻ nhân viên đặt phòng làm một biến môi trường mới – các biến môi trường hiện có (TOOLBOX_URL, GOOGLE_CLOUD_PROJECT, v.v.) sẽ được giữ nguyên:

gcloud run deploy restaurant-agent \
  --source . \
  --region=$REGION \
  --allow-unauthenticated \
  --update-env-vars="RESERVATION_AGENT_CARD_URL=$RESERVATION_AGENT_CARD_URL" \
  --min-instances=0 \
  --max-instances=1 \
  --memory=1Gi \
  --port=8080

Kiểm thử hệ thống đã triển khai đầy đủ

Lấy URL của dịch vụ đã triển khai:

AGENT_URL=$(gcloud run services describe restaurant-agent --region=$REGION --format='value(status.url)')
echo "Agent URL: $AGENT_URL"

Mở URL trong trình duyệt. Giao diện người dùng web ADK sẽ tải – đây là giao diện tương tự mà bạn đã sử dụng cục bộ, hiện đang chạy trên Cloud Run.

Bạn có thể trò chuyện thoải mái với tác nhân

Truy vấn trình đơn

What spicy dishes do you have?

Yêu cầu đặt chỗ

Book a table for 4 on Friday at 7pm. Name: Eve, Phone: 555-0505

Kiểm tra thông tin đặt chỗ

Tạo phiên mới ( bắt đầu cuộc trò chuyện mới):

Check reservation for 555-0505

69ae9a7c35255fc.png

55145841338ec9b3.png

Hệ thống đa tác nhân đã được triển khai đầy đủ. Tác nhân nhà hàng trên Cloud Run điều phối giữa 2 dịch vụ phụ trợ: MCP Toolbox cho các hoạt động liên quan đến thực đơn và tác nhân đặt chỗ A2A trên Agent Runtime.

11. Xin chúc mừng!

Bạn đã xây dựng và triển khai một hệ thống đa tác nhân bằng giao thức A2A trên Google Cloud.

Kiến thức bạn học được

  • Xây dựng một tác nhân ADK sử dụng trạng thái phiên (ToolContext) để quản lý dữ liệu đặt phòng mà không cần cơ sở dữ liệu
  • Triển khai một tác nhân A2A vào Thời gian chạy tác nhân bằng SDK Nền tảng tác nhân
  • Đã sử dụng một tác nhân A2A từ xa từ một tác nhân ADK khác bằng cách dùng RemoteA2aAgent làm tác nhân phụ
  • Kiểm thử hệ thống theo từng bước: A2A cục bộ → A2A đã triển khai → tích hợp một phần → triển khai đầy đủ

Dọn dẹp

Để tránh bị tính phí vào tài khoản Google Cloud của bạn, hãy xoá các tài nguyên đã tạo trong lớp học lập trình này.

gcloud projects delete $GOOGLE_CLOUD_PROJECT

Cách 2: Xoá từng tài nguyên

# Delete the Agent Runtime deployment
uv run python -c "
import vertexai
from google.genai import types
vertexai.init(project='$GOOGLE_CLOUD_PROJECT', location='$REGION')
client = vertexai.Client(
    project='$GOOGLE_CLOUD_PROJECT', location='$REGION',
    http_options=types.HttpOptions(api_version='v1beta1'),
)
agent = client.agent_engines.get(name='$RESERVATION_AGENT_RESOURCE_NAME')
agent.delete(force=True)
print('Agent Runtime deployment deleted.')
"

# Delete Cloud Run services
gcloud run services delete restaurant-agent --region=$REGION --quiet
gcloud run services delete toolbox-service --region=$REGION --quiet

# Delete Cloud SQL instance
gcloud sql instances delete $DB_INSTANCE --quiet

# Delete GCS staging bucket
gsutil rm -r gs://$STAGING_BUCKET