Créer un agent Gemini en Go : comparaison du SDK brut, de Genkit et d’ADK
Un agent d’évaluation de jeux rétros a développé trois méthodes en Go, à l’aide du SDK GenAI, de Genkit et du Kit de développement d’agents, ainsi que des instructions pour exécuter chacune d’elles sur Cloud Run ou la plateforme d’agents de Google.
La création d’applications et d’agents autonomes utilisant l’IA en Go peut s’effectuer à trois niveaux d’abstraction différents. Cette présentation décompose les mécanismes d’un agent, choisit un cas d’usage concret, à savoir un évaluateur de jeux rétros, puis le développe à trois reprises :
- Un cycle d’agent de bas niveau écrit directement à l’aide du SDK GenAI de Go.
- Un pipeline structuré orienté flux, construit avec Genkit.
- Un système multi-agent modulaire avec des sessions, basé sur l’Agent Development Kit (ADK) de Google.
Elle se termine par les environnements d’exécution permettant de déployer des agents Go dans le cloud de manière fiable.
Qu’est-ce qui fait d’un élément un agent
Ce terme est utilisé de manière familière, mais en ingénierie de l’IA, il possède une signification architecturale assez précise : un système autonome composé de trois éléments : un LLM, un ensemble d’outils tels que des fonctions appelables ou des API, et un mécanisme qui exécute tout cela au sein d’un cycle de rétroaction.
Un modèle sans outils n’est qu’un générateur de texte. Il ne peut répondre que sur la base de ses poids d’entraînement fixes ou de ce qui figure dans la demande. Pour lui conférer une capacité d’action, c’est-à-dire la possibilité d’inspecter l’état externe, de tester des hypothèses et de modifier des éléments dans le monde réel, il est nécessaire de le relier à des outils exécutables.
Un détail architectural est facile à négliger. Le modèle ne exécute jamais de code ni ne fait appel directement à des API externes. Le système intermédiaire, ici un programme Go, se trouve entre les deux. Lorsque le modèle décide qu’il a besoin d’informations externes ou d’une action, il envoie une demande structurée pour appeler un outil. Le système intermédiaire exécute alors la fonction Go correspondante, capture la sortie et transmet le résultat au modèle pour sa prochaine étape de traitement. Le modèle examine ensuite le nouveau contexte et soit appelle un autre outil, soit rédige sa réponse finale.
Tous les outils ne s’exécutent pas localement. Gemini propose des outils intégrés tels que Google Search grounding, qui s’exécutent du côté serveur. Une fois déclarés dans la configuration de la requête, l’API traite les requêtes de recherche et ajoute des métadonnées de contextualisation à la réponse, sans nécessiter de aller-retour local. Cependant, pour une logique de domaine personnalisée, c’est le framework Go qui gère tout : la définition du schéma, l’envoi des arguments et le retour des résultats serialisés.
Le cas d’usage : un évaluateur de jeux vidéo rétro
Tout ceux qui collectionnent des jeux vidéo rétro connaissent un problème courant. Sur un marché de jeux rétro, il est facile d’acheter une copie d’un jeu apprécié pour découvrir à la maison qu’il se trouve déjà sur l’étagère. De plus, ce hobby se caractérise par des prix volatils, plusieurs niveaux de condition (cartouche non emballée, boîte complète ou CIB, scellée par le fabricant) ainsi que de nombreuses annonces de contrefaçons.
Vérifier en temps réel le prix demandé via Gemini tout en se trouvant devant un stand s’avère être incroyablement utile. De nombreux acheteurs sont prêts à payer un prix élevé pour l’expérience en personne et pour soutenir les vendeurs locaux, mais personne ne veut être arnaqué ou payer trop cher pour un cartouche falsifiée.
L’agent évaluateur prend en compte les deux aspects : il compare l’inventaire personnel avec des données en ligne actualisées pour déterminer une valeur de marché juste.
Comment un collectionneur l’utilise
Le collectionneur tape des questions simples, telles que :
- Une question pour savoir si un titre donné, par exemple Chrono Trigger, fait déjà partie de la collection.
- Un EarthBound complet dans son emballage d’origine pour SNES vendu 350 dollars, avec la question de savoir s’il est déjà en possession de quelqu’un et si ce prix est inférieur aux valeurs actuelles du marché.
Contrats d’outils
Pour répondre sans inventer d’inventaire ou de prix, l’agent s’appuie sur deux sources fiables :
search_catalogest une fonction locale en Go qui interroge la propre base de données du collectionneur. Elle recherche des mots-clés dans les titres et les plateformes, puis indique le propriétaire, l’état, la date d’acquisition et le prix d’achat.google_searchest un outil de vérification côté serveur. Il recherche sur les marchés et les plateformes d’enchères des valeurs marchandes actuelles, des ventes récentes vérifiées ainsi que des références concernant l’état des objets.
Stratégie de raisonnement
Pour une éventuelle vente, l’agent suit une séquence fixe. Il vérifie d’abord le catalogue local afin de déterminer si le jeu est déjà en possession de quelqu’un et dans quel état il se trouve. Ensuite, il recherche sur Internet les prix actuels pour cette plateforme et ce niveau d’état. Enfin, il combine ces informations pour établir une évaluation : le prix demandé par rapport à la moyenne du marché, les possibilités d’amélioration telles que le remplacement d’une cartouche non emballée par une version en boîte, ainsi qu’une recommandation claire.
Version un : le Go GenAI SDK
Le développement directement à partir du SDK (google.golang.org/genai) correspond au niveau d’abstraction le plus bas, avec une correspondance un à un avec le protocole de communication de l’API Gemini. Sans framework entre votre code et l’API, vous contrôlez vous-même l’envoi des outils, l’historique des conversations et les critères d’arrêt du cycle.
Cela fonctionne pour de petits projets et des scripts utilitaires, pour toute personne qui étudie le fonctionnement réel des appels de fonction et de la liaison avec l’environnement, ainsi que pour tout cas nécessitant des boucles très spécifiques que les frameworks standards ne couvrent pas. Le résultat se compile en un binaire Go autonome sans dépendances de framework, ce qui permet de le déployer sur des hébergeurs web ou API ordinaires tels que Google Cloud Run, Kubernetes ou une simple machine virtuelle.
L’implémentation complète et exécutable :
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,
})
}
}
}
Pour l’exécuter, configurez un projet Google Cloud et connectez-vous avec les identifiants par défaut de l’application en utilisant gcloud auth application-default login :
export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
go run main.go
Un chat interactif s’ouvre dans la terminal. La conversation peut durer plusieurs échanges et se termine lorsque vous tapez exit, appuyez sur Ctrl+C ou envoyez EOF avec 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!
La version du SDK rend le flux de contrôle explicite et facile à suivre. Grâce à des modèles puissants et de grandes fenêtres de tokens, il est étonnamment simple d’écrire soi-même les mécanismes de base, les boucles de déploiement d’outils et les structures de soutien nécessaires.
Cette facilité est cependant un piège. Chaque ligne de code développée en interne devient un coût de maintenance permanent et un terrain propice à des bugs subtils. Concevoir un framework d’agent de zéro est, au mieux, une distraction par rapport au développement de fonctionnalités, et au pire, une accumulation de dettes techniques qui détournent les efforts des objectifs commerciaux réels. Le meilleur code, c’est aucun code ; le suivant de mieux en mieux, c’est ce qui résout le problème avec le moins de code personnalisé possible.
Pourquoi passer à un framework
La boucle brute du SDK est difficile à surpasser pour apprendre les mécanismes de bas niveau ou pour créer des boucles sur mesure, mais les applications en production nécessitent un niveau d’abstraction plus élevé, pour plusieurs raisons :
- Réflexion automatique des schémas. Rédiger manuellement des schémas JSON à l’aide de
&genai.Schema{...}est fastidieux et sujette aux erreurs. Les frameworks de production dérivent les schémas à partir des structs natifs de Go ainsi que des commentaires de documentation. - Observabilité et traçabilité. L’utilisation en production exige des traces OpenTelemetry, des analyses de latence par outil et des métriques de consommation de tokens prêtes à l’emploi.
- Gestion des prompts. Les prompts codés en dur en tant que chaînes de caractères Go entravent la collaboration avec les ingénieurs en prompts et empêchent une versionnement des templates séparément des versions binaires.
- Persistance des sessions et état. Conserver l’historique des échanges multiples au sein de requêtes HTTP sans état nécessite un stockage sécurisé par threads et découplé.
L’écosystème Go dispose de deux principaux frameworks open source pour ces besoins : Genkit et l’Agent Development Kit.
Version deux : Genkit
Genkit offre un niveau d’abstraction intermédiaire et apporte une discipline d’ingénierie logicielle ainsi qu’une observabilité structurée aux applications d’IA. Tout est organisé autour de flux, qui sont des pipelines fortement typés et observables, ainsi que d’outils, qui sont des fonctions Go sécurisées par type avec génération automatique de schémas.
Il convient aux tâches de génération ponctuelles telles que les outils en ligne de commande, le traitement par lots et les points de terminaison webhook, ainsi qu’aux workflows déterministes et aux agents monodomaine comme un pipeline de restauration photo multimodal. Il prend en charge plusieurs fournisseurs de modèles via des plugins. Comme une application Genkit n’est rien de plus qu’un serveur HTTP standard en Go, elle fonctionne sur n’importe quelle plateforme backend, Google Cloud Run étant la cible privilégiée en raison de l’hébergement facile des conteneurs et de l’échelle automatique.
L’appréciateur écrit avec 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.")
}
Exécution du flux
Démarrez le serveur Genkit avec votre projet Google Cloud configuré :
export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
export PORT=8080
go run main.go
Depuis un autre terminal, envoyez une demande d’évaluation :
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 .
Le flux s’exécute, appelle l’outil de catalogue et renvoie un en-tête d’évaluation structuré :
{
"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 élimine la boucle de traitement manuelle. Il déstructure les arguments en structs natifs de Go, exécute l’outil, renvoie la charge utile au modèle et génère automatiquement des spans OpenTelemetry pour chaque étape. C’est la solution idéale lorsque la charge de travail suit un flux structuré et déterministe.
Version trois : le Kit de développement d’agents
Alors que Genkit cible des pipelines d’applications structurés, le ADK de Google fonctionne à un niveau d’abstraction élevé. Il a été conçu pour des agents conversationnels autonomes, l’orchestration de multiples agents ainsi que des architectures complexes nécessitant une mémoire à long terme et des systèmes RAG d’entreprise.
Le kit standardise également les cycles de vie des agents, la délégation de tâches aux sous-agents et le protocole de communication agent-à-agent (A2A) ; il peut en outre utiliser plusieurs fournisseurs de modèles via des adaptateurs, tout comme Genkit.
Pour le déploiement, la plateforme Gemini Enterprise Agent est la solution privilégiée lorsque vous souhaitez que Google gère le stockage des sessions, les connecteurs d’entreprise ainsi que la mémoire, afin d’éviter de devoir développer vos propres couches de base de données. Les équipes qui utilisent leurs propres systèmes de stockage d’état ou des microservices conteneurisés trouveront un meilleur soutien avec Google Cloud Run.
L’évaluateur d’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())
}
}
Exécution de l’agent ADK
ADK fournit un lanceur universel, full.NewLauncher(), ce qui évite la nécessité de développer manuellement un routage HTTP, un marquage JSON ou du code de base pour les sessions. Les arguments en ligne de commande transmis au lanceur permettent de choisir le mode d’interaction sans aucune modification du code de l’application.
Pour démarrer une conversation interactive en terminal :
export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
go run main.go
Le lanceur démarre en mode console et l’évaluateur peut être interrogé directement :
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 gère lui-même l’état de la conversation et la continuité de la session, de sorte que les questions suivantes conservent tout le contexte des échanges précédents.
Comme alternative, il existe une interface web dédiée aux développeurs intégrée, accompagnée d’une API REST :
go run main.go web webui api
En ouvrant http://localhost:8080, on accède à une conversation interactive avec des réponses en streaming en temps réel, un historique des sessions ainsi qu’une visibilité étape par étape des exécutions des outils, couvrant à la fois l’outil fonction personnalisée search_catalog et la fonction native GoogleSearch.
ADK sépare clairement la définition de l’agent (llmagent), les liens avec les outils (functiontool et des fonctions intégrées comme geminitool.GoogleSearch) ainsi que l’environnement d’exécution (launcher et runner). Cela en fait le cadre de prédilection pour des hiérarchies multi-agents complexes et des assistants conversationnels.
Où s’exécutent les agents Go
Rédiger un agent en Go n’est que la moitié du travail. Il nécessite ensuite un environnement d’exécution capable de gérer des réponses en streaming de longue durée, des appels à des outils en arrière-plan, des identifiants sécurisés ainsi que des pics de trafic, sans difficultés opérationnelles. Go compile en binaires autonomes ne comportant presque aucun surcoût en temps de exécution, ce qui permet aux agents écrits dans ce langage d’être rapides et peu coûteux à héberger. En fonction du framework et des exigences en matière d’état, deux cibles principales s’offrent à nous.
Cloud Run, le backend polyvalent
Google Cloud Run constitue la cible principale pour les agents Go conteneurisés, qu’ils soient développés à l’aide du SDK brut, des flux Genkit ou des exécutants ADK. Les builds Docker à plusieurs étapes permettent de compiler l’agent en une image minimale scratch ou distroless ne contenant aucune dépendance de runtime. Sans besoin de démarrer un interpréteur lourd ou une machine virtuelle, le service peut s’adapter rapidement à zéro instance, évitant ainsi les coûts liés au démarrage lent que subissent les environnements de runtime plus lourds.
Les principaux avantages de Cloud Run pour les agents Go :
- Échelle à zéro avec une forte concurrence. Vous ne payez que pour les millisecondes d’utilisation du CPU pendant que l’agent travaille réellement ou traite les réponses des outils, et les goroutines légères de Go permettent à une instance de conteneur de gérer des centaines d’opérations de l’agent en même temps avec une faible consommation mémoire.
- Délais d’expiration longs pour les requêtes. Les requêtes ont par défaut une limite de 5 minutes, qui peut être prolongée jusqu’à 60 minutes (3 600 secondes). Cela permet aux boucles de raisonnement à plusieurs étapes, aux tâches de recherche approfondies et aux essaims de sous-agents itératifs d’avoir suffisamment de temps pour s’achever.
- Flux bidirectionnels et WebSockets. Le transfert par tranches HTTP/2, les événements envoyés par le serveur et les WebSockets sont pris en charge nativement. Pour les agents multimodaux en temps réel ou de voix à voix via l’API Gemini Live, les WebSockets assurent un flux continu bidirectionnel entre le client et le serveur.
- Affinité de session. Un agent qui conserve des caches temporaires en mémoire ou un contexte local entre les tours peut utiliser
--session-affinity, en se basant sur l’IP du client ou les cookies, afin d’envoyer des demandes supplémentaires à la même instance de conteneur. - Résilience sans état. En production, les données doivent être stockées dans des systèmes de gestion externes tels que Firestore, Redis ou Cloud SQL. En cas de coupure de réseau au milieu d’une opération, l’agent peut reprendre son travail en utilisant son identifiant d’interaction sans perdre de contexte.
- Identité des charges de travail sans secrets. Aucune clé API ou identifiants enregistrés manuellement n’est nécessaire, car l’agent s’authentifie auprès de la Gemini Developer API, de Vertex AI et de Cloud Storage via le compte de service Cloud Run en cours d’exécution.
Plateforme d’agents Gemini Enterprise
Les agents ADK conversationnels ainsi que les solutions d’entreprise qui doivent conserver les sessions, stocker de la mémoire à long terme et fonder leurs réponses sur les connaissances de l’entreprise peuvent utiliser la plateforme Gemini Enterprise Agent Platform, présentée comme la successeure de Vertex AI Agent Engine, qui offre un environnement d’exécution entièrement géré et sans serveur. Au lieu de mettre en place des bases de données et d’écrire des adaptateurs personnalisés pour le stockage des sessions, les équipes disposent d’une infrastructure gérée dotée des fonctionnalités suivantes :
- Persistence des sessions déconnectée. Le stockage à long terme des conversations (
SessionServicedans ADK) est séparé du cycle d’exécution en flux temporaire (LiveSession), avec des mécanismes automatiques de reprise des sessions lors des reconnections. - Annulation de mise à jour à l’échelle entreprise et recherche vectorielle. Des connecteurs prêts à l’emploi permettent d’accéder à des bases de connaissances d’entreprise telles que Google Drive, BigQuery et des dépôts intranet, ainsi qu’aux index de recherche vectorielle de Vertex AI, y compris des niveaux optimisés pour le stockage, afin d’obtenir une récupération sémantique rapide.
- Exécution de code en environnement isolé. Des environnements isolés permettent aux agents d’écrire et d’exécuter du code, par exemple des analyses de données ou des scripts en Python et Go, sans exposer l’infrastructure hôte.
- Protocoles d’agent à agent. Une communication standard permet aux agents d’entreprise indépendants de découvrir leurs capacités, de négocier des schémas et de déléguer des tâches au-delà des frontières organisationnelles.
- Sécurité, identité de l’agent et Model Armor. Des permissions IAM fines pour les agents, des périmètres de contrôle des services VPC ainsi que la protection en temps réel de Model Armor inspectent les entrées et les sorties afin de détecter les injections de prompts, l’exfiltration de données et les violations des politiques.
Choix parmi les trois
Pour comprendre le fonctionnement des appels de fonctions et du grounding, ou pour une petite utilité, la boucle SDK brute suffit. Pour un pipeline structuré et déterministe ou un agent monodomaine exposé via une interface HTTP, Genkit convient. Pour les assistants conversationnels, les hiérarchies multi-agents, la délégation et la mémoire à long terme, ADK constitue une base plus adaptée. Cloud Run convient aux trois cas, tandis que la plateforme Gemini Enterprise Agent Platform est l’option idéale pour les agents ADK nécessitant des sessions gérées, de la mémoire et un grounding d’entreprise.