如何使用 OpenAI Node.js SDK 构建代理

Oct 5, 2023
在 Github 中打开

OpenAI 函数使你的应用程序能够根据用户输入采取行动。这意味着它可以例如搜索网络、发送电子邮件或代表你的用户预订机票,使其比普通聊天机器人更强大。

在本教程中,你将构建一个应用程序,该应用程序使用 OpenAI 函数以及最新版本的 Node.js SDK。该应用程序在浏览器中运行,因此你只需要一个代码编辑器,例如 VS Code Live Server 即可在本地进行操作。或者,通过 Scrimba 上的这个代码游乐场 直接在浏览器中编写代码。

你将构建什么

我们的应用程序是一个简单的代理,可以帮助你找到你所在地区的活动。它可以访问两个函数,getLocation()getCurrentWeather(),这意味着它可以确定你所在的位置以及当前的天气。

在这一点上,重要的是要理解 OpenAI 不会为你执行任何代码。它只是告诉你的应用程序在给定场景中应该使用哪些函数,然后由你的应用程序来调用它们。

一旦我们的代理知道你的位置和天气,它将使用 GPT 的内部知识为你推荐合适的当地活动。

导入 SDK 并使用 OpenAI 进行身份验证

我们首先在 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 描述我们的函数

为了让 OpenAI 理解这些函数的目的,我们需要使用特定的模式来描述它们。我们将创建一个名为 tools 的数组,其中包含每个函数一个对象。每个对象将有两个键:typefunction,并且 function 键有三个子键:namedescriptionparameters

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 数组

我们还需要定义一个 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 函数

我们现在准备构建应用程序的逻辑,该逻辑位于 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"

将 OpenAI 响应转换为函数调用

现在我们有了作为字符串的函数名称,我们需要将其转换为函数调用。为了帮助我们做到这一点,我们将把我们的两个函数收集在一个名为 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);