第六章:回调与插件 -- 在关键节点插入你的逻辑

本章你将学到

  • 理解 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)

注意几个关键点:

  1. BeforeModelCallback 可以拿到完整的 LLMRequest,包括消息历史、工具声明、系统指令等
  2. AfterModelCallback 同时接收响应和错误,方便你统一处理
  3. 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.Contextagent.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)的场景。

核心概念

工具确认的流程大致是这样的:

  1. LLM 决定调用某个工具
  2. 工具函数里检查 ctx.ToolConfirmation() 是否为 nil
  3. 如果为 nil,说明还没有用户确认,调用 ctx.RequestConfirmation() 请求确认
  4. 框架会发出一个特殊的 adk_request_confirmation 事件
  5. 客户端收到事件后,展示确认界面给用户
  6. 用户做出决定后,客户端把结果发回给 Agent
  7. 工具函数再次被调用,这次 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。你的客户端需要:

  1. 识别这个特殊的 FunctionCall
  2. 从中提取原始的工具调用信息
  3. 展示确认界面给用户
  4. 把用户的决定以 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.ConfigRequireConfirmation 简化配置

记住一个简单的选择原则:如果逻辑只跟某一个 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 的回复。