OpenAI 开放的 Function calling 函数调用 功能使您的应用程序能够根据用户输入执行操作。这意味着它可以代表您的用户搜索网络、发送电子邮件或预订门票,使其比常规聊天机器人更强大。
接下来我们将使用 OpenAI 函数以及最新版本的 Node.js SDK 的构建应用程序。实现实时查询当前天气,并根据天气给出一些你的今天活动建议。
我们的应用程序是一个简单的代理,可帮助您查找您所在地区的活动。它可以访问两个功能,getLocation()
和 getCurrentWeather()
,这意味着它可以确定您所在的位置以及当前的天气情况。
在这一点上,重要的是要了解 OpenAI 不会为您执行任何代码。它只是告诉应用在给定方案中应该使用哪些函数,然后由应用来调用它们。这一点一定要注意,这个也是 Function calling 执行的核心逻辑;也就是openai 不会执行你的代码。只是告诉你应该调用那个代码。
一旦我们的代理知道您的位置和天气,它将使用 GPT 的内部知识为您推荐合适的当地活动。
调用 openai
js
async function fetchOpenAI(body) {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer sk-xxxxxxx`
},
body: JSON.stringify({
max_tokens: 2048,
...body
})
});
const data = await response.json();
return data;
}
创建我们的两个函数
接下来,我们将创建这两个函数。第一个 - getLocation
- 使用 IP API 获取用户的位置。
js
async function getLocation() {
const response = await fetch("https://ipapi.co/json/");
const locationData = await response.json();
return locationData;
}
IP API 返回一堆关于您的位置的数据,包括您的纬度和经度,我们将在第二个函数 getCurrentWeather
中将其用作参数。它使用 Open Meteo API 获取当前天气数据,如下所示:
js
async function getCurrentWeather(latitude, longitude) {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&hourly=apparent_temperature`;
const response = await fetch(url);
const weatherData = await response.json();
return weatherData;
}
描述我们为 OpenAI 提供的功能
为了让 OpenAI 理解这些函数的用途,我们需要使用特定的模式来描述它们。我们将创建一个名为的数组 functionDefinitions
,该数组包含每个函数一个对象。每个对象将有三个键: name
、 description
和 parameters
。
js
const functionDefinitions = [
{
name: "getCurrentWeather",
description:
"获取给定位置的当前天气(以纬度和经度表示)",
parameters: {
type: "object",
properties: {
latitude: {
type: "string",
},
longitude: {
type: "string",
},
},
required: ["longitude", "latitude"],
},
},
{
name: "getLocation",
description: "根据 IP 地址获取用户的位置",
parameters: {
type: "object",
properties: {},
},
},
];
设置 messages 数组
我们还需要定义一个 messages
数组。这将跟踪我们的应用程序和 OpenAI 之间的所有消息来回。
数组中的第一个对象应始终将 role
属性设置为 "system"
,这告诉 OpenAI 这就是我们希望它的行为方式。
js
const messages = [
{
role: "system",
content: `你是一个有用的助手。仅使用为您提供的功能`,
},
];
创建代理函数
现在,我们已准备好构建应用的逻辑,该逻辑存在于 agent
函数中。它是异步的,并接受一个参数 userInput
。
我们首先将 userInput
推送到 messages 数组。这一次,我们将 role
设置为 "user"
,以便 OpenAI 知道这是来自用户的输入。
js
async function agent(userInput) {
messages.push([
{
role: "user",
content: userInput,
},
]);
const response = await fetchOpenAI({
model: "gpt-4-1106-preview",
messages: messages,
functions: functionDefinitions,
});
console.log(response);
}
通过简单的输入运行我们的应用程序
让我们尝试使用需要函数调用才能给出合适回复的输入来运行。 agent
js
agent("Where am I located right now?");
当我们运行上面的代码时,我们看到 OpenAI 的响应注销到控制台,如下所示:
json
{
model: 'gpt-4-1106-preview',
object: 'chat.completion',
usage: { prompt_tokens: 108, completion_tokens: 9, total_tokens: 117 },
id: 'chatcmpl-8LCJE8i12wCzCcy9pfO2ocnyz03vm',
created: 1700062896,
choices: [
{
index: 0,
delta: null,
message: {
role: 'assistant',
function_call: { name: 'getLocation', arguments: '{}' }
},
finish_reason: 'function_call'
}
]
}
此响应告诉我们,我们应该调用我们的函数之一,因为它包含以下键: finish:_reason: "function_call"
函数的名称可以在键中找到,该 response.choices[0].message.function_call.name
键设置为 "getLocation"
。
将 OpenAI 响应转换为函数调用
现在我们已经将函数的名称转换为字符串,我们需要将其转换为函数调用。为了帮助我们解决这个问题,我们将把两个函数都收集到一个名为 availableFunctions
js
const availableFunctions = {
getCurrentWeather,
getLocation,
};
这很方便,因为我们可以通过括号表示法和从 OpenAI 返回的字符串来访问该 getLocation
函数,如下所示: availableFunctions["getLocation"]
.
js
const { finish_reason, message } = response.choices[0];
if (finish_reason === "function_call") {
const functionName = message.function_call.name;
const functionToCall = availableFunctions[functionName];
const functionArgs = JSON.parse(message.function_call.arguments);
const functionArgsArr = Object.values(functionArgs);
const functionResponse = await functionToCall.apply(null, functionArgsArr);
console.log(functionResponse);
}
我们还抓住了 OpenAI 希望我们传递到函数中的任何论点: message.function_call.arguments
.但是,对于第一个函数调用,我们不需要任何参数。
如果我们使用相同的输入 ( "Where am I located right now?"
) 再次运行代码,我们将看到一个 functionResponse
对象,里面填充了用户现在所处位置的位置。就我而言,那是北京。
getCurrentWeather 调用时候生成的链接如下:api.open-meteo.com/v1/forecast...
我们将此数据添加到数组中的新项中 messages
,其中我们还指定了我们调用的函数的名称。
js
messages.push({
role: "function",
name: functionName,
content: `最后一个函数的结果是这样的:: ${JSON.stringify(
functionResponse
)}
`,
});
请注意,设置为 role
"function"
。这告诉 OpenAI,该参数包含函数调用的结果, content
而不是用户的输入。
此时,我们需要使用这个更新 messages
的数组向 OpenAI 发送一个新请求。但是,我们不想对新的函数调用进行硬编码,因为我们的代理可能需要在自身和 GPT 之间来回切换几次,直到它为用户找到最终答案。
这可以通过几种不同的方式解决,例如递归、while 循环或 for 循环。为了简单起见,我们将使用一个好的旧 for 循环。
创建循环
在 agent
函数的顶部,我们将创建一个循环,让我们最多可以运行整个过程五次。
如果我们从 GPT 返回 finish_reason: "function_call"
,我们只会将函数调用的结果推送到数组并 messages
跳转到循环的下一次迭代,从而触发新的请求。
如果我们回来 finish_reason: "stop"
,那么 GPT 已经找到了合适的答案,所以我们将返回函数并取消循环。
js
for (let i = 0; i < 1; i++) {
const response = await fetchOpenAI({
model: "gpt-4-1106-preview",
messages: messages,
functions: functionDefinitions,
});
const { finish_reason, message } = response.choices[0];
console.log("response:", message);
if (finish_reason === "function_call") {
const functionName = message.function_call.name;
const functionToCall = availableFunctions[functionName];
const functionArgs = JSON.parse(message.function_call.arguments);
const functionArgsArr = Object.values(functionArgs);
const functionResponse = await functionToCall.apply(
null,
functionArgsArr
);
messages.push({
role: "function",
name: functionName,
content: `
最后一个函数的结果是这样的: ${JSON.stringify(
functionResponse
)}
`,
});
} else if (finish_reason === "stop") {
messages.push(message);
return message.content;
}
}
return "已达到最大迭代次数但没有合适的答案。请使用更具体的输入重试。";
如果我们在五次迭代中没有看到 a finish_reason: "stop"
,我们将返回一条消息,说我们找不到合适的答案。
运行最终应用
在这一点上,我们准备尝试我们的应用程序!我会要求程序根据我的位置和当前的天气建议一些活动。
js
const response = await agent(
"请根据我的位置和天气建议一些活动。需要返回天气的信息。"
);
console.log("response:", response);
以下是我们在控制台中看到的内容(格式化以使其更易于阅读):
如果我们在引擎盖下达到顶峰,并在循环的每次迭代中注销 response.choices[0].message
,我们会看到 GPT 在得出答案之前已经指示我们使用这两个函数。
js
{role: "assistant", content: null, function_call: {name: "getLocation", arguments: "{}"}}
{role: "assistant", content: null, function_call: {name: "getCurrentWeather", arguments: " { "longitude": "39.911", "latitude": "116.395" }"}}
现在,您已经使用 OpenAI 函数和 Node.js SDK 构建了 AI 代理!如果您正在寻找额外的挑战,请考虑增强此应用程序。例如,您可以添加一个函数,用于获取有关用户所在位置的事件和活动的最新信息。
Complete code 完整代码
js
async function fetchOpenAI(body) {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer sk-xxxxxx`
},
body: JSON.stringify({
max_tokens: 2048,
...body
})
});
const data = await response.json();
console.log("data:", data);
return data;
}
async function getLocation() {
const response = await fetch("https://ipapi.co/json/");
const locationData = await response.json();
return locationData;
}
async function getCurrentWeather(latitude, longitude) {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&hourly=apparent_temperature`;
const response = await fetch(url);
const weatherData = await response.json();
return weatherData;
}
const functionDefinitions = [
{
name: "getCurrentWeather",
description:
"获取给定位置的当前天气(以纬度和经度表示)",
parameters: {
type: "object",
properties: {
latitude: {
type: "string",
},
longitude: {
type: "string",
},
},
required: ["longitude", "latitude"],
},
},
{
name: "getLocation",
description: "根据 IP 地址获取用户的位置",
parameters: {
type: "object",
properties: {},
},
},
];
const availableFunctions = {
getCurrentWeather,
getLocation,
};
const messages = [
{
role: "system",
content: `你是一个有用的助手。仅使用为您提供的功能`,
},
];
async function agent(userInput) {
messages.push({
role: "user",
content: userInput,
});
for (let i = 0; i < 1; i++) {
const response = await fetchOpenAI({
model: "gpt-4-1106-preview",
messages: messages,
functions: functionDefinitions,
});
const { finish_reason, message } = response.choices[0];
console.log("response:", message);
if (finish_reason === "function_call") {
const functionName = message.function_call.name;
const functionToCall = availableFunctions[functionName];
const functionArgs = JSON.parse(message.function_call.arguments);
const functionArgsArr = Object.values(functionArgs);
const functionResponse = await functionToCall.apply(
null,
functionArgsArr
);
messages.push({
role: "function",
name: functionName,
content: `
最后一个函数的结果是这样的: ${JSON.stringify(
functionResponse
)}
`,
});
} else if (finish_reason === "stop") {
messages.push(message);
return message.content;
}
}
return "已达到最大迭代次数但没有合适的答案。请使用更具体的输入重试。";
}
const response = await agent(
"请根据我的位置和天气建议一些活动。需要返回天气的信息。"
);
console.log("response:", response);