Go + Eino 构建 AI Agent(二):Tool Calling

20 阅读3分钟

TL;DR: Tool Calling 让 LLM 能够调用外部函数。Eino 提供 utils.InferTool 从 Go 函数自动推断工具定义,通过 chatModel.BindTools() 绑定工具,模型返回的 response.ToolCalls 包含要调用的工具和参数。

Tool Calling 流程

用户问题 → LLM 分析 → 返回 ToolCalls → 执行工具 → 结果返回 LLM → 最终回答

关键点:

  • LLM 不执行工具,只决定调用哪个工具、传什么参数
  • 你的代码负责执行工具并把结果返回给 LLM
  • LLM 基于工具结果生成最终回答

定义工具

方式一:InferTool(推荐)

从 Go 函数自动推断工具的 JSON Schema:

type WeatherInput struct {
	City string `json:"city" jsonschema:"description=城市名称,如:北京、上海"`
}

type WeatherOutput struct {
	City        string `json:"city"`
	Temperature int    `json:"temperature"`
	Condition   string `json:"condition"`
}

weatherTool, _ := utils.InferTool(
	"get_weather",                    // 工具名称
	"获取指定城市的当前天气信息",      // 工具描述
	func(ctx context.Context, input *WeatherInput) (*WeatherOutput, error) {
		// 实现逻辑
		return &WeatherOutput{City: input.City, Temperature: 25, Condition: "晴"}, nil
	},
)

jsonschema:"description=..." 标签会被提取为参数描述,帮助 LLM 理解如何使用工具。

方式二:实现 Tool 接口

手动定义工具的 Schema:

type AddTool struct{}

func (t *AddTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name: "add",
		Desc: "计算两个数的和",
		ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
			"a": {Desc: "第一个数", Type: schema.Number, Required: true},
			"b": {Desc: "第二个数", Type: schema.Number, Required: true},
		}),
	}, nil
}

func (t *AddTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
	var input struct {
		A float64 `json:"a"`
		B float64 `json:"b"`
	}
	json.Unmarshal([]byte(argumentsInJSON), &input)
	return fmt.Sprintf(`{"result": %v}`, input.A+input.B), nil
}

完整示例:多工具调用

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/cloudwego/eino-ext/components/model/ark"
	"github.com/cloudwego/eino/components/tool"
	"github.com/cloudwego/eino/components/tool/utils"
	"github.com/cloudwego/eino/schema"
	"github.com/joho/godotenv"
)

func init() {
	godotenv.Load()
}

type WeatherInput struct {
	City string `json:"city" jsonschema:"description=城市名称"`
}

type AddInput struct {
	A float64 `json:"a" jsonschema:"description=第一个数"`
	B float64 `json:"b" jsonschema:"description=第二个数"`
}

func main() {
	ctx := context.Background()

	// 1. 定义工具
	weatherTool, _ := utils.InferTool("get_weather", "获取城市天气",
		func(ctx context.Context, input *WeatherInput) (string, error) {
			data := map[string]string{"北京": "晴,25°C", "上海": "多云,28°C"}
			if w, ok := data[input.City]; ok {
				return w, nil
			}
			return "暂无数据", nil
		})

	addTool, _ := utils.InferTool("add", "计算两个数的和",
		func(ctx context.Context, input *AddInput) (string, error) {
			return fmt.Sprintf(`{"result": %v}`, input.A+input.B), nil
		})

	// 2. 创建工具映射表(用于路由)
	toolMap := map[string]tool.InvokableTool{
		"get_weather": weatherTool,
		"add":         addTool,
	}

	// 3. 收集工具信息
	var toolInfos []*schema.ToolInfo
	for _, t := range toolMap {
		info, _ := t.Info(ctx)
		toolInfos = append(toolInfos, info)
	}

	// 4. 创建模型并绑定工具
	chatModel, _ := ark.NewChatModel(ctx, &ark.ChatModelConfig{
		APIKey: os.Getenv("ARK_API_KEY"),
		Model:  os.Getenv("ARK_MODEL_ID"),
	})
	chatModel.BindTools(toolInfos)

	// 5. 发送请求
	messages := []*schema.Message{
		schema.UserMessage("北京天气怎么样?另外算一下 123+456"),
	}
	response, _ := chatModel.Generate(ctx, messages)

	// 6. 处理工具调用
	if len(response.ToolCalls) > 0 {
		messages = append(messages, response)

		for _, tc := range response.ToolCalls {
			fmt.Printf("🔧 调用: %s(%s)\n", tc.Function.Name, tc.Function.Arguments)

			t, ok := toolMap[tc.Function.Name]
			if !ok {
				log.Printf("未知工具: %s", tc.Function.Name)
				continue
			}

			result, _ := t.InvokableRun(ctx, tc.Function.Arguments)
			fmt.Printf("   结果: %s\n", result)

			messages = append(messages, &schema.Message{
				Role:       schema.Tool,
				Content:    result,
				ToolCallID: tc.ID,
			})
		}

		// 7. 基于工具结果生成最终回答
		finalResponse, _ := chatModel.Generate(ctx, messages)
		fmt.Println("\n🤖 最终回答:", finalResponse.Content)
	}
}

输出示例:

🔧 调用: get_weather({"city":"北京"})
   结果: 晴,25°C
🔧 调用: add({"a":123,"b":456})
   结果: {"result": 579}

🤖 最终回答: 北京今天天气晴朗,气温25°C。123+456的结果是579

消息序列

Tool Calling 的消息流:

1. User: "北京天气?123+456=?"
2. Assistant: { ToolCalls: [{name:"get_weather"}, {name:"add"}] }  ← 模型决定调用
3. Tool: "晴,25°C"                                                ← get_weather 结果
4. Tool: {"result": 579}                                          ← add 结果
5. Assistant: "北京晴朗25°C,123+456=579"                          ← 最终回答

关键:ToolCallID 必须匹配,模型才能知道哪个结果对应哪个调用。

Trade-offs

方式优点缺点
InferTool代码少,自动推断 Schema依赖 struct tag,灵活性低
实现接口完全控制 Schema代码多,需手动定义参数

工具设计建议

  • 工具描述要清晰,帮助 LLM 判断何时使用
  • 参数描述要具体,包含示例值
  • 返回 JSON 格式,便于 LLM 解析

总结

概念说明
utils.InferTool从 Go 函数自动推断工具定义
chatModel.BindTools()将工具绑定到模型
response.ToolCalls模型返回的工具调用列表
InvokableRun()执行工具,输入输出都是 JSON 字符串
schema.Tool工具结果消息的角色
ToolCallID关联工具调用和结果