在上一篇中,我们已经掌握了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. 第一个参数必须是
context.Context(上下文,用于传递超时、日志等信息); - 2. 第二个参数必须是结构体指针(对应工具的入参,比如上一篇的ZipParam、WeatherParam);
- 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. 定义入参结构体;
- 2. 编写符合规范的Go执行函数;
- 3. 用
utils.NewTool将ToolInfo和函数绑定,生成InvokableTool; - 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. 定义带jsonschema标签的入参结构体;
- 2. 编写符合规范的Go执行函数;
- 3. 用
utils.InferTool自动生成InvokableTool(自动推导ToolInfo+绑定函数); - 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. 手动封装的邮编工具:成功构建InvokableTool,执行后返回了北京的邮编,查询深圳时返回无数据(符合模拟逻辑);
- 2. 自动封装的天气工具:成功自动推导ToolInfo并封装为InvokableTool,执行后返回上海的天气,查询深圳时返回无数据;
- 3. 整个流程验证了InvokableTool的核心能力:绑定ToolInfo+执行函数,接收参数并返回结果。
四、避坑指南&使用建议
4.1 常见坑点
❌ Go函数格式不符合规范:比如缺少context.Context、第二个参数不是结构体指针、返回值没有error——这是最常见的错误,Eino会无法封装;
❌ 参数JSON格式错误:比如构造参数时少引号、字段名和结构体不一致,导致Execute解析参数失败;
❌ 手动封装时ToolInfo的参数类型写错:比如把schema.String写成schema.Int,模型传参类型不匹配;
❌ 自动封装时jsonschema标签格式错误:比如标签里的enum和required写法错误,导致ToolInfo推导不完整;
❌ 执行结果类型转换错误:比如工具返回int,但代码里转成string,会报类型断言失败。
4.2 使用建议
- 1. 函数规范:严格遵守“context.Context + 结构体指针参数 + (任意类型, error)返回值”的格式,写之前先核对;
- 2. 封装方式:学习阶段用手动封装理解原理,实际开发优先用自动封装(
utils.InferTool),减少手写代码; - 3. 参数构造:执行工具时,先把参数转成JSON字符串(模拟模型生成的ToolCall参数),确保字段名和结构体一致;
- 4. 错误处理:执行工具后一定要处理error,避免程序崩溃,同时方便排查问题;
- 5. 结果处理:执行结果先做类型断言(比如转string),再使用,避免类型错误。
五、知识点回顾
- 1. InvokableTool是“ToolInfo + Go执行函数”的结合体,具备实际执行能力;
- 2. Go函数要封装成InvokableTool,必须满足“context.Context + 结构体指针参数 + (任意类型, error)返回值”的规范;
- 3. 手动封装:用
utils.NewTool绑定手动构建的ToolInfo和执行函数,适合理解原理; - 4. 自动封装:用
utils.InferTool自动推导ToolInfo并绑定函数,推荐实际开发使用; - 5. 执行InvokableTool的核心是调用
InvokableRun方法,传入JSON格式的参数,处理返回结果。
MiaoAll