上两篇我们学习了 ToolInfo(工具说明书)、InvokableTool(可执行工具)。我们已经可以手动拿到 ToolCall,调用单个工具。
但真实的大模型工具调用链路并不是手动写代码去判断、匹配、执行工具。大模型输出携带 ToolCall 的 Assistant 消息之后,我们需要一套组件:自动解析调用指令、匹配对应的工具、控制串行或并行执行、捕获异常、把结果封装成标准的 ToolMessage。
在 Eino 中,承担这一整套工作的组件就是 compose.ToolsNode 工具节点。
💡重要概念区分
- 1.
ToolInfo:工具的说明书,专门给大模型阅读,描述工具名字、功能、入参; - 2.
InvokableTool:可执行工具,把 Go 业务函数 + ToolInfo 封装到一起,可以被调用执行; - 3.
ToolsNode:工具调度执行器。它不做意图判断,只接收携带 ToolCall 的消息,自动完成解析、匹配、执行、结果封装。
通俗比喻:
- • ToolInfo = 工具说明书;
- • InvokableTool = 螺丝刀、扳手这些实实在在的工具;
- • ToolsNode = 工具操作台。收到“调用螺丝刀”的指令,自动拿出工具干活,干完把结果整理好返回。
补充:后面我们学习的
adk.ChatModelAgent智能体,底层内部就是复用了 ToolsNode 来完成工具执行逻辑。
一、ToolsNode 基础配置与核心API
包路径:github.com/cloudwego/eino/compose
ToolsNodeConfig 配置结构体
创建 ToolsNode 需要传入 ToolsNodeConfig,几个核心配置字段:
| 配置项 | 作用 |
|---|---|
Tools []tool.BaseTool | 注册全部可用工具,传入我们封装好的 InvokableTool |
UnknownToolsHandler | 大模型幻觉调用不存在工具时的兜底处理函数,避免程序直接报错崩溃 |
ExecuteSequentially | 是否串行执行多个ToolCall;false 默认并行执行;true 按顺序逐个执行 |
ToolArgumentsHandler | 工具入参预处理钩子,可以修改模型输出的JSON参数字符串 |
ToolCallMiddlewares []ToolMiddleware | 工具中间件,实现日志打印、耗时统计、鉴权、参数拦截 |
两个核心方法
- 1.
Invoke(ctx context.Context, input *schema.Message) ([]*schema.Message, error)
同步执行。输入必须是携带 ToolCalls 的 assistant 角色消息,输出返回一组[]*schema.Message,全部为 Tool 角色消息。
⚠️注意:如果输入消息里面没有 ToolCalls,ToolsNode 不会报错,直接返回空切片。
- 2.
Stream(ctx context.Context, input *schema.Message) (*schema.StreamReader[[]*schema.Message], error)
流式执行工具,用于 compose 流式编排链路。
ToolsNode完整工作流程
- 1. 接收输入:
*schema.Message(assistant消息,携带ToolCall数组) - 2. 提取消息内部全部
ToolCall; - 3. 根据工具名称,在注册的工具列表匹配对应的
InvokableTool; - 4. 如果找不到对应工具,执行
UnknownToolsHandler兜底; - 5. 根据
ExecuteSequentially配置,选择并行或者串行执行多个工具; - 6. 执行过程可以经过参数预处理钩子、工具中间件;
- 7. 每一个工具执行完成,自动把返回结果封装为标准
schema.ToolMessage; - 8. 返回全部工具结果消息切片。
关键点:ToolsNode只负责执行工具,它不会调用大模型!
完整ReAct工具调用闭环流程:ChatModel生成ToolCall消息 → ToolsNode执行工具 → 把原始消息+工具结果消息一起送回ChatModel,生成最终回答。
二、完整可运行Demo
本Demo复用第十三篇的业务工具,全部使用
utils.InferTool自动推导生成工具。
为了聚焦学习ToolsNode本身,不接入真实大模型,手动构造模拟的ToolCall消息。
依赖安装
go get github.com/cloudwego/eino
go get github.com/cloudwego/eino/components/tool
go get github.com/cloudwego/eino/components/tool/utils
go get github.com/cloudwego/eino/compose
go mod tidymain.go完整代码
package main
import (
"context"
"fmt"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/components/tool/utils"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
)
// ==================== 1、工具入参结构体 ====================
// ZipParam 邮编查询工具入参
type ZipParam struct {
City string `json:"city" jsonschema:"description=待查询的国内省会城市名称,如北京、上海、广州,required=true"`
}
// WeatherParam 天气查询工具入参
type WeatherParam struct {
City string `json:"city" jsonschema:"description=待查询的国内城市名称,enum=北京,enum=上海,enum=广州,required=true"`
}
// ==================== 2、工具业务执行函数(模拟业务) ====================
// 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
}
// 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
}
// ==================== 3、构建InvokableTool ====================
func buildZipTool() (tool.InvokableTool, error) {
return utils.InferTool("get_city_zipcode", "查询国内省会城市的邮政编码", getZipcode)
}
func buildWeatherTool() (tool.InvokableTool, error) {
return utils.InferTool("get_city_weather", "查询国内城市当日天气,仅支持北京/上海/广州", getWeather)
}
// ==================== 4、创建ToolsNode节点 ====================
func createToolsNode(ctx context.Context) (*compose.ToolsNode, error) {
zipTool, err := buildZipTool()
if err != nil {
return nil, fmt.Errorf("构建邮编工具失败:%w", err)
}
weatherTool, err := buildWeatherTool()
if err != nil {
return nil, fmt.Errorf("构建天气工具失败:%w", err)
}
cfg := &compose.ToolsNodeConfig{
// 注册全部可用工具
Tools: []tool.BaseTool{zipTool, weatherTool},
// 兜底处理:模型幻觉调用不存在工具,返回友好提示,不直接抛错
UnknownToolsHandler: func(ctx context.Context, name, input string) (string, error) {
return fmt.Sprintf("错误:工具【%s】不存在,请检查工具名称", name), nil
},
// false:多个tool_call并行执行;true:按顺序串行执行
ExecuteSequentially: false,
}
return compose.NewToolNode(ctx, cfg)
}
// makeToolCall 辅助函数:构造模拟大模型输出的Assistant消息,携带ToolCall
func makeToolCall(calls ...schema.ToolCall) *schema.Message {
return &schema.Message{
Role: schema.Assistant,
ToolCalls: calls,
}
}
// runToolCase 封装测试用例,简化重复代码
func runToolCase(ctx context.Context, tn *compose.ToolsNode, title string, msg *schema.Message) {
fmt.Printf("===== %s =====\n", title)
results, err := tn.Invoke(ctx, msg)
if err != nil {
fmt.Printf("执行失败:%v\n\n", err)
return
}
for _, m := range results {
fmt.Printf("ToolMessage | toolCallID=%s | content=%s\n", m.ToolCallID, m.Content)
}
fmt.Println()
}
func main() {
ctx := context.Background()
toolsNode, err := createToolsNode(ctx)
if err != nil {
fmt.Printf("创建 ToolsNode 失败:%v\n", err)
return
}
// 案例1:单个工具调用,查询上海天气
runToolCase(ctx, toolsNode, "案例1:单个工具调用(查询上海天气)",
makeToolCall(schema.ToolCall{
ID: "call_001",
Type: "function",
Function: schema.FunctionCall{
Name: "get_city_weather",
Arguments: `{"city":"上海"}`,
},
}))
// 案例2:同时调用两个工具,并行执行,查询北京天气+邮编
runToolCase(ctx, toolsNode, "案例2:多工具并行调用(北京天气+邮编)",
makeToolCall(
schema.ToolCall{
ID: "call_002",
Type: "function",
Function: schema.FunctionCall{
Name: "get_city_weather",
Arguments: `{"city":"北京"}`,
},
},
schema.ToolCall{
ID: "call_003",
Type: "function",
Function: schema.FunctionCall{
Name: "get_city_zipcode",
Arguments: `{"city":"北京"}`,
},
},
))
// 案例3:模型幻觉,调用没有注册的工具,触发UnknownToolsHandler
runToolCase(ctx, toolsNode, "案例3:幻觉调用不存在的工具,触发兜底逻辑",
makeToolCall(schema.ToolCall{
ID: "call_004",
Type: "function",
Function: schema.FunctionCall{
Name: "get_stock_price",
Arguments: `{"code":"000001"}`,
},
}))
}程序运行输出
===== 案例1:单个工具调用(查询上海天气) =====
ToolMessage | toolCallID=call_001 | content=上海 今日天气:28℃ 多云
===== 案例2:多工具并行调用(北京天气+邮编) =====
ToolMessage | toolCallID=call_002 | content=北京 今日天气:26℃ 晴,微风
ToolMessage | toolCallID=call_003 | content=北京 的邮政编码:100000
===== 案例3:幻觉调用不存在的工具(触发兜底) =====
ToolMessage | toolCallID=call_004 | content=错误:工具【get_stock_price】不存在,请检查工具名称三、代码关键点解读
- 1. 输入消息要求:传入
Invoke的消息必须是assistant角色,并且填充ToolCalls;如果消息没有ToolCalls,ToolsNode直接返回空切片,不会报错。 - 2. UnknownToolsHandler:专门用来处理大模型幻觉编造不存在工具的场景,返回的字符串会被自动封装为ToolMessage,不会直接panic程序。
- 3. ExecuteSequentially:默认
false代表并行执行多个工具,适合工具之间没有依赖;如果工具存在先后依赖关系,设置为true串行执行。 - 4. 返回值是
[]*schema.Message,全部为Tool角色消息,每一条消息的ToolCallID和输入的ToolCall一一对应。
完整ReAct业务闭环伪代码,帮助大家理解上下文如何拼接:
// messages 初始对话上下文
resp1, _ := chatModel.Generate(ctx, messages)
// resp1 是assistant消息,携带ToolCall
toolMsgs, _ := toolsNode.Invoke(ctx, resp1)
// 拼接完整上下文:原始上下文 + 模型输出的toolcall消息 + 工具返回结果消息
messages = append(messages, resp1)
messages = append(messages, toolMsgs...)
// 二次送入模型,拿到大模型结合工具结果之后的最终回答
finalResp, _ := chatModel.Generate(ctx, messages)四、ToolsNode扩展能力简单了解
1. ToolArgumentsHandler 参数预处理钩子
在工具执行之前拦截、修改模型输出的JSON参数字符串,适合统一做参数清洗。
ToolArgumentsHandler: func(ctx context.Context,name string,arguments string)(string,error){
fmt.Printf("工具:%s,原始参数:%s\n",name,arguments)
return arguments,nil
},2. ToolCallMiddlewares 工具中间件
洋葱模型中间件,可以统一做工具耗时统计、日志打印、权限校验。
ToolCallMiddlewares: []compose.ToolMiddleware{
{
Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint {
return func(ctx context.Context,input *compose.ToolInput)(*compose.ToolOutput,error){
fmt.Printf("开始执行工具:%s\n",input.Name)
out,err:=next(ctx,input)
fmt.Printf("工具执行完毕 err=%v\n",err)
return out,err
}
},
},
},五、新手避坑清单
❌ 传入ToolsNode的消息没有ToolCalls:不会报错,直接返回空切片,工具不会执行;
❌ ToolCall里面工具Name大小写和注册工具不一致,直接进入UnknownToolsHandler兜底;
❌ 拼接上下文的时候,漏掉模型输出的assistant(ToolCall)消息,只塞ToolMessage,模型无法理解工具返回;
❌ 误以为ToolsNode会做意图判断:ToolsNode不会自己去调用工具,必须上游传入携带ToolCall的消息;
❌ 多工具并行执行时,业务函数不要读写共享变量,注意并发安全。
💡小提示:后面学习
adk.ChatModelAgentConfig.ToolsConfig,底层就是对ToolsNodeConfig做封装,Agent自动完成整套ReAct循环。
六、知识点回顾
- 1.
ToolsNode是Eino的工具调度执行器,接收携带ToolCall的assistant消息,自动完成工具匹配、执行、结果封装; - 2. 分清三者关系:ToolInfo(说明书)、InvokableTool(可执行业务工具)、ToolsNode(调度操作台);
- 3. 核心方法
Invoke()输入一条assistant消息,输出一组tool角色消息; - 4. 支持串行/并行执行、不存在工具兜底、参数预处理、工具中间件扩展;
- 5. 标准ReAct闭环:ChatModel生成ToolCall → ToolsNode执行工具 → 拼接完整上下文,再次调用ChatModel生成最终答案。
MiaoAll