Создание агента Gemini на Go: сравнение Raw SDK, Genkit и ADK
Один эксперт по оценке ретро-игр создал три решения на языке Go с использованием GenAI SDK, Genkit и Agent Development Kit, а также объяснил, как запустить каждое из них на Cloud Run или платформе агентов Google.
Разработка приложений с использованием ИИ и автономных агентов на языке Go может осуществляться на трех разных уровнях абстракции. В этом обзоре рассматриваются механизмы работы агента, выбирается конкретный пример применения — оценщик ретро-игр, — после чего он создается трижды:
- Цикл работы агента низкого уровня, написанный непосредственно с использованием SDK GenAI для Go.
- Структурированная система в виде потока, созданная с помощью Genkit.
- Модульная система из нескольких агентов с сессиями, разработанная на основе инструментария Agent Development Kit (ADK) от Google.
В конце рассматриваются среды выполнения, позволяющие надежно развертывать агенты на Go в облаке.
Что делает что-то агентом
Этот термин используется в неформальной речи, однако в инженерии искусственного интеллекта он имеет довольно точное архитектурное значение: автономная система, состоящая из трех компонентов — LLM, набора инструментов, таких как вызываемые функции или API, и механизма, который выполняет всё в рамках цикла обратной связи.
Модель без инструментов — это просто генератор текста. Она может отвечать только на основе своих фиксированных параметров обучения или информации, указанной в запросе. Чтобы придать ей способность к самостоятельным действиям, то есть возможность анализировать внешнее состояние, проверять гипотезы и вносить изменения в реальном мире, необходимо подключить её к исполняемым инструментам.
Один архитектурный нюанс легко упустить из виду. Сама модель никогда не выполняет код и не обращается к внешним API. Посредником выступает локальная программа, в данном случае на языке Go. Когда модель решает, что ей нужна внешняя информация или какое-то действие, она отправляет структурированный запрос к инструменту. Посредник запускает соответствующую функцию на Go, сохраняет результат и передаёт его обратно модели для дальнейшей обработки. Затем модель анализирует новый контекст и либо вызывает другой инструмент, либо формирует свой окончательный ответ.
Не все инструменты работают локально. Gemini предлагает встроенные инструменты, такие как функция опоры на результаты поиска Google, которые выполняются на стороне сервера. После указания их в конфигурации запроса API обрабатывает запросы к поиску и добавляет метаданные к ответу без необходимости повторных запросов на локальном устройстве. Однако для реализации собственной логики необходимо использовать инструменты на языке Go: они отвечают за определение структуры данных, передачу аргументов и возвращение сериализованных результатов.
Сценарий применения: оценщик ретро-игр
Каждый, кто коллекционирует ретро-видеоигры, знаком с этой проблемой. На рынке ретро-игр легко купить желаемую игру, но дома оказаться, что она уже есть в коллекции. Кроме того, в этой сфере хобби цены могут сильно меняться, существует несколько уровней состояния игр (кассета без упаковки, полная упаковка или CIB, игра в заводской упаковке), а также часто встречаются поддельные предложения.
Проверка цены запроса через Gemini в реальном времени, находясь прямо у стенда, оказывается удивительно полезной. Многие покупатели готовы заплатить приемлемую премию за возможность приобрести товар лично и поддержать местных продавцов, но никто не хочет стать жертвой мошенничества или переплатить за поддельный картридж.
Агент-оценщик учитывает обе стороны: он сопоставляет личный каталог товаров с данными с веб-страниц и определяет справедливую рыночную цену на основе актуальной информации из Интернета.
Как им пользуется коллекционер
Коллекционер вводит обычные вопросы, такие как:
- Вопрос о том, уже ли имеется в коллекции определенная игра, например Chrono Trigger.
- Информация о полностью упакованной версии EarthBound для SNES по цене 350 долларов, с вопросом о том, уже ли она принадлежит кому-то, и соответствует ли цена текущим рыночным значениям.
Контракты с инструментами
Чтобы дать ответ, не выдумывая информацию о наличии товара или ценах, агент опирается на два источника достоверных данных:
search_catalog— это локальная функция на языке Go, которая запрашивает собственную базу данных коллекционера. Она ищет ключевые слова в названиях и платформах, затем сообщает информацию об владельце, состоянии, дате приобретения и цене покупки.google_search— это инструмент для поиска на стороне сервера. Он ищет на рынках и в системах отслеживания аукционов текущие справедливые цены, недавние подтверждённые продажи и эталоны состояния.
Стратегия обоснования
Для возможной покупки агент действует по фиксированной последовательности. Сначала он проверяет местный каталог, чтобы узнать, принадлежит ли игра и в каком состоянии она находится. Затем он ищет в Интернете текущие цены на этой платформе и для данного уровня состояния. В конце он объединяет полученную информацию в оценку: цену, предложенную для продажи, по сравнению с средним рыночным уровнем, возможности улучшения, такие как замена картриджа без упаковки на версию в коробке, а также четкую рекомендацию.
Версия один: Go GenAI SDK
Прямая работа с SDK (google.golang.org/genai) представляет собой уровень наименьшей абстракции, однозначно соответствующий протоколу передачи данных API Gemini. Поскольку между вашим кодом и API нет никаких фреймворков, вы сами контролируете направление вызовов инструментов, историю разговоров и критерии прекращения цикла.
Это решение подходит для небольших проектов и утилитных скриптов, для тех, кто изучает принципы вызова функций и их работы, а также для случаев с крайне специфическими требованиями к циклам, которые не покрываются стандартными фреймворками. Результат компилируется в один самодостаточный бинарный файл на Go без зависимостей от фреймворков, поэтому его можно развернуть на обычных платформах хостинга веб-сайтов или API, таких как Google Cloud Run, Kubernetes или обычная виртуальная машина.
Полная рабочая реализация:
package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"google.golang.org/genai"
)
// GameItem represents a collectible item in the user's personal inventory.
type GameItem struct {
Title string `json:"title"`
Platform string `json:"platform"`
Year int `json:"year"`
Condition string `json:"condition"` // e.g. "Loose Cartridge", "CIB (Complete in Box)", "Mint"
PricePaid float64 `json:"price_paid"`
Notes string `json:"notes"`
}
// localCatalog simulates an inventory database for retro games.
var localCatalog = []GameItem{
{
Title: "Chrono Trigger",
Platform: "Super Nintendo (SNES)",
Year: 1995,
Condition: "CIB (Complete in Box)",
PricePaid: 210.00,
Notes: "Includes original map and registration card.",
},
{
Title: "EarthBound",
Platform: "Super Nintendo (SNES)",
Year: 1994,
Condition: "Loose Cartridge",
PricePaid: 180.00,
Notes: "Authentic board verified; label in excellent shape.",
},
{
Title: "Castlevania: Symphony of the Night",
Platform: "Sony PlayStation",
Year: 1997,
Condition: "CIB (Black Label)",
PricePaid: 135.00,
Notes: "Original soundtrack disc included.",
},
}
// searchCatalogTool searches the local collection for matching games.
func searchCatalogTool(args map[string]any) map[string]any {
query, _ := args["query"].(string)
queryLower := strings.ToLower(strings.TrimSpace(query))
var matches []GameItem
for _, item := range localCatalog {
if strings.Contains(strings.ToLower(item.Title), queryLower) ||
strings.Contains(strings.ToLower(item.Platform), queryLower) {
matches = append(matches, item)
}
}
if len(matches) == 0 {
return map[string]any{
"found": false,
"message": fmt.Sprintf("No items matching %q found in your collection.", query),
}
}
return map[string]any{
"found": true,
"count": len(matches),
"results": matches,
}
}
func main() {
ctx := context.Background()
// Initialise GenAI client for Gemini Enterprise
client, err := genai.NewClient(ctx, &genai.ClientConfig{
Project: os.Getenv("GOOGLE_CLOUD_PROJECT"),
Location: "global",
Backend: genai.BackendEnterprise,
})
if err != nil {
log.Fatalf("failed to create client: %v", err)
}
// 1. Declare custom function schema for collection lookup
catalogToolDecl := &genai.FunctionDeclaration{
Name: "search_catalog",
Description: "Search the collector's personal inventory for owned games by title or platform.",
Parameters: &genai.Schema{
Type: genai.TypeObject,
Properties: map[string]*genai.Schema{
"query": {
Type: genai.TypeString,
Description: "Game title or platform to search (e.g. 'EarthBound', 'SNES').",
},
},
Required: []string{"query"},
},
}
// 2. Configure model tools: custom function declaration + Google Search grounding
config := &genai.GenerateContentConfig{
SystemInstruction: &genai.Content{
Parts: []*genai.Part{
{Text: "You are an expert Retro Game Appraiser. When evaluating purchases, check the user's " +
"collection catalog first to see if they already own the item, then check current market " +
"prices using Google Search to evaluate whether the deal is fair, overpriced, or a bargain."},
},
},
Tools: []*genai.Tool{
{
FunctionDeclarations: []*genai.FunctionDeclaration{catalogToolDecl},
},
{
GoogleSearch: &genai.GoogleSearch{},
},
},
}
// 3. Graceful shutdown on Ctrl+C (SIGINT) or SIGTERM
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Println("\nGoodbye!")
os.Exit(0)
}()
model := "gemini-3.8-flash"
var contents []*genai.Content
fmt.Println("Retro Game Appraiser (SDK Agent)")
fmt.Println("Type your question below, or 'exit' (Ctrl+C / Ctrl+D) to quit.")
fmt.Println("-----------------------------------------------------------------")
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("\nUser: ")
if !scanner.Scan() {
fmt.Println("\nGoodbye!")
break
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
continue
}
if strings.EqualFold(input, "exit") {
fmt.Println("Goodbye!")
break
}
contents = append(contents, &genai.Content{
Role: "user",
Parts: []*genai.Part{genai.NewPartFromText(input)},
})
// 4. The Agent Loop: model generation -> tool dispatch -> feedback -> until final answer
for {
resp, err := client.Models.GenerateContent(ctx, model, contents, config)
if err != nil {
log.Printf("error generating content: %v", err)
break
}
if len(resp.Candidates) == 0 || resp.Candidates[0].Content == nil {
log.Println("received empty response candidate from model")
break
}
// Append the model's response to the conversation history
modelContent := resp.Candidates[0].Content
contents = append(contents, modelContent)
// Check if the model requested any client-side tool executions
funcCalls := resp.FunctionCalls()
if len(funcCalls) == 0 {
fmt.Printf("\nAppraiser: %s\n", resp.Text())
break
}
// Execute each requested tool and prepare response parts
var responseParts []*genai.Part
for _, call := range funcCalls {
fmt.Printf("[Harness] Executing tool: %s(args=%v)\n", call.Name, call.Args)
var result map[string]any
switch call.Name {
case "search_catalog":
result = searchCatalogTool(call.Args)
default:
result = map[string]any{"error": fmt.Sprintf("unsupported tool: %s", call.Name)}
}
responseParts = append(responseParts, genai.NewPartFromFunctionResponse(call.Name, result))
}
// Return tool execution results as a user turn
contents = append(contents, &genai.Content{
Role: "user",
Parts: responseParts,
})
}
}
}
Чтобы запустить его, настройте проект в Google Cloud и войдите с использованием учетных данных по умолчанию приложения с помощью команды gcloud auth application-default login:
export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
go run main.go
В терминале открывается интерактивный чат. Разговор может вестись несколько раундов и заканчивается, когда вы вводите exit, нажимаете Ctrl+C или отправляете сигнал EOF с помощью Ctrl+D:
Retro Game Appraiser (SDK Agent)
Type your question below, or 'exit' (Ctrl+C / Ctrl+D) to quit.
-----------------------------------------------------------------
User: I found a copy of EarthBound for SNES in mint Complete-in-Box (CIB) condition for $350. Do I already own it, and is $350 a good deal compared to current market prices?
[Harness] Executing tool: search_catalog(args=map[query:EarthBound])
Appraiser: Here is your collection check and appraisal for **EarthBound (SNES)**:
1. **Current Collection Status**:
- You currently own **EarthBound** on Super Nintendo as a **Loose Cartridge**, purchased for **$180.00**.
2. **Market Price Appraisal**:
- Verified market sales for an authentic, **Complete-in-Box (CIB)** copy of EarthBound typically range between **$1,200.00 and $1,500.00** depending on the condition of the box, tray, and original player's guide.
3. **Recommendation**:
- At **$350.00**, a genuine Mint CIB copy is an **exceptional deal** (more than 70% below prevailing market value).
- **Caution**: Because EarthBound is one of the most heavily counterfeited SNES titles, inspect the box printing, registration card, and PCB board carefully before completing the transaction. If verified authentic, this is an outstanding opportunity to upgrade your loose copy to CIB.
User: exit
Goodbye!
Версия SDK делает логику управления явной и легкой для понимания. Благодаря мощным моделям и большим окнам токенов написание внутренних механизмов, циклов вызова инструментов и вспомогательных структур оказывается удивительно простым.
Однако эта простота является ловушкой. Каждая строка кода самостоятельно разработанной фреймворковой структуры превращается в постоянные затраты на техническое обслуживание и потенциальное место появления скрытых ошибок. Разработка фреймворка агентов с нуля в лучшем случае отвлекает от реализации функций, а в худшем — приводит к накоплению технического долга, отвлекающего усилия от реальных бизнес-целей. Лучший код — это код, который вообще не нужен, а следующий по значимости вариант — это то, что решает проблему с наименьшим количеством собственного кода.
Почему стоит перейти на фреймворк
Чистый цикл SDK трудно превзойти для изучения низкоуровневых механизмов или для создания специализированных циклов, однако производственные приложения требуют более высокого уровня абстракции по нескольким причинам:
- Автоматическое отражение схемы. Ручное создание JSON-схем с помощью
&genai.Schema{...}— это трудоемкий и подверженный ошибкам процесс. Фреймворки для производственного использования генерируют схемы на основе встроенных структур Go и комментариев к документации. - Наблюдаемость и трейсинг. Для производственных задач необходимы встроенные функции OpenTelemetry для отслеживания, разбивка задержек по инструментам и метрики потребления токенов.
- Управление промптами. Жестко закодированные промпты в виде строк Go мешают сотрудничеству с инженерами по промптам и исключают возможность версионирования шаблонов отдельно от бинарных релизов.
- Персистентность сессий и состояние. Для сохранения истории нескольких взаимодействий в рамках безсостоянийных HTTP-запросов требуется надежное хранилище, безопасное для многопоточной работы и развязанное от остальных компонентов.
В экосистеме Go существуют два основных фреймворка с открытым исходным кодом для решения этих задач: Genkit и Agent Development Kit.
Вторая версия: Genkit
Genkit обеспечивает средний уровень абстракции и вносит принципы инженерии программного обеспечения и структурированную возможность отслеживания в приложениях ИИ. Всё организовано вокруг потоков — строго типизированных, отслеживаемых конвейеров, — а также инструментов, представляющих собой типобезопасные функции на Go с автоматическим генерированием схем.
Он подходит для одноразовых задач генерации, таких как инструменты командной строки, пакетная обработка и конечные точки webhook, а также для детерминистичных рабочих процессов и агентов с одним доменом, например, для мультимодальных систем восстановления фотографий. Он поддерживает несколько поставщиков моделей через плагины. Поскольку приложение Genkit представляет собой просто стандартный HTTP-сервер на Go, оно может работать на любой платформе с бэкендом, причем Google Cloud Run является предпочтительным вариантом благодаря удобству хостинга контейнеров и автоматическому масштабированию.
Пример приложения, написанного с использованием Genkit Go:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/googlegenai"
"google.golang.org/genai"
)
// GameItem represents a collectible item in the user's personal inventory.
type GameItem struct {
Title string `json:"title"`
Platform string `json:"platform"`
Year int `json:"year"`
Condition string `json:"condition"`
PricePaid float64 `json:"price_paid"`
Notes string `json:"notes"`
}
// localCatalog simulates an inventory database for retro games.
var localCatalog = []GameItem{
{
Title: "Chrono Trigger",
Platform: "Super Nintendo (SNES)",
Year: 1995,
Condition: "CIB (Complete in Box)",
PricePaid: 210.00,
Notes: "Includes original map and registration card.",
},
{
Title: "EarthBound",
Platform: "Super Nintendo (SNES)",
Year: 1994,
Condition: "Loose Cartridge",
PricePaid: 180.00,
Notes: "Authentic board verified; label in excellent shape.",
},
{
Title: "Castlevania: Symphony of the Night",
Platform: "Sony PlayStation",
Year: 1997,
Condition: "CIB (Black Label)",
PricePaid: 135.00,
Notes: "Original soundtrack disc included.",
},
}
type CatalogRequest struct {
Query string `json:"query" jsonschema:"description=The game title or platform to search in the inventory"`
}
type CatalogResponse struct {
Found bool `json:"found"`
Message string `json:"message,omitempty"`
Count int `json:"count,omitempty"`
Results []GameItem `json:"results,omitempty"`
}
type AppraiserRequest struct {
Prompt string `json:"prompt" jsonschema:"description=The collector's question or purchase offer to evaluate"`
}
type AppraiserResponse struct {
Appraisal string `json:"appraisal"`
}
func main() {
ctx := context.Background()
// 1. Initialise Genkit with Vertex AI plugin
g := genkit.Init(ctx,
genkit.WithPlugins(&googlegenai.VertexAI{
ProjectID: os.Getenv("GOOGLE_CLOUD_PROJECT"),
Location: "global",
}),
)
// 2. Define strongly-typed tool with automatic schema generation
catalogTool := genkit.DefineTool(
g,
"search_catalog",
"Search the collector's personal inventory for owned games by title or platform.",
func(ctx *ai.ToolContext, req CatalogRequest) (CatalogResponse, error) {
queryLower := strings.ToLower(strings.TrimSpace(req.Query))
queryWords := strings.Fields(queryLower)
var matches []GameItem
for _, item := range localCatalog {
itemText := strings.ToLower(item.Title + " " + item.Platform)
allMatch := true
for _, word := range queryWords {
if !strings.Contains(itemText, word) {
allMatch = false
break
}
}
if allMatch {
matches = append(matches, item)
}
}
if len(matches) == 0 {
return CatalogResponse{
Found: false,
Message: fmt.Sprintf("No items matching %q found in personal collection.", req.Query),
}, nil
}
return CatalogResponse{
Found: true,
Count: len(matches),
Results: matches,
}, nil
},
)
// 3. Define structured appraisal flow with typed request and response
appraiserFlow := genkit.DefineFlow(
g,
"appraise_game",
func(ctx context.Context, req AppraiserRequest) (AppraiserResponse, error) {
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("vertexai/gemini-3.8-flash"),
ai.WithSystem(
"You are an expert Retro Game Appraiser. Assist collectors by evaluating prospective purchases, "+
"cross-referencing their personal inventory, and assessing fair market valuations. "+
"Always search the collection catalog using search_catalog before providing purchase recommendations.",
),
ai.WithConfig(&genai.GenerateContentConfig{
ThinkingConfig: &genai.ThinkingConfig{IncludeThoughts: true},
Tools: []*genai.Tool{
{
GoogleSearch: &genai.GoogleSearch{},
},
},
}),
ai.WithPrompt(req.Prompt),
ai.WithTools(catalogTool),
)
if err != nil {
return AppraiserResponse{}, fmt.Errorf("appraisal generation failed: %w", err)
}
return AppraiserResponse{Appraisal: resp.Text()}, nil
},
)
// 4. Mount flow directly using Genkit's built-in HTTP handler
mux := http.NewServeMux()
mux.Handle("POST /api/appraise", genkit.Handler(appraiserFlow))
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
server := &http.Server{
Addr: ":" + port,
Handler: mux,
}
// Graceful shutdown on Ctrl+C (SIGINT) or SIGTERM
serverCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
log.Printf("Retro Game Appraiser (Genkit) listening on :%s", port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server failed: %v", err)
}
}()
<-serverCtx.Done()
log.Println("\nShutting down server gracefully...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("server forced shutdown: %v", err)
}
log.Println("Server exited cleanly.")
}
Запуск потока
Запустите сервер Genkit с настроенным проектом Google Cloud:
export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
export PORT=8080
go run main.go
Из другого терминала отправьте запрос на оценку:
curl -s -X POST http://localhost:8080/api/appraise \
-H "Content-Type: application/json" \
-d '{"data": {"prompt": "I found a copy of EarthBound for SNES for $350. Do I own it, and is it a good deal?"}}' | jq .
Процесс выполняется, вызывает инструмент каталога и возвращает структурированный пакет оценки:
{
"appraisal": "### 1. Catalog Check\n**Yes, you already own it.**\n* **Title:** *EarthBound* (SNES, 1994)\n* **Status in Collection:** Loose Cartridge\n* **Condition/Notes:** Authentic board verified; label in excellent shape.\n* **Price Paid:** $180\n\n---\n\n### 2. Market Appraisal & Deal Analysis\n* **Loose Cartridge:** The current going market rate for an authentic loose copy ranges between **$320 and $380**. At **$350**, it is priced right at **fair market value**—neither an overpriced listing nor a significant bargain.\n* **Complete in Box (CIB) / Boxed with Guide:** If this listing happens to include the original big box and strategy guide with scratch-and-sniff cards, $350 would be an extraordinary steal (CIB copies regularly sell for **$1,500–$2,500+**).\n\n---\n\n### 3. Recommendation\n* **Pass (if Loose):** Since you already have an authentic copy in excellent condition, paying retail market price ($350) for a duplicate loose cart does not offer strong value or upside.\n* **Buy immediately (if Complete/Boxed):** Only pull the trigger if it includes the original packaging or represents a major condition upgrade/variant.\n* **Buyer Beware:** If you do ever consider another copy, always inspect the PCB (printed circuit board) screws and chips, as *EarthBound* is one of the most frequently counterfeited games on the SNES."
}
Genkit устраняет необходимость вручную написанного цикла обработки сообщений. Он преобразует аргументы в стандартные структуры языка Go, запускает соответствующий инструмент, возвращает полученные данные модели и автоматически генерирует записи OpenTelemetry для каждого этапа. Этот подход идеален, когда рабочая нагрузка следует структурированному, детерминированному алгоритму.
Версия три: набор инструментов для разработки агентов
В то время как Genkit ориентирован на структурированные цепочки обработки данных в приложениях, набор инструментов ADK от Google работает на более высоком уровне абстракции. Он разработан для автономных конверсационных агентов, оркестрации нескольких агентов и сложных архитектур, требующих долгосрочной памяти и технологий RAG в корпоративных средах.
Этот набор также стандартизирует жизненные циклы агентов, процедуры делегирования задач субагентам и протоколы взаимодействия между агентами, а также позволяет использовать несколько поставщиков моделей через адаптеры, как это делает Genkit.
Для развертывания платформа Gemini Enterprise Agent является предпочтительным вариантом, если вы хотите, чтобы Google обрабатывал хранение сессий, корпоративные подключения и память, тем самым избавляя вас от необходимости писать собственные слои баз данных. Командам, использующим собственные хранилища состояния или контейнеризованные микросервисы, более подходит Google Cloud Run.
Оценщик в ADK v2:
package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"google.golang.org/genai"
"google.golang.org/adk/v2/agent"
"google.golang.org/adk/v2/agent/llmagent"
"google.golang.org/adk/v2/cmd/launcher"
"google.golang.org/adk/v2/cmd/launcher/full"
"google.golang.org/adk/v2/model/gemini"
"google.golang.org/adk/v2/tool"
"google.golang.org/adk/v2/tool/functiontool"
"google.golang.org/adk/v2/tool/geminitool"
)
// GameItem represents a collectible item in the user's personal inventory.
type GameItem struct {
Title string `json:"title"`
Platform string `json:"platform"`
Year int `json:"year"`
Condition string `json:"condition"`
PricePaid float64 `json:"price_paid"`
Notes string `json:"notes"`
}
// localCatalog simulates an inventory database for retro games.
var localCatalog = []GameItem{
{
Title: "Chrono Trigger",
Platform: "Super Nintendo (SNES)",
Year: 1995,
Condition: "CIB (Complete in Box)",
PricePaid: 210.00,
Notes: "Includes original map and registration card.",
},
{
Title: "EarthBound",
Platform: "Super Nintendo (SNES)",
Year: 1994,
Condition: "Loose Cartridge",
PricePaid: 180.00,
Notes: "Authentic board verified; label in excellent shape.",
},
{
Title: "Castlevania: Symphony of the Night",
Platform: "Sony PlayStation",
Year: 1997,
Condition: "CIB (Black Label)",
PricePaid: 135.00,
Notes: "Original soundtrack disc included.",
},
}
type CatalogRequest struct {
Query string `json:"query" jsonschema:"The game title or platform to search in the inventory."`
}
type CatalogResponse struct {
Found bool `json:"found"`
Message string `json:"message,omitempty"`
Count int `json:"count,omitempty"`
Results []GameItem `json:"results,omitempty"`
}
func main() {
ctx := context.Background()
// 1. Initialise Gemini Model adapter for Gemini Enterprise
model, err := gemini.NewModel(ctx, "gemini-3.8-flash", &genai.ClientConfig{
Project: os.Getenv("GOOGLE_CLOUD_PROJECT"),
Location: "global",
Backend: genai.BackendEnterprise,
})
if err != nil {
log.Fatalf("failed to create Gemini model: %v", err)
}
// 2. Wrap collection lookup as an ADK Function Tool
catalogTool, err := functiontool.New(functiontool.Config{
Name: "search_catalog",
Description: "Search the collector's personal inventory for owned games by title or platform.",
}, func(ctx agent.Context, req CatalogRequest) (CatalogResponse, error) {
queryLower := strings.ToLower(strings.TrimSpace(req.Query))
var matches []GameItem
for _, item := range localCatalog {
if strings.Contains(strings.ToLower(item.Title), queryLower) ||
strings.Contains(strings.ToLower(item.Platform), queryLower) {
matches = append(matches, item)
}
}
if len(matches) == 0 {
return CatalogResponse{
Found: false,
Message: fmt.Sprintf("No items matching %q found in personal collection.", req.Query),
}, nil
}
return CatalogResponse{
Found: true,
Count: len(matches),
Results: matches,
}, nil
})
if err != nil {
log.Fatalf("failed to create catalog tool: %v", err)
}
// 3. Define autonomous LLM Agent
appraiserAgent, err := llmagent.New(llmagent.Config{
Name: "retro_game_appraiser",
Model: model,
Description: "Expert appraiser that analyzes retro video game purchases and collection inventory.",
Instruction: "You are an expert Retro Game Appraiser. Assist collectors by verifying collection " +
"status with search_catalog, assessing condition variants, and offering objective buying recommendations.",
Tools: []tool.Tool{
catalogTool,
geminitool.GoogleSearch{},
},
})
if err != nil {
log.Fatalf("failed to create appraiser agent: %v", err)
}
// 4. Configure launcher and execute
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(appraiserAgent),
}
l := full.NewLauncher()
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
log.Fatalf("run failed: %v\n\n%s", err, l.CommandLineSyntax())
}
}
Запуск агента ADK
ADK поставляет универсальный запускач full.NewLauncher(), поэтому нет необходимости вручную создавать механизмы роутинга HTTP, маршалинга JSON или шаблоны работы с сессиями. Аргументы командной строки, передаваемые запускачу, определяют режим взаимодействия без каких-либо изменений в коде приложения.
Чтобы запустить интерактивный чат в терминале:
export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
go run main.go
Запускатель работает в режиме консоли, и с оценщиком можно связаться напрямую:
User: Do I have Chrono Trigger in my collection?
Agent: Yes, you have Chrono Trigger in your collection! Here are the details from your inventory:
* Title: Chrono Trigger
* Platform: Super Nintendo (SNES)
* Release Year: 1995
* Condition: CIB (Complete in Box)
* Price Paid: $210.00
* Notes: Includes original map and registration card.
User: What did I pay for it?
Agent: You paid $210.00 for it.
ADK сам управляет состоянием диалога и непрерывностью сессии, поэтому последующие вопросы сохраняют полный контекст предыдущих сообщений.
В качестве альтернативы существует встроенный веб-интерфейс для разработчиков, предоставляемый вместе с REST API:
go run main.go web webui api
Открытие адреса http://localhost:8080 позволяет вступить в интерактивный чат с мгновенными ответами в реальном времени, историей сессий и возможностью пошагового отслеживания работы инструментов, включая как пользовательский инструмент функции search_catalog, так и встроенный инструмент GoogleSearch.
ADK чётко разделяет определение агента (llmagent), связи с инструментами (functiontool и встроенные компоненты, такие как geminitool.GoogleSearch) и среду выполнения (launcher и runner). Благодаря этому он становится предпочтительной платформой для создания сложных многоагентных иерархий и конверсационных ассистентов.
Где выполняются агенты Go
Написание агента на Go — это лишь половина работы. После этого ему требуется среда выполнения, способная справляться с длительными потоковыми ответами, вызовами фоновых инструментов, защищёнными учетными данными и всплесками нагрузки без дополнительных сложностей в эксплуатации. Go компилируется в отдельные самодостаточные бинарные файлы с практически нулевыми затратами на работу во время выполнения, поэтому агенты, написанные на этом языке, работают быстро и их хостинг стоит недорого. В зависимости от требований фреймворка и состояния системы существуют два основных варианта реализации.
Cloud Run — универсальный бэкенд
Google Cloud Run является основной целью для контейнеризованных агентов на Go, независимо от того, были ли они созданы с использованием чистого SDK, потоков Genkit или запускачей ADK. Многоэтапная сборка в Docker позволяет скомпилировать агента в минимальное изображение типа scratch или distroless без каких-либо зависимостей во время выполнения. Поскольку не требуется запуск тяжелого интерпретатора или виртуальной машины, сервис быстро масштабируется до нуля, избегая задержек, связанных с запуском, которые характерны для более тяжелых сред выполнения.
Основные преимущества Cloud Run для агентов на Go:
- Масштабирование до нуля с высокой конкурентоспособностью. Вы платите только за миллисекунды использования CPU во время реальной работы агента или обработки ответов от инструментов, а легковесные горутины Go позволяют одному экземпляру контейнера одновременно обрабатывать сотни операций агента с небольшим расходом памяти.
--session-affinity на основе IP-адреса клиента или куки, чтобы отправлять последующие запросы в ту же инстанцию контейнера.Платформа агентов Gemini Enterprise
Агенты ADK для общения с пользователями и корпоративные решения, требующие сохранения сессий, хранения долгосрочной памяти и основания ответов на корпоративных знаниях, могут использовать платформу Gemini Enterprise Agent Platform, считающуюся преемницей Vertex AI Agent Engine. Она предоставляет полностью управляемую, безсерверную среду выполнения. Вместо настройки баз данных и разработки собственных адаптеров для хранения сессий команды получают управляемую инфраструктуру с следующими функциями:
- Разделенное хранение сессий. Долгосрочное хранение информации о разговорах (
SessionServiceв ADK) отделено от временного цикла выполнения в реальном времени (LiveSession), включающего автоматическое возобновление сессий при повторном подключении.
Выбор между тремя вариантами
Для изучения принципов вызова функций и их связи с реальным миром, а также для небольших утилит достаточно простого цикла SDK. Для структурированной, детерминированной обработки данных или агента, работающего в одной области и доступного по HTTP-эндпоинту, подходит Genkit. Для конверсационных ассистентов, иерархий множества агентов, механизмов делегирования и долгосрочной памяти более подходящей основой является ADK. Cloud Run подходит ко всем трем вариантам, в то время как платформа Gemini Enterprise Agent Platform предназначена для агентов ADK, которым требуются управляемые сессии, память и корпоративные механизмы связи с реальным миром.