1. Giới thiệu
Sản phẩm bạn sẽ tạo ra
Trong lớp học lập trình này, bạn sẽ vào vai một nhà phát triển xây dựng Ứng dụng thời trang, một ứng dụng mua sắm Flutter cho một thương hiệu bán lẻ hư cấu. Nhiệm vụ của bạn: thêm 2 tính năng dựa trên AI để thay đổi trải nghiệm mua sắm trực tuyến.
- Phòng thử đồ ảo – Người dùng tải ảnh của chính mình lên, chọn một mặt hàng quần áo và xem hình ảnh do AI tạo về chính mình khi mặc mặt hàng đó.
- Nhà tạo phong cách bằng AI – Dựa trên vị trí, dịp và lựa chọn ưu tiên về phong cách của người dùng, một tác nhân AI sẽ tuyển chọn các đề xuất trang phục hoàn chỉnh và người dùng có thể tinh chỉnh các đề xuất đó thông qua cuộc trò chuyện.
Ý tưởng này rất đơn giản: khi mọi người mặc thử quần áo trong phòng thử đồ, họ có nhiều khả năng mua quần áo đó hơn. Nhưng trên mạng thì sao? Bạn chỉ đang đoán mò. Dự án này sẽ khắc phục khoảng cách đó bằng AI.
Cấu trúc xem nhanh
Flutter App ──── HTTP/REST ────▶ ADK Go Backend
│
┌──────────┼──────────┐
Fitting Room Stylist Catalog
Agent Agent Agent
│
Gemini API + Cloud Storage
Công nghệ cốt lõi
Thành phần | Công nghệ | Mục đích |
Khung tác nhân | ADK (Bộ công cụ phát triển tác nhân) cho Go | Điều phối, phiên, cấu phần phần mềm nhiều tác nhân |
Suy luận của tác nhân (Pro) | Bản xem trước Gemini 3.1 Pro | Cung cấp năng lượng cho phòng thử đồ và nhân viên tư vấn |
Suy luận của tác nhân (Flash) | Bản xem trước Gemini 3 Flash | Cung cấp năng lượng cho các tác nhân gốc và danh mục (định tuyến/tra cứu đơn giản) |
Tạo hình ảnh | Hình ảnh Gemini 2.5 Flash | Tạo hình ảnh thử đồ và trang phục |
Giao diện người dùng | Flutter (Dart) | Ứng dụng đa nền tảng (Web, iOS, Android) |
Bộ nhớ | Google Cloud Storage | Lưu trữ hình ảnh sản phẩm và các thành phần được tạo |
Lưu trữ | Cloud Run | Triển khai vùng chứa không máy chủ |
2. 📦 Điều kiện tiên quyết và chế độ thiết lập Cloud Shell
1. Mở Trình chỉnh sửa Cloud Shell
👉 Mở Cloud Shell Editor trong trình duyệt.
Nếu thiết bị đầu cuối không xuất hiện ở cuối màn hình:
- Nhấp vào View (Xem) → Terminal (Thiết bị đầu cuối)
2. Thiết lập Flutter SDK
Cloud Shell được cài đặt sẵn Flutter tại /google/flutter. Vì thư mục đó thuộc sở hữu của một người dùng hệ thống khác, nên bạn sẽ gặp lỗi fatal: detected dubious ownership vào lần đầu tiên chạy flutter. Thêm thư mục đó vào danh sách safe-directory của git một lần:
git config --global --add safe.directory /google/flutter
Xác minh rằng Flutter có trên PATH và đang hoạt động:
flutter --version
Lần chạy đầu tiên sẽ tải SDK Dart xuống và tạo công cụ Flutter – hãy đợi một phút. Bạn sẽ thấy một biểu tượng như Flutter 3.x • channel stable.
3. Sao chép Kho lưu trữ
cd ~
git clone https://github.com/gca-americas/fashion-app-demo
cd fashion_app_demo
4. Khám phá Cấu trúc dự án
fashion_app_demo/
├── adk_backend/ # Go backend with ADK agents
│ ├── main.go # Entry point — wires all agents + REST server
│ ├── catalog/ # Catalog Agent — product lookup
│ │ ├── agent.go
│ │ ├── catalog.yaml # Product database (YAML)
│ │ └── instructions.md # Agent persona prompt
│ ├── fittingroom/ # Fitting Room Agent — virtual try-on
│ │ ├── agent.go
│ │ ├── instructions.md # Agent persona prompt
│ │ └── tool_instructions.md # Image generation prompt
│ ├── stylist/ # Stylist Agent — outfit curation
│ │ ├── agent.go
│ │ └── instructions.md # Agent persona + output format
│ ├── rootagent/ # Root Agent — routes to the right agent
│ │ └── agent.go
│ └── tools/ # Shared tools
│ ├── imagetool.go # getProductImage — loads product images
│ ├── outfit_gen_tool.go # generate_outfit_image — creates outfit images
│ └── cors_helper.go # CORS middleware + request logging
│
├── flutter_frontend/ # Flutter cross-platform app
│ ├── lib/
│ │ ├── main.dart # App entry point + Provider setup
│ │ ├── app_config.dart # Backend URL configuration
│ │ ├── core_app/ # Pre-built shopping app (browse, cart, etc.)
│ │ └── workshop_tasks/ # AI feature code
│ │ ├── step_1_try_it_on/ # Virtual Try-On flow
│ │ │ ├── providers/ # TryItOnProvider (state management)
│ │ │ ├── services/ # AdkFittingRoomService (HTTP calls)
│ │ │ └── ui/ # Screens (product detail → try on → fitting room)
│ │ └── step_2_style_me/ # Style Me flow
│ │ ├── models/ # Outfit, StyleRequest data classes
│ │ ├── providers/ # StylingProvider (state management)
│ │ ├── services/ # AdkStylingService (HTTP calls)
│ │ └── ui/ # Screens (form sheet → outfit carousel)
│ └── assets/images/ # Product catalog images
3. ☁️ Thiết lập dự án trên Google Cloud
1. Tạo dự án mới
gcloud projects create fashion-app-demo --name="Fashion App Demo"
gcloud config set project fashion-app-demo
2. Liên kết tài khoản thanh toán
Liệt kê tài khoản thanh toán:
gcloud billing accounts list
Xem
OPEN
cột. Nội dung phải là True. Nếu bạn thấy thông báo False (thường gặp khi hết thời gian dùng thử miễn phí), thì tài khoản đã bị đóng và sẽ không phải trả bất kỳ khoản phí nào. Hãy chuyển đến phần khắc phục sự cố bên dưới trước khi tiếp tục.
Sao chép ACCOUNT_ID của một tài khoản OPEN: True (có dạng 0X0X0X-0X0X0X-0X0X0X) rồi liên kết với dự án của bạn:
gcloud billing projects link fashion-app-demo \
--billing-account=YOUR_BILLING_ACCOUNT_ID
Xác minh đường liên kết:
gcloud billing projects describe fashion-app-demo
Bạn sẽ thấy billingEnabled: true. Nếu bạn thấy biểu tượng billingEnabled: false ngay cả sau khi liên kết, thì tài khoản đó đã đóng (OPEN: False) – hãy xem khối khắc phục sự cố bên dưới.
3. Bật các API bắt buộc
gcloud services enable \
aiplatform.googleapis.com \
storage.googleapis.com \
run.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com
API | Mục đích |
| Vertex AI – |
| Cloud Storage – lưu trữ hình ảnh danh mục sản phẩm và kết quả dùng thử được tạo |
| Cloud Run – lưu trữ phần phụ trợ dưới dạng một vùng chứa không máy chủ |
| Cloud Build – tạo các hình ảnh Docker từ nguồn |
| Artifact Registry – lưu trữ các hình ảnh Docker đã tạo |
4. Tạo bộ chứa GCS
export PROJECT_ID=$(gcloud config get-value project)
gcloud storage buckets create gs://fashion-app-$PROJECT_ID \
--location=us-central1 \
--uniform-bucket-level-access
5. Tải hình ảnh danh mục sản phẩm lên
Công cụ getProductImage của phần phụ trợ sẽ đọc từ gs://$GCS_BUCKET/catalog-assets/images/. Tải hình ảnh trong danh mục lên đúng đường dẫn đó:
cd ~/fashion_app_demo
gcloud storage cp flutter_frontend/assets/images/*.png \
gs://fashion-app-$PROJECT_ID/catalog-assets/images/
Xác minh quá trình tải lên (bạn sẽ thấy danh sách các tệp .png):
gcloud storage ls gs://fashion-app-$PROJECT_ID/catalog-assets/images/
6. Định cấu hình tệp .env
cd ~/fashion_app_demo/adk_backend
cat > .env << EOF
GOOGLE_CLOUD_PROJECT=$PROJECT_ID
GCS_BUCKET=fashion-app-$PROJECT_ID
EOF
7. Xác thực bằng Thông tin xác thực mặc định của ứng dụng
Bạn phải chạy lệnh này trước khi bắt đầu chạy phần phụ trợ cục bộ. Phần phụ trợ Go sử dụng ADC để xác thực mọi lệnh gọi đến Vertex AI (Gemini) và Cloud Storage. Nếu không có ADC, phần phụ trợ sẽ khởi động nhưng mọi yêu cầu dùng thử đều sẽ không thành công với 401 CREDENTIALS_MISSING.
Một thông tin đăng nhập có hiệu lực cho cả hai dịch vụ. Chạy hai lệnh sau theo thứ tự:
# 1. Log in (opens a browser; in Cloud Shell, paste the verification code back)
gcloud auth application-default login
# 2. Attach your project as the quota / billing project for ADC
gcloud auth application-default set-quota-project $(gcloud config get-value project)
Xác minh rằng ADC hoạt động bình thường:
gcloud auth application-default print-access-token | head -c 20 && echo "..."
Bạn sẽ thấy khoảng 20 ký tự của mã thông báo, theo sau là .... Nếu xảy ra lỗi, tức là bạn chưa đăng nhập được – hãy chạy lại bước 1.
4. 🏗️ Tổng quan về cấu trúc
Giờ đây, khi môi trường đã sẵn sàng, hãy tìm hiểu cách hệ thống hoạt động trước khi xem xét mã.
Hệ thống bốn tác nhân
Phần phụ trợ được xây dựng dưới dạng một hệ thống nhiều tác nhân bằng ADK (Bộ công cụ phát triển tác nhân) cho Go. Bốn tác nhân hoạt động cùng nhau, mỗi tác nhân có một trách nhiệm cụ thể:
┌──────────────┐
│ Root Agent │ ← Routes requests to the right agent
│ (gemini-3- │
│ flash- │
│ preview) │
└──────┬───────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌────────────────┐ ┌──────────┐ ┌────────────────┐
│ Fitting Room │ │ Catalog │ │ Stylist │
│ Agent │ │ Agent │ │ Agent │
│ (gemini-3.1- │ │ (gemini- │ │ (gemini-3.1- │
│ pro-preview) │ │ 3-flash-│ │ pro-preview) │
│ │ │ preview)│ │ │
│ Tools: │ │ │ │ Tools: │
│ • fitting_tool │ │ Tools: │ │ • fitting_tool │
│ • getProduct │ │ • list │ │ • getProduct │
│ Image │ │ Products │ │ Image │
│ • catalog_agent│ │ • get │ │ • catalog_agent│
│ (delegation) │ │ Product │ │ (delegation) │
│ │ │ Image │ │ • generate_ │
└────────────────┘ └──────────┘ │ outfit_image │
└────────────────┘
Tác nhân | Mô hình | Vai trò |
Root Agent |
| Cảnh sát giao thông. Đọc tin nhắn của người dùng và uỷ quyền cho nhân viên hỗ trợ chuyên trách phù hợp. Sử dụng một mô hình nhanh, gọn nhẹ vì chỉ cần đưa ra quyết định định tuyến. |
Catalog Agent |
| Chuyên gia sản phẩm. Tải danh mục sản phẩm từ một tệp YAML và trả lời các câu hỏi về sản phẩm. Ngoài ra, nó còn có dung lượng nhẹ – chỉ tra cứu dữ liệu. |
Fitting Room Agent |
| Chuyên gia về tính năng thử ảo. Lấy ảnh người dùng và hình ảnh sản phẩm rồi tạo ra một hình ảnh kết hợp cho thấy người đó đang mặc sản phẩm đó. Sử dụng một mô hình có nhiều chức năng hơn vì cần suy luận về hình ảnh. |
Stylist Agent |
| Tư vấn thời trang. Dựa trên địa điểm, dịp và sở thích, công cụ này sẽ chọn ra 3 tổ hợp trang phục trong danh mục. Có thể tạo hình ảnh dùng thử cho từng trang phục. Ngoài ra, mô hình này còn sử dụng mô hình có khả năng suy luận sáng tạo. |
Điểm truy cập: main.go
Mọi thứ bắt đầu ở main.go, nơi kết nối các tác nhân với nhau và khởi động máy chủ HTTP:
// main.go — simplified for clarity
func main() {
godotenv.Load() // Load .env file
// 1. Create the artifact storage (GCS-backed)
artifacts, _ := gcsartifact.NewService(ctx, bucket)
// 2. Build agents bottom-up (dependencies first)
catagent, _ := catalog.NewCatalogAgent(apikey, "catalog/catalog.yaml")
fitagent, _ := fittingroom.NewFittingRoomAgent(apikey, catagent)
stylistAgent, _ := stylist.NewStylistAgent(apikey, catagent)
ragent, _ := rootagent.NewRootAgent(apikey, fitagent, catagent, stylistAgent)
// 3. Register all agents with a multi-loader
loader, _ := agent.NewMultiLoader(ragent, fitagent, catagent, stylistAgent)
// 4. Create the ADK REST server
restHandler, _ := adkrest.NewServer(adkrest.ServerConfig{
SessionService: session.InMemoryService(),
MemoryService: memory.InMemoryService(),
AgentLoader: loader,
ArtifactService: artifacts,
})
// 5. Mount behind /api/ with CORS support
r := mux.NewRouter()
r.Use(tools.LocalhostCORS)
r.PathPrefix("/api/").Handler(
http.StripPrefix("/api", tools.LogHandler(restHandler)))
http.Server{Addr: ":8080", Handler: r}.ListenAndServe()
}
Một số điều quan trọng cần lưu ý:
- Các tác nhân được xây dựng từ dưới lên: Tác nhân danh mục được tạo trước vì cả tác nhân phòng thử đồ và tác nhân nhà tạo mẫu đều phụ thuộc vào tác nhân này (chúng uỷ quyền tra cứu sản phẩm cho tác nhân danh mục).
agent.NewMultiLoaderđăng ký cả 4 tác nhân để API REST có thể định tuyến đến bất kỳ tác nhân nào trong số đó theo tên.adkrest.NewServertự động cung cấp API REST – bạn không cần tự viết trình xử lý điểm cuối. ADK cung cấp cho bạn tính năng quản lý phiên, lưu trữ cấu phần phần mềm và thực thi tác nhân ngay từ đầu.session.InMemoryService()lưu trữ các phiên trong bộ nhớ. Điều này có nghĩa là các phiên sẽ bị mất nếu máy chủ khởi động lại, điều này không sao đối với bản minh hoạ. Trong quá trình phát hành chính thức, bạn sẽ sử dụng một bộ nhớ liên tục.gcsartifact.NewServicelưu trữ các cấu phần phần mềm (hình ảnh được tạo) trong Google Cloud Storage, vì vậy, các cấu phần phần mềm này sẽ tồn tại trong các yêu cầu và có thể được chia sẻ thông qua URI GCS.
5. 🤖 Tìm hiểu chuyên sâu về ADK (Bộ công cụ phát triển tác nhân)
ADK là gì?
Bộ công cụ phát triển tác nhân (ADK) là một khung nguồn mở của Google để xây dựng các tác nhân AI bằng Go (và Python/Java). Đây là lớp giữa ứng dụng của bạn và Gemini API.
Bạn có thể gọi trực tiếp Gemini API. Nhưng một khi ứng dụng của bạn cần:
- Tra cứu sản phẩm trong danh mục
- Tạo hình ảnh dựa trên ảnh của người dùng
- Ghi nhớ những trang phục đã được đề xuất trước đó
- Điều phối nhiều tác nhân AI
Bạn cần có cấu trúc. ADK cung cấp cấu trúc đó.
Vòng lặp của tác nhân
Mọi tác nhân ADK đều tuân theo một vòng lặp:
1. Receive a message (from user or another agent)
2. Think — the LLM reasons about what to do
3. Act — call a tool, delegate to a sub-agent, or respond
4. Return — send the result back
Vòng lặp này có thể lặp lại nhiều lần trong một yêu cầu. Ví dụ: tác nhân nhà tạo mẫu có thể:
- Nhận yêu cầu "Tạo phong cách cho tôi để đi nghỉ ở bãi biển"
- Gọi công cụ
catalog_agentđể lấy danh sách sản phẩm - Chọn 3 tổ hợp trang phục
- Gọi
fitting_toolcho từng trang phục để tạo hình ảnh - Trả về phản hồi JSON có cấu trúc
Các khái niệm cốt lõi (Có mã từ kho lưu trữ này)
Tác nhân LLM
Thành phần cơ bản. Được tạo bằng llmagent.New():
// From catalog/agent.go
agent, err := llmagent.New(llmagent.Config{
Name: "catalog_agent", // Unique identifier
Model: m, // Which LLM to use
Description: "An agent that can search and list products from the catalog",
Instruction: instructions, // Persona prompt (embedded from .md file)
Tools: []tool.Tool{listTool, imageTool}, // What the agent can do
})
Trường Instruction là tính cách của tác nhân – trường này cho biết LLM là ai và cách hành xử. Trong kho lưu trữ này, các hướng dẫn được viết dưới dạng tệp đánh dấu và được nhúng tại thời gian biên dịch bằng cách sử dụng chỉ thị //go:embed của Go:
//go:embed instructions.md
var instructions string
Điều này giúp giữ các câu lệnh dưới dạng các tài liệu riêng biệt, có thể tạo phiên bản thay vì các chuỗi nội tuyến.
Công cụ
Công cụ là các hàm Go mà LLM có thể gọi. ADK xử lý việc dịch giữa định dạng gọi công cụ của LLM và hàm Go mà bạn đã nhập:
// From catalog/agent.go
type ListProductsArgs struct{} // Input (can be empty)
type ListProductsResult struct {
Products []Product `json:"products"` // Output
}
func ListProducts(ctx tool.Context, args ListProductsArgs) (ListProductsResult, error) {
return ListProductsResult{Products: catalogProducts}, nil
}
// Register it:
listTool, _ := functiontool.New(functiontool.Config{
Name: "listProducts",
Description: "list all products in the catalog",
}, ListProducts)
ADK tự động tạo một giản đồ JSON từ các cấu trúc Go của bạn và gửi giản đồ đó đến LLM. Khi LLM quyết định gọi listProducts, ADK sẽ chuyển đổi tuần tự các đối số, gọi hàm của bạn và gửi kết quả trở lại.
Tham số tool.Context cho phép các công cụ truy cập vào các dịch vụ thời gian chạy của ADK – quan trọng nhất là các cấu phần phần mềm:
// Save an image as an artifact
ctx.Artifacts().Save(ctx, "my_image", imagePart)
// Load an artifact
resp, _ := ctx.Artifacts().Load(ctx, "my_image")
Uỷ quyền cho tác nhân phụ
Một tác nhân có thể sử dụng tác nhân khác làm công cụ thông qua agenttool.New():
// From fittingroom/agent.go
Tools: []tool.Tool{
loadartifactstool.New(), // List available artifacts
imgtool, // Get product images
agenttool.New(catalogAgent, nil), // Delegate to catalog agent
fittingTool, // Generate try-on image
},
Khi cần thông tin sản phẩm, nhân viên phòng thử đồ có thể gọi nhân viên danh mục như thể đó là một công cụ thông thường. LLM sẽ thấy công cụ này trong danh sách công cụ và có thể quyết định gọi công cụ đó.
Phiên
Phiên theo dõi nhật ký trò chuyện. API REST của ADK sẽ tự động quản lý các khoá này:
POST /api/apps/{appName}/users/{userId}/sessions → Creates a new session
POST /api/run (with sessionId) → Runs agent within that session
Một quyết định quan trọng về thiết kế trong ứng dụng này: phòng thử đồ tạo một phiên mới cho mỗi yêu cầu (mỗi lần thử đồ là độc lập), trong khi nhà tạo mẫu sử dụng lại cùng một phiên (vì vậy, ứng dụng sẽ ghi nhớ các đề xuất trước đó và có thể tinh chỉnh dựa trên ý kiến phản hồi).
Tiểu bang
Trạng thái là một kho khoá-giá trị được gắn vào một phiên. Các tác nhân đọc và ghi trạng thái để điều phối:
// Write to state
ctx.State().Set("previously_used_products", "[\"id_bomber\",\"id_hat\"]")
// Read from state
val, err := ctx.State().Get("previously_used_products")
Tác nhân tạo kiểu sử dụng trạng thái để ghi nhớ những sản phẩm mà tác nhân đã đề xuất, vì vậy, tác nhân sẽ chọn những sản phẩm khác vào lần tiếp theo.
Tệp phần mềm
Artifact là các đối tượng nhị phân được đặt tên (thường là hình ảnh) được lưu trữ theo phiên. Không giống như các câu trả lời dạng văn bản, các câu trả lời dạng hình ảnh được lưu trữ riêng và được tìm nạp theo tên:
// Save a generated image as an artifact
artName := fmt.Sprintf("generated_fitting_%s_%s", ctx.InvocationID(), uuid.NewString()[:8])
ctx.Artifacts().Save(ctx, artName, imagePart)
// The frontend fetches it via:
// GET /api/apps/{app}/users/{user}/sessions/{session}/artifacts/{artName}
Điều này giúp các phản hồi có dung lượng nhỏ – tác nhân phần mềm chỉ trả về tên của cấu phần phần mềm và giao diện người dùng tìm nạp riêng dữ liệu hình ảnh nhị phân.
Lệnh gọi lại
Lệnh gọi lại là các lệnh gọi chạy tại những điểm cụ thể trong vòng lặp của tác nhân. Chúng có thể kiểm tra, sửa đổi hoặc rút ngắn quá trình thực thi:
llmagent.Config{
// Runs before the agent starts — used to save uploaded images
BeforeAgentCallbacks: []agent.BeforeAgentCallback{SaveIncomingBlobs},
// Runs before each LLM call — used to inject context
BeforeModelCallbacks: []llmagent.BeforeModelCallback{
ExtractAndInjectUserImage,
InjectPreviousProducts,
},
// Runs after each LLM response — used to extract data
AfterModelCallbacks: []llmagent.AfterModelCallback{SaveSelectedProducts},
}
Nếu một lệnh gọi lại trả về phản hồi không phải là giá trị rỗng, thì hành vi mặc định sẽ bị bỏ qua. Ví dụ: một BeforeModelCallback trả về phản hồi được lưu vào bộ nhớ đệm sẽ bỏ qua hoàn toàn lệnh gọi LLM thực tế.
Thực thi giản đồ JSON
Cả hai tác nhân phòng thử đồ và nhà tạo mẫu đều buộc LLM phản hồi bằng JSON có cấu trúc:
GenerateContentConfig: &genai.GenerateContentConfig{
ResponseMIMEType: "application/json",
ResponseJsonSchema: fittingSchemaMap(), // Defines the expected structure
}
Điều này đảm bảo giao diện người dùng Flutter luôn nhận được dữ liệu có thể phân tích cú pháp, chứ không phải văn bản dạng tự do.
Catalog Agent: Ví dụ đơn giản nhất
Tác nhân danh mục (catalog/agent.go) là tác nhân đơn giản nhất trong hệ thống – một điểm xuất phát tốt để tìm hiểu các mẫu ADK.
Công cụ này có 2 chức năng:
listProducts– Trả về toàn bộ danh mục sản phẩm từ một tệp YAMLgetProductImage– Tải hình ảnh sản phẩm từ GCS (hoặc dự phòng cục bộ) và lưu dưới dạng một cấu phần phần mềm
Công cụ getProductImage cho thấy một mẫu quan trọng – tải nhiều nguồn bằng cách lưu trữ tạm thời các cấu phần phần mềm:
// From tools/imagetool.go — simplified
func GetProductImage(ctx tool.Context, args GetProductImageArgs) (GetProductImageResult, error) {
// First: check if it's already an artifact
_, err := ctx.Artifacts().Load(ctx, args.ImageName)
if err == nil {
return GetProductImageResult{Image: args.ImageName}, nil // Cache hit!
}
// Second: try loading from GCS bucket
rc, err := client.Bucket(bucket).Object("catalog-assets/images/" + args.ImageName).NewReader(ctx)
if err != nil {
// Third: fall back to local filesystem
localData, _ := os.ReadFile("../flutter_frontend/assets/images/" + args.ImageName)
ctx.Artifacts().Save(ctx, args.ImageName, &genai.Part{InlineData: ...})
return GetProductImageResult{Image: args.ImageName}, nil
}
// Save to artifact cache and return
ctx.Artifacts().Save(ctx, args.ImageName, &genai.Part{InlineData: ...})
return GetProductImageResult{Image: args.ImageName}, nil
}
Công cụ này sẽ thử các cấu phần phần mềm trước, sau đó đến GCS rồi đến các tệp cục bộ. Sau khi được tải, hình ảnh sẽ được lưu vào bộ nhớ đệm dưới dạng một cấu phần phần mềm để các lệnh gọi tiếp theo diễn ra ngay lập tức.
6. 🧪 AI Pipeline: Các tác nhân trong thực tế
Bây giờ, hãy cùng tìm hiểu về 2 tác nhân tinh vi nhất – những tác nhân thực sự tạo ra hình ảnh tạo sinh và tuyển chọn trang phục.
6.1 The Fitting Room Agent
Tệp:
adk_backend/fittingroom/agent.go
Tác nhân phòng thử đồ là công cụ đằng sau tính năng "Thử quần áo ảo". Khi người dùng tải ảnh của họ lên và chọn một sản phẩm, công cụ này sẽ tạo ra một hình ảnh kết hợp về người đang mặc sản phẩm đó.
fitting_tool – Từng bước
Logic cốt lõi nằm trong hàm doFitting. Sau đây là những gì sẽ xảy ra khi nhân viên hỗ trợ gọi phương thức này:
Bước 1: Phân giải hình ảnh người dùng
func doFitting(ctx tool.Context, args FittingToolArgs) (FittingToolResult, error) {
if len(args.Accessories) > 2 {
args.Accessories = args.Accessories[:2] // Safety limit: max 2 items
}
var userPart *genai.Part
if strings.HasPrefix(args.UserImage, "gs://") {
// If we have a GCS URI from a previous fitting, use it directly
userPart = &genai.Part{FileData: &genai.FileData{
FileURI: args.UserImage,
MIMEType: gcsURIMimeType(args.UserImage),
}}
} else {
// Otherwise, load the image from artifact storage
userImgResp, err := ctx.Artifacts().Load(ctx, args.UserImage)
userPart = userImgResp.Part
}
Hình ảnh người dùng có thể đến từ 2 nguồn:
- Tên của một cấu phần phần mềm (chẳng hạn như
upload_abc123_1) – đây là lần tải lên ban đầu, được lưu bằng lệnh gọi lạiSaveIncomingBlobs - Một URI
gs://– đây là kết quả phù hợp được tạo trước đó, được lưu trữ trong GCS để sử dụng lại trên nhiều phiên
Thiết kế hai đường dẫn này là có chủ ý: khi tác nhân tạo kiểu tạo ra các bản thử trang phục sau này, tác nhân này sẽ sử dụng lại URL GCS từ kết quả phòng thử đồ ban đầu để danh tính của người dùng nhất quán trên tất cả các trang phục.
Bước 2: Xây dựng câu lệnh đa phương thức
parts := []*genai.Part{
genai.NewPartFromText(toolInstructions), // Identity preservation prompt
genai.NewPartFromText("Reference Person Photo:"),
userPart, // The user's photo
}
for _, acc := range args.Accessories {
accResp, _ := ctx.Artifacts().Load(ctx, acc) // Load product image artifact
parts = append(parts, genai.NewPartFromText("Product Image to Apply:"))
parts = append(parts, accResp.Part) // The product photo
}
toolInstructions (được nhúng từ tool_instructions.md) là yếu tố quan trọng – yếu tố này yêu cầu Gemini giữ nguyên danh tính của người dùng (khuôn mặt, dáng người, màu da, tóc) trong khi chỉ áp dụng trang phục. Nếu không có thiết kế câu lệnh này, mô hình có thể thay đổi diện mạo của người đó.
Bước 3: Gọi Gemini để tạo hình ảnh
client, _ := genai.NewClient(ctx, &genai.ClientConfig{
Backend: genai.BackendVertexAI, // Vertex AI endpoint
Project: os.Getenv("GOOGLE_CLOUD_PROJECT"), // From your .env
Location: "global", // Multi-region endpoint
})
resp, _ := client.Models.GenerateContent(ctx, "gemini-2.5-flash-image",
[]*genai.Content{genai.NewContentFromParts(parts, "user")},
&genai.GenerateContentConfig{
ResponseModalities: []string{"TEXT", "IMAGE"}, // Request both text and image output
Temperature: genai.Ptr(float32(0.2)), // Low temperature for consistency
})
Cả 4 tác nhân và công cụ tạo hình ảnh đều dùng chung một đường dẫn xác thực: Backend: genai.BackendVertexAI có mã dự án, được xác thực thông qua Thông tin xác thực mặc định của ứng dụng. Các mô hình điều phối (gemini-3.1-pro-preview, gemini-3-flash-preview) và mô hình hình ảnh (gemini-2.5-flash-image) đều nằm sau cùng một điểm cuối Vertex AI, đồng thời cùng một ADC cũng cho phép truy cập vào Cloud Storage – một thông tin đăng nhập cho mỗi lệnh gọi.
Bước 4: Lưu kết quả
// Find the image in the response (may contain both text + image parts)
var genPart *genai.Part
for _, p := range resp.Candidates[0].Content.Parts {
if p.InlineData != nil {
genPart = p
break
}
}
// Save as an ADK artifact
artName := fmt.Sprintf("generated_fitting_%s_%s", ctx.InvocationID(), uuid.NewString()[:8])
ctx.Artifacts().Save(ctx, artName, genPart)
// Also upload to GCS for cross-session reuse
objectName := fmt.Sprintf("generated-fittings/%s.jpg", ctx.InvocationID())
w := storageClient.Bucket(bucket).Object(objectName).NewWriter(ctx)
w.Write(genPart.InlineData.Data)
gcsURI := fmt.Sprintf("gs://%s/%s", bucket, objectName)
return FittingToolResult{ArtifactName: artName, GCSUrl: gcsURI}, nil
Tính năng lưu kép (hiện vật + GCS) là chìa khoá để chuyển giao khách hàng giữa phòng thử đồ và nhà tạo mẫu. Đối tượng này cung cấp quyền truy cập ngay lập tức trong phiên hiện tại, trong khi URI GCS cho phép nhà tạo kiểu (chạy trong một phiên khác) tham chiếu cùng một hình ảnh sau này.
Lệnh gọi lại SaveIncomingBlobs
Trước khi tác nhân bắt đầu suy luận, BeforeAgentCallback này sẽ chạy để lưu mọi hình ảnh mà người dùng tải lên:
func SaveIncomingBlobs(ctx agent.CallbackContext) (*genai.Content, error) {
for pindex, p := range ctx.UserContent().Parts {
if p.InlineData != nil {
aname := fmt.Sprintf("upload_%s_%d", ctx.InvocationID(), pindex)
ctx.Artifacts().Save(ctx, aname, p)
}
}
return nil, nil // Return nil to proceed with normal agent execution
}
Bằng cách trả về (nil, nil), lệnh gọi lại sẽ báo hiệu "Tôi đã hoàn tất quá trình tiền xử lý – giờ hãy chạy tác nhân như bình thường". Nếu trả về nội dung không phải là giá trị rỗng, thì thao tác này sẽ bỏ qua hoàn toàn tác nhân.
6.2 Tác nhân tạo mẫu
Tệp:
adk_backend/stylist/agent.go
Tác nhân tạo kiểu là tác nhân phức tạp nhất trong hệ thống. Tính năng này tuyển chọn các đề xuất trang phục dành riêng cho bạn và hỗ trợ tinh chỉnh lặp lại thông qua cuộc trò chuyện.
Ba cuộc gọi lại – Ký ức của nhà tạo mẫu
Trình tạo kiểu sử dụng 3 lệnh gọi lại để duy trì ngữ cảnh trong các cuộc trò chuyện nhiều lượt:
Lệnh gọi lại 1:
InjectPreviousProducts (BeforeModel)
Vấn đề: Nếu người dùng nói "cho tôi xem các lựa chọn khác", thì LLM có thể đề xuất lại các sản phẩm tương tự vì LLM không theo dõi những gì đã đề xuất.
Giải pháp: Sau mỗi phản hồi, mã sản phẩm sẽ được lưu vào trạng thái phiên. Trước lệnh gọi LLM tiếp theo, lệnh gọi lại này sẽ đọc các thông tin đó và chèn một gợi ý:
func InjectPreviousProducts(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) {
prev, err := ctx.State().Get(stateKeyPreviousProducts) // Read from session state
if err != nil {
return nil, nil // No previous state — first run
}
// Append hint to the user's message
for i := len(req.Contents) - 1; i >= 0; i-- {
if req.Contents[i].Role == "user" {
req.Contents[i].Parts = append(req.Contents[i].Parts,
genai.NewPartFromText(fmt.Sprintf(
"IMPORTANT: You previously suggested these products: %s. "+
"You MUST pick DIFFERENT complementary products this time.", prev)))
break
}
}
return nil, nil // Continue to LLM call
}
Lệnh gọi lại 2:
ExtractAndInjectUserImage (BeforeModel)
Vấn đề: Khi người dùng cung cấp ý kiến phản hồi ("make it more casual" – làm cho câu trả lời bớt trang trọng hơn), thông báo tiếp theo không bao gồm lại ảnh của người dùng. Nhưng công cụ điều chỉnh cần có thông tin này.
Giải pháp: Trong yêu cầu đầu tiên, lệnh gọi lại này sẽ trích xuất thông tin tham chiếu hình ảnh người dùng và lưu thông tin đó vào trạng thái. Trong các yêu cầu tiếp theo, nó sẽ chèn lại mã này:
func ExtractAndInjectUserImage(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) {
var foundImgStr string
// Search for user image in the latest message
for i := len(req.Contents) - 1; i >= 0; i-- {
if req.Contents[i].Role == "user" {
for _, part := range req.Contents[i].Parts {
if strings.Contains(part.Text, "User try-on base image") {
foundImgStr = part.Text // Found the GCS URI reference
}
}
break
}
}
if foundImgStr != "" {
ctx.State().Set(stateKeyUserImageStr, foundImgStr) // Save for later
} else {
// Not in current message — retrieve from state and inject
val, _ := ctx.State().Get(stateKeyUserImageStr)
if savedImgStr, ok := val.(string); ok {
// Inject into the latest user message
req.Contents[last].Parts = append(req.Contents[last].Parts,
genai.NewPartFromText("REMINDER: Use this image: " + savedImgStr))
}
}
return nil, nil
}
Callback 3:
SaveSelectedProducts (AfterModel)
Sau khi LLM phản hồi bằng các đề xuất về trang phục, lệnh gọi lại này sẽ phân tích cú pháp JSON để trích xuất mã sản phẩm và lưu mã sản phẩm cho lệnh gọi lại InjectPreviousProducts sử dụng vào lần tiếp theo:
func SaveSelectedProducts(ctx agent.CallbackContext, resp *model.LLMResponse, respErr error) (*model.LLMResponse, error) {
for _, part := range resp.Content.Parts {
ids := extractProductIDs(part.Text) // Parse JSON → extract product IDs
if len(ids) > 0 {
data, _ := json.Marshal(ids)
ctx.State().Set(stateKeyPreviousProducts, string(data)) // Save to state
}
}
return nil, nil // Don't modify the response
}
Khi kết hợp với nhau, 3 lệnh gọi lại này sẽ tạo ra một vòng lặp phản hồi:
Request 1: User sends styling request + user image
→ ExtractAndInjectUserImage SAVES image to state
→ LLM generates 3 outfits
→ SaveSelectedProducts SAVES product IDs to state
Request 2: User says "make it more casual"
→ ExtractAndInjectUserImage INJECTS saved image into prompt
→ InjectPreviousProducts INJECTS "don't reuse these IDs"
→ LLM generates 3 NEW outfits
→ SaveSelectedProducts UPDATES product IDs in state
6.3 Tác nhân gốc
Tệp:
adk_backend/rootagent/agent.go
Tác nhân đơn giản nhất – chỉ có 31 dòng:
func NewRootAgent(project string, fittingAgent, catalogAgent, stylistAgent agent.Agent) (agent.Agent, error) {
m, _ := gemini.NewModel(ctx, "gemini-3-flash-preview", &genai.ClientConfig{
Backend: genai.BackendVertexAI,
Project: project,
Location: "global",
})
return llmagent.New(llmagent.Config{
Name: "root_agent",
Model: m,
Description: "A root agent that delegates to other agents",
Instruction: "You are a helpful shopping assistant. If the user asks about fitting " +
"items or generating images, delegate to the fitting room agent. If the user " +
"asks about products, delegate to the catalog agent. If the user asks for " +
"styling advice, delegate to the stylist agent.",
SubAgents: []agent.Agent{fittingAgent, catalogAgent, stylistAgent},
})
}
Công cụ này sử dụng gemini-3-flash-preview (mô hình nhanh nhất) vì các quyết định định tuyến rất đơn giản – LLM (mô hình ngôn ngữ lớn) chỉ cần đọc ý định của người dùng và chọn đúng tác nhân. Không cần dùng công cụ; SubAgents sẽ tự động xử lý việc uỷ quyền.
7. 📱 Cấu trúc giao diện người dùng Flutter
Giao diện người dùng Flutter là một ứng dụng mua sắm bán lẻ có đầy đủ chức năng. Các tính năng AI nằm trong flutter_frontend/lib/workshop_tasks/, tách biệt với trải nghiệm mua sắm được tạo sẵn trong core_app/.
Mẫu MVVM
Ứng dụng tuân theo cấu trúc Model-View-ViewModel bằng gói Provider:
┌──────────────────┐ ┌────────────────────┐ ┌──────────────────┐
│ View (Widget) │◀───│ ViewModel (Provider)│◀───│ Service (HTTP) │
│ │ │ │ │ │
│ • Renders UI │ │ • Holds state │ │ • Makes API calls│
│ • User gestures │───▶│ • Business logic │───▶│ • Parses response│
│ • Listens for │ │ • notifyListeners() │ │ • Returns data │
│ state changes │ │ │ │ │
└──────────────────┘ └────────────────────┘ └──────────────────┘
Mỗi lớp đều có một vai trò rõ ràng:
- Mô hình: Các lớp dữ liệu như
Product,Outfit,StyleRequestvà các enum nhưTryOnState - ViewModel (
ChangeNotifier): Lưu giữ trạng thái hiện tại và truyền các thay đổi đến giao diện người dùng thông quanotifyListeners() - View (Tiện ích): Đăng ký ViewModel bằng
context.watchvà tạo lại khi trạng thái thay đổi() - Dịch vụ: Thực hiện các lệnh gọi HTTP đến phần phụ trợ ADK và trả về dữ liệu đã nhập
Lớp dịch vụ
Các dịch vụ được xác định là giao diện trừu tượng, có các phương thức triển khai dành riêng cho ADK:
// Abstract interface — defines WHAT the service does
abstract class TryItOnService {
Future<(Uint8List?, String?)> generateTryOnImage(
Uint8List userImageBytes,
Uint8List productImageBytes,
);
}
// Concrete implementation — defines HOW (via ADK REST API)
class AdkFittingRoomService implements TryItOnService { ... }
Việc tách biệt này có nghĩa là bạn có thể hoán đổi phần phụ trợ ADK cho Firebase AI, một dịch vụ mô phỏng hoặc bất kỳ cách triển khai nào khác mà không cần thay đổi phần còn lại của ứng dụng.
Mô hình API gồm 3 bước
Cả AdkFittingRoomService và AdkStylingService đều tuân theo cùng một mẫu để giao tiếp với phần phụ trợ ADK:
Bước 1: Tạo một phiên
Future<String> _createSession() async {
final url = Uri.parse(
'$_baseUrl/apps/${Uri.encodeComponent(_appName)}/users/$_userId/sessions');
final response = await _client.post(url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({}));
final body = jsonDecode(response.body) as Map<String, dynamic>;
return body['id'] as String; // Returns the session ID
}
Bước 2: Chạy tác nhân
Future<(String?, String?)> _runAgent({required String sessionId, ...}) async {
final requestBody = jsonEncode({
'appName': _appName,
'userId': _userId,
'sessionId': sessionId,
'newMessage': {
'role': 'user',
'parts': [
{'text': 'Generate a virtual try-on...'},
{'inlineData': {'mimeType': 'image/jpeg', 'data': base64Encode(userImageBytes)}},
{'inlineData': {'mimeType': 'image/png', 'data': base64Encode(productImageBytes)}},
],
},
});
final response = await _client.post(Uri.parse('$_baseUrl/run'), body: requestBody);
// Parse response events for artifact name and GCS URL...
}
Bước 3: Tìm nạp cấu phần phần mềm
Future<Uint8List?> _loadArtifact({required String sessionId, required String artifactName}) async {
final url = Uri.parse(
'$_baseUrl/apps/$_appName/users/$_userId/sessions/$sessionId/artifacts/$artifactName');
final response = await _client.get(url);
final part = jsonDecode(response.body) as Map<String, dynamic>;
final data = part['inlineData']['data'] as String;
return base64Decode(data); // Returns raw image bytes
}
Một điểm khác biệt quan trọng về thiết kế: dịch vụ phòng thử đồ tạo một phiên mới cho mỗi yêu cầu (_createSession() được gọi mỗi lần), trong khi dịch vụ tạo kiểu sử dụng lại cùng một phiên (_sessionId ??= await _createSession()) để cho phép cuộc trò chuyện nhiều lượt.
Quản lý trạng thái: TryItOnProvider
Tệp:
workshop_tasks/step_1_try_it_on/providers/try_it_on_provider.dart
TryItOnProvider quản lý toàn bộ quy trình dùng thử. Thư viện này sử dụng enum TryOnState làm máy trạng thái:
enum TryOnState { initial, imagePicked, generating, success, error }
class TryItOnProvider with ChangeNotifier {
TryOnState _state = TryOnState.initial;
Uint8List? _userImageBytes;
Uint8List? _generatedImage;
String? _errorMessage;
Quá trình chuyển đổi trạng thái riêng tư đảm bảo tính nhất quán – bạn không bao giờ cập nhật trạng thái mà không xoá dữ liệu cũ và thông báo cho giao diện người dùng:
void _setGenerating() {
_state = TryOnState.generating;
_errorMessage = null; // Clear any previous error
_wasLastGenerationCached = false;
notifyListeners(); // Tell the UI to rebuild
}
void _setSuccess(Uint8List image, {bool isCached = false}) {
_generatedImage = image;
_errorMessage = null;
_wasLastGenerationCached = isCached;
_state = TryOnState.success;
notifyListeners();
}
Phương thức tạo chính sẽ liên kết mọi thứ với nhau:
Future<String?> generateTryOnImage(String productImagePath) async {
final userImageBytes = _userImageBytes;
if (userImageBytes == null) {
_setError('No image selected.');
return _errorMessage;
}
// Check local cache first
final cachedImage = ImageUtils.getCachedImage(_sessionCache, userImageBytes, productUint8List);
if (cachedImage != null) {
_setSuccess(cachedImage, isCached: true);
return null;
}
_setGenerating(); // Triggers loading state in UI
try {
final (generatedBytes, gcsUrl) = await _aiService.generateTryOnImage(
userImageBytes, productUint8List);
if (generatedBytes != null) {
_fittingGcsUrl = gcsUrl; // Save for the stylist agent later
_setSuccess(generatedBytes);
}
} catch (e) {
_setError(e.toString());
}
return _state == TryOnState.success ? null : _errorMessage;
}
Giao diện người dùng: Màn hình dưới dạng Bộ định tuyến trạng thái
Tệp:
workshop_tasks/step_1_try_it_on/ui/2_try_it_on_screen.dart
Màn hình thử đồ dùng tính năng so khớp mẫu của Dart 3 với AnimatedSwitcher để định tuyến giữa các màn hình phụ dựa trên trạng thái của nhà cung cấp:
class TryItOnScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final tryOnProvider = context.watch<TryItOnProvider>();
return ScaffoldWithBackgroundNoise(
appBar: const TryOnAppBar(),
body: AnimatedSwitcher(
duration: AppDurations.medium,
child: switch (tryOnProvider.state) {
TryOnState.initial || TryOnState.error => const ChooseImageScreen(),
TryOnState.imagePicked || TryOnState.generating => LoadingScreen(
userImage: tryOnProvider.userImageBytes),
TryOnState.success => const FittingRoomScreen(),
},
),
);
}
}
context.watch đăng ký theo dõi nhà cung cấp. Bất cứ khi nào notifyListeners() được gọi, tiện ích này sẽ được tạo lại và AnimatedSwitcher sẽ chuyển đổi mượt mà giữa các màn hình. Không có Navigator.push – nội dung màn hình thay đổi tại chỗ dựa trên enum trạng thái.
Bàn giao cho AI tác nhân: Phòng thử đồ → Nhà tạo mẫu
Mẫu trải nghiệm người dùng thú vị nhất là cách ứng dụng truyền ngữ cảnh từ tác nhân phòng thử đồ đến tác nhân nhà tạo mẫu.
Trong 5_fitting_room.dart, sau khi hình ảnh thử đồ được tạo, nút "Tạo phong cách cho tôi" sẽ mở một biểu mẫu. Khi người dùng gửi:
// From 1_style_me_form_sheet.dart
Navigator.pop(context, StyleRequest(
location: _locationController.text.trim(),
occasion: _occasionController.text.trim(),
notes: _notesController.text.trim(),
gcsUserImageUrl: provider.fittingGcsUrl, // GCS URI from fitting result
userImageData: provider.fittingGcsUrl == null
? provider.userImageBytes : null, // Fallback to raw bytes
selectedProductId: provider.selectedProduct?.id, // Product they already tried on
selectedProductTitle: provider.selectedProduct?.title,
));
StyleRequest gói tất cả những gì nhà tạo mẫu cần:
- Vị trí và dịp – ngữ cảnh văn bản để tạo kiểu
- URL hình ảnh người dùng GCS – để nhà tạo mẫu có thể sử dụng lại chính xác hình ảnh người dùng
- Sản phẩm được chọn – để nhà tạo mẫu đưa sản phẩm đó vào mọi trang phục
Đây là bàn giao dựa trên tác nhân – chuyển ngữ cảnh đa phương thức một cách liền mạch từ tác nhân AI này sang tác nhân AI khác, trong đó người dùng chỉ thấy một biểu mẫu đơn giản.
Quy trình tạo kiểu: StylingProvider
Tệp:
workshop_tasks/step_2_style_me/providers/styling_provider.dart
StylingProvider đơn giản hơn TryItOnProvider vì nó uỷ quyền hầu hết độ phức tạp cho phần phụ trợ:
class StylingProvider with ChangeNotifier {
StylingState _state = StylingState.initial;
List<Outfit> _outfits = [];
Future<bool> getStyleSuggestions(StyleRequest request) async {
if (_state == StylingState.loading) return false; // Prevent spam
_currentRequest = request;
_setLoading();
try {
final suggestions = await _stylingService.getStyleSuggestions(request);
_setSuccess(suggestions);
return true;
} catch (e) {
_setError('Something went wrong.');
}
return false;
}
// Multi-turn refinement — uses the same session
Future<bool> refineWithFeedback(String feedback) async {
if (_state == StylingState.loading) return false;
_setLoading();
try {
final suggestions = await _stylingService.refineWithFeedback(feedback);
_setSuccess(suggestions);
return true;
} catch (e) {
_setError('Something went wrong.');
}
return false;
}
}
Phương thức refineWithFeedback sẽ gửi một tin nhắn văn bản thuần tuý đến cùng một phiên – các lệnh gọi lại InjectPreviousProducts và ExtractAndInjectUserImage của phần phụ trợ sẽ tự động xử lý tất cả hoạt động quản lý bối cảnh.
8. 🚀 Chạy ứng dụng trên thiết bị
Để có trải nghiệm mượt mà với Cloud Shell, phần phụ trợ Go sẽ phân phát ứng dụng web Flutter đã biên dịch từ cùng một cổng (8080). Một quy trình, một URL xem trước, không gặp vấn đề về nhiều nguồn gốc, không cần chỉnh sửa tệp cấu hình.
Trước khi bắt đầu – kiểm tra nhanh ADC
Phần phụ trợ cần có Thông tin xác thực mặc định của ứng dụng để gọi Vertex AI. Nếu đã hoàn tất bước 7 của quy trình thiết lập dự án trong phiên Cloud Shell này và Tài khoản Google này, thì bạn đã hoàn tất. Nếu bạn quay lại sau một thời gian nghỉ, chuyển đổi tài khoản hoặc không chắc chắn, hãy dành 5 giây để xác minh:
gcloud auth application-default print-access-token | head -c 20 && echo "..."
Nếu lệnh đó in ra khoảng 20 ký tự của một mã thông báo, thì bạn đã hoàn tất. Nếu xảy ra lỗi, hãy chạy lại bước 7 của quy trình thiết lập dự án:
gcloud auth application-default login
gcloud auth application-default set-quota-project $(gcloud config get-value project)
Bạn sẽ sử dụng 2 thiết bị đầu cuối Cloud Shell:
- Terminal A – chạy liên tục phần phụ trợ (
./run.sh). Hãy để cửa sổ này mở. - Terminal B – chạy bản dựng web Flutter một lần (
flutter build web). Thoát khi hoàn tất.
Không cần quan tâm đến thứ tự, bạn có thể bắt đầu bằng cách nào trước cũng được. Nhưng để có trải nghiệm chạy lần đầu mượt mà nhất, hãy tạo Flutter trước để phần phụ trợ có giao diện người dùng để phân phát ngay từ khi bắt đầu.
1. Terminal B – Tạo gói web Flutter (một lần)
Mở một thẻ Cloud Shell mới (biểu tượng + ở đầu bảng điều khiển dòng lệnh), sau đó:
cd ~/fashion_app_demo/flutter_frontend
flutter pub get
flutter build web
Thao tác này sẽ tạo ra flutter_frontend/build/web/ – một thư mục chứa các tệp tĩnh (HTML, JS, tài sản) – và thoát khi hoàn tất. Phần phụ trợ sẽ phân phát các tệp này ngay khi thấy thư mục tồn tại.
2. Terminal A – Start the Backend (long-running) (Terminal A – Bắt đầu phần phụ trợ (chạy trong thời gian dài))
Trong cửa sổ Cloud Shell ban đầu:
cd ~/fashion_app_demo/adk_backend
./run.sh
Bạn sẽ thấy một số dòng mã như:
Serving Flutter web build from ../flutter_frontend/build/web
Để thiết bị đầu cuối này chạy – phần phụ trợ sẽ hoạt động miễn là run.sh còn hoạt động. Để dừng, hãy nhấn vào Ctrl+C.
Máy chủ hiển thị mọi thứ trên cổng 8080:
/– Ứng dụng web Flutter (giao diện người dùng mua sắm)/api/– Các điểm cuối REST của ADK (do ứng dụng Flutter gọi)- Giao diện người dùng ADK Dev – cũng ở
/khi không có bản dựng Flutter; hữu ích cho việc gỡ lỗi trực tiếp tác nhân
3. Mở bản xem trước trên web
- Trong Cloud Shell, hãy nhấp vào biểu tượng Xem trước trên web (ở trên cùng bên phải) → Xem trước trên cổng 8080
- Ứng dụng mua sắm Flutter sẽ tải trong một thẻ mới
- Duyệt xem danh mục sản phẩm rồi chọn một mặt hàng
- Nhấn vào biểu tượng hình người (👤) để bắt đầu quy trình dùng thử
- Tải ảnh lên và xem AI tạo ra hình ảnh thử đồ
- Nhấn vào "Tạo phong cách cho tôi" để nhận đề xuất về trang phục
- Nhập ý kiến phản hồi tiếp theo, chẳng hạn như "viết lại sao cho gần gũi hơn" – tinh chỉnh trong cùng một phiên
9. ☁️ Triển khai lên Cloud Run
Gói bản dựng Flutter vào phần phụ trợ
Vùng chứa Cloud Run gửi cả API và giao diện người dùng từ một hình ảnh. Sao chép bản dựng web Flutter vào adk_backend/flutter_web/ – đây là đường dẫn đầu tiên mà máy chủ Go kiểm tra khi chọn giao diện người dùng để phân phát:
cd ~/fashion_app_demo/flutter_frontend
flutter build web
rm -rf ../adk_backend/flutter_web
cp -r build/web ../adk_backend/flutter_web
(Nếu đã lặp lại cục bộ, bạn có thể đã có build/web từ bước Chạy cục bộ. Bạn vẫn có thể chạy lại flutter build web.)
Triển khai phần phụ trợ (Phục vụ API + giao diện người dùng)
cd ~/fashion_app_demo/adk_backend
gcloud run deploy fashion-app-backend \
--source . \
--region us-central1 \
--allow-unauthenticated \
--set-env-vars "GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GCS_BUCKET=fashion-app-$PROJECT_ID" \
--memory 1Gi \
--cpu 2 \
--timeout 300s \
--min-instances 0 \
--max-instances 3
Khi quá trình triển khai hoàn tất, bạn sẽ nhận được một URL dịch vụ như https://fashion-app-backend-xyz-uc.a.run.app. Mở ứng dụng này trong trình duyệt – ứng dụng mua sắm Flutter tải từ / và các lệnh gọi API của ứng dụng này sẽ chuyển đến /api/ trên cùng một máy chủ lưu trữ. Không cần chỉnh sửa cấu hình giao diện người dùng, không có khoá API nào được truyền.
Xác minh quá trình triển khai
Mở URL Cloud Run trong trình duyệt và chạy toàn bộ quy trình:
- Duyệt xem → Chọn một sản phẩm
- Thử đồ → Tải ảnh của bạn lên → Xem hình ảnh do AI tạo
- Tạo phong cách cho tôi → Điền thông tin về vị trí/dịp → Xem trang phục được tuyển chọn
- Phản hồi → Nhập "thay đổi trang phục cho phù hợp với hoàn cảnh bình thường hơn" → Xem trang phục mới
- Thêm vào giỏ hàng → Hoàn tất quy trình mua sắm
10. 🎉 Kết luận
Sản phẩm bạn đã tạo
Bạn đã khám phá trải nghiệm bán lẻ trọn vẹn dựa trên AI với:
- ✅ Một phần phụ trợ nhiều tác nhân với 4 tác nhân chuyên biệt hoạt động cùng nhau
- ✅ Phòng thử đồ ảo tạo ra hình ảnh thử đồ được cá nhân hoá
- ✅ Nhà tạo mẫu AI có thể chọn trang phục và tinh chỉnh trang phục thông qua cuộc trò chuyện
- ✅ Một ứng dụng Flutter nhiều nền tảng kết nối với phần phụ trợ của tác nhân
- ✅ Triển khai Cloud Run để lưu trữ không máy chủ có khả năng mở rộng
Khái niệm chính
Khái niệm | Nơi bạn thấy nội dung đó |
Điều phối nhiều tác nhân ADK | Định tuyến tác nhân gốc đến phòng thử đồ, danh mục và tác nhân tạo kiểu |
Tính năng tạo hình ảnh đa phương thức của Gemini |
|
Trạng thái phiên cho AI đàm thoại | Phiên làm việc lại của nhà tạo mẫu để nhận ý kiến phản hồi lặp lại |
Lưu trữ cấu phần phần mềm cho dữ liệu nhị phân | Tách bộ nhớ lưu trữ hình ảnh khỏi các câu trả lời bằng văn bản |
Lệnh gọi lại cho logic phần mềm trung gian |
|
MVVM + Provider trong Flutter |
|
Bàn giao cho trợ lý AI |
|
Các bước tiếp theo
- 🎨 Tuỳ chỉnh câu lệnh cho trợ lý – chỉnh sửa
instructions.mdđể thay đổi tính cách của nhà tạo mẫu - 🛍️ Thêm sản phẩm khác – cập nhật
catalog.yamlbằng các mặt hàng mới - 📱 Tạo quảng cáo cho thiết bị di động – chạy
flutter build ioshoặcflutter build apk - 🔄 Thêm các phiên liên tục – thay thế
InMemoryServicebằng một quy trình triển khai dựa trên cơ sở dữ liệu - 🔒 Thêm quy trình xác thực – bảo mật điểm cuối Cloud Run bằng IAM
Tài nguyên
- Tài liệu về ADK – Tài liệu chính thức về Bộ công cụ phát triển đại lý
- Mã nguồn ADK Go – Kho lưu trữ GitHub
- Tài liệu tham khảo về gói ADK Go – Tài liệu tham khảo API
- Tài liệu về Gemini API – Các tính năng và hướng dẫn về mô hình
- Gói nhà cung cấp Flutter – Tài liệu quản lý trạng thái
- Tài liệu về Cloud Run – Hướng dẫn triển khai và mở rộng quy mô