第八章:高级特性 -- 解锁更多可能
恭喜你走到这里!前面七章我们把 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。这个工具会:
- 自动告诉 LLM 当前会话有哪些 artifact
- 当用户问到某个文件时,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 通过 InputSchema 和 OutputSchema 让你精确控制 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,可以用 InstructionProvider 和 GlobalInstructionProvider:
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 测试的几个关键原则:
- 用 InMemory 服务:Session、Artifact 都用内存实现,测试快、不依赖外部系统
- 工具独立测试:每个工具单独写测试,确保逻辑正确
- 集成测试用真实 Agent:端到端跑一下,确保各部分能协作
- 对 LLM 输出做宽松断言:LLM 的回复不是确定性的,不要逐字比对,而是检查关键要素(比如是否包含某个字段、格式是否正确)
- 使用 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 系统吧!