Mọi thứ bạn cần để tạo các tác nhân chạy trong thời gian dài trên Google Cloud

1. Trước khi bắt đầu

Hầu hết các trang web bán vé đều đưa người mua vào hàng đợi để ngăn chặn sự cố máy chủ và quản lý nhu cầu cao. Bạn bắt đầu mua hàng và thấy mình đang đứng sau 14.000 người. Hàng đợi sẽ di chuyển trong 40 phút. Khi đến lượt, bạn sẽ có một khoảng thời gian ngắn để chọn ghế và thanh toán. Nếu bạn bỏ lỡ khoảng thời gian đó, vị trí của bạn trong hàng đợi sẽ bị huỷ.

Nhiều tác vụ trong thế giới thực tuân theo mẫu này: một tác vụ mất nhiều thời gian hơn một cuộc trò chuyện duy nhất, chạy trên một hệ thống bên ngoài mà bạn không kiểm soát. Ví dụ: chờ nhà cung cấp bổ sung hàng, chờ phê duyệt yêu cầu bồi thường hoặc chờ trong hàng đợi.

Các tác nhân AI tiêu chuẩn hoạt động trong một lượt trò chuyện duy nhất, kéo dài vài giây và hoàn toàn phản ứng theo yêu cầu. Họ không thể chờ đợi 40 phút hoặc hành động một cách độc lập. Để giải quyết vấn đề này, bạn cần có một tác nhân chạy trong thời gian dài: một hệ thống ở trạng thái không hoạt động trong thời gian chờ dài, duy trì bối cảnh trong quá trình khởi động lại và tự động tiếp tục khi được kích hoạt bởi một yếu tố bên ngoài.

Hội thảo này hướng dẫn bạn cách xây dựng một tác nhân hàng đợi vé buổi hoà nhạc, bao gồm các mẫu kiến trúc cốt lõi cần thiết cho các tác nhân chạy trong thời gian dài trên Google Cloud.

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

Sản phẩm bạn đang tạo

Trong hội thảo này, bạn sẽ tạo một tác nhân đặt vé hoạt động trong thời gian dài trong khi tìm hiểu các mẫu phần mềm quan trọng của tác nhân:

  • Tạm ngưng và tiếp tục xử lý: Tạm dừng lệnh gọi tác nhân trong thời gian chờ đợi lâu và tiếp tục thực thi thông qua các trình kích hoạt bên ngoài.
  • Bộ nhớ trạng thái bền vững: Duy trì trạng thái và cấu phần phần mềm của cuộc trò chuyện trong quá trình khởi động lại bằng SQLite/Cloud SQL và Cloud Storage.
  • Cơ chế bảo vệ dữ liệu cũ và tính chất bất biến: Xác minh dữ liệu động (chẳng hạn như tình trạng còn ghế) trước khi hành động và sử dụng khoá bất biến để ngăn chặn các giao dịch trùng lặp.
  • Biểu đồ quy trình làm việc của ADK: Cấu trúc quá trình thực thi bằng cách sử dụng các nút mã xác định cho các quy tắc theo quy trình và các nút tác nhân để đưa ra quyết định theo ngữ cảnh.
  • Phê duyệt có sự tham gia của con người: Tạm dừng thực thi để thu thập và duy trì hạn mức chi tiêu rõ ràng của người dùng.
  • Triển khai đám mây: Tạo vùng chứa và triển khai tác nhân vào Cloud Run bằng các trình kích hoạt Cloud Scheduler và Pub/Sub.

Bắt đầu thiết lập

Trước tiên, hãy chuẩn bị môi trường và đảm bảo bạn có quyền truy cập vào Google Cloud. Bạn nên chạy lệnh này trong Cloud Shell vì Cloud Shell đã có mọi công cụ bạn cần. Bạn chỉ cần kiểm tra để đảm bảo có đủ dung lượng và tài khoản của bạn có thể tạo dự án.

👉💻 Sao chép kho lưu trữ và chạy chế độ thiết lập:

cd ~
git clone https://github.com/gca-americas/monstertix
cd ~/monstertix
./setup.sh

Hệ thống sẽ hỏi bạn một lần:

  Which Google Cloud project should this workshop use?
  Find yours at https://console.cloud.google.com  (top-left picker)

  Project id [YOUR_PROJECT_ID]:

Sau đó, nó sẽ thực hiện các thao tác sau theo thứ tự:

virtualenv

tạo .venv và cài đặt ADK 2 vào đó

đăng nhập

gcloud auth application-default login nếu bạn chưa có thông tin đăng nhập

project

trỏ gcloud đến dự án của bạn và đặt dự án đó làm dự án hạn mức

API

cho phép 10 người tham gia hội thảo. aiplatform, run, cloudbuild, artifactregistry, storage, pubsub, cloudscheduler, cloudtrace, logging và sqladmin.

.env

ghi mã dự án, khu vực và mô hình của bạn, vì vậy mọi tập lệnh và adk web đều chọn chúng

kiểm tra mô hình

thực hiện một lệnh gọi thực đến Gemini, vì vậy, một dự án hoặc mã mô hình không hợp lệ sẽ không thành công tại đây

seed

tạo một phiên đã được 2 ngày, bước 3 sẽ mở phiên này

Cloud SQL

khởi động một phiên bản Postgres trong nền

venue

triển khai người bán vé giả của riêng bạn lên Cloud Run

👉💻 Kích hoạt môi trường:

cd ~/monstertix
source .venv/bin/activate

Câu lệnh của bạn hiện bắt đầu bằng (.venv).

Tổng quan về tình huống

Trong tình huống của buổi hội thảo này, trợ lý của bạn sẽ đặt 2 vé cho một buổi hoà nhạc ("The Midnight Signal" ở Amsterdam). Các lựa chọn ưu tiên của người dùng được lưu trữ trong bộ nhớ trước đây bao gồm:

  • Phải là chương trình cuối tuần (Sam không thể tham dự vào các buổi tối trong tuần).
  • Tránh ngồi ở hàng ghế trên cùng tại Ziggo Dome (tầm nhìn kém).
  • Giá mục tiêu là khoảng 200 đô la mỗi vé, với giới hạn tối đa là 250 đô la.

The Midnight Signal, on tour

Xác minh quá trình cài đặt

👉💻 Một lệnh, tám dấu kiểm:

./verify.sh
  Checking the rig

  ✓ adk installed                      adk, version 2.6.2
  ✓ adk 2 apis                         App + ResumabilityConfig
  ✓ workflow graphs                    Workflow + node
  ✓ async sqlite drivers               sqlalchemy + aiosqlite + greenlet
  ✓ project                            your-project-id
  ✓ vertex ai                          no API keys anywhere
  ✓ seeded session                     13 events, two days old
  ✓ venue                              https://venue-you-xxxx.us-central1.run.app

  Ready. Cloud SQL is still building in the background and nothing
  before step 10 needs it.

👉 Mở thẻ trình duyệt thứ hai:

Google AI Ultra for Business

Định nghĩa

$VENUE_URL/panel

Bảng điều khiển, các nút. setup.sh đã in URL này và đặt vào .env

2. Bạn có thể tự chủ nhờ câu lệnh?

Câu lệnh không thể tự bắt đầu

Câu lệnh chỉ là văn bản được truyền đến một mô hình AI khi có cuộc gọi. Thiết bị này không thể tự chạy, tự đánh thức hoặc bắt đầu cuộc gọi mới.

Nếu bạn yêu cầu một đặc vụ "kiểm tra lại sau 10 phút" trong câu lệnh, thì thực tế là không có gì xảy ra sau khi lượt hiện tại kết thúc. Mô hình này không có đồng hồ nội bộ hoặc vòng lặp. Để làm cho một tác nhân hoạt động độc lập, một thứ gì đó bên ngoài tác nhân phải đánh thức và gọi tác nhân đó.

Vị trí của bước này

Bắt đầu giàn khoan

Chúng ta sẽ bắt đầu bằng cách kiểm thử công cụ dành cho nhà phát triển. adk web khởi động giao diện người dùng web ADK, giao diện phát triển đi kèm với ADK. Công cụ này cho phép bạn trò chuyện với tác nhân mà bạn đang tạo mà không cần viết giao diện người dùng trước, đồng thời cho bạn biết những gì mà cửa sổ trò chuyện không thể cho biết, chẳng hạn như mọi lệnh gọi công cụ và đối số của lệnh gọi đó, trạng thái phiên khi thay đổi, các cấu phần phần mềm mà tác nhân đã lưu và các sự kiện thô bên dưới. Bạn sẽ không để lộ khoá này trong quá trình phát hành công khai. Đây là cửa sổ tốt nhất để xem một tác nhân đang chạy trong khi bạn học. Ở bước 3, bạn sẽ tạo giao diện người dùng mà bạn thực sự muốn hiển thị và ở bước cuối cùng, bạn sẽ triển khai giao diện đó.

👉🔴 Trên bảng điều khiển địa điểm, hãy nhấn vào ĐẶT LẠI ĐỊA ĐIỂM. Mỗi bước đều bắt đầu từ cùng một thế giới: 8 chương trình, tất cả ghế đều có sẵn, đồng hồ ở tốc độ 1x.

👉💻 Terminal 1 – tải mã của bước này và khởi động giao diện người dùng web ADK:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 2 --force
adk web agent --port 8000 --allow_origins "*"

use-solution.sh sao chép mã đã hoàn tất của một bước vào agent/, là nơi bạn làm việc. Mọi bước đều bắt đầu bằng bước này và bước này sẽ in những tệp mà bước này đã thay đổi để bạn biết cần đọc những tệp nào.

Những gì bạn bắt đầu:

Tệp

Định nghĩa

agent.py

tác nhân – tên, mô hình, hướng dẫn, danh sách công cụ

tools.py

search_events và get_seatmap

venue.py

một ứng dụng HTTP cho địa điểm. Không có gì thú vị

config.py

mô hình cần sử dụng và kiểm tra xem thông tin đăng nhập của bạn có hoạt động hay không

. ./set_env.sh sẽ kích hoạt virtualenv, tải dự án và mô hình của bạn, đồng thời xuất $WORKSHOP – đường dẫn tuyệt đối đến ~/monstertix. Kích hoạt lệnh này trong mọi cửa sổ dòng lệnh mà bạn mở. Nó in ra những gì đã đặt:

  folder   /home/you/monstertix
  project  your project id
  model    gemini-2.5-flash (We'll avoid using 3.x for now to avoid high usage demands)
  venue    https://venue-yourname-xxxx.run.app

👉 Mở các thẻ trình duyệt đầu tiên:

Google AI Ultra for Business

Định nghĩa

Giao diện người dùng web ADK

localhost:8000 – tác nhân của bạn, cùng với các thẻ Trạng thái / Sự kiện / Cấu phần phần mềm

Nói chuyện với nó

👉✨ Trong giao diện người dùng web ADK, hãy nhập:

I want to see The Midnight Signal. I'm in Amsterdam, going with Sam, budget around $200 each.

Công cụ này tìm kiếm chuyến tham quan, đọc sơ đồ chỗ ngồi và đưa ra lý do về ngân sách cũng như công ty của bạn. Đây là một trợ lý thực sự có năng lực. Tuy nhiên, khi đợt bán trước bắt đầu lúc 10:00, tác nhân sẽ không tự động thực thi vì không có quy trình hoặc trình kích hoạt nền nào đang chạy.

Đọc những gì vừa xảy ra

Phần giữa màn hình không chỉ là nhật ký trò chuyện. Mỗi bước của lượt tương tác đều được đánh số, bao gồm cả lệnh gọi công cụ:

  ▸ I want to see The Midnight Signal. I'm in Amsterdam...   ← user input
  ⚡ search_events({"city": "Amsterdam"})                     ← tool execution
  ✓ search_events                                            ← venue API response
    Two Amsterdam shows: Tuesday 10 Nov and Saturday 14 ...   ← model response

👉 Nhấp vào hàng có biểu tượng ⚡. Bảng điều khiển bên trái cho biết các đối số mà mô hình đã chọn. Nhấp vào ✓ bên dưới để xem JSON đã trả về.

Thông tin về chuyến tham quan không có trong dữ liệu huấn luyện hoặc câu lệnh của mô hình. Nhân viên hỗ trợ đã truy xuất dữ liệu này bằng cách thực thi công cụ search_events, công cụ này đã truy vấn API của địa điểm.

Mở mã bạn vừa chạy

Bạn đã thấy cách hoạt động của nó và thấy dấu vết. Bây giờ, hãy xem xét mã này.

agent/concert/
├── agent.py     the agent — about twenty lines
├── tools.py     what it can do
├── venue.py     an httpx wrapper, nothing interesting
└── config.py    which model, and a credentials check

👉💻 Mở agent/concert/agent.py. Toàn bộ tác nhân là phần cuối của tệp:

INSTRUCTION = """
You help someone plan a trip to see a band on tour.

You can search tour dates and read seat maps. Be concrete: name the show, the
city, the section, and the price...
"""

root_agent = Agent(
    name="concert",
    model=MODEL,
    instruction=INSTRUCTION,
    tools=[search_events, get_seatmap],
)

Tên, mô hình, nội dung mô tả về cách tác nhân nên hoạt động và danh sách các hàm Python. Đó là tất cả những gì Agent có. adk web tìm thấy vì tệp có tên là agent.py và biến có tên là root_agent.

👉💻 Hiện tại agent/concert/tools.py:

def search_events(city: str = "", weekday: str = "") -> dict:
    """List tour dates. Call with no arguments to see the whole tour.

    This venue sells one artist and one tour, so there is nothing to search by
    name — never ask the user which artist they mean.

    Args:
        city: Optional. Filter to one city, e.g. "Amsterdam".
        weekday: Optional. Filter to one weekday, e.g. "Saturday".
    """
    return venue.get("/events", city=city, weekday=weekday)

Một hàm thông thường. Không có trình trang trí, không có đăng ký, không có giản đồ để ghi. ADK đọc chữ ký và chuỗi tài liệu, đồng thời tạo định nghĩa công cụ mà mô hình nhìn thấy. Đó là lý do chuỗi tài liệu đó có tính chất chịu tải và lý do việc xoá một đối số đã làm thay đổi hành vi của mô hình.

Mô hình này không bao giờ chạy mã của bạn. Khi bạn thấy search_events({"city": "Amsterdam"}), mô hình đang yêu cầu cuộc gọi đó. ADK đã chạy hàm này, trả lại kết quả và mô hình đã viết một câu về kết quả đó. Vòng lặp này:

👉 Các biểu tượng ở cạnh ngoài cùng bên trái sẽ chuyển đổi nội dung mà bảng điều khiển hiển thị. Công cụ thứ hai là biểu đồ tác nhân – hãy thử công cụ này và bạn sẽ thấy 2 công cụ:

Biểu đồ tác nhân cho thấy các công cụ search_events và get_seatmap

Vì vậy, hãy yêu cầu nó không ngủ

Bạn sẽ làm gì để khắc phục vấn đề này? Hãy thử những thứ mà hầu hết mọi người đều tìm đến đầu tiên. Nhớ lại INSTRUCTION ở bước 1, văn bản được truyền cho mô hình ở mỗi lượt và viết lại văn bản đó.

👉💻 Mở agent/concert/agent.py. Bên dưới INSTRUCTION là một đoạn mã thứ hai đã được viết sẵn cho bạn. Bạn có thể tự thay đổi nếu muốn:

PROACTIVE_INSTRUCTION = """
You are a proactive ticket-buying assistant.

Monitor the presale for The Midnight Signal. The moment it opens at 10:00 on
Tuesday, buy two tickets for the Amsterdam Saturday show — do not wait to be
asked, and do not wait for me to say anything. Act on your own.

Keep checking until the tickets are bought.
"""
# agent/concert/agent.py
root_agent = Agent(
    name="concert",
    model=MODEL,
    instruction=PROACTIVE_INSTRUCTION,     # ← was INSTRUCTION
    tools=[search_events, get_seatmap],
)

👉💻 Trong thiết bị đầu cuối 1, hãy khởi động lại:

cd ~/monstertix
adk web agent --port 8000 --allow_origins "*"

👉✨ Cho biết kế hoạch theo đúng cách bạn nói với một người:

I want to see The Midnight Signal in Amsterdam with Sam. The presale opens at 10:00 on Tuesday. Grab us two tickets the moment it opens.

Nó sẽ đồng ý và nghe như thể nó thực sự muốn vậy.

👉 Bây giờ hãy chờ. Không nhập bất kỳ nội dung nào khác. Xem buổi thảo luận.

Chờ một lát. KHÔNG CÓ GÌ XẢY RA!

Lý do khiến câu lệnh đó không bao giờ hiệu quả

Đọc lại với một tâm thế mới. Giám sát. Ngay khi mở. Hãy tiếp tục kiểm tra. Mỗi chỉ số đó mô tả một điều gì đó xảy ra theo thời gian.

Bây giờ, hãy nhớ lại instruction= thực sự là gì. Đó là một chuỗi được truyền đến mô hình trong yêu cầu, tại thời điểm có người gửi tin nhắn. Không có gì đọc được dữ liệu này giữa các lượt.

# agent/concert/agent.py
root_agent = Agent(
    instruction=PROACTIVE_INSTRUCTION,   # read on invocation
    ...                                  # ...and there was no invocation
)

Một chỉ thị không thể gọi hàm mà chỉ thị đó được viết bên trong.

Các biến thể câu lệnh không thành công

Việc cố gắng thực thi tự động thông qua hướng dẫn bằng câu lệnh sẽ không thành công bất kể cách diễn đạt. Các câu lệnh thường dùng, chẳng hạn như:

  • "Hẹn giờ 10:00 thứ Ba và mua vé vào lúc đó."
  • "Kiểm tra sau mỗi 5 phút để xem đợt bán trước đã bắt đầu hay chưa."
  • "Tự hành động mà không cần chờ hoạt động đầu vào của người dùng."

Tất cả đều không thành công vì cùng một lý do: lời nhắc chỉ được đọc khi có lệnh gọi. Nếu không có phương thức gọi bên ngoài, không có mã nào được thực thi giữa các lượt.

3. Không có hoạt động dựa trên sự kiện

Các tác nhân không thể hoạt động mãi mãi

Việc để một quy trình chạy liên tục sẽ lãng phí bộ nhớ, tốn tiền và gặp sự cố nếu máy tính xách tay của bạn chuyển sang chế độ ngủ hoặc khởi động lại.

Thay vì liên tục chạy trong khi chờ đợi, một tác nhân chạy trong thời gian dài sẽ hoàn toàn ở trạng thái không hoạt động. Một sự kiện bên ngoài (chẳng hạn như bộ hẹn giờ, webhook hoặc thông báo) chỉ đánh thức tác nhân khi có công việc thực tế cần thực hiện.

👉🔴 Trên bảng điều khiển địa điểm, hãy nhấn vào ĐẶT LẠI ĐỊA ĐIỂM. Mỗi bước đều bắt đầu từ cùng một thế giới: 8 chương trình, tất cả ghế đều có sẵn, đồng hồ ở tốc độ 1x.

👉💻 Tải mã của bước này:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 3 --force

Hãy chú ý đến những gì đã được cài đặt: monstertix/, giữ máy chủ, đồng hồ và một thứ nữa mà bạn chưa thấy. Kiểm tra để đảm bảo rằng agent/ không thay đổi, vì không có gì về nhân viên hỗ trợ thay đổi trong bước này. Hãy nhớ điều đó, đó là điểm mấu chốt.

Tệp

Định nghĩa

monstertix/server.py

mới. Một máy chủ web có Runner phía sau

monstertix/clock.py

mới. Một thứ biết thời gian và gọi máy chủ

Khoá này có hai phần và chúng nằm trong hai tệp cho đến khi được tải lên đám mây: Nguyên nhân và người gây ra được tách biệt

trở thành, ở bước 10

điều kiện kích hoạt

một điểm cuối có thể chạy tác nhân

Cloud Run

trình kích hoạt

một thứ có đồng hồ, thứ gọi nó

Cloud Scheduler

Điều kiện kích hoạt – máy chủ web của riêng bạn

Có, một máy chủ web. Phải có một thứ đang nghe thì mới có thể gọi đến. Giao diện người dùng web ADK trong các bước trước là một giao diện phát triển có thể chạy một tác nhân.

👉💻 Mở monstertix/server.py:

from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from concert.agent import root_agent

session_service = InMemorySessionService()
runner = Runner(
    agent=root_agent,
    app_name="concert",
    session_service=session_service,
)

@app.post("/wake")
async def wake(payload: dict = Body(default={})):
    session_id = f"wake-{uuid.uuid4().hex[:8]}"          # brand new, every time
    await session_service.create_session(...)

    async for event in runner.run_async(
        user_id=USER_ID, session_id=session_id,
        new_message=types.Content(role="user",
                                  parts=[types.Part(text=payload["message"])]),
    ):
        ...

Agent không tự chạy. Đó là một nội dung mô tả: tên, mô hình, hướng dẫn, danh sách công cụ. Một thành phần nào đó phải nhận được tin nhắn đến, tìm đúng phiên, chuyển cuộc trò chuyện cho đến thời điểm đó cho mô hình, thực thi bất kỳ công cụ nào mà mô hình yêu cầu, cung cấp kết quả trở lại, lặp lại cho đến khi mô hình ngừng gọi công cụ và ghi mọi bước của quá trình đó vào bộ nhớ. Đó chính là Runner.

Những việc mà Runner thực hiện xung quanh tác nhân của bạn

Giao diện người dùng web ADK có một giao diện mặc định cho bạn. Tại đây, bạn tự xây dựng và đó là toàn bộ sự khác biệt giữa giao diện người dùng web ADK và thứ mà bạn có thể triển khai.

👉💻 Terminal 2 (Thiết bị đầu cuối 2) – khởi động:

Khởi động Runner,

cd ~/monstertix
. ./set_env.sh
python -m monstertix.server
[server] agent concert · gemini-x-flash
[server] listening on http://xxxx_URL/wake
[server] nothing will happen until something calls it. Ctrl-C to stop.

Đọc dòng cuối cùng đó. Trình kích hoạt đang chạy và tác nhân đã được tải, nhưng không có gì xảy ra. Tách biệt hai ý tưởng: có thể gọi và được gọi.

Và cửa trước, trong khi chúng ta ở đây

Mặc dù adk web là một công cụ phát triển tuyệt vời, nhưng bạn không nên phát hành công cụ này. Bạn không nên bật tính năng này khi triển khai cho phiên bản phát hành công khai và bạn sẽ không hướng khách hàng đến tính năng này nếu đã bật. Vì vậy, cùng một máy chủ cũng phân phát một trang.

👉 Mở http://127.0.0.1:8090 trong trình duyệt – cùng một địa chỉ.

Giới thiệu về MonsterTix. Một cửa sổ trò chuyện, một tên ở góc, hai nút. Nó đăng lên /wake và hiển thị những gì nhận được, đồng thời sẽ hoạt động mà không thay đổi đối với mọi điểm cuối trả lời theo cách tương tự.

Trên trang

Nội dung bên dưới

cuộc trò chuyện

POST /wake, mỗi lần một tin nhắn

+ Phiên mới

một mã phiên mới – cuộc trò chuyện mới và sau đó là một ngân sách mới

Đặt lại bộ nhớ

xoá mọi phiên và tệp bộ nhớ. Ứng dụng sẽ hỏi trước và cho bạn biết chính xác vị trí của tiểu bang đó

👉✨ Nói điều gì đó với nó – What Amsterdam dates are there? – và xem terminal 2. Cùng Runner, cùng tác nhân, cùng dòng nhật ký. Chỉ người gọi thay đổi.

Trình kích hoạt – một thứ biết thời gian

👉💻 Mở monstertix/clock.py. Xem mã.

time.sleep(args.delay)                                    # wait
while True:
    client.post(f"{TRIGGER_URL}/wake",                    # knock
                json={"message": args.message})
    if not args.every:
        return                                            # once, unless told otherwise
    time.sleep(args.every)                                # or again, forever

Đây là một cơ chế rất đơn giản: ngủ, POST, có thể lặp lại. Mọi trình lập lịch biểu mà bạn đã sử dụng đều làm như vậy, cộng với các lần thử lại, múi giờ và đảm bảo rằng trình lập lịch biểu đó vẫn hoạt động sau khi khởi động lại.

👉💻 Terminal 3 (Thiết bị đầu cuối 3) – kích hoạt một lần, 10 giây nữa:

cd ~/monstertix
. ./set_env.sh
python -m monstertix.clock --in 10
[clock] target   http://127.0.0.1:8090/wake
[clock] first fire in 60s, once only
[clock] go and watch the other terminal — do not touch the keyboard

👉 Bây giờ, hãy dừng nhập. Chắc chắn. Xem nhà ga số 2.

Một phút sau, không có ai ở bàn phím:

[clock]  firing → POST /wake
[server] woken · session wake-23930983
[server]   called search_events
[server]   called purchase
[server]   said   I've purchased 2 tickets for The Midnight Signal in
                  Amsterdam on Saturday, section A. Your total is 420.
[clock]  done. the agent ran and you were not involved.

Đó là câu trả lời cho bước 2. Một thứ gì đó bên ngoài tác nhân đã gọi nó, và thứ đó là một sleep và một yêu cầu HTTP.

👉💻 Ctrl-C khi bạn đã xem video đó vài lần. Hãy dùng --every 30 trước nếu bạn muốn xem lại.

Bây giờ hãy đọc nội dung mà ứng dụng đã nói

"Tôi đã mua 2 vé xem The Midnight Signal ở Amsterdam, khu vực A, giá 4.200.000 VND."

👉🔴 Kiểm tra bảng điều khiển địa điểm. Bạn sẽ thấy vé đã được mua.

Vậy vấn đề vẫn còn là gì

Nó đã mua một thứ gì đó. Xem những gì mà công cụ này phải đoán. Nó chọn Amsterdam và phần A vì đây là những thông tin đầu tiên mà nó tìm thấy, và bạn đã yêu cầu trợ lý mua vé trực tiếp mà không cho trợ lý biết rằng Sam không thể đi vào các buổi tối trong tuần. Một tác nhân hành động dựa trên phỏng đoán lúc 3 giờ sáng còn tệ hơn một tác nhân không làm gì cả.

Thức dậy là phần dễ dàng. Một trợ lý ảo thức dậy mà không có gì – không có nhật ký, không có lựa chọn ưu tiên, không có ai lắng nghe – thì chưa đáng để thức dậy.

4. Quản lý thời gian tồn tại của ngữ cảnh

Thông tin được lưu trữ ở nhiều nơi trong nhiều khoảng thời gian

Một tác nhân lưu trữ thông tin ở nhiều nơi: trong cuộc trò chuyện, trong trạng thái phiên, trong các tệp (hiện vật) hoặc trong bộ nhớ dài hạn của người dùng. Mỗi địa điểm sẽ tồn tại trong một khoảng thời gian khác nhau.

Nếu bạn đặt thông tin ở sai vị trí:

  • Nội dung biến mất quá nhanh: Các chi tiết quan trọng sẽ bị xoá khi cuộc trò chuyện kết thúc.
  • Thông tin lưu lại quá lâu: Thông tin tạm thời làm ảnh hưởng đến bộ nhớ dài hạn và gây nhầm lẫn trong các cuộc trò chuyện sau này.

Bạn cần phải khớp từng thông tin với vị trí lưu trữ và thời gian lưu giữ phù hợp.

Tải mã

👉🔴 Trên bảng điều khiển địa điểm, hãy nhấn vào ĐẶT LẠI ĐỊA ĐIỂM.

👉💻 Trong thiết bị đầu cuối 1, hãy chạy:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 4 --force

Những thay đổi kể từ bước gần đây nhất:

Tệp

Những nội dung thay đổi

tools.py

đã thêm note_companion. get_seatmap hiện lưu sơ đồ chỗ ngồi vào một tệp

memory.py

mới. Cửa hàng lưu trữ kỷ niệm của riêng chúng tôi

agent.py

3 công cụ khác trong danh sách và hướng dẫn yêu cầu nhân viên hỗ trợ sử dụng các công cụ đó

👉💻 Khởi động lại tác nhân trong terminal 1:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

Hai cờ mà bạn chưa từng sử dụng trước đây:

Cờ

Định nghĩa

session_service_uri

ADK ghi mọi thông báo, lệnh gọi công cụ và thay đổi trạng thái vào đó dưới dạng các hàng (cơ sở dữ liệu SQLite).

artifact_service_uri

Các tệp mà tác nhân lưu vào.(Tệp)

Nếu không có các đối tượng này, ADK sẽ lưu giữ mọi thứ trong bộ nhớ và loại bỏ khi bạn dừng quy trình. Hai chế độ cài đặt này giúp duy trì trạng thái.

Cơ sở dữ liệu sessions.db chứa một cuộc trò chuyện trước đây được gieo hạt (two-days-ago) với 13 sự kiện được ghi lại:

you    We're thinking about seeing The Midnight Signal. Me, Sam, and maybe Priya.
you    Sam can't do weeknights though, bailed on every single one.
you    Priya's out, she's away that weekend. Just me and Sam.
agent  Two it is. Still Saturday the 14th, still Amsterdam.
you    You said the upper bowl at Ziggo was bad last time?

Một tệp bộ nhớ từ 3 lượt đặt vé trước đây, memory/userx.md, bạn có thể mở tệp này trong một trình chỉnh sửa:

- Sam bails on weeknights. Every weeknight show we booked, Sam cancelled.
- Hates the upper bowl at Ziggo Dome, "couldn't see a thing" from section B.
- Comfortable spend is around $200 per ticket. $250 is the hard ceiling.
- Always books two seats, always together.

Đó là lý do khiến các hàng ghế màu hổ phách vào buổi tối trong tuần trở nên quan trọng. Một suất chiếu vào tối thứ trong tuần có giá rẻ hơn 10 đô la mỗi ghế, còn nhiều ghế tốt hơn và nằm ở đầu danh sách. Tuy nhiên, đây lại là câu trả lời không phù hợp với người này. Lý do duy nhất khiến tác nhân có thể biết được điều đó là do tác nhân đã ghi một tệp sau lần đặt phòng gần đây nhất. Liệu sự thật đó có tồn tại hay không là bước 4 và 5.

3 công cụ mà chúng tôi đã thêm

Ba công cụ này và nơi mỗi công cụ viết

note_companion ghi lại thông tin về những người đến và những hạn chế đối với họ:

# agent/concert/tools.py
def note_companion(name: str, constraint: str, tool_context: ToolContext) -> dict:
    prefs = dict(tool_context.state.get("user:prefs", {}))
    prefs[name] = constraint
    tool_context.state["user:prefs"] = prefs
    return {"companions": prefs}

Thêm sự kiện đó vì "Sam không thể tham gia vào các buổi tối trong tuần" vẫn phải đúng vào tuần tới. Hãy viết nó vào user:prefs để thực hiện việc đó và xem phần user: của tên thực hiện công việc. Hãy quay lại phần đó ở bên dưới.

recall

và

remember đọc và ghi tệp văn bản, memory/userx.md. Giữ những tư thế đó trong chốc lát.

👉 Bây giờ, hãy xem 2 nơi mà tác nhân lưu trữ ngữ cảnh:

vị trí

Nội dung

Trạng thái

bộ nhớ của phiên này, dưới dạng các cặp khoá/giá trị – bạn sẽ ở đây trong bước 4

Hiện vật

các tệp mà tác nhân đã lưu, không bao giờ xuất hiện trong câu lệnh

Xem những thay đổi trong

get_seatmap

. Trước đây, hàm này trả về toàn bộ sơ đồ chỗ ngồi. Bây giờ, chương trình sẽ lưu sơ đồ chỗ ngồi vào một tệp và trả về một bản tóm tắt ngắn:

# agent/concert/tools.py
async def get_seatmap(event_id: str, tool_context: ToolContext) -> dict:
    seatmap = venue.get(f"/events/{event_id}/seatmap")       # about 6 KB of JSON

    part = types.Part(inline_data=types.Blob(
        mime_type="application/json",
        data=json.dumps(seatmap, indent=2).encode()))
    await tool_context.save_artifact(f"seatmap_{event_id}.json", part)

    summary = {"event_id": ..., "sections": [...], "captured_at": ...}
    tool_context.state["temp:seatmap"] = summary
    return summary

Đây là lý do khiến thay đổi này trở nên quan trọng.

Mọi tải trọng trả về của công cụ đều được thêm vào nhật ký trò chuyện và gửi lại cho mô hình ở tất cả các lượt tiếp theo. Việc trả về các đối tượng JSON lớn (chẳng hạn như sơ đồ chỗ ngồi 6 KB) sẽ làm tăng mức tiêu thụ mã thông báo ở mỗi lượt tiếp theo.

Thay vào đó, hãy lưu vào một tệp và trả một tên tệp. Cho phép tác nhân yêu cầu cung cấp thông tin chi tiết bằng load_artifact nếu cần và dự kiến hầu hết thời gian tác nhân sẽ không cần đến thông tin này.

Nơi tệp được chuyển đến: dịch vụ tạo tác

Hãy xem lại dòng này trong get_seatmap, ở agent/concert/tools.py:

await tool_context.save_artifact(f"seatmap_{event_id}.json", part)

tool_context đến từ ADK, ADK sẽ truyền vào công cụ của bạn dưới dạng một đối số và save_artifact là một phương thức trên đối số đó. Đó là dòng mà công cụ của bạn chuyển các byte đến ADK.

Sau đó, ADK sẽ ghi các tệp này bằng dịch vụ tạo tác mà ADK đã bắt đầu. Bạn đã chọn câu trả lời có cờ:

--artifact_service_uri="file://$WORKSHOP/artifacts"

ADK có 3 vị trí để lưu trữ các tệp này:

URI

Nơi các byte được chuyển đến

memory://

RAM. Sẽ biến mất khi bạn dừng quá trình này. Đây là chế độ mặc định nếu bạn không truyền cờ nào

file://...

một thư mục trên ổ đĩa. Những gì bạn đang sử dụng

gs://bucket

Cloud Storage. Những gì tác nhân được triển khai sử dụng ở bước cuối cùng

👉💻 Chọn tệp tại đây:

ls -R ~/monstertix/artifacts

Để chuyển từ máy tính xách tay sang Cloud Storage, hãy thay đổi file:// thành gs://. (Chúng ta sẽ thấy bước này trong Bước 10)

Cửa hàng kỷ niệm của chúng tôi

Bộ nhớ dưới dạng một giao diện, có một tệp phía sau

ADK cung cấp cho bạn một giao diện cho bộ nhớ dài hạn có tên là BaseMemoryService. Nó có hai phương thức:

# google.adk.memory.BaseMemoryService — ADK's interface, not your file
async def add_session_to_memory(session)                   # take a conversation in
async def search_memory(*, app_name, user_id, query)       # hand memories back

ADK cung cấp 3 cách triển khai:

Lớp

URI

Những gì cần có

InMemoryMemoryService

memory://

không có gì. Mất mọi thứ khi bạn dừng quy trình

VertexAiRagMemoryService

rag://

một tập hợp dữ liệu RAG của Vertex AI

VertexAiMemoryBankService

agentengine://

tài nguyên Agent Engine

Chúng tôi sẽ viết một bài cho hội thảo này. Loại bỏ InMemoryMemoryService vì nó sẽ quên mọi thứ khi bạn dừng quy trình, đồng thời loại bỏ 2 lựa chọn còn lại vì chúng cần tài nguyên trên đám mây và mất vài phút để tạo. Thay vào đó, hãy chọn một tệp vì bạn có thể gieo trước và đọc tệp đó trong trình chỉnh sửa. Vì vậy, hãy coi memory.py là cách triển khai của riêng bạn, chúng ta sẽ lưu trữ mọi thứ trong một tệp Markdown:

class MarkdownMemoryService(BaseMemoryService):

    async def search_memory(self, *, app_name, user_id, query) -> SearchMemoryResponse:
        path = self._path(user_id)                      # memory/userx.md
        return SearchMemoryResponse(memories=[
            MemoryEntry(content=types.Content(
                role="user", parts=[types.Part(text=path.read_text())]))])

SearchMemoryResponse và MemoryEntry là các loại của ADK, còn Ngân hàng bộ nhớ trả về các đối tượng tương tự. Một dịch vụ có dịch vụ được quản lý đằng sau. Tệp của chúng tôi có một tệp.

Chọn một tệp vì một lý do: bạn có thể mở và đọc tệp đó. Toàn bộ bộ nhớ dài hạn của tác nhân là một trang văn bản.

Vậy bạn sẽ hướng nó vào đâu?

Để tóm tắt việc triển khai trong hội thảo này, bạn có thể chọn 2 trong số 3 dịch vụ trên dòng lệnh. Thứ ba, chúng tôi đã tự triển khai vì lý do nêu trên:

Dịch vụ

Nơi bạn đặt

Hội thảo này

phiên

--session_service_uri trên adk web

sqlite+aiosqlite:///$WORKSHOP/sessions.db

sản phẩm đầu ra

--artifact_service_uri trên adk web

file://$WORKSHOP/artifacts

bộ nhớ

không có ở đâu trên dòng lệnh

MEMORY_DIR, đọc trong agent/concert/memory.py

Vì vậy, memory.py sẽ tự tạo dịch vụ từ một biến môi trường:

# agent/concert/memory.py
MEMORY_DIR = pathlib.Path(os.environ.get("MEMORY_DIR", "./memory"))
MEMORY_USER = os.environ.get("MEMORY_USER", "userx")
memory_service = MarkdownMemoryService(MEMORY_DIR)

. ./set_env.sh xuất cả hai. Lấy các tập hợp MEMORY_DIR nơi tác nhân đọc bộ nhớ và MEMORY_USER xác định tệp.

Hãy đi tìm xem mọi thứ ở đâu

Giờ đây, bạn có 4 vị trí có thể chứa một thông tin. Hãy xem cả 4.

Trước tiên, hãy tạo phiên của riêng bạn

👉 Nhấp vào Phiên mới trong thanh trên cùng.

👉✨ Nói 3 điều liên tiếp:

Sam can't do weeknights, by the way.

Phiên mới, lượt tương tác đầu tiên

Show me the seat map for the Amsterdam Saturday show.

Phiên mới, lượt thứ hai

Actually, hold that thought — what else is on in Amsterdam?

Thông báo thứ ba đó rất quan trọng. Thao tác này sẽ kết thúc lượt đọc bản đồ chỗ ngồi và việc kết thúc lượt đọc là nguyên nhân khiến temp: biến mất.

Có 3 thứ được cất giữ ở 3 nơi khác nhau:

Dữ liệu đã được lưu trữ

Công cụ nào đã làm việc này

Nơi mà nó đã đi

1

Sam không thể tham gia vào các buổi tối trong tuần

note_companion

user:prefs, ở trạng thái phiên

2

sơ đồ chỗ ngồi đầy đủ, 6 KB

get_seatmap, qua save_artifact

dịch vụ tạo tác

3

bản tóm tắt sơ đồ chỗ ngồi trên một dòng

get_seatmap, thành temp:seatmap

nowhere

👉 Kiểm tra ngay. Thẻ State (Trạng thái) và thẻ Artifacts (Hiện vật) nằm ở bên trái của adk web.

Thẻ Tiểu bang

Thẻ Cấu phần phần mềm

Trong thẻ State (Trạng thái), hãy lưu ý rằng temp:seatmap bị bỏ qua. Các khoá trạng thái có tiền tố temp: chỉ tồn tại trong quá trình gọi đang hoạt động và được ADK tự động loại bỏ trước khi duy trì trạng thái phiên vào cơ sở dữ liệu.

Sau đó là cuộc trò chuyện cũ hơn

👉 Chuyển sang two-days-ago trong trình đơn thả xuống của phiên ở trên cùng.

Phiên này chứa nhật ký trò chuyện được điền sẵn trong cơ sở dữ liệu.

Phiên cách đây 2 ngày

👉 Xem luồng sự kiện (cột ở giữa). Xin lưu ý rằng các lượt rẽ cũ đã được thay thế bằng một khối sự kiện tóm tắt.

Ngã rẽ thu gọn

👉 Xem thẻ State (Trạng thái). Có 3 loại khoá cùng nằm ở đó:

target_event_id   ms-ams-01                      ← plain: this booking only
party_size        2                              ← plain: this booking only
user:prefs        {"excluded_weekdays": [...]}   ← the person, every session

Tiền tố user: (ví dụ: user:prefs) đặt phạm vi trạng thái trên tất cả các phiên hoạt động của một người dùng nhất định. Ngược lại, các khoá không có tiền tố (

target_event_id

,

party_size

) biểu thị trạng thái phiên đơn giản, được giới hạn nghiêm ngặt trong phiên đang hoạt động và sẽ bị xoá khi phiên kết thúc.

👉 Bây giờ, hãy so sánh hai thẻ State (Trạng thái). Trong khi user:prefs và các cấu phần phần mềm được lưu trữ vẫn tồn tại, thì temp:seatmap sẽ không có trong cả hai phiên. Các khoá trạng thái có tiền tố temp: hoàn toàn là tạm thời và không bao giờ được duy trì trong cơ sở dữ liệu.

Sau đó, tệp

👉💻 Mở tệp đó trong trình chỉnh sửa:

cloudshell edit ~/monstertix/memory/userx.md

Có 3 lượt đặt phòng, tính từ tháng 1. Tác nhân sẽ đọc dữ liệu này khi gọi recall() và dữ liệu này vẫn sẽ ở đó ngay cả khi chúng ta mất dữ liệu phiên.

Bốn địa điểm

Nơi lưu trữ

Có tồn tại sau thông báo tiếp theo không?

Còn lại sau khi khởi động lại?

Vẫn tồn tại sau khi xoá phiên?

temp:seatmap

no

không

không

party_size – trạng thái phiên đơn giản, trong two-days-ago

có

có

không

user:prefs

có

có

yes

memory/userx.md

có

có

có và vẫn tồn tại sau khi bạn xoá cơ sở dữ liệu

một cấu phần phần mềm

có

có

tuỳ thuộc vào dịch vụ

Hãy đọc bảng đó như thể nó nói về các vùng chứa, chứ không phải về nội dung trong đó.memory/userx.md tồn tại trong mọi trường hợp và chỉ chứa những gì mà remember() chọn đặt vào đó. Không có gì ghi cuộc trò chuyện hiện tại của bạn vào đó. Vì vậy, việc tệp tồn tại lâu hơn một phiên không giống với việc tác nhân ghi nhớ những gì bạn vừa nói, và bước 5 là nơi sự khác biệt đó trở nên rõ ràng.

5 nơi mà một thông tin có thể xuất hiện và thời gian tồn tại của mỗi nơi

What state is

Hãy coi trạng thái là một từ điển đi kèm với phiên. Đọc và ghi thông tin này từ các công cụ của bạn thông qua tool_context.state, đồng thời cho phép ADK lưu thông tin này cùng với cuộc trò chuyện.

ADK chỉ định ý nghĩa của các tiền tố. Bạn không xác định chúng ở bất kỳ đâu. ADK đọc phần đầu tiên của tên khoá và dùng phần đó để quyết định vị trí và thời gian tồn tại của giá trị:

Một khoá có tên

ADK lưu trữ thông tin này

Kéo dài

party_size

với phiên

cho đến khi phiên bị xoá

user:prefs

đối với người dùng

trong mọi phiên mà người dùng đó có

app:tour_id

đối với ứng dụng

trên mọi người dùng

temp:seatmap

không nơi nào

một lời gọi

Lệnh gọi là một chu kỳ yêu cầu-phản hồi duy nhất, bao gồm tất cả hoạt động suy luận của mô hình, thực thi công cụ và phản hồi cuối cùng.

Khi một lệnh gọi kết thúc, ADK sẽ ghi trạng thái mới vào cơ sở dữ liệu và xoá mọi khoá temp: trên đường đi.

Một điều chưa được giải thích

Trong two-days-ago, bạn thấy một bản tóm tắt ở vị trí mà trước đây là một vài lượt đi đầu tiên của người chơi. ADK tự động viết câu này khi cuộc trò chuyện đủ dài.

Mọi sự thật đều có vòng đời nội dung

5. Giảm cấp và nén bối cảnh

Tóm tắt nội dung, bỏ qua chi tiết

Khi một cuộc trò chuyện quá dài, hệ thống sẽ tóm tắt các tin nhắn cũ để tiết kiệm dung lượng và mã thông báo.

Tóm tắt là một cách hay để nắm bắt ý chính và giúp tiết kiệm một số lượng mã thông báo.

Tính năng nén hoạt động trên phiên. Bộ nhớ là một kho lưu trữ riêng biệt

👉🔴 Trên bảng điều khiển địa điểm, hãy nhấn vào ĐẶT LẠI ĐỊA ĐIỂM. Mỗi bước đều bắt đầu từ cùng một thế giới: 8 chương trình, tất cả ghế đều có sẵn, đồng hồ ở tốc độ 1x.

👉💻 Chuyển đến mã của bước này:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 5 --force

Những thay đổi kể từ bước gần đây nhất:

Tệp

Những nội dung thay đổi

agent.py

tác nhân được bao bọc trong một App, với chế độ nén được bật. Một tác nhân thứ hai, budget_split, thực hiện phép tính số học về vé

mọi thứ khác

không thay đổi

agent.py không còn kết thúc bằng root_agent nữa. Nội dung kết thúc như sau:

app = App(
    name="concert",
    root_agent=root_agent,
    events_compaction_config=EventsCompactionConfig(
        compaction_interval=3,   # summarise after every 3 turns
        overlap_size=1,          # re-read 1 turn either side when summarising
    ),
)

Biết rằng adk web trước tiên sẽ tìm một biến có tên là app rồi quay lại root_agent. Gói tác nhân trong một App bất cứ khi nào bạn cần một thứ gì đó áp dụng cho toàn bộ ứng dụng thay vì cho một tác nhân: nén ở đây và khả năng tiếp tục ở các bước sắp tới.

compaction_interval=3 có nghĩa là ADK sẽ tóm tắt sau mỗi 3 lượt. Một lượt tương tác là khi bạn nói điều gì đó và tác nhân hoàn tất câu trả lời, bất kể tác nhân đã gọi bao nhiêu công cụ trong quá trình này. Ba là số nhỏ nên sẽ được kích hoạt trong một buổi hội thảo. Một ứng dụng thực tế sẽ sử dụng một số lượng lớn hơn nhiều.

overlap_size=1 có nghĩa là mỗi bản tóm tắt bắt đầu sớm hơn một lượt so với mức cần thiết.

ADK tóm tắt lượt 1, 2 và 3. Sau 3 lượt, hệ thống sẽ tóm tắt đợt tiếp theo. Với overlap_size=1, bản tóm tắt thứ hai sẽ bắt đầu lại từ lượt 3, chứ không phải lượt 4:

Hai bản tóm tắt, chồng chéo nhau một lượt

Lưu ý rằng lượt rẽ thứ 3 được tóm tắt 2 lần. Đó là hành động có chủ ý. Nếu bạn đặt câu hỏi ở lượt 3 và nhân viên hỗ trợ trả lời ở lượt 4, thì việc phân chia rõ ràng sẽ đặt câu hỏi vào một bản tóm tắt và câu trả lời vào bản tóm tắt khác, khiến cả hai đều khó hiểu. Việc lặp lại một lượt là một cách đảm bảo chắc chắn để tránh cắt ngang suy nghĩ của người khác.

Đặt cả hai trường. ADK không có giá trị mặc định cho cả hai.

Quá trình nén và dịch vụ bộ nhớ của bạn không bao giờ gặp nhau

Bạn nên nói rõ vì hai từ này nghe có vẻ liên quan đến nhau nhưng các hệ thống thì không.

EventsCompactionConfig nằm trên App ở agent.py, bên cạnh root_agent. Đây là một thuộc tính của ứng dụng chứ không phải của bất kỳ dịch vụ nào:

# agent/concert/agent.py
app = App(
    name="concert",
    root_agent=root_agent,
    events_compaction_config=EventsCompactionConfig(
        compaction_interval=3,
        overlap_size=1,
    ),
)

Bản tóm tắt viết lại cuộc trò chuyện. Thao tác này không ghi đè tệp.

Đó là lý do duy nhất khiến remember() tồn tại dưới dạng một lệnh gọi có chủ ý. Mọi nội dung còn lại trong bản chép lời cuối cùng sẽ được đọc dưới dạng một bản diễn giải do người khác viết; mọi nội dung được ghi vào tệp sẽ được đọc chính xác như nội dung đã được lưu trữ, vào ngày mai và tuần sau. Cuối bước này là lúc bạn nhận thấy sự khác biệt.

Bản tóm tắt được lưu trữ ở đâu

Trong sessions.db, trong bảng events, một bản ghi nén được tác nhân ghi vào một hàng sự kiện, có dấu thời gian như mọi sự kiện lượt khác.

Một sự kiện thông thường có trường content và nội dung tóm tắt nằm trong actions.compaction.

👉💻 Xem trong cơ sở dữ liệu của riêng bạn:

cd ~/monstertix
. ./set_env.sh
sqlite3 sessions.db \
  "select json_extract(event_data,'\$.actions.compaction.compacted_content.parts[0].text')
   from events where event_data like '%compaction%';"

Hình dạng của sự kiện sẽ như sau:

events row
└── event_data  (JSON)
    ├── id, author, timestamp, invocation_id      ← like every other event
    ├── content                                    ← ABSENT on this one
    └── actions
        └── compaction
            ├── start_timestamp                    ← the window it replaced
            ├── end_timestamp
            └── compacted_content.parts[0].text    ← the summary itself

Luồng sự kiện lấy từng sự kiện từ nội dung của sự kiện đó. Bản tóm tắt trong cơ sở dữ liệu và được gửi đến mô hình ở mỗi lượt.

Tác nhân thứ hai

budget_split là một tác nhân hoàn toàn riêng biệt, chứ không phải là một công cụ:

# agent/concert/agent.py
budget_split = Agent(
    name="budget_split",
    model=MODEL,
    include_contents="none",      # sees none of the conversation
    instruction="You do ticket arithmetic and nothing else...", #just the budget
    output_key="budget_plan",     # its answer goes into state
)

root_agent = Agent(
    ...,
    tools=[..., AgentTool(agent=budget_split)],
)

Tại sao lại là một tác nhân chứ không phải một hàm đơn thuần? Vì công việc này cần có một mô hình. Đọc "trong số các phần này, phần nào phù hợp với ngân sách 2.000.000 VND cho 4 người và tổng số tiền là bao nhiêu" dưới dạng phép tính số học được đưa ra dưới dạng phán đoán, với câu trả lời mà một người phải đọc được. Hãy để hàm Python nhân và đừng yêu cầu hàm này quyết định những lựa chọn nào đáng đề cập.

Vậy tại sao không để tác nhân chính thực hiện việc này? Nhìn vào dòng tiếp theo include_contents="none" có nghĩa là tác nhân này không được chuyển bất kỳ cuộc trò chuyện nào. Không có cuộc trò chuyện nhóm, không có lựa chọn ưu tiên, không có 40 ngày lưu diễn. Nó nhận được yêu cầu và không có gì khác. Cờ đó chỉ tồn tại trên một Agent, đó là lý do thực sự khiến đây là một tác nhân chứ không phải một hàm.

Một nhân viên hỗ trợ cấp dưới chỉ nhìn thấy yêu cầu và không thấy thông tin nào khác

Ưu tiên câu lệnh ngắn: rẻ hơn, nhanh hơn và khó bị chệch hướng hơn nhiều. Dựa vào số học không bị ảnh hưởng bởi những gì đã nói 20 lượt trước đó, vì số học không bao giờ thấy những điều đó.

Lý do

AgentTool

? Gói tác nhân bằng công cụ này để tác nhân chính có thể gọi tác nhân đó như mọi công cụ khác. Về phía mô hình, hãy đọc budget_split giống hệt như search_events.

Và

output_key="budget_plan" sẽ đặt câu trả lời vào trạng thái phiên theo tên đó thay vì để câu trả lời ở dạng tự do trong cuộc trò chuyện. Xem lý do tại sao điều này lại quan trọng.

Chạy ứng dụng

👉💻 Trong terminal 1, Ctrl-C và khởi động lại bằng các cờ tương tự:

cd ~/monstertix
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

👉 Bắt đầu một Phiên mới và trò chuyện thực sự. 5 hoặc 6 vòng. Cách này có hiệu quả:

hello
what's on in Amsterdam?
Sam is coming with us, and he can't do weeknights
there are 4 of us and I can spend about $200 a head. What fits?

👉 Mở thẻ Trạng thái.

Tìm budget_plan ở đó, chứa câu trả lời đầy đủ của tác nhân số học.

budget_plan sau câu hỏi đầu tiên về ngân sách

👉✨ Bây giờ, hãy thay đổi các con số trên đó:

what if 10 people come and I can only do $120 each?

👉 Xem lại budget_plan. Nội dung này đã được ghi đè chứ không được thêm vào. Đó là vì output_key giữ phiên bản mới nhất.

budget_plan sau câu hỏi thứ hai về ngân sách

Những nội dung bạn không thể xem: bản tóm tắt

Trong những lượt quay đó, ADK đã tóm tắt những lượt quay trước đó.

👉💻 Trong Terminal mới, hãy chạy để đọc trực tiếp:

cd ~/monstertix
. ./set_env.sh
./show-compaction.sh

Đọc nội dung mà AI đã viết

Sau đây là một ví dụ thực tế (đã rút gọn):

── summary 1 ──────────────────────────────────────────────────────

Conversation Language: English

  **User Request:**
  The user initially wanted to know what concerts are playing in Amsterdam.
  Subsequently, the user provided a constraint regarding a companion, Sam.

  **Context Summary:**
  The user is planning a concert trip. The agent identified two events in
  Amsterdam: "The Midnight Signal" at the Ziggo Dome on Tuesday, November
  10 and Saturday, November 14, 2026. The user then informed the agent that a
  companion named "Sam" will be joining, but "he can't do weeknights." The
  agent has successfully noted this constraint for Sam.

  **Key Decisions/Information Obtained:**
  *   **City of interest:** Amsterdam
  *   **Events found:** "The Midnight Signal" at the Ziggo Dome, Tuesday
  10 November and Saturday 14 November 2026.
  *   **Companion:** Sam
  *   **Companion constraint:** Sam cannot attend concerts on weeknights.

  **Unresolved Questions/Tasks:**
  *   The agent has not explicitly confirmed if the found event (which is
  on a Saturday) is suitable given Sam's constraint, nor has it asked the
  user for the next steps regarding this event or further event searches.

  **Tools Used:**
  *   `recall`
  *   `search_events`
  *   `note_companion`

Nó giữ nguyên giá, các phần, thành phố và lựa chọn ưu tiên, đồng thời ghi nhận chính xác rằng chưa có quyết định nào được đưa ra.

Từ đó, có 3 điều sau:

Đây là một cách diễn đạt lại. Không có nội dung nào ở đây là do người khác nói. Đó là một bản ghi lại những gì đã được nói, được tạo ra bằng cùng một loại quy trình tạo ra mọi thứ khác mà mô hình đôi khi hiểu sai.

Lưu ý: Các chuỗi tóm tắt được lưu trữ trong các bản ghi sự kiện phiên nội bộ và không được hiển thị trực tiếp dưới dạng các thẻ giao diện người dùng riêng biệt.

Cứ sau 3 lượt, trong một cuộc trò chuyện hiện có một phần là bản tóm tắt trước đó.

Cố gắng đặt phòng mà không lặp lại yêu cầu

Trong một phiên mới Đừng bắt đầu một phiên mới – bản tóm tắt mà bạn vừa đọc nằm trong các sự kiện của phiên này và đó là lý do duy nhất khiến trợ lý vẫn biết thông tin về Amsterdam.

👉✨ Không có gì mới:

Book us something.

Xem nhân viên hỗ trợ hỏi về chương trình, phần và số người. Đánh dấu là chính xác: bản tóm tắt cho biết người dùng chưa bao giờ xác nhận bất cứ điều gì, vì vậy trợ lý ảo đã hỏi. Hãy ghi nhận công lao của quá trình nén.

Ngay cả khi quá trình nén thành công, việc tóm tắt các lượt trước đó có thể gây ra sự mơ hồ, khiến trợ lý ảo phải xác nhận lại các ràng buộc với người dùng.

👉✨ Trả lời và cho phép yêu cầu đặt phòng được thực hiện:

Amsterdam Saturday, section C, 8 people

Nó sẽ đọc lại lệnh trước khi chi tiêu bất cứ thứ gì:

"Bạn muốn đặt 8 vé ở Khu C cho buổi biểu diễn The Midnight Signal tại Amsterdam vào thứ Bảy, ngày 14 tháng 11. Tổng chi phí sẽ là 760 USD. Tôi có nên mua những vé này không?"

👉✨ Xác nhận:

yes

Giờ đây, ứng dụng sẽ cho bạn biết rằng tính năng này đã hoạt động:

"Giao dịch mua 8 vé ở Khu vực C cho buổi biểu diễn của The Midnight Signal tại Amsterdam vào Thứ Bảy, ngày 14 tháng 11, với tổng giá trị là 760 USD đã hoàn tất. Mã đơn đặt hàng của bạn là ord_227877fe."

Hãy kiểm tra xem bạn có làm như vậy hay không

👉 Chuyển sang bảng điều khiển địa điểm. Đừng vội tin lời của nhân viên hỗ trợ.

Thứ tự trên bảng điều khiển địa điểm

Trên bảng điều khiển

Nội dung bạn nên xem

Số vé đã mua

1 và chính xác – một lượt đặt phòng, một đơn đặt hàng

đơn đặt hàng bên dưới

ord_xxxxxxxx — 8× section C · 760

Chuyến tham quan, Amsterdam, thứ Bảy

phần C giảm 8

Hoạt động

ord_xxxxxxxx — 8x section C @ 95 = 760

Kiểm tra bảng điều khiển để biết thông tin về việc bán vé.

Vì sao user:prefs là lựa chọn phù hợp

Hãy nhớ lại việc viết ràng buộc của note_companion vào bước cuối cùng của user:prefs và phản đối rõ ràng: tại sao phải bận tâm đến một công cụ khi tác nhân có thể chỉ cần nhớ nó.

Sau đây là lý do. Không đưa trạng thái vào cuộc trò chuyện và không có bản tóm tắt nào đề cập đến trạng thái đó. Kiểm tra user:prefs và budget_plan so với cách chúng được nhập vào và thấy rằng chúng không thay đổi. Sau đó, hãy kiểm tra bản chép lời.

Bản tóm tắt vẫn giữ được hình dạng chung nhưng mất đi các chi tiết. Lưu trữ mọi thứ mà bạn không thể để mất ở một nơi khác ngoài cuộc trò chuyện.

6. Tiếp tục xử lý

Chờ 40 phút mà không cần kết nối mạng

Khi một nhân viên hỗ trợ phải chờ trong hàng đợi gồm 40 phút, việc duy trì một quy trình Python đang hoạt động và chờ đợi là điều khó khăn và tốn kém.

Cách xử lý đúng cách khi thời gian chờ lâu:

  1. Lưu trạng thái vào cơ sở dữ liệu: Lưu trữ vị trí mà tác nhân đã tạm dừng để quy trình có thể tắt một cách an toàn.
  2. Đánh thức theo yêu cầu: Khi hết thời gian chờ, một tín hiệu bên ngoài sẽ khởi động lại tác nhân và tiếp tục thực thi ngay tại nơi tác nhân dừng lại.

👉🔴 Trên bảng điều khiển địa điểm, hãy nhấn vào ĐẶT LẠI ĐỊA ĐIỂM. Mỗi bước đều bắt đầu từ cùng một thế giới: 8 chương trình, tất cả ghế đều có sẵn, đồng hồ ở tốc độ 1x.

👉💻 Chuyển đến mã của bước này:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 6 --force

Chạy ứng dụng

Những thay đổi kể từ bước gần đây nhất:

Tệp

Những nội dung thay đổi

tools.py

đã thêm join_queue và check_queue

agent.py

join_queue được bao bọc trong LongRunningFunctionTool và App bật ResumabilityConfig

join_queue trả về ngay lập tức một vé thay vì chặn, còn ResumabilityConfig cho phép quá trình chạy tạm dừng và tiếp tục sau đó.

Những điểm nổi bật của tính năng tiếp tục

👉💻 Thiết bị đầu cuối 1 – tác nhân, không thay đổi:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

👉✨ Yêu cầu tác nhân mua hàng:

Get us two tickets to the Amsterdam show.

Nó gọi recall, sau đó gọi search_events, rồi gọi join_queue và trả lời bằng nội dung tương tự như sau:

You are in the queue for The Midnight Signal at Ziggo Dome, Amsterdam, on Saturday, November 14. Your position is 14203.

Bạn sẽ thấy rằng ứng dụng đã chọn Thứ Bảy mà không cần bạn yêu cầu. Có 2 buổi biểu diễn ở Amsterdam, ngày 17 là thứ Ba và tệp ghi nhớ ở bước cuối cùng cho biết Sam không biểu diễn vào các buổi tối trong tuần. Đó là một dữ kiện từ lượt đặt phòng trước đây quyết định một câu hỏi trong lượt đặt phòng này.

Cũng lưu ý rằng yêu cầu này đã được thêm vào hàng đợi trước khi xem xét một chỗ ngồi hoặc giá duy nhất và không đề cập đến cả hai. Đó là quy định của địa điểm, bạn mua vé cho đến khi đến lượt, vì vậy, điều duy nhất đáng có sớm là một vị trí trong hàng đợi. Chọn chỗ ngồi nào là câu hỏi cho 40 phút nữa.

👉 Xem bảng điều khiển. Biểu ngữ có nội dung:

Hàng đợi vẫn tiếp tục di chuyển trong khi không có nội dung nào của bạn chạy

Agent is waiting in line — #14,203
About 2400s at 1×. Safe to kill the agent right now — it will still be here.

Hàng đợi này thực sự dài 40 phút và sẽ không biến mất trong khi bạn làm việc.

👉💻 Huỷ tác nhân.:

Ctrl-C

👉💻 Khởi động lại trong thiết bị đầu cuối 1:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

👉✨ Trong cùng một phiên, hãy hỏi trợ lý ảo xem nó đang ở vị trí nào trong hàng đợi:

Where am I in line?
You are currently at position 12982 in the queue. It is not your turn yet.

Cùng một vé đó sau khi khởi động lại

So sánh vé và xem vị trí tiếp tục giảm trong khi không có gì đang chạy. Lưu ý rằng tác nhân không biết mình đã bị huỷ. Thao tác này gọi check_queue bằng vé mà thao tác nhận được trước khi khởi động lại, vì vé đó nằm trên ổ đĩa.

👉💻 Xem lý do:

cd ~/monstertix
sqlite3 sessions.db "select name from pragma_table_info('events');"
id
app_name
user_id
session_id
invocation_id
timestamp
event_data

Lưu ý rằng mỗi lượt trò chuyện là một hàng, và lượt trò chuyện đó, người nói, công cụ được gọi, nội dung trả về và lưu ý rằng đó là JSON bên trong event_data.

👉💻 Tìm vé hàng đợi của bạn tại đó:

sqlite3 sessions.db "select event_data from events;" \
  | grep -o '"ticket": "q_[a-z0-9]*", "position": [0-9]*' | tail -1
"ticket": "q_8cc2c6f9", "position": 14203

Khi join_queue trả về, vé của yêu cầu này sẽ được lưu trực tiếp vào cơ sở dữ liệu. Sau khi khởi động lại, ADK sẽ tải lại các lượt và kết quả công cụ trước đó từ ổ đĩa, cho phép tác nhân tiếp tục từ nơi đã dừng lại.

Và lưu ý những gì không bắt đầu lại. Tác nhân sẽ tiếp tục trực tiếp thay vì phát lại quy trình từ đầu. Vì LongRunningFunctionTool và ResumabilityConfig duy trì trạng thái thực thi trên ổ đĩa, nên ADK biết rằng tác nhân đã chờ đợi, ngăn tác nhân tìm kiếm lại ngày tham quan hoặc yêu cầu vé trùng lặp.

Cùng một phiếu yêu cầu, trong một quy trình đã bị buộc tắt

Tại sao nên chọn join_queue mà không phải check_queue

Cả hai hàm đều trả về giá trị tính bằng mili giây. Điểm khác biệt là liệu công việc cơ bản có hoàn tất khi hàm trả về hay không.

kết quả trả về

công việc đã hoàn thành chưa?

join_queue

Vé vào hàng đợi và vị trí ban đầu

Không. Quá trình xử lý tiếp tục không đồng bộ trong hệ thống bên ngoài.

check_queue

Trạng thái hiện tại của hàng đợi

Có. Trả về câu trả lời tức thì tại một thời điểm cụ thể.

LongRunningFunctionTool được thiết kế cho các thao tác trả về một lệnh xử lý cho công việc đang chờ xử lý. Việc bao bọc join_queue cho phép tác nhân tạm dừng thực thi trong khi hàng đợi bên ngoài tiến hành.

Ngược lại, check_queue là đồng bộ và hoàn tất. Việc bao bọc sẽ khiến tác nhân tạm ngưng thực thi không chính xác mỗi khi kiểm tra trạng thái của tác nhân, ngay cả khi tác nhân đến đầu hàng và cần hoàn tất giao dịch mua ngay lập tức.

Lệnh gọi nào vẫn đang chờ xử lý khi lệnh gọi đó trả về

Điều gì đã giúp bạn thành công

Có 3 thành phần cho phép tạm ngưng và khôi phục quá trình thực thi:

# agent/concert/agent.py
LongRunningFunctionTool(func=join_queue)      # the tool returns without finishing
ResumabilityConfig(is_resumable=True)         # the run may pause and be picked up
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db"
  • LongRunningFunctionTool: Đánh dấu một công cụ là không chặn, trả về một giá trị nhận dạng đang hoạt động để tác nhân có thể chờ mà không treo luồng.
  • ResumabilityConfig: Hướng dẫn thời gian chạy tạm dừng vòng đời thực thi trên một tác vụ chưa hoàn thành và tiếp tục từ trạng thái được ghi lại gần đây nhất.
  • Session Storage URI (--session_service_uri): Chuyển hướng hoạt động duy trì trạng thái đến bộ nhớ ngoài (ví dụ: SQLite, Cloud SQL) thay vì bộ nhớ xử lý, ngăn ngừa mất dữ liệu khi khởi động lại.

7. Hành động dựa trên dữ liệu lỗi thời

Thông tin sẽ lỗi thời trong khi bạn chờ

Nhật ký cuộc trò chuyện được lưu trữ không phải là trạng thái theo thời gian thực. Trong khoảng thời gian chờ đợi dài, dữ liệu bên ngoài (chẳng hạn như tình trạng còn vé hoặc giá vé) có thể thay đổi mà không được nhân viên hỗ trợ biết.

Một nhân viên thiếu thông tin sẽ tạm dừng để yêu cầu làm rõ, nhưng một nhân viên có dữ liệu cũ sẽ hành động ngay lập tức dựa trên những giả định lỗi thời. Để ngăn chặn các hành động không hợp lệ, bạn phải xác minh lại dữ liệu nhạy cảm về thời gian ngay trước khi thực hiện các thao tác quan trọng.

Chưa tải mã mới. Tiếp tục sử dụng tác nhân của bước trước (tác nhân này có thể mua vé nhưng hiện không có biện pháp bảo vệ nào đối với độ mới hoặc tính chất bất biến).

👉🔴 Nhấn vào ĐẶT LẠI ĐỊA ĐIỂM, sau đó bắt đầu một phiên mới trong adk web — nút + Phiên mới ở phía trên cuộc trò chuyện.

Bắt đầu một phiên mới để xoá vé hàng đợi trước đó và trạng thái giao dịch mua đang chờ xử lý. Bước này yêu cầu một ngữ cảnh thực thi rõ ràng để minh hoạ cách xử lý dữ liệu cũ một cách thích hợp.

Ảnh chụp nhanh nằm

👉✨ Yêu cầu phát lại chương trình đã xem:

Get us two tickets to the Amsterdam show.

👉✨ Bây giờ, trong khi chờ đợi, hãy hỏi về chỗ ngồi:

How much are the seats, and what's still available?
Section A: $210 per ticket, with 400 seats available.
Section B: $145 per ticket, with 900 seats available.
Section C: $95 per ticket, with 1200 seats available.

👉🔴 Nhấn vào BÁN VÉ NGỒI Ở VỊ TRÍ THUẬN LỢI. Phần A giảm xuống 0.

👉🔴 Nhấn vào BỎ QUA THỜI GIAN CHỜ để đưa nhân viên hỗ trợ lên đầu hàng đợi.

Ở đầu hàng đợi

👉✨ Yêu cầu mua:

Buy the two section A seats.

Thao tác này sẽ thực thi purchase(section="A") mà không cần xác minh lại tình trạng còn ghế, khiến API địa điểm trả về một phản hồi lỗi:

{"error": "sold_out", "available": 0,
 "message": "those seats are gone — re-fetch the seatmap before buying"}
Oh no! It looks like the Section A tickets just sold out. Let me get an
updated seat map to see what's still available.

Lỗi Venue API: phần A đã bán hết

Xin lưu ý rằng chỉ khi nhận được phản hồi lỗi API, tác nhân mới truy vấn lại sơ đồ chỗ ngồi sau đó. Nguyên nhân gốc là do ngữ cảnh cũ: trước đó trong phiên, trợ lý đã ghi lại "phần A: 400 chỗ trống" và lượt đó vẫn còn trong nhật ký trò chuyện được truyền cho mô hình.

Nêu rõ nơi lưu trữ thông tin cũ: thông tin đó vẫn tồn tại trong nhật ký cuộc trò chuyện và các tiền tố trạng thái như temp: không thể ghi đè thông tin đó.

Sơ đồ chỗ ngồi mà nhân viên vẫn đang sử dụng

Cách khắc phục

👉🔴 Trên bảng điều khiển địa điểm, hãy nhấn vào ĐẶT LẠI ĐỊA ĐIỂM. Mỗi bước đều bắt đầu từ cùng một thế giới: 8 chương trình, tất cả ghế đều có sẵn, đồng hồ ở tốc độ 1x.

👉💻 Bây giờ, hãy tải mã của bước này:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 7 --force

👉💻 Trong thiết bị đầu cuối 1, hãy nhấn Ctrl-C rồi khởi động lại:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

Nội dung thay đổi:

Tệp

Những nội dung thay đổi

fence.py

mới. Chạy ngay trước khi mua và có thể chặn giao dịch mua

agent.py

before_tool_callback=refresh_before_purchase

tools.py

purchase hiện gửi tiêu đề Idempotency-Key

fence.py là một lệnh gọi lại mà ADK chạy ngay trước khi có lệnh gọi công cụ:

root_agent = Agent(
    ...,
    before_tool_callback=refresh_before_purchase,   # re-read inventory, then decide
)

Bạn hãy chứng minh đi

👉🔴 ĐẶT LẠI ĐỊA ĐIỂM. Xếp hàng, nhấn vào BÁN GHẾ TỐT trong lúc chờ đợi, sau đó yêu cầu mua vé ở khu vực A.

Lần này, quá trình này sẽ tự dừng. Hãy xem trợ lý nói những câu như:

"Có vẻ như Khu vực A vừa bán hết vé. Tôi cần đọc lại sơ đồ chỗ ngồi."

Sau đó, hãy xem nó gọi lại get_seatmap mà không cần nhắc và quay lại đề nghị phần C.

👉🔴 Đọc kỹ nguồn cấp dữ liệu hoạt động của địa điểm, vì nguồn cấp dữ liệu này cho thấy cả hai hiệp:

error  purchase failed: Section A has 0 seats left, 2 requested. Re-read the
       seat map and pick again.
agent  calling purchase

Nguồn cấp dữ liệu địa điểm: nỗ lực và từ chối

Trên trang tổng quan, bạn thấy giao dịch mua cuộc gọi, nhưng bộ đếm vé vẫn là 0. Điều này xảy ra vì fence.py đóng vai trò là một cơ chế bảo vệ trước khi công cụ chạy:

  1. Tác nhân cố gắng gọi công cụ mua hàng.
  2. fence.py sẽ nhận yêu cầu trước và kiểm tra tình trạng còn chỗ ngồi.
  3. Khi thấy rằng các ghế đã bán hết, fence.py sẽ trả về lỗi ngay lập tức, huỷ giao dịch mua trước khi API được gọi.

Hướng dẫn về câu lệnh không thể đảm bảo rằng LLM sẽ không cố gắng mua những ghế đã bán hết. Các biện pháp bảo vệ bằng mã (before_tool_callback) đảm bảo giao dịch mua không hợp lệ sẽ không bao giờ xảy ra ngoài đời thực.

👉🔴 Nhấn vào ĐẶT LẠI ĐỊA ĐIỂM, sau đó nhấn vào HUỶ GIAO DỊCH MUA TIẾP THEO

👉✨ Yêu cầu Gemini mua và cho phép Gemini thử lại bao nhiêu lần tuỳ thích.

Vẫn chỉ có một đơn đặt hàng, dù bạn thử lại bao nhiêu lần

Số lượng đơn đặt hàng vẫn là 1 vì địa điểm nhận được cùng một khoá bất biến khi thử lại và trả về thông tin xác nhận ban đầu thay vì tạo một đơn đặt hàng trùng lặp.

8. Quy trình làm việc tự động

Mô hình ngôn ngữ lớn (LLM) không tuân thủ các quy tắc cứng nhắc mọi lúc

LLM rất giỏi đưa ra quyết định (chẳng hạn như chọn một chương trình dựa trên lựa chọn ưu tiên của người dùng). Nhưng nếu dựa vào LLM để tuân theo các quy tắc từng bước nghiêm ngặt (chẳng hạn như "luôn tham gia hàng đợi trước khi kiểm tra chỗ ngồi"), thì đôi khi LLM sẽ bỏ qua các bước hoặc làm rối loạn thứ tự.

Để khắc phục vấn đề này, hãy chia quy trình thành 2 loại bước:

  • Quy tắc cố định (Hàm mã): Các bước phải diễn ra theo đúng cách mỗi lần (lấy vé hàng đợi, kiểm tra vị trí) được viết dưới dạng các nút mã thông thường.
  • Quyết định (Nút tác nhân): Các bước đòi hỏi phải có phán đoán (chọn khu vực chỗ ngồi, kiểm tra lựa chọn ưu tiên của người dùng) sẽ được giao cho tác nhân AI.

Xem nội dung thực tế trong quy trình này. Một số nội dung hoàn toàn không có ý phán xét:

Bước

Phán quyết?

lấy đúng một vé hàng đợi

không. Luôn luôn là một, luôn luôn trước khi mua

kiểm tra xem đến lượt bạn hay chưa

không. Đặt câu hỏi, đọc câu trả lời

viết bản tóm tắt cho người mua

không. Cùng một câu, cùng một hình dạng, mỗi lần

chương trình nào, phần nào

có. Giá, ngày, người đến, những điều họ không thích

lựa chọn ưu tiên và hạn chế của người dùng

có.

Các quy trình nghiêm ngặt không nên dựa vào LLM vì các mô hình xác suất cuối cùng sẽ không tuân theo các trình tự cứng nhắc khi không được giám sát.

Workflow của ADK tách biệt quá trình thực thi có tính xác định với hoạt động suy luận của AI:

  • Các nút chức năng thực thi các quy tắc cố định (chẳng hạn như tham gia hàng đợi hoặc kiểm tra vị trí) một cách nhất quán mỗi lần.
  • Các nút tác nhân xử lý các quyết định theo bối cảnh (chẳng hạn như chọn chỗ ngồi dựa trên lựa chọn ưu tiên của người dùng).
  • Các nút chờ tạm ngưng việc thực thi mà không duy trì một quy trình hoạt động trong bộ nhớ.

Tác nhân ban đầu sẽ không bị thay thế. Đây sẽ là nút cuối cùng trong biểu đồ quy trình công việc, giữ lại các công cụ, quy trình kiểm tra độ mới và các biện pháp bảo vệ tính chất bất biến. Biểu đồ kiểm soát thứ tự thực thi, trong khi tác nhân thực thi quyết định mua.

👉🔴 Trên bảng điều khiển địa điểm, hãy nhấn vào ĐẶT LẠI ĐỊA ĐIỂM.

👉💻 Chuyển đến mã của bước này:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 8 --force

Chạy ứng dụng

👉💻 Trong thiết bị đầu cuối 1, hãy nhấn Ctrl-C rồi khởi động lại. Vẫn là lệnh như mọi khi:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

Những thay đổi kể từ bước gần đây nhất:

Tệp

Những nội dung thay đổi

nightly.py

mới. Chạy không cần giám sát dưới dạng biểu đồ Workflow ADK 2

agent.py

root_agent hiện là biểu đồ. Nhân viên hỗ trợ mà bạn đang trò chuyện vẫn ở đó, được đổi tên thành buyer_agent và là một nút bên trong

agent.py

nó cũng mất join_queue và check_queue – đồ thị hiện sở hữu hàng đợi

Chạy biểu đồ – 3 thông báo theo thứ tự

Chạy tất cả các thao tác sau trong python hằng đêm (Đây là nơi biểu đồ nằm). Gửi dữ liệu đầu vào giống hệt nhau ở mỗi lượt để quan sát quá trình chuyển đổi biểu đồ quy trình công việc qua các trạng thái của biểu đồ.

1. Lưu ý đối tượng bạn đang nói chuyện

👉 Tải lại giao diện người dùng web ADK.

# concert/agent.py, at the bottom

buyer_agent = Agent(name="concert", ...)   # ← everything you built, renamed

from .nightly import nightly
root_agent = nightly                       # ← what adk web actually runs

Xin lưu ý rằng adk web sẽ thực thi bất kỳ đối tượng nào được chỉ định cho root_agent. Ở đây, root_agent trỏ đến biểu đồ nightly Quy trình công việc, với buyer_agent được định cấu hình làm nút cuối cùng trong biểu đồ.

2. Bắt đầu

👉✨ Nhập nội dung sau rồi nhấn Enter:

go

Lưu ý: Trong biểu đồ quy trình công việc tự động hoá, quá trình thực thi ban đầu sẽ bắt đầu mà không cần chờ người dùng nhập dữ liệu. Việc gửi tin nhắn trong adk web sẽ kích hoạt lần chạy đầu tiên.

👉 Giờ hãy nhìn vào trình duyệt web. Bảng điều khiển bên trái vẽ biểu đồ và tô màu các nút khi chúng chạy. Bảng điều khiển bên phải là luồng sự kiện, cho biết cách đưa ra quyết định:

Đồ thị, được đỗ tại check_front

Xem queue_up ghi queue_ticket và queue_event_id vào trạng thái phiên.

Đồ thị, được đỗ tại check_front

Bạn sẽ thấy một lệnh gọi hàm có tên là adk_request_input, đang chờ câu trả lời. Sau đó, quá trình chạy sẽ kết thúc. Biểu đồ đạt đến check_front, cho thấy bạn ở vị trí thứ 14.203 và dừng lại.

👉 Kiểm tra bảng điều khiển địa điểm: một vé hàng đợi và không có gì đang chạy.

Đồ thị, được đỗ tại check_front

3. Đánh thức nó và chú ý đến ô mà bạn nhập

Di chuyển luồng sự kiện đến sự kiện adk_request_input và tìm hộp nhập được liên kết với sự kiện cụ thể đó:

Đồ thị, được đỗ tại check_front

Giờ đây, bạn có thể nhập nội dung ở hai nơi và mỗi nơi sẽ thực hiện một việc khác nhau.

Nơi bạn nhập

Điều sẽ xảy ra

hộp Enter your response... trên sự kiện adk_request_input

trả lời yêu cầu tạm dừng. Lượt chạy đang đỗ tiếp tục từ check_front

hộp trò chuyện chính ở dưới cùng

bắt đầu một lệnh gọi mới. Đồ thị phát lại từ START, chọn lại một chương trình và lấy vé hàng đợi thứ hai

👉✨ Nhập nội dung bất kỳ vào hộp Enter your response... rồi nhấn vào mũi tên.

👉 Nhà ga số 1:

[check_front] #14,197 — not yet. Pausing.

Một dòng. Hệ thống không chọn chương trình thứ hai và không lấy vé hàng đợi thứ hai, nếu không bạn sẽ ở cuối hàng đợi gồm 14.203 người.

Nếu bạn thấy [open] và [pick_show] chạy lại, tức là bạn đã nhập nội dung vào hộp trò chuyện. Nhấn vào ĐẶT LẠI ĐỊA ĐIỂM và bắt đầu lại bước này.

👉 Kiểm tra lại bảng thông tin về địa điểm: vẫn là một vé và vị trí đã giảm một chút vì đồng hồ của địa điểm vẫn chạy trong khi đồng hồ của bạn không chạy.

Đã được đánh thức một lần và vẫn còn một vé

4. Lên trước rồi kết thúc

👉🔴 Nhấn vào BỎ QUA THỜI GIAN CHỜ trên bảng điều khiển địa điểm.

Biểu đồ hoàn chỉnh, mọi nút đều có màu xanh lục

👉✨ Trả lời sự kiện adk_request_input mới nhất một lần nữa — vẫn trong hộp sự kiện chứ không phải hộp trò chuyện. Biểu đồ hoàn chỉnh, mọi nút đều có màu xanh lục

Lần này, check_front sẽ tìm thấy bạn ở phía trước và rơi xuống. Quan sát bảng điều khiển bên trái sáng lên brief và concert, hai nút cuối cùng.

Biểu đồ hoàn chỉnh, mọi nút đều có màu xanh lục

👉 Và xem trình duyệt. Đây là phần kết thúc của quá trình chạy và có 4 điều đáng đọc trong đó.

Biểu đồ hoàn chỉnh, mọi nút đều có màu xanh lục

Đồ thị có màu xanh lục hoàn toàn và các cạnh được tính. brief và concert đã được làm nổi bật và các cạnh của pick_show, queue_up và check_front hiện có một huy hiệu.

2x

là số lần biểu đồ đi qua cạnh đó, mỗi lần một lần khi thức. Số 2 ở trên cùng là giá của việc tiếp tục: mỗi lần, trình lập lịch sẽ nhập lại biểu đồ từ đầu. Điều này không có nghĩa là công việc đã diễn ra 3 lần và bảng điều khiển địa điểm là bằng chứng: một vé hàng đợi, không phải 2. @node(rerun_on_resume=False) là lý do.

Biểu đồ

Đồ thị: các hàm cho quy tắc, các tác nhân cho phán đoán

# agent/concert/nightly.py
nightly = Workflow(
    name="concert_nightly",
    edges=[(START, open_the_night, pick_show, queue_up,
            check_front, brief, buyer_agent)],
)

Lưu ý rằng có 2 nút là tác nhân và 2 nút là hàm, đồng thời coi việc xác định nút nào là nút nào là câu hỏi thiết kế tổng thể.

Nút

Tại sao lại có hình dạng đó

pick_show

agent. Việc chọn một chương trình trong nhật ký của người khác là hành vi đánh giá. Phương thức này gọi recall, đọc chuyến tham quan và trả về một Plan đã xác thực

queue_up

function. Tham gia hàng đợi là một quy tắc. Không nên đưa ra quy tắc vào lúc 3 giờ sáng

check_front

function. Câu hỏi "Am I at the front" (Tôi có ở phía trước không) có một câu trả lời đúng và địa điểm có câu trả lời đó

buy_it

agent. Đó là buyer_agent – phiên bản root_agent trước đây, không có gì thay đổi. Phần A đã bán hết trong thời gian chờ đợi – lấy B, lấy C hay không lấy gì cả? Đó là phán đoán mà bạn đã dành thời gian giảng dạy trong Mô-đun 4

Xem nút cuối cùng và tìm tác nhân mà bạn đã tạo. Không phải là nội dung sao chép, không phải là nội dung viết lại:

# agent/concert/nightly.py
from .agent import buyer_agent

Tất cả đều đi kèm: mọi công cụ mà nó có, việc đọc lại sơ đồ chỗ ngồi trong fence.py, khoá tính chất bất biến trong purchase. Chỉ có một khoá trùng khớp cho mỗi tệp.

Lý do khiến xe dừng lại thay vì chờ

Đây là phiên bản mà không ai nên viết:

# what NOT to write — this is nobody's file
while not ready:          # never do this
    time.sleep(1)

Nó duy trì một quy trình hoạt động trong 40 phút mà không làm gì cả, kết thúc cùng với thiết bị đầu cuối và không thể tiếp tục. Thay vào đó, check_front sẽ làm những việc sau:

# agent/concert/nightly.py — check_front
if not status["ready"]:
    return RequestInput(message=f"Still at #{position}. Wake me and I'll check.")

RequestInput là một gián đoạn. Đồ thị dừng ở vị trí hiện tại, lệnh gọi sẽ chuyển đến kho lưu trữ phiên và quy trình có thể thoát.

Chạy không tốn chi phí trong khi chờ đợi và kết thúc ở đúng một nơi: vé đã mua. Có thể bạn sẽ mất cả đêm và phải thức dậy vài lần mới đạt được trạng thái đó.

9. Làm việc với con người

Đừng chi tiền mà không kiểm tra lại với người khác

Một tác nhân tự trị chạy qua đêm không nên đưa ra những lựa chọn tài chính tốn kém dựa trên các lựa chọn ưu tiên được đoán hoặc không rõ ràng trong nhật ký trò chuyện.

Trước khi thực hiện các hành động quan trọng (chẳng hạn như chi tiền mua vé), tác nhân phải tạm dừng, yêu cầu người dùng xác nhận ngân sách của họ bằng lời nói và lưu lại nội dung xác nhận rõ ràng đó trước khi mua hàng.

👉🔴 Trên bảng điều khiển địa điểm, hãy nhấn vào ĐẶT LẠI ĐỊA ĐIỂM. Mỗi bước đều bắt đầu từ cùng một thế giới: 8 chương trình, tất cả ghế đều có sẵn, đồng hồ ở tốc độ 1x.

👉💻 Chuyển đến mã của bước này:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 9 --force

Chạy ứng dụng

👉💻 Trong thiết bị đầu cuối 1, hãy nhấn Ctrl-C rồi khởi động lại. Vẫn là lệnh đó:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

Những thay đổi kể từ bước gần đây nhất:

Tệp

Những nội dung thay đổi

budget.py

Lưu giữ những gì người đó đã đồng ý, dưới dạng câu mà họ đã nói. load() đọc thông tin về trạng thái của thiết bị và someone_is_there() trả lời xem có ai ở gần đó để hỏi hay không

tools.py

đã thêm set_budget, để tác nhân có thể ghi hạn mức đã thoả thuận vào trạng thái phiên sau khi một người xác nhận hạn mức đó

nightly.py

một nút đầu tiên mới, agree_budget, sẽ hỏi và đọc lại trước khi chạy bất kỳ nội dung nào khác. Nó ghi lại ngân sách và phần còn lại của yêu cầu, cả dưới dạng văn bản tự do và SEATS vì hằng số đã biến mất

Đừng mô hình hoá. Lưu câu.

Mở agent/concert/budget.py và đọc toàn bộ nội dung:

def load(state) -> str:
    """What they agreed, verbatim. Empty string means nobody has said."""
    return str(state.get("budget") or "")
state["budget"] = "up to $100 for the upper bowl or general admission, up to
                   $250 for the lower bowl, and up to $300 for a Saturday show"

Nút này kiểm tra hai điều: người đó đã nói và họ đã xác nhận.

Sau đó, nó sẽ giữ câu trong trạng thái phiên thay vì trong user:, có chủ đích. Ngân sách được thoả thuận cho một lượt đặt phòng. "250 đô la nếu họ là những người giỏi" là câu nói về ban nhạc đó, vào đêm đó và không liên quan gì đến việc đặt vé.

Điều này cũng đúng với mọi thứ khác mà họ nói

Đừng dừng lại ở ngân sách. Mọi người có thể thay đổi quyết định trong khi đang quyết định, vì vậy, hãy ghi lại mọi yêu cầu của họ tại đây thay vì dựa vào những gì nhân viên tình cờ nhớ được.

"I live in NYC, I can only do weekends, I have 10 people coming with me"
# agent/concert/nightly.py
def what_they_asked_for(ctx) -> str:
    """Everything this person typed, joined, in their own words."""
    said = []
    for event in (ctx.session.events or []):
        if event.author != "user":
            continue
        for part in (event.content.parts if event.content else []) or []:
            text = (getattr(part, "text", None) or "").strip()
            if text and text not in said:
                said.append(text)
    return "\n".join(said)

Đọc Chỉ những sự kiện do người dùng tạo, chỉ xem Chỉ

text

các phần từ function_response, vì vậy, hỏi và đáp về ngân sách sẽ không được phản hồi lại ở đây. Không có nội dung trùng lặp.

Lưu ý: agree_budget gọi lệnh này trong mỗi lần chạy, chứ không chỉ lần đầu tiên, vì vậy, một nội dung chỉnh sửa được nhập trong khi tác nhân đang xếp hàng cũng sẽ được ghi nhận. Sau đó, open_the_night người chọn sẽ đưa cả hai câu, câu hỏi và ngân sách cho người được chọn đọc.

Đồ thị, dài hơn một nút

Hãy bắt đầu bằng một cuộc trò chuyện:

# agent/concert/nightly.py
nightly = Workflow(
    name="concert_nightly",
    edges=[(START, agree_budget, open_the_night, pick_show, queue_up,
            check_front, brief, buyer_agent)],
)

Đồ thị tương tự, với agree_budget ở phía trước

Hai nút dừng và chờ, và chúng có cùng một cơ chế. check_front chờ một địa điểm và được một chiếc đồng hồ trả lời. agree_budget chờ một người và chỉ có thể được trả lời bởi một người. Đó là toàn bộ bước 10.

Kích hoạt từng bước

1. Bắt đầu

👉 Tải lại giao diện người dùng web ADK rồi chọn tác nhân concert.

👉✨ Yêu cầu những gì bạn thực sự muốn:

Get us two tickets to the Amsterdam show.

Lượt chạy dừng lại và hỏi bạn sẽ chi bao nhiêu

Ở bước 8, biểu đồ không có người dùng, vì vậy, bất cứ nội dung nào bạn nhập đều bị loại bỏ và go cũng như vậy. Ở đây, agree_budget gọi what_they_asked_for(ctx) trên dòng đầu tiên, thu thập mọi nội dung bạn đã nhập và đặt nội dung đó vào state["request"]. Sau đó, open_the_night sẽ đưa yêu cầu này cho người chọn cùng với ngân sách.

Bạn không nêu tên một ngày cụ thể và Amsterdam có 2 suất chiếu. Vì vậy, hãy xem bộ chọn chuyển về recall() cho ngày đó, tìm "Sam bails on weeknights" (Sam không tham gia vào các buổi tối trong tuần) và chọn thứ Bảy.

Trong trình duyệt, quá trình chạy dừng ở sự kiện adk_request_input:

Một biểu đồ vừa hỏi bạn một câu hỏi. Cùng một thông báo ngắt như khi chờ trong hàng đợi ở bước 8, nhưng giờ đây, thông báo này sẽ hướng đến một người để trả lời câu hỏi.

2. Trả lời theo cách mà mọi người thực sự trả lời

👉✨ Trong hộp Enter your response... (không phải hộp trò chuyện ở dưới cùng):

About a 100 for the cheap seats. 250 if they're the good ones.

Quan sát quá trình dừng và xác nhận:

[agree_budget] reading back: About a hundred for the cheap seats. Two-fifty
               if they're the good ones.

⚡ adk_request_input
   So: About a hundred for the cheap seats. Two-fifty if they're the good
   ones. Have I got that right?

Nó sẽ đọc lại số trước khi thực hiện

3. Xác nhận

👉✨ Cùng hộp:

Yes, that's right.
[agree_budget] agreed: About a hundred for the cheap seats. Two-fifty if
               they're the good ones.
[open]         Budget: About a hundred for the cheap seats. Two-fifty if
               they're the good ones.
[pick_show]    → {"section": "A", "reason": "..."}
[queue_up]     ms-ams-01 section A — ticket q_5924edc2 at #14,203
[check_front]  #14,203 — not yet. Pausing.

Bốn nút chạy trên một từ. 👉 Mở thẻ Trạng thái rồi tìm budget – câu của bạn, nguyên văn.

ngân sách trong thẻ Tiểu bang, theo cách diễn đạt của riêng bạn

Không có tiền tố. Đó là trạng thái phiên và bạn nên đưa ra quyết định có mục đích nếu bắt đầu một Phiên mới và ngân sách sẽ hết, vì vậy, hệ thống sẽ hỏi lại bạn.

4. Đưa lên trước và hoàn tất

👉🔴 Nhấn vào BỎ QUA THỜI GIAN CHỜ trên bảng điều khiển địa điểm.

👉✨ Trả lời adk_request_input mới nhất – một lần nữa trong hộp của:

go

Được đánh thức lại và không hỏi hai lần

👉 Kiểm tra bảng điều khiển địa điểm: một đơn đặt hàng, với mức giá nằm trong phạm vi bạn đã thoả thuận cho loại ghế đó.

Một đơn đặt hàng, trong phạm vi bạn đã đồng ý

Cách một biểu đồ đặt câu hỏi cho một người

Mở nightly.py và đọc agree_budget. Hãy đảm bảo 3 điều kiện sau đây cùng xảy ra và dự kiến sẽ gặp phải lỗi tương tự khi thiếu một trong các điều kiện: yêu cầu sẽ được gửi mãi mãi.

@node(rerun_on_resume=True)              # 1. run again when woken
def agree_budget(node_input, ctx):
    answers = ctx.resume_inputs or {}    # 3. where the answer arrives

    said = answers.get(ASK_BUDGET)
    if not said:
        return RequestInput(
            interrupt_id=ASK_BUDGET,     # 2. STABLE id, not the default
            message="What are you willing to spend?",
        )
    ...

rerun_on_resume=True

nếu không có, nút sẽ được coi là đã hoàn tất và bị bỏ qua

một chuồng ngựa interrupt_id

theo mặc định, mỗi lần chạy sẽ có một UUID mới, nên không có gì để tra cứu câu trả lời

ctx.resume_inputs[id]

nơi câu trả lời xuất hiện. Không phải là giá trị trả về của RequestInput – ADK lặp lại một nút thay vì gửi vào nút đó

Biểu đồ

Đây là đồ thị của bước 8, có thêm một nút ở phía trước. Đồ thị tương tự, với agree_budget ở phía trước

Các ràng buộc do người dùng xác nhận có phạm vi trong phiên và hết hạn khi cuộc trò chuyện kết thúc. Việc duy trì các giới hạn tạm thời trong các phiên có nguy cơ biến các quyết định một lần thành các giá trị mặc định vĩnh viễn ngoài ý muốn.

10. Ngăn xếp đám mây

Máy tính xách tay chuyển sang chế độ ngủ và tệp trên máy biến mất

Trên máy tính xách tay, một tác nhân phụ thuộc vào việc máy tính của bạn luôn ở chế độ hoạt động, cơ sở dữ liệu SQLite cục bộ và tệp cục bộ. Nếu bạn đóng nắp máy tính xách tay hoặc triển khai vào một vùng chứa đám mây khởi động lại, thì các tệp và bộ nhớ cục bộ sẽ biến mất.

Để chạy một cách đáng tin cậy trong quá trình sản xuất:

  1. Điện toán đám mây: Chạy tác nhân trong một vùng chứa (chẳng hạn như Cloud Run) luôn trực tuyến.
  2. Cơ sở dữ liệu đám mây bền vững: Thay thế các tệp cục bộ và SQLite bằng các dịch vụ đám mây (Cloud SQL cho dữ liệu phiên, Cloud Storage cho tệp).
  3. Cloud Timers: Sử dụng các trình lập lịch được quản lý (như Cloud Scheduler và Pub/Sub) để nhân viên hỗ trợ thức dậy ngay cả khi máy tính xách tay của bạn tắt.

👉🔴 Trên bảng điều khiển địa điểm, hãy nhấn vào ĐẶT LẠI ĐỊA ĐIỂM. Mỗi bước đều bắt đầu từ cùng một thế giới: 8 chương trình, tất cả ghế đều có sẵn, đồng hồ ở tốc độ 1x.

👉💻 Chuyển đến mã cuối cùng:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 10 --force

Tệp

Những nội dung thay đổi

main.py

mới. Một FastAPI phục vụ trang trò chuyện và điểm cuối mà trình lập lịch gọi

Dockerfile

mới. Python-slim và uvicorn thông thường. Không có gì về việc này là dành riêng cho ADK

monstertix/

được sao chép vào, nên trang này sẽ được chuyển trong cùng một vùng chứa với tác nhân

concert/

không thay đổi. Từng byte của thư mục mà bạn đã hoàn tất bước 9

Trang bạn đã tạo

** Giao diện người dùng web ADK** hoàn toàn là một giao diện gỡ lỗi cục bộ để kiểm tra trạng thái, luồng sự kiện và biểu đồ quy trình trong quá trình phát triển. Ứng dụng này không được đóng gói để triển khai trong môi trường phát hành công khai.

Đối với bản phát hành công khai, hãy sử dụng giao diện người dùng trò chuyện độc lập được tạo ở Bước 3. Nó xử lý các thông báo và hoạt động tương tác của người dùng bị gián đoạn thông qua điểm cuối /wake mà không làm lộ các chế độ kiểm soát gỡ lỗi nội bộ.

Di chuyển trạng thái cục bộ sang Dịch vụ đám mây

Dừng máy chủ và xem nội dung mà máy chủ đã ghi:

sessions.db          a SQLite file next to the code
memory/userx.md      a markdown file you can open in an editor
artifacts/           seat maps written to a folder

Mặc dù các tệp cục bộ hoạt động tốt trong quá trình phát triển, nhưng chúng sẽ không hoạt động trong quá trình sản xuất vì các vùng chứa Cloud Run sử dụng bộ nhớ tạm thời, cục bộ của phiên bản.

Khi chạy trong Cloud Run:

  • Các phiên bản có thể giảm xuống 0: Khi lưu lượng truy cập dừng lại, vùng chứa sẽ chấm dứt và hệ thống tệp cục bộ của vùng chứa sẽ bị xoá.
  • Các phiên bản mở rộng quy mô độc lập: Nhiều phiên bản đồng thời không dùng chung dung lượng ổ đĩa, mỗi phiên bản chạy với hệ thống tệp riêng biệt.

Nếu một tác nhân lưu một phiếu yêu cầu trong hàng đợi hoặc trạng thái phiên vào một tệp SQLite cục bộ (sessions.db), thì các yêu cầu tiếp theo được định tuyến đến một phiên bản vùng chứa mới hoặc khác sẽ tìm thấy một cơ sở dữ liệu trống. Để duy trì trạng thái bền vững trong quá trình mở rộng quy mô và khởi động lại, bạn phải thay thế các tệp cục bộ bằng các dịch vụ bên ngoài được quản lý.

Thay vào đó, bạn cần trạng thái tồn tại lâu hơn bất kỳ vùng chứa nào. Ba dịch vụ, mỗi dịch vụ cho một tệp:

Trên máy tính xách tay

Trên đám mây

Định nghĩa

sessions.db

Cloud SQL

cơ sở dữ liệu Postgres được quản lý. Nó tồn tại cho dù có ứng dụng nào đang chạy hay không

memory/userx.md

Cloud Storage

một bộ chứa. Tệp dưới dạng các đối tượng, có thể truy cập từ mọi phiên bản cùng một lúc

artifacts/

Cloud Storage

cùng một nhóm

clock.py

Cloud Scheduler

một cron mà Google chạy. Thiết bị đầu cuối của bạn không phải là đồng hồ báo thức

Tạo các mục này

Cung cấp cơ sở dữ liệu PostgreSQL, người dùng cơ sở dữ liệu và vùng lưu trữ Cloud Storage. Xin lưu ý rằng quá trình tạo phiên bản Cloud SQL mất vài phút và được bắt đầu ở chế độ nền trong setup.sh.

👉💻 Thu thập và thực hiện phần còn lại:

cd ~/monstertix
./setup-cloud-state.sh
→ instance  RUNNABLE
→ database  adk created
→ user      adk created
→ bucket    created
→ memory    memory/userx.md → gs://your-project-agent-you/memory/
✓ durable state ready. Written to .env

Tập lệnh đó đã tạo ra 4 đối tượng (mỗi đối tượng cho một hàng trong bảng ở trên) và ghi 2 URI vào .env:

CLOUD_SESSION_SERVICE_URI=postgresql+asyncpg://adk:...@/adk?host=/cloudsql/proj:region:instance
CLOUD_ARTIFACT_SERVICE_URI=gs://your-project-agent-you/artifacts

Triển khai Tác nhân bằng Trình kích hoạt được quản lý theo lịch

Sau khi các dịch vụ đã sẵn sàng, hãy cung cấp cho tác nhân vị trí lưu trú cố định.

👉💻 3 hoặc 4 phút. Bắt đầu và đọc tiếp:

cd ~/monstertix
. ./set_env.sh
./deploy-agent.sh

Tập lệnh này tạo một hình ảnh vùng chứa từ agent/, triển khai hình ảnh đó vào Cloud Run và cung cấp cơ sở hạ tầng hướng sự kiện hỗ trợ.

Quy trình triển khai hoàn chỉnh bao gồm 5 thành phần riêng biệt:

1. gcloud run deploy       the container: your agent, the graph, and the page
2. a Pub/Sub topic         a queue for messages
3. a service account       an identity, so the message is allowed in
4. a push subscription     topic ──► /trigger/wake
5. a Cloud Scheduler job   the alarm clock, on a cron

Cloud Scheduler làm Nguồn kích hoạt

Cloud Scheduler thay thế bộ hẹn giờ thực thi cục bộ (clock.py) bằng một dịch vụ cron được quản lý, duy trì độc lập với các phiên thiết bị đầu cuối cục bộ, xử lý các lần thử lại tự động và hỗ trợ lập lịch theo múi giờ.

Pub/Sub là lớp tách rời thông báo

Việc đặt một chủ đề Pub/Sub giữa Cloud Scheduler và Cloud Run mang lại 3 lợi thế về kiến trúc:

  • Tính bền vững của thông báo: Xếp hàng các sự kiện kích hoạt đến nếu dịch vụ Cloud Run đang khởi động nguội, triển khai lại hoặc tạm thời không hoạt động.
  • Chính sách thử lại tích hợp: Tự động thử lại các lỗi phân phối tạm thời mà không yêu cầu logic thử lại tuỳ chỉnh ở cấp ứng dụng.
  • Khả năng mở rộng Fan-Out: Cho phép nhiều dịch vụ hạ nguồn đăng ký cùng một sự kiện theo lịch mà không cần định cấu hình lại trình lập lịch.

văn bản thay thế

Việc định cấu hình trigger_sources=["pubsub"] sẽ đăng ký một điểm cuối truyền dữ liệu đẩy tương thích với tải trọng phong bì Pub/Sub tiêu chuẩn, dịch trực tiếp các thông báo đến thành các sự kiện gọi tác nhân.

→ agent    https://concert-you-xxxx.run.app
→ topic    presale-you created
→ iam      concert-trigger@your-project... may invoke concert-you
→ push     → https://concert-you-xxxx.run.app/trigger/wake
→ schedule 0 3 * * *

Nói chuyện với nó

👉 Mở URL của trợ lý ảo trong trình duyệt. 👉✨ Yêu cầu đặt chỗ:

Book me 3 tickets for The Midnight Signal in Tokyo. Best seats in the house,
I don't want the cheap ones.

Bạn nên hỏi xem bạn sẵn sàng chi bao nhiêu

👉✨ Trả lời như thể bạn đang nói to, sau đó xác nhận:

up to 250 a seat for the good ones

Lệnh chạy này hiện đang nằm trong hàng đợi.

👉🔴 Nhấn vào BỎ QUA THỜI GIAN CHỜ trên bảng điều khiển địa điểm để hàng đợi sẵn sàng khi tin nhắn đến.

Để chạy theo lịch thực tế, bạn phải bật công việc – jobs run từ chối công việc đang tạm dừng bằng Job.state must be ENABLED:

gcloud scheduler jobs resume presale-$(gcloud config get-value account | cut -d@ -f1) \
  --location=$GOOGLE_CLOUD_REGION

👉 Trong vòng vài giây, tác nhân sẽ thực hiện giao dịch mua trong giới hạn ngân sách đã xác nhận và tự động trả về thông tin xác nhận đơn đặt hàng.

👉🔴 Kiểm tra bảng điều khiển địa điểm. Một đơn đặt hàng.

Sau đây là cách để bạn chạy lệnh này sau mỗi 5 phút:

gcloud scheduler jobs update pubsub presale-$(gcloud config get-value account | cut -d@ -f1) \
  --location=$GOOGLE_CLOUD_REGION --schedule="*/5 * * * *"

Dọn dẹp

Đếm hai thứ được triển khai: địa điểm và nhân viên bên cạnh địa điểm đó. Cả hai đều có thể giảm xuống 0, vì vậy, một phiên bản không hoạt động sẽ không tốn phí. Vẫn xoá.

./destroy-agent.sh          # service, topic, subscription, scheduler job
./destroy-venue.sh

Lưu ý rằng những người này cố tình bỏ qua Cloud SQL và bộ chứa vì chúng chứa dữ liệu của bạn và cả hai đều tiếp tục tính phí. Khi bạn chắc chắn đã hoàn tất:

./destroy-agent.sh --all

Mọi thứ khác đều là nội dung địa phương. Xoá thư mục này khi bạn hoàn tất hoặc giữ lại và kích hoạt thư mục này trên dự án của riêng bạn trong thời gian tuỳ ý.

Tác nhân, các công cụ, lệnh gọi lại và ngân sách của tác nhân không thay đổi. Hai URI và một biến môi trường đã làm được.

11. Tổng quan về hệ thống và hướng dẫn khắc phục sự cố

Sự khác biệt về môi trường: Cấu trúc cục bộ so với cấu trúc đám mây

Máy tính xách tay

Trên đám mây

Những nội dung thay đổi

--session_service_uri=sqlite+aiosqlite:///...

Cloud SQL

một chuỗi kết nối

--artifact_service_uri=file://...

gs://bucket

một URI

memory/userx.md

gs://bucket/memory/

một đường dẫn

adk web

một dịch vụ Cloud Run

Dockerfile và main.py

monstertix/clock.py

Cloud Scheduler → Pub/Sub

trigger_sources=["pubsub"]

bạn, khi dùng bàn phím

someone_is_there()

mặc định, bị ghi đè theo từng yêu cầu

Việc triển khai nhân viên hỗ trợ cốt lõi, bao gồm root_agent, định nghĩa công cụ, cơ chế bảo vệ lệnh gọi lại và việc thực thi ngân sách vẫn hoàn toàn không thay đổi. Khi so sánh solutions/step10_deploy/concert/ với Bước 9, bạn sẽ thấy một kết quả khớp chính xác từng byte, xác thực cách lớp trừu tượng hoá dịch vụ của ADK tách cấu hình cơ sở hạ tầng khỏi logic của tác nhân.

Nếu bạn gặp phải tình huống này

Bạn có thể xem mã nguồn hoàn chỉnh ở bất kỳ bước nào:

./use-solution.sh N --force          # any step. resets memory + sessions + artifacts
./use-solution.sh N --force --keep   # code only, leave state alone

Khi xảy ra sự cố

Dấu hiệu

Cách khắc phục

✗ Cannot access project

Mã nhận dạng không chính xác. rm ~/project_id.txt && ./setup.sh

Mô hình trả về lỗi 404

Các email đại diện -latest chỉ có trên AI Studio. Sử dụng ADK_MODEL=gemini-latest-flash

address already in use

Đã có một ứng dụng sử dụng cổng 8000 hoặc 8090. lsof -ti:8000 | xargs kill -9

Nhân viên không thể đến địa điểm

curl $VENUE_URL/health hoặc ./deploy-venue.sh một lần nữa

Hàng đợi không bao giờ chuyển tiếp

Nhấn vào BỎ QUA THỜI GIAN CHỜ. Ở tốc độ 1x, hàng đợi sẽ không tự động chuyển lên phía trước

Vé hàng đợi thứ hai sẽ xuất hiện trên bảng điều khiển

Bạn đã trả lời trong hộp trò chuyện thay vì hộp Enter your response... trong sự kiện adk_request_input. Hộp trò chuyện sẽ bắt đầu một lượt chạy mới

Trình đơn thả xuống đang trống hoặc ứng dụng sẽ không tải

adk web coi mọi thư mục trong agent/ là một ứng dụng và sẽ không thành công toàn bộ danh sách nếu có một thư mục không phải là ứng dụng. ls agent/ chỉ nên hiển thị concert/ và không có gì khác – việc chạy các mục từ bên trong agent/ có thể để lại một memory/ không mong muốn. ./use-solution.sh 8 --force sẽ sắp xếp lại

[EXPERIMENTAL] cảnh báo

Dự kiến. EventsCompactionConfig và ResumabilityConfig đều ở giai đoạn trước khi phát hành rộng rãi trong phiên bản 2.6.2

Mọi thứ đều kỳ lạ

Nhấn vào ĐẶT LẠI ĐỊA ĐIỂM, rồi nhấn vào rm sessions.db

Tệp bộ nhớ chứa đầy các lượt đặt chỗ trùng lặp

remember() sẽ thêm vào mọi lượt chạy. ./use-solution.sh N --force sẽ đặt lại hoặc cp seed/memory/default.md memory/userx.md

adk: command not found

source .venv/bin/activate – mọi thẻ mới đều cần có

Cloud Shell hết thời gian chờ

Bắt đầu lại vào adk web. sessions.db đã sống sót – đây là bước 6, xảy ra với bạn mà không mất phí