Strona główna / Artykuły

Tworzenie agenta Gemini w Go: porównanie SDK Raw, Genkit i ADK

Jeden ekspert od oceny gier retro stworzył trzy rozwiązania w języku Go, wykorzystując GenAI SDK, Genkit oraz Agent Development Kit, a także pokazał, jak uruchomić każde z nich na Cloud Run lub platformie agentów Google.

Wydawca: REACTAPP.TOP19 min czytania4462 słów

Źródła: Gemini for Go Developers: Building Agents in Go

Budowanie aplikacji wykorzystujących sztuczną inteligencję oraz autonomicznych agentów w języku Go może odbywać się na trzech różnych poziomach abstrakcji. Ten przewodnik analizuje mechanizmy działania agenta, wybiera konkretny przypadek użycia – oceniacza gier retro – a następnie buduje go trzy razy:

  1. Pętla agenta na niskim poziomie, napisana bezpośrednio z wykorzystaniem SDK Go GenAI.
  2. Strukturalna, zorientowana na przepływ architektura budowana za pomocą Genkit.
  3. Modularny system wielu agentów z sesjami, stworzony na bazie Google Agent Development Kit (ADK).

Na końcu omówione są środowiska uruchomieniowe umożliwiające niezawodne wdrażanie agentów napisanych w Go w chmurze.

Co czyni coś agentem

Termin ten jest używany swobodnie, jednak w inżynierii sztucznej inteligencji ma dość precyzyjne znaczenie architektoniczne: system autonomiczny składający się z trzech części – modelu LLM, zestawu narzędzi takich jak funkcje wywoływalne lub API oraz mechanizmu, który uruchamia wszystko w ramach pętli sprzężenia zwrotnego.

Model bez narzędzi to po prostu generator tekstu. Może odpowiadać jedynie na podstawie swoich ustalonych wag treningowych lub informacji zawartych w zadaniu. Nadanie mu autonomii, czyli możliwości sprawdzania stanu zewnętrznego, testowania hipotez i zmieniania rzeczy w rzeczywistym świecie, wymaga połączenia go z wykonywalnymi narzędziami.

Jeden z detalów architektonicznych łatwo przeoczyć. Model sam w sobie nigdy nie uruchamia kodu ani nie wywołuje zewnętrznych API. Pomiędzy nimi znajduje się lokalny mechanizm, w tym przypadku program w języku Go. Gdy model postanawia, że potrzebuje informacji z zewnątrz lub podjęcia jakiejś akcji, wysyła uporządkowany wniosek o wywołanie narzędzia. Mechanizm ten uruchamia odpowiednią funkcję w Go, przechwytuje wynik i przekazuje go z powrotem do następnego kroku modelu. Model następnie analizuje nowy kontekst i albo wywołuje inne narzędzie, albo pisze swoją ostateczną odpowiedź.

Nie każde narzędzie działa lokalnie. Gemini oferuje wbudowane narzędzia, takie jak funkcja Google Search grounding, które są wykonywane po stronie serwera. Po ich zadeklarowaniu w konfiguracji żądania API przetwarza zapytania wyszukiwawcze i dołącza metadane dotyczące tych danych do odpowiedzi, bez konieczności powrotu do lokalnego komputera. Jednak w przypadku niestandardowej logiki domeny wszystko jest kontrolowane przez narzędzie Go: deklaruje ono schemat, przekazuje argumenty i zwraca zserializowane wyniki.

Przypadek użycia: oceniający gry retro

Każdy, kto kolekcjonuje gry wideo retro, zna ten typowy problem. Na targach z grami retro łatwo jest kupić egzemplarz ulubionej gry, by potem odkryć w domu, że już ją posiada. Ponadto ten hobby charakteryzuje się niestabilnymi cenami, różnymi poziomami stanu (kasetka bez pudełka, kompletna w pudełku lub CIB, zapieczętowana fabrycznie) oraz częstymi fałszywymi ofertami.

Sprawdzanie ceny proponowanej za pomocą Gemini w czasie rzeczywistym, stojąc przy stoisku, okazuje się zaskakująco przydatne. Wielu kupujących chętnie zapłaci odpowiednio wyższą cenę za możliwość zakupu osobiście oraz za wspieranie lokalnych sprzedawców, ale nikt nie chce zostać oszukany ani zapłacić za fałszywy kartrydż zbyt wysokiej kwoty.

Agent oceniający bierze pod uwagę obie strony: porównuje indywidualną listę kolekcji z aktualnymi danymi z sieci, aby określić sprawiedliwą wartość rynkową.

Jak go używa kolekcjoner

Kolekcjoner wpisuje zwykłe pytania, takie jak:

  • Pytanie, czy dany tytuł, na przykład Chrono Trigger, już znajduje się w kolekcji.
  • Informacja o kompletnym egzemplarzu EarthBound dla SNES w oryginalnej pudełce sprzedawanym za 350 dolarów, wraz z pytaniem, czy już go ktoś posiada oraz czy cena jest korzystniejsza niż obecne ceny rynkowe.
  • Prośba o informację o cenie, jaką zapłacono za Castlevania: Symphony of the Night, oraz o to, czy jej wartość rynkowa od tamtej pory wzrosła.

Umowy dotyczące narzędzi

Aby udzielić odpowiedzi bez wymyślania danych o zapasach czy cenach, agent opiera się na dwóch źródłach informacji:

  1. search_catalog to lokalna funkcja w języku Go, która wyszukuje dane w własnej bazie danych kolekcjonera. Szuka kluczowych słów w tytułach i platformach, a następnie podaje informacje o właścicielu, stanie przedmiotu, dacie nabycia oraz cenie zakupu.
  2. google_search to narzędzie służące do sprawdzania informacji po stronie serwera. Wyszukuje aktualne, sprawiedliwe ceny na platformach handlowych i portalach aukcyjnych, a także ostatnie potwierdzone transakcje i standardy dotyczące stanu przedmiotów.

Strategia rozumowania

Przy ewentualnej transakcji agent postępuje według określonej sekwencji. Najpierw sprawdza lokalny katalog, aby ustalić, czy gra jest już w posiadaniu klienta i w jakim stanie się znajduje. Następnie wyszukuje w internecie aktualne ceny na danej platformie i dla danego poziomu stanu. Na koniec łączy te informacje w ocenę: proponowaną cenę w porównaniu z średnią rynkową, możliwości ulepszenia, takie jak zamiana kasetki bez pudełka na wersję w pudełku, oraz jasną rekomendację.

Wersja pierwsza: Go GenAI SDK

Budowanie bezpośrednio na SDK (google.golang.org/genai) zapewnia najniższy poziom abstrakcji, odpowiadający w pełni protokolowi komunikacyjnemu API Gemini. Bez żadnego frameworku pomiędzy kodem a API sam decydujesz o wysyłaniu zapytań, historii rozmów oraz kryteriach zatrzymania pętli.

Działa przy małych projektach i skryptach użytkowych, dla wszystkich, którzy badają, jak faktycznie funkcjonują wywołania funkcji i ich implementacja, a także w przypadkach o bardzo specyficznych wymaganiach dotyczących pętli, których nie obejmują standardowe frameworki. Wynikiem jest jeden samodzielny plik binarny w języku Go bez żadnych zależności od frameworków, dzięki czemu można go uruchomić na zwykłym hostingu internetowym lub API, takim jak Google Cloud Run, Kubernetes lub zwykła maszyna wirtualna.

Pełna, działająca implementacja:

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,
   })
  }
 }
}

Aby to uruchomić, skonfiguruj projekt w Google Cloud i zaloguj się za pomocą domyślnych uprawnień aplikacji, używając polecenia gcloud auth application-default login:

export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
go run main.go

W terminalu otwiera się interaktywna rozmowa. Rozmowa może trwać kilka rund i kończy się, gdy wpiszesz exit, naciśniesz Ctrl+C lub wyślesz EOF za pomocą 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!

Wersja SDK sprawia, że przepływ sterowania jest jasny i łatwy do zrozumienia. Dzięki silnym modelom oraz dużym oknom tokenów napisanie własnych mechanizmów obsługi, pętli wysyłania narzędzi oraz niezbędnych elementów nośnych okazuje się zaskakująco proste.

Jednak ta łatwość jest pułapką. Każda linia kodu stworzonego od zera staje się trwałym kosztem konserwacji oraz potencjalnym źródłem subtelnych błędów. Tworzenie ram roboczych dla agentów od zera w najlepszym przypadku odwraca uwagę od wdrażania nowych funkcji, a w najgorszym – tworzy górę długu technicznego, który odciąga zasoby od rzeczywistych celów biznesowych. Najlepszy kod to żaden kod, a drugi najlepszy to taki, który rozwiązuje problem przy użyciu jak najmniejszej ilości kodu dostosowanego pod konkretne potrzeby.

Dlaczego przenieść się na framework?

Surowa pętla SDK jest trudna do prześcignięcia pod względem nauki mechanizmów niskiego poziomu lub tworzenia specjalistycznych pętli, jednak aplikacje produkcyjne wymagają wyższego poziomu abstrakcji z kilku powodów:

  • Automaticzne odzwierciedlanie schematów. Ręczne tworzenie schematów JSON za pomocą &genai.Schema{...} jest uciążliwe i podatne na błędy. Frameworki produkcyjne wytwarzają schematy na podstawie natywnych struktur Go oraz komentarzy dokumentacyjnych.
  • Obserwowalność i śledzenie. W warunkach produkcyjnych wymagane są natychmiastowe ślady działania z OpenTelemetry, szczegółowe dane o opóźnieniach w poszczególnych narzędziach oraz metryki zużycia tokenów.
  • Zarządzanie promptami. Prompty zapisane jako ciągi znaków w języku Go utrudniają współpracę z specjalistami od promptów i wykluczają możliwość wersjonowania szablonów oddzielnie od wersji binarnych.
  • Trwałość sesji i stan. Przechowywanie historii wieloetapowych rozmów w ramach bezstanowych żądań HTTP wymaga bezpiecznego pod względem wątków, odseparowanego magazynowania danych.
  • Model przenośności. SDK dla Go jest przeznaczone specjalnie dla Gemini, podczas gdy frameworki umożliwiają zmianę dostawców lub wypróbowanie lokalnych modeli bez ingerencji w logikę biznesową.
  • Ekosystem Go posiada dwa główne frameworki o otwartym kodzie dostosowane do tych potrzeb: Genkit oraz Agent Development Kit.

    Wersja druga: Genkit

    Genkit charakteryzuje się średnim poziomem abstrakcji i wprowadza dyscyplinę inżynierii oprogramowania oraz uporządkowaną możliwość obserwacji w aplikacjach AI. Wszystko jest zorganizowane wokół „przepływów” – silnie typowanych, obserwowalnych łańcuchów przetwarzania – oraz narzędzi, którymi są bezpieczne pod względem typów funkcje w Go z automatycznym generowaniem schematów.

    Podoba się do zadań generowania wykonywanych raz, takich jak narzędzia wiersza poleceń, przetwarzanie zbiorcze i punkty końcowe webhook, a także do deterministycznych procesów pracy i agentów jednodomenowych, jak pipeline wielomodalnego restytuowania zdjęć. Poprzez pluginy obsługuje kilku dostawców modeli. Ponieważ aplikacja Genkit to po prostu standardowy serwer HTTP w języku Go, działa na każdej platformie backendowej, przy czym Google Cloud Run jest preferowanym wyborem ze względu na łatwe hostowanie kontenerów i automatyczne skalowanie.

    Aplikacja oceniająca napisana z użyciem 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.")
    }
    

    Rozpoczęcie przepływu

    Zainstaluj serwer Genkit z skonfigurowanym projektem Google Cloud:

    export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
    export PORT=8080
    go run main.go
    

    Z innego terminala wyślij prośbę o ocenę:

    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 .
    

    Proces jest wykonywany, wywołuje narzędzie katalogowe i zwraca ustrukturyzowany pakiet oceny:

    {
      "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 eliminuje ręcznie pisany pętla wysyłania danych. Rozpakowuje argumenty do natywnych struktur Go, uruchamia narzędzie, zwraca treść do modelu i automatycznie generuje elementy OpenTelemetry dla każdego kroku. Jest idealnym rozwiązaniem, gdy obciążenie pracy przebiega według uporządkowanego, deterministycznego schematu.

    Wersja trzecia: Agent Development Kit

    Podczas gdy Genkit skupia się na uporządkowanych pipeline’ach aplikacji, ADK firmy Google działa na wyższym poziomie abstrakcji. Został zaprojektowany dla autonomicznych agentów konwersacyjnych, orkiestracji wielu agentów oraz złożonych architektur wymagających długoterminowej pamięci i rozwiązań typu RAG w środowiskach korporacyjnych.

    Kit ten standaryzuje również cykle życia agentów, delegację zadań pomiędzy podagentami oraz protokół komunikacji między agentami (A2A), a dzięki adapterom może korzystać z wielu dostawców modeli, tak jak to robi Genkit.

    Dla wdrożenia platforma Gemini Enterprise Agent jest preferowanym rozwiązaniem, jeśli chcesz, aby Google zarządzał przechowywaniem sesji, łącznikami korporacyjnymi oraz pamięcią, dzięki czemu nie musisz samodzielnie programować warstw bazy danych. Zespoły korzystające z własnych magazynów stanu lub mikrosług w kontenerach lepiej radzą sobie z Google Cloud Run.

    Ocena w 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())
     }
    }
    

    Uruchamianie agenta ADK

    ADK dostarcza uniwersalnego uruchamiacza, full.NewLauncher(), więc nie ma potrzeby ręcznego tworzenia mechanizmów routingu HTTP, konwertowania danych do formatu JSON ani szablonów sesji. Argumenty linii poleceń przekazywane do uruchamiacza wybierają tryb interakcji bez żadnych zmian w kodzie aplikacji.

    Aby rozpocząć interaktywną rozmowę w terminalu:

    export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
    go run main.go
    

    Uruchamianie odbywa się w trybie konsoli, a osoba oceniająca może być bezpośrednio pytana:

    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 samodzielnie zarządza stanem rozmowy oraz ciągłością sesji, dzięki czemu kolejne pytania zachowują pełny kontekst wcześniejszych kroków.

    Jako alternatywę dostępna jest wbudowana interfejs użytkownika internetowego dla programistów, dostarczana razem z API REST:

    go run main.go web webui api
    

    Otwarcie http://localhost:8080 umożliwia interaktywną rozmowę z odpowiedziami w czasie rzeczywistym, historię sesji oraz szczegółową wizualizację przebiegu wykonywania narzędzi, obejmującą zarówno dostosowane narzędzie funkcji search_catalog, jak i wbudowane rozwiązanie GoogleSearch.

    ADK wyraźnie oddziela definicję agenta (llmagent), powiązania z narzędziami (functiontool oraz wbudowane elementy takie jak geminitool.GoogleSearch) oraz środowisko wykonywania (launcher i runner). Dzięki temu staje się preferowanym frameworkiem dla złożonych hierarchii wielu agentów oraz asystentów konwersacyjnych.

    Gdzie działają agenci Go

    Napisanie agenta w Go to tylko połowa drogi. Następnie potrzebne jest środowisko wykonawcze, które potrafi radzić sobie z długotrwałymi odpowiedziami strumieniowymi, wywołaniami narzędzi w tle, bezpiecznymi danych uwierzytelniających oraz nagłymi wzrostami ruchu bez problemów operacyjnych. Go kompiluje się do pojedynczych, samodzielnych binarów z niemal zerowymi kosztami działania, dzięki czemu agenci napisani w tym języku są szybkie i tanie w hostowaniu. W zależności od wymagań frameworku i stanu istnieją dwa główne cele.

    Cloud Run – uniwersalny backend

    Google Cloud Run stanowi główny cel dla agentów w formacie kontenerowym w języku Go, niezależnie od tego, czy zostały one stworzone przy użyciu surowego SDK, procesów Genkit, czy narzędzi ADK. Budowa aplikacji w formacie Docker w kilku etapach umożliwia skompilowanie agenta do minimalnego obrazu typu scratch lub distroless bez żadnych zależności runtime. Ponieważ nie trzeba uruchamiać ciężkiego interpretera ani maszyny wirtualnej, usługa szybko skaluje się do poziomu zera, unikając kosztownych opóźnień związanych z uruchamianiem, które występują przy cięższych środowiskach runtime.

    Główne zalety Cloud Run dla agentów w języku Go:

    • Skalowanie do zera przy szybkiej równoczesności. Płacisz tylko za milisekundy wykorzystania CPU, gdy agent faktycznie pracuje lub przetwarza odpowiedzi narzędzi, a lekkie goroutines w języku Go pozwalają jednej instancji kontenera przetwarzać setki zadań agenta równocześnie przy niewielkim zużyciu pamięci.
    • Długi czas oczekiwania na żądania. Domyślnie czas ten wynosi 5 minut, ale można go wydłużyć do 60 minut (3 600 sekund). Dzięki temu pętle rozumowania wieloetapowego, złożone zadania badawcze oraz iteracyjne zespoły podagentów mają wystarczająco dużo czasu na zakończenie pracy.
    • Strumieniowanie dwukierunkowe i WebSockets. Natywnie obsługiwane są transfer w formie fragmentów w protokole HTTP/2, Server-Sent Events oraz WebSockets. W przypadku agentów multimodalnych pracujących w czasie rzeczywistym lub przekształcających głos w głos w ramach API Gemini Live, WebSockets umożliwiają ciągłe strumieniowanie w obie strony pomiędzy klientem a serwerem.
    • Afinitet sesji. Agent, który przechowuje tymczasowe pamięci cache w pamięci lub lokalny kontekst pomiędzy kolejnymi krokami, może użyć opcji --session-affinity, opartej na adrese IP klienta lub pliku cookie, aby wysyłać dalsze informacje do tej samej instancji kontenera.
    • Odporność bez stanu. W środowisku produkcyjnym dane powinny być przechowywane w zewnętrznych, zarządzanych bazach danych, takich jak Firestore, Redis czy Cloud SQL. Po przerwie w połączeniu sieciowym agent może kontynuować działanie dzięki swojemu identyfikatorowi interakcji, bez utraty kontekstu.
    • Tożsamość zadań bez poufnych danych. Nie są potrzebne hardkodowane klucze API ani dane uwierzytelniające, ponieważ agent autoryzuje się w Gemini Developer API, Vertex AI i Cloud Storage za pośrednictwem konta usługowego Cloud Run.

    Platforma agentów Gemini Enterprise

    Agenci konwersacyjne ADK oraz rozwiązania korporacyjne, które muszą przechowywać sesje, utrzymywać długoterminową pamięć i opierać odpowiedzi na wiedzy firmy, mogą korzystać z platformy Gemini Enterprise Agent Platform, określanej jako następca Vertex AI Agent Engine, która oferuje w pełni zarządzane, bezserwerowe środowisko wykonywania. Zamiast konfigurować bazy danych i pisać własne adaptery do przechowywania sesji, zespoły otrzymują zarządzaną infrastrukturę z następującymi funkcjami:

    • Rozdzielona trwałość sesji. Długoterminowe przechowywanie rozmów (SessionService w ADK) jest oddzielone od tymczasowego cyklu wykonywania w formie strumienia (LiveSession), z automatycznym przywracaniem sesji po ponownym połączeniu.
  • Uziemianie przedsiębiorstw i wyszukiwanie wektorowe. Gotowe łączniki umożliwiają dostęp do baz wiedzy przedsiębiorstw takich jak Google Drive, BigQuery oraz repozytoriów intranetowych, a także do indeksów Vertex AI Vector Search, w tym wersji optymalizowanych pod kątem przechowywania, służących do szybkiego wyszukiwania semantycznego.
  • Wykonywanie kodu w środowisku izolowanym. Izolowane środowiska pozwalają agentom pisać i uruchamiać kod, na przykład skrypty do analizy danych lub w językach Python i Go, bez narażania infrastruktury hosta.
  • Protokoły między agentami. Standardowa komunikacja umożliwia niezależnym agentom przedsiębiorstw odkrywanie możliwości, negocjowanie schematów oraz delegowanie zadań pomimo granic organizacyjnych.
  • Bezpieczeństwo, tożsamość agenta i Model Armor. Szczegółowe uprawnienia IAM dla agentów, perymetry VPC Service Controls oraz ochrona w czasie wykonywania Model Armor sprawdzają dane wejściowe i wyjściowe pod kątem iniekcji poleceń, wycieku danych i naruszeń zasad.
  • Wybór spośród trzech rozwiązań

    Aby poznać zasady wywoływania funkcji i ich implementacji lub do prostych zadań, wystarczy surowy cykl SDK. Do zorganizowanych, deterministycznych procesów lub agentów działających w jednej domenie i dostępnych jako punkty końcowe HTTP, odpowiedni jest Genkit. Dla asystentów konwersacyjnych, hierarchii wielu agentów, delegacji i pamięci długoterminowej lepszą bazą jest ADK. Cloud Run nadaje się do wszystkich trzech przypadków, natomiast platforma Gemini Enterprise Agent Platform stanowi naturalne środowisko dla agentów ADK wymagających zarządzanych sesji, pamięci oraz funkcji typu enterprise grounding.

    Literatura pokrewna