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

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:

go 复制代码
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:

go 复制代码
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
}

完整示例:多工具调用

go 复制代码
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)
	}
}

输出示例:

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

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

消息序列

Tool Calling 的消息流:

css 复制代码
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 关联工具调用和结果
相关推荐
CCC:CarCrazeCurator41 分钟前
从零开始构建一个编码智能体
人工智能·ai·transformer
小超同学你好1 小时前
OpenClaw 中的 Skills 机制与复现
人工智能·语言模型·langchain
mCell6 小时前
关于 Openclaw,最近的一点思考。
人工智能·安全·aigc
qq_171538857 小时前
纳采问名定佳期:中国传统订婚文化的千年传承与地域风华
人工智能
zzb15807 小时前
RAG from Scratch-优化-query
java·数据库·人工智能·后端·spring·mybatis
uzong7 小时前
315晚会曝光“AI大模型被投毒”,让AI听话,GEO是什么,带给我们什么思考
人工智能
V搜xhliang02467 小时前
机器人建模(URDF)与仿真配置
大数据·人工智能·深度学习·机器学习·自然语言处理·机器人
房产中介行业研习社7 小时前
2026年3月哪些房源管理系统功能全
大数据·运维·人工智能
Shining05967 小时前
CUDA 编程系列(三)《内存模型与规约优化》
人工智能·学习·其他·学习方法·infinitensor
lisw058 小时前
基于图像的恶意软件分类方法!
人工智能·机器学习