第八章:高级特性 -- 解锁更多可能

恭喜你走到这里!前面七章我们把 ADK-Go 的核心玩法都过了一遍。但 ADK-Go 的能力远不止那些,它还有很多"隐藏技能"等着你来解锁。

这一章我们就来聊聊那些高级特性 -- 文件管理、可观测性、结构化输出、上下文控制、模型参数调优、企业级集成,以及怎么写出可测试的 Agent。

本章你将学到

  • 用 Artifact 服务管理文件和二进制数据(图片、PDF、音频等)
  • 集成 OpenTelemetry 实现可观测性(追踪、监控)
  • 用 Schema 控制 Agent 的输入输出格式
  • IncludeContents 选项控制 Agent 看到的上下文
  • GlobalInstruction 和 Instruction 的区别与配合
  • GenerateContentConfig 精细调优 LLM 行为
  • Vertex AI 企业级集成
  • 构建可测试的 Agent 的最佳实践

内容不少,但每个点我都会讲得清楚明白。准备好了就开始吧!


1. Artifact:文件和工件管理

1.1 什么是 Artifact?

在跟 Agent 对话的过程中,你经常会遇到需要处理文件的场景:用户上传了一张图片让 Agent 分析、Agent 生成了一份 PDF 报告、或者需要保存一段音频。这些二进制数据怎么管理?

ADK-Go 提供了 Artifact 服务来解决这个问题。你可以把它理解为一个跟 Session 绑定的"文件柜" -- 每个文件都通过 AppName + UserID + SessionID + FileName 来唯一标识,而且还自带版本管理。

简单来说: - Artifact = 跟会话关联的文件/二进制数据 - 支持 版本控制,每次保存同名文件会自动递增版本号 - 支持 用户级别共享,文件名以 user: 开头的 artifact 可以在同一用户的所有会话中共享

1.2 创建和使用 Artifact 服务

ADK-Go 内置了一个基于内存的 Artifact 服务,适合开发和测试。来看怎么用:

package main

import (
    "context"
    "fmt"
    "os"

    "google.golang.org/genai"

    "google.golang.org/adk/artifact"
)

func main() {
    ctx := context.Background()

    // 创建一个内存版的 Artifact 服务
    // 生产环境可以换成 GCS 版本:gcsartifact.New(...)
    artifactService := artifact.InMemoryService()

    // ============ 保存工件 ============

    // 读取一张图片
    imageBytes, err := os.ReadFile("photo.png")
    if err != nil {
        panic(err)
    }

    // 把图片保存为 artifact
    saveResp, err := artifactService.Save(ctx, &artifact.SaveRequest{
        AppName:   "my_app",
        UserID:    "user_123",
        SessionID: "session_abc",
        FileName:  "photo.png",
        // Part 是 genai.Part,可以存储任意二进制数据
        Part: &genai.Part{
            InlineData: &genai.Blob{
                MIMEType: "image/png",
                Data:     imageBytes,
            },
        },
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("保存成功,版本号: %d\n", saveResp.Version) // 输出: 保存成功,版本号: 1

    // ============ 加载工件 ============

    // 加载最新版本
    loadResp, err := artifactService.Load(ctx, &artifact.LoadRequest{
        AppName:   "my_app",
        UserID:    "user_123",
        SessionID: "session_abc",
        FileName:  "photo.png",
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("加载成功,数据大小: %d 字节\n", len(loadResp.Part.InlineData.Data))

    // 加载指定版本
    loadResp, err = artifactService.Load(ctx, &artifact.LoadRequest{
        AppName:   "my_app",
        UserID:    "user_123",
        SessionID: "session_abc",
        FileName:  "photo.png",
        Version:   1, // 明确指定版本号
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("加载版本1成功\n")
}

1.3 版本控制

Artifact 的版本控制是自动的。每次对同一个文件调用 Save,版本号会自动加 1:

// 第一次保存 -> 版本 1
resp1, _ := artifactService.Save(ctx, &artifact.SaveRequest{
    AppName: "my_app", UserID: "user_123",
    SessionID: "session_abc", FileName: "report.pdf",
    Part: &genai.Part{
        InlineData: &genai.Blob{
            MIMEType: "application/pdf",
            Data:     pdfV1Bytes,
        },
    },
})
fmt.Println(resp1.Version) // 1

// 第二次保存同名文件 -> 版本 2
resp2, _ := artifactService.Save(ctx, &artifact.SaveRequest{
    AppName: "my_app", UserID: "user_123",
    SessionID: "session_abc", FileName: "report.pdf",
    Part: &genai.Part{
        InlineData: &genai.Blob{
            MIMEType: "application/pdf",
            Data:     pdfV2Bytes,
        },
    },
})
fmt.Println(resp2.Version) // 2

// 查看所有版本
versionsResp, _ := artifactService.Versions(ctx, &artifact.VersionsRequest{
    AppName: "my_app", UserID: "user_123",
    SessionID: "session_abc", FileName: "report.pdf",
})
fmt.Println(versionsResp.Versions) // [2, 1]  -- 最新的在前面

// 列出会话中的所有文件
listResp, _ := artifactService.List(ctx, &artifact.ListRequest{
    AppName: "my_app", UserID: "user_123",
    SessionID: "session_abc",
})
fmt.Println(listResp.FileNames) // [photo.png, report.pdf]

1.4 在 Runner 中配置 Artifact 服务

要让你的 Agent 能使用 Artifact,需要在创建 Runner 时传入 Artifact 服务:

import (
    "google.golang.org/adk/artifact"
    "google.golang.org/adk/runner"
    "google.golang.org/adk/session"
)

// 创建 Runner 时注入 Artifact 服务
r, err := runner.New(runner.Config{
    AppName:         "my_app",
    Agent:           myAgent,
    SessionService:  session.InMemoryService(),
    ArtifactService: artifact.InMemoryService(), // 加上这一行!
})
if err != nil {
    panic(err)
}

配置好之后,Agent 在运行时就可以通过 ctx.Artifacts() 来访问 Artifact 服务了。

1.5 用 loadartifactstool 让 Agent 自动加载文件

ADK-Go 提供了一个内置工具 loadartifactstool,它能让 LLM 自动发现和加载 Artifact。这个工具会:

  1. 自动告诉 LLM 当前会话有哪些 artifact
  2. 当用户问到某个文件时,LLM 会自动调用这个工具来加载文件内容
import (
    "google.golang.org/adk/agent/llmagent"
    "google.golang.org/adk/tool"
    "google.golang.org/adk/tool/loadartifactstool"
)

// 创建一个能访问文件的 Agent
myAgent, err := llmagent.New(llmagent.Config{
    Name:        "file_assistant",
    Model:       model,
    Instruction: "你是一个文件助手,帮用户管理和分析文件。",
    Tools: []tool.Tool{
        loadartifactstool.New(), // 加上这个内置工具
    },
})

这样用户就可以自然地跟 Agent 说"帮我看看 photo.png 里有什么",Agent 会自动去加载对应的 artifact。

1.6 在自定义工具中操作 Artifact

如果你写了自定义工具,也可以通过 tool.Context 来操作 Artifact:

import (
    "google.golang.org/genai"
    "google.golang.org/adk/tool"
)

// 一个会保存文件的自定义工具
func saveReportTool() tool.Tool {
    return tool.NewTool(
        &genai.FunctionDeclaration{
            Name:        "save_report",
            Description: "生成并保存一份报告",
            Parameters: &genai.Schema{
                Type: genai.TypeObject,
                Properties: map[string]*genai.Schema{
                    "title":   {Type: genai.TypeString, Description: "报告标题"},
                    "content": {Type: genai.TypeString, Description: "报告内容"},
                },
                Required: []string{"title", "content"},
            },
        },
        func(ctx tool.Context, args map[string]any) (map[string]any, error) {
            title := args["title"].(string)
            content := args["content"].(string)

            // 通过 ctx.Artifacts() 保存工件
            artifacts := ctx.Artifacts()
            if artifacts == nil {
                return map[string]any{"error": "Artifact 服务未配置"}, nil
            }

            // 保存为文本格式的 artifact
            fileName := title + ".txt"
            _, err := artifacts.Save(ctx, fileName, &genai.Part{
                Text: content,
            })
            if err != nil {
                return map[string]any{"error": err.Error()}, nil
            }

            return map[string]any{
                "status":   "success",
                "fileName": fileName,
                "message":  "报告已保存",
            }, nil
        },
    )
}

1.7 用户级共享 Artifact

文件名以 user: 开头的 artifact 会存储在用户级别的命名空间中,同一个用户的所有会话都能访问到:

// 保存一个用户级别的 artifact
// 注意文件名以 "user:" 开头
artifactService.Save(ctx, &artifact.SaveRequest{
    AppName:   "my_app",
    UserID:    "user_123",
    SessionID: "session_abc",            // 虽然指定了 session,但会存到用户级别
    FileName:  "user:profile_photo.png", // "user:" 前缀是关键
    Part: &genai.Part{
        InlineData: &genai.Blob{
            MIMEType: "image/png",
            Data:     photoBytes,
        },
    },
})

// 在另一个会话中也能加载到
loadResp, _ := artifactService.Load(ctx, &artifact.LoadRequest{
    AppName:   "my_app",
    UserID:    "user_123",
    SessionID: "session_xyz", // 不同的会话
    FileName:  "user:profile_photo.png",
})
// loadResp.Part 就是之前保存的图片

这在一些场景下特别有用,比如用户上传了头像或个人偏好文件,希望所有对话都能用到。


2. OpenTelemetry 遥测 -- 可观测性

2.1 为什么需要可观测性?

当你的 Agent 上了生产环境,你就会开始问这些问题: - 这次调用为什么这么慢? - LLM 调用花了多久?工具调用花了多久? - 哪些 Agent 被调用了?调用顺序是什么? - 用户的请求经过了哪些环节?

OpenTelemetry(简称 OTel) 是业界标准的可观测性框架,ADK-Go 原生支持它。配好之后,你的 Agent 的每一次调用都会生成追踪(trace)数据,让你一目了然地看到整个执行链路。

2.2 基本配置

ADK-Go 的 telemetry 包提供了开箱即用的 OTel 集成:

package main

import (
    "context"
    "log"
    "time"

    "go.opentelemetry.io/otel/sdk/resource"
    semconv "go.opentelemetry.io/otel/semconv/v1.36.0"

    "google.golang.org/adk/telemetry"
)

func main() {
    ctx := context.Background()

    // 第一步:创建 OTel Resource,描述你的服务
    res, err := resource.New(ctx,
        resource.WithAttributes(
            // 服务名称,在追踪后端会用这个来标识你的服务
            semconv.ServiceNameKey.String("my-agent-service"),
            // 版本号,方便区分不同版本的行为
            semconv.ServiceVersionKey.String("1.0.0"),
        ),
    )
    if err != nil {
        log.Fatalf("创建 resource 失败: %v", err)
    }

    // 第二步:初始化遥测
    telemetryProviders, err := telemetry.New(ctx,
        telemetry.WithResource(res),
        // 如果要导出到 GCP Cloud Trace,打开这个开关
        // telemetry.WithOtelToCloud(true),
    )
    if err != nil {
        log.Fatalf("初始化遥测失败: %v", err)
    }

    // 第三步:程序退出时优雅关闭(确保数据都发送出去了)
    defer func() {
        shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        if err := telemetryProviders.Shutdown(shutdownCtx); err != nil {
            log.Printf("遥测关闭失败: %v", err)
        }
    }()

    // 第四步:注册为全局 Provider
    telemetryProviders.SetGlobalOtelProviders()

    // 接下来正常创建 Agent 和 Runner 就行了
    // ADK-Go 内部会自动使用全局 Provider 来生成追踪数据
    // ...
}

2.3 导出到不同的后端

ADK-Go 的遥测支持多种导出方式:

方式一:导出到 GCP Cloud Trace

// 直接导出到 Google Cloud Trace
telemetryProviders, err := telemetry.New(ctx,
    telemetry.WithOtelToCloud(true),  // 开启 GCP 导出
    telemetry.WithResource(res),
    // 可选:指定 GCP 项目
    // telemetry.WithGcpResourceProject("my-gcp-project"),
    // telemetry.WithGcpQuotaProject("my-gcp-project"),
)

方式二:通过 OTLP 导出到 Jaeger、Zipkin 等

只需要设置环境变量,ADK-Go 会自动检测并创建 OTLP HTTP 导出器:

# 设置 OTLP 端点,ADK-Go 自动检测这些环境变量
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
# 或者只设置 traces 端点
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="http://localhost:4318/v1/traces"
// 代码里不需要额外配置,ADK-Go 会自动读取环境变量
telemetryProviders, err := telemetry.New(ctx,
    telemetry.WithResource(res),
)

方式三:自定义 SpanProcessor

如果你有特殊需求,可以注册自定义的 SpanProcessor:

import (
    "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

// 创建一个把追踪数据打印到控制台的导出器(适合调试)
consoleExporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
if err != nil {
    log.Fatal(err)
}

telemetryProviders, err := telemetry.New(ctx,
    telemetry.WithResource(res),
    // 注册自定义的 SpanProcessor
    telemetry.WithSpanProcessors(
        sdktrace.NewSimpleSpanProcessor(consoleExporter),
    ),
)

2.4 追踪到的内容

配好遥测之后,ADK-Go 会自动为以下操作生成 span:

  • Agent 调用:每次 agent.Run() 都会创建一个 span,包含 Agent 名称、Session ID 等信息
  • Agent 转移:当一个 Agent 把控制权转给另一个 Agent 时,链路关系一目了然
  • Agent 回复:最终的响应内容和错误信息都会记录在 span 中

通过这些追踪数据,你可以在 Jaeger、Cloud Trace 或其他兼容 OTel 的后端中看到完整的调用链,就像这样:

[Runner.Run]
  |-- [Agent: coordinator]           (200ms)
  |     |-- [LLM Call]               (180ms)
  |-- [Agent: data_analyst]          (500ms)
  |     |-- [LLM Call]               (300ms)
  |     |-- [Tool: query_database]   (150ms)
  |     |-- [LLM Call]               (50ms)

这对排查问题、优化性能简直太有用了。


3. 输入输出 Schema 控制

3.1 为什么要控制 Schema?

默认情况下,LLM 回复的是自由格式的文本。但在很多场景下,你需要 Agent 返回结构化的数据 -- 比如 JSON 格式的分析结果、标准化的评分、或者固定格式的回复。

ADK-Go 通过 InputSchemaOutputSchema 让你精确控制 Agent 的输入输出格式。

3.2 OutputSchema -- 结构化输出

这是最常用的功能。设置 OutputSchema 后,LLM 的回复会严格遵循你定义的 JSON Schema:

import (
    "google.golang.org/genai"
    "google.golang.org/adk/agent/llmagent"
)

// 创建一个返回结构化数据的 Agent
agent, err := llmagent.New(llmagent.Config{
    Name:  "sentiment_analyzer",
    Model: model,
    Instruction: `你是一个情感分析专家。
分析用户输入文本的情感倾向,返回分析结果。`,

    // 定义输出格式
    OutputSchema: &genai.Schema{
        Type: genai.TypeObject,
        Properties: map[string]*genai.Schema{
            "sentiment": {
                Type:        genai.TypeString,
                Description: "情感倾向:positive/negative/neutral",
                Enum:        []string{"positive", "negative", "neutral"},
            },
            "confidence": {
                Type:        genai.TypeNumber,
                Description: "置信度,0.0 到 1.0 之间",
            },
            "keywords": {
                Type:        genai.TypeArray,
                Description: "关键情感词汇",
                Items:       &genai.Schema{Type: genai.TypeString},
            },
            "summary": {
                Type:        genai.TypeString,
                Description: "简短的分析总结",
            },
        },
        Required: []string{"sentiment", "confidence", "summary"},
    },
})

设了 OutputSchema 之后,Agent 的回复会是这样的 JSON:

{
    "sentiment": "positive",
    "confidence": 0.92,
    "keywords": ["喜欢", "很棒", "推荐"],
    "summary": "用户对产品表达了积极的评价"
}

重要提示:当设置了 OutputSchema 时,Agent 只能回复,不能使用任何工具(function tools、Agent 转移等)。这是因为结构化输出模式下,LLM 的回复格式被严格限制了。

3.3 InputSchema -- 约束输入格式

InputSchema 用在 Agent 作为子 Agent(被其他 Agent 当工具调用)时,定义它接受什么样的输入:

// 这个 Agent 作为子 Agent 使用时,
// 父 Agent 调用它时需要提供特定格式的输入
subAgent, err := llmagent.New(llmagent.Config{
    Name:        "translator",
    Description: "翻译文本到指定语言",
    Model:       model,
    Instruction: "你是一个翻译专家,把文本翻译到用户指定的语言。",

    // 限制输入格式
    InputSchema: &genai.Schema{
        Type: genai.TypeObject,
        Properties: map[string]*genai.Schema{
            "text": {
                Type:        genai.TypeString,
                Description: "要翻译的文本",
            },
            "target_language": {
                Type:        genai.TypeString,
                Description: "目标语言",
            },
        },
        Required: []string{"text", "target_language"},
    },

    // 输出也可以限制
    OutputSchema: &genai.Schema{
        Type: genai.TypeObject,
        Properties: map[string]*genai.Schema{
            "translated_text": {Type: genai.TypeString},
            "source_language": {Type: genai.TypeString},
        },
    },
})

3.4 实战:问答评分 Agent

来看一个完整的例子 -- 一个评分 Agent,输入问题和回答,输出结构化的评分结果:

// 创建一个评分 Agent
graderAgent, err := llmagent.New(llmagent.Config{
    Name:  "grader",
    Model: model,
    Instruction: `你是一个严格但公正的考试评分员。
根据问题和学生的回答,给出评分和反馈。
评分标准:
- 完全正确:90-100分
- 大部分正确:70-89分
- 部分正确:50-69分
- 大部分错误:30-49分
- 完全错误:0-29分`,

    OutputSchema: &genai.Schema{
        Type: genai.TypeObject,
        Properties: map[string]*genai.Schema{
            "score": {
                Type:        genai.TypeInteger,
                Description: "分数,0-100",
            },
            "grade": {
                Type:        genai.TypeString,
                Description: "等级",
                Enum:        []string{"A", "B", "C", "D", "F"},
            },
            "feedback": {
                Type:        genai.TypeString,
                Description: "详细反馈",
            },
            "correct_points": {
                Type:        genai.TypeArray,
                Description: "回答中正确的要点",
                Items:       &genai.Schema{Type: genai.TypeString},
            },
            "missing_points": {
                Type:        genai.TypeArray,
                Description: "回答中遗漏的要点",
                Items:       &genai.Schema{Type: genai.TypeString},
            },
        },
        Required: []string{"score", "grade", "feedback"},
    },
})

4. IncludeContents -- 控制 Agent 看到的上下文

4.1 两种模式

IncludeContents 控制 Agent 在处理请求时能看到多少历史对话。ADK-Go 提供两个选项:

import "google.golang.org/adk/agent/llmagent"

// 默认模式:Agent 能看到所有相关的对话历史
agent1, _ := llmagent.New(llmagent.Config{
    Name:            "chatbot",
    Model:           model,
    Instruction:     "你是一个聊天助手。",
    IncludeContents: llmagent.IncludeContentsDefault, // 这是默认值,可以不写
})

// 无历史模式:Agent 只能看到当前这轮的用户输入
agent2, _ := llmagent.New(llmagent.Config{
    Name:            "classifier",
    Model:           model,
    Instruction:     "对用户输入进行分类。",
    IncludeContents: llmagent.IncludeContentsNone, // 不带历史
})

4.2 什么时候用 None?

  • 无状态任务:分类、情感分析、翻译等,每次调用都是独立的,不需要历史上下文
  • 省 Token:历史对话越多,Token 消耗越大。对于不需要上下文的任务,设成 None 能省不少钱
  • 子 Agent:被当作工具调用的子 Agent,通常只需要处理当前输入,不需要看父 Agent 的对话历史
  • 避免干扰:有时候历史对话反而会干扰 Agent 的判断,特别是做精确分类任务时
// 一个典型的场景:分类子 Agent 不需要历史
classifierAgent, _ := llmagent.New(llmagent.Config{
    Name:  "intent_classifier",
    Model: model,
    Instruction: `对用户意图进行分类,返回以下类别之一:
- question: 提问
- complaint: 投诉
- praise: 表扬
- request: 请求`,
    IncludeContents: llmagent.IncludeContentsNone,
    OutputSchema: &genai.Schema{
        Type: genai.TypeObject,
        Properties: map[string]*genai.Schema{
            "intent": {
                Type: genai.TypeString,
                Enum: []string{"question", "complaint", "praise", "request"},
            },
        },
    },
})

5. GlobalInstruction vs Instruction

5.1 两者的区别

在 ADK-Go 的多 Agent 系统中,有两种 Instruction:

Instruction GlobalInstruction
作用范围 只对当前这个 Agent 生效 对整个 Agent 树中的所有 Agent 生效
设置位置 每个 Agent 都能设 只有根 Agent 的 GlobalInstruction 生效
典型用途 定义 Agent 的具体行为 定义全局规则、统一人格/身份

5.2 使用场景

// 根 Agent -- 设置全局规则
rootAgent, _ := llmagent.New(llmagent.Config{
    Name:  "coordinator",
    Model: model,

    // GlobalInstruction 对整棵 Agent 树生效
    // 所有子 Agent 在被调用时都会带上这条指令
    GlobalInstruction: `全局规则:
1. 始终使用中文回复用户
2. 保持专业但友好的语气
3. 不透露内部系统信息
4. 如果不确定,明确告诉用户你不知道`,

    // Instruction 只对这个 coordinator Agent 生效
    Instruction: `你是团队协调者。根据用户的需求,
把任务分配给合适的专家 Agent。`,

    SubAgents: []agent.Agent{techAgent, salesAgent},
})

// 子 Agent -- 只需要设自己的 Instruction
// GlobalInstruction 会自动从根 Agent 传下来
techAgent, _ := llmagent.New(llmagent.Config{
    Name:  "tech_support",
    Model: model,
    Instruction: `你是技术支持专家。
帮助用户解决技术问题,提供步骤清晰的解决方案。`,
    // 不需要设 GlobalInstruction,根 Agent 的会自动生效
})

salesAgent, _ := llmagent.New(llmagent.Config{
    Name:  "sales",
    Model: model,
    Instruction: `你是销售顾问。
帮助用户了解产品,推荐合适的方案。`,
})

5.3 动态 Instruction

如果你需要根据运行时状态动态生成 Instruction,可以用 InstructionProviderGlobalInstructionProvider

import "google.golang.org/adk/agent"

agent, _ := llmagent.New(llmagent.Config{
    Name:  "dynamic_agent",
    Model: model,

    // 动态生成 Instruction
    InstructionProvider: func(ctx agent.ReadonlyContext) (string, error) {
        // 可以根据 session state 动态调整指令
        userName, err := ctx.ReadonlyState().Get("user_name")
        if err != nil {
            // 没有用户名就用默认打招呼
            return "你是一个智能助手,帮助用户解决问题。", nil
        }
        return fmt.Sprintf("你是 %s 的个人助手,了解他的偏好并提供个性化服务。", userName), nil
    },
})

5.4 Instruction 模板变量

普通的 Instruction 字符串支持模板语法,可以插入 session state 中的值:

agent, _ := llmagent.New(llmagent.Config{
    Name:  "personalized_agent",
    Model: model,
    // {user_name} 会被替换为 session state 中 "user_name" 的值
    // {preference?} 末尾的 ? 表示可选,如果不存在不会报错
    Instruction: `你好 {user_name},我是你的个人助手。
你的偏好设置:{preference?}
请问有什么可以帮你的?`,
})

模板规则: - {key_name} -- 从 session state 中取值,不存在会报错 - {key_name?} -- 可选变量,不存在时替换为空字符串 - {artifact.file_name} -- 插入指定 artifact 的文本内容 - key_name 必须匹配 ^[a-zA-Z_][a-zA-Z0-9_]*$,不匹配的花括号会被当作普通文本


6. GenerateContentConfig -- 精细调优 LLM 行为

6.1 可以调什么?

GenerateContentConfig 让你精细控制 LLM 的生成行为。最常用的参数有:

import "google.golang.org/genai"

agent, _ := llmagent.New(llmagent.Config{
    Name:        "creative_writer",
    Model:       model,
    Instruction: "你是一个创意写作助手。",

    GenerateContentConfig: &genai.GenerateContentConfig{
        // Temperature: 控制生成的随机性
        // 0.0 = 非常确定性,总是选最可能的 token
        // 1.0 = 很随机,更有创造力
        // 推荐:对话场景 0.7-0.9,代码生成 0.1-0.3,创意写作 0.9-1.0
        Temperature: genai.Ptr(float32(0.9)),

        // TopP: 核采样参数
        // 只从累积概率达到 TopP 的 token 中采样
        // 0.1 = 只考虑最可能的少量 token
        // 0.9 = 考虑更多可能的 token
        TopP: genai.Ptr(float32(0.95)),

        // MaxOutputTokens: 最大输出 token 数
        // 控制回复的最大长度
        MaxOutputTokens: genai.Ptr(int32(4096)),

        // TopK: 只从概率最高的 K 个 token 中采样
        TopK: genai.Ptr(int32(40)),
    },
})

6.2 不同场景的推荐配置

// 场景一:精确问答(客服、知识库查询)
// 低温度 + 低 TopP = 稳定、确定性高
preciseConfig := &genai.GenerateContentConfig{
    Temperature: genai.Ptr(float32(0.1)),
    TopP:        genai.Ptr(float32(0.8)),
}

// 场景二:日常对话
// 中等温度 = 自然但不跑偏
chatConfig := &genai.GenerateContentConfig{
    Temperature:     genai.Ptr(float32(0.7)),
    TopP:            genai.Ptr(float32(0.9)),
    MaxOutputTokens: genai.Ptr(int32(2048)),
}

// 场景三:创意写作(写诗、编故事)
// 高温度 + 高 TopP = 更有创造力
creativeConfig := &genai.GenerateContentConfig{
    Temperature:     genai.Ptr(float32(1.0)),
    TopP:            genai.Ptr(float32(0.95)),
    MaxOutputTokens: genai.Ptr(int32(8192)),
}

// 场景四:代码生成
// 低温度 = 逻辑严谨,减少错误
codeConfig := &genai.GenerateContentConfig{
    Temperature:     genai.Ptr(float32(0.2)),
    TopP:            genai.Ptr(float32(0.85)),
    MaxOutputTokens: genai.Ptr(int32(4096)),
}

6.3 安全设置

你还可以通过 GenerateContentConfig 配置安全过滤:

agent, _ := llmagent.New(llmagent.Config{
    Name:  "safe_agent",
    Model: model,
    GenerateContentConfig: &genai.GenerateContentConfig{
        Temperature: genai.Ptr(float32(0.7)),
        SafetySettings: []*genai.SafetySetting{
            {
                Category:  genai.HarmCategoryDangerousContent,
                Threshold: genai.HarmBlockThresholdBlockMediumAndAbove,
            },
            {
                Category:  genai.HarmCategoryHarassment,
                Threshold: genai.HarmBlockThresholdBlockMediumAndAbove,
            },
        },
    },
})

7. Vertex AI 集成 -- 企业级方案

7.1 为什么要用 Vertex AI?

如果你在企业环境中使用 ADK-Go,Vertex AI 是最佳搭档:

  • 安全合规:数据不出 Google Cloud,满足企业安全要求
  • 可扩展性:自动伸缩,不用操心基础设施
  • 监控告警:与 Cloud Monitoring 深度集成
  • 模型多样性:不仅有 Gemini,还能用其他模型

7.2 配置 Vertex AI 模型

使用 Vertex AI 只需要在创建模型客户端时指定项目和区域:

import "google.golang.org/genai"

// 创建 Vertex AI 客户端(而不是普通的 Gemini API 客户端)
client, err := genai.NewClient(ctx, &genai.ClientConfig{
    // 使用 Vertex AI 后端
    Backend:  genai.BackendVertexAI,
    Project:  "your-gcp-project-id",
    Location: "us-central1",
})
if err != nil {
    log.Fatal(err)
}

7.3 与遥测联动

在 Vertex AI 环境下,遥测数据可以直接导出到 Cloud Trace:

// Vertex AI + Cloud Trace 的完美配合
telemetryProviders, err := telemetry.New(ctx,
    telemetry.WithOtelToCloud(true),
    telemetry.WithGcpResourceProject("your-gcp-project-id"),
    telemetry.WithResource(res),
)

这样你的 Agent 调用链路就能在 Google Cloud Console 的 Trace 页面中直接查看了。

7.4 GCS 存储的 Artifact 服务

生产环境中,你肯定不想把 artifact 存在内存里。ADK-Go 提供了基于 Google Cloud Storage 的 Artifact 服务实现:

import "google.golang.org/adk/artifact/gcsartifact"

// 使用 GCS 后端存储 artifact(生产环境推荐)
// 需要配置 GCS bucket
gcsArtifactService, err := gcsartifact.New(ctx, "your-gcs-bucket-name")
if err != nil {
    log.Fatal(err)
}

// 在 Runner 中使用
r, _ := runner.New(runner.Config{
    AppName:         "my_app",
    Agent:           myAgent,
    SessionService:  sessionService,
    ArtifactService: gcsArtifactService, // 用 GCS 存储
})

8. 构建可测试的 Agent

8.1 测试的重要性

Agent 系统比普通应用更需要测试,因为: - LLM 的输出有不确定性 - 工具调用链路可能很复杂 - 多 Agent 协作容易出现边界问题

好消息是 ADK-Go 的设计非常适合写测试 -- 核心服务都是接口,很容易 mock。

8.2 使用 InMemory 服务进行集成测试

ADK-Go 提供了所有核心服务的内存实现,非常适合测试:

package myagent_test

import (
    "context"
    "testing"

    "google.golang.org/genai"

    "google.golang.org/adk/artifact"
    "google.golang.org/adk/runner"
    "google.golang.org/adk/session"
)

func TestAgentIntegration(t *testing.T) {
    ctx := context.Background()

    // 测试时全部用 InMemory 服务,不依赖外部系统
    sessionService := session.InMemoryService()
    artifactService := artifact.InMemoryService()

    // 创建你的 Agent(这里用真实的 Agent)
    myAgent := createMyAgent(t)

    // 创建 Runner
    r, err := runner.New(runner.Config{
        AppName:         "test_app",
        Agent:           myAgent,
        SessionService:  sessionService,
        ArtifactService: artifactService,
    })
    if err != nil {
        t.Fatalf("创建 runner 失败: %v", err)
    }

    // 创建测试会话
    createResp, err := sessionService.Create(ctx, &session.CreateRequest{
        AppName: "test_app",
        UserID:  "test_user",
    })
    if err != nil {
        t.Fatalf("创建会话失败: %v", err)
    }
    sess := createResp.Session

    // 发送消息并收集所有事件
    var events []*session.Event
    msg := &genai.Content{
        Role:  genai.RoleUser,
        Parts: []*genai.Part{genai.NewPartFromText("你好")},
    }

    for event, err := range r.Run(ctx, "test_user", sess.ID(), msg, agent.RunConfig{}) {
        if err != nil {
            t.Fatalf("运行出错: %v", err)
        }
        events = append(events, event)
    }

    // 验证结果
    if len(events) == 0 {
        t.Fatal("没有收到任何事件")
    }

    // 检查最后一个事件是否有回复
    lastEvent := events[len(events)-1]
    if lastEvent.Content == nil || len(lastEvent.Content.Parts) == 0 {
        t.Fatal("最后一个事件没有内容")
    }
}

8.3 单独测试工具

工具是最容易测试的部分,因为它们是纯函数:

func TestCalculatorTool(t *testing.T) {
    // 创建工具
    calcTool := createCalculatorTool()

    // 验证声明
    decl := calcTool.Declaration()
    if decl.Name != "calculator" {
        t.Errorf("工具名称不对: got %s, want calculator", decl.Name)
    }

    // 测试运行(对于简单工具,可以传 nil context)
    result, err := calcTool.Run(nil, map[string]any{
        "expression": "2 + 3",
    })
    if err != nil {
        t.Fatalf("工具运行出错: %v", err)
    }

    // 验证结果
    if result["result"] != float64(5) {
        t.Errorf("计算结果不对: got %v, want 5", result["result"])
    }
}

8.4 测试 Artifact 操作

func TestArtifactOperations(t *testing.T) {
    ctx := context.Background()
    svc := artifact.InMemoryService()

    // 测试保存
    saveResp, err := svc.Save(ctx, &artifact.SaveRequest{
        AppName:   "test",
        UserID:    "user1",
        SessionID: "sess1",
        FileName:  "test.txt",
        Part:      genai.NewPartFromText("hello world"),
    })
    if err != nil {
        t.Fatalf("保存失败: %v", err)
    }
    if saveResp.Version != 1 {
        t.Errorf("版本号不对: got %d, want 1", saveResp.Version)
    }

    // 测试加载
    loadResp, err := svc.Load(ctx, &artifact.LoadRequest{
        AppName:   "test",
        UserID:    "user1",
        SessionID: "sess1",
        FileName:  "test.txt",
    })
    if err != nil {
        t.Fatalf("加载失败: %v", err)
    }
    if loadResp.Part.Text != "hello world" {
        t.Errorf("内容不对: got %s, want hello world", loadResp.Part.Text)
    }

    // 测试版本控制
    saveResp2, _ := svc.Save(ctx, &artifact.SaveRequest{
        AppName:   "test",
        UserID:    "user1",
        SessionID: "sess1",
        FileName:  "test.txt",
        Part:      genai.NewPartFromText("updated content"),
    })
    if saveResp2.Version != 2 {
        t.Errorf("第二个版本号不对: got %d, want 2", saveResp2.Version)
    }

    // 测试列表
    listResp, _ := svc.List(ctx, &artifact.ListRequest{
        AppName:   "test",
        UserID:    "user1",
        SessionID: "sess1",
    })
    if len(listResp.FileNames) != 1 || listResp.FileNames[0] != "test.txt" {
        t.Errorf("文件列表不对: %v", listResp.FileNames)
    }

    // 测试删除
    err = svc.Delete(ctx, &artifact.DeleteRequest{
        AppName:   "test",
        UserID:    "user1",
        SessionID: "sess1",
        FileName:  "test.txt",
    })
    if err != nil {
        t.Fatalf("删除失败: %v", err)
    }
}

8.5 测试的最佳实践

总结一下 Agent 测试的几个关键原则:

  1. 用 InMemory 服务:Session、Artifact 都用内存实现,测试快、不依赖外部系统
  2. 工具独立测试:每个工具单独写测试,确保逻辑正确
  3. 集成测试用真实 Agent:端到端跑一下,确保各部分能协作
  4. 对 LLM 输出做宽松断言:LLM 的回复不是确定性的,不要逐字比对,而是检查关键要素(比如是否包含某个字段、格式是否正确)
  5. 使用 OutputSchema 简化断言:有了结构化输出,你可以用 JSON 解析来验证格式
// 用 OutputSchema 的 Agent 更容易测试
func TestStructuredOutput(t *testing.T) {
    // ... 运行 Agent 并拿到回复 ...

    // 解析结构化输出
    var result struct {
        Sentiment  string  `json:"sentiment"`
        Confidence float64 `json:"confidence"`
    }
    if err := json.Unmarshal([]byte(responseText), &result); err != nil {
        t.Fatalf("解析输出失败: %v", err)
    }

    // 结构化数据很容易断言
    if result.Sentiment == "" {
        t.Error("sentiment 不应该为空")
    }
    if result.Confidence < 0 || result.Confidence > 1 {
        t.Errorf("confidence 应该在 0-1 之间, got %f", result.Confidence)
    }
}

9. 小结

这一章我们探索了 ADK-Go 的高级特性,让我们来回顾一下:

特性 解决什么问题 关键要点
Artifact 文件和二进制数据管理 自带版本控制,支持用户级共享,可用 GCS 存储
OpenTelemetry 可观测性 追踪调用链路,支持 Cloud Trace / Jaeger 等后端
Schema 控制 结构化输入输出 OutputSchema 强制 JSON 格式,但会禁用工具
IncludeContents 控制上下文 None 模式适合无状态任务,省 Token
GlobalInstruction 全局规则 只有根 Agent 的生效,对整棵树有效
GenerateContentConfig LLM 参数调优 Temperature、TopP、MaxOutputTokens 等
Vertex AI 企业级集成 安全合规、GCS 存储、Cloud Trace 监控
可测试性 质量保证 InMemory 服务、工具独立测试、宽松断言

掌握了这些高级特性,你就能构建出生产级别的 Agent 系统了。


10. 动手练习

练习 1:文件管理助手

构建一个文件管理 Agent,要求: - 用户可以"上传"文本文件(通过 artifact 保存) - 用户可以问"我有哪些文件?"(列出所有 artifact) - 用户可以查看文件内容(加载 artifact) - 用户可以更新文件(新版本覆盖)

提示:

// 1. 创建自定义工具来保存和管理文件
// 2. 使用 loadartifactstool 来加载文件
// 3. 在 Runner 中配置 ArtifactService

练习 2:结构化输出 -- 天气分析

创建一个天气分析 Agent,要求输出严格遵循以下格式:

{
    "city": "北京",
    "temperature": 25,
    "weather": "晴",
    "suggestion": "适合户外活动",
    "clothing": "短袖短裤即可"
}

提示:用 OutputSchema 定义输出格式。

练习 3:可观测的多 Agent 系统

搭建一个有 3 个 Agent 的系统(coordinator + 2个专家 Agent),要求: - 配置 OpenTelemetry,追踪导出到控制台 - 设置 GlobalInstruction 统一所有 Agent 的行为规范 - 每个 Agent 有不同的 Temperature 设置

提示:

// 1. 用 stdouttrace 导出追踪数据到控制台
// 2. 在根 Agent 设置 GlobalInstruction
// 3. 给每个 Agent 配不同的 GenerateContentConfig

练习 4:完整的可测试 Agent

为你之前写的任何一个 Agent 编写完整的测试套件: - 至少 3 个工具的单元测试 - 1 个集成测试(用 InMemory 服务跑完整流程) - 1 个 Artifact 操作的测试

记住:好的测试不是测 LLM 说了什么具体的话,而是测系统的行为是否符合预期(工具被正确调用了吗?输出格式对吗?状态更新了吗?)。


到这里,整个 ADK-Go 教程的核心内容就都讲完了。你已经掌握了从基础概念到高级特性的全部知识。

回顾一下我们的学习路径: 1. 基础概念和环境搭建 2. Agent 和 Tool 的基本用法 3. Session 和 State 管理 4. 多 Agent 协作 5. 工作流 Agent(Sequential、Parallel、Loop) 6. Callback 机制 7. 回调和生命周期 8. 高级特性(本章)

接下来就是实践了。挑一个你感兴趣的项目,把学到的这些知识组合起来,构建属于你自己的智能 Agent 系统吧!