乐于分享
好东西不私藏

第八期:工具系统 —— 让 Agent 拥有行动力

第八期:工具系统 —— 让 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[IanyOany](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), &params); 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[IanyOany](    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?

场景
推荐
参数是结构化的 JSON 对象
NewFunc

 —— 自动生成 Schema,类型安全
参数很简单(单个字符串等)
NewTool

 —— 手动控制 Schema
需要自定义 Schema(如复杂的嵌套/条件)
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[IanyOany](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[IanyOany](    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 继续循环                   ││                                                                       │└─────────────────────────────────────────────────────────────────────┘

数据流是这样的:

  1. 模型看到工具定义Name()Description()InputSchema() 告诉模型有什么工具可用以及怎么调用
  2. 模型生成调用请求:模型输出 ToolPart,包含工具名称和 JSON 参数
  3. Agent 找到工具并调用findTool() 按名称匹配,然后调用 Handle()
  4. Handle 内部流程:工具中间件链 -> JSONAdapter 反序列化 -> 业务函数 -> JSONAdapter 序列化
  5. 结果反馈给模型:工具输出作为 RoleTool 消息追加到历史,模型在下一轮迭代中看到结果

本期小结

这一期我们构建了一个完整、强大的工具系统。让我们回顾关键知识点:

  1. Tool 接口:四个方法 —— Name()Description()InputSchema()Handle()
  2. Handler / HandleFunc:Go 经典的函数-接口适配器模式
  3. JSONAdapter:泛型函数,自动完成 JSON 序列化/反序列化,桥接类型安全的 Go 世界和文本世界
  4. GenerateSchema:利用反射从 Go 结构体自动生成 JSON Schema,消除手写 Schema 的痛苦
  5. tools.NewFunc:最常用的工具创建方式,类型安全 + 自动 Schema + 自动 JSON 适配
  6. tools.NewTool:手动创建方式,适合需要完全控制的场景
  7. 工具中间件WithLoggingWithTimeoutWithRetry —— 给工具添加通用能力
  8. ToolResolver:动态工具解析,根据上下文按需提供工具
  9. NewAgentTool:Agent-as-Tool 模式,让 Agent 可以嵌套组合

核心设计理念

  • 开发者体验至上NewFunc 让创建工具像写普通函数一样简单
  • 类型安全:泛型 + JSONAdapter 让编译器帮你检查类型
  • 可组合:中间件、ToolResolver、Agent-as-Tool 都是组合的体现
  • 容错设计:工具执行失败返回错误信息给模型,而不是终止整个 Agent

下期预告

Agent 有了大脑(核心循环)和双手(工具系统),但它还缺一个关键能力 —— 记忆。下一期我们将深入会话管理与上下文控制(Session & Context Management),实现对话历史持久化、滑动窗口策略、上下文摘要等功能,让 Agent 在多轮对话中保持连贯的记忆。