👗 Xây dựng phòng thử đồ ảo và chuyên gia tạo phong cách bằng AI bằng Flutter, ADK Go và Gemini

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.

  1. 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 đó.
  2. 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

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

aiplatform.googleapis.com

Vertex AIfitting_tool gọi tính năng tạo hình ảnh của Gemini thông qua Vertex AI

storage.googleapis.com

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

run.googleapis.com

Cloud Run – lưu trữ phần phụ trợ dưới dạng một vùng chứa không máy chủ

cloudbuild.googleapis.com

Cloud Build – tạo các hình ảnh Docker từ nguồn

artifactregistry.googleapis.com

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

gemini-3-flash-preview

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

gemini-3-flash-preview

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

gemini-3.1-pro-preview

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

gemini-3.1-pro-preview

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.NewServer tự độ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.NewService lư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ể:

  1. Nhận yêu cầu "Tạo phong cách cho tôi để đi nghỉ ở bãi biển"
  2. Gọi công cụ catalog_agent để lấy danh sách sản phẩm
  3. Chọn 3 tổ hợp trang phục
  4. Gọi fitting_tool cho từng trang phục để tạo hình ảnh
  5. 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:

  1. listProducts – Trả về toàn bộ danh mục sản phẩm từ một tệp YAML
  2. getProductImage – 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ại SaveIncomingBlobs
  • 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, StyleRequest và 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 qua notifyListeners()
  • View (Tiện ích): Đăng ký ViewModel bằng context.watch() và 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ả AdkFittingRoomServiceAdkStylingService đề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 InjectPreviousProductsExtractAndInjectUserImage 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

  1. 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
  2. Ứng dụng mua sắm Flutter sẽ tải trong một thẻ mới
  3. Duyệt xem danh mục sản phẩm rồi chọn một mặt hàng
  4. Nhấn vào biểu tượng hình người (👤) để bắt đầu quy trình dùng thử
  5. Tải ảnh lên và xem AI tạo ra hình ảnh thử đồ
  6. Nhấn vào "Tạo phong cách cho tôi" để nhận đề xuất về trang phục
  7. 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:

  1. Duyệt xem → Chọn một sản phẩm
  2. Thử đồ → Tải ảnh của bạn lên → Xem hình ảnh do AI tạo
  3. 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
  4. 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
  5. 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

fitting_tool kết hợp ảnh người dùng với hình ảnh sản phẩm

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

SaveIncomingBlobs, InjectPreviousProducts, SaveSelectedProducts

MVVM + Provider trong Flutter

TryItOnProviderStylingProvider với ChangeNotifier

Bàn giao cho trợ lý AI

StyleRequest truyền ngữ cảnh đa phương thức giữa các tác nhân

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.yaml bằ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 ios hoặc flutter build apk
  • 🔄 Thêm các phiên liên tục – thay thế InMemoryService bằ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