第二章:工具系统 - 给Agent装上手脚
本章你将学到
- 理解为什么Agent需要工具,以及工具在Agent架构中的角色
- 使用
functiontool把普通的Go函数变成Agent可以调用的工具 - 理解ADK如何从Go结构体自动生成JSON Schema
- 组合多个工具给Agent使用
- 使用Gemini内置工具(如Google搜索)
- 用
agenttool把一个Agent包装成工具给另一个Agent用 - 深入了解
tool.Context的各种能力 - 理解工具调用在Agent循环中的完整生命周期
为什么Agent需要工具
先来想一个问题:大语言模型(LLM)厉害在哪里?它能理解自然语言、做推理、写代码、 总结文章......但它有一个致命的弱点 -- 它只能"想",不能"做"。
打个比方,LLM就像一个超级聪明的大脑,但是被装在一个罐子里。它能帮你想出一套 完美的旅行计划,但它没办法帮你订机票;它能告诉你今天应该查一下天气,但它自己 查不了天气。
工具(Tool)就是给这个大脑装上手脚。
有了工具,Agent就变成了一个完整的"人":
- 大脑(LLM)负责理解用户意图、做出决策
- 手脚(工具)负责执行具体操作
一个典型的Agent工作流程是这样的:
用户:"帮我查一下北京今天的天气"
Agent的大脑(LLM)想:
"用户想知道北京的天气,我应该用搜索工具查一下"
Agent的手(工具)做:
调用 GoogleSearch("北京今天天气")
得到结果:晴,25度
Agent的大脑(LLM)又想:
"搜到了,让我组织一下语言回复用户"
Agent回复:"北京今天天气晴朗,气温25度,适合出门。"
看到了吧?LLM负责"想",工具负责"做"。这就是Agent和普通LLM聊天的本质区别。
在ADK-Go里,工具系统被设计得很优雅。所有工具都实现同一个 tool.Tool 接口:
// tool.Tool 接口定义 - 所有工具的基础
type Tool interface {
// 工具的名字,LLM通过名字来调用工具
Name() string
// 工具的描述,LLM通过描述来理解工具能做什么
Description() string
// 是否是长时间运行的工具(高级用法,后面会讲)
IsLongRunning() bool
}
ADK-Go提供了好几种工具类型,我们一个个来看。
FunctionTool:把Go函数变成工具
这是你最常用的工具类型。它的核心思想非常简单:写一个普通的Go函数,ADK帮你 把它包装成LLM能调用的工具。
第一个工具:计算器
让我们从一个简单的计算器开始:
package main
import (
"context"
"fmt"
"log"
"os"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/cmd/launcher"
"google.golang.org/adk/cmd/launcher/full"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
)
// 第一步:定义输入结构体
// ADK会自动把这个结构体转成JSON Schema,告诉LLM这个工具需要什么参数
type CalcInput struct {
A float64 `json:"a" description:"第一个数字"`
B float64 `json:"b" description:"第二个数字"`
Op string `json:"op" description:"运算符: add, sub, mul, div"`
}
// 第二步:定义输出结构体
type CalcOutput struct {
Result float64 `json:"result"`
Error string `json:"error,omitempty"`
}
// 第三步:写工具函数
// 注意:第一个参数必须是 tool.Context,第二个参数是你的输入结构体
func calculate(ctx tool.Context, input CalcInput) (CalcOutput, error) {
switch input.Op {
case "add":
return CalcOutput{Result: input.A + input.B}, nil
case "sub":
return CalcOutput{Result: input.A - input.B}, nil
case "mul":
return CalcOutput{Result: input.A * input.B}, nil
case "div":
if input.B == 0 {
return CalcOutput{Error: "除数不能为0"}, nil
}
return CalcOutput{Result: input.A / input.B}, nil
default:
return CalcOutput{Error: "不支持的运算符"}, nil
}
}
func main() {
ctx := context.Background()
// 创建模型
model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatalf("创建模型失败: %v", err)
}
// 第四步:用 functiontool.New 把Go函数包装成工具
calcTool, err := functiontool.New(functiontool.Config{
Name: "calculator",
Description: "执行基本数学运算(加减乘除)",
}, calculate)
if err != nil {
log.Fatalf("创建工具失败: %v", err)
}
// 第五步:把工具交给Agent
a, err := llmagent.New(llmagent.Config{
Name: "math_agent",
Model: model,
Description: "一个能做数学计算的助手",
Instruction: "你是一个数学助手。当用户需要做计算时,使用calculator工具。" +
"把计算结果用友好的方式告诉用户。",
Tools: []tool.Tool{calcTool},
})
if err != nil {
log.Fatalf("创建Agent失败: %v", err)
}
// 启动
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(a),
}
l := full.NewLauncher()
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
log.Fatalf("运行失败: %v", err)
}
}
运行起来之后,你可以这样跟它聊:
你:帮我算一下 123.45 乘以 67.89
Agent:123.45 x 67.89 = 8,381.40(Agent内部调用了calculator工具)
看起来就这么简单。但背后发生了不少事情,让我们拆解一下。
结构体标签和JSON Schema
你可能注意到了输入结构体上的标签:
type CalcInput struct {
A float64 `json:"a" description:"第一个数字"`
B float64 `json:"b" description:"第二个数字"`
Op string `json:"op" description:"运算符: add, sub, mul, div"`
}
这里有两类标签:
json:"a"- 标准的Go JSON标签,定义了字段在JSON中的名字description:"第一个数字"- ADK专用标签,会被用来生成JSON Schema里的描述
ADK会自动把这个结构体转成类似这样的JSON Schema:
{
"type": "object",
"properties": {
"a": {
"type": "number",
"description": "第一个数字"
},
"b": {
"type": "number",
"description": "第二个数字"
},
"op": {
"type": "string",
"description": "运算符: add, sub, mul, div"
}
},
"required": ["a", "b", "op"]
}
这个JSON Schema会被发送给LLM,LLM就知道:哦,要调用这个calculator工具, 我需要提供两个数字和一个运算符。
description标签很重要! 它直接影响LLM能不能正确使用你的工具。写得清楚明白, LLM就能更准确地传参。写得含糊不清,LLM可能会传错参数。
函数签名的约定
functiontool.New 是一个泛型函数,它的签名是这样的:
func New[TArgs, TResults any](cfg Config, handler Func[TArgs, TResults]) (tool.Tool, error)
其中 Func 的定义是:
type Func[TArgs, TResults any] func(tool.Context, TArgs) (TResults, error)
所以你的工具函数必须遵循这个模式:
func 你的函数名(ctx tool.Context, input 输入类型) (输出类型, error)
几个要点:
- 第一个参数必须是
tool.Context- 这是ADK传给你的上下文,包含了很多有用的信息 - 第二个参数是你的输入结构体 - 必须是struct或map类型,不能是基本类型
- 返回值是 (输出类型, error) - 输出类型通常也是struct,但基本类型也行
- 输入类型不能是 string、int 这种基本类型 - 必须是struct或map
为什么输入必须是struct?因为LLM调用工具时,传的是一个JSON对象,ADK需要把它 映射到你的结构体上。如果你只需要一个字符串参数,那就包一层:
// 这样不行!
// func myTool(ctx tool.Context, input string) (string, error)
// 这样才行
type MyInput struct {
Text string `json:"text" description:"输入的文本"`
}
type MyOutput struct {
Result string `json:"result"`
}
func myTool(ctx tool.Context, input MyInput) (MyOutput, error) {
// ...
}
functiontool.Config 详解
创建工具时需要传一个 Config:
type Config struct {
// 工具名字 - LLM通过这个名字来调用工具
Name string
// 工具描述 - LLM通过描述来决定什么时候该用这个工具
Description string
// 可选:手动指定输入的JSON Schema
// 如果不设置,ADK会从你的输入结构体自动推导
InputSchema *jsonschema.Schema
// 可选:手动指定输出的JSON Schema
OutputSchema *jsonschema.Schema
// 是否是长时间运行的工具(后面章节会讲)
IsLongRunning bool
// 是否需要用户确认才能执行(人在回路)
RequireConfirmation bool
// 动态判断是否需要用户确认的函数
RequireConfirmationProvider any
}
大部分情况下,你只需要设置 Name 和 Description 就够了:
calcTool, err := functiontool.New(functiontool.Config{
Name: "calculator",
Description: "执行基本数学运算(加减乘除)",
}, calculate)
Name 和 Description 怎么写很有讲究:
- Name 要简洁、语义清晰,用小写字母和下划线,比如
get_weather、send_email - Description 要告诉LLM这个工具"能做什么"和"什么时候该用"。越准确越好
多个工具组合
一个Agent可以同时拥有多个工具,就像一个人可以同时会用锤子和螺丝刀一样。 LLM会根据用户的请求自动选择合适的工具。
package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"time"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/cmd/launcher"
"google.golang.org/adk/cmd/launcher/full"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
)
// === 工具1:计算器 ===
type CalcInput struct {
A float64 `json:"a" description:"第一个数字"`
B float64 `json:"b" description:"第二个数字"`
Op string `json:"op" description:"运算符: add, sub, mul, div"`
}
type CalcOutput struct {
Result float64 `json:"result"`
Error string `json:"error,omitempty"`
}
func calculate(ctx tool.Context, input CalcInput) (CalcOutput, error) {
switch input.Op {
case "add":
return CalcOutput{Result: input.A + input.B}, nil
case "sub":
return CalcOutput{Result: input.A - input.B}, nil
case "mul":
return CalcOutput{Result: input.A * input.B}, nil
case "div":
if input.B == 0 {
return CalcOutput{Error: "除数不能为0"}, nil
}
return CalcOutput{Result: input.A / input.B}, nil
default:
return CalcOutput{Error: "不支持的运算符"}, nil
}
}
// === 工具2:获取当前时间 ===
type TimeInput struct {
Timezone string `json:"timezone" description:"时区名称,如 Asia/Shanghai、America/New_York"`
}
type TimeOutput struct {
CurrentTime string `json:"current_time"`
Timezone string `json:"timezone"`
Error string `json:"error,omitempty"`
}
func getCurrentTime(ctx tool.Context, input TimeInput) (TimeOutput, error) {
// 默认使用上海时区
tz := input.Timezone
if tz == "" {
tz = "Asia/Shanghai"
}
loc, err := time.LoadLocation(tz)
if err != nil {
return TimeOutput{Error: fmt.Sprintf("无效的时区: %s", tz)}, nil
}
now := time.Now().In(loc)
return TimeOutput{
CurrentTime: now.Format("2006-01-02 15:04:05"),
Timezone: tz,
}, nil
}
// === 工具3:字符串处理 ===
type StringInput struct {
Text string `json:"text" description:"要处理的文本"`
Operation string `json:"operation" description:"操作类型: upper, lower, reverse, count"`
}
type StringOutput struct {
Result string `json:"result"`
Error string `json:"error,omitempty"`
}
func processString(ctx tool.Context, input StringInput) (StringOutput, error) {
switch input.Operation {
case "upper":
return StringOutput{Result: strings.ToUpper(input.Text)}, nil
case "lower":
return StringOutput{Result: strings.ToLower(input.Text)}, nil
case "reverse":
// 翻转字符串
runes := []rune(input.Text)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return StringOutput{Result: string(runes)}, nil
case "count":
return StringOutput{Result: fmt.Sprintf("%d个字符", len([]rune(input.Text)))}, nil
default:
return StringOutput{Error: "不支持的操作"}, nil
}
}
func main() {
ctx := context.Background()
model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatalf("创建模型失败: %v", err)
}
// 创建三个工具
calcTool, err := functiontool.New(functiontool.Config{
Name: "calculator",
Description: "执行基本数学运算(加减乘除)",
}, calculate)
if err != nil {
log.Fatalf("创建计算器工具失败: %v", err)
}
timeTool, err := functiontool.New(functiontool.Config{
Name: "get_current_time",
Description: "获取指定时区的当前时间",
}, getCurrentTime)
if err != nil {
log.Fatalf("创建时间工具失败: %v", err)
}
stringTool, err := functiontool.New(functiontool.Config{
Name: "process_string",
Description: "处理字符串:转大写、转小写、翻转、计数",
}, processString)
if err != nil {
log.Fatalf("创建字符串工具失败: %v", err)
}
// 把所有工具都交给Agent
a, err := llmagent.New(llmagent.Config{
Name: "multi_tool_agent",
Model: model,
Description: "一个拥有多种能力的助手",
Instruction: "你是一个万能助手,可以做数学计算、查询时间、处理字符串。" +
"根据用户的需求选择合适的工具来完成任务。",
Tools: []tool.Tool{calcTool, timeTool, stringTool},
})
if err != nil {
log.Fatalf("创建Agent失败: %v", err)
}
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(a),
}
l := full.NewLauncher()
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
log.Fatalf("运行失败: %v", err)
}
}
现在这个Agent可以处理各种请求了:
你:帮我算一下 256 除以 8
Agent:(调用calculator工具)256 / 8 = 32
你:现在东京几点了?
Agent:(调用get_current_time工具)东京现在是 2025-05-20 14:30:00
你:把 "Hello World" 翻转一下
Agent:(调用process_string工具)翻转后是 "dlroW olleH"
LLM是怎么知道该调用哪个工具的?因为我们给每个工具都写了清晰的 Name 和
Description。LLM会根据用户的请求和工具的描述来判断。这也是为什么
Description 写得好不好非常关键 -- 它直接影响Agent的"智商"。
Gemini内置工具
除了自定义的函数工具,ADK-Go还支持Gemini模型内置的工具。这些工具不需要你写 任何函数,Gemini模型本身就能执行它们。
GoogleSearch
最常用的内置工具就是Google搜索:
import "google.golang.org/adk/tool/geminitool"
a, err := llmagent.New(llmagent.Config{
Name: "search_agent",
Model: model,
Description: "可以搜索互联网信息的助手",
Instruction: "你是一个搜索助手,用Google搜索帮用户找信息。",
Tools: []tool.Tool{
geminitool.GoogleSearch{}, // 就这么简单!
},
})
geminitool.GoogleSearch{} 和 functiontool.New 创建的工具有本质区别:
| FunctionTool | GoogleSearch | |
|---|---|---|
| 执行位置 | 你的Go代码里 | Gemini服务端 |
| 需要写代码 | 需要 | 不需要 |
| 可以自定义逻辑 | 可以 | 不可以 |
| 调用方式 | LLM发出函数调用请求,ADK执行你的函数 | LLM在内部直接完成搜索 |
简单来说,GoogleSearch是Gemini模型"自带"的能力,不需要你的代码参与执行。
用 geminitool.New 创建其他内置工具
除了 GoogleSearch,你还可以通过 geminitool.New 来使用Gemini的其他内置能力:
import (
"google.golang.org/genai"
"google.golang.org/adk/tool/geminitool"
)
// 创建一个自定义的Gemini原生工具
myGeminiTool := geminitool.New("data_retrieval", &genai.Tool{
Retrieval: &genai.Retrieval{
// 配置检索相关参数...
},
})
这给了你使用Gemini所有原生能力的入口。
重要限制:内置工具和函数工具不能混用
这里有一个很重要的API限制:在同一个Agent里,你不能同时使用GoogleSearch和 FunctionTool。 这是Gemini API的限制,不是ADK的限制。
// 这样会出问题!
a, err := llmagent.New(llmagent.Config{
Name: "broken_agent",
Model: model,
Tools: []tool.Tool{
geminitool.GoogleSearch{}, // 内置工具
calcTool, // 函数工具
// 两种工具混在一起,Gemini API不支持
},
})
那怎么办?答案就是下一节要讲的 agenttool。
AgentTool:把Agent当工具用
agenttool 是一个非常巧妙的设计。它的思路是:既然不同类型的工具不能放在
同一个Agent里,那就把每种工具放在一个子Agent里,再把这些子Agent包装成"工具"
交给根Agent。
这就是ADK的 multipletools 示例展示的模式:
package main
import (
"context"
"log"
"os"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/cmd/launcher"
"google.golang.org/adk/cmd/launcher/full"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/agenttool"
"google.golang.org/adk/tool/functiontool"
"google.golang.org/adk/tool/geminitool"
)
func main() {
ctx := context.Background()
model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatalf("创建模型失败: %v", err)
}
// ========================================
// 子Agent 1:搜索Agent(使用Gemini内置工具)
// ========================================
searchAgent, err := llmagent.New(llmagent.Config{
Name: "search_agent",
Model: model,
Description: "专门做Google搜索,获取互联网最新信息",
Instruction: "你是Google搜索专家。根据用户的请求进行搜索,返回准确的信息。",
Tools: []tool.Tool{
geminitool.GoogleSearch{},
},
})
if err != nil {
log.Fatalf("创建搜索Agent失败: %v", err)
}
// ========================================
// 子Agent 2:计算Agent(使用自定义函数工具)
// ========================================
type CalcInput struct {
A float64 `json:"a" description:"第一个数字"`
B float64 `json:"b" description:"第二个数字"`
Op string `json:"op" description:"运算符: add, sub, mul, div"`
}
type CalcOutput struct {
Result float64 `json:"result"`
Error string `json:"error,omitempty"`
}
calcTool, err := functiontool.New(functiontool.Config{
Name: "calculator",
Description: "执行基本数学运算",
}, func(ctx tool.Context, input CalcInput) (CalcOutput, error) {
switch input.Op {
case "add":
return CalcOutput{Result: input.A + input.B}, nil
case "sub":
return CalcOutput{Result: input.A - input.B}, nil
case "mul":
return CalcOutput{Result: input.A * input.B}, nil
case "div":
if input.B == 0 {
return CalcOutput{Error: "除数不能为0"}, nil
}
return CalcOutput{Result: input.A / input.B}, nil
default:
return CalcOutput{Error: "不支持的运算符"}, nil
}
})
if err != nil {
log.Fatalf("创建计算器工具失败: %v", err)
}
calcAgent, err := llmagent.New(llmagent.Config{
Name: "calc_agent",
Model: model,
Description: "专门做数学计算",
Instruction: "你是数学计算专家。使用calculator工具完成计算任务。",
Tools: []tool.Tool{calcTool},
})
if err != nil {
log.Fatalf("创建计算Agent失败: %v", err)
}
// ========================================
// 根Agent:把两个子Agent包装成工具
// ========================================
rootAgent, err := llmagent.New(llmagent.Config{
Name: "root_agent",
Model: model,
Description: "一个能搜索信息和做数学计算的智能助手",
Instruction: "你是一个智能助手。" +
"当用户需要搜索信息时,使用search_agent。" +
"当用户需要做数学计算时,使用calc_agent。",
Tools: []tool.Tool{
agenttool.New(searchAgent, nil), // 把搜索Agent包装成工具
agenttool.New(calcAgent, nil), // 把计算Agent包装成工具
},
})
if err != nil {
log.Fatalf("创建根Agent失败: %v", err)
}
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(rootAgent),
}
l := full.NewLauncher()
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
log.Fatalf("运行失败: %v", err)
}
}
agenttool.New 的细节
agenttool.New 的签名很简单:
func New(agent agent.Agent, cfg *Config) tool.Tool
第二个参数 cfg 可以传 nil,也可以传配置:
type Config struct {
// 如果为true,子Agent执行完后不会让LLM再做一次总结
SkipSummarization bool
}
SkipSummarization 是什么意思呢?默认情况下,子Agent执行完毕后,根Agent的
LLM会对子Agent的结果进行一次"总结"。如果你觉得没必要(比如子Agent返回的结果
已经很好了),可以设成 true 跳过这一步,省点tokens。
Agent当工具用的工作流程
用一张图来说明整个流程:
用户:"搜一下北京明天的天气,然后帮我算一下25度转华氏度是多少"
根Agent(LLM)想:
"用户有两个需求,先搜天气,再做计算"
根Agent调用 search_agent 工具:
→ search_agent 收到请求 "北京明天天气"
→ search_agent 使用 GoogleSearch 搜索
→ search_agent 返回:"北京明天晴,25度"
根Agent调用 calc_agent 工具:
→ calc_agent 收到请求 "25度转华氏度"
→ calc_agent 使用 calculator 工具:25 * 9/5 + 32
→ calc_agent 返回:"77华氏度"
根Agent(LLM)整合结果:
"北京明天天气晴朗,气温25摄氏度(约77华氏度)。"
看到了吧?通过 agenttool,根Agent可以"指挥"多个专业子Agent协作完成任务。
每个子Agent各司其职,互不干扰。
tool.Context 详解
你可能已经注意到了,每个工具函数的第一个参数都是 tool.Context。这个Context
不仅仅是一个普通的 context.Context(它嵌入了 context.Context),还提供了
很多Agent框架级别的能力。
来看看 tool.Context 接口到底有什么:
type Context interface {
agent.CallbackContext // 继承了回调上下文的能力
// 获取当前函数调用的唯一ID
FunctionCallID() string
// 获取当前事件的Actions,可以用来修改状态、触发Agent转移等
Actions() *session.EventActions
// 在Agent的记忆中搜索相关信息
SearchMemory(context.Context, string) (*memory.SearchResponse, error)
// 检查当前是否有用户确认(人在回路)
ToolConfirmation() *toolconfirmation.ToolConfirmation
// 主动请求用户确认
RequestConfirmation(hint string, payload any) error
}
因为嵌入了 agent.CallbackContext,你还可以访问这些信息:
// 来自 CallbackContext -> ReadonlyContext
ctx.AgentName() // 当前Agent的名字
ctx.UserID() // 当前用户的ID
ctx.AppName() // 应用名称
ctx.SessionID() // 当前会话ID
ctx.InvocationID() // 当前调用的ID
ctx.Branch() // 当前分支
ctx.UserContent() // 用户发送的原始内容
ctx.ReadonlyState() // 只读状态(通过ReadonlyContext时)
// 来自 CallbackContext
ctx.State() // 可读写的Session状态
ctx.Artifacts() // 访问工件(文件等)
FunctionCallID
每次LLM调用工具时,都会生成一个唯一的ID。你可以用它来追踪和关联请求:
func myTool(ctx tool.Context, input MyInput) (MyOutput, error) {
callID := ctx.FunctionCallID()
log.Printf("工具被调用,Call ID: %s", callID)
// ...
}
通过 Actions 修改状态
Actions() 返回的 EventActions 让你可以在工具执行过程中修改Agent的状态:
func myTool(ctx tool.Context, input MyInput) (MyOutput, error) {
// 在Session状态中记录一些信息
actions := ctx.Actions()
actions.StateDelta["last_tool_call"] = "myTool"
actions.StateDelta["call_count"] = 42
// 你也可以通过 ctx.State() 来直接操作状态
ctx.State().Set("some_key", "some_value")
return MyOutput{Result: "done"}, nil
}
StateDelta会被合并到Session的状态中,在后续的对话中可以读取。
SearchMemory 搜索记忆
如果你的Agent配置了MemoryService,工具可以通过 SearchMemory 搜索历史对话中的
相关信息:
func myTool(ctx tool.Context, input MyInput) (MyOutput, error) {
// 搜索历史对话中关于"用户偏好"的记忆
result, err := ctx.SearchMemory(ctx, "用户的偏好设置")
if err != nil {
return MyOutput{}, err
}
// result 包含匹配到的历史对话片段
// 你可以用这些信息来个性化工具的行为
return MyOutput{Result: "基于你之前的偏好..."}, nil
}
ToolConfirmation 和 RequestConfirmation(人在回路)
有时候工具要执行一些敏感操作,比如删除文件、发送邮件、付款。这时候你可能希望 在执行之前先问一下用户"你确定吗?"。这就是"人在回路"(Human-in-the-Loop)机制。
有两种方式可以启用确认机制。
方式一:在Config中静态设置
最简单的方式,适用于工具每次调用都需要确认的场景:
dangerousTool, err := functiontool.New(functiontool.Config{
Name: "delete_file",
Description: "删除指定文件",
RequireConfirmation: true, // 每次调用都需要用户确认
}, deleteFile)
方式二:动态判断是否需要确认
有时候你只想在某些条件下请求确认:
dangerousTool, err := functiontool.New(functiontool.Config{
Name: "transfer_money",
Description: "转账",
// 只有金额超过10000时才需要确认
RequireConfirmationProvider: func(input TransferInput) bool {
return input.Amount > 10000
},
}, transferMoney)
方式三:在工具函数内部手动控制
这是最灵活的方式,你可以完全控制确认流程:
func requestVacation(ctx tool.Context, input VacationInput) (VacationOutput, error) {
// 先检查有没有用户确认
confirmation := ctx.ToolConfirmation()
if confirmation == nil {
// 还没有确认,主动请求确认
err := ctx.RequestConfirmation(
"你确定要请假吗?请批准或拒绝这个请假请求。",
map[string]any{"days": input.Days}, // 附加信息
)
if err != nil {
return VacationOutput{}, err
}
// 返回一个中间状态,等待用户确认
return VacationOutput{Status: "等待审批"}, nil
}
// 有确认了,检查结果
if confirmation.Confirmed {
// 用户批准了,执行操作
return VacationOutput{Status: "已批准", Days: input.Days}, nil
}
// 用户拒绝了
return VacationOutput{Status: "已拒绝"}, nil
}
这个流程大致是这样的:
1. LLM调用 requestVacation 工具
2. 工具发现还没确认 → 调用 ctx.RequestConfirmation()
3. ADK把确认请求发给前端/用户
4. 用户在UI上点击"批准"或"拒绝"
5. ADK再次调用 requestVacation 工具,这次 ctx.ToolConfirmation() 不为nil
6. 工具根据确认结果执行或拒绝操作
工具的生命周期
理解工具调用的完整生命周期,对于写出好用的工具非常重要。让我们从头到尾 走一遍:
Agent 循环
┌─────────────────┐
v |
用户输入 → [LLM分析] → 决定调用工具? ──┤
| | |
| 是 | 否 |
| v |
| [执行工具函数] |
| | |
| 工具返回结果 |
| | |
| [结果返回LLM] |
| | |
| LLM继续分析 ──────┘
|
v
LLM生成最终回答 → 返回给用户
更详细地说,一次完整的工具调用经历这些步骤:
1. 用户发送消息
用户:"帮我算 123 + 456"
2. ADK把消息和工具信息一起发给LLM
ADK会把你注册的所有工具的JSON Schema(包括名字、描述、参数定义)和用户消息 一起发送给LLM。
3. LLM分析并决定调用工具
LLM看到工具列表和用户消息,决定:我应该调用 calculator 工具,参数是
{"a": 123, "b": 456, "op": "add"}。
LLM返回的不是文字,而是一个"函数调用请求"(FunctionCall)。
4. ADK收到函数调用请求,执行你的Go函数
ADK解析LLM返回的函数调用请求,找到对应的工具,把JSON参数反序列化成你的输入 结构体,然后调用你的Go函数。
// ADK在内部大致做了这样的事情:
input := CalcInput{A: 123, B: 456, Op: "add"}
output, err := calculate(toolCtx, input)
// output = CalcOutput{Result: 579}
5. ADK把工具的返回值发回给LLM
工具的输出会被序列化成JSON,作为"函数调用响应"(FunctionResponse)发回给LLM。
6. LLM根据工具的结果生成最终回答
LLM:"123 + 456 的结果是 579。"
7. 如果LLM还需要调用其他工具,循环继续
LLM可能需要连续调用多个工具才能完成任务。比如用户说"搜一下天气然后转换温度", LLM会先调用搜索工具,再调用计算器工具。这个循环会一直持续,直到LLM认为可以 直接回答用户为止。
工具函数里发生panic怎么办?
不用担心,ADK做了保护。如果你的工具函数panic了,ADK会recover并把panic信息 包装成error返回给LLM,不会让整个程序崩溃。
// 这段代码来自ADK的functiontool源码
func (f *functionTool[TArgs, TResults]) Run(ctx tool.Context, args any) (result map[string]any, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic in tool %q: %v\nstack: %s", f.Name(), r, debug.Stack())
}
}()
// ...
}
但还是建议你在工具函数里做好错误处理,不要依赖panic recovery。
内置的工具包
除了 functiontool、geminitool 和 agenttool,ADK-Go还提供了一些内置工具,
帮你处理常见场景:
loadmemorytool - 加载记忆
让Agent主动搜索历史对话中的信息:
import "google.golang.org/adk/tool/loadmemorytool"
a, err := llmagent.New(llmagent.Config{
Name: "memory_agent",
Model: model,
Tools: []tool.Tool{
loadmemorytool.New(),
},
})
preloadmemorytool - 预加载记忆
每次对话开始时自动加载相关记忆:
import "google.golang.org/adk/tool/preloadmemorytool"
a, err := llmagent.New(llmagent.Config{
Name: "memory_agent",
Model: model,
Tools: []tool.Tool{
preloadmemorytool.New(),
loadmemorytool.New(),
},
})
loadartifactstool - 加载工件
让Agent可以访问保存的工件(如文件、图片等):
import "google.golang.org/adk/tool/loadartifactstool"
// 具体用法后续章节会介绍
exitlooptool - 退出循环
在循环型工作流中,让Agent可以主动退出循环:
import "google.golang.org/adk/tool/exitlooptool"
// 配合LoopAgent使用,后续章节会介绍
mcptoolset - MCP工具集
ADK-Go支持MCP(Model Context Protocol)标准,可以连接外部MCP服务器获取工具:
import "google.golang.org/adk/tool/mcptoolset"
// 连接MCP服务器获取工具,后续章节会详细介绍
工具设计的最佳实践
写到这里,让我分享一些写工具时的经验:
1. 描述要写清楚
// 差的描述 - LLM可能搞不清什么时候该用这个工具
functiontool.Config{
Name: "do_thing",
Description: "做一些事情",
}
// 好的描述 - LLM能精准判断
functiontool.Config{
Name: "search_product",
Description: "在商品数据库中搜索商品。支持按名称、分类、价格范围搜索。" +
"当用户想要查找、浏览或比较商品时使用此工具。",
}
2. 错误要优雅返回
func myTool(ctx tool.Context, input MyInput) (MyOutput, error) {
// 方式1:业务错误放在输出结构体里(推荐)
// LLM可以理解这个错误并告诉用户
if input.Value < 0 {
return MyOutput{Error: "数值不能为负数"}, nil
}
// 方式2:返回Go error(用于真正的系统错误)
// 比如数据库连接失败、网络超时等
result, err := callExternalAPI(input)
if err != nil {
return MyOutput{}, fmt.Errorf("调用外部API失败: %w", err)
}
return MyOutput{Result: result}, nil
}
区别在于:如果是"用户可以理解的错误"(比如参数不对),放在输出结构体里; 如果是"系统级的错误"(比如数据库挂了),返回Go error。
3. 工具粒度要合适
// 太细了 - 一个简单的任务要调用好几个工具
// add_numbers, subtract_numbers, multiply_numbers, divide_numbers
// 太粗了 - 一个工具做太多事,LLM搞不清该传什么参数
// do_everything
// 刚好 - 一个工具解决一类问题
// calculator (支持加减乘除)
4. description 标签别偷懒
// 偷懒版
type Input struct {
Q string `json:"q"`
}
// 用心版
type Input struct {
Query string `json:"query" description:"搜索关键词,支持自然语言查询"`
}
description 标签越清晰,LLM传参就越准确。
小结
这一章我们学了不少东西,来回顾一下关键点:
-
工具是Agent的手脚 - LLM负责思考,工具负责执行
-
FunctionTool 是最常用的工具类型 - 写一个Go函数,ADK自动包装成工具 - 函数签名:
func(tool.Context, InputStruct) (OutputStruct, error)- ADK从结构体自动生成JSON Schema -description标签很重要 -
Gemini内置工具 - 如
geminitool.GoogleSearch{},在Gemini服务端执行 -
AgentTool - 把Agent包装成工具,解决不同类型工具不能混用的问题
-
tool.Context 提供了丰富的上下文信息和操作能力: -
FunctionCallID()- 调用唯一ID -Actions()- 修改状态 -State()- 读写会话状态 -SearchMemory()- 搜索记忆 -ToolConfirmation()/RequestConfirmation()- 人在回路 -
工具生命周期 - 用户输入 -> LLM分析 -> 调用工具 -> 结果返回LLM -> 生成回答
动手练习
练习1:翻译工具
创建一个翻译工具,让Agent能把文本在中英文之间互译。
提示:
// 输入
type TranslateInput struct {
Text string `json:"text" description:"要翻译的文本"`
TargetLang string `json:"target_lang" description:"目标语言: zh(中文) 或 en(英文)"`
}
// 输出
type TranslateOutput struct {
TranslatedText string `json:"translated_text"`
SourceLang string `json:"source_lang"`
TargetLang string `json:"target_lang"`
}
// 思考:
// 1. 你可以用一个简单的map来做"翻译"(练习目的),也可以调用真实的翻译API
// 2. 别忘了处理不支持的语言
// 3. 给Agent写一个好的Instruction,告诉它什么时候该用翻译工具
练习2:文件读取工具
创建一个能从磁盘读取文件内容的工具。
提示:
type ReadFileInput struct {
Path string `json:"path" description:"文件路径"`
}
type ReadFileOutput struct {
Content string `json:"content"`
FileSize int64 `json:"file_size"`
Error string `json:"error,omitempty"`
}
// 思考:
// 1. 要考虑文件不存在的情况
// 2. 要考虑文件太大的情况(比如限制只读前10KB)
// 3. 安全性:你可能想限制只能读取某些目录下的文件
// 4. 错误信息要对LLM友好,让它能告诉用户发生了什么
练习3:组合GoogleSearch和自定义工具
用 agenttool 把GoogleSearch和一个自定义工具组合在一起。
提示:
// 1. 创建一个搜索Agent,使用 geminitool.GoogleSearch{}
// 2. 创建一个自定义工具,比如:
// - 一个单位换算工具(温度、长度、重量等)
// - 或者用上面写的翻译工具
// 3. 创建一个自定义工具Agent
// 4. 用 agenttool.New 把两个子Agent包装成工具
// 5. 创建根Agent,把两个Agent工具都给它
// 6. 试着问:"搜一下东京今天的气温,然后帮我换算成华氏度"
这三个练习做完,你对ADK-Go的工具系统就有很扎实的理解了。下一章我们会深入学习 Agent之间的协作 -- 多Agent系统。