天天看點

【OpenAI】ChatGPT函數調用(Function Calling)實踐

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能力的應用提供了更豐富的可能性。應用開發者可以通過函數調用功能,讓使用者通過自然語言互動,擷取實時資料、結構化資料,同時也可以與應用進行各類互動。本文中描述的幾個案例場景僅為抛磚引玉,歡迎大家多多讨論,嘗試更多應用場景。

作者:京東零售 牛曉光

來源:京東雲開發者社群

繼續閱讀