1. ก่อนเริ่มต้น
เว็บไซต์จำหน่ายตั๋วส่วนใหญ่จะให้ผู้ซื้อเข้าคิวเพื่อป้องกันไม่ให้เซิร์ฟเวอร์ล่มและเพื่อจัดการกับดีมานด์ที่สูง คุณเริ่มซื้อและพบว่ามีคนรอคิวอยู่ 14,000 คน คิวจะเลื่อนไป 40 นาที เมื่อถึงคิวของคุณ คุณจะมีเวลาสั้นๆ ในการเลือกที่นั่งและชำระเงิน หากพลาดช่วงเวลาดังกล่าว ระบบจะให้สิทธิ์แก่ผู้อื่นแทน
งานในโลกแห่งความเป็นจริงหลายอย่างเป็นไปตามรูปแบบนี้ นั่นคือ งานที่ใช้เวลานานกว่าการสนทนาครั้งเดียวมาก ซึ่งทำงานกับระบบภายนอกที่คุณควบคุมไม่ได้ ตัวอย่างเช่น การรอให้ซัพพลายเออร์เติมสต็อก การรอการอนุมัติการอ้างสิทธิ์ หรือการรอในคิว
เอเจนต์ AI มาตรฐานจะอยู่ในแชทเดียว ซึ่งมีระยะเวลาไม่กี่วินาทีและเป็นแบบโต้ตอบเท่านั้น โดยไม่สามารถรอ 40 นาทีหรือดำเนินการด้วยตนเองได้ หากต้องการแก้ปัญหานี้ คุณต้องมีAgent ที่รันเป็นเวลานาน ซึ่งเป็นระบบที่อยู่ในสถานะไม่ได้ใช้งานเป็นเวลานาน คงบริบทไว้เมื่อรีสตาร์ทกระบวนการ และกลับมาทำงานต่อโดยอัตโนมัติเมื่อทริกเกอร์ภายนอกเรียกให้ทำงาน
เวิร์กช็อปนี้จะแนะนำวิธีสร้าง Agent คิวตั๋วคอนเสิร์ต โดยครอบคลุมรูปแบบสถาปัตยกรรมหลักที่จำเป็นสำหรับ Agent ที่ทำงานเป็นเวลานานใน Google Cloud
สิ่งที่คุณจะสร้าง

ในเวิร์กช็อปนี้ คุณจะได้สร้างเอเจนต์จองตั๋วที่ทำงานเป็นเวลานานไปพร้อมๆ กับการเรียนรู้รูปแบบซอฟต์แวร์ที่สำคัญของเอเจนต์
- การระงับและกลับมาทำงานอีกครั้ง: พักการเรียกใช้เอเจนต์ในช่วงเวลารอนานและกลับมาดำเนินการต่อผ่านทริกเกอร์ภายนอก
- ที่เก็บข้อมูลสถานะที่ทนทาน: คงสถานะการสนทนาและอาร์ติแฟกต์เมื่อรีสตาร์ทกระบวนการโดยใช้ SQLite/Cloud SQL และ Cloud Storage
- การป้องกันความล้าสมัยและความไม่เปลี่ยนแปลง: ตรวจสอบข้อมูลแบบไดนามิก (เช่น ความพร้อมให้บริการของที่นั่ง) ก่อนดำเนินการ และใช้คีย์ความไม่เปลี่ยนแปลงเพื่อป้องกันธุรกรรมที่ซ้ำกัน
- กราฟเวิร์กโฟลว์ ADK: จัดโครงสร้างการดำเนินการโดยใช้โหนดโค้ดที่กำหนดได้สำหรับกฎตามขั้นตอน และโหนด Agent สำหรับการตัดสินใจตามบริบท
- การอนุมัติโดยมีมนุษย์เป็นผู้ควบคุม: หยุดการดำเนินการชั่วคราวเพื่อรวบรวมและคงขีดจำกัดการใช้จ่ายที่ชัดเจนจากผู้ใช้
- การติดตั้งใช้งานในระบบคลาวด์: สร้างคอนเทนเนอร์และติดตั้งใช้งาน Agent ใน Cloud Run ด้วยทริกเกอร์ Cloud Scheduler และ Pub/Sub
ตั้งค่า
ก่อนอื่น ให้เตรียมสภาพแวดล้อมและตรวจสอบว่าคุณมีสิทธิ์เข้าถึง Google Cloud ฉันขอแนะนำให้เรียกใช้คำสั่งนี้ใน Cloud Shell ซึ่งมีเครื่องมือทุกอย่างที่คุณต้องการอยู่แล้ว เพียงตรวจสอบว่าคุณมีพื้นที่เพียงพอและบัญชีของคุณสามารถสร้างโปรเจ็กต์ได้
👉💻 โคลนที่เก็บและเรียกใช้การตั้งค่า
cd ~
git clone https://github.com/gca-americas/monstertix
cd ~/monstertix
./setup.sh
ระบบจะขอให้คุณดำเนินการนี้เพียงครั้งเดียว
Which Google Cloud project should this workshop use?
Find yours at https://console.cloud.google.com (top-left picker)
Project id [YOUR_PROJECT_ID]:
จากนั้นระบบจะดำเนินการต่อไปนี้ตามลำดับ
virtualenv | สร้าง | |
ลงชื่อเข้าใช้ |
| |
โปรเจ็กต์ | ชี้ | |
API | ช่วยให้เวิร์กช็อปมีสิ่งที่จำเป็น | |
| เขียนรหัสโปรเจ็กต์ ภูมิภาค และโมเดล เพื่อให้ทุกสคริปต์และ | |
การตรวจสอบโมเดล | จะโทรหา Gemini จริงๆ 1 ครั้ง ดังนั้นหากรหัสโปรเจ็กต์หรือโมเดลไม่ถูกต้อง ระบบจะแสดงข้อผิดพลาดที่นี่ | |
seed | สร้างเซสชันที่มีอายุ 2 วันแล้ว ซึ่งขั้นตอนที่ 3 จะเปิดขึ้น | |
Cloud SQL | เริ่มอินสแตนซ์ Postgres ในเบื้องหลัง | |
venue | ติดตั้งใช้งานผู้จำหน่ายตั๋วจำลองของคุณเองใน Cloud Run | |
👉💻 เปิดใช้งานสภาพแวดล้อม
cd ~/monstertix
source .venv/bin/activate
ตอนนี้พรอมต์ของคุณขึ้นต้นด้วย (.venv)
ภาพรวมสถานการณ์
ในสถานการณ์สมมติของเวิร์กช็อปนี้ ตัวแทนของคุณจองตั๋วคอนเสิร์ต 2 ใบ ("The Midnight Signal" ในอัมสเตอร์ดัม) ค่ากำหนดของผู้ใช้ที่จัดเก็บไว้ในหน่วยความจำประวัติ ได้แก่
- ต้องเป็นรายการช่วงสุดสัปดาห์ (แซมไม่สามารถเข้าร่วมในคืนวันธรรมดาได้)
- หลีกเลี่ยงที่นั่งชั้นบนสุดที่ Ziggo Dome (มองเห็นไม่ชัด)
- ราคาเป้าหมายอยู่ที่ประมาณ $200 ต่อตั๋ว โดยมีขีดจำกัดสูงสุดที่ $250

ยืนยันการติดตั้ง
👉💻 คำสั่งเดียว 8 เครื่องหมายถูก
./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.
👉 เปิดแท็บเบราว์เซอร์ที่ 2
สถานที่ | คำอธิบาย |
| แผงควบคุม ปุ่มต่างๆ |
2. ใช้พรอมต์เพื่อสร้างอิสระในการทำงานใช่ไหม
พรอมต์ไม่สามารถเริ่มได้ด้วยตัวเอง
พรอมต์เป็นเพียงข้อความที่ส่งไปยังโมเดล AI เมื่อมีการเรียกใช้ โดยจะทำงานเอง ปลุกตัวเอง หรือเริ่มการโทรใหม่ไม่ได้
หากคุณบอกเอเจนต์ในพรอมต์ว่า "กลับมาตรวจสอบอีกครั้งใน 10 นาที" จะไม่มีอะไรเกิดขึ้นหลังจากรอบปัจจุบันสิ้นสุดลง โมเดลไม่มีนาฬิกาหรือลูปภายใน หากต้องการให้เอเจนต์ทำงานได้โดยอัตโนมัติ จะต้องมีสิ่งภายนอกเอเจนต์ที่เรียกให้เอเจนต์ทำงาน

เริ่มการติดตั้ง
เราจะเริ่มด้วยการทดสอบเครื่องมือสำหรับนักพัฒนาแอป adk web จะเริ่มUI เว็บของ ADK ซึ่งเป็นอินเทอร์เฟซการพัฒนาที่มาพร้อมกับ ADK ซึ่งช่วยให้คุณพูดคุยกับ Agent ที่คุณสร้างได้โดยไม่ต้องเขียนฟรอนท์เอนด์ก่อน และแสดงสิ่งต่างๆ ที่หน้าต่างแชทไม่สามารถแสดงได้ เช่น การเรียกใช้เครื่องมือแต่ละรายการและอาร์กิวเมนต์ของเครื่องมือ สถานะเซสชันเมื่อมีการเปลี่ยนแปลง อาร์ติแฟกต์ที่ Agent บันทึกไว้ และเหตุการณ์ดิบที่อยู่ด้านล่าง คุณจะไม่เปิดเผยในเวอร์ชันที่ใช้งานจริง ซึ่งเป็นหน้าต่างที่ดีที่สุดในการดูเอเจนต์ที่ทำงานอยู่ขณะที่คุณกำลังเรียนรู้ ในขั้นตอนที่ 3 คุณจะสร้างส่วนหน้าที่จะแสดงจริง และในขั้นตอนสุดท้าย คุณจะนำไปใช้งาน
👉🔴 กดรีเซ็ตสถานที่ในแผงสถานที่ ทุกขั้นตอนเริ่มต้นจากโลกเดียวกัน นั่นคือ 8 รายการ ทุกที่นั่งว่าง และนาฬิกาที่ 1x
👉💻 Terminal 1 - โหลดโค้ดของขั้นตอนนี้และเริ่ม ADK web UI:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 2 --force
adk web agent --port 8000 --allow_origins "*"
use-solution.sh จะคัดลอกโค้ดที่เสร็จแล้วของขั้นตอนไปยัง agent/ ซึ่งเป็นที่ที่คุณทำงาน ทุกขั้นตอนเริ่มต้นด้วยคำสั่งนี้ และจะพิมพ์ไฟล์ที่เปลี่ยนแปลงเพื่อให้คุณทราบว่าต้องอ่านไฟล์ใด
สิ่งที่คุณเริ่มต้นด้วย
ไฟล์ | คำอธิบาย |
| เอเจนต์ - ชื่อ รุ่น คำสั่ง รายการเครื่องมือ |
|
|
| ไคลเอ็นต์ HTTP สำหรับสถานที่ ไม่มีอะไรน่าสนใจ |
| โมเดลที่จะใช้ และตรวจสอบว่าข้อมูลเข้าสู่ระบบของคุณใช้งานได้ |
. ./set_env.sh จะเปิดใช้งาน virtualenv โหลดโปรเจ็กต์และโมเดลของคุณ และส่งออก $WORKSHOP ซึ่งเป็นเส้นทางแบบสัมบูรณ์ไปยัง ~/monstertix เรียกใช้ในเทอร์มินัลทุกเทอร์มินัลที่คุณเปิด โดยจะพิมพ์ค่าที่ตั้งไว้
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
👉 เปิดแท็บเบราว์เซอร์แรก
สถานที่ | คำอธิบาย |
UI ของเว็บ ADK |
|
คุยกับมัน
👉✨ ใน ADK Web UI ให้พิมพ์
I want to see The Midnight Signal. I'm in Amsterdam, going with Sam, budget around $200 each.
โดยจะค้นหาทัวร์ อ่านแผนที่ที่นั่ง และพิจารณางบประมาณและบริษัทของคุณ นี่คือผู้ช่วยที่มีความสามารถอย่างแท้จริง อย่างไรก็ตาม เมื่อการขายล่วงหน้าเปิดขึ้นในเวลา 10:00 น. เอเจนต์จะไม่ดำเนินการโดยอัตโนมัติเนื่องจากไม่มีกระบวนการหรือทริกเกอร์เบื้องหลังที่ทำงานอยู่
อ่านสิ่งที่เพิ่งเกิดขึ้น
ตรงกลางของหน้าจอไม่ได้เป็นเพียงบันทึกการแชท ทุกขั้นตอนของเทิร์นจะมีหมายเลขกำกับ รวมถึงการเรียกใช้เครื่องมือด้วย
▸ 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
👉 คลิกแถวที่มี ⚡ แผงด้านซ้ายแสดงอาร์กิวเมนต์ที่โมเดลเลือก คลิก ✓ ด้านล่างเพื่อดู JSON ที่ส่งกลับมา
ข้อมูลทัวร์ไม่ได้อยู่ในข้อมูลการฝึกหรือพรอมต์ของโมเดล เอเจนต์ดึงข้อมูลนี้โดยการเรียกใช้เครื่องมือ search_events ซึ่งจะค้นหา API ของสถานที่
เปิดโค้ดที่คุณเพิ่งเรียกใช้
คุณได้เห็นการทำงานและร่องรอยของมัน ตอนนี้มาดูที่โค้ดกัน
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
👉💻 เปิด agent/concert/agent.py เอเจนต์ทั้งหมดจะอยู่ด้านล่างของไฟล์
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],
)
ชื่อ โมเดล คำอธิบายเกี่ยวกับลักษณะการทำงานของเอเจนต์ และรายการฟังก์ชัน Python นั่นคือทั้งหมดที่ Agent เป็น adk web พบเพราะไฟล์ชื่อ agent.py และตัวแปรชื่อ root_agent
👉💻 ตอนนี้ 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)
ฟังก์ชันปกติ ไม่ต้องใช้ Decorator ไม่ต้องลงทะเบียน ไม่ต้องเขียนสคีมา ADK จะอ่านลายเซ็นและ Docstring แล้วสร้างคำจำกัดความของเครื่องมือที่โมเดลเห็น ซึ่งเป็นเหตุผลที่ Docstring นั้นมีความสำคัญ และเหตุผลที่การลบอาร์กิวเมนต์หนึ่งรายการทำให้ลักษณะการทำงานของโมเดลเปลี่ยนไป
โมเดลจะไม่เรียกใช้โค้ดของคุณ เมื่อคุณเห็น search_events({"city": "Amsterdam"}) แสดงว่าโมเดลขอการเรียกใช้ฟังก์ชันนั้น ADK เรียกใช้ฟังก์ชัน ส่งผลลัพธ์กลับ และโมเดลเขียนประโยคเกี่ยวกับผลลัพธ์นั้น โดยลูปมีลักษณะดังนี้
👉 ไอคอนที่ขอบด้านซ้ายสุดจะสลับสิ่งที่แผงแสดง ส่วนที่ 2 คือกราฟตัวแทน ลองใช้ดู แล้วคุณจะเห็นเครื่องมือ 2 อย่าง

ดังนั้นจงบอกให้มันอย่าหลับ
คุณจะทำอย่างไรเพื่อแก้ไข ลองใช้สิ่งที่คนส่วนใหญ่ใช้เป็นอย่างแรก เรียกคืนINSTRUCTIONจากขั้นตอนที่ 1 ซึ่งเป็นข้อความที่ส่งให้โมเดลในทุกๆ รอบ แล้วเขียนใหม่
👉💻 เปิด agent/concert/agent.py ด้านล่าง INSTRUCTION มีอีกหนึ่งรายการที่เขียนไว้ให้คุณแล้ว หากต้องการเปลี่ยนด้วยตนเอง ให้ทำดังนี้
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],
)
👉💻 ใน Terminal 1 ให้รีสตาร์ทโดยทำดังนี้
cd ~/monstertix
adk web agent --port 8000 --allow_origins "*"
👉✨ บอกแผนให้ชัดเจนเหมือนที่คุณบอกกับคนปกติ
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.
โดยจะตอบรับและฟังดูเหมือนว่าตั้งใจตอบจริงๆ
👉 ตอนนี้ให้รอ อย่าพิมพ์อย่างอื่น ดูการสนทนา
โปรดรอสักครู่ ไม่มีอะไรเกิดขึ้น
เหตุผลที่พรอมต์นั้นใช้ไม่ได้
อ่านอีกครั้งด้วยมุมมองใหม่ ตรวจสอบ ทันทีที่เปิด โปรดคอยตรวจสอบ ซึ่งแต่ละรายการจะอธิบายสิ่งที่เกิดขึ้นเมื่อเวลาผ่านไป
ตอนนี้มาทบทวนกันว่า instruction= คืออะไร คือสตริงที่ส่งไปยังโมเดลเป็นส่วนหนึ่งของคำขอ ในขณะที่ผู้ใช้ส่งข้อความ ไม่มีอะไรอ่านระหว่างเทิร์น
# agent/concert/agent.py
root_agent = Agent(
instruction=PROACTIVE_INSTRUCTION, # read on invocation
... # ...and there was no invocation
)
คำสั่งจะเรียกใช้ฟังก์ชันที่เขียนไว้ภายในไม่ได้
รูปแบบพรอมต์ที่ไม่สำเร็จ
การพยายามบังคับใช้การดำเนินการแบบอัตโนมัติผ่านคำสั่งพรอมต์จะไม่สำเร็จไม่ว่าจะใช้คำใดก็ตาม การพยายามป้อนพรอมต์ที่พบบ่อย เช่น
- "ตั้งตัวจับเวลาเป็น 10:00 น. วันอังคาร แล้วซื้อตั๋วตอนนั้น"
- "ตรวจสอบทุกๆ 5 นาทีว่าการขายล่วงหน้าเปิดหรือยัง"
- "ดำเนินการโดยอัตโนมัติโดยไม่ต้องรอข้อมูลจากผู้ใช้"
ทั้งหมดล้มเหลวด้วยเหตุผลเดียวกัน นั่นคือ ระบบจะอ่านพรอมต์เมื่อมีการเรียกใช้เท่านั้น หากไม่มีผู้โทรภายนอก ระบบจะไม่เรียกใช้โค้ดระหว่างเทิร์น
3. การไม่มีการเคลื่อนไหวที่เกิดจากเหตุการณ์
เอเจนต์ไม่ได้ตื่นอยู่ตลอดเวลา
การปล่อยให้กระบวนการทำงานแบบสำรวจอย่างต่อเนื่องจะทำให้สิ้นเปลืองหน่วยความจำ เสียเงิน และเกิดข้อขัดข้องหากแล็ปท็อปเข้าสู่โหมดสลีปหรือรีสตาร์ท
แทนที่จะให้ Agent ทำงานอย่างต่อเนื่องในขณะที่รอ Agent ที่รันเป็นเวลานานควรหยุดทำงานไปเลย เหตุการณ์ภายนอก (เช่น ตัวจับเวลา, เว็บฮุก หรือข้อความ) จะเรียกใช้เอเจนต์เมื่อมีงานที่ต้องทำจริงๆ เท่านั้น
👉🔴 กดรีเซ็ตสถานที่ในแผงสถานที่ ทุกขั้นตอนเริ่มต้นจากโลกเดียวกัน นั่นคือ 8 รายการ ทุกที่นั่งว่าง และนาฬิกาที่ 1x
👉💻 โหลดโค้ดของขั้นตอนนี้
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 3 --force
คุณจะเห็นว่าเราติดตั้ง monstertix/ ซึ่งมีเซิร์ฟเวอร์ นาฬิกา และอีกอย่างที่คุณยังไม่เคยเห็น ตรวจสอบว่าไม่ได้แตะต้อง agent/ เนื่องจากไม่มีการเปลี่ยนแปลงใดๆ เกี่ยวกับตัวแทนในขั้นตอนนี้ จำไว้ว่านั่นคือประเด็น
ไฟล์ | คำอธิบาย |
| อยู่เสมอ เว็บเซิร์ฟเวอร์ที่มี Runner อยู่ด้านหลัง |
| อยู่เสมอ สิ่งที่ทราบเวลาและเรียกเซิร์ฟเวอร์ |
โดยวิดีโอจะแบ่งออกเป็น 2 ส่วนและจะอยู่ใน 2 ไฟล์ตลอดจนถึงระบบคลาวด์ 
กลายเป็นในขั้นตอนที่ 10 | |||
ทริกเกอร์ | อุปกรณ์ปลายทางที่เรียกใช้เอเจนต์ได้ | Cloud Run | |
ผู้ทริกเกอร์ | อะไรก็ตามที่มีนาฬิกาที่เรียก | Cloud Scheduler | |
ทริกเกอร์ - เว็บเซิร์ฟเวอร์ของคุณเอง
ใช่ เว็บเซิร์ฟเวอร์ ต้องมีสิ่งรับฟังก่อนจึงจะเรียกใช้ได้ UI เว็บของ ADK จากขั้นตอนก่อนหน้าคืออินเทอร์เฟซการพัฒนาที่สามารถเรียกใช้เอเจนต์ได้
👉💻 เปิด 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 จะไม่ทำงานด้วยตัวเอง ซึ่งอาจเป็นคำอธิบาย เช่น ชื่อ รุ่น คำสั่ง หรือรายการเครื่องมือ โดยจะต้องมีสิ่งหนึ่งที่รับข้อความขาเข้า ค้นหาเซสชันที่ถูกต้อง ส่งต่อการสนทนาที่ผ่านมาให้โมเดล เรียกใช้เครื่องมือที่โมเดลขอ ส่งผลลัพธ์กลับ วนซ้ำจนกว่าจะหยุดเรียกใช้เครื่องมือ และเขียนทุกขั้นตอนของการดำเนินการดังกล่าวลงในที่เก็บข้อมูล ซึ่งก็คือRunner

UI ของเว็บ ADK มีค่าเริ่มต้นให้คุณ คุณสร้างที่นี่ด้วยตนเอง และนั่นคือความแตกต่างทั้งหมดระหว่าง ADK Web UI กับสิ่งที่คุณสามารถติดตั้งใช้งานได้
👉💻 Terminal 2 — เริ่มต้นใช้งาน
เริ่ม 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.
อ่านบรรทัดสุดท้าย ทริกเกอร์ทำงานและโหลด Agent แล้ว แต่ไม่มีอะไรเกิดขึ้น แยกแนวคิด 2 อย่างนี้ออกจากกัน ได้แก่ การเรียกใช้ได้และการถูกเรียกใช้
และประตูหน้าในขณะที่เราอยู่ที่นี่
แม้ว่า adk web จะเป็นเครื่องมือพัฒนาที่ยอดเยี่ยม แต่ก็ไม่ควรเป็นสิ่งที่คุณจัดส่ง คุณไม่ควรเปิดใช้เมื่อติดตั้งใช้งานในเวอร์ชันที่ใช้งานจริง และไม่ควรชี้ให้ลูกค้าดูหากเปิดใช้ ดังนั้นเซิร์ฟเวอร์เดียวกันจึงแสดงหน้าเว็บด้วย
👉 เปิด http://127.0.0.1:8090 ในเบราว์เซอร์ ซึ่งเป็นที่อยู่เดียวกัน
พบกับ MonsterTix หน้าต่างแชท ชื่อที่มุม และปุ่ม 2 ปุ่ม โดยจะโพสต์ไปยัง /wake และแสดงผลสิ่งที่ส่งกลับมา และจะทำงานโดยไม่มีการเปลี่ยนแปลงกับปลายทางใดๆ ที่ตอบกลับในลักษณะเดียวกัน
ในหน้า | สิ่งที่อยู่ข้างใต้ |
การสนทนา |
|
+ เซสชันใหม่ | รหัสเซสชันใหม่ - การสนทนาใหม่ และต่อมาก็งบประมาณใหม่ |
รีเซ็ตหน่วยความจำ | จะลบทุกเซสชันและไฟล์ความทรงจำ โดยจะถามก่อนและแสดงให้คุณเห็นว่าสถานะดังกล่าวอยู่ที่ใด |
👉✨ พูดอะไรก็ได้กับ What Amsterdam dates are there? แล้วดูเทอร์มินัล 2 Runner เดียวกัน เอเจนต์เดียวกัน บรรทัดบันทึกเดียวกัน มีเพียงผู้โทรเท่านั้นที่เปลี่ยนไป
ทริกเกอร์ - สิ่งที่รู้เวลา
👉💻 เปิด monstertix/clock.py ดูรหัส
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
กลไกนี้ง่ายมาก นั่นคือ สลีป, POST, อาจทำซ้ำ ตัวกำหนดเวลางานทุกตัวที่คุณเคยใช้จะทำงานในลักษณะเดียวกัน รวมถึงการลองใหม่ เขตเวลา และการรับประกันว่าตัวกำหนดเวลางานจะยังคงทำงานได้แม้จะมีการรีสตาร์ท
👉💻 Terminal 3 — fire once, 10 seconds from now:
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
👉 ตอนนี้หยุดพิมพ์ เอามือลง ดูเทอร์มินัล 2
1 นาทีต่อมาโดยไม่มีใครแตะแป้นพิมพ์
[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.
นั่นคือคำตอบของขั้นตอนที่ 2 มีบางอย่างภายนอกเอเจนต์เรียกใช้ฟังก์ชันนี้ และสิ่งนั้นคือ sleep และคำขอ HTTP
👉💻 Ctrl-C ทั้งเมื่อคุณดูวิดีโอไปแล้ว 2-3 ครั้ง ให้ใช้ --every 30 ก่อนหากต้องการให้เล่นซ้ำ
ตอนนี้อ่านสิ่งที่เขียนไว้
"ฉันซื้อตั๋ว 2 ใบสำหรับ The Midnight Signal ในอัมสเตอร์ดัม โซน A ราคา 4200 บาท"
👉🔴 ตรวจสอบแผงสถานที่ แล้วคุณจะเห็นว่ามีการซื้อตั๋วแล้ว
ดังนั้นสิ่งที่ยังคงไม่ถูกต้อง
โดยซื้อบางอย่าง ดูว่าโมเดลต้องคาดเดาอะไรบ้าง ระบบเลือกอัมสเตอร์ดัมและส่วน A เนื่องจากเป็นสิ่งที่พบเป็นอันดับแรก และคุณบอกให้ตัวแทนซื้อโดยตรงโดยไม่ทราบว่าแซมไม่สามารถไปได้ในคืนวันธรรมดา เอเจนต์ที่ดำเนินการตามการคาดเดาตอนตี 3 แย่กว่าเอเจนต์ที่ไม่ทำอะไรเลย
การตื่นนอนเป็นเรื่องง่าย เอเจนต์ที่ตื่นขึ้นมาโดยไม่มีอะไรเลย ไม่ว่าจะเป็นประวัติ ค่ากำหนด หรือไม่มีใครฟัง ก็ยังไม่คุ้มค่าที่จะปลุก
4. การจัดการอายุการใช้งานของบริบท
ข้อมูลจะอยู่ในที่ต่างๆ ในช่วงเวลาที่ต่างกัน
เอเจนต์จะจัดเก็บข้อเท็จจริงไว้หลายที่ ได้แก่ ในการสนทนาแชท ในสถานะเซสชัน ในไฟล์ (อาร์ติแฟกต์) หรือในหน่วยความจำระยะยาวของผู้ใช้ โดยแต่ละสถานที่จะมีระยะเวลาที่แตกต่างกัน
หากคุณใส่ข้อมูลในตำแหน่งที่ไม่ถูกต้อง ให้ทำดังนี้
- ข้อความหายไปเร็วเกินไป: รายละเอียดสำคัญจะหายไปเมื่อแชทสิ้นสุดลง
- อยู่ได้นานเกินไป: ข้อเท็จจริงชั่วคราวจะปนเปื้อนหน่วยความจำระยะยาวและทำให้การสนทนาในอนาคตสับสน
คุณต้องจับคู่ข้อมูลแต่ละชิ้นกับที่เก็บข้อมูลและอายุการใช้งานที่ถูกต้อง
โหลดรหัส
👉🔴 กดรีเซ็ตสถานที่ในแผงสถานที่
👉💻 ในเทอร์มินัล 1 ให้เรียกใช้คำสั่งต่อไปนี้
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 4 --force
สิ่งที่มีการเปลี่ยนแปลงตั้งแต่ขั้นตอนสุดท้าย
ไฟล์ | สิ่งที่เปลี่ยนแปลง |
| เพิ่ม |
| อยู่เสมอ ร้านค้าความทรงจำของเราเอง |
| เครื่องมืออื่นๆ อีก 3 รายการในรายการ และคำสั่งจะบอกให้ตัวแทนใช้เครื่องมือเหล่านั้น |
👉💻 รีสตาร์ทเอเจนต์ในเทอร์มินัล 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"
ธง 2 รายการที่คุณยังไม่เคยใช้
ธง | คำอธิบาย |
| ADK จะเขียนข้อความ การเรียกใช้เครื่องมือ และการเปลี่ยนแปลงสถานะทั้งหมดลงในฐานข้อมูลเป็นแถว (ฐานข้อมูล SQLite) |
| ไฟล์ที่ Agent บันทึก (ไฟล์) |
หากไม่มีไฟล์เหล่านี้ ADK จะเก็บทุกอย่างไว้ในหน่วยความจำและทิ้งเมื่อคุณหยุดกระบวนการ การตั้งค่า 2 อย่างนี้จะทำให้ข้อมูลคงอยู่
ฐานข้อมูล sessions.db มีการสนทนาที่ผ่านมาซึ่งเริ่มต้นไว้ (two-days-ago) พร้อมเหตุการณ์ที่บันทึกไว้ 13 รายการ ดังนี้
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?
ไฟล์ความทรงจำจากการจอง 3 ครั้งที่ผ่านมา memory/userx.md ซึ่งคุณเปิดในเครื่องมือแก้ไขได้
- 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.
ด้วยเหตุนี้ แถวสีเหลืองในคืนวันธรรมดาจึงมีความสำคัญ การแสดงในคืนวันธรรมดามีราคาถูกกว่าที่นั่งละ $10 มีที่นั่งเหลือที่ดีกว่า และอยู่ด้านบนสุดของรายการ แต่ก็เป็นคำตอบที่ไม่ถูกต้องสำหรับบุคคลนี้ เหตุผลเดียวที่เอเจนต์ทราบได้คือไฟล์ที่เอเจนต์เขียนขึ้นหลังจากการจองครั้งล่าสุด ส่วนขั้นตอนที่ 4 และ 5 จะเป็นการตรวจสอบว่าข้อเท็จจริงนั้นยังคงอยู่หรือไม่
เครื่องมือ 3 อย่างที่เราเพิ่ม

note_companion บันทึกว่าใครมาและมีข้อจำกัดอะไรบ้าง
# 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}
เพิ่มเพราะ "แซมไม่ว่างในคืนวันธรรมดา" จะต้องยังคงเป็นจริงในสัปดาห์หน้า เขียนลงใน user:prefs เพื่อให้เกิดสิ่งนั้นขึ้น และดูส่วน user: ของชื่อทำงาน โปรดกลับไปดูที่ด้านล่าง
recall
และ
remember อ่านและเขียนไฟล์ข้อความ memory/userx.md พักเรื่องนั้นไว้ก่อน
👉 ตอนนี้มาดู 2 ที่ที่เอเจนต์จัดเก็บบริบทของตัวเอง
สถานที่ | สิ่งที่เก็บไว้ |
สถานะ | หน่วยความจำของเซสชันนี้เป็นคู่คีย์/ค่า ซึ่งคุณจะอยู่ในขั้นที่ 4 |
อาร์ติแฟกต์ | ไฟล์ที่ตัวแทนบันทึกไว้ ซึ่งจะไม่ปรากฏในพรอมต์ |
ดูสิ่งที่เปลี่ยนแปลงใน
get_seatmap
ก่อนหน้านี้จะแสดงผลแผนที่ที่นั่งทั้งหมด ตอนนี้ระบบจะบันทึกแผนผังที่นั่งลงในไฟล์และแสดงข้อมูลสรุปสั้นๆ ดังนี้
# 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
เหตุผลที่การเปลี่ยนแปลงนี้มีความสำคัญ
ระบบจะต่อท้ายเพย์โหลดการตอบกลับของเครื่องมือทุกรายการในประวัติการสนทนาและส่งกลับไปยังโมเดลในทุกๆ รอบต่อๆ ไป การส่งคืนออบเจ็กต์ JSON ขนาดใหญ่ (เช่น แผนที่ที่นั่งขนาด 6 KB) จะเพิ่มการใช้โทเค็นในทุกๆ เทิร์นต่อๆ ไป
ให้บันทึกเป็นไฟล์แทน แล้วจ่ายเงินสำหรับชื่อไฟล์เดียว ปล่อยให้ตัวแทนขอรายละเอียดกลับมาพร้อมกับ load_artifact หากจำเป็น และคาดการณ์ว่าส่วนใหญ่แล้วตัวแทนจะไม่ขอรายละเอียด
ตำแหน่งของไฟล์: บริการอาร์ติแฟกต์
ดูบรรทัดใน get_seatmap อีกครั้งใน agent/concert/tools.py
await tool_context.save_artifact(f"seatmap_{event_id}.json", part)
tool_context มาจาก ADK ซึ่งส่งไปยังเครื่องมือของคุณเป็นอาร์กิวเมนต์ และ save_artifact เป็นเมธอดใน ADK บรรทัดนั้นคือจุดที่เครื่องมือของคุณส่งไบต์ไปยัง ADK
จากนั้น ADK จะเขียนโดยใช้บริการอาร์ติแฟกต์ที่ใช้เริ่มต้น คุณเลือกรายการนี้ด้วยการแจ้งว่าไม่เหมาะสม
--artifact_service_uri="file://$WORKSHOP/artifacts"
ADK มีตำแหน่ง 3 แห่งสำหรับจัดเก็บข้อมูล ได้แก่
URI | ตำแหน่งที่ไบต์ไป |
| RAM จะหายไปเมื่อคุณหยุดกระบวนการ นี่คือค่าเริ่มต้นหากคุณไม่ส่งแฟล็ก |
| โฟลเดอร์ในดิสก์ สิ่งที่คุณใช้อยู่ในขณะนี้ |
| Cloud Storage สิ่งที่ Agent ที่ติดตั้งใช้งานใช้ในขั้นตอนสุดท้าย |
👉💻 เลือกไฟล์โดยทำดังนี้
ls -R ~/monstertix/artifacts
หากต้องการย้ายจากแล็ปท็อปไปยัง Cloud Storage ให้เปลี่ยน file:// เป็น gs:// (เราจะเห็นสิ่งนี้ในขั้นตอนที่ 10)
ร้านค้าความทรงจำของเรา

ADK มีอินเทอร์เฟซสำหรับหน่วยความจำระยะยาวที่เรียกว่า BaseMemoryService โดยมี 2 วิธีดังนี้
# 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 มีการใช้งาน 3 แบบดังนี้
ระดับ | URI | สิ่งที่ต้องมี |
|
| ไม่มี สูญเสียทุกอย่างเมื่อคุณหยุดกระบวนการ |
|
| คลังข้อมูล RAG ของ Vertex AI |
|
| ทรัพยากร Agent Engine |
เราจะเขียนสคริปต์ของเราเองสำหรับเวิร์กช็อปนี้ ตัด InMemoryMemoryService ออกไป ซึ่งจะลืมทุกอย่างเมื่อคุณหยุดกระบวนการ และตัดอีก 2 รายการออกไป ซึ่งต้องใช้ทรัพยากรระบบคลาวด์ที่ใช้เวลาหลายนาทีในการสร้าง เลือกไฟล์แทน เนื่องจากคุณสามารถเริ่มต้นใช้งานล่วงหน้าและอ่านในโปรแกรมแก้ไขได้ ดังนั้นให้ใช้ memory.py เป็นการติดตั้งใช้งานของคุณเอง เราจะจัดเก็บทุกอย่างไว้ในไฟล์มาร์กดาวน์
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 และ MemoryEntry คือประเภทของ ADK และ Memory Bank จะแสดงออบเจ็กต์เดียวกัน โดยมีบริการที่มีการจัดการอยู่เบื้องหลัง ของเรามีไฟล์
เลือกไฟล์ด้วยเหตุผลเดียวคือคุณสามารถเปิดและอ่านไฟล์ได้ หน่วยความจำระยะยาวทั้งหมดของเอเจนต์คือข้อความในหน้าเว็บ
แล้วคุณจะชี้ไปที่ไหน
สรุปในการเวิร์กช็อปนี้ เราเลือก 2 ใน 3 บริการในบรรทัดคำสั่ง ส่วนที่ 3 เราได้ติดตั้งใช้งานเองด้วยเหตุผลข้างต้น
บริการ | ตำแหน่งที่คุณตั้งค่า | เวิร์กช็อปนี้ |
เซสชัน |
|
|
อาร์ติแฟกต์ |
|
|
ความทรงจำ | ที่ใดก็ตามในบรรทัดคำสั่ง |
|
ดังนั้น memory.py จึงสร้างบริการด้วยตนเองจากตัวแปรสภาพแวดล้อม
# 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 จะส่งออกทั้ง 2 อย่าง รับMEMORY_DIRชุดที่ Agent อ่านหน่วยความจำและMEMORY_USERกำหนดไฟล์
ไปดูว่าสิ่งต่างๆ อยู่ที่ไหน
ตอนนี้คุณมี 4 ที่ที่สามารถเก็บข้อเท็จจริงได้ ดูทั้ง 4 อย่าง
ก่อนอื่นคือเซสชันของคุณเอง
👉 คลิกเซสชันใหม่ในแถบด้านบน
👉✨ พูด 3 อย่างติดต่อกัน
Sam can't do weeknights, by the way.

Show me the seat map for the Amsterdam Saturday show.

Actually, hold that thought — what else is on in Amsterdam?
ข้อความที่ 3 นั้นสำคัญ ซึ่งจะสิ้นสุดการเลี้ยวที่อ่านแผนที่ที่นั่ง และการสิ้นสุดการเลี้ยวจะทำให้ temp: หายไป
โดยมีสิ่งต่างๆ 3 อย่างที่ถูกนำไปเก็บไว้ใน 3 ที่ที่แตกต่างกัน ดังนี้
สิ่งที่จัดเก็บ | เครื่องมือใดที่ใช้ | ตำแหน่งที่ไป | |
1 | แซมไม่สะดวกในคืนวันธรรมดา |
|
|
2 | แผนที่ที่นั่งทั้งหมด 6 KB |
| บริการอาร์ติแฟกต์ |
3 | สรุปแผนผังที่นั่ง 1 บรรทัด |
| ไม่มี |
👉 ตอนนี้ให้ตรวจสอบ แท็บสถานะและแท็บอาร์ติแฟกต์จะอยู่ทางด้านซ้ายของ adk web


ในแท็บรัฐ คุณจะเห็นว่ามีการละเว้น temp:seatmap คีย์สถานะที่มีคำนำหน้า temp: จะมีอยู่เฉพาะในระหว่างการเรียกใช้ที่ใช้งานอยู่ และ ADK จะนำออกโดยอัตโนมัติก่อนที่จะบันทึกสถานะเซสชันลงในฐานข้อมูล
แล้วตามด้วยการสนทนาที่เก่ากว่า
👉 เปลี่ยนไปใช้ two-days-ago ในเมนูแบบเลื่อนลงของเซสชันที่ด้านบน
เซสชันนี้มีประวัติการสนทนาที่ป้อนข้อมูลไว้ล่วงหน้าในฐานข้อมูล

👉 ดูสตรีมเหตุการณ์ (คอลัมน์กลาง) โปรดสังเกตว่าระบบได้แทนที่การเลี้ยวที่เก่ากว่าด้วยบล็อกเหตุการณ์ที่สรุปแล้ว

👉 ดูแท็บสถานะ คีย์ 3 ประเภทที่อยู่ด้วยกันมีดังนี้
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
คำนำหน้า user: (เช่น user:prefs) จะกำหนดขอบเขตสถานะในเซสชันทั้งหมดสำหรับผู้ใช้ที่ระบุ ในทางตรงกันข้าม คีย์ที่ไม่มีคำนำหน้า (
target_event_id
,
party_size
) แสดงสถานะเซสชันธรรมดา ซึ่งมีขอบเขตเฉพาะเซสชันที่ใช้งานอยู่และจะถูกลบเมื่อเซสชันสิ้นสุดลง
👉 ตอนนี้ให้เปรียบเทียบแท็บรัฐ 2 แท็บ แม้ว่า user:prefs และอาร์ติแฟกต์ที่จัดเก็บจะยังคงอยู่ แต่ temp:seatmap จะหายไปจากทั้ง 2 เซสชัน คีย์สถานะที่มีคำนำหน้า temp: จะเป็นคีย์ชั่วคราวโดยเฉพาะและจะไม่ได้รับการบันทึกลงในฐานข้อมูล
จากนั้นไฟล์
👉💻 เปิดในเครื่องมือแก้ไข
cloudshell edit ~/monstertix/memory/userx.md
การจอง 3 รายการตั้งแต่เดือนมกราคม ตัวแทนจะอ่านเมื่อเรียกใช้ recall() และจะยังคงอยู่แม้ว่าเราจะสูญเสียข้อมูลเซสชันไปก็ตาม
สถานที่ทั้ง 4 แห่ง
ตำแหน่งที่อยู่ | จะรอดจากข้อความถัดไปไหม | ยังคงอยู่หลังการรีสตาร์ทไหม | ยังคงอยู่หลังจากการลบเซสชันใช่ไหม |
| no | ไม่ | ไม่ |
| ใช่ | ใช่ | ไม่ |
| ใช่ | ใช่ค่ะ | yes |
| ใช่ค่ะ | ใช่ | ใช่ และจะยังคงอยู่แม้จะลบฐานข้อมูล |
อาร์ติแฟกต์ | ใช่ | ใช่ | ขึ้นอยู่กับบริการ |
โปรดอ่านตารางนั้นในแง่ของคอนเทนเนอร์ ไม่ใช่สิ่งที่อยู่ภายในmemory/userx.mdจะอยู่รอดได้ทุกสถานการณ์ และจะมีเพียงสิ่งที่ remember()เลือกที่จะใส่ไว้เท่านั้น ไม่มีอะไรเขียนการสนทนาปัจจุบันของคุณลงในนั้น ดังนั้นการที่ไฟล์มีอายุการใช้งานนานกว่าเซสชันจึงไม่เหมือนกับการที่เอเจนต์จดจำสิ่งที่คุณเพิ่งพูดถึง และขั้นตอนที่ 5 คือจุดที่ความแตกต่างนี้เกิดขึ้น

รัฐใด
ให้คิดว่าสถานะเป็นพจนานุกรมที่เดินทางไปกับเซสชัน อ่านและเขียนจากเครื่องมือผ่าน tool_context.state แล้วให้ ADK บันทึกไว้ข้างการสนทนา
ADK กำหนดความหมายของคำนำหน้า คุณไม่ได้กำหนดไว้ที่ใดเลย ADK จะอ่านส่วนแรกของชื่อคีย์และใช้เพื่อกำหนดตำแหน่งของค่าและระยะเวลาที่ค่าจะอยู่
คีย์ที่เรียกว่า | ADK จะจัดเก็บ | ระยะเวลา |
| ด้วยเซสชัน | จนกว่าจะมีการลบเซสชัน |
| ต่อผู้ใช้ | ในทุกเซสชันที่ผู้ใช้มี |
| ต่อแอป | ในผู้ใช้ทุกคน |
| ไม่มีที่ใดเลย | การเรียกใช้ 1 ครั้ง |
การเรียกใช้คือวงจรคำขอ-การตอบกลับเดียว ซึ่งรวมถึงการให้เหตุผลของโมเดล การดำเนินการเครื่องมือ และคำตอบสุดท้ายทั้งหมด
เมื่อการเรียกใช้สิ้นสุดลง ADK จะเขียนสถานะใหม่ลงในฐานข้อมูลและลบคีย์ temp: ทุกรายการระหว่างทาง
สิ่งหนึ่งที่ยังไม่ได้รับการอธิบาย
ใน two-days-ago คุณเห็นข้อมูลสรุปในตำแหน่งที่เคยเป็นเทิร์นแรกๆ ของผู้เล่น ADK เขียนสรุปโดยอัตโนมัติเมื่อการสนทนายาวพอ
ข้อเท็จจริงทุกอย่างมีอายุการดู
5. การลดทอนและการบีบอัดบริบท
สรุปโดยไม่ต้องลงรายละเอียด
เมื่อการสนทนายาวเกินไป ระบบจะสรุปข้อความเก่าๆ เพื่อประหยัดพื้นที่และโทเค็น
สรุปมีประโยชน์ในการจับใจความสำคัญทั่วไป ซึ่งช่วยประหยัดการใช้โทเค็นได้

👉🔴 กดรีเซ็ตสถานที่ในแผงสถานที่ ทุกขั้นตอนเริ่มต้นจากโลกเดียวกัน นั่นคือ 8 รายการ ทุกที่นั่งว่าง และนาฬิกาที่ 1x
👉💻 ไปที่โค้ดของขั้นตอนนี้
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 5 --force
สิ่งที่มีการเปลี่ยนแปลงตั้งแต่ขั้นตอนสุดท้าย
ไฟล์ | สิ่งที่เปลี่ยนแปลง |
| โดยจะห่อหุ้มเอเจนต์ไว้ใน |
อื่นๆ ทั้งหมด | ไม่มีการเปลี่ยนแปลง |
agent.py ไม่ได้ลงท้ายด้วย root_agent อีกต่อไป โดยมีข้อความดังนี้
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
),
)
โปรดทราบว่า adk web จะค้นหาตัวแปรที่ชื่อ app ก่อน แล้วจึงเปลี่ยนไปใช้ root_agent ห่อหุ้ม Agent ใน App เมื่อใดก็ตามที่คุณต้องการสิ่งที่ใช้กับทั้งแอปพลิเคชันแทนที่จะใช้กับ Agent รายเดียว เช่น การบีบอัดที่นี่ และความสามารถในการกลับมาทำงานต่อในขั้นตอนถัดไป
compaction_interval=3 หมายความว่า ADK จะสรุปหลังจากผ่านไปทุกๆ 3 รอบ หนึ่งรอบคือการที่คุณพูดอะไรบางอย่างและ Agent ตอบกลับจนจบ ไม่ว่า Agent จะเรียกใช้เครื่องมือไปกี่ครั้งก็ตาม 3 เป็นจำนวนที่น้อย จึงสามารถยิงได้ในระหว่างเวิร์กช็อป แอปจริงจะใช้ตัวเลขที่มากกว่านี้มาก
overlap_size=1 หมายความว่าแต่ละข้อมูลสรุปจะเริ่มก่อนเวลาที่จำเป็น 1 รอบ
ADK จะสรุปเทิร์นที่ 1, 2 และ 3 และอีก 3 รอบต่อมาก็จะสรุปชุดถัดไป เมื่อใช้ overlap_size=1 สรุปวินาทีที่ 2 จะเริ่มที่เทิร์น 3 อีกครั้ง ไม่ใช่เทิร์น 4

ระบบจะสรุปการเลี้ยวที่ 3 สองครั้ง เราจงใจทำเช่นนี้ หากคุณถามคำถามในเทิร์นที่ 3 และตัวแทนตอบในเทิร์นที่ 4 การแยกที่ชัดเจนจะทำให้คำถามอยู่ในสรุปหนึ่งและคำตอบอยู่ในอีกสรุปหนึ่ง ซึ่งทำให้ทั้ง 2 สรุปสับสน การพูดซ้ำเป็นการป้องกันไม่ให้ความคิดขาดตอนได้ดี
ตั้งค่าทั้ง 2 ช่อง ADK ไม่มีค่าเริ่มต้นสำหรับทั้ง 2 อย่าง
การบีบอัดและบริการหน่วยความจำของคุณจะไม่พบกัน
ควรระบุให้ชัดเจนเนื่องจากคำทั้ง 2 คำฟังดูคล้ายกัน แต่ระบบไม่ได้เชื่อมโยงกัน
EventsCompactionConfig ตั้งอยู่บน App ใน agent.py ข้าง root_agent เป็นพร็อพเพอร์ตี้ของแอปพลิเคชัน ไม่ใช่ของบริการใดๆ
# agent/concert/agent.py
app = App(
name="concert",
root_agent=root_agent,
events_compaction_config=EventsCompactionConfig(
compaction_interval=3,
overlap_size=1,
),
)
สรุปจะเขียนการสนทนาใหม่ แต่จะไม่เขียนไฟล์ใหม่
นั่นคือเหตุผลทั้งหมดที่remember()มีอยู่เพื่อเป็นคำที่ตั้งใจใช้ ข้อความที่เหลือในข้อความถอดเสียงจะอ่านเป็นคำที่เขียนโดยบุคคลอื่นในที่สุด ส่วนข้อความที่เขียนลงในไฟล์จะอ่านตรงตามที่จัดเก็บไว้ในวันพรุ่งนี้และสัปดาห์หน้า และในตอนท้ายของขั้นตอนนี้ คุณจะเห็นความแตกต่างที่เกิดขึ้น
ตำแหน่งที่เก็บข้อมูลสรุป
ใน sessions.db ในตาราง events เอเจนต์จะเขียนบันทึกการบีบอัดไปยังแถวเหตุการณ์ โดยมีการประทับเวลาเช่นเดียวกับเหตุการณ์การเลี้ยวอื่นๆ
เหตุการณ์ปกติจะมีฟิลด์ content และข้อมูลสรุปจะอยู่ใน actions.compaction
👉💻 ดูในฐานข้อมูลของคุณเอง
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%';"
รูปร่างของเหตุการณ์มีลักษณะดังนี้
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
สตรีมเหตุการณ์จะดึงเหตุการณ์แต่ละรายการจากเนื้อหาของเหตุการณ์นั้น สรุปในฐานข้อมูลและส่งไปยังโมเดลในทุกๆ รอบ
ตัวแทนที่ 2
budget_split เป็นเอเจนต์ที่แยกต่างหากโดยสมบูรณ์ ไม่ใช่เครื่องมือ
# 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)],
)
เหตุใดจึงต้องใช้เอเจนต์แทนฟังก์ชันธรรมดา เนื่องจากงานต้องใช้โมเดล อ่าน "ส่วนใดในนี้ที่เหมาะกับงบประมาณ 2000 บาทสำหรับ 4 คน และมีค่าใช้จ่ายทั้งหมดเท่าใด" เป็นการคำนวณที่ซ่อนอยู่ในการตัดสิน โดยมีคำตอบที่บุคคลต้องอ่านได้ ให้ฟังก์ชัน Python คูณ และอย่าขอให้ฟังก์ชันตัดสินว่าตัวเลือกใดควรค่าแก่การกล่าวถึง
แล้วทำไมไม่ให้เอเจนต์หลักเป็นผู้ดำเนินการ ดูบรรทัดถัดไป include_contents="none" หมายความว่าระบบจะไม่ส่งต่อการสนทนาให้ตัวแทนรายนี้ ไม่มีแชทเป็นกลุ่ม ไม่มีค่ากำหนด ไม่มีวันที่ทัวร์ 40 วัน โดยจะรับคำขอและไม่มีการดำเนินการอื่น โดยแฟล็กดังกล่าวมีเฉพาะใน Agent ซึ่งเป็นเหตุผลที่แท้จริงว่าทำไมจึงเป็น Agent แทนที่จะเป็นฟังก์ชัน

ใช้พรอมต์สั้นๆ: ถูกกว่า เร็วกว่า และเบี่ยงเบนได้ยากกว่ามาก อย่าคาดหวังว่าการคำนวณจะรับรู้สิ่งที่พูดไปเมื่อ 20 รอบที่แล้ว เพราะการคำนวณไม่เคยเห็นสิ่งนั้น
เหตุผล
AgentTool
? ห่อหุ้ม Agent ด้วยเครื่องมือนี้เพื่อให้ Agent หลักเรียกใช้ได้เหมือนเครื่องมืออื่นๆ จากฝั่งโมเดล budget_split จะอ่านเหมือนกับ search_events
และ
output_key="budget_plan" จะใส่คำตอบลงในสถานะเซสชันภายใต้ชื่อนั้นแทนที่จะปล่อยให้คำตอบลอยอยู่ในการสนทนา ดูว่าทำไมเรื่องนี้ถึงสำคัญ
ดำเนินการ
👉💻 ในเทอร์มินัล 1 ให้กด Ctrl-C แล้วรีสตาร์ทด้วย Flag เดิม
cd ~/monstertix
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
👉 เริ่มเซสชันใหม่และสนทนาจริง 5 หรือ 6 รอบ ตัวอย่างที่ใช้ได้
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?
👉 เปิดแท็บสถานะ
ค้นหา budget_plan ที่นั่น ซึ่งมีคำตอบทั้งหมดของเอเจนต์เลขคณิต

👉✨ ตอนนี้ให้เปลี่ยนตัวเลขในวิดเจ็ตโดยทำดังนี้
what if 10 people come and I can only do $120 each?
👉 ดูที่ budget_plan อีกครั้ง ระบบได้เขียนทับ ไม่ได้ต่อท้าย เนื่องจาก output_key มีเวอร์ชันล่าสุด

สิ่งที่คุณดูไม่ได้: สรุป
ในระหว่างการเลี้ยวเหล่านั้น ADK ได้สรุปการเลี้ยวครั้งก่อนๆ
👉💻 ในเทอร์มินัลใหม่ ให้เรียกใช้คำสั่งต่อไปนี้เพื่ออ่านโดยตรง
cd ~/monstertix
. ./set_env.sh
./show-compaction.sh
อ่านสิ่งที่เขียน
นี่คือตัวอย่างจริงที่ตัดทอนแล้ว
── 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`
โดยยังคงราคา ส่วน เมือง และค่ากำหนดไว้ และระบุอย่างถูกต้องว่ายังไม่มีการตัดสินใจใดๆ
ซึ่งจะส่งผลให้เกิด 3 สิ่งต่อไปนี้
เป็นการถอดความ ไม่มีข้อความใดๆ ที่นี่เป็นสิ่งที่ใครพูด ซึ่งเป็นบันทึกของสิ่งที่พูด โดยสร้างขึ้นจากกระบวนการประเภทเดียวกันกับที่สร้างสิ่งอื่นๆ ที่โมเดลอาจเข้าใจผิดเป็นครั้งคราว
หมายเหตุ: ระบบจะจัดเก็บสตริงสรุปไว้ในบันทึกเหตุการณ์เซสชันภายใน และจะไม่แสดงเป็นแท็บ UI แยกต่างหากโดยตรง
ทุกๆ 3 รอบการสนทนาในการสนทนาที่ตอนนี้ประกอบด้วยข้อมูลสรุปก่อนหน้าบางส่วน
ลองจองโดยไม่ต้องพูดซ้ำ
ในเซสชันใหม่ อย่าเริ่มเซสชันใหม่ สรุปที่คุณเพิ่งอ่านจะอยู่ในเหตุการณ์ของเซสชันนี้ และนั่นเป็นเหตุผลเดียวที่ตัวแทนยังคงทราบข้อมูลเกี่ยวกับอัมสเตอร์ดัม
👉✨ อย่าป้อนข้อมูลใหม่
Book us something.
ดูตัวแทนถามว่าต้องการดูรายการไหน ส่วนไหน และมีกี่คน ทำเครื่องหมายว่าถูกต้อง: ข้อมูลสรุประบุว่าผู้ใช้ไม่เคยยืนยันอะไรเลย ดังนั้นเอเจนต์จึงถาม ให้เครดิตการบีบอัด
แม้ว่าการบีบอัดจะสำเร็จ แต่การสรุปการสนทนาก่อนหน้าอาจทำให้เกิดความคลุมเครือซึ่งทำให้ Agent ต้องยืนยันข้อจำกัดกับผู้ใช้อีกครั้ง
👉✨ ตอบคำถามและอนุญาตให้ดำเนินการจอง
Amsterdam Saturday, section C, 8 people
โดยจะอ่านคำสั่งอีกครั้งก่อนที่จะใช้จ่าย
"คุณต้องการจองตั๋ว 8 ใบในส่วน C สำหรับ The Midnight Signal ในอัมสเตอร์ดัมในวันเสาร์ที่ 14 พฤศจิกายน ต้นทุนทั้งหมดจะอยู่ที่ $760 ฉันควรซื้อตั๋วเหล่านี้เลยไหม"
👉✨ ยืนยัน
yes
ตอนนี้ระบบจะแจ้งให้คุณทราบว่าการดำเนินการสำเร็จแล้ว
"การซื้อตั๋ว 8 ใบในส่วน C สำหรับ The Midnight Signal ในอัมสเตอร์ดัมในวันเสาร์ที่ 14 พฤศจิกายน รวมเป็นเงิน 7600 บาทเสร็จสมบูรณ์แล้ว รหัสคำสั่งซื้อของคุณคือ ord_227877fe"
ไปตรวจสอบว่ามีการอัปเดตหรือไม่
👉 เปลี่ยนไปใช้แผงสถานที่ อย่าเพิ่งเชื่อคำพูดของตัวแทน

ในแผง | สิ่งที่คุณควรเห็น |
ตั๋วที่ซื้อ |
|
คำสั่งซื้อที่อยู่ด้านล่าง |
|
The tour, อัมสเตอร์ดัม วันเสาร์ | ส่วน C ลดลง 8 |
กิจกรรม |
|
ตรวจสอบแผงเพื่อดูการขายตั๋ว
เหตุผลที่ user:prefs เป็นตัวเลือกที่เหมาะสม
โปรดจำnote_companionการเขียนข้อจำกัดของแซมในuser:prefsขั้นตอนสุดท้าย และข้อโต้แย้งที่ชัดเจน นั่นคือ ทำไมต้องใช้เครื่องมือในเมื่อ Agent จำได้อยู่แล้ว
ด้วยเหตุผลต่อไปนี้ อย่าให้สถานะเข้ามาเกี่ยวข้องกับการสนทนาและอย่าให้สรุปแตะต้องสถานะ ตรวจสอบ user:prefs และ budget_plan กับวิธีที่ใส่ข้อมูล และดูว่าข้อมูลยังคงเหมือนเดิม จากนั้นตรวจสอบข้อความถอดเสียง
การสรุปจะคงรูปร่างทั่วไปไว้และตัดรายละเอียดออก จัดเก็บสิ่งที่คุณไม่อยากให้สูญหายไว้ที่อื่นนอกเหนือจากการสนทนา
6. การกลับมาทำงานต่อ
รอ 40 นาทีโดยไม่ต้องออนไลน์
เมื่อตัวแทนต้องรอในคิวการแจ้งปัญหา 40 นาที การทำให้กระบวนการ Python ทำงานอยู่และรอต่อไปนั้นเป็นเรื่องที่ทำได้ยากและมีค่าใช้จ่ายสูง
วิธีจัดการการรอสายนานอย่างเหมาะสม
- บันทึกสถานะลงในฐานข้อมูล: จัดเก็บตำแหน่งที่ Agent หยุดชั่วคราวเพื่อให้กระบวนการปิดได้อย่างปลอดภัย
- ปลุกตามต้องการ: เมื่อการรอสิ้นสุดลง สัญญาณภายนอกจะรีสตาร์ทตัวแทนและดำเนินการต่อจากจุดที่หยุดไว้
👉🔴 กดรีเซ็ตสถานที่ในแผงสถานที่ ทุกขั้นตอนเริ่มต้นจากโลกเดียวกัน นั่นคือ 8 รายการ ทุกที่นั่งว่าง และนาฬิกาที่ 1x
👉💻 ไปที่โค้ดของขั้นตอนนี้
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 6 --force
ดำเนินการ
สิ่งที่มีการเปลี่ยนแปลงตั้งแต่ขั้นตอนสุดท้าย
ไฟล์ | สิ่งที่เปลี่ยนแปลง |
| เพิ่ม |
|
|
join_queue จะกลับมาพร้อมตั๋วทันทีแทนที่จะบล็อก และ ResumabilityConfig จะช่วยให้การแข่งขันหยุดชั่วคราวและกลับมาแข่งขันต่อได้

👉💻 เทอร์มินัล 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"
👉✨ ขอให้ตัวแทนซื้อ
Get us two tickets to the Amsterdam show.
โดยจะเรียกใช้ recall จากนั้น search_events แล้ว join_queue และตอบกลับด้วยข้อความที่คล้ายกับข้อความต่อไปนี้
You are in the queue for The Midnight Signal at Ziggo Dome, Amsterdam, on Saturday, November 14. Your position is 14203.
คุณจะเห็นว่าระบบเลือกวันเสาร์โดยไม่ต้องถาม มีรายการในอัมสเตอร์ดัม 2 รายการ วันที่ 17 เป็นวันอังคาร และไฟล์บันทึกจากขั้นตอนสุดท้ายระบุว่าแซมไม่ว่างในคืนวันทำการ นั่นคือข้อเท็จจริงจากการจองครั้งก่อนที่ตัดสินคำถามในการจองครั้งนี้
โปรดสังเกตด้วยว่าระบบได้เพิ่มคำขอของคุณลงในคิวก่อนที่จะดูที่นั่งหรือราคาใดราคาหนึ่ง และไม่ได้พูดถึงที่นั่งหรือราคาเลย นั่นคือกฎของสถานที่จัดงาน คุณจะซื้อตั๋วได้จนกว่าจะถึงคิว ดังนั้นสิ่งเดียวที่ควรมีก่อนคือการได้เข้าแถว คุณจะนั่งที่ไหนเป็นคำถามที่ต้องตอบในอีก 40 นาที
👉 ดูที่แผงควบคุม แบนเนอร์ระบุว่า

Agent is waiting in line — #14,203
About 2400s at 1×. Safe to kill the agent right now — it will still be here.
คิวจะยาว 40 นาทีจริงๆ และจะยังคงอยู่เช่นนั้นในขณะที่คุณทำงาน
👉💻 หยุดการทำงานของ Agent:
Ctrl-C
👉💻 เริ่มอีกครั้งในเทอร์มินัล 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"
👉✨ ในเซสชันเดียวกัน ให้ถาม Agent ว่าคุณอยู่ลำดับที่เท่าไร
Where am I in line?
You are currently at position 12982 in the queue. It is not your turn yet.

เปรียบเทียบตั๋วและดูว่าตำแหน่งลดลงเรื่อยๆ ขณะที่ไม่มีอะไรทำงานอยู่ โปรดสังเกตว่า Agent ไม่ทราบว่ามีการหยุดการทำงาน โดยจะเรียกใช้ check_queue พร้อมกับตั๋วที่ได้รับก่อนการรีสตาร์ท เนื่องจากตั๋วดังกล่าวอยู่ในดิสก์
👉💻 ดูสาเหตุ
cd ~/monstertix
sqlite3 sessions.db "select name from pragma_table_info('events');"
id
app_name
user_id
session_id
invocation_id
timestamp
event_data
โปรดทราบว่าทุกๆ เทิร์นของการสนทนาแต่ละครั้งคือ 1 แถว และเทิร์นนั้นเอง ผู้พูด เครื่องมือที่เรียกใช้ สิ่งที่ส่งกลับมา และหมายเหตุคือ JSON ภายใน event_data
👉💻 ค้นหาบัตรคิวของคุณได้ที่นี่
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
เมื่อ join_queue กลับมา ระบบจะบันทึกตั๋วลงในฐานข้อมูลโดยตรง เมื่อรีสตาร์ท ADK จะโหลดเทิร์นก่อนหน้าและผลลัพธ์ของเครื่องมือจากดิสก์อีกครั้ง ซึ่งจะช่วยให้เอเจนต์ดำเนินการต่อจากที่ค้างไว้ได้
และสังเกตว่าวิดีโอใดไม่ได้เริ่มเล่นใหม่ Agent จะกลับมาทำงานต่อโดยตรงแทนที่จะเล่นเวิร์กโฟลว์ซ้ำตั้งแต่ต้น เนื่องจาก LongRunningFunctionTool และ ResumabilityConfig จะบันทึกสถานะการดำเนินการลงในดิสก์ ADK จึงทราบว่า Agent กำลังรอคิวอยู่แล้ว ซึ่งจะช่วยป้องกันไม่ให้ Agent ค้นหาวันที่ทัวร์อีกครั้งหรือขอตั๋วซ้ำ

เหตุใดจึงควรใช้ join_queue และไม่ควรใช้ check_queue
ทั้ง 2 ฟังก์ชันจะแสดงผลเป็นมิลลิวินาที ความแตกต่างคือการพิจารณาว่างานพื้นฐานเสร็จสมบูรณ์หรือไม่เมื่อฟังก์ชันแสดงผล
ค่าที่ส่งคืน | งานเสร็จแล้วหรือยัง | |
| คิวตั๋วและตำแหน่งเริ่มต้น | ไม่ การประมวลผลจะดำเนินการต่อแบบไม่พร้อมกันในระบบภายนอก |
| สถานะคิวปัจจุบัน | ได้ แสดงคำตอบแบบทันที ณ จุดเวลาหนึ่งๆ |
LongRunningFunctionTool ออกแบบมาสำหรับการดำเนินการที่ส่งคืนแฮนเดิลไปยังงานที่รอดำเนินการ การเปลี่ยนjoin_queueช่วยให้ตัวแทนระงับการดำเนินการได้ในขณะที่คิวภายนอกดำเนินการต่อ
ในทางตรงกันข้าม check_queue จะซิงโครนัสและสมบูรณ์ การห่อหุ้มจะทำให้ตัวแทนระงับการดำเนินการอย่างไม่ถูกต้องทุกครั้งที่ตรวจสอบสถานะ แม้ว่าตัวแทนจะไปถึงคิวแรกและต้องทำการซื้อให้เสร็จสมบูรณ์ทันทีก็ตาม

สาเหตุที่ทำให้ได้ผล
องค์ประกอบ 3 อย่างที่ช่วยให้ระงับและกู้คืนการดำเนินการได้มีดังนี้
# 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: ทำเครื่องมือเป็นแบบไม่บล็อก โดยจะแสดงแฮนเดิลที่ใช้งานอยู่เพื่อให้ตัวแทนรอได้โดยไม่ต้องหยุดเธรดResumabilityConfig: สั่งให้รันไทม์หยุดวงจรการดำเนินการชั่วคราวในงานที่ยังไม่เสร็จสมบูรณ์ และดำเนินการต่อจากสถานะที่บันทึกไว้ล่าสุดSession Storage URI (--session_service_uri): กำหนดให้การคงสถานะไปยังที่เก็บข้อมูลภายนอก (เช่น SQLite, Cloud SQL) แทนที่จะเป็นหน่วยความจำของกระบวนการ เพื่อป้องกันการสูญเสียข้อมูลเมื่อรีสตาร์ท
7. การดำเนินการกับข้อมูลที่ไม่มีอัปเดต
ข้อมูลจะล้าสมัยขณะที่คุณรอ
ประวัติการสนทนาที่บันทึกไว้ไม่ใช่สถานะแบบเรียลไทม์ ในช่วงเวลารอสายนาน ข้อมูลภายนอก เช่น ความพร้อมจำหน่ายตั๋วหรือราคา อาจเปลี่ยนแปลงโดยที่ตัวแทนไม่ทราบ
Agent ที่ไม่มีข้อมูลจะหยุดชั่วคราวเพื่อขอคำชี้แจง แต่ Agent ที่มีข้อมูลล้าสมัยจะดำเนินการตามสมมติฐานที่ล้าสมัยทันที ข้อมูลที่คำนึงถึงเวลาต้องได้รับการยืนยันอีกครั้งทันทีก่อนที่จะดำเนินการที่สำคัญ เพื่อป้องกันการดำเนินการที่ไม่ถูกต้อง
อย่าโหลดรหัสใหม่ในตอนนี้ อยู่กับเอเจนต์ของขั้นตอนก่อนหน้า (ซื้อตั๋วได้ แต่ปัจจุบันไม่มีการป้องกันความเก่าหรือการดำเนินการซ้ำ)
👉🔴 กดรีเซ็ตสถานที่ แล้วเริ่มเซสชันใหม่ใน adk web โดยกดปุ่ม + เซสชันใหม่เหนือแชท
เริ่มเซสชันใหม่เพื่อล้างตั๋วในคิวและสถานะการซื้อที่รอดำเนินการก่อนหน้า ขั้นตอนนี้ต้องมีบริบทการดำเนินการที่สะอาดเพื่อแสดงให้เห็นว่ามีการจัดการความล้าสมัยอย่างเหมาะสม
สแนปชอตอยู่
👉✨ ขอให้เล่นรายการเดิมอีกครั้ง
Get us two tickets to the Amsterdam show.
👉✨ ตอนนี้ขณะที่รอสาย ให้ถามเกี่ยวกับที่นั่งดังนี้
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.
👉🔴 กดขายที่นั่งดีๆ ส่วน A ลดลงเหลือ 0
👉🔴 กดข้ามการรอสายเพื่อส่งตัวแทนไปที่คิวแรก

👉✨ บอกให้ซื้อ
Buy the two section A seats.
โดยจะดำเนินการ purchase(section="A") โดยไม่ต้องยืนยันความพร้อมที่นั่งอีกครั้ง ทำให้ Venue API แสดงการตอบกลับข้อผิดพลาด
{"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.

โปรดสังเกตว่าเอเจนต์จะค้นหาแผนที่ที่นั่งอีกครั้งหลังจากได้รับการตอบกลับข้อผิดพลาดของ API เท่านั้น สาเหตุหลักคือบริบทที่ล้าสมัย ในช่วงต้นของเซสชัน ตัวแทนบันทึก "ส่วน ก: พร้อมให้บริการ 400 รายการ" และเทิร์นนั้นยังคงอยู่ในประวัติการสนทนาที่ส่งไปยังโมเดล
ระบุให้ชัดเจนว่าข้อมูลเก่าจัดเก็บไว้ที่ใด โดยข้อมูลจะยังคงอยู่ในประวัติการสนทนา ซึ่งคำนำหน้าของรัฐ เช่น temp: จะลบล้างไม่ได้

วิธีแก้ไข
👉🔴 กดรีเซ็ตสถานที่ในแผงสถานที่ ทุกขั้นตอนเริ่มต้นจากโลกเดียวกัน นั่นคือ 8 รายการ ทุกที่นั่งว่าง และนาฬิกาที่ 1x
👉💻 ตอนนี้ให้โหลดโค้ดของขั้นตอนนี้
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 7 --force
👉💻 ในเทอร์มินัล 1 ให้กด Ctrl-C แล้วรีสตาร์ท
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"
สิ่งที่เปลี่ยนแปลง
ไฟล์ | สิ่งที่เปลี่ยนแปลง |
| อยู่เสมอ ทํางานทันทีก่อนการซื้อและสามารถบล็อกการซื้อได้ |
|
|
| ตอนนี้ |
fence.py คือการเรียกกลับที่ ADK เรียกใช้ในขณะที่ก่อนการเรียกใช้เครื่องมือ
root_agent = Agent(
...,
before_tool_callback=refresh_before_purchase, # re-read inventory, then decide
)
พิสูจน์สิ
👉🔴 รีเซ็ตสถานที่ ต่อคิว กดขายที่นั่งดีๆ ระหว่างรอ แล้วบอกให้ซื้อที่นั่งในโซน A
คราวนี้ระบบจะหยุดเอง ดู Agent พูดประมาณว่า
"ดูเหมือนว่าส่วน A จะขายหมดแล้ว ฉันต้องอ่านแผนที่ที่นั่งอีกครั้ง"
จากนั้นก็ดูว่าโมเดลจะเรียกใช้ get_seatmap อีกครั้งโดยไม่ต้องแจ้งเตือน และกลับมาเสนอส่วน ค. แทน
👉🔴 อ่านฟีดกิจกรรมของสถานที่จัดงานอย่างละเอียด เนื่องจากจะแสดงทั้ง 2 ช่วง
error purchase failed: Section A has 0 seats left, 2 requested. Re-read the
seat map and pick again.
agent calling purchase

คุณเห็นการซื้อการโทรในแดชบอร์ด แต่เคาน์เตอร์ตั๋วยังคงเป็น 0 ปัญหานี้เกิดขึ้นเนื่องจาก fence.py ทำหน้าที่เป็นตัวป้องกันก่อนที่เครื่องมือจะทำงาน โดยมีรายละเอียดดังนี้
- Agent พยายามเรียกใช้เครื่องมือซื้อ
fence.pyจะรับคำขอก่อนและตรวจสอบที่นั่งที่ว่างอยู่- เมื่อเห็นว่าที่นั่งขายหมด
fence.pyจะแสดงข้อผิดพลาดโดยตรงและยกเลิกการซื้อก่อนที่จะเรียกใช้ API
คำสั่งพรอมต์ไม่สามารถรับประกันได้ว่า LLM จะไม่พยายามซื้อที่นั่งที่ขายหมดแล้ว การป้องกันด้วยโค้ด (before_tool_callback) ช่วยรับประกันว่าการซื้อที่ไม่ดีจะไม่เกิดขึ้นในโลกแห่งความเป็นจริง
👉🔴 กดรีเซ็ตสถานที่ แล้วยกเลิกการซื้อครั้งถัดไป
👉✨ ขอให้ซื้อ แล้วปล่อยให้ลองใหม่ได้หลายครั้งตามต้องการ

จำนวนคำสั่งซื้อจะยังคงอยู่ที่ 1 เนื่องจากสถานที่จัดงานได้รับคีย์การดำเนินการแบบไอดีมโปเทนซีเดียวกันในการลองอีกครั้ง และแสดงการยืนยันเดิมแทนที่จะสร้างคำสั่งซื้อที่ซ้ำกัน
8. เวิร์กโฟลว์อัตโนมัติ
LLM ทำตามกฎที่เข้มงวดทุกครั้งได้ไม่ดี
LLM ทำได้ดีในการตัดสินใจ (เช่น การเลือกรายการตามค่ากำหนดของผู้ใช้) แต่การพึ่งพา LLM ให้ทำตามกฎแบบทีละขั้นตอนอย่างเคร่งครัด (เช่น "ต่อคิวก่อนตรวจสอบที่นั่งเสมอ") หมายความว่าบางครั้ง LLM จะข้ามขั้นตอนหรือสลับลำดับ
หากต้องการแก้ไขปัญหานี้ ให้แบ่งเวิร์กโฟลว์ออกเป็น 2 ประเภท ดังนี้
- กฎที่กำหนดไว้ (ฟังก์ชันโค้ด): ขั้นตอนที่ต้องเกิดขึ้นในลักษณะเดียวกันทุกครั้ง (รับบัตรคิว ตรวจสอบตำแหน่ง) จะเขียนเป็นโหนดโค้ดธรรมดา
- การตัดสินใจ (โหนด Agent): ขั้นตอนที่ต้องใช้การตัดสิน (เลือกส่วนที่นั่ง ตรวจสอบค่ากำหนดของผู้ใช้) จะมอบหมายให้ Agent AI
ดูว่าโฟลว์นี้มีอะไรบ้าง บางส่วนไม่มีการตัดสินเลย
ขั้นตอน | การตัดสิน |
รับบัตรคิว 1 ใบ | ไม่ 1 ครั้งเสมอ ก่อนซื้อเสมอ |
ตรวจสอบว่าถึงตาคุณหรือยัง | ไม่ ถาม อ่านคำตอบ |
เขียนบรีฟสำหรับผู้ซื้อ | ไม่ ประโยคเดียวกัน รูปร่างเดียวกัน ทุกครั้ง |
รายการใด ส่วนใด | ใช่ ราคา วันที่ ใครจะมา และสิ่งที่ไม่ชอบ |
ค่ากำหนดและข้อจำกัดของผู้ใช้ | ใช่ |
กระบวนการที่เข้มงวดไม่ควรอาศัย LLM เนื่องจากโมเดลเชิงความน่าจะเป็นจะทำตามลำดับที่เข้มงวดไม่ได้ในที่สุดเมื่อไม่มีการตรวจสอบ
Workflow ของ ADK จะแยกการดำเนินการที่แน่นอนจากการให้เหตุผลของ AI ดังนี้
- โหนดฟังก์ชันจะบังคับใช้กฎที่กำหนด (เช่น การเข้าร่วมคิวหรือการตรวจสอบตำแหน่ง) อย่างสม่ำเสมอทุกครั้ง
- โหนดตัวแทนจะจัดการการตัดสินใจตามบริบท (เช่น การเลือกที่นั่งตามค่ากำหนดของผู้ใช้)
- โหนด Wait จะระงับการดำเนินการโดยไม่ต้องเก็บกระบวนการไว้ในหน่วยความจำ
ระบบจะไม่แทนที่เอเจนต์เดิม โดยจะกลายเป็นโหนดสุดท้ายในกราฟเวิร์กโฟลว์ ซึ่งยังคงมีเครื่องมือ การตรวจสอบความล้าสมัย และการป้องกันการดำเนินการซ้ำ กราฟจะควบคุมลำดับการดำเนินการ ขณะที่เอเจนต์จะดำเนินการตัดสินใจซื้อ
👉🔴 กดรีเซ็ตสถานที่ในแผงสถานที่
👉💻 ไปที่โค้ดของขั้นตอนนี้
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 8 --force
ดำเนินการ
👉💻 ในเทอร์มินัล 1 ให้กด Ctrl-C แล้วรีสตาร์ท คำสั่งเดิมที่ใช้เป็นประจำ
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"
สิ่งที่มีการเปลี่ยนแปลงตั้งแต่ขั้นตอนสุดท้าย
ไฟล์ | สิ่งที่เปลี่ยนแปลง |
| อยู่เสมอ การเรียกใช้แบบอัตโนมัติเป็นกราฟ ADK 2 |
|
|
| นอกจากนี้ ยังสูญเสีย |
เรียกใช้กราฟ - ข้อความ 3 รายการตามลำดับ
เรียกใช้การดำเนินการต่อไปนี้ทั้งหมดใน Python nightly (ที่เก็บกราฟ) ส่งอินพุตที่เหมือนกันในแต่ละรอบเพื่อสังเกตการเปลี่ยนกราฟเวิร์กโฟลว์ผ่านสถานะต่างๆ
1. สังเกตว่าคุณกำลังพูดกับอะไร
👉 โหลดเว็บ UI ของ 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
โปรดทราบว่า adk web จะเรียกใช้ออบเจ็กต์ที่กำหนดให้กับ root_agent ในที่นี้ root_agent ชี้ไปยังnightlyกราฟเวิร์กโฟลว์ โดยมีbuyer_agentกำหนดค่าเป็นโหนดสุดท้ายในกราฟ
2. เริ่มเลย
👉✨ พิมพ์คำสั่งนี้แล้วกด Enter
go
หมายเหตุ: ในกราฟเวิร์กโฟลว์อัตโนมัติ การดำเนินการเริ่มต้นจะเริ่มโดยไม่ต้องรอข้อมูลจากผู้ใช้ การส่งข้อความใน adk web จะทริกเกอร์การเรียกใช้ครั้งแรก
👉 ตอนนี้ให้ดูที่เว็บเบราว์เซอร์ แผงด้านซ้ายจะวาดกราฟและระบายสีในโหนดขณะที่โหนดทำงาน แผงด้านขวาคือสตรีมเหตุการณ์ ซึ่งจะแสดงให้เห็นว่าระบบตัดสินใจอย่างไร

ดู queue_up เขียน queue_ticket และ queue_event_id ลงในสถานะเซสชัน

คุณควรเห็นการเรียกใช้ฟังก์ชันชื่อ adk_request_input ซึ่งรอคำตอบอยู่ จากนั้นก็จะเงียบลงและสิ้นสุดการวิ่ง กราฟถึง check_front แล้วพบว่าคุณอยู่อันดับที่ 14,203 และหยุด
👉 ตรวจสอบแผงสถานที่: ตั๋วคิว 1 ใบและไม่มีอะไรกำลังเล่นอยู่

3. ปลุกมันขึ้นมา แล้วดูว่าคุณพิมพ์ในช่องใด
เลื่อนสตรีมเหตุการณ์ไปยังเหตุการณ์ adk_request_input แล้วค้นหากล่องป้อนข้อมูลที่เชื่อมโยงกับเหตุการณ์นั้นๆ

ตอนนี้คุณพิมพ์ได้ 2 ที่และแต่ละที่ก็มีหน้าที่ต่างกัน
ตำแหน่งที่คุณพิมพ์ | สิ่งที่เกิดขึ้น |
ช่อง | ตอบการขัดจังหวะ การวิ่งที่จอดไว้จะดำเนินต่อจาก |
กล่องแชทหลักที่ด้านล่าง | เริ่มการเรียกใช้ใหม่ กราฟจะเล่นซ้ำตั้งแต่ |
👉✨ พิมพ์อะไรก็ได้ลงในช่อง Enter your response... แล้วกดลูกศร
👉 เทอร์มินัล 1:
[check_front] #14,197 — not yet. Pausing.
บรรทัดเดียว ระบบไม่ได้เลือกรายการที่ 2 และไม่ได้ใช้ตั๋วคิวที่ 2 ซึ่งจะทำให้คุณอยู่ในคิวที่ 14,203
หากเห็น [open] และ [pick_show] ทำงานอีกครั้ง แสดงว่าคุณพิมพ์ในช่องแชท กดรีเซ็ตสถานที่ แล้วเริ่มขั้นตอนใหม่
👉 ตรวจสอบแผงสถานที่อีกครั้ง: ยังคงมีตั๋ว1 ใบ และอันดับลดลงเล็กน้อยเนื่องจากนาฬิกาของสถานที่ยังคงเดินต่อไปในขณะที่ไม่มีการดำเนินการใดๆ จากคุณ

4. ไปที่ด้านหน้า แล้วทำต่อให้เสร็จ
👉🔴 กดข้ามการรอสายในแผงสถานที่

👉✨ ตอบคำถามในกิจกรรม adk_request_input ล่าสุดเป็นครั้งสุดท้ายในช่องของกิจกรรม ไม่ใช่ในแชท 
คราวนี้เราจะcheck_frontหาคุณที่ด้านหน้าและตกลงไป ดูแผงด้านซ้ายสว่างขึ้น brief และ concert ซึ่งเป็น 2 โหนดสุดท้าย

👉 และดูที่เบราว์เซอร์ นี่คือการสิ้นสุดการวิ่ง และมี 4 สิ่งที่ควรค่าแก่การอ่าน

กราฟเป็นสีเขียวทั้งหมดและระบบจะนับขอบ brief และ concert สว่างขึ้น และขอบเขตใน pick_show, queue_up และ check_front มีป้ายแล้ว
2x
คือจำนวนครั้งที่กราฟเดินตามขอบนั้น 1 ครั้งต่อการปลุก ส่วน 2 ที่ด้านบนคือราคาของการกลับมาทำงานต่อ โดยตัวจัดกำหนดการจะเข้าสู่กราฟจากจุดเริ่มต้นทุกครั้ง แต่ไม่ได้หมายความว่ามีการดำเนินการ 3 ครั้ง และแผงสถานที่จัดงานก็เป็นเครื่องพิสูจน์ว่ามีตั๋วคิว 1 ใบ ไม่ใช่ 2 ใบ @node(rerun_on_resume=False)
กราฟ

# agent/concert/nightly.py
nightly = Workflow(
name="concert_nightly",
edges=[(START, open_the_night, pick_show, queue_up,
check_front, brief, buyer_agent)],
)
โปรดทราบว่า 2 โหนดเป็นเอเจนต์และ 2 โหนดเป็นฟังก์ชัน และถือว่าโหนดใดเป็นโหนดใดเป็นคำถามการออกแบบทั้งหมด
โหนด | เหตุผลที่ใช้รูปร่างนั้น |
| agent การเลือกรายการจากประวัติของผู้อื่นเป็นการตัดสิน โดยจะเรียกใช้ |
| ฟังก์ชัน การต่อคิวเป็นกฎ กฎไม่ควรเป็นการเสี่ยงทายในเวลา 03:00 น. |
| ฟังก์ชัน "ฉันอยู่ด้านหน้าไหม" มีคำตอบที่ถูกต้องเพียงคำตอบเดียวและสถานที่จัดงานก็มีคำตอบนั้น |
| agent ซึ่งก็คือ |
ดูโหนดสุดท้ายและค้นหา Agent ที่คุณสร้าง ไม่ใช่การคัดลอกหรือการเขียนใหม่
# agent/concert/nightly.py
from .agent import buyer_agent
ทุกอย่างจะมาพร้อมกัน ไม่ว่าจะเป็นเครื่องมือทุกอย่างที่มาพร้อมกับเครื่องมือนี้ การอ่านแผนที่ที่นั่งอีกครั้งใน fence.py และคีย์การดำเนินการแบบไอดีมโปเทนท์ใน purchase มีเพียงรายการเดียว
เหตุใดจึงหยุดแทนที่จะรอ
นี่คือเวอร์ชันที่ไม่มีใครควรเขียน
# what NOT to write — this is nobody's file
while not ready: # never do this
time.sleep(1)
โดยจะรันกระบวนการเป็นเวลา 40 นาทีโดยไม่ทำอะไรเลย จากนั้นจะหยุดทำงานพร้อมกับเทอร์มินัลและดำเนินการต่อไม่ได้ check_front จะทำสิ่งต่อไปนี้แทน
# agent/concert/nightly.py — check_front
if not status["ready"]:
return RequestInput(message=f"Still at #{position}. Wake me and I'll check.")
RequestInput เป็นการขัดจังหวะ กราฟจะหยุดที่จุดนั้น การเรียกใช้จะไปที่ที่เก็บเซสชัน และกระบวนการจะออกได้โดยอิสระ
การเรียกใช้ไม่มีค่าใช้จ่ายขณะรอ และสิ้นสุดที่จุดเดียวเท่านั้น นั่นคือตั๋วที่ซื้อ แต่ก็อาจต้องใช้เวลาทั้งคืนและตื่นขึ้นมาหลายครั้งกว่าจะทำได้
9. การทำงานร่วมกับมนุษย์
อย่าใช้จ่ายเงินโดยไม่ตรวจสอบกับบุคคลอื่นอีกครั้ง
เอเจนต์อัตโนมัติที่ทำงานข้ามคืนไม่ควรตัดสินใจทางการเงินที่มีค่าใช้จ่ายสูงโดยอิงตามความชอบที่คาดเดาหรือคลุมเครือจากประวัติการแชท
ก่อนที่จะดำเนินการที่มีความเสี่ยงสูง (เช่น การใช้จ่ายเงินซื้อตั๋ว) เอเจนต์ต้องหยุดชั่วคราว ขอให้ผู้ใช้ยืนยันงบประมาณของตนเองด้วยเสียง และบันทึกการยืนยันอย่างชัดเจนนั้นก่อนที่จะทำการซื้อใดๆ
👉🔴 กดรีเซ็ตสถานที่ในแผงสถานที่ ทุกขั้นตอนเริ่มต้นจากโลกเดียวกัน นั่นคือ 8 รายการ ทุกที่นั่งว่าง และนาฬิกาที่ 1x
👉💻 ไปที่โค้ดของขั้นตอนนี้
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 9 --force
ดำเนินการ
👉💻 ในเทอร์มินัล 1 ให้กด Ctrl-C แล้วรีสตาร์ท คำสั่งเดิม:
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"
สิ่งที่มีการเปลี่ยนแปลงตั้งแต่ขั้นตอนสุดท้าย
ไฟล์ | สิ่งที่เปลี่ยนแปลง |
| เก็บสิ่งที่บุคคลนั้นตกลงไว้เป็นประโยคที่บุคคลนั้นพูด |
| เพิ่ม |
| โหนดแรกใหม่ |
อย่าทำตาม จัดเก็บประโยค
เปิด agent/concert/budget.py แล้วอ่านทั้งหมด
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"
โหนดจะตรวจสอบ 2 สิ่ง ได้แก่ บุคคลนั้นพูด และบุคคลนั้นยืนยัน
จากนั้นจะเก็บประโยคไว้ในสถานะเซสชันแทนที่จะเก็บไว้ใน user: โดยตั้งใจ ตกลงงบประมาณสำหรับการจอง 1 ครั้ง "250 ดอลลาร์หากเป็นวงที่เก่ง" เป็นคำพูดที่พูดถึงวงดนตรีวงนั้นในคืนนั้น และไม่เกี่ยวข้องกับการจองตั๋ว
และคำพูดอื่นๆ ของเขาก็เป็นความจริงเช่นกัน
อย่าหยุดที่งบประมาณ ผู้คนอาจเปลี่ยนใจขณะตัดสินใจ ดังนั้นให้บันทึกทุกสิ่งที่ลูกค้าขอไว้ที่นี่แทนที่จะพึ่งพาสิ่งที่ตัวแทนจำได้
"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)
อ่านเฉพาะเหตุการณ์ที่ผู้ใช้สร้างขึ้น โดยจะดูเฉพาะ
text
บางส่วนจาก function_response เพื่อไม่ให้คำถามและคำตอบเกี่ยวกับงบประมาณปรากฏที่นี่ ไม่มีรายการซ้ำ
Note agree_budget จะเรียกใช้ฟังก์ชันนี้ทุกครั้ง ไม่ใช่แค่ครั้งแรก ดังนั้นการแก้ไขที่พิมพ์ขณะที่ตัวแทนกำลังรอสายก็จะปรากฏขึ้นด้วย จากนั้นopen_the_nightยื่นประโยคทั้ง 2 ประโยค คำขอ และงบประมาณให้ผู้เลือกอ่าน
กราฟที่ยาวขึ้น 1 โหนด
มาเริ่มด้วยการสนทนา
# 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)],
)

โหนด 2 โหนดจะหยุดและรอ ซึ่งเป็นกลไกเดียวกัน check_front รอสถานที่และนาฬิกาตอบ agree_budget จะรอให้บุคคลรับสายและรับสายได้โดยบุคคลเท่านั้น ความแตกต่างนั้นคือทั้งหมดของขั้นตอนที่ 10
ดำเนินการทีละขั้นตอน
1. เริ่มเลย
👉 โหลดเว็บ UI ของ ADK ซ้ำ แล้วเลือกเอเจนต์ concert
👉✨ ขอในสิ่งที่คุณต้องการจริงๆ
Get us two tickets to the Amsterdam show.

ในขั้นตอนที่ 8 กราฟไม่มีผู้ใช้ ดังนั้นข้อความที่คุณพิมพ์จะถูกทิ้งและ go จะทำงานได้ดีเช่นกัน ที่นี่ agree_budget เรียกใช้ what_they_asked_for(ctx) ในบรรทัดแรก ซึ่งจะรวบรวมทุกสิ่งที่คุณพิมพ์และใส่ไว้ใน state["request"] จากนั้นopen_the_nightจะส่งให้ผู้หยิบสินค้าพร้อมกับงบประมาณ
คุณไม่ได้ระบุวันและอัมสเตอร์ดัมมี 2 รายการ ดังนั้นให้ดูว่าเครื่องมือเลือกจะกลับไปเป็น recall() หรือไม่ จากนั้นค้นหา "แซมไม่ว่างในคืนวันธรรมดา" แล้วเลือกวันเสาร์
และในเบราว์เซอร์ การเรียกใช้จะหยุดเมื่อเกิดเหตุการณ์ adk_request_input
กราฟเพิ่งถามคำถามคุณ การขัดจังหวะแบบเดียวกับการรอคิวในขั้นตอนที่ 8 แต่ตอนนี้จะชี้ไปที่บุคคลเพื่อตอบคำถาม
2. ตอบในแบบที่ผู้ใช้ตอบจริงๆ
👉✨ ในช่อง Enter your response... ไม่ใช่ช่องแชทที่ด้านล่าง ให้ทำดังนี้
About a 100 for the cheap seats. 250 if they're the good ones.
ดูการหยุดและยืนยัน
[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?

3. ยืนยัน
👉✨ กล่องเดียวกัน
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.
โดยมี 4 โหนดที่ทำงานด้วยคำเดียว 👉 เปิดแท็บสถานะ แล้วค้นหา budget ซึ่งเป็นประโยคของคุณแบบคำต่อคำ

ไม่มีคำนำหน้า นั่นคือสถานะเซสชัน และเป็นสิ่งที่ควรตัดสินใจโดยตั้งใจหากคุณเริ่มเซสชันใหม่และงบประมาณจะหมด ระบบจึงจะถามคุณอีกครั้ง
4. ไปที่ด้านหน้าและดำเนินการให้เสร็จสิ้น
👉🔴 กดข้ามการรอสายในแผงสถานที่
👉✨ ตอบคำถามใหม่ล่าสุดของ adk_request_input ในช่องของคุณอีกครั้ง
go

👉 ตรวจสอบแผงสถานที่: 1 คำสั่งซื้อในราคาที่คุณตกลงสำหรับที่นั่งประเภทนั้น

กราฟถามคำถามกับผู้ใช้อย่างไร
เปิด nightly.py แล้วอ่าน agree_budget ทำให้ทั้ง 3 อย่างเป็นจริงพร้อมกัน และคาดหวังความล้มเหลวแบบเดียวกันเมื่อไม่มีอย่างใดอย่างหนึ่ง นั่นคือ ระบบจะถามไปเรื่อยๆ
@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?",
)
...
| หากไม่มี ระบบจะถือว่าโหนดเสร็จสมบูรณ์แล้วและข้ามไป | |
เสถียร | ค่าเริ่มต้นคือ UUID ใหม่ต่อการเรียกใช้แต่ละครั้ง ดังนั้นจึงไม่มีสิ่งใดที่จะใช้ค้นหาคำตอบได้ | |
| ซึ่งเป็นที่ที่คำตอบจะปรากฏ ไม่ใช่ค่าที่ส่งคืนของ | |
กราฟ
ซึ่งเป็นกราฟของขั้นตอนที่ 8 ที่มีโหนดเพิ่มอีก 1 โหนดอยู่ด้านหน้า 
ข้อจํากัดที่ผู้ใช้ยืนยันจะมีขอบเขตระดับเซสชันและจะหมดอายุเมื่อการสนทนาสิ้นสุดลง การคงขีดจำกัดชั่วคราวไว้ในเซสชันต่างๆ อาจทำให้การตัดสินใจแบบครั้งเดียวกลายเป็นค่าเริ่มต้นถาวรโดยไม่ตั้งใจ
10. The Cloud Stack
แล็ปท็อปเข้าสู่โหมดสลีปและไฟล์ในเครื่องหายไป
ในแล็ปท็อป เอเจนต์จะขึ้นอยู่กับการที่คอมพิวเตอร์ยังคงทำงานอยู่ ฐานข้อมูล SQLite ในเครื่อง และไฟล์ในเครื่อง หากคุณปิดฝาแล็ปท็อปหรือติดตั้งใช้งานในคอนเทนเนอร์ระบบคลาวด์ที่รีสตาร์ท ไฟล์ในเครื่องและหน่วยความจำจะหายไป
หากต้องการให้ทำงานได้อย่างน่าเชื่อถือในเวอร์ชันที่ใช้งานจริง ให้ทำดังนี้
- Cloud Compute: เรียกใช้ Agent ในคอนเทนเนอร์ (เช่น Cloud Run) ที่ออนไลน์อยู่
- ฐานข้อมูลระบบคลาวด์ที่ทนทาน: แทนที่ไฟล์ในเครื่องและ SQLite ด้วยบริการระบบคลาวด์ (Cloud SQL สำหรับข้อมูลเซสชัน, Cloud Storage สำหรับไฟล์)
- ตัวตั้งเวลาในระบบคลาวด์: ใช้ตัวกำหนดเวลาที่มีการจัดการ (เช่น Cloud Scheduler และ Pub/Sub) เพื่อให้ตัวแทนทำงานได้แม้ว่าแล็ปท็อปจะปิดอยู่ก็ตาม
👉🔴 กดรีเซ็ตสถานที่ในแผงสถานที่ ทุกขั้นตอนเริ่มต้นจากโลกเดียวกัน นั่นคือ 8 รายการ ทุกที่นั่งว่าง และนาฬิกาที่ 1x
👉💻 ไปที่โค้ดสุดท้าย
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 10 --force
ไฟล์ | สิ่งที่เปลี่ยนแปลง |
| อยู่เสมอ FastAPI หนึ่งรายการที่แสดงหน้าแชทและปลายทางที่ตัวจัดกำหนดการเรียกใช้ |
| อยู่เสมอ python-slim และ uvicorn ทั่วไป ไม่มีส่วนใดที่เฉพาะเจาะจงสำหรับ ADK |
| คัดลอกเข้ามาเพื่อให้หน้าเว็บอยู่ในคอนเทนเนอร์เดียวกับเอเจนต์ |
| ไม่เปลี่ยนแปลง ไบต์ต่อไบต์ โฟลเดอร์ที่คุณใช้ในขั้นตอนที่ 9 |
หน้าที่คุณสร้างไว้แล้ว
** UI เว็บของ ADK** เป็นอินเทอร์เฟซการแก้ไขข้อบกพร่องในเครื่องอย่างเคร่งครัดสำหรับการตรวจสอบสถานะ สตรีมเหตุการณ์ และกราฟเวิร์กโฟลว์ระหว่างการพัฒนา โดยไม่ได้จัดแพ็กเกจสำหรับการติดตั้งใช้งานจริง
สำหรับการใช้งานจริง ให้ใช้ส่วนหน้าของแชทแบบสแตนด์อโลนที่สร้างขึ้นในขั้นตอนที่ 3 โดยจะจัดการการรับส่งข้อความและการโต้ตอบของผู้ใช้ผ่าน/wakeปลายทางโดยไม่ต้องเปิดเผยการควบคุมการแก้ไขข้อบกพร่องภายใน
การย้ายข้อมูลสถานะในเครื่องไปยังบริการระบบคลาวด์
หยุดเซิร์ฟเวอร์และดูสิ่งที่เซิร์ฟเวอร์เขียน
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
แม้ว่าไฟล์ในเครื่องจะทำงานได้ดีสำหรับการพัฒนา แต่จะทำงานไม่ได้ในสภาพแวดล้อมการใช้งานจริงเนื่องจากคอนเทนเนอร์ Cloud Run ใช้พื้นที่เก็บข้อมูลชั่วคราวเฉพาะอินสแตนซ์
เมื่อเรียกใช้ใน Cloud Run
- อินสแตนซ์จะปรับขนาดเป็น 0: เมื่อการรับส่งหยุดลง คอนเทนเนอร์จะสิ้นสุดและระบบจะลบระบบไฟล์ในเครื่อง
- อินสแตนซ์จะเพิ่มโหนดออกแยกกัน: อินสแตนซ์ที่ทำงานพร้อมกันหลายรายการจะไม่แชร์พื้นที่ดิสก์ โดยแต่ละอินสแตนซ์จะทำงานด้วยระบบไฟล์ที่แยกกัน
หากตัวแทนบันทึกตั๋วในคิวหรือสถานะเซสชันลงในไฟล์ SQLite ในเครื่อง (sessions.db) คำขอที่ตามมาซึ่งกำหนดเส้นทางไปยังอินสแตนซ์คอนเทนเนอร์ใหม่หรืออินสแตนซ์อื่นจะพบฐานข้อมูลที่ว่างเปล่า หากต้องการรักษาสถานะที่คงทนเมื่อมีการปรับขนาดและรีสตาร์ท คุณต้องแทนที่ไฟล์ในเครื่องด้วยบริการภายนอกที่มีการจัดการ
สิ่งที่คุณต้องการแทนคือสถานะที่อยู่ได้นานกว่าคอนเทนเนอร์ใดๆ บริการ 3 อย่าง โดยแต่ละอย่างจะใช้กับไฟล์ 1 ไฟล์
ในแล็ปท็อป | ในระบบคลาวด์ | คำอธิบาย |
| Cloud SQL | ฐานข้อมูล Postgres ที่มีการจัดการ โดยจะทำงานอยู่ไม่ว่าจะมีอะไรทำงานอยู่หรือไม่ก็ตาม |
| Cloud Storage | Bucket ไฟล์เป็นออบเจ็กต์ที่เข้าถึงได้จากทุกอินสแตนซ์พร้อมกัน |
| Cloud Storage | ที่อยู่ในกลุ่มเดียวกัน |
| Cloud Scheduler | Cron ที่ Google เรียกใช้ เทอร์มินัลไม่ใช่นาฬิกาปลุก |
สร้าง
จัดสรรฐานข้อมูล PostgreSQL, ผู้ใช้ฐานข้อมูล และ Bucket ของ Cloud Storage โปรดทราบว่าการสร้างอินสแตนซ์ Cloud SQL จะใช้เวลาหลายนาทีและเริ่มต้นในเบื้องหลังระหว่าง setup.sh
👉💻 รวบรวมและสร้างส่วนที่เหลือ
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
สคริปต์ดังกล่าวสร้าง 4 รายการ โดยรายการละ 1 แถวในตารางด้านบน และเขียน URI 2 รายการลงใน .env ดังนี้
CLOUD_SESSION_SERVICE_URI=postgresql+asyncpg://adk:...@/adk?host=/cloudsql/proj:region:instance
CLOUD_ARTIFACT_SERVICE_URI=gs://your-project-agent-you/artifacts
การติดตั้งใช้งานเอเจนต์ด้วยทริกเกอร์ที่กำหนดเวลาไว้ที่จัดการแล้ว
เมื่อบริการพร้อมใช้งานแล้ว ให้กำหนดตำแหน่งถาวรสำหรับตัวแทน
👉💻 3 หรือ 4 นาที เริ่มอ่านได้เลย
cd ~/monstertix
. ./set_env.sh
./deploy-agent.sh
สคริปต์จะสร้างอิมเมจคอนเทนเนอร์จาก agent/ ทำให้ใช้งานได้ใน Cloud Run และจัดสรรโครงสร้างพื้นฐานที่รองรับแบบขับเคลื่อนด้วยเหตุการณ์
การติดตั้งใช้งานที่สมบูรณ์ประกอบด้วยคอมโพเนนต์ที่แตกต่างกัน 5 อย่าง ดังนี้
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 เป็นแหล่งที่มาของทริกเกอร์
Cloud Scheduler จะแทนที่ตัวจับเวลาการดำเนินการในเครื่อง (clock.py) ด้วยบริการ Cron ที่มีการจัดการซึ่งคงอยู่โดยไม่ขึ้นอยู่กับเซสชันเทอร์มินัลในเครื่อง จัดการการลองใหม่โดยอัตโนมัติ และรองรับการกำหนดเวลาตามเขตเวลา
Pub/Sub เป็นเลเยอร์การแยกข้อความ
การวางหัวข้อ Pub/Sub ระหว่าง Cloud Scheduler กับ Cloud Run มีข้อดีด้านสถาปัตยกรรม 3 ประการ ดังนี้
- ความทนทานของข้อความ: จัดคิวเหตุการณ์ทริกเกอร์ขาเข้าหากบริการ Cloud Run เริ่มต้นแบบ Cold Start, ทำให้ใช้งานได้อีกครั้ง หรือไม่พร้อมใช้งานชั่วคราว
- นโยบายการลองใหม่ในตัว: ลองใหม่โดยอัตโนมัติเมื่อนำส่งไม่สำเร็จชั่วคราวโดยไม่ต้องใช้ตรรกะการลองใหม่ระดับแอปพลิเคชันที่กำหนดเอง
- ความสามารถในการปรับขนาดแบบแฟนเอาต์ (Fan-Out): ช่วยให้บริการดาวน์สตรีมหลายรายการสมัครใช้บริการเหตุการณ์ที่กำหนดเวลาเดียวกันได้โดยไม่ต้องกำหนดค่าตัวกำหนดเวลาใหม่

การกำหนดค่า trigger_sources=["pubsub"] จะลงทะเบียนปลายทางการพุชที่เข้ากันได้กับเพย์โหลดของซองจดหมาย Pub/Sub มาตรฐาน ซึ่งจะแปลข้อความขาเข้าไปเป็นเหตุการณ์การเรียกใช้ตัวแทนโดยตรง
→ 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 * * *
คุยกับมัน
👉 เปิด URL ของตัวแทนในเบราว์เซอร์ 👉✨ ขอให้จอง
Book me 3 tickets for The Midnight Signal in Tokyo. Best seats in the house,
I don't want the cheap ones.
ควรถามว่าคุณยินดีที่จะใช้จ่ายเท่าใด
👉✨ ตอบคำถามเหมือนพูดออกมา แล้วยืนยันโดยทำดังนี้
up to 250 a seat for the good ones
ขณะนี้การทดสอบอยู่ในคิว
👉🔴 กดข้ามการรอคิวในแผงสถานที่ เพื่อให้คิวพร้อมเมื่อข้อความมาถึง
หากต้องการดำเนินการตามกำหนดเวลาจริง คุณต้องเปิดใช้งานงาน โดย jobs run จะปฏิเสธงานที่หยุดชั่วคราวด้วย Job.state must be ENABLED
gcloud scheduler jobs resume presale-$(gcloud config get-value account | cut -d@ -f1) \
--location=$GOOGLE_CLOUD_REGION
👉 ภายในไม่กี่วินาที เอเจนต์จะดำเนินการซื้อภายในวงเงินงบประมาณที่ยืนยันแล้วและส่งคืนการยืนยันคำสั่งซื้อโดยอัตโนมัติ
👉🔴 ตรวจสอบแผงสถานที่ คำสั่งซื้อเดียว
วิธีตั้งค่าให้สคริปต์ทำงานทุกๆ 5 นาทีมีดังนี้
gcloud scheduler jobs update pubsub presale-$(gcloud config get-value account | cut -d@ -f1) \
--location=$GOOGLE_CLOUD_REGION --schedule="*/5 * * * *"
ล้างข้อมูล
นับ 2 สิ่งที่ติดตั้งใช้งาน ได้แก่ สถานที่และตัวแทนที่อยู่ข้างๆ ทั้ง 2 อย่างจะปรับขนาดเป็น 0 ดังนั้นจึงไม่มีค่าใช้จ่ายเมื่อไม่ได้ใช้งาน ลบเลย
./destroy-agent.sh # service, topic, subscription, scheduler job
./destroy-venue.sh
โปรดทราบว่าเราจะเว้น Cloud SQL และ Bucket ไว้โดยเจตนา เนื่องจากมีข้อมูลของคุณและทั้ง 2 รายการจะยังคงมีการเรียกเก็บเงิน เมื่อคุณมั่นใจว่าดำเนินการเสร็จแล้ว ให้ทำดังนี้
./destroy-agent.sh --all
ส่วนอื่นๆ จะเป็นแบบท้องถิ่น ลบโฟลเดอร์เมื่อเสร็จสิ้น หรือเก็บไว้และดำเนินการกับโปรเจ็กต์ของคุณเองได้นานเท่าที่ต้องการ
เอเจนต์ เครื่องมือ การเรียกกลับ และงบประมาณยังคงเหมือนเดิม แต่มี 2 URI และตัวแปรสภาพแวดล้อม 1 รายการที่ทำได้
11. ภาพรวมของระบบและคู่มือการแก้ปัญหา
ความแตกต่างของสภาพแวดล้อม: สถาปัตยกรรมในเครื่องเทียบกับสถาปัตยกรรมในระบบคลาวด์
แล็ปท็อปของคุณ | ในระบบคลาวด์ | สิ่งที่เปลี่ยนแปลง |
| Cloud SQL | สตริงการเชื่อมต่อ |
|
| URI |
|
| เส้นทาง |
| บริการ Cloud Run 1 รายการ |
|
| Cloud Scheduler → Pub/Sub |
|
คุณที่แป้นพิมพ์ |
| ค่าเริ่มต้นที่ลบล้างต่อคำขอ |
การติดตั้งใช้งานเอเจนต์หลัก ซึ่งรวมถึง root_agent, คำจำกัดความของเครื่องมือ, การป้องกันการเรียกกลับ และการบังคับใช้งบประมาณ จะยังคงเหมือนเดิมทุกประการ การเปรียบเทียบ solutions/step10_deploy/concert/ กับขั้นตอนที่ 9 แสดงให้เห็นว่าไบต์ต่อไบต์ตรงกันทุกประการ ซึ่งเป็นการตรวจสอบว่าการแยกการกำหนดค่าโครงสร้างพื้นฐานออกจากตรรกะของเอเจนต์ของนามธรรมบริการของ ADK ทำงานอย่างไร
หากคุณพบปัญหา
คุณสามารถดูโค้ดที่ใช้งานได้ทั้งหมดในทุกขั้นตอน
./use-solution.sh N --force # any step. resets memory + sessions + artifacts
./use-solution.sh N --force --keep # code only, leave state alone
เมื่อเกิดข้อผิดพลาด
ลักษณะปัญหา | แก้ไข |
| รหัสไม่ถูกต้อง |
โมเดลแสดงข้อผิดพลาด 404 |
|
| มีบางอย่างใช้พอร์ต 8000 หรือ 8090 อยู่แล้ว |
Agent ไปที่สถานที่จัดงานไม่ได้ |
|
คิวไม่เลื่อน | กดข้ามการรอสาย ที่ 1× คิวจะไม่เลื่อนไปข้างหน้าด้วยตัวเอง |
ตั๋วคิวที่ 2 จะปรากฏในแผง | คุณตอบในช่องแชทแทนที่จะตอบในช่อง |
เมนูแบบเลื่อนลงว่างเปล่าหรือแอปไม่โหลด |
|
มีคำเตือน | คาดการณ์ |
ทุกอย่างดูแปลกไป | กดรีเซ็ตสถานที่ แล้ว |
ไฟล์หน่วยความจำเต็มไปด้วยการจองที่ซ้ำกัน |
|
|
|
Cloud Shell หมดเวลาแล้ว | เริ่ม |