使用 openai Function calling 实现天气查询

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 ,该数组包含每个函数一个对象。每个对象将有三个键: namedescriptionparameters

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);
相关推荐
m0_74825718几秒前
Spring Boot FileUpLoad and Interceptor(文件上传和拦截器,Web入门知识)
前端·spring boot·后端
桃园码工18 分钟前
15_HTML5 表单属性 --[HTML5 API 学习之旅]
前端·html5·表单属性
百万蹄蹄向前冲1 小时前
2024不一样的VUE3期末考查
前端·javascript·程序员
轻口味1 小时前
【每日学点鸿蒙知识】AVCodec、SmartPerf工具、web组件加载、监听键盘的显示隐藏、Asset Store Kit
前端·华为·harmonyos
alikami1 小时前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
wakangda2 小时前
React Native 集成原生Android功能
javascript·react native·react.js
吃杠碰小鸡2 小时前
lodash常用函数
前端·javascript
丰云2 小时前
一个简单封装的的nodejs缓存对象
缓存·node.js
emoji1111112 小时前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼2 小时前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs