你好,我是邢云阳。
自2023年3月 ChatGPT 在中国爆火以来,大模型已经悄然改变了许多人的提问方式,尤其是在互联网圈子里。从以前的“有问题,Google 一下”,到现在的“先问问大模型”,这种转变反映了技术对日常生活的深远影响,比如图中这位女士就将 ChatGPT 使用的淋漓尽致。

但是在使用过程中,我们会发现,有时大模型并不是万能的,它会一本正经的给出错误答案,业界把这种现象称之为“幻觉”。比如我问 ChatGPT-4o 一个它肯定不会的问题。

我们会发现,大模型给出了看似正确实则“废话”的答案。
再比如,我问一道小学一二年级的数学题:

我们很容易知道1+2+3+4-5-6=-1,但大模型给我们的答案是0。
“幻觉”出现的原因其实很简单。我们知道作为人类来说,即使是才高八斗,学富五车,也不可能什么都懂,于是就有这样一种人,为了面子,在遇到不会的问题时,也要强行给出一个模模糊糊的答案,我们称之为不懂装懂。
同样,作为大模型,训练数据是有限的,特别是对于一些垂直领域以及实时性的问题,例如附近哪有加油站?今天的茅台股票多少钱一股?大模型是无法给出正确的回答的。那大模型为什么也处理不了小学数学题呢?这是因为大模型的训练方法是通过学习语言的结构和模式,使得其能够生成与人类语言相似的文本,而不是针对数学问题这种精确逻辑做的训练,因此它的数学能力很弱。
我们应如何解决这类问题呢?OpenAI 公司为了能让大模型与外界进行交互,发明了 Function Calling 机制,即可以在向大模型提问时,给大模型提供一些工具(函数),由大模型根据需要,自行选择合适的工具,从而解决问题。
接下来,我将使用Go语言,为你演示一下 Function Calling 功能,我们就以加法减法工具为例,让大模型通过工具来进行数学运算。
代码实战前置工作
环境准备
- 运行环境:Windows/Linux
- Go版本:1.19
- LLM:阿里云 qwen-turbo
- SDK:go-openai v1.32.0
考虑到使用 OpenAI 的模型,需要科学上网,且购买 API 的调用额度,需要美国信用卡支付。因此我们选择国内模型作为替代演示。如果有同学想了解如何使用 OpenAI API ,可留言区留言评论,我们不在课程中展示。
通义千问大模型开通
阿里云通义千问提供了比较丰富的大模型产品供用户使用,且其请求方式是兼容 OpenAI SDK 的。本小节实战所使用的模型是通义千问中最便宜的 qwen-turbo 模型。如何开通服务,可参考官网教程开通DashScope并创建API-KEY_模型服务灵积(DashScope)-阿里云帮助中心 (aliyun.com) 。
代码实战演示
模型环境变量配置
当我们获取了通义千问大模型的 api_key 之后,为了保密和调用方便,可以将其配置到环境变量。
以 Windows 系统为例,我的电脑->右键属性->高级系统设置->环境变量,在系统变量中点击新建。

输入变量名和 api_key 的值即可。
接下来我们开始写初始化 OpenAI 客户端的代码。
首先将 go-openai sdk 下载下来。
1 | go get github.com/sashabaranov/go-openai |
之后,开始初始化一个 OpenAI 客户端,需要填充 token 和 baseurl 两项,用于客户端与大模型服务器的连接。
1 | func NewOpenAiClient() *openai.Client { |
由于通义千问大模型是兼容 OpenAI 的,因此可以使用 OpenAI SDK 初始化大模型客户端。使用 os 包从环境变量中获取 api_key。
Chat Completions
我们在演示工具选择之前,首先需要把和大模型对话的基础代码写好。这就要用到 Chat Completions。Chat Completions 是 OpenAI SDK 提供的一次性对话的方法,我们使用它可以完成和大模型的对话。
在与大模型的对话过程中,会有三种基础角色,用来让大模型清楚某句话是谁说的。
- system:系统角色,可以理解为全局变量或前置条件,设置上这个角色之后,就会规定大模型的聊天范围,业界通常称之为“人设”。
- user:人类角色,代表这句话是人类说的。在包括 LangChain 在内的很多框架和场景下,user 角色也会被写成 human。
- assistant:AI角色,代表这句话是大模型给我们的返回。在包括 LangChain 在内的很多框架和场景下,assistant 角色也会被写成 AI。
我举一个例子,演示一下使用以上三种角色完成一次 Chat Completions。
1 | system: 你是一个足球领域的专家,请尽可能地帮我回答与足球相关的问题。 |
如果想要实现多轮对话效果,则需要每一次都带着历史对话提问,例如:
1 | system: 你是一个足球领域的专家,请尽可能地帮我回答与足球相关的问题。 |
虽然最后一次人类的提问“内马尔呢”是一个模糊提问,但由于存在历史对话,因此大模型可以理解用户的提问的意思是“内马尔是哪个国家的足球运动员?”
理解了三种角色后,我们开始写代码。
1 | func Chat(message []openai.ChatCompletionMessage) openai.ChatCompletionMessage { |
在这个函数中,首先我获取了 OpenAI 客户端,之后通过客户端调用了 CreateChatCompletion 方法。该方式是完成一次与大模型的 Chat。该方法的返回值 rsp,便是大模型的回复。
在这次 Chat Compleetion 中,我使用的模型是 qwen-plus,message 即向大模型发送的消息。message 的类型是一个 openai.ChatCompletionMessage 切片。
openai.ChatCompletionMessage包含了多个字段,其中有两个基础字段为:
1 | Role string `json:"role"` |
role 代表角色,content 代表对话内容。
openai.ChatCompletionMessage为切片类型,是因为考虑到会有历史消息,因此我们需要构建一个历史消息存储器来存储历史对话。核心代码如下:
1 | var MessageStore ChatMessages |
在上述代码中,我们定义了历史消息存储器MessageStore,其本质是一个用于存放各类角色对话内容的 openai.ChatCompletionMessage 切片。并编写了 AddFor 方法,用于添加各类角色的对话内容,最后编写了 ToMessage 方法,用于取出存储器中的所有消息。
完成了以上编码后,我们可以测试一下和大模型对话的效果了。
1 | func main() { |
输出:
1 | 内马尔是巴西足球运动员。 |
在上面的例子中,大模型成功的根据对话历史,理解了“内马尔呢?”表达的真正含义。
Function Calling
由于 Function Calling 功能是 OpenAI 公司发明的,因此我们定义工具需要遵循 OpenAI SDK 的规范。规范如下:
1 | const ( |
规范还是很简单的,包含了工具类型 Type 和工具定义 Function 两个部分,其实工具类型是写死的 “fuction”。工具定义包含名称 Name、描述 Description 以及参数 Parameters 三个部分。
接下来我来定义一个加法工具,给你做一下演示。
1 | var AddToolDefine = openai.Tool{ |
在我的工具定义的 Description 部分,我不仅写清楚了 AddTool 工具的作用,还举了一个例子,这样可以让大模型更好地理解。在Parameters部分,我使用了标准的 json schema 方式编写了参数名称、类型等,这样也有助于大模型准确理解。
在定义好工具后,我们需要在向大模型提问时,带上工具,因此 Chat Completions 增加了两个参数,一个是 Tools,用于接收 []openai.tool 列表;另一个参数是ToolChoice,用于设置让大模型使用工具还是不使用工具,一般设置为 “auto”,意思是让大模型自己根据实际情况选择是否调用工具。修改后的 chat 函数代码如下:
1 | func ToolsChat(message []openai.ChatCompletionMessage, tools []openai.Tool) openai.ChatCompletionMessage { |
有了这些基础,我们就可以向大模型提问,测试工具选择了。我们让大模型计算一下”1+2=?”
1 | func main() { |
输出:
1 | 大模型的回复是: |
可以看到大模型选择了工具 AddTool,并将参数按照我们的要求拆解成了 [1, 2] 这样一个切片。只要大模型选择了工具,则其回复就会是空字符串。
测试到这里,我们可以初步理解所谓大模型“调用”工具的机制。其实就是将工具用文字描述清楚,并和问题一起发送给大模型,由大模型判断选择哪个工具能解决问题。因此其实 Function Calling 这个表述我个人感觉并不准确,或许叫 Function Selecting 会更加没有歧义。
这里我们可以得出两个结论:
- 工具的定义也是 prompt,也就是要消耗 token 的。
- 大模型只能选择使用工具!而不能调用工具!真正调用工具的仍然是人类!
最后我们看一下,人类如何调用工具,并将结果反馈给大模型,从而辅助大模型完成任务。
OpenAI 的 SDK 文档给出了说明,经过我简化后,方法如下:
1 | #1. 将提问存储到MessageStore |
可以看到,相比正常的 user assistant 的多轮对话模式,Function Calling 的对话模式,只是略有不同,不同点在于第一要将大模型选择的工具添加到 assistant 对话中,第二是要在assistant 之后添加 tool 角色的消息,用于存储工具调用结果、工具名称以及 id。
理解了原理后,我们来看代码:
1 | if toolCalls != nil { |
首先判断一下大模型有没有调用工具,如果调用了,则从 toolCalls[0].Function.Arguments 中解析出函数入参。之后根据工具名称,调用相应的工具函数。最后按照上文中 OpenAI 规定的格式,将对应的 assistant 以及 tool 消息填好,反馈给大模型。
输出:
1 | 函数计算结果: 3 |
总结
本节课我在开篇用了两个小例子为你展示了大模型不是万能的,大模型也有自身的弱点以及无法解决的问题,让你体验了一下什么是业界常说的“幻觉”。
OpenAI 公司为了解决这些问题,想到了让大模型与外界环境交互的破解之法,因此提出了 Function Calling 机制,并在 SDK 中进行了支持,在迅速成为了行业标杆做法后,其他公司包括国内公司的大模型,也对该机制进行了兼容,因此我们可以使用 OpenAI SDK 配合阿里云的通义千问大模型体验该机制。
最后我用一个加法小例子,为你展示了 Function Calling 的代码应如何写,并介绍了其前置基础 Chat Completion。本节课的代码已公开在 GitHub 上,链接为:Geek/function-calling at main · xingyunyang01/Geek (github.com)
思考题
我在课程中的代码,为你演示了 “1+2=?” 这个小例子,如果是计算 “1+2+3+4-5-6=?” 呢?代码该如何编写?大模型能否给出正确的回答?
欢迎你在留言区展示你的思考过程,我们一起探讨。如果你觉得这节课的内容对你有帮助的话,也欢迎你分享给其他朋友,我们下节课再见!