首页 / 文章

用 Go 语言构建 Gemini Agent:Raw SDK、Genkit 与 ADK 的对比

有一位复古游戏评估师开发了三种基于 Go 的代理程序,分别使用了 GenAI SDK、Genkit 以及 Agent Development Kit,并介绍了如何在 Cloud Run 或谷歌的代理平台上运行这些程序。

发布者:REACTAPP.TOP阅读约 19 分钟4462 词

来源: Gemini for Go Developers: Building Agents in Go

使用 Go 语言构建具备人工智能功能的应用程序和自主智能体可以在三种不同的抽象层级上进行。本指南将拆解智能体的运作机制,选定一个具体的应用场景——复古游戏评估器,然后分别用三种方式实现它:

  1. 直接基于 Go GenAI SDK 编写的低级智能体循环。
  2. 利用 Genkit 构建的具有结构化流程的管道。
  3. 基于 Google 的 Agent Development Kit (ADK) 构建的带有会话功能的模块化多智能体系统。

最后会介绍可用于将 Go 智能体可靠地部署到云端的运行时环境。

什么构成一个智能体

这个术语虽被随意使用,但在人工智能工程中其实有着相当明确的架构含义:它指的是一个由三部分构成的自主系统——大型语言模型、诸如可调用函数或API之类的工具集,以及在一个反馈循环中执行所有操作的协调机制。

没有工具的模型仅仅是一个文本生成器,它只能依据固定的训练权重或提示内容来回答问题。要赋予其自主能力,即让其能够感知外部状态、验证假设并在现实世界中做出改变,就必须将其与可执行的工具相连。

有一个架构细节很容易被忽视。该模型本身从不运行代码或调用外部 API。位于中间的本地处理程序——此处为 Go 程序——负责协调工作。当模型决定需要外部信息或执行某项操作时,它会发出结构化的工具调用请求。处理程序会运行相应的 Go 函数,捕获输出结果,并将其反馈给下一个模型处理环节。模型随后会根据新的上下文信息,要么调用其他工具,要么输出最终答案。

并非所有工具都在本地运行。Gemini提供了诸如Google搜索关联功能等内置工具,这些工具均在服务器端执行。一旦在请求配置中完成设置,API就会直接处理搜索查询,并将关联的元数据附加到响应中,无需进行任何本地往返操作。不过,对于自定义的领域逻辑,Go语言框架会负责所有工作:定义数据结构、传递参数以及返回序列化后的结果。

应用场景:复古游戏评估器

任何收集复古视频游戏的人都会遇到一个常见的问题。在复古游戏市场上,很容易买到自己心仪的游戏,但回到家后发现它已经在自己的收藏架上。此外,这一爱好领域的价格波动较大,游戏状态也有多种分类(散装卡带、原盒完整或CIB状态、出厂密封状态),而且假冒商品也屡见不鲜。

站在摊位前就能实时通过Gemini查询物品的定价,这一功能其实非常实用。许多买家愿意为这种面对面的体验以及支持本地卖家而支付合理的溢价,但没人希望被骗或为假货多付钱。

该评估工具能兼顾双方需求:它既会比对个人拥有的藏品清单,也会根据实时网络数据来判断物品的合理市场价值。

收藏家如何使用它

收藏家可以输入一些常见的问题,例如:

  • 询问某款作品,比如《时之笛》,是否已经收录在自己的收藏中。
  • 发现一款售价350美元的SNES版《地球冒险》原封未拆版本,想知道是否已被他人拥有,以及该价格是否低于当前市场行情。
  • 询问《恶魔城:夜之交响曲》的售价,以及此后其市场价值是否上升。
  • 工具合约

    为在不虚构库存信息或价格的情况下给出答案,该代理依赖两个可靠信息源:

    1. search_catalog 是一个本地 Go 函数,用于查询收藏家自身的数据库。它会根据标题和平台查找关键词,然后反馈物品的所有权、状况、获取日期及购买价格。
    2. google_search 是一个服务器端查询工具,用于在各类市场平台及拍卖追踪网站上查找当前的合理价格、近期已验证的成交记录以及物品状况的参考标准。

    推理策略

    在考虑是否购买时,代理商会按照固定流程操作。首先查看本地目录,确认该游戏是否已被拥有以及现状如何。接着在网络上搜索该平台及相应状况下的当前价格。最后将所有信息整合成一份评估报告:包括建议售价与市场平均价的对比、升级选项(如将散装游戏卡带更换为盒装版),以及明确的购买建议。

    版本一:Go GenAI SDK

    直接基于SDK(google.golang.org/genai)进行开发属于最低抽象层级,能够与Gemini API的通信协议实现一一对应。由于代码与API之间没有中间框架,你可以自行控制工具调用、对话历史以及循环终止的条件。

    它适用于小型项目与实用脚本,适合那些想要了解函数调用机制的人,也适用于标准框架无法满足的、具有高度特定循环需求的情况。其输出为无需任何框架依赖的独立 Go 二进制文件,因此可以部署在 Google Cloud Run、Kubernetes 或普通虚拟机等常规 Web 或 API 托管平台上。

    完整的可运行实现代码如下:

    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或使用Ctrl+D发送EOF时对话即结束:

    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的循环结构难以被超越,但出于多种原因,生产环境中的应用更需要更高层次的抽象:

    • 自动模式反射。使用&genai.Schema{...}手动编写JSON模式既繁琐又容易出错。生产级框架能够直接从Go原生结构体和文档注释中生成模式。
    • 可观测性与追踪。实际应用需要开箱即用的OpenTelemetry追踪功能、针对各工具的延迟分析以及令牌消耗指标。
    • 提示词管理。将提示词硬编码为Go字符串会妨碍与提示词工程师的合作,也无法实现与二进制版本分离的模板版本控制。
    • 会话持久化与状态管理。在无状态HTTP请求之间保留多轮对话历史需要线程安全且解耦的存储方案。
  • 模型可移植性。Go SDK是专为Gemini设计的,而框架则允许你在不改动业务逻辑的情况下更换服务提供商或尝试本地模型。
  • 为满足这些需求,Go生态系统中主要有两个开源框架:Genkit和Agent Development Kit。

    第二版:Genkit

    Genkit具有中等抽象层次,它为人工智能应用带来了软件工程化的规范与结构化的可观测性。所有内容都围绕“流”来组织,这些流是强类型的、可观测的管道;而工具则是具有类型安全性的Go函数,并能自动生成架构规范。

    它适用于一次性生成任务,如命令行工具、批处理和 webhook 端点,以及确定性工作流和单域代理,例如多模态照片修复流程。它可通过插件支持多种模型提供商。由于 Genkit 应用本质上只是标准的 Go HTTP 服务器,因此可在任何后端平台上运行,其中 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.")
    }
    

    运行流程

    使用已配置好 Google Cloud 项目的环境来启动 Genkit 服务器:

    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跟踪数据。当工作负载遵循结构化、确定性的流程时,这就是最佳解决方案。

    第三版:Agent开发工具包

    虽然Genkit针对的是结构化的应用程序流程,但谷歌的ADK则处于更高级别的抽象层面。它专为自主对话型智能体、多智能体协调以及需要长期记忆功能和企业级RAG技术的复杂架构而设计。

    该工具包还标准化了智能体的生命周期、子智能体的任务分配以及智能体间的通信协议,并且能够像Genkit一样通过适配器使用多种模型提供商。

    在部署方面,如果您希望由 Google 负责会话存储、企业连接器及内存管理,从而无需自行编写数据库层代码,那么 Gemini Enterprise Agent Platform 是最佳选择。而对于那些自行管理状态存储或使用容器化微服务的团队来说,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

    对于基于容器化的 Go 代理而言,Google Cloud Run 是首要选择,无论这些代理是用原始 SDK、Genkit 流程还是 ADK 运行器构建的。多阶段 Docker 构建可以将代理编译为不含任何运行时依赖的极简 scratch 或 distroless 镜像。由于无需启动沉重的解释器或虚拟机,该服务能够快速实现从零扩展,避免了那些重量级运行时所带来的冷启动开销。

    Cloud Run 对 Go 代理的主要优势包括:

    • 具备快速并发能力的零扩展能力。您只需为代理实际工作或处理工具响应时所消耗的 CPU 毫秒数付费,而 Go 轻量级的 goroutine 允许单个容器实例以较低的内存占用同时处理数百次代理请求。
  • 较长的请求超时时间。请求的默认限制为5分钟,可延长至60分钟(3,600秒)。这样,多步骤推理循环、复杂的研究任务以及迭代式子代理群就有足够的时间完成。
  • 双向流与WebSocket。HTTP/2的分块传输、服务器推送事件以及WebSocket均得到原生支持。对于Gemini Live API中的语音对语音或实时多模态代理,WebSocket可实现客户端与服务器之间的持续双向流传输。
  • 会话关联性。那些在多次交互之间会保留临时内存缓存或本地上下文的代理,可以通过客户端IP或Cookie启用--session-affinity选项,从而将后续请求发送到同一个容器实例。
  • 无状态弹性。在生产环境中,状态应存储在Firestore、Redis或Cloud SQL等外部托管数据存储中。当在处理过程中发生网络中断时,代理可通过其交互ID继续运行,而不会丢失上下文。
  • 无需密钥的工作负载身份认证。由于代理可通过现有的Cloud Run服务账户对Gemini Developer API、Vertex AI和Cloud Storage进行身份验证,因此无需硬编码API密钥或凭证。
  • Gemini企业级代理平台

    需要保持会话、存储长期记忆并将答案基于企业知识库的对话型 ADK 智能体及企业级解决方案,可选用 Gemini 企业智能体平台。该平台被视为 Vertex AI Agent Engine 的继任者,提供完全托管的无服务器执行环境。团队无需自行配置数据库或编写自定义会话存储适配器,即可获得具备以下功能的管理型基础设施:

    • 解耦的会话持久化机制。 长期对话存储(ADK 中的 SessionService)与临时的流式执行循环(LiveSession)相互分离,且能在重新连接时自动恢复会话。
  • 企业级数据关联与向量搜索。现成的连接器可接入谷歌云端硬盘、BigQuery等企业知识库以及内网存储库,同时还能连接Vertex AI向量搜索索引,其中包括针对存储优化设计的层级,从而实现快速的语义检索。
  • 沙箱式代码执行。隔离式环境允许智能体编写并运行代码,例如数据分析脚本或Python、Go语言编写的脚本,而不会暴露主机基础设施。
  • 智能体间通信协议。标准化的通信机制使得独立的企业智能体能够发现彼此的功能、协商数据结构,并在组织边界之间分配任务。
  • 安全性、智能体身份与模型护盾。精细化的智能体IAM权限、VPC服务控制边界以及模型护盾运行时保护功能,能够检测输入和输出中的提示词注入、数据窃取及政策违规行为。
  • 三种方案的选择

    若只是想了解函数调用与基础功能实现方式,或用于小型工具,使用原始SDK循环即可。对于结构化、可预测的流程,或是作为HTTP端点提供的单领域智能体,Genkit更为合适。而对于对话助手、多智能体层级架构、任务委托及长期记忆功能,ADK则是更佳的基础选择。Cloud Run适用于这三种场景,而Gemini企业智能体平台则专为需要托管会话、内存管理及企业级基础功能的ADK智能体设计。