模型Function Call

工具调用(Function Calling)是一种将大模型与外部工具和 API 相连的关键功能,作为自然语言与信息接口之间的"翻译官",它能够将用户的自然语言请求智能地转化为对特定工具或 API 的调用,从而高效满足用户的特定需求。

简单来说,大模型的知识是固定的,你问他李白有哪些诗它肯定知道,但是你问它现在北京天气怎么样,它就不知道了。但大模型有推理能力,如果你问它北京天气怎么样的同时,还告诉他有哪些API可以使用,它就会选择对应的API。这样我们可以用工程手段调用这些API,然后将API结果再给大模型,由大模型进行总结了。

前置操作

首先我们得开通个大模型,这里我用火山的模型来演示。吐槽一句,我感觉各家的云平台操作起来都听不顺畅的,找个内容得四处翻。

登录:www.volcengine.com/

进入控制台:console.volcengine.com/ark/region:...

开通模型:我选择开通的是豆包-1.5-pro-32k

查看模型细节:

获取API key和使用demo,大家shell里执行export ARK_API_KEY="**"就能执行我下面的代码了

代码

看代码前,希望大家能先看一下这两个文档

  1. Chat接口的入参和出参:www.volcengine.com/docs/82379/...
  2. Function Call的调用方式:www.volcengine.com/docs/82379/... ,这个文档主要使用的是非流式方式,而且Go的demo很少,强烈建议加个流式的demo
go 复制代码
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"

	"github.com/volcengine/volcengine-go-sdk/service/arkruntime"
	"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
	"github.com/volcengine/volcengine-go-sdk/volcengine"
)

type WeatherArgs struct {
	Location string `json:"location"`
	Unit     string `json:"unit,omitempty"` // omitempty 允许 unit 为可选
}

// 目标工具
func getCurrentWeather(location string, unit string) string {
	if unit == "" {
		unit = "摄氏度" // 默认单位
	}
	// 此处为示例,返回模拟的天气数据
	return fmt.Sprintf("%s今天天气晴朗,温度 25 %s。", location, unit)
}

func MarshStr(data interface{}) string {
	d, _ := json.Marshal(data)
	return string(d)
}

func main() {
	// 从环境变量中获取 API Key,请确保已设置 ARK_API_KEY
	apiKey := os.Getenv("ARK_API_KEY")
	if apiKey == "" {
		fmt.Println("错误:请设置 ARK_API_KEY 环境变量。")
		return
	}

	client := arkruntime.NewClientWithApiKey(apiKey)
	ctx := context.Background()

	// 初始化消息列表
	messages := []*model.ChatCompletionMessage{
		{
			Role: model.ChatMessageRoleSystem,
			Content: &model.ChatCompletionMessageContent{
				StringValue: volcengine.String("你是豆包,是由字节跳动开发的 AI 人工智能助手"),
			},
		},
		{
			Role: model.ChatMessageRoleUser,
			Content: &model.ChatCompletionMessageContent{
				StringValue: volcengine.String("先帮我查看一下北京的天气,然后再查看一下上海的天气"),
			},
		},
	}

	// 步骤 1: 定义工具
	tools := []*model.Tool{
		{
			Type: model.ToolTypeFunction,
			Function: &model.FunctionDefinition{
				Name:        "get_current_weather",
				Description: "获取指定地点的天气信息",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"location": map[string]interface{}{
							"type":        "string",
							"description": "地点的位置信息,例如北京、上海",
						},
						"unit": map[string]interface{}{
							"type": "string",
							"enum": []string{
								"摄氏度",
								"华氏度",
							},
							"description": "温度单位",
						},
					},
					"required": []string{"location"},
				},
			},
		},
	}
	count := 0
	for {
		if count > 5 {
			return
		}
		count++
		// 步骤 2: 发起流式模型请求
		req := model.CreateChatCompletionRequest{
			Model:    "doubao-1-5-pro-32k-250115",
			Messages: messages,
			Tools:    tools,
			Stream:   volcengine.Bool(true),
		}

		fmt.Println("----- streaming request -----")
		stream, err := client.CreateChatCompletionStream(ctx, req)
		//fmt.Print(2)
		if err != nil {
			fmt.Printf("stream chat error: %v\n", err)
			return
		}
		defer stream.Close()
		content := ""
		toolName := ""
		toolArgs := ""
		toolCallId := ""
		//fmt.Print(11)
		for {
			recv, err := stream.Recv()
			if err == io.EOF {
				break
			}
			if err != nil {
				fmt.Printf("Stream chat error: %v\n", err)
				return
			}

			if len(recv.Choices) > 0 {
				respMsg := recv.Choices[0].Delta
				// data, _ := json.Marshal(recv.Choices[0])
				// fmt.Println(string(data))

				// fmt.Print("content: "+recv.Choices[0].Delta.Content, "finishreason: "+recv.Choices[0].FinishReason, "\n")

				//fmt.Println(recv.Choices[0].Delta.ToolCalls[0], recv.Choices[0].Delta.Content)
				//展示模型中间过程的回复内容
				if respMsg.Content != "" {
					content += respMsg.Content
					fmt.Print(respMsg.Content)
				}

				//处理工具信息
				if len(recv.Choices[0].Delta.ToolCalls) > 0 {
					toolCall := recv.Choices[0].Delta.ToolCalls[0]
					if toolCall.Function.Name != "" {
						toolName = toolCall.Function.Name
					}
					if toolCall.Function.Arguments != "" {
						toolArgs += toolCall.Function.Arguments
					}
					if toolCall.ID != "" {
						toolCallId = toolCall.ID
					}
				}
				//处理非fc的结束
				if recv.Choices[0].FinishReason != "" && recv.Choices[0].FinishReason != model.FinishReasonToolCalls {
					messages = append(messages, &model.ChatCompletionMessage{
						Role: model.ChatMessageRoleAssistant,
						Content: &model.ChatCompletionMessageContent{
							StringValue: volcengine.String(content),
						},
					},
					)
					fmt.Println("正常finish", MarshStr(messages))
					return //真正结束
				}
				//处理fc的结束
				if recv.Choices[0].FinishReason == model.FinishReasonToolCalls && len(toolName) > 0 {
					// 将模型的回复(包含工具调用请求)添加到消息历史中
					messages = append(messages, &model.ChatCompletionMessage{
						Role: model.ChatMessageRoleAssistant,
						Content: &model.ChatCompletionMessageContent{
							StringValue: volcengine.String(content),
						},
						ToolCalls: []*model.ToolCall{
							{
								ID: toolCallId,
								Function: model.FunctionCall{
									Name:      toolName,
									Arguments: toolArgs,
								},
								Type: model.ToolTypeFunction,
							},
						},
					})

					fmt.Printf("\n模型尝试调用工具: %s, ID: %s", toolName, toolCallId)
					fmt.Println("  参数:", toolArgs)

					var toolResult string
					if toolName == "get_current_weather" {
						// 调用外部工具
						var args WeatherArgs
						err := json.Unmarshal([]byte(toolArgs), &args)
						if err != nil {
							fmt.Printf("解析工具参数错误 (%s): %v", toolName, err)
							toolResult = fmt.Sprintf("解析参数失败: %v", err)
						} else {
							toolResult = getCurrentWeather(args.Location, args.Unit)
							fmt.Println("  工具执行结果:", toolResult)
						}

						// 回填工具结果
						messages = append(messages, &model.ChatCompletionMessage{
							Role:       model.ChatMessageRoleTool,
							Content:    &model.ChatCompletionMessageContent{StringValue: volcengine.String(toolResult)},
							ToolCallID: toolCallId,
							Name:       &toolName,
						})
					}

					fmt.Println("\n--- 下一轮对话 ---", MarshStr(messages))
					break
				}
			}
		}
		//break
	}
}

执行结果如下:

swift 复制代码
➜  my go run main.go
----- streaming request -----
用户需要查询北京和上海的天气,调用 get_current_weather 函数分别获取两地的天气信息。
模型尝试调用工具: get_current_weather, ID: call_pjov7bifbrdpv7e8kncsmhjz  参数:  {
        "location": "北京",
        "unit": "摄氏度"
    }
  工具执行结果: 北京今天天气晴朗,温度 25 摄氏度。

--- 下一轮对话 --- [{"role":"system","content":"你是豆包,是由字节跳动开发的 AI 人工智能助手","name":null},{"role":"user","content":"先帮我查看一下北京的天气,然后再查看一下上海的天气","name":null},{"role":"assistant","content":"用户需要查询北京和上海的天气,调用 get_current_weather 函数分别获取两地的天气信息。","name":null,"tool_calls":[{"id":"call_pjov7bifbrdpv7e8kncsmhjz","type":"function","function":{"name":"get_current_weather","arguments":" {\n        \"location\": \"北京\",\n        \"unit\": \"摄氏度\"\n    }"}}]},{"role":"tool","content":"北京今天天气晴朗,温度 25 摄氏度。","name":"get_current_weather","tool_call_id":"call_pjov7bifbrdpv7e8kncsmhjz"}]
----- streaming request -----

模型尝试调用工具: get_current_weather, ID: call_0qfxc8xe5h6m74pf9olwptao  参数:  {"location": "上海", "unit": "摄氏度"}
  工具执行结果: 上海今天天气晴朗,温度 25 摄氏度。

--- 下一轮对话 --- [{"role":"system","content":"你是豆包,是由字节跳动开发的 AI 人工智能助手","name":null},{"role":"user","content":"先帮我查看一下北京的天气,然后再查看一下上海的天气","name":null},{"role":"assistant","content":"用户需要查询北京和上海的天气,调用 get_current_weather 函数分别获取两地的天气信息。","name":null,"tool_calls":[{"id":"call_pjov7bifbrdpv7e8kncsmhjz","type":"function","function":{"name":"get_current_weather","arguments":" {\n        \"location\": \"北京\",\n        \"unit\": \"摄氏度\"\n    }"}}]},{"role":"tool","content":"北京今天天气晴朗,温度 25 摄氏度。","name":"get_current_weather","tool_call_id":"call_pjov7bifbrdpv7e8kncsmhjz"},{"role":"assistant","content":"","name":null,"tool_calls":[{"id":"call_0qfxc8xe5h6m74pf9olwptao","type":"function","function":{"name":"get_current_weather","arguments":" {\"location\": \"上海\", \"unit\": \"摄氏度\"}"}}]},{"role":"tool","content":"上海今天天气晴朗,温度 25 摄氏度。","name":"get_current_weather","tool_call_id":"call_0qfxc8xe5h6m74pf9olwptao"}]
----- streaming request -----
北京今天天气晴朗,温度 25 摄氏度。上海今天天气晴朗,温度 25 摄氏度。 正常finish [{"role":"system","content":"你是豆包,是由字节跳动开发的 AI 人工智能助手","name":null},{"role":"user","content":"先帮我查看一下北京的天气,然后再查看一下上海的天气","name":null},{"role":"assistant","content":"用户需要查询北京和上海的天气,调用 get_current_weather 函数分别获取两地的天气信息。","name":null,"tool_calls":[{"id":"call_pjov7bifbrdpv7e8kncsmhjz","type":"function","function":{"name":"get_current_weather","arguments":" {\n        \"location\": \"北京\",\n        \"unit\": \"摄氏度\"\n    }"}}]},{"role":"tool","content":"北京今天天气晴朗,温度 25 摄氏度。","name":"get_current_weather","tool_call_id":"call_pjov7bifbrdpv7e8kncsmhjz"},{"role":"assistant","content":"","name":null,"tool_calls":[{"id":"call_0qfxc8xe5h6m74pf9olwptao","type":"function","function":{"name":"get_current_weather","arguments":" {\"location\": \"上海\", \"unit\": \"摄氏度\"}"}}]},{"role":"tool","content":"上海今天天气晴朗,温度 25 摄氏度。","name":"get_current_weather","tool_call_id":"call_0qfxc8xe5h6m74pf9olwptao"},{"role":"assistant","content":"北京今天天气晴朗,温度 25 摄氏度。上海今天天气晴朗,温度 25 摄氏度。 ","name":null}]

我的问题是:先帮我查看一下北京的天气,然后再查看一下上海的天气。

我同时告诉模型,有获取天气的API-get_current_weather可以调用。

第一次:信息给到模型后,模型认为需要分别获取两地的天气,所以先返回了查询北京的参数,并决定调用get_current_weather进行查询。

{ "location" : "北京" , "unit" : "摄氏度" }

我代码里收到是调用工具,开始调用API,并将结果添加到msg中。

第二次:带着上次的信息给模型后,模型推理要查上海的天气了,同理我完成了一次API调用

第三次:带着两个地区的天气信息我给模型,模型认为无需调用工具,正常结束,这次会话就完全完成了。

我们看一下最终的消息体:

json 复制代码
[
    {
        "role": "system",
        "content": "你是豆包,是由字节跳动开发的 AI 人工智能助手",
        "name": null
    },
    {
        "role": "user",
        "content": "先帮我查看一下北京的天气,然后再查看一下上海的天气",
        "name": null
    },
    {
        "role": "assistant",
        "content": "用户需要查询北京和上海的天气,调用 get_current_weather 函数分别获取两地的天气信息。",
        "name": null,
        "tool_calls": [
            {
                "id": "call_pjov7bifbrdpv7e8kncsmhjz",
                "type": "function",
                "function": {
                    "name": "get_current_weather",
                    "arguments": " {\n        \"location\": \"北京\",\n        \"unit\": \"摄氏度\"\n    }"
                }
            }
        ]
    },
    {
        "role": "tool",
        "content": "北京今天天气晴朗,温度 25 摄氏度。",
        "name": "get_current_weather",
        "tool_call_id": "call_pjov7bifbrdpv7e8kncsmhjz"
    },
    {
        "role": "assistant",
        "content": "",
        "name": null,
        "tool_calls": [
            {
                "id": "call_0qfxc8xe5h6m74pf9olwptao",
                "type": "function",
                "function": {
                    "name": "get_current_weather",
                    "arguments": " {\"location\": \"上海\", \"unit\": \"摄氏度\"}"
                }
            }
        ]
    },
    {
        "role": "tool",
        "content": "上海今天天气晴朗,温度 25 摄氏度。",
        "name": "get_current_weather",
        "tool_call_id": "call_0qfxc8xe5h6m74pf9olwptao"
    },
    {
        "role": "assistant",
        "content": "北京今天天气晴朗,温度 25 摄氏度。上海今天天气晴朗,温度 25 摄氏度。 ",
        "name": null
    }
]

FC在我眼里就像是人的四肢,模型是大脑,只有这样才能发挥更大的作用。如果加上硬件,那就是配备了钛合金盔甲。

疑问

流式调用是为了能够尽快将模型返回内容返回给前端,如果是最终结果肯定没什么问题,但是如果是fc的呢?有办法即使用流式方案,同时只把tool相关的内容返回,content的内容不返回吗?

ruby 复制代码
➜  my go run main.go
----- streaming request -----
{"index":0,"delta":{"role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"用户","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"想","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"了解","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"北京","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"和","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"上海","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"的","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"天气","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":",","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"调用","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":" get","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"_current","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"_","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"weather","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":" ","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"函数","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"获取","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"北京","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"和","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"上海","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"的","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"天气","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"信息","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"content":"。","role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"role":"assistant"},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"call_d17ewqycgbxdfj65zqycj3bu","type":"function","function":{"name":"get_current_weather"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":" {"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"\n"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"       "},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":" \""},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"location"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"\":"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":" \""},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"北京"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":","},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"上海"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"\","},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"\n"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"       "},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":" \""},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"unit"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"\":"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":" \""},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"摄氏度"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"\""},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"\n"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":"   "},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{"arguments":" }"},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"","type":"","function":{},"index":0}]},"finish_reason":null}
{"index":0,"delta":{"role":"assistant"},"finish_reason":"tool_calls"}

资料

www.volcengine.com/docs/82379/...

www.volcengine.com/docs/82379/...

www.volcengine.com/docs/82379/...

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:shidawuhen.github.io/

往期文章回顾:

  1. 设计模式

  2. 招聘

  3. 思考

  4. 存储

  5. 算法系列

  6. 读书笔记

  7. 小工具

  8. 架构

  9. 网络

  10. Go语言

相关推荐
love530love26 分钟前
是否需要预先安装 CUDA Toolkit?——按使用场景分级推荐及进阶说明
linux·运维·前端·人工智能·windows·后端·nlp
泯泷1 小时前
「译」为 Rust 及所有语言优化 WebAssembly
前端·后端·rust
梦想很大很大2 小时前
把业务逻辑写进数据库中:老办法的新思路(以 PostgreSQL 为例)
前端·后端·架构
Android洋芋2 小时前
GitHub开源协作实践:HelloGitHub项目详解与企业级应用实战
后端
雨果talk3 小时前
Spring Boot集成Mina的Socket资源管理:从稳定通信到高性能优化
spring boot·后端·性能优化
雨果talk3 小时前
【一文看懂多模块Bean初始化难题】Spring Boot多模块项目中的Bean初始化难题:包名不一致的优雅解决方案
java·spring boot·后端·spring·springboot
ZHOU_WUYI4 小时前
flask JWT 认证
后端·flask·jwt
小奏技术5 小时前
Jason Evans:jemalloc的开源20年回忆录
后端·开源
程序员爱钓鱼5 小时前
Go语言同步原语与数据竞争:数据竞争的检测工具
后端·google·go
汪子熙5 小时前
编写一个 Word Macro,调用 DeepSeek API
后端