OpenAI 函数使你的应用程序能够根据用户输入采取行动。这意味着它可以例如搜索网络、发送电子邮件或代表你的用户预订机票,使其比普通聊天机器人更强大。
在本教程中,你将构建一个应用程序,该应用程序使用 OpenAI 函数以及最新版本的 Node.js SDK。该应用程序在浏览器中运行,因此你只需要一个代码编辑器,例如 VS Code Live Server 即可在本地进行操作。或者,通过 Scrimba 上的这个代码游乐场 直接在浏览器中编写代码。
我们的应用程序是一个简单的代理,可以帮助你找到你所在地区的活动。它可以访问两个函数,getLocation()
和 getCurrentWeather()
,这意味着它可以确定你所在的位置以及当前的天气。
在这一点上,重要的是要理解 OpenAI 不会为你执行任何代码。它只是告诉你的应用程序在给定场景中应该使用哪些函数,然后由你的应用程序来调用它们。
一旦我们的代理知道你的位置和天气,它将使用 GPT 的内部知识为你推荐合适的当地活动。
我们首先在 JavaScript 文件的顶部导入 OpenAI SDK,并使用我们的 API 密钥进行身份验证,该密钥已存储为环境变量。
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
dangerouslyAllowBrowser: true,
});
由于我们在 Scrimba 的浏览器环境中运行代码,我们还需要设置 dangerouslyAllowBrowser: true
以确认我们了解客户端 API 请求所涉及的风险。请注意,在生产应用程序中,你应该将这些请求转移到 Node 服务器上。
接下来,我们将创建两个函数。第一个函数 - getLocation
- 使用 IP API 获取用户的位置。
async function getLocation() {
const response = await fetch("https://ipapi.co/json/");
const locationData = await response.json();
return locationData;
}
IP API 返回有关你位置的大量数据,包括你的纬度和经度,我们将这些数据用作第二个函数 getCurrentWeather
中的参数。它使用 Open Meteo API 获取当前天气数据,如下所示
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 理解这些函数的目的,我们需要使用特定的模式来描述它们。我们将创建一个名为 tools
的数组,其中包含每个函数一个对象。每个对象将有两个键:type
、function
,并且 function
键有三个子键:name
、description
和 parameters
。
const tools = [
{
type: "function",
function: {
name: "getCurrentWeather",
description: "Get the current weather in a given location",
parameters: {
type: "object",
properties: {
latitude: {
type: "string",
},
longitude: {
type: "string",
},
},
required: ["longitude", "latitude"],
},
}
},
{
type: "function",
function: {
name: "getLocation",
description: "Get the user's location based on their IP address",
parameters: {
type: "object",
properties: {},
},
}
},
];
我们还需要定义一个 messages
数组。这将跟踪我们的应用程序和 OpenAI 之间来回的所有消息。
数组中的第一个对象应始终将 role
属性设置为 "system"
,这告诉 OpenAI 这是我们希望它如何表现。
const messages = [
{
role: "system",
content:
"You are a helpful assistant. Only use the functions you have been provided with.",
},
];
我们现在准备构建应用程序的逻辑,该逻辑位于 agent
函数中。它是异步的,并接受一个参数:userInput
。
我们首先将 userInput
推送到 messages 数组。这次,我们将 role
设置为 "user"
,以便 OpenAI 知道这是来自用户的输入。
async function agent(userInput) {
messages.push({
role: "user",
content: userInput,
});
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: messages,
tools: tools,
});
console.log(response);
}
接下来,我们将通过 Node SDK 中的 chat.completions.create()
方法向 Chat completions 端点发送请求。此方法将配置对象作为参数。在其中,我们将指定三个属性
model
- 决定我们要使用哪个 AI 模型(在我们的例子中是 GPT-4)。messages
- 用户和 AI 之间直到此时的整个消息历史记录。tools
- 模型可能调用的工具列表。目前,仅支持函数作为工具。我们将使用我们之前创建的 tools
数组。让我们尝试使用需要函数调用才能给出合适回复的输入来运行 agent
。
agent("Where am I located right now?");
当我们运行上面的代码时,我们看到来自 OpenAI 的响应像这样记录到控制台
{
id: "chatcmpl-84ojoEJtyGnR6jRHK2Dl4zTtwsa7O",
object: "chat.completion",
created: 1696159040,
model: "gpt-4-0613",
choices: [{
index: 0,
message: {
role: "assistant",
content: null,
tool_calls: [
id: "call_CBwbo9qoXUn1kTR5pPuv6vR1",
type: "function",
function: {
name: "getLocation",
arguments: "{}"
}
]
},
logprobs: null,
finish_reason: "tool_calls" // OpenAI wants us to call a function
}],
usage: {
prompt_tokens: 134,
completion_tokens: 6,
total_tokens: 140
}
system_fingerprint: null
}
此响应告诉我们应该调用我们的一个函数,因为它包含以下键:finish_reason: "tool_calls"
。
函数的名称可以在 response.choices[0].message.tool_calls[0].function.name
键中找到,该键设置为 "getLocation"
。
现在我们有了作为字符串的函数名称,我们需要将其转换为函数调用。为了帮助我们做到这一点,我们将把我们的两个函数收集在一个名为 availableTools
的对象中
const availableTools = {
getCurrentWeather,
getLocation,
};
这很方便,因为我们将能够通过方括号表示法和我们从 OpenAI 返回的字符串来访问 getLocation
函数,如下所示:availableTools["getLocation"]
。
const { finish_reason, message } = response.choices[0];
if (finish_reason === "tool_calls" && message.tool_calls) {
const functionName = message.tool_calls[0].function.name;
const functionToCall = availableTools[functionName];
const functionArgs = JSON.parse(message.tool_calls[0].function.arguments);
const functionArgsArr = Object.values(functionArgs);
const functionResponse = await functionToCall.apply(null, functionArgsArr);
console.log(functionResponse);
}
我们还获取了 OpenAI 希望我们传递到函数中的任何参数:message.tool_calls[0].function.arguments
。但是,对于第一个函数调用,我们不需要任何参数。
如果我们再次使用相同的输入("Where am I located right now?"
)运行代码,我们将看到 functionResponse
是一个对象,其中填充了有关用户当前位置的信息。在我的例子中,那是挪威奥斯陆。
{ip: "193.212.60.170", network: "193.212.60.0/23", version: "IPv4", city: "Oslo", region: "Oslo County", region_code: "03", country: "NO", country_name: "Norway", country_code: "NO", country_code_iso3: "NOR", country_capital: "Oslo", country_tld: ".no", continent_code: "EU", in_eu: false, postal: "0026", latitude: 59.955, longitude: 10.859, timezone: "Europe/Oslo", utc_offset: "+0200", country_calling_code: "+47", currency: "NOK", currency_name: "Krone", languages: "no,nb,nn,se,fi", country_area: 324220, country_population: 5314336, asn: "AS2119", org: "Telenor Norge AS"}
我们将此数据添加到 messages
数组中的新项,其中我们还指定了我们调用的函数的名称。
messages.push({
role: "function",
name: functionName,
content: `The result of the last function was this: ${JSON.stringify(
functionResponse
)}
`,
});
请注意,role
设置为 "function"
。这告诉 OpenAI content
参数包含函数调用的结果,而不是来自用户的输入。
在这一点上,我们需要使用这个更新的 messages
数组向 OpenAI 发送新请求。但是,我们不想硬编码新的函数调用,因为我们的代理可能需要在自身和 GPT 之间来回多次,直到它找到用户的最终答案。
这可以通过几种不同的方式解决,例如递归、while 循环或 for 循环。为了简单起见,我们将使用一个经典的 for 循环。
在 agent
函数的顶部,我们将创建一个循环,使我们能够运行整个过程最多五次。
如果我们从 GPT 返回 finish_reason: "tool_calls"
,我们将只需将函数调用的结果推送到 messages
数组,并跳转到循环的下一次迭代,从而触发新请求。
如果我们返回 finish_reason: "stop"
,则 GPT 已找到合适的答案,因此我们将返回该函数并取消循环。
for (let i = 0; i < 5; i++) {
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: messages,
tools: tools,
});
const { finish_reason, message } = response.choices[0];
if (finish_reason === "tool_calls" && message.tool_calls) {
const functionName = message.tool_calls[0].function.name;
const functionToCall = availableTools[functionName];
const functionArgs = JSON.parse(message.tool_calls[0].function.arguments);
const functionArgsArr = Object.values(functionArgs);
const functionResponse = await functionToCall.apply(null, functionArgsArr);
messages.push({
role: "function",
name: functionName,
content: `
The result of the last function was this: ${JSON.stringify(
functionResponse
)}
`,
});
} else if (finish_reason === "stop") {
messages.push(message);
return message.content;
}
}
return "The maximum number of iterations has been met without a suitable answer. Please try again with a more specific input.";
如果我们在五次迭代中没有看到 finish_reason: "stop"
,我们将返回一条消息,说明我们找不到合适的答案。
在这一点上,我们准备尝试我们的应用程序了!我将要求代理根据我的位置和当前天气推荐一些活动。
const response = await agent(
"Please suggest some activities based on my location and the current weather."
);
console.log(response);
这是我们在控制台中看到的内容(格式化后更易于阅读)
Based on your current location in Oslo, Norway and the weather (15°C and snowy),
here are some activity suggestions:
1. A visit to the Oslo Winter Park for skiing or snowboarding.
2. Enjoy a cosy day at a local café or restaurant.
3. Visit one of Oslo's many museums. The Fram Museum or Viking Ship Museum offer interesting insights into Norway’s seafaring history.
4. Take a stroll in the snowy streets and enjoy the beautiful winter landscape.
5. Enjoy a nice book by the fireplace in a local library.
6. Take a fjord sightseeing cruise to enjoy the snowy landscapes.
Always remember to bundle up and stay warm. Enjoy your day!
如果我们深入了解内部,并在循环的每次迭代中记录 response.choices[0].message
,我们将看到 GPT 已指示我们在提出答案之前使用我们的两个函数。
首先,它告诉我们调用 getLocation
函数。然后它告诉我们调用 getCurrentWeather
函数,并将 "longitude": "10.859", "latitude": "59.955"
作为参数传递进去。这是它从我们做的第一个函数调用中获得的数据。
{"role":"assistant","content":null,"tool_calls":[{"id":"call_Cn1KH8mtHQ2AMbyNwNJTweEP","type":"function","function":{"name":"getLocation","arguments":"{}"}}]}
{"role":"assistant","content":null,"tool_calls":[{"id":"call_uc1oozJfGTvYEfIzzcsfXfOl","type":"function","function":{"name":"getCurrentWeather","arguments":"{\n\"latitude\": \"10.859\",\n\"longitude\": \"59.955\"\n}"}}]}
你现在已经使用 OpenAI 函数和 Node.js SDK 构建了一个 AI 代理!如果你正在寻找额外的挑战,请考虑增强此应用程序。例如,你可以添加一个函数来获取用户位置中事件和活动的最新信息。
编码愉快!
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
dangerouslyAllowBrowser: true,
});
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 tools = [
{
type: "function",
function: {
name: "getCurrentWeather",
description: "Get the current weather in a given location",
parameters: {
type: "object",
properties: {
latitude: {
type: "string",
},
longitude: {
type: "string",
},
},
required: ["longitude", "latitude"],
},
}
},
{
type: "function",
function: {
name: "getLocation",
description: "Get the user's location based on their IP address",
parameters: {
type: "object",
properties: {},
},
}
},
];
const availableTools = {
getCurrentWeather,
getLocation,
};
const messages = [
{
role: "system",
content: `You are a helpful assistant. Only use the functions you have been provided with.`,
},
];
async function agent(userInput) {
messages.push({
role: "user",
content: userInput,
});
for (let i = 0; i < 5; i++) {
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: messages,
tools: tools,
});
const { finish_reason, message } = response.choices[0];
if (finish_reason === "tool_calls" && message.tool_calls) {
const functionName = message.tool_calls[0].function.name;
const functionToCall = availableTools[functionName];
const functionArgs = JSON.parse(message.tool_calls[0].function.arguments);
const functionArgsArr = Object.values(functionArgs);
const functionResponse = await functionToCall.apply(
null,
functionArgsArr
);
messages.push({
role: "function",
name: functionName,
content: `
The result of the last function was this: ${JSON.stringify(
functionResponse
)}
`,
});
} else if (finish_reason === "stop") {
messages.push(message);
return message.content;
}
}
return "The maximum number of iterations has been met without a suitable answer. Please try again with a more specific input.";
}
const response = await agent(
"Please suggest some activities based on my location and the weather."
);
console.log("response:", response);