第八期:工具系统 —— 让 Agent 拥有行动力
引言
上一期我们实现了 Agent 的核心循环,让 Agent 能够在”思考”和”行动”之间不断迭代。但你可能注意到了一个问题:我们的天气工具是手写的一个结构体,需要实现四个方法(Name(), Description(), InputSchema(), Handle()),而且 Handle 的输入输出都是 string —— 没有类型安全,没有自动校验,使用起来不够方便。
想象一下,如果你的 Agent 需要 20 个工具,每个工具都要手写一个结构体、四个方法,代码量会非常大。而且 InputSchema() 返回的 JSON Schema 要手写 map[string]any,既繁琐又容易出错。
这一期我们要打造一个强大而优雅的工具系统,让工具的创建变得像写普通函数一样简单。我们将实现:
-
类型安全的工具创建函数 tools.NewFunc—— 写一个普通 Go 函数就能变成工具 -
自动 JSON Schema 生成 —— 从 Go 结构体自动推导参数格式 -
工具中间件 —— 给工具添加日志、超时、重试等通用能力 -
Agent-as-Tool 模式 —— 把一个 Agent 包装成另一个 Agent 的工具
这是框架实用性的关键一环。让我们开始!
8.1 回顾 Tool 接口
先回顾第四期和第七期定义的 Tool 接口:
// Tool 代表 Agent 可以调用的外部能力type Tool interface { Name() string// 工具名称 Description() string// 工具描述(给模型看的) InputSchema() map[string]any // 输入参数的 JSON Schema Handle(ctx context.Context, input string) (string, error) // 执行工具}
四个方法各有其用:
-
Name():工具的唯一标识符,模型通过名称来调用工具 -
Description():自然语言描述,大模型根据这个描述来判断什么时候该用这个工具 -
InputSchema():JSON Schema 格式的参数定义,大模型根据它来生成正确的参数 -
Handle():真正的执行逻辑,接收 JSON 字符串参数,返回字符串结果
为什么 Handle 的输入输出都是 string? 因为大模型的世界是”文本世界”。模型生成的工具调用参数是 JSON 字符串,工具的返回值最终也要变成文本喂回给模型。string 是模型和工具之间的”通用语言”。
但这对开发者来说不太友好 —— 我们希望写工具时能用 Go 的结构体作为参数类型,享受编译时的类型检查。这就是我们这期要解决的核心问题。
8.2 Handler 接口与 HandleFunc 适配器
在实现高级工具之前,我们先定义一个更基础的处理器接口:
package toolsimport"context"// Handler 是工具的执行逻辑接口// 和 Tool 的 Handle 方法签名一致,但独立出来方便组合type Handler interface { Handle(ctx context.Context, input string) (string, error)}
然后是经典的 HandleFunc 适配器:
// HandleFunc 是一个适配器类型// 它让一个普通函数可以满足 Handler 接口type HandleFunc func(ctx context.Context, input string)(string, error)// Handle 实现了 Handler 接口func(f HandleFunc)Handle(ctx context.Context, input string)(string, error) {return f(ctx, input)}
这个模式你在第七期已经见过了 —— HandlerFunc 适配器。这里是同样的思路:把一个函数”升级”成接口的实现。
// 有了 HandleFunc,你可以直接用匿名函数创建一个 Handlervar h Handler = HandleFunc(func(ctx context.Context, input string)(string, error) {return"hello", nil})
在 Java 中,这类似于用 Lambda 表达式实现 @FunctionalInterface。Go 的方式更显式 —— 你需要用 HandleFunc() 做一次类型转换来告诉编译器”这个函数实现了 Handler 接口”。
8.3 JSONAdapter —— 类型安全的桥梁
这是工具系统中最巧妙的一个设计。JSONAdapter 是一个泛型函数,它把”接受 Go 结构体、返回 Go 结构体”的强类型函数,适配为”接受 string、返回 string”的 Handler 接口。
package toolsimport ("context""encoding/json""fmt")// JSONAdapter 将一个类型安全的函数适配为 Handler// I 是输入类型,O 是输出类型// 输入会自动从 JSON 字符串反序列化为 I 类型// 输出会自动从 O 类型序列化为 JSON 字符串funcJSONAdapter[Iany, Oany](fn func(ctx context.Context, input I)(O, error)) Handler {return HandleFunc(func(ctx context.Context, rawInput string)(string, error) {// 1. 将 JSON 字符串反序列化为 Go 结构体var input Iif err := json.Unmarshal([]byte(rawInput), &input); err != nil {return"", fmt.Errorf("invalid input JSON: %w (raw: %s)", err, rawInput) }// 2. 调用真正的业务函数 output, err := fn(ctx, input)if err != nil {return"", err }// 3. 将返回值序列化为 JSON 字符串 result, err := json.Marshal(output)if err != nil {return"", fmt.Errorf("failed to marshal output: %w", err) }returnstring(result), nil })}
**泛型语法 [I any, O any]**:这是 Go 1.18 引入的泛型。I 和 O 是类型参数,any 是类型约束(表示可以是任何类型)。如果你熟悉 Java 泛型,[I any, O any] 就类似于 Java 的 <I, O>。
让我们看看 JSONAdapter 的效果:
// 定义输入输出结构体type WeatherInput struct { City string`json:"city"`}type WeatherOutput struct { Temperature float64`json:"temperature"` Condition string`json:"condition"`}// 写一个普通的 Go 函数funcgetWeather(ctx context.Context, input WeatherInput)(WeatherOutput, error) {// 这里可以调用真正的天气 APIreturn WeatherOutput{ Temperature: 22.5, Condition: "晴", }, nil}// 用 JSONAdapter 适配为 Handlerhandler := JSONAdapter(getWeather)// 现在可以传 JSON 字符串了result, err := handler.Handle(ctx, `{"city": "北京"}`)// result = `{"temperature":22.5,"condition":"晴"}`
看到转变了吗?开发者写的是类型安全的 getWeather 函数,参数是 WeatherInput 结构体。但通过 JSONAdapter,它变成了一个能接受 JSON 字符串的 Handler。JSON 的序列化和反序列化完全自动完成!
为什么这个设计很重要? 在没有 JSONAdapter 的情况下,每个工具的 Handle 方法都要手写 JSON 解析代码:
// 没有 JSONAdapter 时的痛苦写法func(w *weatherTool)Handle(ctx context.Context, input string)(string, error) {// 每个工具都要写这段样板代码var params struct { City string`json:"city"` }if err := json.Unmarshal([]byte(input), ¶ms); err != nil {return"", err }// 真正的业务逻辑 result := queryWeather(params.City)// 又要手写序列化 output, _ := json.Marshal(result)returnstring(output), nil}
20 个工具就要写 20 遍几乎一样的 JSON 解析代码。JSONAdapter 把这些样板代码(boilerplate) 全部消除了。
8.4 自动 JSON Schema 生成
大模型需要知道工具的参数格式才能正确调用。这个格式用 JSON Schema 来描述。手写 JSON Schema 非常繁琐:
// 手写 JSON Schema —— 繁琐且容易出错func(w *weatherTool)InputSchema()map[string]any {returnmap[string]any{"type": "object","properties": map[string]any{"city": map[string]any{"type": "string","description": "城市名称", },"unit": map[string]any{"type": "string","description": "温度单位","enum": []string{"celsius", "fahrenheit"}, }, },"required": []string{"city"}, }}
我们的目标是从 Go 结构体自动生成 JSON Schema。利用 Go 的反射(reflect)机制,我们可以实现这个功能。
package toolsimport ("reflect""strings")// GenerateSchema 从 Go 类型自动生成 JSON Schema// 使用反射(reflect)分析结构体的字段和标签funcGenerateSchema[Tany]()map[string]any {var zero T t := reflect.TypeOf(zero)// 如果是指针类型,取底层类型if t.Kind() == reflect.Ptr { t = t.Elem() }return generateObjectSchema(t)}// generateObjectSchema 生成对象类型的 SchemafuncgenerateObjectSchema(t reflect.Type)map[string]any { properties := make(map[string]any)var required []stringfor i := 0; i < t.NumField(); i++ { field := t.Field(i)// 跳过未导出字段(小写字母开头的字段)if !field.IsExported() {continue }// 获取 JSON 标签名 jsonTag := field.Tag.Get("json") name := field.Nameif jsonTag != "" { parts := strings.Split(jsonTag, ",")if parts[0] != "" && parts[0] != "-" { name = parts[0] }// 如果标签包含 "-",跳过此字段if parts[0] == "-" {continue } }// 生成字段的 Schema fieldSchema := generateFieldSchema(field.Type)// 读取 description 标签if desc := field.Tag.Get("description"); desc != "" { fieldSchema["description"] = desc }// 读取 enum 标签if enum := field.Tag.Get("enum"); enum != "" { fieldSchema["enum"] = strings.Split(enum, ",") } properties[name] = fieldSchema// 判断是否必填:没有 omitempty 标签的字段视为必填if !strings.Contains(jsonTag, "omitempty") { required = append(required, name) } } schema := map[string]any{"type": "object","properties": properties, }iflen(required) > 0 { schema["required"] = required }return schema}// generateFieldSchema 根据 Go 类型生成对应的 JSON Schema 类型funcgenerateFieldSchema(t reflect.Type)map[string]any {// 处理指针类型if t.Kind() == reflect.Ptr { t = t.Elem() }switch t.Kind() {case reflect.String:returnmap[string]any{"type": "string"}case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:returnmap[string]any{"type": "integer"}case reflect.Float32, reflect.Float64:returnmap[string]any{"type": "number"}case reflect.Bool:returnmap[string]any{"type": "boolean"}case reflect.Slice, reflect.Array: items := generateFieldSchema(t.Elem())returnmap[string]any{"type": "array","items": items, }case reflect.Struct:return generateObjectSchema(t)case reflect.Map:if t.Key().Kind() == reflect.String {returnmap[string]any{"type": "object","additionalProperties": generateFieldSchema(t.Elem()), } }returnmap[string]any{"type": "object"}default:returnmap[string]any{"type": "string"} }}
reflect 反射机制简介:Go 的 reflect 包让程序能在运行时”检查”类型信息。reflect.TypeOf(x) 返回变量 x 的类型信息,然后你可以:
-
t.Kind()—— 获取类型种类(struct、string、int、slice 等) -
t.NumField()—— 获取结构体的字段数量 -
t.Field(i)—— 获取第 i 个字段的信息(名称、类型、标签等) -
field.Tag.Get("json")—— 获取字段的json标签值
如果你用过 Java 的反射(Class.getDeclaredFields()、Field.getAnnotation()),Go 的反射和它非常类似,只是 API 风格不同。
让我们看看自动生成的效果:
type WeatherInput struct { City string`json:"city" description:"城市名称,如北京、上海"` Unit string`json:"unit,omitempty" description:"温度单位" enum:"celsius,fahrenheit"`}schema := GenerateSchema[WeatherInput]()// 生成结果:// {// "type": "object",// "properties": {// "city": {"type": "string", "description": "城市名称,如北京、上海"},// "unit": {"type": "string", "description": "温度单位", "enum": ["celsius", "fahrenheit"]}// },// "required": ["city"]// }
注意 unit 字段有 omitempty 标签,所以它不在 required 列表中。city 没有 omitempty,所以是必填字段。description 和 enum 是我们自定义的标签,用来提供额外的 Schema 信息。
8.5 tools.NewFunc —— 类型安全的工具创建
有了 JSONAdapter 和 GenerateSchema,我们可以创建最核心的工具创建函数 NewFunc:
package toolsimport"context"// funcTool 是通过 NewFunc 创建的工具的内部实现type funcTool[I any, O any] struct { name string description string handler Handler schema map[string]any}func(t *funcTool[I, O])Name()string { return t.name }func(t *funcTool[I, O])Description()string { return t.description }func(t *funcTool[I, O])InputSchema()map[string]any { return t.schema }func(t *funcTool[I, O])Handle(ctx context.Context, input string)(string, error) {return t.handler.Handle(ctx, input)}// NewFunc 创建一个类型安全的工具// I 是输入类型(必须是可 JSON 反序列化的结构体)// O 是输出类型(必须是可 JSON 序列化的类型)funcNewFunc[Iany, Oany]( name string, description string, fn func(ctx context.Context, input I)(O, error),) Tool {return &funcTool[I, O]{ name: name, description: description, handler: JSONAdapter(fn), // 自动 JSON 适配 schema: GenerateSchema[I](), // 自动 Schema 生成 }}
这就是工具系统的杀手级 API! 让我们看看用它创建工具有多简单:
// 定义输入结构体type WeatherInput struct { City string`json:"city" description:"城市名称"`}// 定义输出结构体type WeatherOutput struct { Temp float64`json:"temperature"` Condition string`json:"condition"` Humidity int`json:"humidity"`}// 一行代码创建工具!weatherTool := tools.NewFunc[WeatherInput, WeatherOutput]("get_weather","获取指定城市的实时天气信息",func(ctx context.Context, input WeatherInput)(WeatherOutput, error) {// 这里写你的业务逻辑// input 已经是 WeatherInput 类型,有完整的类型检查return WeatherOutput{ Temp: 22.5, Condition: "晴", Humidity: 45, }, nil },)// weatherTool 自动具备:// - Name() → "get_weather"// - Description() → "获取指定城市的实时天气信息"// - InputSchema() → 从 WeatherInput 自动生成的 JSON Schema// - Handle() → 自动 JSON 序列化/反序列化
对比手写方式:
// 手写方式:需要一个结构体 + 四个方法 + 手写 Schema + 手写 JSON 解析// 大约 40-50 行代码// NewFunc 方式:只需要定义输入输出结构体 + 一个函数// 大约 15-20 行代码,而且类型安全!
代码量减少了一半以上,还消除了手写 JSON Schema 和 JSON 解析的出错风险。
8.6 tools.NewTool —— 手动创建工具
有时候你需要更精细的控制(比如自定义 JSON Schema,或者输入不需要 JSON 解析),这时可以用 NewTool:
// manualTool 是通过 NewTool 创建的工具type manualTool struct { name string description string schema map[string]any handler HandleFunc}func(t *manualTool)Name()string { return t.name }func(t *manualTool)Description()string { return t.description }func(t *manualTool)InputSchema()map[string]any { return t.schema }func(t *manualTool)Handle(ctx context.Context, input string)(string, error) {return t.handler(ctx, input)}// NewTool 手动创建一个工具,完全控制所有参数funcNewTool( name string, description string, schema map[string]any, handler func(ctx context.Context, input string)(string, error),) Tool {return &manualTool{ name: name, description: description, schema: schema, handler: HandleFunc(handler), }}
使用示例:
// 手动创建一个计算器工具calculatorTool := tools.NewTool("calculator","执行数学计算",map[string]any{"type": "object","properties": map[string]any{"expression": map[string]any{"type": "string","description": "数学表达式,如 2+3*4", }, },"required": []string{"expression"}, },func(ctx context.Context, input string)(string, error) {// input 是原始 JSON 字符串,你自己解析// 适合不需要复杂参数结构的简单工具return"42", nil },)
何时用 NewFunc,何时用 NewTool?
|
|
|
|---|---|
|
|
NewFunc
|
|
|
NewTool
|
|
|
NewTool
|
|
|
NewFunc
|
8.7 工具中间件
就像 Agent 有中间件一样,Tool 也可以有中间件。工具中间件可以在工具执行前后添加通用逻辑,比如日志记录、超时控制、结果缓存等。
package toolsimport ("context""fmt""log/slog""time")// ToolMiddleware 是工具中间件类型// 接受一个 Handler,返回包装后的 Handlertype ToolMiddleware func(Handler)Handler// WithLogging 创建一个日志中间件// 记录工具的调用参数和耗时funcWithLogging()ToolMiddleware {returnfunc(next Handler)Handler {return HandleFunc(func(ctx context.Context, input string)(string, error) { start := time.Now() slog.Info("tool call start", "input", input) output, err := next.Handle(ctx, input) duration := time.Since(start)if err != nil { slog.Error("tool call failed","error", err,"duration", duration, ) } else { slog.Info("tool call success","output_length", len(output),"duration", duration, ) }return output, err }) }}// WithTimeout 创建一个超时中间件// 防止工具执行时间过长funcWithTimeout(d time.Duration)ToolMiddleware {returnfunc(next Handler)Handler {return HandleFunc(func(ctx context.Context, input string)(string, error) {// 创建带超时的 context ctx, cancel := context.WithTimeout(ctx, d)defer cancel()// 用 channel + goroutine 实现超时控制type result struct { output string err error } ch := make(chan result, 1)gofunc() { output, err := next.Handle(ctx, input) ch <- result{output, err} }()select {case r := <-ch:return r.output, r.errcase <-ctx.Done():return"", fmt.Errorf("tool execution timed out after %v", d) } }) }}// WithRetry 创建一个重试中间件funcWithRetry(maxRetries int)ToolMiddleware {returnfunc(next Handler)Handler {return HandleFunc(func(ctx context.Context, input string)(string, error) {var lastErr errorfor i := 0; i <= maxRetries; i++ { output, err := next.Handle(ctx, input)if err == nil {return output, nil } lastErr = err slog.Warn("tool call failed, retrying","attempt", i+1,"max_retries", maxRetries,"error", err, ) }return"", fmt.Errorf("tool failed after %d retries: %w", maxRetries, lastErr) }) }}
怎么给工具应用中间件? 我们可以创建一个包装函数:
// WrapTool 给一个已有的 Tool 应用中间件funcWrapTool(tool Tool, middlewares ...ToolMiddleware)Tool {// 从最后一个中间件开始包装(洋葱模型)var handler Handler = HandleFunc(tool.Handle)for i := len(middlewares) - 1; i >= 0; i-- { handler = middlewares[i](handler) }return &wrappedTool{ name: tool.Name(), description: tool.Description(), schema: tool.InputSchema(), handler: handler, }}type wrappedTool struct { name string description string schema map[string]any handler Handler}func(t *wrappedTool)Name()string { return t.name }func(t *wrappedTool)Description()string { return t.description }func(t *wrappedTool)InputSchema()map[string]any { return t.schema }func(t *wrappedTool)Handle(ctx context.Context, input string)(string, error) {return t.handler.Handle(ctx, input)}
使用示例:
// 创建一个带日志和超时的天气工具weatherTool := tools.WrapTool( tools.NewFunc[WeatherInput, WeatherOutput]("get_weather", "获取天气", getWeather, ), tools.WithLogging(), tools.WithTimeout(5 * time.Second), tools.WithRetry(2),)// 执行顺序:日志记录 → 超时控制 → 重试 → 真正执行
8.8 ToolResolver —— 动态工具解析
有些场景下,工具列表不是固定的,而是需要根据上下文动态确定。比如:
-
根据用户的权限决定提供哪些工具 -
根据对话主题动态加载相关工具 -
从外部服务(如 MCP 服务器)动态获取工具
这就是 ToolResolver 接口的用途:
package tinyagentimport"context"// ToolResolver 动态工具解析器接口type ToolResolver interface { ResolveTools(ctx context.Context, inv *Invocation) ([]Tool, error)}// ToolResolverFunc 适配器type ToolResolverFunc func(ctx context.Context, inv *Invocation)([]Tool, error)func(f ToolResolverFunc)ResolveTools(ctx context.Context, inv *Invocation)([]Tool, error) {return f(ctx, inv)}
使用示例:
// 根据用户角色动态提供工具roleBasedResolver := ToolResolverFunc(func(ctx context.Context, inv *Invocation)([]Tool, error) {var tools []Tool// 所有用户都有搜索工具 tools = append(tools, searchTool)// 管理员额外有数据库工具if role, ok := inv.Session.State()["role"]; ok && role == "admin" { tools = append(tools, dbQueryTool, dbWriteTool) }return tools, nil})adminAgent := NewAgent("admin-assistant", WithModel(openaiProvider), WithToolResolver(roleBasedResolver),)
在第七期中,我们已经在 prepareInvocation 方法中集成了 ToolResolver 的调用逻辑。动态解析出的工具会和静态配置的工具合并在一起。
8.9 NewAgentTool —— 把 Agent 变成工具
这是工具系统中最强大的模式之一:Agent-as-Tool(Agent 作为工具)。
想象一下:你有一个”主管 Agent”负责和用户对话,它手下有几个”专家 Agent” —— 天气专家、翻译专家、编程专家。主管 Agent 不需要知道这些专家的内部细节,只需要像调用工具一样调用它们。
package toolsimport ("context""strings""github.com/yourname/tinyagent")// NewAgentTool 将一个 Agent 包装为 Tool// 这样一个 Agent 就可以作为另一个 Agent 的工具来使用funcNewAgentTool(a tinyagent.Agent)tinyagent.Tool {return &agentTool{agent: a}}type agentTool struct { agent tinyagent.Agent}func(t *agentTool)Name()string {return t.agent.Name()}func(t *agentTool)Description()string {return t.agent.Description()}func(t *agentTool)InputSchema()map[string]any {// Agent-as-Tool 的输入就是一条自然语言消息returnmap[string]any{"type": "object","properties": map[string]any{"message": map[string]any{"type": "string","description": "要发送给此 Agent 的消息", }, },"required": []string{"message"}, }}func(t *agentTool)Handle(ctx context.Context, input string)(string, error) {// 解析输入var req struct { Message string`json:"message"` }if err := json.Unmarshal([]byte(input), &req); err != nil {// 如果不是 JSON,直接当作纯文本消息 req.Message = input }// 创建 Invocation,调用子 Agent inv := &tinyagent.Invocation{ ID: uuid.New().String(), Message: &tinyagent.Message{ ID: uuid.New().String(), Role: tinyagent.RoleUser, Parts: []tinyagent.Part{ &tinyagent.TextPart{Text: req.Message}, }, }, }// 运行子 Agent,收集所有输出 gen := t.agent.Run(ctx, inv)var result strings.Builderfor msg, err := range gen {if err != nil {return"", err }// 只收集助手回复的文本if msg.Role == tinyagent.RoleAssistant { text := msg.Text()if text != "" { result.WriteString(text) } } }return result.String(), nil}
使用示例 —— 多 Agent 协作:
// 创建专家 AgentweatherExpert := NewAgent("weather-expert", WithModel(openaiProvider), WithInstruction("你是一个天气专家,专门回答天气相关的问题。"), WithTools(weatherAPITool, geocodingTool),)translationExpert := NewAgent("translation-expert", WithModel(openaiProvider), WithInstruction("你是一个翻译专家,负责中英文互译。"),)// 创建主管 Agent,把专家 Agent 作为工具supervisor := NewAgent("supervisor", WithModel(openaiProvider), WithInstruction("你是一个全能助手。根据用户的问题,选择合适的专家来回答。"), WithTools( tools.NewAgentTool(weatherExpert), // 天气专家作为工具 tools.NewAgentTool(translationExpert), // 翻译专家作为工具 ),)// 用户问:"北京天气怎么样?"// 1. supervisor 收到问题// 2. supervisor 决定调用 weather-expert 工具// 3. weather-expert 执行自己的 ReAct 循环(可能调用天气 API 工具)// 4. weather-expert 返回结果给 supervisor// 5. supervisor 整理结果给用户
这就是 “一切皆 Agent” 设计原则的威力:Agent 可以被当作 Tool,Tool 可以被 Agent 使用,Agent 可以嵌套组合,形成任意复杂的协作网络。
8.10 完整可运行示例
让我们把本期所有内容整合成一个完整的、可运行的示例。
项目结构
tinyagent-tools-demo/├── go.mod├── tinyagent/│ ├── types.go // 核心类型(复用第七期)│ ├── agent.go // Agent 实现(复用第七期)│ └── tools/│ ├── tools.go // 工具创建函数│ └── schema.go // JSON Schema 生成└── main.go // 演示程序
tools/tools.go —— 完整的工具包
package toolsimport ("context""encoding/json""fmt""log/slog""strings""time""github.com/google/uuid")// ========================// 核心接口(从 tinyagent 包引用)// ========================// Tool 接口定义(为了演示独立性,这里重新定义)type Tool interface { Name() string Description() string InputSchema() map[string]any Handle(ctx context.Context, input string) (string, error)}// Handler 工具执行逻辑接口type Handler interface { Handle(ctx context.Context, input string) (string, error)}// HandleFunc 适配器type HandleFunc func(ctx context.Context, input string)(string, error)func(f HandleFunc)Handle(ctx context.Context, input string)(string, error) {return f(ctx, input)}// ========================// JSONAdapter// ========================// JSONAdapter 将类型安全的函数适配为 HandlerfuncJSONAdapter[Iany, Oany](fn func(ctx context.Context, input I)(O, error)) Handler {return HandleFunc(func(ctx context.Context, rawInput string)(string, error) {var input Iif err := json.Unmarshal([]byte(rawInput), &input); err != nil {return"", fmt.Errorf("invalid input JSON: %w", err) } output, err := fn(ctx, input)if err != nil {return"", err } result, err := json.Marshal(output)if err != nil {return"", fmt.Errorf("marshal output: %w", err) }returnstring(result), nil })}// ========================// NewFunc - 类型安全的工具创建// ========================type funcTool[I any, O any] struct { name string description string handler Handler schema map[string]any}func(t *funcTool[I, O])Name()string { return t.name }func(t *funcTool[I, O])Description()string { return t.description }func(t *funcTool[I, O])InputSchema()map[string]any { return t.schema }func(t *funcTool[I, O])Handle(ctx context.Context, input string)(string, error) {return t.handler.Handle(ctx, input)}// NewFunc 创建类型安全的工具funcNewFunc[Iany, Oany]( name string, description string, fn func(ctx context.Context, input I)(O, error),) Tool {return &funcTool[I, O]{ name: name, description: description, handler: JSONAdapter(fn), schema: GenerateSchema[I](), }}// ========================// NewTool - 手动创建工具// ========================type manualTool struct { name string description string schema map[string]any handler HandleFunc}func(t *manualTool)Name()string { return t.name }func(t *manualTool)Description()string { return t.description }func(t *manualTool)InputSchema()map[string]any { return t.schema }func(t *manualTool)Handle(ctx context.Context, input string)(string, error) {return t.handler(ctx, input)}// NewTool 手动创建工具funcNewTool( name string, description string, schema map[string]any, handler func(ctx context.Context, input string)(string, error),) Tool {return &manualTool{ name: name, description: description, schema: schema, handler: HandleFunc(handler), }}// ========================// 工具中间件// ========================// ToolMiddleware 工具中间件类型type ToolMiddleware func(Handler)Handler// WithLogging 日志中间件funcWithLogging(toolName string)ToolMiddleware {returnfunc(next Handler)Handler {return HandleFunc(func(ctx context.Context, input string)(string, error) { start := time.Now() slog.Info("tool call start", "tool", toolName, "input", input) output, err := next.Handle(ctx, input) duration := time.Since(start)if err != nil { slog.Error("tool call failed", "tool", toolName, "error", err, "duration", duration) } else { slog.Info("tool call success", "tool", toolName, "duration", duration) }return output, err }) }}// WithTimeout 超时中间件funcWithTimeout(d time.Duration)ToolMiddleware {returnfunc(next Handler)Handler {return HandleFunc(func(ctx context.Context, input string)(string, error) { ctx, cancel := context.WithTimeout(ctx, d)defer cancel()type result struct { output string err error } ch := make(chan result, 1)gofunc() { out, err := next.Handle(ctx, input) ch <- result{out, err} }()select {case r := <-ch:return r.output, r.errcase <-ctx.Done():return"", fmt.Errorf("tool timed out after %v", d) } }) }}// WrapTool 给工具应用中间件funcWrapTool(tool Tool, middlewares ...ToolMiddleware)Tool {var handler Handler = HandleFunc(tool.Handle)for i := len(middlewares) - 1; i >= 0; i-- { handler = middlewares[i](handler) }return &wrappedTool{ name: tool.Name(), description: tool.Description(), schema: tool.InputSchema(), handler: handler, }}type wrappedTool struct { name string description string schema map[string]any handler Handler}func(t *wrappedTool)Name()string { return t.name }func(t *wrappedTool)Description()string { return t.description }func(t *wrappedTool)InputSchema()map[string]any { return t.schema }func(t *wrappedTool)Handle(ctx context.Context, input string)(string, error) {return t.handler.Handle(ctx, input)}// ========================// NewAgentTool - Agent 作为 Tool// ========================// AgentLike 是一个简化的 Agent 接口,用于 Agent-as-Tool// 在实际框架中直接用 tinyagent.Agent 接口type AgentLike interface { Name() string Description() string Run(ctx context.Context, message string) (string, error)}// agentTool 将 Agent 包装为 Tooltype agentTool struct { name string description string runFunc func(ctx context.Context, message string)(string, error)}func(t *agentTool)Name()string { return t.name }func(t *agentTool)Description()string { return t.description }func(t *agentTool)InputSchema()map[string]any {returnmap[string]any{"type": "object","properties": map[string]any{"message": map[string]any{"type": "string","description": "要发送给此 Agent 的消息", }, },"required": []string{"message"}, }}func(t *agentTool)Handle(ctx context.Context, input string)(string, error) {var req struct { Message string`json:"message"` }if err := json.Unmarshal([]byte(input), &req); err != nil { req.Message = input }return t.runFunc(ctx, req.Message)}// NewAgentTool 创建一个 Agent-as-Tool// runFunc 封装了调用 Agent 并收集结果的逻辑funcNewAgentTool( name string, description string, runFunc func(ctx context.Context, message string)(string, error),) Tool {return &agentTool{ name: name, description: description, runFunc: runFunc, }}// ========================// 辅助// ========================funcNewID()string {return uuid.New().String()}// Ignore is used to suppress "imported and not used" errorsvar _ = strings.Contains
tools/schema.go —— Schema 生成
package toolsimport ("reflect""strings")// GenerateSchema 从 Go 类型自动生成 JSON SchemafuncGenerateSchema[Tany]()map[string]any {var zero T t := reflect.TypeOf(zero)if t.Kind() == reflect.Ptr { t = t.Elem() }return generateObjectSchema(t)}funcgenerateObjectSchema(t reflect.Type)map[string]any { properties := make(map[string]any)var required []stringfor i := 0; i < t.NumField(); i++ { field := t.Field(i)if !field.IsExported() {continue } jsonTag := field.Tag.Get("json") name := field.Nameif jsonTag != "" { parts := strings.Split(jsonTag, ",")if parts[0] == "-" {continue }if parts[0] != "" { name = parts[0] } } fieldSchema := generateFieldSchema(field.Type)if desc := field.Tag.Get("description"); desc != "" { fieldSchema["description"] = desc }if enum := field.Tag.Get("enum"); enum != "" { fieldSchema["enum"] = strings.Split(enum, ",") } properties[name] = fieldSchemaif !strings.Contains(jsonTag, "omitempty") { required = append(required, name) } } schema := map[string]any{"type": "object","properties": properties, }iflen(required) > 0 { schema["required"] = required }return schema}funcgenerateFieldSchema(t reflect.Type)map[string]any {if t.Kind() == reflect.Ptr { t = t.Elem() }switch t.Kind() {case reflect.String:returnmap[string]any{"type": "string"}case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:returnmap[string]any{"type": "integer"}case reflect.Float32, reflect.Float64:returnmap[string]any{"type": "number"}case reflect.Bool:returnmap[string]any{"type": "boolean"}case reflect.Slice, reflect.Array:returnmap[string]any{"type": "array", "items": generateFieldSchema(t.Elem())}case reflect.Struct:return generateObjectSchema(t)case reflect.Map:if t.Key().Kind() == reflect.String {returnmap[string]any{"type": "object", "additionalProperties": generateFieldSchema(t.Elem())} }returnmap[string]any{"type": "object"}default:returnmap[string]any{"type": "string"} }}
main.go —— 完整演示程序
package mainimport ("context""encoding/json""fmt""math""time""tinyagent-tools-demo/tools")// ==========================================// 示例 1:用 NewFunc 创建天气工具// ==========================================type WeatherInput struct { City string`json:"city" description:"城市名称,如北京、上海、广州"` Unit string`json:"unit,omitempty" description:"温度单位" enum:"celsius,fahrenheit"`}type WeatherOutput struct { City string`json:"city"` Temperature float64`json:"temperature"` Condition string`json:"condition"` Humidity int`json:"humidity"`}funcgetWeather(ctx context.Context, input WeatherInput)(WeatherOutput, error) {// 模拟天气查询 weatherData := map[string]WeatherOutput{"北京": {City: "北京", Temperature: 22.5, Condition: "晴", Humidity: 45},"上海": {City: "上海", Temperature: 25.0, Condition: "多云", Humidity: 72},"广州": {City: "广州", Temperature: 30.2, Condition: "阵雨", Humidity: 85}, } data, ok := weatherData[input.City]if !ok {return WeatherOutput{}, fmt.Errorf("未找到城市 %q 的天气数据", input.City) }// 如果要求华氏度,做转换if input.Unit == "fahrenheit" { data.Temperature = data.Temperature*9/5 + 32 }return data, nil}// ==========================================// 示例 2:用 NewFunc 创建计算器工具// ==========================================type CalcInput struct { Operation string`json:"operation" description:"运算类型" enum:"add,subtract,multiply,divide,sqrt,power"` A float64`json:"a" description:"第一个操作数"` B float64`json:"b,omitempty" description:"第二个操作数(sqrt 操作不需要)"`}type CalcOutput struct { Result float64`json:"result"` Expr string`json:"expression"`}funccalculate(ctx context.Context, input CalcInput)(CalcOutput, error) {var result float64var expr stringswitch input.Operation {case"add": result = input.A + input.B expr = fmt.Sprintf("%.2f + %.2f = %.2f", input.A, input.B, result)case"subtract": result = input.A - input.B expr = fmt.Sprintf("%.2f - %.2f = %.2f", input.A, input.B, result)case"multiply": result = input.A * input.B expr = fmt.Sprintf("%.2f * %.2f = %.2f", input.A, input.B, result)case"divide":if input.B == 0 {return CalcOutput{}, fmt.Errorf("除数不能为零") } result = input.A / input.B expr = fmt.Sprintf("%.2f / %.2f = %.2f", input.A, input.B, result)case"sqrt":if input.A < 0 {return CalcOutput{}, fmt.Errorf("不能对负数求平方根") } result = math.Sqrt(input.A) expr = fmt.Sprintf("sqrt(%.2f) = %.2f", input.A, result)case"power": result = math.Pow(input.A, input.B) expr = fmt.Sprintf("%.2f ^ %.2f = %.2f", input.A, input.B, result)default:return CalcOutput{}, fmt.Errorf("不支持的运算: %s", input.Operation) }return CalcOutput{Result: result, Expr: expr}, nil}// ==========================================// 示例 3:用 NewTool 手动创建工具// ==========================================funccreateTimeTool()tools.Tool {return tools.NewTool("get_current_time","获取当前时间",map[string]any{"type": "object","properties": map[string]any{"timezone": map[string]any{"type": "string","description": "时区,如 Asia/Shanghai、America/New_York", }, },"required": []string{"timezone"}, },func(ctx context.Context, input string)(string, error) {var req struct { Timezone string`json:"timezone"` }if err := json.Unmarshal([]byte(input), &req); err != nil {return"", err } loc, err := time.LoadLocation(req.Timezone)if err != nil {return"", fmt.Errorf("无效的时区: %s", req.Timezone) } now := time.Now().In(loc)return fmt.Sprintf("当前时间(%s):%s", req.Timezone, now.Format("2006-01-02 15:04:05")), nil }, )}funcmain() { ctx := context.Background() fmt.Println("========================================") fmt.Println("TinyAgent 工具系统演示") fmt.Println("========================================")// ---- 演示 1: NewFunc 创建天气工具 ---- fmt.Println("\n--- 1. NewFunc 创建天气工具 ---") weatherTool := tools.NewFunc[WeatherInput, WeatherOutput]("get_weather","获取指定城市的实时天气信息", getWeather, ) fmt.Printf("工具名称: %s\n", weatherTool.Name()) fmt.Printf("工具描述: %s\n", weatherTool.Description())// 打印自动生成的 JSON Schema schemaJSON, _ := json.MarshalIndent(weatherTool.InputSchema(), "", " ") fmt.Printf("输入 Schema (自动生成):\n%s\n", schemaJSON)// 调用工具 result, err := weatherTool.Handle(ctx, `{"city": "北京"}`)if err != nil { fmt.Printf("错误: %v\n", err) } else { fmt.Printf("调用结果: %s\n", result) }// ---- 演示 2: NewFunc 创建计算器工具 ---- fmt.Println("\n--- 2. NewFunc 创建计算器工具 ---") calcTool := tools.NewFunc[CalcInput, CalcOutput]("calculator","执行数学运算", calculate, ) calcSchema, _ := json.MarshalIndent(calcTool.InputSchema(), "", " ") fmt.Printf("计算器 Schema:\n%s\n", calcSchema)// 测试各种运算 testCases := []string{`{"operation": "add", "a": 10, "b": 3}`,`{"operation": "multiply", "a": 7, "b": 8}`,`{"operation": "sqrt", "a": 144}`,`{"operation": "power", "a": 2, "b": 10}`, }for _, tc := range testCases { result, err := calcTool.Handle(ctx, tc)if err != nil { fmt.Printf(" 输入: %s → 错误: %v\n", tc, err) } else {var out CalcOutput json.Unmarshal([]byte(result), &out) fmt.Printf(" %s\n", out.Expr) } }// ---- 演示 3: NewTool 手动创建工具 ---- fmt.Println("\n--- 3. NewTool 手动创建时间工具 ---") timeTool := createTimeTool() timeResult, err := timeTool.Handle(ctx, `{"timezone": "Asia/Shanghai"}`)if err != nil { fmt.Printf("错误: %v\n", err) } else { fmt.Printf("结果: %s\n", timeResult) }// ---- 演示 4: 工具中间件 ---- fmt.Println("\n--- 4. 工具中间件演示 ---") wrappedWeather := tools.WrapTool( weatherTool, tools.WithLogging("get_weather"), tools.WithTimeout(3*time.Second), ) fmt.Println("调用带中间件的天气工具:") result, err = wrappedWeather.Handle(ctx, `{"city": "上海"}`)if err != nil { fmt.Printf("错误: %v\n", err) } else { fmt.Printf("结果: %s\n", result) }// ---- 演示 5: Agent-as-Tool ---- fmt.Println("\n--- 5. Agent-as-Tool 演示 ---")// 模拟一个"翻译专家 Agent"作为工具 translatorTool := tools.NewAgentTool("translator","一个翻译专家,能进行中英文互译",func(ctx context.Context, message string)(string, error) {// 在真实场景中,这里会调用 agent.Run()// 这里为了演示,直接返回模拟结果return fmt.Sprintf("[翻译结果] %s → Hello, World!", message), nil }, ) fmt.Printf("Agent-as-Tool 名称: %s\n", translatorTool.Name()) fmt.Printf("Agent-as-Tool 描述: %s\n", translatorTool.Description()) agentToolSchema, _ := json.MarshalIndent(translatorTool.InputSchema(), "", " ") fmt.Printf("Agent-as-Tool Schema:\n%s\n", agentToolSchema) translateResult, _ := translatorTool.Handle(ctx, `{"message": "你好世界"}`) fmt.Printf("调用结果: %s\n", translateResult)// ---- 演示 6: 展示工具如何集成到 Agent ---- fmt.Println("\n--- 6. 工具与 Agent 集成示例 ---") fmt.Println("在真实的 Agent 中,你会这样使用工具:") fmt.Println() fmt.Println(` myAgent := tinyagent.NewAgent("smart-assistant",`) fmt.Println(` tinyagent.WithModel(openaiProvider),`) fmt.Println(` tinyagent.WithInstruction("你是一个全能助手。"),`) fmt.Println(` tinyagent.WithTools(`) fmt.Println(` weatherTool, // 天气工具 (NewFunc 创建)`) fmt.Println(` calcTool, // 计算器工具 (NewFunc 创建)`) fmt.Println(` timeTool, // 时间工具 (NewTool 创建)`) fmt.Println(` translatorTool, // 翻译 Agent 作为工具`) fmt.Println(` ),`) fmt.Println(` )`) fmt.Println() fmt.Println("Agent 核心循环会:") fmt.Println(" 1. 将工具的 Name、Description、InputSchema 发送给大模型") fmt.Println(" 2. 大模型根据用户问题决定调用哪个工具") fmt.Println(" 3. 大模型生成符合 InputSchema 的 JSON 参数") fmt.Println(" 4. Agent 调用工具的 Handle 方法执行") fmt.Println(" 5. 工具结果返回给大模型继续思考") fmt.Println("\n========================================") fmt.Println("演示完毕!") fmt.Println("========================================")}
运行输出:
========================================TinyAgent 工具系统演示========================================--- 1. NewFunc 创建天气工具 ---工具名称: get_weather工具描述: 获取指定城市的实时天气信息输入 Schema (自动生成):{"type": "object","properties": {"city": {"type": "string","description": "城市名称,如北京、上海、广州" },"unit": {"type": "string","description": "温度单位","enum": ["celsius", "fahrenheit"] } },"required": ["city"]}调用结果: {"city":"北京","temperature":22.5,"condition":"晴","humidity":45}--- 2. NewFunc 创建计算器工具 ---计算器 Schema:{"type": "object","properties": {"operation": {"type": "string","description": "运算类型","enum": ["add", "subtract", "multiply", "divide", "sqrt", "power"] },"a": {"type": "number","description": "第一个操作数" },"b": {"type": "number","description": "第二个操作数(sqrt 操作不需要)" } },"required": ["operation", "a"]} 10.00 + 3.00 = 13.00 7.00 * 8.00 = 56.00 sqrt(144.00) = 12.00 2.00 ^ 10.00 = 1024.00--- 3. NewTool 手动创建时间工具 ---结果: 当前时间(Asia/Shanghai):2025-01-15 14:30:00--- 4. 工具中间件演示 ---调用带中间件的天气工具:结果: {"city":"上海","temperature":25,"condition":"多云","humidity":72}--- 5. Agent-as-Tool 演示 ---Agent-as-Tool 名称: translatorAgent-as-Tool 描述: 一个翻译专家,能进行中英文互译Agent-as-Tool Schema:{"type": "object","properties": {"message": {"type": "string","description": "要发送给此 Agent 的消息" } },"required": ["message"]}调用结果: [翻译结果] 你好世界 → Hello, World!--- 6. 工具与 Agent 集成示例 ---(展示使用说明)========================================演示完毕!========================================
8.11 工具系统与 Agent 核心循环的集成
让我们回顾工具系统如何与第七期实现的 Agent 核心循环配合工作。整个流程可以用下图总结:
┌─────────────────────────────────────────────────────────────────────┐│ Agent 核心循环 ││ ││ ┌───────────────┐ ││ │ 用户消息 │ ││ └───────┬───────┘ ││ │ ││ ▼ ││ ┌───────────────┐ ┌──────────────────────────────────────────┐ ││ │ Model.Generate │────>│ 大模型接收: │ ││ │ │ │ - 对话历史 │ ││ │ │ │ - 系统指令 │ ││ │ │ │ - 工具列表(Name + Description + Schema) │ ││ └───────────────┘ └─────────────────┬────────────────────────┘ ││ │ ││ 大模型输出 ToolPart: ││ name="get_weather" ││ request={"city":"北京"} ││ │ ││ ▼ ││ ┌─────────────────────────────┐ ││ │ executeTools() │ ││ │ │ ││ │ findTool("get_weather") │ ││ │ │ │ ││ │ ▼ │ ││ │ ┌──────────────────┐ │ ││ │ │ tool.Handle(ctx, │ │ ││ │ │ '{"city":"北京"}')│ │ ││ │ └────────┬─────────┘ │ ││ │ │ │ ││ │ [工具中间件链] │ ││ │ 日志 → 超时 → 重试 │ ││ │ │ │ ││ │ [JSONAdapter] │ ││ │ JSON → Go struct │ ││ │ │ │ ││ │ [业务函数 getWeather()] │ ││ │ Go struct → Go struct │ ││ │ │ │ ││ │ [JSONAdapter] │ ││ │ Go struct → JSON │ ││ │ │ │ ││ └───────────┼─────────────────┘ ││ │ ││ ▼ ││ 工具结果追加到 History ││ 回到 Model.Generate 继续循环 ││ │└─────────────────────────────────────────────────────────────────────┘
数据流是这样的:
-
模型看到工具定义: Name()、Description()、InputSchema()告诉模型有什么工具可用以及怎么调用 -
模型生成调用请求:模型输出 ToolPart,包含工具名称和 JSON 参数 -
Agent 找到工具并调用: findTool()按名称匹配,然后调用Handle() -
Handle 内部流程:工具中间件链 -> JSONAdapter 反序列化 -> 业务函数 -> JSONAdapter 序列化 -
结果反馈给模型:工具输出作为 RoleTool消息追加到历史,模型在下一轮迭代中看到结果
本期小结
这一期我们构建了一个完整、强大的工具系统。让我们回顾关键知识点:
-
Tool 接口:四个方法 —— Name(),Description(),InputSchema(),Handle() -
Handler / HandleFunc:Go 经典的函数-接口适配器模式 -
JSONAdapter:泛型函数,自动完成 JSON 序列化/反序列化,桥接类型安全的 Go 世界和文本世界 -
GenerateSchema:利用反射从 Go 结构体自动生成 JSON Schema,消除手写 Schema 的痛苦 -
tools.NewFunc:最常用的工具创建方式,类型安全 + 自动 Schema + 自动 JSON 适配 -
tools.NewTool:手动创建方式,适合需要完全控制的场景 -
工具中间件: WithLogging、WithTimeout、WithRetry—— 给工具添加通用能力 -
ToolResolver:动态工具解析,根据上下文按需提供工具 -
NewAgentTool:Agent-as-Tool 模式,让 Agent 可以嵌套组合
核心设计理念:
-
开发者体验至上: NewFunc让创建工具像写普通函数一样简单 -
类型安全:泛型 + JSONAdapter 让编译器帮你检查类型 -
可组合:中间件、ToolResolver、Agent-as-Tool 都是组合的体现 -
容错设计:工具执行失败返回错误信息给模型,而不是终止整个 Agent
下期预告
Agent 有了大脑(核心循环)和双手(工具系统),但它还缺一个关键能力 —— 记忆。下一期我们将深入会话管理与上下文控制(Session & Context Management),实现对话历史持久化、滑动窗口策略、上下文摘要等功能,让 Agent 在多轮对话中保持连贯的记忆。
夜雨聆风