1. Введение
Что вы построите
В этом практическом занятии вы примерите на себя роль разработчика, создающего Fashion App — приложение для покупок на Flutter для вымышленного розничного бренда. Ваша задача: добавить две функции на основе искусственного интеллекта, которые преобразят опыт онлайн-шопинга.
- Виртуальная примерочная — пользователь загружает свою фотографию, выбирает предмет одежды и видит сгенерированное искусственным интеллектом изображение себя в этом предмете.
- Искусственный интеллект-стилист — на основе местоположения пользователя, случая и стилевых предпочтений, агент с искусственным интеллектом подбирает готовые варианты нарядов, которые пользователь может уточнить в ходе беседы.
Идея проста: когда люди примеряют одежду в примерочной, они с гораздо большей вероятностью её покупают. А в интернете? Это просто гадание. Этот проект с помощью искусственного интеллекта решает эту проблему.
Архитектура вкратце
Flutter App ──── HTTP/REST ────▶ ADK Go Backend
│
┌──────────┼──────────┐
Fitting Room Stylist Catalog
Agent Agent Agent
│
Gemini API + Cloud Storage
Основные технологии
Компонент | Технологии | Цель |
Агентская структура | ADK (Agent Development Kit for Go) | Многоагентная оркестровка, сессии, артефакты |
Логическое мышление агента (Pro) | Gemini 3.1 Pro Preview | Обеспечивает работу примерочных и стилистов. |
Логическое мышление агента (Flash) | Предварительный просмотр Gemini 3 Flash | Обеспечивает работу корневого и каталогового агентов (легковесная маршрутизация/поиск). |
Генерация изображений | Изображение со вспышкой Gemini 2.5 | Создает изображения для примерки и комплектов одежды. |
Внешний интерфейс | Флаттер (Дротик) | Кроссплатформенное приложение (веб, iOS, Android) |
Хранилище | Google Облачное хранилище | Сохраняет изображения товаров и созданные артефакты. |
Хостинг | Cloud Run | развертывание бессерверных контейнеров |
2. 📦 Предварительные условия и настройка Cloud Shell
1. Откройте редактор Cloud Shell.
👉 Откройте редактор Cloud Shell в своем браузере.
Если терминал не отображается внизу экрана:
- Нажмите «Вид» → «Терминал».
2. Настройка Flutter SDK
В Cloud Shell Flutter предустановлен по адресу /google/flutter . Поскольку этот каталог принадлежит другому системному пользователю, при первом запуске flutter вы столкнетесь с ошибкой fatal: detected dubious ownership . Добавьте его в список безопасных каталогов Git один раз:
git config --global --add safe.directory /google/flutter
Убедитесь, что Flutter находится в переменной PATH и работает:
flutter --version
При первом запуске загружается SDK Dart и собирается инструмент Flutter — подождите минуту. Вы должны увидеть что-то вроде Flutter 3.x • channel stable .
3. Клонируйте репозиторий.
cd ~
git clone https://github.com/gca-americas/fashion-app-demo
cd fashion_app_demo
4. Изучите структуру проекта.
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. ☁️ Настройка проекта Google Cloud
1. Создайте новый проект
gcloud projects create fashion-app-demo --name="Fashion App Demo"
gcloud config set project fashion-app-demo
2. Привязка платежного аккаунта
Перечислите свои платежные аккаунты:
gcloud billing accounts list
Посмотрите на
OPEN
В столбце должно быть указано True . Если указано False (что часто встречается при истекшем бесплатном пробном периоде), учетная запись закрыта, и вы ничего не сможете оплатить — перед продолжением перейдите к разделу устранения неполадок ниже.
Скопируйте ACCOUNT_ID учетной записи с OPEN: True (выглядит как 0X0X0X-0X0X0X-0X0X0X ) и свяжите ее с вашим проектом:
gcloud billing projects link fashion-app-demo \
--billing-account=YOUR_BILLING_ACCOUNT_ID
Проверьте ссылку:
gcloud billing projects describe fashion-app-demo
Вы должны видеть billingEnabled: true . Если вы видите billingEnabled: false даже после привязки, учетная запись закрыта ( OPEN: False ) — см. раздел устранения неполадок ниже.
3. Включите необходимые API.
gcloud services enable \
aiplatform.googleapis.com \
storage.googleapis.com \
run.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com
API | Цель |
| Vertex AI — инструмент |
| Облачное хранилище — хранит изображения из каталога товаров и результаты примерки. |
| Cloud Run — размещает бэкэнд в виде бессерверного контейнера. |
| Cloud Build — создает образы Docker из исходного кода. |
| Реестр артефактов — хранит созданные образы Docker. |
4. Создайте сегмент 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. Загрузите изображения из каталога продукции.
Инструмент getProductImage в бэкэнде считывает данные из gs://$GCS_BUCKET/catalog-assets/images/ Загрузите изображения из каталога по указанному пути:
cd ~/fashion_app_demo
gcloud storage cp flutter_frontend/assets/images/*.png \
gs://fashion-app-$PROJECT_ID/catalog-assets/images/
Проверьте загруженный файл (вы должны увидеть список файлов .png ):
gcloud storage ls gs://fashion-app-$PROJECT_ID/catalog-assets/images/
6. Настройте файл .env
cd ~/fashion_app_demo/adk_backend
cat > .env << EOF
GOOGLE_CLOUD_PROJECT=$PROJECT_ID
GCS_BUCKET=fashion-app-$PROJECT_ID
EOF
7. Аутентификация с использованием учетных данных приложения по умолчанию.
Это необходимо выполнить перед локальным запуском бэкенда. Бэкенд на Go использует ADC для аутентификации каждого вызова к Vertex AI (Gemini) и Cloud Storage. Без ADC бэкенд запустится, но каждый запрос на примерку завершится ошибкой 401 CREDENTIALS_MISSING .
Одни учетные данные подходят для обеих служб. Выполните следующие две команды по порядку:
# 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)
Убедитесь в исправности АЦП:
gcloud auth application-default print-access-token | head -c 20 && echo "..."
Вы должны увидеть токен длиной примерно в 20 символов, за которым следует ... . Если возникнет ошибка, значит, вход в систему не удался — повторите шаг 1.
4. 🏗️ Обзор архитектуры
Теперь, когда среда готова, давайте разберемся, как работает система, прежде чем переходить к коду.
Четырехагентная система
Бэкенд построен как многоагентная система с использованием ADK (Agent Development Kit) для Go. Четыре агента работают вместе, каждый со своей определённой обязанностью:
┌──────────────┐
│ 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 │
└────────────────┘
Агент | Модель | Роль |
Корневой агент | | Регулировщик дорожного движения. Читает сообщение пользователя и перенаправляет его нужному специалисту. Использует быструю и легковесную модель, поскольку ему нужно только принимать решения о маршрутизации. |
Агент по каталогам | | Эксперт по товарам. Загружает каталог товаров из YAML-файла и отвечает на запросы по товарам. К тому же, он очень простой — просто ищет данные. |
Агент примерочной | | Специалист по виртуальной примерке. Берет фотографию пользователя и изображение товара и создает составное изображение человека, который будет носить этот предмет. Использует более совершенную модель, поскольку ей необходимо анализировать изображения. |
Агент-стилист | | Модный консультант. Учитывая местоположение, случай и предпочтения, он подбирает 3 варианта комплектов одежды из каталога. Может генерировать изображения примерки для каждого комплекта. Также использует функциональную модель для творческого мышления. |
Точка входа: main.go
Всё начинается в main.go , который связывает агентов между собой и запускает 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()
}
Несколько важных моментов, на которые следует обратить внимание:
- Систему агентов формируют снизу вверх : агент каталога создается первым, поскольку от него зависят как агенты примерочной, так и агенты стилистов (они делегируют ему поиск товаров).
-
agent.NewMultiLoaderрегистрирует все четыре агента, чтобы REST API мог направлять запросы к любому из них по имени. -
adkrest.NewServerавтоматически предоставляет REST API — вам не нужно самостоятельно писать обработчики конечных точек. ADK обеспечивает управление сессиями, хранение артефактов и выполнение агентов «из коробки». -
session.InMemoryService()сохраняет сессии в памяти. Это означает, что сессии теряются при перезапуске сервера, что вполне приемлемо для демонстрации. В производственной среде следует использовать постоянное хранилище. -
gcsartifact.NewServiceхранит артефакты (сгенерированные изображения) в Google Cloud Storage, поэтому они сохраняются между запросами и могут быть переданы через URI GCS.
5. 🤖 Подробный анализ ADK (комплекта разработки агентов)
Что такое ADK?
Agent Development Kit (ADK) — это фреймворк с открытым исходным кодом от Google для создания агентов искусственного интеллекта на языке Go (а также Python/Java). Он является связующим звеном между вашим приложением и API Gemini.
Вы можете напрямую вызывать API Gemini. Но как только вашему приложению потребуется:
- Найдите товары в каталоге
- Генерировать изображения на основе фотографий пользователя.
- Вспомните, какие наряды были предложены ранее.
- Координировать действия нескольких агентов ИИ.
Вам нужна структура. ADK предоставляет эту структуру.
Петля агента
Каждый агент ADK следует циклу:
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
Этот цикл может повторяться несколько раз в рамках одного запроса. Например, агент-стилист может:
- Получите "Создайте свой стиль для пляжного отдыха"
- Воспользуйтесь инструментом
catalog_agent, чтобы получить список товаров. - Выберите 3 варианта комплектов одежды
- Для каждого комплекта одежды вызовите
fitting_tool, чтобы сгенерировать изображения. - Возвращает структурированный JSON-ответ.
Основные концепции (с кодом из этого репозитория)
Агенты LLM
Основной строительный блок. Создан с помощью 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
})
Поле Instruction описывает личность агента — оно сообщает LLM, кто он и как себя вести. В этом репозитории инструкции написаны в виде файлов Markdown и встраиваются во время компиляции с помощью директивы Go ` //go:embed :
//go:embed instructions.md
var instructions string
Это позволяет сохранять подсказки в виде отдельных, версионируемых документов, а не в виде встроенных строк.
Инструменты
Инструменты — это функции Go, которые может вызывать LLM. ADK обрабатывает преобразование между форматом вызова инструментов LLM и вашей типизированной функцией Go:
// 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 автоматически генерирует JSON-схему из ваших структур Go и отправляет её в LLM. Когда LLM решает вызвать listProducts , ADK десериализует аргументы, вызывает вашу функцию и отправляет результат обратно.
Параметр tool.Context предоставляет инструментам доступ к службам среды выполнения ADK, и прежде всего к артефактам :
// Save an image as an artifact
ctx.Artifacts().Save(ctx, "my_image", imagePart)
// Load an artifact
resp, _ := ctx.Artifacts().Load(ctx, "my_image")
Делегирование полномочий субагента
Агент может использовать другого агента в качестве инструмента с помощью 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
},
Когда сотруднику примерочной требуется информация о товаре, он может обратиться к сотруднику, отвечающему за каталог, как к обычному инструменту. Сотрудник, отвечающий за логистику, видит эту информацию в списке инструментов и может принять решение о её использовании.
Сессии
Сессии отслеживают историю переписки. REST API ADK автоматически управляет ими:
POST /api/apps/{appName}/users/{userId}/sessions → Creates a new session
POST /api/run (with sessionId) → Runs agent within that session
Ключевое дизайнерское решение в этом приложении: примерочная создает новую сессию для каждого запроса (каждая примерка независима), в то время как стилист использует одну и ту же сессию (что позволяет запоминать предыдущие предложения и корректировать их на основе отзывов).
Состояние
Состояние — это хранилище типа «ключ-значение», привязанное к сессии. Агенты считывают и записывают состояние для координации действий:
// Write to state
ctx.State().Set("previously_used_products", "[\"id_bomber\",\"id_hat\"]")
// Read from state
val, err := ctx.State().Get("previously_used_products")
Стилист использует состояние, чтобы запомнить, какие продукты он уже предлагал, поэтому в следующий раз он выберет другие.
Артефакты
Артефакты представляют собой именованные бинарные объекты (обычно изображения), хранящиеся в течение каждой сессии. В отличие от текстовых ответов, они хранятся отдельно и извлекаются по имени:
// 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}
Это позволяет сделать ответы легковесными — агент возвращает только имя артефакта, а фронтенд отдельно получает двоичные данные изображения.
Обратные звонки
Обратные вызовы — это механизмы, которые запускаются в определенных точках цикла агента. Они могут проверять, изменять или прерывать выполнение:
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},
}
Если функция обратного вызова возвращает ненулевой ответ, поведение по умолчанию пропускается. Например, функция BeforeModelCallback , возвращающая кэшированный ответ, полностью пропустит фактический вызов LLM.
Применение JSON-схемы
Как агенты примерочной, так и стилисты заставляют LLM отвечать в структурированном формате JSON:
GenerateContentConfig: &genai.GenerateContentConfig{
ResponseMIMEType: "application/json",
ResponseJsonSchema: fittingSchemaMap(), // Defines the expected structure
}
Это гарантирует, что интерфейс Flutter всегда будет получать данные, пригодные для анализа, а не произвольный текст.
Агент по каталогам: простейший пример
Агент каталога ( catalog/agent.go ) — это самый простой агент в системе, хорошая отправная точка для понимания шаблонов ADK.
В нём есть два инструмента:
-
listProducts— Возвращает полный каталог товаров из YAML-файла. -
getProductImage— Загружает изображение товара из GCS (или локального резервного источника) и сохраняет его как артефакт.
Инструмент getProductImage демонстрирует важную закономерность — загрузку из нескольких источников с кэшированием артефактов :
// 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
}
Инструмент сначала пытается загрузить артефакты, затем GCS, а потом локальные файлы. После загрузки изображение кэшируется как артефакт, поэтому последующие вызовы выполняются мгновенно.
6. 🧪 Конвейер обработки данных ИИ: агенты в действии
Теперь давайте рассмотрим двух самых продвинутых агентов — тех, кто непосредственно генерирует изображения и подбирает наряды.
6.1 Агент примерочной
Файл:
adk_backend/fittingroom/agent.go
Агент в примерочной — это движущая сила «виртуальной примерки». Когда пользователь загружает свою фотографию и выбирает товар, этот агент генерирует составное изображение человека, который носит этот товар.
Инструмент fitting_tool — пошаговая инструкция
Основная логика находится в функции doFitting . Вот что происходит, когда агент вызывает её:
Шаг 1: Разрешение изображения пользователя
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
}
Образ пользователя может формироваться из двух источников:
- Имя артефакта (например,
upload_abc123_1) — это первоначальная загрузка, сохраняемая функцией обратного вызоваSaveIncomingBlobs - URI gs
gs://— это ранее сгенерированный результат подгонки, сохраненный в GCS для повторного использования в разных сессиях.
Такая двухканальная структура является преднамеренной: когда стилист позже создает примерки одежды, он повторно использует URL-адрес GCS из первоначального результата примерки, чтобы идентификация пользователя оставалась неизменной для всех образов.
Шаг 2: Создайте многомодальную подсказку.
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 (встроенная из tool_instructions.md ) имеет решающее значение — она указывает Gemini сохранять индивидуальные особенности пользователя (лицо, тип телосложения, тон кожи, волосы), применяя только предмет одежды. Без этой подсказки модель может изменить внешний вид человека.
Шаг 3: Свяжитесь с Gemini для создания изображения.
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
})
Все четыре агента и инструмент генерации образов используют единый путь аутентификации: Backend: genai.BackendVertexAI с идентификатором проекта, аутентификация осуществляется через учетные данные приложения по умолчанию. Модели оркестрации ( gemini-3.1-pro-preview , gemini-3-flash-preview ) и модель образов ( gemini-2.5-flash-image ) находятся за одной и той же конечной точкой Vertex AI, и тот же ADC также авторизует доступ к облачному хранилищу — одни учетные данные для каждого вызова.
Шаг 4: Сохраните результат.
// 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
Двойное сохранение (артефакт + GCS) является ключом к передаче информации между примерочной и стилистом. Артефакт обеспечивает немедленный доступ в рамках текущей сессии, а URI GCS позволяет стилисту (работающему в другой сессии) ссылаться на то же изображение позже.
Функция обратного вызова SaveIncomingBlobs
Прежде чем агент начнет рассуждать, выполняется функция BeforeAgentCallback , которая сохраняет все изображения, загруженные пользователем:
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
}
Возвращая (nil, nil) , функция обратного вызова сигнализирует: «Предварительная обработка завершена — теперь запускайте агента в обычном режиме». Если бы она возвращала ненулевое содержимое, это полностью бы прервало работу агента.
6.2 Агент-стилист
Файл:
adk_backend/stylist/agent.go
Стилист-агент — самый продвинутый специалист в системе. Он подбирает персонализированные рекомендации по выбору одежды и поддерживает итеративное совершенствование образов посредством общения.
Три отсылки — Память стилиста
Стилист использует три обратных вызова для поддержания контекста в многоэтапных диалогах:
Обратный вызов 1:
InjectPreviousProducts (BeforeModel)
Проблема: если пользователь говорит «покажите мне другие варианты», LLM может снова предложить те же самые товары, поскольку он по своей сути не отслеживает то, что уже рекомендовал.
Решение: После каждого ответа идентификаторы продуктов сохраняются в состоянии сессии. Перед следующим вызовом LLM этот коллбэк считывает их и добавляет подсказку:
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
}
Обратный вызов 2:
ExtractAndInjectUserImage (BeforeModel)
Проблема: когда пользователь оставляет отзыв («сделайте это более неформальным»), в последующем сообщении фотография пользователя снова не отображается. А инструменту для подбора размера она необходима.
Решение: При первом запросе этот коллбэк извлекает ссылку на изображение пользователя и сохраняет её в состоянии. При последующих запросах он повторно внедряет её:
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
}
Обратный вызов 3:
SaveSelectedProducts (AfterModel)
После того, как LLM предоставит варианты комплектов одежды, эта функция обратного вызова анализирует JSON для извлечения идентификаторов товаров и сохраняет их для использования функцией обратного вызова InjectPreviousProducts в следующий раз:
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
}
Вместе эти три функции обратного вызова создают цикл обратной связи:
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 Корневой агент
Файл:
adk_backend/rootagent/agent.go
Самый простой агент — всего 31 строка:
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},
})
}
В нем используется gemini-3-flash-preview (самая быстрая модель), поскольку решения по маршрутизации просты — LLM нужно лишь прочитать намерение пользователя и выбрать подходящего субагента. Никаких дополнительных инструментов не требуется; SubAgents автоматически обрабатывает делегирование.
7. 📱 Архитектура фронтенда Flutter
Фронтенд на Flutter представляет собой полнофункциональное приложение для розничной торговли. Функции искусственного интеллекта находятся в flutter_frontend/lib/workshop_tasks/ , отдельно от встроенного интерфейса покупок в core_app/ .
Паттерн MVVM
Приложение использует архитектуру Model-View-ViewModel с пакетом 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 │ │ │ │ │
└──────────────────┘ └────────────────────┘ └──────────────────┘
Каждый слой выполняет свою четкую роль:
- Модель : Классы данных, такие как
Product,Outfit,StyleRequest, и перечисления, такие какTryOnState - ViewModel (
ChangeNotifier): хранит текущее состояние и передает изменения в пользовательский интерфейс черезnotifyListeners() - Виджет (View): Подписывается на ViewModel с помощью
context.watchи перестраивается при изменении состояния() - Сервис : выполняет HTTP-запросы к бэкэнду ADK и возвращает типизированные данные.
Уровень сервисов
Сервисы определены как абстрактные интерфейсы с реализациями, специфичными для 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 { ... }
Такое разделение означает, что вы можете заменить бэкенд ADK на Firebase AI, фиктивный сервис или любую другую реализацию, не меняя остальную часть приложения.
Трехшаговый шаблон API
Сервисы AdkFittingRoomService и AdkStylingService используют один и тот же алгоритм взаимодействия с бэкэндом ADK:
Шаг 1: Создайте сессию
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
}
Шаг 2: Запустите агента
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...
}
Шаг 3: Получите артефакт
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
}
Ключевое различие в дизайне: сервис примерочной создает новую сессию для каждого запроса ( _createSession() вызывается каждый раз), в то время как сервис стилизации повторно использует одну и ту же сессию ( _sessionId ??= await _createSession() ), что позволяет вести диалог в несколько этапов.
Управление состоянием: поставщик услуг TryItOn
Файл:
workshop_tasks/step_1_try_it_on/providers/try_it_on_provider.dart
Компонент TryItOnProvider управляет всем процессом примерки. В качестве конечного автомата он использует перечисление TryOnState :
enum TryOnState { initial, imagePicked, generating, success, error }
class TryItOnProvider with ChangeNotifier {
TryOnState _state = TryOnState.initial;
Uint8List? _userImageBytes;
Uint8List? _generatedImage;
String? _errorMessage;
Приватные переходы состояния обеспечивают согласованность — вы никогда не обновляете состояние, не очистив при этом устаревшие данные и не уведомив пользовательский интерфейс:
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();
}
Основной метод генерации объединяет все воедино:
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;
}
Пользовательский интерфейс: экраны в качестве маршрутизатора состояний.
Файл:
workshop_tasks/step_1_try_it_on/ui/2_try_it_on_screen.dart
На экране примерки используется сопоставление с шаблонами Dart 3 с помощью AnimatedSwitcher для переключения между подэкранами в зависимости от состояния поставщика услуг:
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 подписывается на провайдера. При каждом вызове notifyListeners() этот виджет перестраивается, и AnimatedSwitcher плавно переходит между экранами. Нет Navigator.push — содержимое экрана изменяется на месте в зависимости от перечисления состояний.
Передача информации агенту: Примерочная → Стилист
Наиболее интересный UX-паттерн — это то, как приложение передает контекст от сотрудника примерочной к стилисту.
В 5_fitting_room.dart после генерации изображения для примерки кнопка "Style Me" открывает форму. Когда пользователь отправляет форму:
// 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 входит все необходимое для стилиста:
- Место и повод — текстовый контекст для оформления.
- URL-адрес изображения пользователя в GCS — чтобы стилист мог повторно использовать точно такое же изображение пользователя.
- Выбранный продукт — чтобы стилист включал его в каждый образ.
Это и есть передача информации от одного ИИ-агента другому — плавный перенос многомодального контекста, при этом пользователь видит лишь простую форму.
Процесс создания стиля: Поставщик стилей
Файл:
workshop_tasks/step_2_style_me/providers/styling_provider.dart
StylingProvider проще, чем TryItOnProvider , потому что он переносит большую часть сложных операций на бэкэнд:
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;
}
}
Метод refineWithFeedback отправляет сообщение в виде обычного текста в ту же сессию — функции обратного вызова InjectPreviousProducts и ExtractAndInjectUserImage на стороне бэкэнда автоматически обрабатывают все операции управления контекстом.
8. 🚀 Запустите приложение локально
Для бесперебойной работы Cloud Shell бэкенд на Go запускает скомпилированное веб-приложение Flutter с того же порта (8080). Один процесс, один URL для предварительного просмотра, никаких проблем с междоменными запросами, никакого редактирования конфигурационных файлов.
Прежде чем начать — проверьте адекватность ADC.
Для вызова Vertex AI бэкэнду необходимы учетные данные приложения по умолчанию . Если вы выполнили шаг 7 настройки проекта в этом сеансе Cloud Shell и в этой учетной записи Google, все в порядке. Если вы возвращаетесь после перерыва, смены учетной записи или не уверены, потратьте 5 секунд на проверку:
gcloud auth application-default print-access-token | head -c 20 && echo "..."
Если при этом отобразится примерно 20 символов токена, всё в порядке. Если возникнет ошибка, повторите шаг 7 настройки проекта :
gcloud auth application-default login
gcloud auth application-default set-quota-project $(gcloud config get-value project)
Вам понадобятся два терминала Cloud Shell :
- Терминал A — запускает бэкенд непрерывно (
./run.sh). Оставьте его открытым. - Терминал B — запускает сборку веб-приложения Flutter один раз (
flutter build web). Завершает работу после завершения.
Порядок запуска не имеет значения — вы можете запустить любой из них первым. Но для наиболее чистого взаимодействия при первом запуске лучше сначала собрать Flutter, чтобы бэкенд имел готовый пользовательский интерфейс с момента запуска.
1. Терминал B — Сборка веб-пакета Flutter (одноразовое задание)
Откройте новую вкладку Cloud Shell (значок «+» в верхней части панели терминала), затем:
cd ~/fashion_app_demo/flutter_frontend
flutter pub get
flutter build web
В результате создается каталог flutter_frontend/build/web/ — каталог со статическими файлами (HTML, JS, ресурсы) — и процесс завершается. Бэкенд начнет обслуживать эти файлы, как только обнаружит существование каталога.
2. Терминал A — Запуск бэкэнда (длительно работающий процесс)
В вашем исходном терминале Cloud Shell:
cd ~/fashion_app_demo/adk_backend
./run.sh
Вы должны увидеть что-то подобное:
Serving Flutter web build from ../flutter_frontend/build/web
Оставьте этот терминал запущенным — бэкэнд будет работать до тех пор, пока запущен run.sh Чтобы остановить его, нажмите Ctrl+C .
Сервер предоставляет доступ ко всему через порт 8080:
-
/— Веб-приложение Flutter (пользовательский интерфейс для покупок) -
/api/— REST-конечные точки ADK (вызываемые приложением Flutter) - ADK Dev UI — также доступен в
/, когда нет сборки Flutter; полезен для прямой отладки агента.
3. Открыть предварительный просмотр веб-страницы
- В Cloud Shell нажмите значок « Предварительный просмотр веб-страниц» (в правом верхнем углу) → «Предварительный просмотр на порту 8080».
- Приложение для покупок Flutter загружается в новой вкладке.
- Просмотрите каталог продукции и выберите нужный товар.
- Нажмите на значок человека (👤), чтобы начать примерку.
- Загрузите фотографию и наблюдайте, как искусственный интеллект создаст изображение для примерки.
- Нажмите «Стиль», чтобы получить рекомендации по выбору наряда.
- В качестве дополнительной обратной связи используйте фразу «сделайте общение более неформальным» — это позволит внести коррективы в ходе одной сессии.
9. ☁️ Развертывание в облаке
Включите Flutter Build в бэкэнд.
Контейнер Cloud Run содержит как API, так и пользовательский интерфейс из одного образа. Скопируйте сборку Flutter web в папку adk_backend/flutter_web/ — это первый путь, который проверяет сервер Go при выборе того, какой пользовательский интерфейс обслуживать:
cd ~/fashion_app_demo/flutter_frontend
flutter build web
rm -rf ../adk_backend/flutter_web
cp -r build/web ../adk_backend/flutter_web
(Если вы выполняли итерации локально, у вас, возможно, уже есть build/web из шага "Запуск локально". Повторный запуск flutter build web по-прежнему допустим.)
Разверните бэкэнд (предоставляющий API и пользовательский интерфейс).
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
После завершения развертывания вы получите URL-адрес сервиса, например, https://fashion-app-backend-xyz-uc.a.run.app . Откройте его в браузере — приложение Flutter для покупок загрузится из / , а вызовы API будут осуществляться через /api/ на том же хосте. Никаких изменений в конфигурации фронтенда не требуется, ключ API не передается.
Проверьте развертывание.
Откройте URL-адрес Cloud Run в браузере и пройдите весь процесс:
- Просмотреть → Выбрать товар
- Примерка → Загрузите свою фотографию → Посмотрите изображение, сгенерированное ИИ
- Style Me → Укажите место/событие → Посмотрите подборку образов
- Обратная связь → Напишите «сделать более повседневным» → Смотрите обновленные образы
- Добавить в корзину → Завершить процесс покупки
10. 🎉 Заключение
Что ты построил
Вы изучили полноценный опыт розничной торговли с использованием искусственного интеллекта, включающий в себя:
- ✅ Многоагентная система управления с 4 специализированными агентами, работающими вместе.
- ✅ Виртуальная примерочная , генерирующая персонализированные изображения для примерки.
- ✅ Искусственный интеллект-стилист , который подбирает и совершенствует образы посредством диалога.
- ✅ Кроссплатформенное Flutter-приложение , подключающееся к бэкэнду агента.
- ✅ Развертывание Cloud Run для масштабируемого бессерверного хостинга
Ключевые понятия
Концепция | Где вы это видели |
ADK многоагентная оркестрация | Маршрутизация корневых агентов к агентам в примерочных, каталогах и стилистам. |
Gemini мультимодальная генерация изображений | |
Состояние сессии для разговорного ИИ | Стилист повторно использует сессии для получения итеративной обратной связи. |
Хранилище артефактов для бинарных данных | Разделение хранения изображений и текстовых ответов. |
Обратные вызовы для логики промежуточного ПО | |
MVVM + Провайдер во Flutter | |
Передача полномочий агенту | |
Следующие шаги
- 🎨 Настройте подсказки для агента — отредактируйте файл
instructions.md, чтобы изменить характер стилиста. - 🛍️ Добавьте больше товаров — обновите
catalog.yaml, добавив новые позиции. - 📱 Создайте сборку для мобильных устройств — запустите
flutter build iosилиflutter build apk - 🔄 Добавьте постоянные сессии — замените
InMemoryServiceреализацией на основе базы данных. - 🔒 Добавьте аутентификацию — защитите конечную точку Cloud Run с помощью IAM.
Ресурсы
- Документация ADK — Официальная документация по комплекту разработки агентов
- Исходный код ADK на Go — репозиторий на GitHub
- Справочник по пакетам ADK для Go — Справочник по API
- Документация по API Gemini — Возможности моделей и руководства.
- Пакет поставщика Flutter — Документация по управлению состоянием
- Документация Cloud Run — Руководства по развертыванию и масштабированию