接下来我们将要步入Agent领域。
function Calling是?
在正式开始之前,我们先来看看一切AI Agent的基础——「Function Calling」
这个东西的本质就是给LLM了解和调用外界函数的能力,LLM会根据他的理解,在最合适的时间,返回对函数的调用与参数,然后根据函数调用的结果进行回答。
比如,在构建一个旅游计划的chatbot,用户给出问题:“我计划于12月25日到日本涩谷参加圣诞节,请帮我查询那个时间的天气和当地节日规定”。
这样,LLM就会判断需要调用12月25日的实时天气API来获取日本涩谷的那一天的天气情况,并且查询涩谷对于圣诞节的安排。根据返回的结果,进行回答。
这里,我们先不用Langchain,来用OpenAI官方API去尝试理解一下Function Calling这个API。
OpenAI已经将Function Calling改名成了tools,不过目前大多数主流资料依然将其成为Function Calling,我们这里谨遵OpenAI的标准,之后都会以tools来称呼这个API
function Calling 案例
我们就用获取天气这个经典操作来作为使用案例吧,这个需要实时获取外部API的结果,LLM无法回答。
这里我们就继续用回NoteBook+deno,因为可以更细致化地观察每一步运行的过程。
我们来使用OpenAI官方库,尝试引入并且初始化:
import { load } from "dotenv"
const env = await load()
...
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: env["OPENAI_API_KEY"],
baseURL: env["OPENAI_API_URL"]
});
创建一个假的获取天气的函数
function getCurrentWeather({location, unit="fahrenheit"}) {
const weather_info = {
"location": location,
"unit": unit,
"temperature": 72,
"forecast": ["sunny","windy"]
}
return JSON.stringify(weather_info)
}
然后我们按照OpenAI官方API文档指定的格式,创建这个函数的描述信息:
const tools = [
{
type: "function",
function: {
name: "getCurrentWeather",
description: "Get the current weather in a given location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "The city and state, eg. 'San Francisco, CA'"
},
unit: {
type: "string",
description: "The unit for temperature",
enum: ["fahrenheit", "celsius"], // 限定取值范围
}
},
require : ["location"]
}
}
}
]
我们来解析一下这里的定义:
type: "function"
:这里目前只支持Function,是必须指定的内容,没有的话会抛出错误。function
:对具体函数的描述name
:函数名字,需要和你那边要调用的函数的名字一样,我们才可以实现对函数名的调用description
:对函数的描述,可以理解成,提供对工具功能的简要描述,帮助模型理解并正确使用工具parameters
:函数的参数,OpenAI使用的是通用的JSON Schema去描述函数的各个参数
关于parameters,这里分别详细说一下:
- 外层的
type
描述参数的顶层数据结构类型,通常就是Object properties
列出对象中的所有参数,并定义每个参数的名称、类型和约束require
,定义必需参数的数组
properties
里又包含了每个参数属性的详细信息,分别是:
- type:参数的数据类型(如 string、number、boolean)
- description:描述参数的用途
- enum:限定参数值的可选范围
- default:为参数提供默认值(如果适用)
- format:限制参数的格式(如 email、uri)
然后我们来试试调用LLM的tools功能:
const messages = [
{
"role": "user",
"content": "成都天气如何?"
}
]
const result = await openai.chat.completions.create({
model: env["OPENAI_MODEL"],
messages,
tools
})
console.log(result.choices[0])
我们得到的结果如图:
可以看到,content是空的,也就是说大模型没有返回文本信息,只是在tool_calls里声明了一下需要调用的函数内容,参数location变成了“成都,中国”,有意思的是,他给定了我圈出的另一个参数unit,指定为摄氏度。
我们来试试英文提问:
const messages = [
{
"role": "user",
// "content": "成都天气如何?"
"content": "What's the weather in Chengdu?"
}
]
这里就没有给出第二个参数了,这里再次体现了LLM基于大数据训练出来的涌现的智能,在这种细节也有体现,不过我们并不会完全依赖,算是一个小彩蛋吧,毕竟LLM是一个黑盒,这种小细节值得记录一下。
控制LLM调用函数的行为
这里,tools还有一个可以选的参数,是tool_choice
,有几种使用方式:
none
表示,禁止LLM使用任何函数,也就是说,无论用户怎么输入,LLM都不会调用函数auto
表示,让LLM自己决定要不要使用函数,LLM的返回值可能是函数调用,也可能是正常的信息- 最后一种,就是指定一个函数,让LLM强制使用函数,类型是Object,有两个属性:
type
:只能指定为function
function
:值为一个对象,有且仅有一个key name为函数名称,比如我们上面的例子:{ type: "function", function: { name: "getCurrentWeather", } }
有了这个能力,我们就具备了使用更加细致的颗粒度去调用LLM,比如我们直接禁止LLM使用函数功能:
const messages = [
{
"role": "user",
"content": "成都天气如何?"
// "content": "What's the weather in Chengdu?"
}
]
const result = await openai.chat.completions.create({
model: env["OPENAI_MODEL"],
messages,
tools,
tool_choice: "none"
})
console.log(result.choices[0])
我们得到的结果是:
那如果我们来强制要求调用我之前定义的那个函数,并且我不给他提供那个函数需要的信息呢?
const messages = [
{
"role": "user",
"content": "你好?"
// "content": "What's the weather in Chengdu?"
}
]
const result = await openai.chat.completions.create({
model: env["OPENAI_MODEL"],
messages,
tools,
tool_choice: {
type: "function",
function: {
name: "getCurrentWeather",
}
}
})
console.log(result.choices[0])
Opoos!好像它产生了很严重的幻觉问题,定位给定位到北京去了
综上所述,我们可以得知,用户并没有提供任何跟天气与城市有关系的信息,但是因为我们强制LLM调用请求天气信息的函数,导致LLM出现了非常严重的幻觉问题,至于,这种强制调用有什么用,据我所知,在我计划学习的周期里,数据提取会需要这个操作。所以我们把这个问题抛给之后的学习过程吧。
然后,我们把LLM返回的调用参数,传递给JS的函数里。
function getCurrentWeather({location, unit="fahrenheit"}) {
const weather_info = {
"location": location,
"unit": unit,
"temperature": 2,
"forecast": ["sunny","windy"]
}
return JSON.stringify(weather_info)
}
const functions = {
"getCurrentWeather": getCurrentWeather
}
我们修改定义函数的位置,把其参数设计为类似于React function Component那种写法的Object,方便我们在这里调用对应的函数,我们可以直接这样:
const functionInfo = result.choices[0].message.tool_calls[0].function
const functionName = functionInfo.name
const functionParams = functionInfo.arguments
console.log(JSON.parse(functionParams));
const functionResult = functions[functionName](JSON.parse(functionParams))
console.log(functionResult)
把LLM给出的arguments直接塞给对应函数里,就可以得到响应的结果了:
并发调用函数
在比较新版的Tools里,引入了并发调用函数的特性。
我们可以简单的理解成之前的function Calling每次LLM只会返回对一个函数的调用请求,但是在Tools里可以返回一系列的函数调用,来获取更多的信息,函数之间我们可以并行的调用来节约调用外部API所占用的时间。在旧的function Calling里,只能让LLM依次调用请求,形成串行。
比如我现在再写一个获取当前时间的函数:getCurrentTime
function getCurrentWeather({location, unit="fahrenheit"}) {
const weather_info = {
"location": location,
"unit": unit,
"temperature": 2,
"forecast": ["sunny","windy"]
}
return JSON.stringify(weather_info)
}
function getCurrentTime({ format = "iso" } = {}) {
let currentTime;
switch (format) {
case "iso":
currentTime = new Date().toISOString();
break;
case "locale":
currentTime = new Date().toLocaleString();
break;
default:
currentTime = new Date().toString();
break;
}
return currentTime;
}
const functions = {
"getCurrentWeather": getCurrentWeather,
"getCurrentTime": getCurrentTime
}
我们对tools也追加描述:
const tools = [
{
type: "function",
function: {
name: "getCurrentTime",
description: "Get the current time",
parameters: {
type: "object",
properties: {
format: {
type: "string",
description: "The format of the time, eg. 'iso', 'locale', 'string'",
enum: ["iso", "locale", "string"]
}
},
require : ['format']
}
}
},
{
type: "function",
function: {
name: "getCurrentWeather",
description: "Get the current weather in a given location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "The city and state, eg. 'San Francisco, CA'"
},
unit: {
type: "string",
description: "The unit for temperature",
enum: ["fahrenheit", "celsius"], // 限定取值范围
}
},
require : ["location"]
}
}
}
]
那么,我们来试一下:
const messages = [
{
"role": "user",
"content": "请告诉我现在的时间,和成都的天气"
// "content": "What's the weather in Chengdu?"
}
]
const result = await openai.chat.completions.create({
model: env["OPENAI_MODEL"],
messages,
tools,
})
console.log(result.choices[0])
得到的结果是:
也是成功获得了两个函数都需要的参数,这就是并行调用。
根据函数结果进行回答
我们把上述所有内容联系到一起,把函数运行的结果输入给LLM,让LLM参考进行回答。
messages.push(result.choices[0].message);
const choices = result.choices[0].message.tool_calls
for (const choice of choices) {
const cellId = choice.id
const functionInfo = choice.function
const functionName = functionInfo.name
const functionParams = functionInfo.arguments
const functionResult = functions[functionName](JSON.parse(functionParams))
messages.push({
tool_call_id: cellId,
role: "tool",
name: functionName,
content: functionResult
})
}
console.log(messages);
这样处理之后,最后的样子就是:
[
{ role: "user", content: "请告诉我现在的时间,和成都的天气" },
{
role: "assistant",
content: null,
tool_calls: [
{
id: "call_b1EBd02zeYZYKfLIwlemtoYE",
type: "function",
function: { name: "getCurrentTime", arguments: '{"format": "locale"}' }
},
{
id: "call_42TvIMQHWYpDyzkB5ExkLN9f",
type: "function",
function: {
name: "getCurrentWeather",
arguments: '{"location": "Chengdu, China", "unit": "celsius"}'
}
}
],
refusal: null
},
{
tool_call_id: "call_b1EBd02zeYZYKfLIwlemtoYE",
role: "tool",
name: "getCurrentTime",
content: "12/29/2024, 9:51:27 PM"
},
{
tool_call_id: "call_42TvIMQHWYpDyzkB5ExkLN9f",
role: "tool",
name: "getCurrentWeather",
content: '{"location":"Chengdu, China","unit":"celsius","temperature":2,"forecast":["sunny","windy"]}'
}
]
也就是:
- 一条用户的提问
- 一条LLM对函数的调用
- 一条是我们调用函数的结果
然后我们把messages打包发给LLM:
const res = await openai.chat.completions.create({
model: env["OPENAI_MODEL"],
messages,
})
console.log(res.choices[0].message);
得到的结果,就是我们假设的函数得到的结果的总结语句了。
总结
经过上面的这番折腾,我们成功地完成了给LLM提供外部函数,调用外部函数,传递结果给LLM,让LLM根据结果进行回答的逻辑闭环。
但是,发现了吗?为了整个过程,我们需要大量的代码来进行跑通。
而且我做了这个逻辑也只考虑了用户提问完之后LLM一定会调用外界API来进行回答的情况,如果,没有调用API呢?是不是要加一个IF?
真正的完整的Agent要写的东西还差得远,比如我想要实现一个LLM自主决定行动并且根据行动结果进行下一步行动的Agent,想了想或许还远远不够。接下来来试试用Langchain提供的工具来缩减开发复杂度。。