京东质量社区 【OpenAI】ChatGPT 函数调用(Function Calling)实践 | 京东云技术团队

京东云开发者 · 2023年07月05日 · 最后由 陈恒捷 回复于 2023年07月06日 · 6229 次阅读

6 月 13 日 OpenAI 在 Chat Completions API 中添加了新的函数调用(Function Calling)能力,帮助开发者通过 API 方式实现类似于 ChatGPT 插件的数据交互能力。

本文在作者上一篇文章《私有框架代码生成实践》的基础上,依旧使用自然语言低代码搭建场景作为案例,将嵌入向量搜索(Embedding)获取私有知识库的方式,替换为函数调用方式,以我们更熟悉的结构化数据结构、关系型数据库的方式进行知识库管理。同时函数调用能力的灵活性和可扩展性,也可以帮助用户使用自然语言搭建更加复杂的页面内容、进行更丰富的交互操作。

一、 什么是函数调用

函数调用(Function Calling)是 OpenAI 在 6 月 13 日发布的新能力。根据官方博客描述,函数调用能力可以让模型输出一个请求调用函数的消息,其中包含所需调用的函数信息、以及调用函数时所携带的参数信息。这是一种将 GPT 能力与外部工具/API 连接起来的新方式。

支持函数调用的新模型,可以根据用户的输入自行判断何时需要调用哪些函数,并且可以根据目标函数的描述生成符合要求的请求参数。

开发人员可以使用函数调用能力,通过 GPT 实现:

  • 在进行自然语言交流时,通过调用外部工具回答问题(类似于 ChatGPT 插件);
  • 将自然语言转换为调用 API 时使用的参数,或者查询数据库时使用的条件;
  • 从文本中提取结构化数据。等

二、 如何使用函数调用

函数调用能力可以通过聊天 API(Chat Completion)使用。为了实现函数调用能力,OpenAI 对聊天 API 进行了修改,增加了新的请求参数、响应类型以及消息角色,应用开发者需要:

  1. 在请求参数中向聊天 API 传递信息,描述应用所提供的可调用函数的信息。
  2. 解析聊天 API 响应的消息类型,若模型决定需要调用函数,则根据模型返回的函数信息和函数传参调用函数,并获得返回结果。
  3. 将函数返回的结果添加到消息列表中,并再次调用聊天 API。

1. 添加请求参数, 描述所支持的函数信息

聊天 API 中新增了两个请求体参数:

functions

当前应用可调用的函数的列表。函数信息中包含了函数的名称、自然语言描述、以及函数所支持传入的参数信息。

functions参数的格式如下:

openai.createChatCompletion({
  model: "gpt-3.5-turbo-0613",
  messages: [
    // ...
  ],
  functions: [
    {
      name: 'function_name',
      description: '该函数所具备能力的自然语言描述',
      parameters: {
        type: 'object',
        properties: {
          argument_name: {
            type: 'string',
            description: '该参数的自然语言描述'
          },
          // ...
        },
        required: ['argument_name']
      }
    },
    // ...
  ]
})

functions参数支持以数组形式录入多组函数信息,其中:

  • name:函数名称。后续模型会在需要调用函数时返回此名称。

  • description:函数功能描述。模型通过该描述理解函数能力,并判断是否需要调用该函数。

  • parameters.properties:函数所需的参数。以对象的形式描述函数所需的参数,其中对象的 key 即为参数名。

    • type:参数类型。支持JSON Schema协议。
    • description:参数描述。
  • required:必填参数的参数名列表。

function_call

控制模型应该如何响应函数调换。支持几种输入:

  1. "none":模型不调用函数,直接返回内容。没有提供可调用函数时的默认值。
  2. "auto":模型根据用户输入自行决定是否调用函数以及调用哪个函数。提供可调用函数时的默认值。
  3. {"name": "function_name"}:强制模型调用指定的函数。

2. 识别响应参数, 描述需要调用的函数信息

聊天 API 在响应内容的可选项(choices)中提供了两个响应参数:

finish_reason

响应内容结束的原因。

可能的原因包括:

  • stop:已返回完整消息。
  • length:已达到令牌限制或由max_tokens参数设置的上限。
  • function_call:模型决定需要调用一个函数。
  • content_filter:内容触发了拦截策略,忽略返回内容。
  • null:API 响应仍在执行。

其中,若返回function_call则表示模型需要调用函数。此时message参数会额外返回函数信息以及函数参数信息。

message.function_call

若响应内容结束的原因是模型需要调用函数,则message参数中会增加一个用于描述函数信息的function_call参数,其格式如下:

  • name:函数名称。
  • arguments:函数参数信息。JSON 字符串格式。

3. 添加对话角色, 向消息列表中添加函数返回值

在函数执行完成后,可以将函数的返回内容追加到消息列表中,并携带完整的消息列表再次请求聊天 API,以获得 GPT 的后续响应。

在消息列表中,角色的可选值除了原有的系统system)、用户user)、助理assistant)外,新增了函数function)类型,用来标识该消息时函数调用的返回内容。

注意:向消息列表中追加函数调用响应消息前,还需要首先将上一步模型返回的消息追加到消息列表中,以保证消息列表中的上下文完整。

完整使用代码

const { Configuration, OpenAIApi } = require("openai");
const openai = new OpenAIApi(new Configuration({ /** OpenAI 配置 */ }));

/** 系统角色信息 **/
const systemPrompt: string = "系统角色prompt";

/** 支持函数信息 **/
const functionsPrompt: unknow[] = [
  {
    name: 'function_name',
    description: '函数功能的自然语言描述',
    parameters: {
      type: 'object',
      properties: {
        argument_name: {
          type: 'string',
          description: '该参数的自然语言描述'
        },
        // ...
      }
    }
  },
  // ...
];

/** 支持函数逻辑 **/
const functionsCalls: { [name: string]: Function } = {
  function_name: (args: { argument_name: string }) => {
    const { argument_name } = args;
    // ...
    return '函数调用结果'
  },
  // ...
}

/** 开始聊天 **/
const chat = async (userPrompt: string) => {
  const messages: unknow[] = [
    { role: 'system', content: systemPrompt },
    { role: 'user', content: userPrompt }
  ];

  let maxCall = 6;
  while (maxCall--) {
    const responseData = await openai.createChatCompletion({
      model: "gpt-3.5-turbo-0613",
      messages,
      functions,
      function_call: maxCall === 0 ? 'none' : 'auto'
    }).then((response) => response.data.choices[0]);

    const message = responseData.message
    messages.push(message)

    const finishReason = responseData.finish_reason
    if (finishReason === 'function_call') {
      const functionName = message.function_call.name
      const functionCall = functionCalls[functionName]
      const functionArguments = JSON.parse(message.function_call.arguments)
      const functionResponse = await functionCall(functionArguments)
      messages.push({
        role: 'function',
        name: functionName,
        content: functionResponse
      })
    } else {
      return message.content
    }
  }
}

三、 低代码自然语言搭建案例

在作者的上一篇文章中,使用嵌入向量搜索提供的 “检索 - 提问解决方案” 进行低代码私有协议的访问。在本文中,将使用函数调用方式进行替代。

同时,基于函数调用的能力,也探索了一些更加复杂的页面搭建能力和低代码平台功能。

1. 私有协议访问

基于我们的低代码平台私有协议,在进行 CMS 类型页面的搭建时,我们将协议的知识划分为几个层级,并分别提供函数供 GPT 按需调用,以实现私有协议的访问。

系统描述信息

const systemPropmpt = `使用CCMS协议编写页面的配置信息。

                       CCMS协议所支持的页面类型包括:
                       - *form*:表单页
                       - *table*:表格页
                       - *detail*:详情页`;

函数信息描述

const functionsPrompt = [
  {
    name: 'get_elements',
    description: '获取CCMS协议在指定页面类型下,所支持的元素类型。',
    parameters: {
      type: 'object',
      properties: {
        page: {
          type: 'array',
          description: '页面类型',
          items: { type: 'string' }
        }
      }
    },
    required: ['page']
  },
  {
    name: 'get_features',
    description: '获取CCMS协议在指定元素类型下,所支持的配置化特性。',
    parameters: {
      type: 'object',
      properties: {
        element: {
          type: 'array',
          description: '元素类型',
          items: { type: 'string' }
        }
      }
    },
    required: ['element']
  },
  {
    name: 'get_descriptions',
    description: '获取CCMS协议下,指定页面类型、元素类型以及配置化特性的详细信息。',
    parameters: {
      type: 'object',
      properties: {
        page: {
          type: 'array',
          description: '页面类型',
          items: { type: 'string' }
        },
        element: {
          type: 'array',
          description: '元素类型',
          items: { type: 'string' }
        },
        feature: {
          type: 'array',
          description: '配置化特性',
          items: { type: 'string' }
        }
      }
    }
  }
]

备注:尽管 GPT 模型支持函数的循环调用,但出于减少 API 调用频次和节省 Token 消耗的目的,我们建议在查询私有协议信息的函数中,使用关键词数组的形式进行批量查询。

函数内容

const functionsCalls = {
  get_elements: (args: { page: string[] }) => {
    const { page } = args;
    // 请自行实现信息查询,下列返回内容仅为示例。
    return page.map((pageType) => {
      switch (pageType) {
        case 'form':
          return `# **form**表单页所支持的元素类型包括:

                  - *form_text*:文本输入框
                  - *form_number*: 数值输入框`;
        default:
          return `# **${pageType}**没有支持的元素。`
      }
    }).join("\n\n");
  },
  get_features: (args: { element: string[] }) => {
    const { element } = args
    // 请自行实现信息查询,下列返回内容仅为示例。
    return element.map((elementKey) => {
      const [ pageType, elementType ] = elementKey.split('_');
      switch (pageType) {
        case 'form':
          switch (elementType) {
            case 'text':
              return `# **form_text**(文本输入框)所支持的配置化特性包括:

                      - *form_text_maxLength*: 文本最大长度限制
                      - *form_text_minLength*: 文本最小长度限制
                      - *form_text_regExp*: 文本正则表达式校验`
            default:
              return `# **${elementKey}**没有支持的配置化特性。`
          }
        default:
          return `# **${elementKey}**没有支持的配置化特性。`
      }
    }).join("\n\n");
  },
  get_descriptions: (args: { page: string[], element: string[], feature: string[] }) => {
    const {
      page = [],
      element = [],
      feature = []
    } = args
    // 请自行实现信息查询,下列返回内容仅为示例。
    return [
      ...page.map((pageType) => `# **${pageType}**的详细描述如下:...`),
      ...element.map((elementType) => `# **${elementType}**的详细描述如下:...`),
      ...feature.map((featureType) => `# **${featureType}**的详细描述如下:...`)
    ].join("\n\n")
  }
}

调用示例

一次完整调用的消息列表:

为了便于阅读,已经调整了消息列表中消息内容的缩进排版,并且将表示函数调用参数的 JSON 字符串解析为对象形式。

[
  // 第一次请求聊天API
  { role: 'system', 
    content: `使用CCMS协议编写页面的配置信息。

              CCMS协议所支持的页面类型包括:
              - *form*: 表单页
              - *table*: 表格页
              - *detail*: 详情页` },
  { role: 'user',
    content: '创建一个课程报名页面' },
  { role: 'assistant',
    content: null,
    function_call: { name: 'get_elements',
                     arguments: { page: ["form"] } } },

  // 第二次请求聊天API
  { role: 'function',
    name: 'get_elements',
    content: `# **form**表单页所支持的元素类型包括:

              - *form_text*:文本输入框
              - *form_number*: 数值输入框` },
  { role: 'assistant',
    content: null,
    function_call: { name: 'get_features',
                     arguments: { element: ["form_text", "form_number"] } } },

  // 第三次请求聊天API
  { role: 'function',
    name: 'get_features',
    content: `# **form_text**(文本输入框)所支持的配置化特性包括:

              - *form_text_maxLength*: 文本最大长度限制
              - *form_text_minLength*: 文本最小长度限制
              - *form_text_regExp*: 文本正则表达式校验

              # **form_number**没有支持的配置化特性。` },
  { role: 'assistant',
    content: null,
    function_call: { name: 'get_descriptions',
                     arguments: { page: ["form"],
                                  element: ["form_text", "form_number"],
                                  feature: ["form_text_maxLength", "form_text_minLength", "form_text_regExp"] } } },

  // 第四次请求聊天API
  { role: 'function',
    name: 'get_descriptions',
    content: `# **form**的详细描述如下:...

              # **form_text**的详细描述如下:...

              # **form_number**的详细描述如下:...

              # **form_text_maxLength**的详细描述如下:...

              # **form_text_minLength**的详细描述如下:...

              # **form_text_regExp**的详细描述如下:...` },
  { role: 'assistant',
    content: '课程报名页面的配置信息如下:\n\n...' }
]

2. 页面搭建能力扩展: 页面上下文跳转场景

在进行低代码页面搭建时,有时会需要在页面配置中加入一些上下文信息。

例如需要在页面中添加一个按钮,用户点击按钮时跳转至另一个页面。此时我们可以通过一个函数,允许模型获取相关的页面列表。

关于按钮、跳转操作等协议内容可以通过上一章节中的方法获取:

## button

按钮。

支持的配置项包括:

- *label*:按钮标签
- *action*:操作类型,支持:
  - *none*:无操作
  - *redirect*:页面重定向
- *redirectTo*:页面标识

函数信息描述

const functionsPrompt = [
  // ...
  {
    name: 'get_page_id',
    description: '查询页面标识列表。其中包含页面标识(`id`)、页面名称(`name`)',
    parameters: {
      type: 'object',
      properties: {
        page: {
          type: 'string',
          description: '页面'
        }
      }
    }
  }
]

函数内容

const functionsCalls = {
  // ...
  get_page_id: (args: {}) => {
    // 请自行实现信息查询,下列返回内容仅为示例。
    return JSON.stringify([
      {
        id: 'page_list',
        name: '列表页'
      },
      {
        id: 'page_create',
        name: '新增页',
        description: '用于新增内容'
      },
      {
        id: 'page_preview',
        name: '预览页'
      }
    ])
  }
}

调用示例

一次完整调用的消息列表:

为了便于阅读,已经调整了消息列表中消息内容的缩进排版,并且将 GPT 返回的配置信息和表示函数调用参数的 JSON 字符串解析为对象形式。

[
  // 已省略系统角色信息以及私有协议访问信息。
  // ...
  { role: 'user',
    content: '添加一个预览按钮,点击后跳转至预览页。'
  },
  // ...
  { role: 'assistant',
    content: { type: "button",
               label: "预览",
               action: "redirect",
               redirectTo: "preview" },
    function_call: { name: 'get_page_id',
                     arguments: { page: "preview" } } },
  { role: 'function',
    name: 'get_page_id',
    content: [ { id: "page_list", name: "列表页" },
               { id: "page_create", name: "新增页" },
               { id: "page_preview", name: "预览页"} ] },
  { role: 'assistant',
    content: { type: "button",
               label: "预览",
               action: "redirect",
               redirectTo: "page_preview" }
]

3. 低代码平台能力扩展: 搭建窗口可视区域调整

在进行自然语言低代码搭建时,我们希望让搭建窗口的可视区域自动滚动到发生变化的区域,此时可以通过系统角色要求在进行页面配置变动时调用页面滚动方法,自动滚动至发生配置变化的元素位置。

系统描述信息

在系统描述信息中添加相关描述:

const systemPropmpt = `//...

                       每次对页面内容进行调整时,需要滚动页面至目标元素位置。

                       CCMS页面配置信息为一个数组,每个页面元素为数组中的一项,如:

                       ```json
                       [
                         {
                           "id": "input",
                           "type": "text",
                           "label": "文本输入框"
                         }
                       ]
                       ```

                       // ...
                       `;

函数信息描述

const functionsPrompt = [
  // ...
  {
    name: 'scroll_to',
    description: '滚动页面至指定元素位置',
    parameters: {
      type: 'object',
      properties: {
        element_id: {
          type: 'string',
          description: '指定元素ID'
        }
      }
    }
  }
]

函数内容

const functionsCalls = {
  // ...
  scroll_id: (args: { element_id: string }) => {
    const { element_id } = args
    // 自行实现页面滚动逻辑
    return '滚动完成'
  }
}

四、 总结

OpenAI 提供的函数调用功能为使用 GPT 能力的应用提供了更丰富的可能性。应用开发者可以通过函数调用功能,让用户通过自然语言交互,获取实时数据、结构化数据,同时也可以与应用进行各类交互。本文中描述的几个案例场景仅为抛砖引玉,欢迎大家多多讨论,尝试更多应用场景。

作者:京东零售 牛晓光

来源:京东云开发者社区

共收到 1 条回复 时间 点赞

学习了

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册