第八篇:ToolUse模型实战——调用外部工具扩展Agent能力

2026-04-02 42 0

在前文ReAct、Planning、Memory Agent中,我们仅用了「计算器」这一个内置工具,而真实场景下的Agent需要调用外部工具(天气API、课程查询、支付接口等)突破能力边界——这就是ToolUse模型的核心价值:让LLM通过Function Calling调用外部工具,从“文本生成器”变成“能落地做事的智能体”。

本篇彻底讲清楚ToolUse的4个核心问题:

  1. 1. 模型“调用”工具到底是什么意思?
  2. 2. 如何把Go函数(工具)正确暴露给模型?
  3. 3. 为什么工具描述直接决定Agent稳定性?
  4. 4. 工具太多模型乱选怎么办?

一、核心误区:模型不会“真的”执行工具

很多人对Function Calling的第一认知是“模型自己调API”——这是典型误解!

1. 本质:模型是“决策者”,程序是“执行者”

LLM本质是文本生成器:它不会直接执行四则运算、查天气,只会输出一段结构化的JSON(比如{"name":"get_weather","arguments":{"city":"北京"}});而真正去运行calculatorgetWeather函数的,是Go程序。

2. 通俗比喻

把模型比作“被关在隔音房的顾问”,把Go程序比作“跑腿的助理”:

  • • 你(用户):“北京今天天气怎么样?”(对应Demo中agent1的UserPrompt);
  • • 你告诉顾问:“你可以用get_weather工具查天气,用calculator工具算数学题”;
  • • 顾问(模型):判断需要查天气,输出工具调用指令(JSON);
  • • 助理(Go程序):通过executeTool函数解析指令,调用getWeather函数拿到模拟天气数据;
  • • 助理把结果递回给顾问,顾问生成最终回答。

整个过程的核心分工:
模型:决定 “调不调工具、调哪个、传什么参数”;
程序:执行工具调用、返回结果 —— 各做各擅长的事。

// selectTool:模型仅做“决策”——选工具、生成参数
func selectTool(client *openai.Client, userMsg string) (openai.ChatCompletionMessage, error)

// executeTool:程序做“执行”——解析参数、运行工具函数
func executeTool(toolCall openai.ToolCall) string

二、工具暴露给模型的核心:四要素

把Go函数(calculator/getWeather)变成模型能“看懂并调用”的形式,本质是给模型写一份“工具说明书”,这份说明书必须包含四要素(缺一不可):

要素给谁看核心作用(结合Demo)
Name(工具名)模型+程序模型用它指定“调哪个工具”,程序用它做“执行分发”(Demo中calculator/get_weather是唯一标识)
Description(描述)模型模型靠它判断“这个工具什么时候该用”(Demo中“用户问数学计算时用calculator”)
Parameters Schema(参数Schema)模型+程序约束模型传参格式(Demo中city限定为北京/上海等枚举值),程序用它做参数校验
Result(结果)模型工具执行结果返回给模型(Demo中getWeather返回的“北京28℃,晴天”)

Demo中的工具四要素实现(toolList)

var toolList = []openai.Tool{
    {
        Type: openai.ToolTypeFunction,
        Function: &openai.FunctionDefinition{
            Name:        "calculator", // Name要素:唯一标识
            Description: "四则运算计算器,用户问数学计算时使用", // Description要素:明确使用场景
            Parameters: map[string]interface{}{ // Parameters Schema要素
                "type": "object",
                "properties": map[string]interface{}{
                    "expression": map[string]interface{}{
                        "type":        "string",        // 限定参数类型
                        "description": "数学表达式,格式:数字 运算符 数字(如 150 + 250)", // 解释语义+例子
                        "required":    true,            // 声明必填参数
                    },
                },
                "required": []string{"expression"},
            },
        },
    },
    {
        Type: openai.ToolTypeFunction,
        Function: &openai.FunctionDefinition{
            Name:        "get_weather", // Name要素
            Description: "天气查询,用户问某城市天气时使用", // Description要素
            Parameters: map[string]interface{}{ // Parameters Schema要素
                "type": "object",
                "properties": map[string]interface{}{
                    "city": map[string]interface{}{
                        "type":        "string",        // 限定类型
                        "enum":        []string{"北京", "上海", "广州", "深圳"}, // 限定可选值
                        "description": "城市名称",        // 解释语义
                        "required":    true,            // 必填
                    },
                },
                "required": []string{"city"},
            },
        },
    },
}

三、关键:工具描述写得好不好,直接决定Agent稳定性

假如你工具描述是“四则运算计算器,用户问数学计算时使用”——这是基础版写法,我们通过对比能更清晰看到描述质量对Agent的影响:

描述写法问题/效果(结合Demo)
“计算器”太模糊!模型可能在用户问天气时调用calculator,导致Agent行为混乱
“四则运算计算器,用户问数学计算时使用”基础版:能区分核心场景,适配2个工具的简单场景,但未明确“不适用场景”
“四则运算计算器,支持加减乘除;适用场景:用户问“X+Y等于多少”等数学计算问题;不适用场景:用户问天气、课程等非计算问题”工业版:模型能100%精准判断,即使工具数量增加到5个也不会乱选

写好Description的3个技巧

  1. 1. 做什么:明确工具功能(如“四则运算计算器,支持加减乘除”);
  2. 2. 什么时候用:列举典型场景(如“用户问150+250等于多少时使用”);
  3. 3. 什么时候不用:列举排除场景(如“用户问天气时不使用”)。

四、参数Schema:让模型“传对参数”的关键

Demo中getWeather函数里,模型很容易传“帝都”“Beijing”等非标准值——参数Schema就是给模型定“传参规矩”,核心要包含5个属性(Demo已实现核心属性):

Schema属性作用示例(weather工具city参数)
required标记必填项(少了模型会补)city设为true:必须传城市,否则模型追问用户
enum限定值范围(避免乱传)限定["北京","上海","广州","深圳"]
type限定参数类型(避免类型错误)设为string:避免模型传数字110代表北京
description解释语义+例子(降低传错概率)“城市名称,示例:北京”
default未传时的默认值(提升容错)可补充default: "北京",Demo暂未实现

Demo中的参数校验(程序端兜底)

即使Schema做了约束,仍需在Go程序中二次校验:

func getWeather(city string) string {
    weatherData := map[string]string{
        "北京": "28℃,晴天,适合出门",
        "上海": "30℃,多云,注意防晒",
        "广州": "32℃,雷阵雨,带伞",
        "深圳": "31℃,阴天,舒适",
    }
    w, ok := weatherData[city]
    if !ok {
        return fmt.Sprintf("不支持的城市:%s(仅支持北京/上海/广州/深圳)", city)
    }
    return fmt.Sprintf("%s今天:%s", city, w)
}

五、多工具混乱?解决办法:控制数量+分层选择

Demo只有2个工具,模型不会乱选,但工具数量增加后会出现决策混乱:

工具数量模型状态
3~5个初版Agent的“舒适区”,稳定性最高
10个以内描述写得好还能稳定
10个以上准确率明显下降,必须做分层处理

工具超过10个的3种应对策略

1. 工具分组(最简单)

把工具按“领域”分组(如“计算类”“查询类”),让模型先选“组”,再选“具体工具”:

// 工具分组
var toolGroups = map[string][]openai.Tool{
    "计算类": {toolList[0]}, // calculator
    "查询类": {toolList[1]}, // get_weather
}

// 第一步:让模型先选组(
func selectToolGroup(client *openai.Client, userMsg string) (string, error) {
    systemPrompt := `仅返回工具组名:计算类/查询类。
规则:1. 数学计算→计算类;2. 天气查询→查询类;3. 只返回组名。`
    // 调用LLM逻辑(略)
}

2. 动态工具检索(最常用)

把工具描述向量化,用户提问时只传Top-3相关工具给模型,减少选择压力。

3. 多Agent架构(工业级)

每个子Agent只负责一组工具,由“总管Agent”分发任务。


六、完整ToolUse模型实战代码

以下是可直接运行的Demo代码:

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "os"
    "strconv"
    "strings"
    "time"

    "github.com/sashabaranov/go-openai"
)

// ==================== 1. 工具执行逻辑 ====================

// calculator 四则运算:解析「数字 运算符 数字」格式的表达式。
func calculator(expression string) string {
    parts := strings.Fields(expression) // 按空白分词,不受空格数量影响
    if len(parts) != 3 {
        return fmt.Sprintf("表达式格式错误(应为「数字 运算符 数字」):%s", expression)
    }
    num1, err1 := strconv.ParseFloat(parts[0], 64)
    num2, err2 := strconv.ParseFloat(parts[2], 64)
    if err1 != nil || err2 != nil {
        return fmt.Sprintf("数字解析失败:%s", expression)
    }
    var res float64
    switch parts[1] {
    case "+":
        res = num1 + num2
    case "-":
        res = num1 - num2
    case "*":
        res = num1 * num2
    case "/":
        if num2 == 0 {
            return "错误:除数不能为 0"
        }
        res = num1 / num2
    default:
        return fmt.Sprintf("不支持运算符:%s", parts[1])
    }
    return fmt.Sprintf("%.2f %s %.2f = %.2f", num1, parts[1], num2, res)
}

// getWeather 天气查询(模拟数据)。
func getWeather(city string) string {
    weatherData := map[string]string{
        "北京": "28℃,晴天,适合出门",
        "上海": "30℃,多云,注意防晒",
        "广州": "32℃,雷阵雨,带伞",
        "深圳": "31℃,阴天,舒适",
    }
    w, ok := weatherData[city]
    if !ok {
        return fmt.Sprintf("不支持的城市:%s(仅支持北京/上海/广州/深圳)", city)
    }
    return fmt.Sprintf("%s今天:%s", city, w)
}

// ==================== 2. 工具定义(LLM 可选工具清单)====================

var toolList = []openai.Tool{
    {
        Type: openai.ToolTypeFunction,
        Function: &openai.FunctionDefinition{
            Name:        "calculator",
            Description: "四则运算计算器,用户问数学计算时使用",
            Parameters: map[string]interface{}{
                "type": "object",
                "properties": map[string]interface{}{
                    "expression": map[string]interface{}{
                        "type":        "string",
                        "description": "数学表达式,格式:数字 运算符 数字(如 150 + 250)",
                    },
                },
                "required": []string{"expression"},
            },
        },
    },
    {
        Type: openai.ToolTypeFunction,
        Function: &openai.FunctionDefinition{
            Name:        "get_weather",
            Description: "天气查询,用户问某城市天气时使用",
            Parameters: map[string]interface{}{
                "type": "object",
                "properties": map[string]interface{}{
                    "city": map[string]interface{}{
                        "type":        "string",
                        "enum":        []string{"北京", "上海", "广州", "深圳"},
                        "description": "城市名称",
                    },
                },
                "required": []string{"city"},
            },
        },
    },
}

// ==================== 3. 工具执行分发 ====================

// executeTool 根据工具名解析参数并执行对应工具。
func executeTool(toolCall openai.ToolCall) string {
    switch toolCall.Function.Name {
    case "calculator":
        var p struct {
            Expression string `json:"expression"`
        }
        if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &p); err != nil {
            return fmt.Sprintf("参数解析失败:%v", err)
        }
        return calculator(p.Expression)
    case "get_weather":
        var p struct {
            City string `json:"city"`
        }
        if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &p); err != nil {
            return fmt.Sprintf("参数解析失败:%v", err)
        }
        return getWeather(p.City)
    default:
        return fmt.Sprintf("未知工具:%s", toolCall.Function.Name)
    }
}

// ==================== 4. ToolUse Agent ====================

// ToolUseAgent 工具使用 Agent:选工具 → 执行 → 返回结果。
type ToolUseAgent struct {
    UserPrompt   string // 用户指令
    SelectedTool string // 选中的工具名
    ToolResult   string // 工具执行结果
    TotalLatency int64  // 总耗时(毫秒)
}

// newClient 创建 DeepSeek 客户端。
func newClient() (*openai.Client, error) {
    apikey := os.Getenv("DEEPSEEK_API_KEY")
    if apikey == "" {
        return nil, fmt.Errorf("DEEPSEEK_API_KEY 环境变量未设置")
    }
    cfg := openai.DefaultConfig(apikey)
    cfg.BaseURL = "https://api.deepseek.com"
    return openai.NewClientWithConfig(cfg), nil
}

// selectTool 调 LLM 从工具清单中选择合适工具并生成参数。
func selectTool(client *openai.Client, userMsg string) (openai.ChatCompletionMessage, error) {
    resp, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
        Model: "deepseek-v4-flash", // DeepSeek 模型,若不存在可改 deepseek-chat
        Messages: []openai.ChatCompletionMessage{
            {Role: openai.ChatMessageRoleSystem, Content: "你是 ToolUse Agent,根据用户问题选择合适的工具并生成参数。"},
            {Role: openai.ChatMessageRoleUser, Content: userMsg},
        },
        Tools:       toolList,
        Temperature: 0.1,
    })
    if err != nil {
        return openai.ChatCompletionMessage{}, fmt.Errorf("LLM 调用失败:%w", err)
    }
    if len(resp.Choices) == 0 {
        return openai.ChatCompletionMessage{}, fmt.Errorf("LLM 未返回结果")
    }
    return resp.Choices[0].Message, nil
}

// Run 执行完整流程:选工具 → 执行工具 → 返回结果。
func (a *ToolUseAgent) Run() {
    fmt.Printf("\n===== ToolUse Agent =====\n用户指令:%s\n", a.UserPrompt)

    client, err := newClient()
    if err != nil {
        a.ToolResult = err.Error()
        return
    }
    start := time.Now()

    // 步骤1:LLM 选工具并生成参数
    msg, err := selectTool(client, a.UserPrompt)
    if err != nil {
        a.ToolResult = err.Error()
        return
    }
    if len(msg.ToolCalls) == 0 {
        a.ToolResult = "模型未选择任何工具"
        return
    }
    a.SelectedTool = msg.ToolCalls[0].Function.Name
    fmt.Printf("🔧 选中工具:%s\n", a.SelectedTool)

    // 步骤2:执行工具
    a.ToolResult = executeTool(msg.ToolCalls[0])
    fmt.Printf("📊 执行结果:%s\n", a.ToolResult)

    a.TotalLatency = time.Since(start).Milliseconds()
}

// ==================== 5. 主函数:测试 ToolUse Agent ====================

func main() {
    // 测试1:天气查询(应选 get_weather)
    agent1 := &ToolUseAgent{UserPrompt: "北京今天天气怎么样?"}
    agent1.Run()
    fmt.Printf("✅ 最终答案:%s(耗时 %dms)\n", agent1.ToolResult, agent1.TotalLatency)

    // 测试2:数学计算(应选 calculator)
    agent2 := &ToolUseAgent{UserPrompt: "150 + 250等于多少?"}
    agent2.Run()
    fmt.Printf("✅ 最终答案:%s(耗时 %dms)\n", agent2.ToolResult, agent2.TotalLatency)

    // 测试3:天气查询(没工具选择)
    agent3 := &ToolUseAgent{UserPrompt: "杭州今天天气怎么样??"}
    agent3.Run()
    fmt.Printf("✅ 最终答案:%s(耗时 %dms)\n", agent3.ToolResult, agent3.TotalLatency) // 测试3:天气(没工具选择)
    
}

代码运行结果(真实执行输出)

===== ToolUse Agent =====
用户指令:北京今天天气怎么样?
🔧 选中工具:get_weather
📊 执行结果:北京今天:28℃,晴天,适合出门
✅ 最终答案:北京今天:28℃,晴天,适合出门(耗时 1191ms)

===== ToolUse Agent =====
用户指令:150 + 250等于多少?
🔧 选中工具:calculator
📊 执行结果:150.00 + 250.00 = 400.00
✅ 最终答案:150.00 + 250.00 = 400.00(耗时 899ms)

===== ToolUse Agent =====
用户指令:杭州今天天气怎么样??
✅ 最终答案:模型未选择任何工具(耗时 0ms)

七、工具的安全边界:避免“致命调用”

但工具越多风险越高——若工具能执行rm -rf /、转账等操作,模型一旦选错参数,后果不可逆。新手落地时要做好3层防护:

  1. 1. 权限控制:工具执行逻辑仅赋予最小权限(如getWeather仅能读取模拟数据,无文件操作权限);
  2. 2. 参数白名单:像Demo中city参数用enum限定可选值,避免传危险值;
  3. 3. 执行前校验:所有工具调用前,程序先校验参数(如Demo中getWeather检查城市是否在白名单)。

八、核心总结

1. ToolUse模型的核心逻辑

  • • 模型是“决策者”(通过selectTool选工具、传参数),程序是“执行者”(通过executeTool调工具、返结果);
  • • 工具暴露给模型的核心是“四要素”(Name/Description/Parameters Schema/Result);
  • • 好的Description(明确适用/不适用场景)是Agent稳定的关键。

2. 多工具管理的核心技巧

  • • 工具数控制在3~5个(初版),超过10个做分组/动态检索/多Agent;
  • • 参数Schema要加required/enum/type,且程序端必须二次校验。

3. 落地关键

  • • 可先扩展“工具分组”,再尝试动态工具检索;
  • • 始终做好参数校验和权限控制,避免安全风险。

ToolUse模型是Agent从“能思考”到“能做事”的核心桥梁——我们能快速落地基础工具调用能力,后续可扩展至链式工具调用、多轮工具交互等高级场景。

相关文章

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

发布评论