模型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语言

相关推荐
程序员侠客行4 分钟前
Mybatis连接池实现及池化模式
java·后端·架构·mybatis
Honmaple9 分钟前
QMD (Quarto Markdown) 搭建与使用指南
后端
PP东28 分钟前
Flowable学习(二)——Flowable概念学习
java·后端·学习·flowable
invicinble37 分钟前
springboot的核心实现机制原理
java·spring boot·后端
全栈老石1 小时前
Python 异步生存手册:给被 JS async/await 宠坏的全栈工程师
后端·python
space62123271 小时前
在SpringBoot项目中集成MongoDB
spring boot·后端·mongodb
Tony Bai2 小时前
再见,丑陋的 container/heap!Go 泛型堆 heap/v2 提案解析
开发语言·后端·golang
寻找奶酪的mouse2 小时前
30岁技术人对职业和生活的思考
前端·后端·年终总结
梦想很大很大2 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
毅炼3 小时前
Java 基础常见问题总结(4)
java·后端