第十三篇:InvokableTool-把Go函数包装成模型可调用的工具

2026-04-25 51 0

在上一篇中,我们已经掌握了ToolInfo的构建——这份写给模型的“工具说明书”,但只有说明书,工具还不能真正干活。今天我们要学习的InvokableTool,就是把Go语言的普通函数和ToolInfo绑定在一起,让工具从“只可描述”变成“可执行”的完整工具。

简单来说:

  • • ToolInfo = 工具说明书(告诉模型怎么用);
  • • Go函数 = 工具的实际功能(工具能做什么);
  • • InvokableTool = 带说明书的可执行工具(既让模型懂用法,又能实际执行)。

这篇基于上篇的邮编工具和天气工具,如何把Go函数封装成InvokableTool,并且实现“传入参数→执行工具→获取结果”的完整流程。

一、先理清:InvokableTool的核心定位

1.1 与ToolInfo的关系

上节课我们创建的ToolInfo只是“静态描述”,没有执行能力;而InvokableTool是Eino框架中具备执行能力的核心工具类型,它的核心组成:

  • • 内置一份ToolInfo(给模型看的说明书);
  • • 绑定一个符合规范的Go执行函数(真正实现业务逻辑);
  • • 自带参数解析能力(把JSON格式的参数转换成Go函数能接收的结构体)。

打个通俗的比方:如果ToolInfo是“电饭煲的使用手册”,Go函数是“电饭煲煮饭的核心逻辑”,那么InvokableTool就是“完整的电饭煲”——既有人能看懂的手册,又能实际煮出饭。

1.2 InvokableTool对Go函数的规范要求

要让Go函数能被封装成InvokableTool,函数必须满足以下3个核心规范(缺一不可),这是最容易出错的地方,一定要记牢:

  1. 1. 第一个参数必须是context.Context(上下文,用于传递超时、日志等信息);
  2. 2. 第二个参数必须是结构体指针(对应工具的入参,比如上一篇的ZipParam、WeatherParam);
  3. 3. 返回值必须是(任意类型, error)(第一个是工具执行结果,第二个是错误信息)。

✅ 正确示例(以上篇的天气函数为例):

func getWeather(ctx context.Context, p *WeatherParam) (string, error) { ... }

❌ 错误示例(:

// 错误1:缺少context.Context参数
func getWeather(p *WeatherParam) (string, error) { ... }
// 错误2:第二个参数不是结构体指针
func getWeather(ctx context.Context, city string) (string, error) { ... }
// 错误3:返回值没有error
func getWeather(ctx context.Context, p *WeatherParam) string { ... }

二、实操:封装Go函数为InvokableTool(两种方式)

我们基于上一篇的两个工具(手动构建的邮编工具、自动推导的天气工具),分别实现InvokableTool的封装和执行,保持内容的连贯性。

2.1 方式1:手动封装(对应手动构建的ToolInfo)

适合场景:理解InvokableTool的底层原理,或需要自定义参数解析逻辑。
核心步骤:

  1. 1. 定义入参结构体;
  2. 2. 编写符合规范的Go执行函数;
  3. 3. 用utils.NewTool将ToolInfo和函数绑定,生成InvokableTool;
  4. 4. 构造参数,调用Execute方法执行工具。

步骤1:复用上一篇的邮编工具代码(入参+执行函数)

// ZipParam 邮编工具入参结构体
type ZipParam struct {
    City string `json:"city"`
}

// getZipcode 邮编工具执行函数(模拟数据)
func getZipcode(ctx context.Context, p *ZipParam) (string, error) {
    zipData := map[string]string{
        "北京": "100000", "上海": "200000", "广州": "510000",
    }
    if zip, ok := zipData[p.City]; ok {
        return fmt.Sprintf("%s 的邮政编码:%s", p.City, zip), nil
    }
    return fmt.Sprintf("暂无 %s 的邮编数据", p.City), nil
}

步骤2:手动封装为InvokableTool

// buildZipTool 手动构建邮编工具的InvokableTool(绑定ToolInfo+执行函数)
func buildZipTool(ctx context.Context) tool.InvokableTool {
    // 1. 先构建ToolInfo
    zipToolInfo := &schema.ToolInfo{
        Name: "get_city_zipcode",
        Desc: "查询国内省会城市的邮政编码,返回6位数字字符串",
        ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
            "city": {
                Type:     schema.String, // 注意:Eino规范用schema.String而非DataTypeString
                Desc:     "待查询的国内省会城市名称,如北京、上海、广州",
                Required: true,
            },
        }),
        Extra: map[string]any{
            "category": "生活工具",
        },
    }

    // 2. 绑定ToolInfo和执行函数,生成InvokableTool
    zipTool := utils.NewTool(zipToolInfo, getZipcode)

    return zipTool
}

步骤3:执行InvokableTool(核心:传入参数,调用Execute)

// executeZipTool 执行邮编工具
func executeZipTool(ctx context.Context, zipTool tool.InvokableTool, city string) {
    // 1. 构造JSON格式的参数(模拟模型生成的ToolCall参数)
    params := map[string]string{"city": city}
    paramsJSON, err := json.Marshal(params)
    if err != nil {
        fmt.Printf("【邮编工具】构造参数失败:%v\n", err)
        return
    }

    // 2. InvokableRun 执行工具
    result, err := zipTool.InvokableRun(ctx, string(paramsJSON))
    if err != nil {
        fmt.Printf("【邮编工具】执行失败:%v\n", err)
        return
    }

    // 3. 打印结果
    fmt.Printf("【邮编工具】执行结果:%s\n", result)
}

2.2 方式2:自动封装(对应自动推导的ToolInfo)

适合场景:工程化开发、减少手写代码。
核心步骤:

  1. 1. 定义带jsonschema标签的入参结构体;
  2. 2. 编写符合规范的Go执行函数;
  3. 3. 用utils.InferTool自动生成InvokableTool(自动推导ToolInfo+绑定函数);
  4. 4. 调用Execute方法执行工具。

步骤1:复用上节课的天气工具代码(入参+执行函数)

// WeatherParam 天气工具入参结构体(带jsonschema标签)
type WeatherParam struct {
    City string `json:"city" jsonschema:"description=待查询的国内城市名称,如北京、上海、广州,enum=北京,上海,广州,required"`
}

// getWeather 天气工具执行函数(模拟数据)
func getWeather(ctx context.Context, p *WeatherParam) (string, error) {
    weatherData := map[string]string{
        "北京": "26℃ 晴,微风",
        "上海": "28℃ 多云",
        "广州": "32℃ 雷阵雨",
    }
    if w, ok := weatherData[p.City]; ok {
        return fmt.Sprintf("%s 今日天气:%s", p.City, w), nil
    }
    return fmt.Sprintf("暂无 %s 的天气数据", p.City), nil
}

步骤2:自动封装为InvokableTool

// buildWeatherTool 自动构建天气工具的InvokableTool
func buildWeatherTool(ctx context.Context) (tool.InvokableTool, error) {
    // 用InferTool自动推导ToolInfo并绑定执行函数,生成InvokableTool
    weatherTool, err := utils.InferTool(
        "get_city_weather", // 工具名称
        "查询国内城市当日实时气温和天气状况,仅支持北京/上海/广州", // 工具描述
        getWeather, // 执行函数
    )
    if err != nil {
        return nil, fmt.Errorf("自动构建天气工具失败:%v", err)
    }
    return weatherTool, nil
}

步骤3:执行天气工具

// executeWeatherTool 执行天气工具
func executeWeatherTool(ctx context.Context, weatherTool tool.InvokableTool, city string) {
    // 1. 构造JSON参数
    params := map[string]string{"city": city}
    paramsJSON, err := json.Marshal(params)
    if err != nil {
        fmt.Printf("【天气工具】构造参数失败:%v\n", err)
        return
    }

    // 2. InvokableRun 执行工具
    result, err := weatherTool.InvokableRun(ctx, string(paramsJSON))
    if err != nil {
        fmt.Printf("【天气工具】执行失败:%v\n", err)
        return
    }

    // 3. 打印结果
    fmt.Printf("【天气工具】执行结果:%s\n", result)
}

三、完整可运行Demo:InvokableTool封装与执行

接下来我们整合以上代码,形成完整的可运行案例,包含工具封装、工具执行、结果输出,注释,可以直接复制运行。

3.1 完整代码

package main

import (
    "context"
    "encoding/json"
    "fmt"

    "github.com/cloudwego/eino/components/tool"
    "github.com/cloudwego/eino/components/tool/utils"
    "github.com/cloudwego/eino/schema"
)

// ==================== 1. 邮编工具:手动封装InvokableTool ====================
// ZipParam 邮编工具入参结构体
type ZipParam struct {
    City string `json:"city"`
}

// getZipcode 邮编工具执行函数(模拟数据)
func getZipcode(ctx context.Context, p *ZipParam) (string, error) {
    zipData := map[string]string{
        "北京": "100000", "上海": "200000", "广州": "510000",
    }
    if zip, ok := zipData[p.City]; ok {
        return fmt.Sprintf("%s 的邮政编码:%s", p.City, zip), nil
    }
    return fmt.Sprintf("暂无 %s 的邮编数据", p.City), nil
}

// buildZipTool 手动构建邮编工具的InvokableTool
func buildZipTool(ctx context.Context) tool.InvokableTool {
    // 构建ToolInfo(和第十二篇一致)
    zipToolInfo := &schema.ToolInfo{
        Name: "get_city_zipcode",
        Desc: "查询国内省会城市的邮政编码,返回6位数字字符串",
        ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
            "city": {
                Type:     schema.String,
                Desc:     "待查询的国内省会城市名称,如北京、上海、广州",
                Required: true,
            },
        }),
        Extra: map[string]any{
            "category": "生活工具",
        },
    }

    // 绑定ToolInfo和执行函数,生成InvokableTool
    zipTool := utils.NewTool(zipToolInfo, getZipcode)
    return zipTool
}

// executeZipTool 执行邮编工具
func executeZipTool(ctx context.Context, zipTool tool.InvokableTool, city string) {
    // 1. 构造JSON格式的参数(模拟模型生成的ToolCall参数)
    params := map[string]string{"city": city}
    paramsJSON, err := json.Marshal(params)
    if err != nil {
        fmt.Printf("【邮编工具】构造参数失败:%v\n", err)
        return
    }

    // 2. InvokableRun 执行工具
    result, err := zipTool.InvokableRun(ctx, string(paramsJSON))
    if err != nil {
        fmt.Printf("【邮编工具】执行失败:%v\n", err)
        return
    }

    // 3. 打印结果
    fmt.Printf("【邮编工具】执行结果:%s\n", result)
}

// ==================== 2. 天气工具:自动封装InvokableTool ====================
// WeatherParam 天气工具入参结构体(带jsonschema标签)
type WeatherParam struct {
    City string `json:"city" jsonschema:"description=待查询的国内城市名称,如北京、上海、广州,enum=北京,上海,广州,required"`
}

// getWeather 天气工具执行函数(模拟数据)
func getWeather(ctx context.Context, p *WeatherParam) (string, error) {
    weatherData := map[string]string{
        "北京": "26℃ 晴,微风",
        "上海": "28℃ 多云",
        "广州": "32℃ 雷阵雨",
    }
    if w, ok := weatherData[p.City]; ok {
        return fmt.Sprintf("%s 今日天气:%s", p.City, w), nil
    }
    return fmt.Sprintf("暂无 %s 的天气数据", p.City), nil
}

// buildWeatherTool 自动构建天气工具的InvokableTool
func buildWeatherTool(ctx context.Context) (tool.InvokableTool, error) {
    // 自动推导ToolInfo并绑定执行函数
    weatherTool, err := utils.InferTool(
        "get_city_weather",
        "查询国内城市当日实时气温和天气状况,仅支持北京/上海/广州",
        getWeather,
    )
    if err != nil {
        return nil, fmt.Errorf("自动构建天气工具失败:%v", err)
    }
    return weatherTool, nil
}

// executeWeatherTool 执行天气工具
func executeWeatherTool(ctx context.Context, weatherTool tool.InvokableTool, city string) {
    // 1. 构造JSON参数
    params := map[string]string{"city": city}
    paramsJSON, err := json.Marshal(params)
    if err != nil {
        fmt.Printf("【天气工具】构造参数失败:%v\n", err)
        return
    }

    // 2. InvokableRun 执行工具
    result, err := weatherTool.InvokableRun(ctx, string(paramsJSON))
    if err != nil {
        fmt.Printf("【天气工具】执行失败:%v\n", err)
        return
    }

    // 3. 打印结果
    fmt.Printf("【天气工具】执行结果:%s\n", result)
}

// ==================== 3. 辅助函数:打印ToolInfo(对比手动/自动) ====================
func printToolInfo(label string, tool tool.InvokableTool, ctx context.Context) {
    info, err := tool.Info(ctx)
    if err != nil {
        fmt.Printf("提取%s的ToolInfo失败:%v\n", label, err)
        return
    }
    fmt.Printf("\n===== %s =====\n", label)
    fmt.Printf("工具名:%s\n", info.Name)
    fmt.Printf("工具描述:%s\n", info.Desc)
    fmt.Printf("参数约束:%v\n", info.ParamsOneOf)
}

// ==================== 4. 主函数:完整流程测试 ====================
func main() {
    // 创建上下文(Eino工具执行必需)
    ctx := context.Background()

    // -------------------- 测试1:手动封装的邮编工具 --------------------
    fmt.Println("===== 测试1:手动封装的邮编工具 =====")
    // 1. 构建邮编工具
    zipTool := buildZipTool(ctx)
    // 2. 打印ToolInfo(验证和第十二篇一致)
    printToolInfo("手动构建的邮编工具ToolInfo", zipTool, ctx)
    // 3. 执行工具(查询北京邮编)
    executeZipTool(ctx, zipTool, "北京")
    // 4. 测试异常场景(查询深圳邮编,无数据)
    executeZipTool(ctx, zipTool, "深圳")

    // -------------------- 测试2:自动封装的天气工具 --------------------
    fmt.Println("\n===== 测试2:自动封装的天气工具 =====")
    // 1. 构建天气工具
    weatherTool, err := buildWeatherTool(ctx)
    if err != nil {
        fmt.Printf("构建天气工具失败:%v\n", err)
        return
    }
    // 2. 打印ToolInfo(验证自动推导结果)
    printToolInfo("自动推导的天气工具ToolInfo", weatherTool, ctx)
    // 3. 执行工具(查询上海天气)
    executeWeatherTool(ctx, weatherTool, "上海")
    // 4. 测试异常场景(查询深圳天气,无数据)
    executeWeatherTool(ctx, weatherTool, "深圳")
}

3.2 运行结果

执行代码后,会看到如下输出:

===== 测试1:手动封装的邮编工具 =====

===== 手动构建的邮编工具ToolInfo =====
工具名:get_city_zipcode
工具描述:查询国内省会城市的邮政编码,返回6位数字字符串
参数约束:&{map[city:0xc0001b6690] <nil>}
【邮编工具】执行结果:北京 的邮政编码:100000
【邮编工具】执行结果:暂无 深圳 的邮编数据

===== 测试2:自动封装的天气工具 =====

===== 自动推导的天气工具ToolInfo =====
工具名:get_city_weather
工具描述:查询国内城市当日实时气温和天气状况,仅支持北京/上海/广州
参数约束:&{map[] 0xc0004fa008}
【天气工具】执行结果:上海 今日天气:28℃ 多云
【天气工具】执行结果:暂无 深圳 的天气数据

3.4 结果解释

  1. 1. 手动封装的邮编工具:成功构建InvokableTool,执行后返回了北京的邮编,查询深圳时返回无数据(符合模拟逻辑);
  2. 2. 自动封装的天气工具:成功自动推导ToolInfo并封装为InvokableTool,执行后返回上海的天气,查询深圳时返回无数据;
  3. 3. 整个流程验证了InvokableTool的核心能力:绑定ToolInfo+执行函数,接收参数并返回结果。

四、避坑指南&使用建议

4.1 常见坑点

❌ Go函数格式不符合规范:比如缺少context.Context、第二个参数不是结构体指针、返回值没有error——这是最常见的错误,Eino会无法封装;
❌ 参数JSON格式错误:比如构造参数时少引号、字段名和结构体不一致,导致Execute解析参数失败;
❌ 手动封装时ToolInfo的参数类型写错:比如把schema.String写成schema.Int,模型传参类型不匹配;
❌ 自动封装时jsonschema标签格式错误:比如标签里的enumrequired写法错误,导致ToolInfo推导不完整;
❌ 执行结果类型转换错误:比如工具返回int,但代码里转成string,会报类型断言失败。

4.2 使用建议

  1. 1. 函数规范:严格遵守“context.Context + 结构体指针参数 + (任意类型, error)返回值”的格式,写之前先核对;
  2. 2. 封装方式:学习阶段用手动封装理解原理,实际开发优先用自动封装(utils.InferTool),减少手写代码;
  3. 3. 参数构造:执行工具时,先把参数转成JSON字符串(模拟模型生成的ToolCall参数),确保字段名和结构体一致;
  4. 4. 错误处理:执行工具后一定要处理error,避免程序崩溃,同时方便排查问题;
  5. 5. 结果处理:执行结果先做类型断言(比如转string),再使用,避免类型错误。

五、知识点回顾

  1. 1. InvokableTool是“ToolInfo + Go执行函数”的结合体,具备实际执行能力;
  2. 2. Go函数要封装成InvokableTool,必须满足“context.Context + 结构体指针参数 + (任意类型, error)返回值”的规范;
  3. 3. 手动封装:用utils.NewTool绑定手动构建的ToolInfo和执行函数,适合理解原理;
  4. 4. 自动封装:用utils.InferTool自动推导ToolInfo并绑定函数,推荐实际开发使用;
  5. 5. 执行InvokableTool的核心是调用InvokableRun方法,传入JSON格式的参数,处理返回结果。

相关文章

第十六篇:Eino入门——核心组件总览与场景选型总结
第十五篇:Eino ChatModelAgent 模型自己决定是否调用工具
第十四篇:Eino ToolsNode 工具调用执行器
第十二篇:Eino ToolInfo工具-给模型看的说明书
第十一篇:Eino Message与Prompt上下文编排
第十篇:Eino ChatModel:将大模型调用抽象成可复用组件

发布评论