第六章:回调与插件 -- 在关键节点插入你的逻辑
本章你将学到
- 理解 ADK-Go 中的回调(Callback)机制,以及它在 Agent 执行流程中的位置
- 掌握三种级别的回调:Agent 回调、Model 回调、Tool 回调
- 学会用回调实现日志、过滤、短路等常见需求
- 了解工具确认(Tool Confirmation)机制,实现"人在回路"(Human-in-the-Loop)
- 理解插件(Plugin)系统,在 Runner 级别实现全局的横切关注点
- 搞清楚回调和插件各自的适用场景
回调机制概述
如果你写过 Web 框架(比如 Gin、Echo),那你一定对中间件(Middleware)不陌生。中间件让你可以在请求处理的前后插入自己的逻辑,比如打日志、做鉴权、记录耗时等等。
ADK-Go 的回调机制就是类似的思路。在 Agent 执行的关键节点上,框架给你留了"钩子"(Hook),你可以往里面塞自己的函数。这些钩子包括:
- Agent 执行前后 -- Agent 开始跑之前和跑完之后
- LLM 调用前后 -- 发请求给大模型之前和拿到响应之后
- Tool 调用前后 -- 执行工具函数之前和拿到结果之后
每个钩子不光能"看一眼"(日志、监控),还能"动手脚":
- 修改数据:改 state、改请求参数、改响应内容
- 短路执行:直接返回结果,跳过后续流程(比如缓存命中就不调 LLM 了)
- 中断流程:返回 error 让整个流程停下来
这种设计让你在不修改 Agent 核心逻辑的前提下,灵活地插入各种横切关注点。下面我们一个一个来看。
Agent 回调
Agent 回调是最外层的钩子,在整个 Agent 执行的前后触发。不管你用的是 llmagent、自定义 Agent 还是工作流 Agent,都可以设置 Agent 回调。
回调签名
先看看类型定义:
// 在 Agent 开始执行前调用
// 返回非 nil 的 Content 或 error 时,Agent 的执行会被跳过
type BeforeAgentCallback func(ctx agent.CallbackContext) (*genai.Content, error)
// 在 Agent 执行完毕后调用
// 返回非 nil 的 Content 或 error 时,会创建一个新的事件
type AfterAgentCallback func(ctx agent.CallbackContext) (*genai.Content, error)
注意这两个回调的签名是一样的,都接收 agent.CallbackContext,返回 (*genai.Content, error)。
CallbackContext 接口提供了这些能力:
type CallbackContext interface {
ReadonlyContext // 只读信息:AgentName、UserContent、InvocationID 等
Artifacts() Artifacts // 访问制品(文件)存储
State() session.State // 读写 session state
}
基本用法
package main
import (
"log"
"time"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
)
func main() {
myAgent, err := llmagent.New(llmagent.Config{
Name: "my_agent",
Model: model, // 假设已经创建好了
// 在 Agent 开始执行前
BeforeAgentCallbacks: []agent.BeforeAgentCallback{
func(ctx agent.CallbackContext) (*genai.Content, error) {
log.Printf("[%s] Agent 即将开始执行...", ctx.AgentName())
// 往 state 里记个开始时间
ctx.State().Set("start_time", time.Now().Format(time.RFC3339))
// 返回 nil, nil 表示继续正常执行
return nil, nil
},
},
// 在 Agent 执行完毕后
AfterAgentCallbacks: []agent.AfterAgentCallback{
func(ctx agent.CallbackContext) (*genai.Content, error) {
log.Printf("[%s] Agent 执行完毕", ctx.AgentName())
// 可以从 state 读取之前记录的开始时间,算一下耗时
startTimeStr, _ := ctx.State().Get("start_time")
if s, ok := startTimeStr.(string); ok {
startTime, _ := time.Parse(time.RFC3339, s)
log.Printf("[%s] 总耗时: %v", ctx.AgentName(), time.Since(startTime))
}
return nil, nil
},
},
})
if err != nil {
log.Fatal(err)
}
_ = myAgent
}
短路执行
BeforeAgentCallback 有一个很强大的特性:如果你返回了非 nil 的 *genai.Content,Agent 的正常执行会被完全跳过,框架会用你返回的 Content 创建一个事件。
这在什么场景有用呢?比如权限检查:
BeforeAgentCallbacks: []agent.BeforeAgentCallback{
func(ctx agent.CallbackContext) (*genai.Content, error) {
// 检查用户是否有权限使用这个 Agent
userID := ctx.UserID()
if !hasPermission(userID, ctx.AgentName()) {
// 返回一个拒绝消息,Agent 不会执行
return genai.NewContentFromText(
"抱歉,您没有权限使用此功能。",
genai.RoleModel,
), nil
}
// 有权限,继续正常执行
return nil, nil
},
},
多个回调的执行顺序
你可以注册多个 BeforeAgentCallback 或 AfterAgentCallback,它们会按顺序依次执行。但是:
- BeforeAgentCallbacks:一旦某个回调返回了非 nil 的 Content 或 error,后面的回调就不执行了,Agent 也不执行
- AfterAgentCallbacks:一旦某个回调返回了非 nil 的 Content 或 error,后面的回调就不执行了
BeforeAgentCallbacks: []agent.BeforeAgentCallback{
logCallback, // 第1个:记录日志
authCallback, // 第2个:权限检查(可能短路)
rateLimitCallback, // 第3个:限流检查(可能短路)
},
Model 回调(仅 LLMAgent)
Model 回调让你可以在 LLM 调用的前后插入逻辑。这是 llmagent 特有的,因为只有 LLMAgent 才会调用大模型。
回调签名
// 在调用 LLM 前
// 返回非 nil 的 LLMResponse 时,实际的 LLM 调用会被跳过(短路)
type BeforeModelCallback func(
ctx agent.CallbackContext,
llmRequest *model.LLMRequest,
) (*model.LLMResponse, error)
// 在 LLM 返回后
// 返回非 nil 的 LLMResponse 时,会替换掉实际的 LLM 响应
type AfterModelCallback func(
ctx agent.CallbackContext,
llmResponse *model.LLMResponse,
llmResponseError error,
) (*model.LLMResponse, error)
// 在 LLM 调用出错时
type OnModelErrorCallback func(
ctx agent.CallbackContext,
llmRequest *model.LLMRequest,
llmResponseError error,
) (*model.LLMResponse, error)
注意几个关键点:
BeforeModelCallback可以拿到完整的LLMRequest,包括消息历史、工具声明、系统指令等AfterModelCallback同时接收响应和错误,方便你统一处理OnModelErrorCallback专门处理 LLM 调用失败的情况,可以返回替代响应
基本用法
myAgent, _ := llmagent.New(llmagent.Config{
Name: "my_agent",
Model: model,
// 在调用 LLM 前
BeforeModelCallbacks: []llmagent.BeforeModelCallback{
func(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) {
log.Printf("即将调用 LLM,消息数: %d", len(req.Contents))
// 记录使用了哪些工具
if len(req.Tools) > 0 {
toolNames := make([]string, 0, len(req.Tools))
for name := range req.Tools {
toolNames = append(toolNames, name)
}
log.Printf("可用工具: %v", toolNames)
}
// 返回 nil, nil 继续正常调用 LLM
return nil, nil
},
},
// 在 LLM 返回后
AfterModelCallbacks: []llmagent.AfterModelCallback{
func(ctx agent.CallbackContext, resp *model.LLMResponse, respErr error) (*model.LLMResponse, error) {
if respErr != nil {
log.Printf("LLM 调用出错: %v", respErr)
return nil, nil // 返回 nil, nil 表示不干预,让原来的错误继续传播
}
// 记录 token 使用情况
if resp.UsageMetadata != nil {
log.Printf("Token 消耗 - 输入: %d, 输出: %d",
resp.UsageMetadata.PromptTokenCount,
resp.UsageMetadata.CandidatesTokenCount,
)
}
// 可以查看 LLM 的回复内容
if resp.Content != nil && len(resp.Content.Parts) > 0 {
firstPart := resp.Content.Parts[0]
if firstPart.Text != "" {
preview := firstPart.Text
if len(preview) > 80 {
preview = preview[:80] + "..."
}
log.Printf("LLM 回复预览: %s", preview)
}
}
return nil, nil // 不修改响应
},
},
})
实现 LLM 响应缓存
BeforeModelCallback 最经典的用途之一就是缓存。如果你返回了非 nil 的 *model.LLMResponse,框架就不会调用真正的 LLM,直接用你返回的响应:
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"sync"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/model"
)
// 简单的内存缓存
var (
cache = make(map[string]*model.LLMResponse)
cacheMutex sync.RWMutex
)
// 根据请求内容生成缓存 key
func cacheKey(req *model.LLMRequest) string {
data, _ := json.Marshal(req.Contents)
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:])
}
// 缓存回调
func cacheBeforeModel(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) {
key := cacheKey(req)
cacheMutex.RLock()
defer cacheMutex.RUnlock()
if resp, ok := cache[key]; ok {
log.Println("缓存命中!跳过 LLM 调用")
return resp, nil // 短路:直接返回缓存的响应
}
return nil, nil // 缓存未命中,继续调用 LLM
}
func cacheAfterModel(ctx agent.CallbackContext, resp *model.LLMResponse, respErr error) (*model.LLMResponse, error) {
if respErr != nil || resp == nil {
return nil, nil
}
// 只缓存最终回复(不是工具调用)
if resp.Content != nil && len(resp.Content.Parts) > 0 && resp.Content.Parts[0].Text != "" {
// 这里简化了,实际使用中你需要从某个地方拿到 request 来生成 key
// 或者用 state 来传递
log.Println("缓存 LLM 响应")
}
return nil, nil // 不修改响应
}
实现内容审核
AfterModelCallback 也常用来做内容审核,在 LLM 回复之后、返回给用户之前做一道检查:
AfterModelCallbacks: []llmagent.AfterModelCallback{
func(ctx agent.CallbackContext, resp *model.LLMResponse, respErr error) (*model.LLMResponse, error) {
if respErr != nil || resp == nil || resp.Content == nil {
return nil, nil
}
for _, part := range resp.Content.Parts {
if part.Text != "" && containsSensitiveContent(part.Text) {
log.Printf("检测到敏感内容,已替换")
// 返回一个替代响应
return &model.LLMResponse{
Content: genai.NewContentFromText(
"抱歉,我无法回答这个问题。",
genai.RoleModel,
),
TurnComplete: true,
}, nil
}
}
return nil, nil // 内容正常,不修改
},
},
Tool 回调(仅 LLMAgent)
Tool 回调让你在工具执行的前后插入逻辑。注意这里的回调签名跟 Agent 回调、Model 回调有点不一样,上下文用的是 tool.Context。
回调签名
// 在工具执行前
// 返回非 nil 的 result 时,实际的工具调用会被跳过
type BeforeToolCallback func(
ctx tool.Context,
tool tool.Tool,
args map[string]any,
) (map[string]any, error)
// 在工具执行后
// 返回非 nil 的 result 时,会替换掉实际的工具结果
type AfterToolCallback func(
ctx tool.Context,
tool tool.Tool,
args, result map[string]any,
err error,
) (map[string]any, error)
// 在工具执行出错时
type OnToolErrorCallback func(
ctx tool.Context,
tool tool.Tool,
args map[string]any,
err error,
) (map[string]any, error)
tool.Context 比 agent.CallbackContext 多了几个有用的方法:
type Context interface {
agent.CallbackContext
FunctionCallID() string // 当前函数调用的 ID
Actions() *session.EventActions // 事件动作(可以修改 state 等)
SearchMemory(context.Context, string) (...) // 搜索 Agent 记忆
ToolConfirmation() *toolconfirmation.ToolConfirmation // 工具确认状态
RequestConfirmation(hint string, payload any) error // 请求人工确认
}
基本用法
myAgent, _ := llmagent.New(llmagent.Config{
Name: "my_agent",
Model: model,
Tools: []tool.Tool{weatherTool, searchTool},
// 在工具执行前
BeforeToolCallbacks: []llmagent.BeforeToolCallback{
func(ctx tool.Context, t tool.Tool, args map[string]any) (map[string]any, error) {
log.Printf("即将调用工具: %s", t.Name())
log.Printf(" 参数: %v", args)
log.Printf(" 调用 ID: %s", ctx.FunctionCallID())
// 返回 nil, nil 表示继续执行工具
return nil, nil
},
},
// 在工具执行后
AfterToolCallbacks: []llmagent.AfterToolCallback{
func(ctx tool.Context, t tool.Tool, args, result map[string]any, err error) (map[string]any, error) {
if err != nil {
log.Printf("工具 %s 执行出错: %v", t.Name(), err)
return nil, nil // 不干预错误处理
}
log.Printf("工具 %s 执行成功, 结果: %v", t.Name(), result)
// 可以修改结果再返回
return result, nil
},
},
})
工具级别的参数校验
BeforeToolCallback 很适合做参数校验。你可以在工具真正执行前检查参数是否合法:
BeforeToolCallbacks: []llmagent.BeforeToolCallback{
func(ctx tool.Context, t tool.Tool, args map[string]any) (map[string]any, error) {
// 只对特定工具做校验
if t.Name() == "transfer_money" {
amount, ok := args["amount"].(float64)
if !ok || amount <= 0 {
// 返回错误结果,跳过工具执行
return map[string]any{
"error": "转账金额必须大于 0",
}, nil
}
if amount > 10000 {
return map[string]any{
"error": "单笔转账不能超过 10000 元,请联系客服",
}, nil
}
}
return nil, nil // 校验通过,继续执行
},
},
工具结果脱敏
AfterToolCallback 可以用来做结果脱敏,在工具返回敏感信息时进行过滤:
AfterToolCallbacks: []llmagent.AfterToolCallback{
func(ctx tool.Context, t tool.Tool, args, result map[string]any, err error) (map[string]any, error) {
if err != nil {
return nil, nil
}
// 对查询用户信息的工具,脱敏手机号和身份证
if t.Name() == "query_user_info" {
if phone, ok := result["phone"].(string); ok && len(phone) > 7 {
result["phone"] = phone[:3] + "****" + phone[7:]
}
if idCard, ok := result["id_card"].(string); ok && len(idCard) > 14 {
result["id_card"] = idCard[:6] + "********" + idCard[14:]
}
return result, nil // 返回脱敏后的结果
}
return nil, nil // 其他工具不做处理
},
},
回调执行时序图
搞清楚这些回调的执行顺序很重要。一个完整的 LLMAgent 执行流程大概是这样的:
BeforeAgent
|
v
[LLM Agent 循环开始]
|
+---> BeforeModel --> LLM 调用 --> AfterModel
| | |
| (可能短路, (可能替换响应,
| 不调 LLM) 或记录日志)
| |
| 如果 LLM 返回了工具调用:
| |
| +---> BeforeTool --> 工具执行 --> AfterTool
| | | |
| | (可能短路, (可能修改结果)
| | 不执行工具)
| |
| +---> BeforeTool --> 工具执行 --> AfterTool (可能有多个工具)
| |
| v
| 工具结果发回给 LLM(下一轮循环)
|
+---> BeforeModel --> LLM 调用 --> AfterModel
| |
| 如果 LLM 给出最终回复,
| 循环结束
|
[LLM Agent 循环结束]
|
v
AfterAgent
用一句话概括就是:
BeforeAgent --> [ BeforeModel --> LLM --> AfterModel --> BeforeTool --> Tool --> AfterTool ... ] --> AfterAgent
中间的方括号部分会循环执行,直到 LLM 给出最终回复(不再调用工具)。
工具确认(Tool Confirmation)
有时候你希望某些敏感操作在执行前先征得用户同意。比如:请假申请、删除数据、发送邮件等。ADK-Go 提供了工具确认机制来支持这种"人在回路"(Human-in-the-Loop)的场景。
核心概念
工具确认的流程大致是这样的:
- LLM 决定调用某个工具
- 工具函数里检查
ctx.ToolConfirmation()是否为 nil - 如果为 nil,说明还没有用户确认,调用
ctx.RequestConfirmation()请求确认 - 框架会发出一个特殊的
adk_request_confirmation事件 - 客户端收到事件后,展示确认界面给用户
- 用户做出决定后,客户端把结果发回给 Agent
- 工具函数再次被调用,这次
ctx.ToolConfirmation()不为 nil,包含了用户的决定
简单示例
假设我们有一个请假工具,需要经理审批:
package main
import (
"fmt"
"log"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
"google.golang.org/adk/tool/toolconfirmation"
)
// 请假参数
type VacationArgs struct {
Days int `json:"days"`
Reason string `json:"reason"`
}
// 请假结果
type VacationResult struct {
Status string `json:"status"`
RequestID string `json:"request_id"`
}
// 请假工具函数
func requestVacation(ctx tool.Context, args VacationArgs) (*VacationResult, error) {
log.Printf("请假工具被调用: %d 天, 原因: %s", args.Days, args.Reason)
// 第一步:检查是否已经有用户确认
confirmation := ctx.ToolConfirmation()
if confirmation == nil {
// 还没有确认,发起确认请求
err := ctx.RequestConfirmation(
fmt.Sprintf("员工申请请假 %d 天,原因:%s。请审批。", args.Days, args.Reason),
map[string]any{"days": args.Days}, // 附带的额外数据
)
if err != nil {
return nil, fmt.Errorf("请求确认失败: %w", err)
}
// 返回一个等待状态
return &VacationResult{
Status: "等待审批中",
RequestID: ctx.FunctionCallID(),
}, nil
}
// 第二步:已经有确认结果了
if confirmation.Confirmed {
log.Println("请假已批准")
return &VacationResult{
Status: "已批准",
RequestID: ctx.FunctionCallID(),
}, nil
}
log.Println("请假被拒绝")
return &VacationResult{
Status: "已拒绝",
RequestID: ctx.FunctionCallID(),
}, nil
}
func main() {
vacationTool, err := functiontool.New(
functiontool.Config{
Name: "request_vacation",
Description: "提交请假申请",
},
requestVacation,
)
if err != nil {
log.Fatal(err)
}
_ = vacationTool
}
更简单的方式:RequireConfirmation
如果你的需求比较简单,不需要自己写确认逻辑,functiontool 提供了更便捷的配置:
// 方式一:所有调用都需要确认
vacationTool, _ := functiontool.New(
functiontool.Config{
Name: "request_vacation",
Description: "提交请假申请",
RequireConfirmation: true, // 自动要求确认
},
requestVacation,
)
// 方式二:根据参数动态决定是否需要确认
vacationTool, _ := functiontool.New(
functiontool.Config{
Name: "request_vacation",
Description: "提交请假申请",
RequireConfirmationProvider: func(ctx tool.Context, args map[string]any) bool {
// 请假超过 3 天才需要审批
days, ok := args["days"].(float64)
return ok && days > 3
},
},
requestVacation,
)
客户端如何处理确认
当 Agent 发出确认请求时,你会收到一个特殊的事件,里面包含一个 FunctionCall,名字是 adk_request_confirmation。你的客户端需要:
- 识别这个特殊的 FunctionCall
- 从中提取原始的工具调用信息
- 展示确认界面给用户
- 把用户的决定以
FunctionResponse的形式发回去
// 处理 Agent 返回的事件
for event, err := range r.Run(ctx, userID, sessionID, content, runConfig) {
if err != nil {
log.Printf("错误: %v", err)
continue
}
if event.Content != nil {
for _, part := range event.Content.Parts {
fc := part.FunctionCall
if fc != nil && fc.Name == toolconfirmation.FunctionCallName {
// 这是一个确认请求!
// 提取原始的工具调用
originalCall, err := toolconfirmation.OriginalCallFrom(fc)
if err != nil {
log.Printf("解析原始调用失败: %v", err)
continue
}
log.Printf("需要确认工具调用: %s, 参数: %v", originalCall.Name, originalCall.Args)
// 这里你可以展示 UI 让用户决定...
// 假设用户同意了
userApproved := true
// 构造确认响应
confirmResponse := &genai.Content{
Role: string(genai.RoleUser),
Parts: []*genai.Part{{
FunctionResponse: &genai.FunctionResponse{
Name: toolconfirmation.FunctionCallName,
ID: fc.ID, // 必须和请求的 ID 一致
Response: map[string]any{
"confirmed": userApproved,
"payload": map[string]any{"note": "批准"},
},
},
}},
}
// 把确认结果发回给 Agent
// r.Run(ctx, userID, sessionID, confirmResponse, runConfig)
_ = confirmResponse
}
}
}
}
插件系统
前面讲的回调都是绑定在单个 Agent 上的。如果你有多个 Agent,每个都要加日志、加监控,那就要写很多重复代码。
插件(Plugin)就是为了解决这个问题。插件是在 Runner 级别注册的,对整个 Agent 树生效。你注册一次,所有 Agent、所有 LLM 调用、所有工具调用都会走你的插件逻辑。
插件的结构
先看看 plugin.Config 的定义:
type Config struct {
Name string
// Runner 级别的回调
OnUserMessageCallback OnUserMessageCallback // 收到用户消息时
BeforeRunCallback BeforeRunCallback // 运行开始前
AfterRunCallback AfterRunCallback // 运行结束后
OnEventCallback OnEventCallback // 每个事件产生时
// Agent 级别的回调(对所有 Agent 生效)
BeforeAgentCallback agent.BeforeAgentCallback
AfterAgentCallback agent.AfterAgentCallback
// Model 级别的回调(对所有 LLMAgent 生效)
BeforeModelCallback llmagent.BeforeModelCallback
AfterModelCallback llmagent.AfterModelCallback
OnModelErrorCallback llmagent.OnModelErrorCallback
// Tool 级别的回调(对所有工具生效)
BeforeToolCallback llmagent.BeforeToolCallback
AfterToolCallback llmagent.AfterToolCallback
OnToolErrorCallback llmagent.OnToolErrorCallback
// 插件关闭时的清理函数
CloseFunc func() error
}
可以看到,插件囊括了 Agent 回调、Model 回调、Tool 回调的所有钩子,还额外提供了 Runner 级别的钩子。
Runner 级别的回调类型
这些是插件独有的,Agent 回调里没有:
// 收到用户消息时调用
// 返回非 nil 的 Content 会替换原始的用户消息
type OnUserMessageCallback func(
ctx agent.InvocationContext,
userMessage *genai.Content,
) (*genai.Content, error)
// 运行开始前调用
// 返回非 nil 的 Content 会作为结果直接返回,跳过 Agent 执行
type BeforeRunCallback func(
ctx agent.InvocationContext,
) (*genai.Content, error)
// 运行结束后调用
type AfterRunCallback func(ctx agent.InvocationContext)
// 每个事件产生时调用
// 返回非 nil 的 Event 会替换原始事件
type OnEventCallback func(
ctx agent.InvocationContext,
event *session.Event,
) (*session.Event, error)
创建和使用插件
package main
import (
"fmt"
"log"
"time"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/model"
"google.golang.org/adk/plugin"
"google.golang.org/adk/runner"
"google.golang.org/adk/session"
"google.golang.org/adk/tool"
)
func main() {
// 创建一个日志插件
loggingPlugin, _ := plugin.New(plugin.Config{
Name: "my_logging_plugin",
// 收到用户消息时
OnUserMessageCallback: func(ctx agent.InvocationContext, msg *genai.Content) (*genai.Content, error) {
if len(msg.Parts) > 0 && msg.Parts[0].Text != "" {
log.Printf("[日志] 用户说: %s", msg.Parts[0].Text)
}
return nil, nil // 返回 nil 不修改消息
},
// 运行开始前
BeforeRunCallback: func(ctx agent.InvocationContext) (*genai.Content, error) {
log.Printf("[日志] 运行开始, 调用 ID: %s", ctx.InvocationID())
return nil, nil
},
// 运行结束后
AfterRunCallback: func(ctx agent.InvocationContext) {
log.Printf("[日志] 运行结束, 调用 ID: %s", ctx.InvocationID())
},
// 每个事件产生时
OnEventCallback: func(ctx agent.InvocationContext, event *session.Event) (*session.Event, error) {
log.Printf("[日志] 事件: author=%s, final=%v", event.Author, event.IsFinalResponse())
return nil, nil // 返回 nil 不修改事件
},
})
// 创建一个性能监控插件
perfPlugin, _ := plugin.New(plugin.Config{
Name: "perf_monitor",
BeforeModelCallback: func(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) {
// 记录 LLM 调用开始时间到 state
ctx.State().Set("_llm_start_time", time.Now().UnixMilli())
return nil, nil
},
AfterModelCallback: func(ctx agent.CallbackContext, resp *model.LLMResponse, respErr error) (*model.LLMResponse, error) {
startTime, _ := ctx.State().Get("_llm_start_time")
if startMs, ok := startTime.(int64); ok {
elapsed := time.Now().UnixMilli() - startMs
log.Printf("[性能] LLM 调用耗时: %d ms, Agent: %s", elapsed, ctx.AgentName())
}
return nil, nil
},
BeforeToolCallback: func(ctx tool.Context, t tool.Tool, args map[string]any) (map[string]any, error) {
log.Printf("[性能] 工具 %s 开始执行", t.Name())
return nil, nil
},
AfterToolCallback: func(ctx tool.Context, t tool.Tool, args, result map[string]any, err error) (map[string]any, error) {
log.Printf("[性能] 工具 %s 执行完成", t.Name())
return nil, nil
},
})
// 在 Runner 中使用插件
r, err := runner.New(runner.Config{
AppName: "my_app",
Agent: myAgent, // 假设已经创建好了
SessionService: session.InMemoryService(),
PluginConfig: runner.PluginConfig{
Plugins: []*plugin.Plugin{loggingPlugin, perfPlugin},
},
})
if err != nil {
log.Fatal(err)
}
_ = r
}
使用内置的日志插件
ADK-Go 自带了一个 loggingplugin,开箱即用,非常适合开发调试:
import "google.golang.org/adk/plugin/loggingplugin"
// 创建日志插件,参数是插件名称(空字符串会用默认名 "logging_plugin")
logPlugin := loggingplugin.MustNew("my_logger")
r, _ := runner.New(runner.Config{
AppName: "my_app",
Agent: myAgent,
SessionService: session.InMemoryService(),
PluginConfig: runner.PluginConfig{
Plugins: []*plugin.Plugin{logPlugin},
},
})
这个插件会在控制台以灰色文字打印出所有关键事件,包括:
- 用户消息和上下文
- Agent 的启动和完成
- LLM 请求和响应(包括 token 用量)
- 工具调用和结果
- 事件和最终响应
- 所有错误信息
非常适合在开发阶段排查问题。
使用 FunctionCallModifier 插件
ADK-Go 还提供了 functioncallmodifier 插件,用于在 LLM 调用前后动态修改函数声明和参数:
import "google.golang.org/adk/plugin/functioncallmodifier"
// 给特定工具的参数中注入额外字段
modifierPlugin, _ := functioncallmodifier.NewPlugin(functioncallmodifier.FunctionCallModifierConfig{
// 决定哪些工具需要修改
Predicate: func(toolName string) bool {
return toolName == "search" || toolName == "query"
},
// 注入的额外参数
Args: map[string]*genai.Schema{
"user_context": {
Type: "STRING",
Description: "当前用户的上下文信息",
},
},
// 可选:修改工具描述
OverrideDescription: func(original string) string {
return original + " (需要提供 user_context 参数)"
},
})
这个插件的工作方式是:
1. BeforeModel 阶段:给匹配的工具声明加上额外的参数字段
2. AfterModel 阶段:从 LLM 返回的 FunctionCall 中提取这些额外参数,存到 state 中,然后从参数里删掉
这样做的好处是让 LLM 填写额外信息,但不影响工具函数本身的签名。
回调 vs 插件:怎么选?
| 维度 | 回调(Callback) | 插件(Plugin) |
|---|---|---|
| 作用范围 | 单个 Agent | 整个 Runner(所有 Agent) |
| 注册位置 | Agent 的 Config 里 | Runner 的 PluginConfig 里 |
| 粒度 | Agent/Model/Tool 级别 | Run/Message/Event + Agent/Model/Tool 级别 |
| 数量 | 每种类型可以注册多个(列表) | 每种类型只有一个(但可以多个插件) |
| 使用场景 | Agent 特定的逻辑 | 全局性的横切关注点 |
| 典型用途 | 权限检查、参数校验、缓存 | 日志、监控、审计、全局过滤 |
简单的原则:
- 如果逻辑只跟某一个 Agent 有关(比如这个 Agent 需要特殊的缓存策略),用回调
- 如果逻辑需要对所有 Agent 生效(比如全局日志、性能监控),用插件
- 如果你需要在 Runner 级别做事情(比如拦截用户消息、监听所有事件),只能用插件
另外要注意执行顺序:当同时存在插件的回调和 Agent 的回调时,插件的回调先执行。如果插件的回调返回了非 nil 的结果,Agent 自身的回调会被跳过。
常见使用场景汇总
1. 日志记录
最基本的需求。用插件来做全局日志,用回调来做特定 Agent 的详细日志:
// 全局日志 - 用插件
logPlugin := loggingplugin.MustNew("")
// 特定 Agent 的详细日志 - 用回调
llmagent.New(llmagent.Config{
BeforeAgentCallbacks: []agent.BeforeAgentCallback{
func(ctx agent.CallbackContext) (*genai.Content, error) {
log.Printf("Agent %s 开始处理用户请求", ctx.AgentName())
return nil, nil
},
},
})
2. 输入/输出过滤
在 LLM 调用前后对内容进行过滤:
// 用插件做全局的输入过滤
plugin.New(plugin.Config{
Name: "content_filter",
OnUserMessageCallback: func(ctx agent.InvocationContext, msg *genai.Content) (*genai.Content, error) {
// 过滤用户消息中的敏感词
for _, part := range msg.Parts {
if part.Text != "" {
part.Text = filterSensitiveWords(part.Text)
}
}
return msg, nil // 返回修改后的消息
},
})
3. 性能监控
记录每次 LLM 调用和工具调用的耗时:
plugin.New(plugin.Config{
Name: "metrics",
BeforeModelCallback: func(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) {
ctx.State().Set("_model_start", time.Now().UnixNano())
return nil, nil
},
AfterModelCallback: func(ctx agent.CallbackContext, resp *model.LLMResponse, err error) (*model.LLMResponse, error) {
if start, _ := ctx.State().Get("_model_start"); start != nil {
elapsed := time.Duration(time.Now().UnixNano() - start.(int64))
metrics.RecordModelLatency(ctx.AgentName(), elapsed)
}
return nil, nil
},
})
4. 权限检查
在 Agent 执行前检查用户权限:
llmagent.New(llmagent.Config{
BeforeAgentCallbacks: []agent.BeforeAgentCallback{
func(ctx agent.CallbackContext) (*genai.Content, error) {
if !checkUserPermission(ctx.UserID(), ctx.AgentName()) {
return genai.NewContentFromText("权限不足", genai.RoleModel), nil
}
return nil, nil
},
},
})
5. 工具调用限流
限制工具的调用频率:
var toolCallCount int
var toolCallMutex sync.Mutex
BeforeToolCallbacks: []llmagent.BeforeToolCallback{
func(ctx tool.Context, t tool.Tool, args map[string]any) (map[string]any, error) {
toolCallMutex.Lock()
defer toolCallMutex.Unlock()
toolCallCount++
if toolCallCount > 10 {
return map[string]any{
"error": "工具调用次数过多,请稍后再试",
}, nil
}
return nil, nil
},
},
完整示例:带回调和插件的 Agent
把前面学的东西串在一起,看一个完整的例子:
package main
import (
"context"
"fmt"
"log"
"time"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/model"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/plugin"
"google.golang.org/adk/plugin/loggingplugin"
"google.golang.org/adk/runner"
"google.golang.org/adk/session"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
)
// 天气查询参数
type WeatherArgs struct {
City string `json:"city" jsonschema:"description=城市名称"`
}
// 天气查询结果
type WeatherResult struct {
City string `json:"city"`
Temp int `json:"temp"`
Weather string `json:"weather"`
}
// 天气查询函数
func getWeather(ctx tool.Context, args WeatherArgs) (*WeatherResult, error) {
// 模拟查询天气
return &WeatherResult{
City: args.City,
Temp: 25,
Weather: "晴",
}, nil
}
func main() {
ctx := context.Background()
// 创建模型
llm, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{})
if err != nil {
log.Fatal(err)
}
// 创建天气工具
weatherTool, _ := functiontool.New(
functiontool.Config{
Name: "get_weather",
Description: "查询指定城市的天气",
},
getWeather,
)
// 创建 Agent,带上各种回调
weatherAgent, _ := llmagent.New(llmagent.Config{
Name: "weather_agent",
Model: llm,
Instruction: "你是一个天气助手,可以帮用户查询天气。请用中文回复。",
Tools: []tool.Tool{weatherTool},
// Agent 级别回调
BeforeAgentCallbacks: []agent.BeforeAgentCallback{
func(ctx agent.CallbackContext) (*genai.Content, error) {
log.Printf("[Agent] %s 开始执行", ctx.AgentName())
ctx.State().Set("agent_start", time.Now().Format(time.RFC3339))
return nil, nil
},
},
AfterAgentCallbacks: []agent.AfterAgentCallback{
func(ctx agent.CallbackContext) (*genai.Content, error) {
log.Printf("[Agent] %s 执行完毕", ctx.AgentName())
return nil, nil
},
},
// Model 级别回调
BeforeModelCallbacks: []llmagent.BeforeModelCallback{
func(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) {
log.Printf("[Model] 发送请求,消息数: %d", len(req.Contents))
return nil, nil
},
},
AfterModelCallbacks: []llmagent.AfterModelCallback{
func(ctx agent.CallbackContext, resp *model.LLMResponse, respErr error) (*model.LLMResponse, error) {
if resp != nil && resp.UsageMetadata != nil {
log.Printf("[Model] Token: 输入=%d, 输出=%d",
resp.UsageMetadata.PromptTokenCount,
resp.UsageMetadata.CandidatesTokenCount,
)
}
return nil, nil
},
},
// Tool 级别回调
BeforeToolCallbacks: []llmagent.BeforeToolCallback{
func(ctx tool.Context, t tool.Tool, args map[string]any) (map[string]any, error) {
log.Printf("[Tool] 调用 %s, 参数: %v", t.Name(), args)
return nil, nil
},
},
AfterToolCallbacks: []llmagent.AfterToolCallback{
func(ctx tool.Context, t tool.Tool, args, result map[string]any, err error) (map[string]any, error) {
if err != nil {
log.Printf("[Tool] %s 出错: %v", t.Name(), err)
} else {
log.Printf("[Tool] %s 完成, 结果: %v", t.Name(), result)
}
return nil, nil
},
},
})
// 创建全局日志插件
logPlugin := loggingplugin.MustNew("")
// 创建自定义审计插件
auditPlugin, _ := plugin.New(plugin.Config{
Name: "audit",
OnEventCallback: func(ctx agent.InvocationContext, event *session.Event) (*session.Event, error) {
if event.IsFinalResponse() && event.Content != nil {
for _, part := range event.Content.Parts {
if part.Text != "" {
log.Printf("[审计] 最终回复: %s", part.Text)
}
}
}
return nil, nil
},
})
// 创建 Runner
sessionService := session.InMemoryService()
r, err := runner.New(runner.Config{
AppName: "weather_app",
Agent: weatherAgent,
SessionService: sessionService,
PluginConfig: runner.PluginConfig{
Plugins: []*plugin.Plugin{logPlugin, auditPlugin},
},
})
if err != nil {
log.Fatal(err)
}
// 创建会话
sess, err := sessionService.Create(ctx, &session.CreateRequest{
AppName: "weather_app",
UserID: "user1",
})
if err != nil {
log.Fatal(err)
}
// 发送消息
userMsg := genai.NewContentFromText("北京今天天气怎么样?", genai.RoleUser)
for event, err := range r.Run(ctx, "user1", sess.Session.ID(), userMsg, agent.RunConfig{}) {
if err != nil {
log.Printf("错误: %v", err)
continue
}
if event.IsFinalResponse() && event.Content != nil {
for _, part := range event.Content.Parts {
if part.Text != "" {
fmt.Printf("助手: %s\n", part.Text)
}
}
}
}
}
小结
这一章我们学了 ADK-Go 中两种扩展 Agent 行为的机制:
回调(Callback): - 注册在单个 Agent 上,对那个 Agent 的执行生效 - 三种级别:BeforeAgent/AfterAgent、BeforeModel/AfterModel、BeforeTool/AfterTool - 可以观察、修改、短路 Agent 的执行流程 - 适合 Agent 特定的逻辑,比如参数校验、缓存、权限检查
插件(Plugin): - 注册在 Runner 上,对整个 Agent 树生效 - 除了 Agent/Model/Tool 级别的回调,还有 Runner 级别的钩子(OnUserMessage、BeforeRun、AfterRun、OnEvent) - 适合全局性的横切关注点,比如日志、监控、审计
工具确认(Tool Confirmation):
- 让敏感工具在执行前征得用户同意
- 通过 ctx.ToolConfirmation() 和 ctx.RequestConfirmation() 实现
- 也可以用 functiontool.Config 的 RequireConfirmation 简化配置
记住一个简单的选择原则:如果逻辑只跟某一个 Agent 有关,用回调;如果需要全局生效,用插件。
动手练习
练习 1:执行耗时监控
创建一个 Agent,注册 BeforeAgent 和 AfterAgent 回调来记录 Agent 的执行耗时。在 BeforeAgent 里把当前时间写入 state,在 AfterAgent 里读出来算差值。
提示:
// BeforeAgent 里
ctx.State().Set("_start_time", time.Now().UnixMilli())
// AfterAgent 里
startTime, _ := ctx.State().Get("_start_time")
练习 2:LLM 响应缓存
实现一对 BeforeModel/AfterModel 回调来缓存 LLM 的响应。可以用一个简单的 map[string]*model.LLMResponse 做内存缓存,key 可以用用户消息的内容做哈希。
要求: - BeforeModel 里检查缓存,命中就直接返回缓存的响应 - AfterModel 里把 LLM 的响应存入缓存 - 只缓存纯文本回复,不缓存工具调用
练习 3:敏感工具确认
创建一个"删除文件"工具,使用 RequireConfirmation: true 配置,要求所有调用都需要用户确认。写一个简单的控制台程序,在收到确认请求时让用户输入 y/n 来决定。
练习 4:全局审计插件
创建一个审计插件,满足以下需求: - 记录每次用户消息的内容和时间 - 记录每次 LLM 调用的 token 消耗 - 记录每次工具调用的名称、参数和结果 - 在运行结束时输出一份汇总报告
提示:可以用一个结构体来累积数据,在 AfterRunCallback 里输出汇总。
练习 5:输入内容改写插件
创建一个插件,在 OnUserMessageCallback 里对用户消息进行改写。比如:
- 自动在用户消息后面追加"请用中文回复"
- 或者过滤掉用户消息中的特殊字符
试试看修改后的消息是否真的影响了 LLM 的回复。