adk-go Agent 类详解

ADK for Go 在 2026 年 3 月底发布 1.0 后,重心已经不只是“怎么写一个 Agent”,而是“怎么把 Agent 放进生产环境运行”。这篇文章按 1.0/1.1.x 的能力把几个关键点补齐:结构化工作流 Agent、Plugin/OTel 可观测性、Retry and Reflect 自愈、Human-in-the-Loop 工具确认流、YAML 配置化,以及多模态内容审核场景里的 SafetySettings。

基础接口

在 ADK 框架中,agent.Agent 是所有 Agent 的基础接口。它定义了 Agent 的基本属性和运行逻辑:

1
2
3
4
5
6
type Agent interface {
Name() string
Description() string
Run(InvocationContext) iter.Seq2[*session.Event, error]
SubAgents() []Agent
}

核心方法可以这样理解:

  • Name() string:返回 Agent 名称。同一棵 Agent 树里必须唯一,且不能是保留名 user
  • Description() string:返回 Agent 能力描述。多 Agent 系统里,LLM 会依赖它判断是否把任务委托给这个 Agent,建议保持一行内的短描述。
  • SubAgents() []Agent:返回直接子 Agent 列表,定义 Agent 的层级关系。框架会基于它建立父子关系,支持跨 Agent 路由。
  • Run(InvocationContext) iter.Seq2[*session.Event, error]:定义 Agent 的执行逻辑。InvocationContext 包含当前调用、会话、状态、内存等信息,返回值会持续产出执行事件或错误。

如果要写完全自定义的 Agent,可以用 agent.Config

1
2
3
4
5
6
7
8
9
customAgent, err := agent.New(agent.Config{
Name: "API_Aggregator",
Description: "聚合内部 API 并返回结构化结果。",
Run: func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] {
return func(yield func(*session.Event, error) bool) {
// 在这里写纯 Go 逻辑,不一定要调用大模型。
}
},
})

agent.Config 适合 API 聚合、确定性任务、编排胶水代码等场景。对大多数业务来说,真正高频使用的是下面的 llmagent.New

llmagent 介绍

大多数 Agent 不会手写 Run,而是通过 llmagent.New 创建 LLM 驱动的 Agent。常见配置如下:

  • Name:Agent 名称,必须非空。
  • Model:底层模型,必填。
  • Description:Agent 能力描述,主要用于父 Agent 动态路由。
  • Instruction / InstructionProvider:局部系统提示词,支持静态字符串或动态函数。
  • GlobalInstruction / GlobalInstructionProvider:全局指令,只在根 Agent 上生效。
  • Tools / Toolsets:可调用工具集合。
  • BeforeModelCallbacks / AfterModelCallbacks:模型调用前后的钩子。
  • BeforeToolCallbacks / AfterToolCallbacks:工具调用前后的钩子。
  • GenerateContentConfig:温度、token 上限、安全策略、结构化输出等模型生成参数。
  • InputSchema / OutputSchema:Agent 被调用或输出时的 JSON Schema 约束。
  • SubAgents:可委托的子 Agent。
  • IncludeContents:是否把对话历史带入模型请求。
  • DisallowTransferToParent / DisallowTransferToPeers:限制向父 Agent 或同级 Agent 转移。
  • OutputKey:把 Agent 输出写入 session state,便于后续 Agent 使用。

一个典型的初始化如下:

1
2
3
4
5
6
7
8
9
10
agent, err := llmagent.New(llmagent.Config{
Name: acfg.name,
Model: m,
Description: acfg.description,
Instruction: acfg.instruction,
Tools: acfg.tools,
BeforeToolCallbacks: []llmagent.BeforeToolCallback{monitor.OnBeforeTool},
AfterToolCallbacks: []llmagent.AfterToolCallback{monitor.OnAfterTool},
GenerateContentConfig: genConfig,
})

这里要注意一个 1.0/1.1.x 很容易写错的点:Plugins 不在 llmagent.Config 上,而是在 runner.Config.PluginConfig 上;HITL 也不是一个统一的 Confirmers 字段,而是通过工具级的 RequireConfirmation / RequireConfirmationProvider,或者 tool.WithConfirmation 包装 Toolset 来实现。也就是说,Agent 负责业务能力,Runner 和 Tool 负责生产级横切能力。

Model

1.0 之后的官方示例已经更偏向 model/gemini 包,而不是旧文章里常见的 provider/googleai 写法。生产代码里不要把 API Key 写死,Gemini API Key 场景建议显式从环境变量读取;如果走 Vertex AI/ADC,再按部署环境传对应的 genai.ClientConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"context"
"os"

"google.golang.org/adk/model"
"google.golang.org/adk/model/gemini"
"google.golang.org/genai"
)

func newGeminiModel(ctx context.Context) (model.LLM, error) {
return gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
}

如果项目使用 Vertex AI/ADC,可以把认证交给运行环境,并在 ClientConfig 中只保留后端、区域等必要配置。最小化示意如下:

1
model, err := gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{})

这样做的好处是代码不携带密钥,容器镜像也不需要因为 API Key 变化而重新构建。

提示词

InstructionGlobalInstruction 的区别可以按作用域理解:

项目 作用范围 生成时机 一句话总结
Instruction 单个 Agent 编译期固化 当前 Agent 的角色和任务边界
InstructionProvider 单个 Agent 每次运行前动态生成 根据 session state、环境、时间等动态拼提示词
GlobalInstruction 整棵 Agent 树 编译期固化 全局红线和所有 Agent 都要遵守的规则
GlobalInstructionProvider 整棵 Agent 树 每次运行前动态生成 把当前环境、日期、租户等动态信息广播给全体 Agent

局部指令示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import (
"fmt"
"time"

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

agent, err := llmagent.New(llmagent.Config{
Name: "My_Agent",
Model: model,

Instruction: "你是一个后端排障助手。当前处理的业务线是 {business_line}。",

InstructionProvider: func(ctx agent.ReadonlyContext) (string, error) {
return fmt.Sprintf("现在的时间是 %v,请简短回复。", time.Now()), nil
},
})

全局指令示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import (
"fmt"
"os"

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

agent, err := llmagent.New(llmagent.Config{
Name: "My_Agent",
Model: model,

GlobalInstruction: "系统最高准则:绝不能在日志中打印用户的明文凭证 {user_token}。",

GlobalInstructionProvider: func(ctx agent.ReadonlyContext) (string, error) {
env := os.Getenv("SERVER_ENV")
return fmt.Sprintf("全局注意:当前系统正运行在 [%s] 环境。", env), nil
},
})

如果同时设置 InstructionInstructionProvider,动态 Provider 通常会接管静态指令。GlobalInstruction 也同理。实际项目里建议把不可变的安全边界放在静态指令里,把环境、租户、日期、灰度策略放在 Provider 里。

GenerateContentConfig

GenerateContentConfig 用来配置底层模型生成参数,例如温度、token 上限、停止词、JSON 输出和安全过滤。注意:工具不要写在这里,工具应该写到 llmagent.Config.ToolsToolsets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
genConfig := &genai.GenerateContentConfig{
MaxOutputTokens: 500,
Temperature: genai.Ptr[float32](0.1),
ResponseMIMEType: "application/json",
SafetySettings: []*genai.SafetySetting{
{
Category: genai.HarmCategoryHateSpeech,
Threshold: genai.HarmBlockThresholdBlockNone,
},
{
Category: genai.HarmCategoryHarassment,
Threshold: genai.HarmBlockThresholdBlockNone,
},
{
Category: genai.HarmCategorySexuallyExplicit,
Threshold: genai.HarmBlockThresholdBlockNone,
},
{
Category: genai.HarmCategoryDangerousContent,
Threshold: genai.HarmBlockThresholdBlockNone,
},
},
}

核心字段:

字段名 作用机制 最佳实践与场景举例
Temperature 控制生成随机性 内容审核、信息抽取、路由决策建议 0.0 到 0.2;创意生成可以更高
MaxOutputTokens 限制单次输出长度 控制成本和接口耗时,例如后端同步接口限制在 500 token 左右
ResponseMIMEType / ResponseSchema 强制结构化输出 企业后端建议输出 JSON,方便 json.Unmarshal 和下游校验
TopP / TopK 控制采样候选范围 一般不单独调,优先调 Temperature
StopSequences 遇到指定字符串停止生成 Few-shot 或协议化输出时防止模型继续模拟用户
SafetySettings 控制底层安全拦截策略 内容审核/有害梗图分析场景必须认真配置,否则模型可能因为安全过滤拒绝分析违规图片

有害梗图分析是一个特殊场景:系统的目标正是识别违规内容,因此需要让模型“看见”违规内容并输出标签,而不是直接被安全层拦截。可以在审核链路里对相关 harm category 设置 BLOCK_NONE,但不要把这个配置无差别用于普通聊天入口。更稳妥的做法是把审核 Agent 部署在隔离链路里,输出强制走 Schema,并在业务层继续做权限、日志脱敏和人工复核。

Schema 约束

Agent 进入后端业务链路后,不应依赖自然语言输出。输入输出都应该用 Schema 收敛成结构化数据。

1
2
3
4
5
6
7
8
9
10
11
12
type MemeAnalyzeInput struct {
ImageID string `json:"image_id" jsonschema:"图片在数据库中的唯一标识"`
OCRText string `json:"ocr_text" jsonschema:"由 RapidOCR 提取出的图片内嵌文本"`
UploaderUID string `json:"uploader_uid" jsonschema:"上传该图片的用户ID"`
}

type MemeAnalyzeOutput struct {
IsHarmful bool `json:"is_harmful" jsonschema:"是否包含有害、违规或引战内容"`
RiskLevel string `json:"risk_level" jsonschema:"风险等级,必须是 low, medium, high 之一"`
Tags []string `json:"tags" jsonschema:"提取的关键标签,如引战、涉黄、暴恐"`
Reasoning string `json:"reasoning" jsonschema:"判断风险等级的依据"`
}

OutputSchema 的价值是把模型输出从自然语言收敛到 Go 可以校验和解析的数据结构里。需要注意:在当前 ADK Go API 中,InputSchemaOutputSchema 的字段类型是 *genai.Schema。如果你在项目里用 struct tag 表达 Schema,需要先通过本地的 JSON Schema 工具或封装函数转换成 *genai.Schema,再传给 llmagent.Config

1
2
3
4
5
6
7
8
9
agent, err := llmagent.New(llmagent.Config{
Name: "Meme_Harmful_Detector",
Model: myModel,

Instruction: "你是一个专业的互联网内容审核引擎。请分析输入的 OCR 文本,判断其是否存在潜在违规风险。",

InputSchema: memeAnalyzeInputSchema,
OutputSchema: memeAnalyzeOutputSchema,
})

还要记住一个重要限制:配置 OutputSchema 后,这个 Agent 通常只能直接回复结构化结果,不能再自由调用工具、RAG 或转交给其他 Agent。实践中更推荐把“会调用工具的编排 Agent”和“最终结构化输出 Agent”拆开:前者负责取证和调用,后者只负责把最终结论压成 Schema。

工具调用与 HITL

1.0/1.1.x 里,HITL 的核心是 Tool Confirmation。对敏感工具设置确认要求后,Agent 在执行工具前会暂停,发出确认事件,等待人类在后台或前端批准、修改参数或拒绝,然后再继续。

典型场景包括删除数据库、大额转账、线上配置变更、批量封禁用户、发送外部邮件等。这类操作不应该只依赖模型判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
)

type DeleteDatabaseArgs struct {
Instance string `json:"instance"`
Reason string `json:"reason"`
}

type DeleteDatabaseResult struct {
Deleted bool `json:"deleted"`
}

func deleteDatabase(ctx tool.Context, args DeleteDatabaseArgs) (DeleteDatabaseResult, error) {
// 真正的删除逻辑必须仍然在服务端做权限校验和审计。
return DeleteDatabaseResult{Deleted: true}, nil
}

deleteDatabaseTool, err := functiontool.New(functiontool.Config{
Name: "delete_database",
Description: "删除一个生产数据库实例。",
RequireConfirmation: true,
}, deleteDatabase)

如果只有部分请求需要确认,可以用动态判断:

1
2
3
4
5
6
7
transferTool, err := functiontool.New(functiontool.Config{
Name: "transfer_money",
Description: "发起转账。",
RequireConfirmationProvider: func(args TransferArgs) bool {
return args.Amount >= 10000 || args.Currency != "CNY"
},
}, transferMoney)

这比在 BeforeToolCallback 里自己拦截更清晰,因为确认流会进入 ADK 的标准事件协议,前端、后台任务和审计日志都能围绕同一个事件模型实现。

子 Agent 与结构化工作流

老版本文章里通常只讲一种模式:父 llmagent 把子 Agent 包成可调用工具,由 LLM 根据 Instruction 和子 Agent 的 Description 动态决定调用谁。这个模式仍然重要,适合意图识别、动态路由和开放式任务拆解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ocrAgent, _ := llmagent.New(llmagent.Config{
Name: "OCR_Worker",
Description: "专门负责从图片中提取文本。当需要知道图片里写了什么时调用我。",
Model: myModel,
Instruction: "你是一个 OCR 助手。解析输入的图片 URL,返回识别到的文本。",
InputSchema: imageInputSchema,
OutputSchema: ocrResultSchema,
})

safetyAgent, _ := llmagent.New(llmagent.Config{
Name: "Safety_Worker",
Description: "专门负责分析文本是否包含违规、引战或有害内容。",
Model: myModel,
Instruction: "你是一个安全审核员。严格判定输入的文本是否违规。",
InputSchema: textAnalyzeInputSchema,
OutputSchema: safetyResultSchema,
})

managerAgent, _ := llmagent.New(llmagent.Config{
Name: "Moderation_Manager",
Model: myModel,
Instruction: `你是多模态审核主管。
收到图片后,请按以下步骤执行:
1. 调用 OCR_Worker 获取图片中的文字。
2. 将获取到的文字传给 Safety_Worker 进行毒性分析。
3. 综合所有信息,生成最终审核报告。`,
SubAgents: []agent.Agent{ocrAgent, safetyAgent},
})

1.0/1.1.x 之后,更推荐把确定性强的流程改成结构化 Workflow Agent,而不是完全交给 LLM 自己决定控制流。

SequentialAgent

SequentialAgentSubAgents 的顺序执行一次,适合固定流水线,例如 OCR -> 文本审核 -> 报告汇总。

1
2
3
4
5
6
7
8
9
10
11
12
import (
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/workflowagents/sequentialagent"
)

pipelineAgent, err := sequentialagent.New(sequentialagent.Config{
AgentConfig: agent.Config{
Name: "Moderation_Pipeline",
Description: "按固定顺序完成多模态审核。",
SubAgents: []agent.Agent{ocrAgent, safetyAgent, reportAgent},
},
})

ParallelAgent

ParallelAgent 并发执行多个子 Agent,适合需要多视角判断或多个独立步骤的任务,例如 OCR、图像内容分类、相似图检索并行执行,然后交给汇总 Agent。

1
2
3
4
5
6
7
8
9
import "google.golang.org/adk/agent/workflowagents/parallelagent"

parallelEvidenceAgent, err := parallelagent.New(parallelagent.Config{
AgentConfig: agent.Config{
Name: "Parallel_Evidence_Collector",
Description: "并行收集审核证据。",
SubAgents: []agent.Agent{ocrAgent, imageClassifierAgent, similaritySearchAgent},
},
})

LoopAgent

LoopAgent 会按顺序反复执行子 Agent,直到达到最大迭代次数或被子 Agent 终止,适合“生成 -> 评审 -> 修正”的迭代流程。

1
2
3
4
5
6
7
8
9
10
import "google.golang.org/adk/agent/workflowagents/loopagent"

reviewLoopAgent, err := loopagent.New(loopagent.Config{
AgentConfig: agent.Config{
Name: "Report_Review_Loop",
Description: "迭代修正审核报告。",
SubAgents: []agent.Agent{draftAgent, critiqueAgent, reviseAgent},
},
MaxIterations: 3,
})

后端开发的经验法则是:控制流能确定就用 Workflow Agent,任务边界开放才交给 LLM 路由。这样更容易测试、监控和排障。

参考资料