Function Calling-从prompt到fine-tune
回顾
几个月之前写过一篇文章如何为chatGPT增加网络访问功能。
当时是使用了一个特殊的prompt(ReAct)来触发GPT的推理能力,告知我们要去调用哪个Tool。
prompt:
'''Answer the following questions as best you can.
You have access to the following tools:\n\nBing Search:
A wrapper around Bing Search. Useful for when you need to answer questions
about current events. Input should be a search query.\n\nUse the following
format:\n\nQuestion: the input question you must answer\nThought:
you should always think about what to do\nAction: the action to take,
should be one of [Bing Search]\nAction Input: the input to the action\n
Observation: the result of the action\n... (this Thought/Action/Action
Input/Observation can repeat N times)\nThought:
I now know the final answer\nFinal Answer:
the final answer to the original input question\n\nBegin!\n\nQuestion:
${问题}\nThought:'''
当我们使用这个prompt时,GPT就会输出它需要知道的搜索关键字,也就是prompt中的 Action Input。
Function Calling
最近openAI为GPT推出了Function Calling的功能, 官方介绍如下:
开发人员现在可以将函数描述为 gpt-4-0613和 gpt-3.5-turbo-0613,并让模型智能地选择输出一个包含参数的 JSON 对象来调用这些函数。这是将 GPT 的功能与外部工具和 API 更可靠地连接起来的一种新方法。
这些模型已经进行了微调,以检测需要调用函数的时间(取决于用户的输入) ,并使用符合函数签名的 JSON 进行响应。函数调用允许开发人员更可靠地从模型中获取结构化数据。
以下是使用Function Calling的流程
使用Function Calling 改造“chatGPT增加网络访问“
我们看看如何用Function Calling功能改造我们之前那个”为chatGPT增加网络访问“的代码以得到更加稳定的输出。
首先是负责串联整体流程的主调用函数getFinalAnswer,可以看到先是调用了一次GPT(getAnswerFromGPT), 然后根据GPT选择的函数再调用了一次我们的外部能力(searchAPI), 然后又调用了次GPT得到最终结果。
async function getFinalAnswer(question, preMessage) {
let prompt = question;
const message = await getAnswerFromGPT(prompt);
if (message.function_call) {
const function_name = message.function_call.name
const function_args = JSON.parse(message.function_call.arguments)
if (function_name === 'searchAPI') {
const searchResponse = await searchAPI(function_args.searchKey);
const messageWithCallFunction = await getAnswerFromGPTWithFunctionResponese(prompt, function_name, searchResponse);
return messageWithCallFunction.content;
}
}
}
从getFinalAnswer函数来看,相比于之前,我们不再需要用来触发GPT推理能力的自定义prompt了。
取而代之的是在调用chatGPT API时传入的2个参数 functions,function_call
async function getAnswerFromGPT(prompt) {
console.log("\n\nprompt is: " + prompt)
const message = { role: "user", "content": prompt };
const messageList = [message];
console.log('messageList:', JSON.stringify(messageList));
const OPEN_AI_KEY = "xxx";
const response = await axios.post('https://api.openai.com/v1/chat/completions', {
messages: messageList,
functions:getFunctions(),
function_call: getFunctionCall(),
model: 'gpt-3.5-turbo-0613',
}, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPEN_AI_KEY}`,
},
});
console.log('getAnswerFromGPT result:', response.data.choices[0].message);
return response.data.choices[0].message;
}
functions代表着我们拥有外部能力(函数)的集合,这里需要特别关注description 的准确性,GPT正是通过这个描述来判断要调用哪个函数的。
function getFunctions() {
return [{
"name": "searchAPI",
"description": "A wrapper around Bing Search. Useful for when you need to answer questions about current events.",
"parameters": {
"type": "object",
"properties": {
"searchKey": {
"type": "string",
"description": "the key of need to search",
},
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] },
},
"required": ["searchKey"],
},
}]
}
functions_call代表调用函数的方式,一般我们会选择’auto’
function getFunctionCall() {
return "auto";
}
当我们输入问题时,就会先调用1次GPT,GPT会根据问题以及函数描述利用自身的推理能力判断需要调用哪个函数。
调用过程如下所示:
prompt is: 美国国务卿最近什么时候会来中国?
messageList: [{"role":"user","content":"美国国务卿最近什么时候会来中国?"}]
getAnswerFromGPT result: {
role: 'assistant',
content: null,
function_call: {
name: 'searchAPI',
arguments: '{\n "searchKey": "美国国务卿最近访华时间"\n}'
}
}
此时我们获取到需要调用的函数以及参数,使用我们自身能力完成函数调用
async function getFinalAnswer(question, preMessage) {
let prompt = question;
const message = await getAnswerFromGPT(prompt);
if (message.function_call) {
const function_name = message.function_call.name
const function_args = JSON.parse(message.function_call.arguments)
if (function_name === 'searchAPI') {
const searchResponse = await searchAPI(function_args.searchKey);
const messageWithCallFunction = await getAnswerFromGPTWithFunctionResponese(prompt, function_name, searchResponse);
return messageWithCallFunction.content;
}
}
}
async function searchAPI(question) {
if (!question || question === '') {
throw new Error('Question cannot be empty');
}
const BING_SEARCH_API_KEY = "xxxx"
const response = await axios.get('https://api.bing.microsoft.com/v7.0/search', {
headers: {
'Ocp-Apim-Subscription-Key': BING_SEARCH_API_KEY,
},
params: {
q: question,
count: 10,
textDecorations: true,
textFormat: "HTML",
},
});
const snippets = response.data.webPages?.value.map(page => page.snippet);
return snippets.join('');
}
最终再调用1次GPT,将之前的问题以及函数调用的结果都传给GPT,得到最终结果
async function getAnswerFromGPTWithFunctionResponese(prompt, functionName, functionResponse) {
console.log("\n\nprompt is: " + prompt)
const message = { role: "user", "content": prompt };
const functionVar = { role: 'function', name: functionName, content: functionResponse};
const messageList = [message, functionVar];
console.log('messageList:', JSON.stringify(messageList));
const OPEN_AI_KEY = "xxxxx";
const response = await axios.post('https://api.openai.com/v1/chat/completions', {
messages: messageList,
functions: getFunctions(),
function_call: getFunctionCall(),
model: 'gpt-3.5-turbo-0613',
}, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPEN_AI_KEY}`,
},
});
console.log('getAnswerFromGPT result:', response.data.choices[0].message);
return response.data.choices[0].message;
}
调用过程如下所示:
prompt is: 美国国务卿最近什么时候会来中国?
messageList: [{"role":"user","content":"美国国务卿最近什么时候会来中国?"},{"role":"function","name":"searchAPI","content":"周末重磅消息,来了。 当前,<b>美国国务卿</b>安东尼·布林肯<b>访华</b>无疑是市场关注的焦点。6月18日早上,布林肯已抵达中国北京,开启两天<b>访华</b>之行..."}]
getAnswerFromGPT result: {
role: 'assistant',
content: '根据搜索结果显示,美国国务卿安东尼·布林肯最近于6月18日至19日访问中国。'
}
代码全文如下:
const express = require('express');
const app = express();
const cors = require('cors');
const axios = require('axios');
app.use(cors());
app.use(express.json());
app.post('/chat_ai_with_internal', async (req, res) => {
//处理post请求
const { query_message, pre_message } = req.body;
if (!query_message || query_message === '') {
console.log('error:', "queryMessage cannot be empty ");
res.status(400).send({ error: "queryMessage cannot be empty" });
return;
}
console.log('queryMessage:', query_message);
let result = await getFinalAnswer(query_message, pre_message);
result = await getAnswerFromGPT("Please translate the following statements into Chinese: " + result);
console.log('result:', result);
res.send({ role: "assistant", content: result });
});
async function getFinalAnswer(question, preMessage) {
let prompt = question;
const message = await getAnswerFromGPT(prompt);
if (message.function_call) {
const function_name = message.function_call.name
const function_args = JSON.parse(message.function_call.arguments)
if (function_name === 'searchAPI') {
const searchResponse = await searchAPI(function_args.searchKey);
const messageWithCallFunction = await getAnswerFromGPTWithFunctionResponese(prompt, function_name, searchResponse);
return messageWithCallFunction.content;
}
}
}
function getFunctions() {
return [{
"name": "searchAPI",
"description": "A wrapper around Bing Search. Useful for when you need to answer questions about current events.",
"parameters": {
"type": "object",
"properties": {
"searchKey": {
"type": "string",
"description": "the key of need to search",
},
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] },
},
"required": ["searchKey"],
},
}]
}
function getFunctionCall() {
return "auto";
}
async function getAnswerFromGPT(prompt) {
console.log("\n\nprompt is: " + prompt)
const message = { role: "user", "content": prompt };
const messageList = [message];
console.log('messageList:', JSON.stringify(messageList));
const OPEN_AI_KEY = "xxxxx";
const response = await axios.post('https://api.openai.com/v1/chat/completions', {
messages: messageList,
functions:getFunctions(),
function_call: getFunctionCall(),
model: 'gpt-3.5-turbo-0613',
}, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPEN_AI_KEY}`,
},
});
console.log('getAnswerFromGPT result:', response.data.choices[0].message);
return response.data.choices[0].message;
}
async function getAnswerFromGPTWithFunctionResponese(prompt, functionName, functionResponse) {
console.log("\n\nprompt is: " + prompt)
const message = { role: "user", "content": prompt };
const functionVar = { role: 'function', name: functionName, content: functionResponse};
const messageList = [message, functionVar];
console.log('messageList:', JSON.stringify(messageList));
const OPEN_AI_KEY = "xxxxxx";
const response = await axios.post('https://api.openai.com/v1/chat/completions', {
messages: messageList,
functions: getFunctions(),
function_call: getFunctionCall(),
model: 'gpt-3.5-turbo-0613',
}, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPEN_AI_KEY}`,
},
});
console.log('getAnswerFromGPT result:', response.data.choices[0].message);
return response.data.choices[0].message;
}
async function searchAPI(question) {
if (!question || question === '') {
throw new Error('Question cannot be empty');
}
const BING_SEARCH_API_KEY = "xxxxxxxx"
const response = await axios.get('https://api.bing.microsoft.com/v7.0/search', {
headers: {
'Ocp-Apim-Subscription-Key': BING_SEARCH_API_KEY,
},
params: {
q: question,
count: 10,
textDecorations: true,
textFormat: "HTML",
},
});
const snippets = response.data.webPages?.value.map(page => page.snippet);
return snippets.join('');
}
思考
我们之前使用ReAct的方式,使用Prompt来使得模型拥有了选择工具的能力,但稳定性不是很好,openAI此次推出了Function Calling,通过fine-tune的方式来让模型拥有选择工具的能力,稳定性较好,而且使用起来更加标准化。在实际场景中使用Prompt还是Fine-tune一直是讨论的热点话题,也许这次让模型如何选择调用函数的迭代,从社区Prompt演化为官方推出Fine-tune版本模型有一些借鉴意义。