2025年4月27日 星期日

OpenAI API 學習筆記 : 用 Function calling 讓模型開外掛 (一)

在前面幾篇測試中, 我們利用特製的回應模板規範 OpenAI API 的回應格式, 從初次回應中得知模型是否能回答所詢問之問題, 如果能就直接取出其生成之內容; 否則應用程式就會搜尋網路 (Call-out), 將取得的資訊做為參考資料再次詢問模型, 要求模型依據參考內容生成回應, 這基本上解決了所詢問題超出模型知識範圍的困境. 

但是這種方法會因為模型的隨機本質, 有時並不會按照規定的回應模板格式回答知或不知, 導致應用程式無法搜尋網路取得參考資料給模型做為依據來生成回應, 參考下面這篇的測試 :


本系列全部文章索引參考 : 


OpenAI 於 2023 年發布的 function calling 功能解決了此問題, 此機制是透過在請求訊息中增加一個 tools 參數告訴模型, 應用程式提供了哪些外部工具函式可調用, 如果 tools 參數中的 tool_choice 屬性為 'auto', 則模型會依據所詢問之內容自行判斷是否需要外部工具的協助, 如果需要就會在回應中以 tools_calls 屬性指定要呼叫的函式名稱與參數, 然後應用程式據此呼叫外部函式, 然後將傳回的結果以 tool 角色再次詢問模型來取得答案. OpenAI API 有了 function calling 功能之後, 使得微調過的 GPT 模型能夠整合外部工具 (即開發者撰寫的第三方函式) 來生成回應, 也就是具備了開外掛的能力. 

外部工具參數 tools 的值是一個用來描述有哪些可用的外部工具函式的字典串列, 每一個字典代表一個工具函式, 它包含兩個鍵 (屬性) type 與 function :
  • type (工具類型) :
    值為字串, 例如函式 'function', 程式碼解譯器 'code_interpreter', 瀏覽器 'browser' 等, 但目前只對 API 開放 'function' 而已. 
  • function (函式描述) : 
    值為符合 JSON 格式的字典 (即鍵必須使用雙引號括起來), 有三個鍵 :
    • "name" : 函式名稱 (字串)
    • "description" : 功能描述 (字串)
    • "parameters" : 參數格式 (值為 JSON 格式字典), 可有三個鍵 :
      • "type" : 參數型態, 值必須是 "object"
      • "properties" : 參數屬性 (值為 JSON 格式字典), 鍵為參數名稱, 例如 :
        {"para1": {
             "type": "string",
             "description": "參數1"},
          "para2": {
              "type": "number",
              "description": "參數2"}
         }
      • "required" : 必要參數 (串列)
注意, 參數格式 parameters 的結構可以是巢狀的, 亦即參數值本身也可以是一個物件, 這時該參數之 type 值必須是 "object", 也會有 properties 鍵. 

以前面測試中搜尋 Google 的函式 search_google() 為例 : 

>>> def search_google(query, cx, api_key, num=3, gl='tw', lr='lang_zh_TW'):
    params={
        'q': query,
        'key': api_key,
        'cx': cx,
        'gl': gl,  
        'lr': lr,          
        'num': num          
        }
    url='https://www.googleapis.com/customsearch/v1'
    r=requests.get(url, params=params)
    data=r.json()
    return data.get('items', [])

外部工具參數 tools 可以這麼寫 : 

>>> tools=[{
    "type": "function",
    "function": {
        "name": "search_google",
        "description": "用 Google Custom Search API 取得搜尋結果",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "要搜尋的關鍵字"
                    },
                "cx": {
                    "type": "string",
                    "description": "Google Custom Search 搜尋引擎 ID"
                    },
                "api_key": {
                    "type": "string",
                    "description": "Google Custom Search API 金鑰"
                    },
                "num": {
                    "type": "integer",
                    "description": "回傳的搜尋結果筆數(最多 10 筆)",
                    "default": 3
                    },
                "gl": {
                    "type": "string",
                    "description": "地理區域 (預設 'tw' 台灣)",
                    "default": "tw"
                    },
                "lr": {
                    "type": "string",
                    "description": "語言 (預設 'lang_zh_TW' 繁體中文)",
                    "default": "lang_zh_TW"
                    }
                },
            "required": ["query", "cx", "api_key"]
            }
        }
    }]

每一個參數都要在 properties 屬性中描述, 必要參數名稱則須放入 required 屬性的串列中. 

為了做個對比, 接下來我們先重複之前的測試, 就是詢問 gpt-3.5-turbo 模型它所不知道之事件, 由於 gpt-3.5-turbo 模型訓練所用之資料是在 2021 年 9 月之前準備的, 因此若詢問它此日期之後之事件 (例如 2024 年台灣總統大選結果), 它無法給出答案. 

我先將 API 金鑰等機敏資訊放在目前工作目錄下的環境變數檔案 .env 內, 然後用 dotenv 模組載入後用 os.environ.get() 取出來 : 

>>> from dotenv import load_dotenv   
>>> load_dotenv()     
True
>>> import os     
>>> openai_api_key=os.environ.get('OPENAI_API')      
>>> custom_search_key=os.environ.get('GOOGLE_CUSTOM_SEARCH_API')     
>>> cx=os.environ.get('SEARCH_ENGINE_ID')      

作法參考 :


詢問 gpt-3.5-turbo 模型關於 2024 台灣總統大選當選人不會得到答案 :
    
>>> from openai import OpenAI 
>>> client=OpenAI(api_key=openai_api_key)   
>>> messages=[
    {'role': 'system', 'content': '你是一個繁體中文AI助理'},
    {'role': 'user', 'content': '2024台灣總統大選是誰當選?'}
    ]   
>>> model='gpt-3.5-turbo'
>>> reply=client.chat.completions.create(
    model=model, 
    messages=messages)
>>> print(reply.choices[0].message.content)   
對不起,我無法提供即時或未來的真實事件資訊。台灣總統大選的結果取決於選舉當時的情況和選民投票。請隨時關注新聞和相關資訊來獲取最新的選舉結果。

可見超出模型知識範圍時, 它就無法給出所要的答案. 但如果傳入上面所定義的外部工具參數 tools, 並將 tool_choice 參數設為 'auto' 讓模型自動判斷此提問是否需要外部工具函式幫忙, 這時回應就會不一樣了 : 

>>> reply=client.chat.completions.create(
    model=model, 
    messages=messages,
    tools=tools,
    tool_choice='auto' 
    )   

因為傳回值為物件型態, 可匯入 rich.print() 函式來讓輸出格式化為較好閱讀之形式 : 

>>> from rich import print as pprint   
>>> pprint(reply)  
ChatCompletion(
    id='chatcmpl-BQcX8LVJMO5xugwO5bXBrqJVUt5xz',
    choices=[
        Choice(
            finish_reason='tool_calls',
            index=0,
            logprobs=None,
            message=ChatCompletionMessage(
                content=None,
                refusal=None,
                role='assistant',
                audio=None,
                function_call=None,
                tool_calls=[
                    ChatCompletionMessageToolCall(
                        id='call_uR88qhBuY8df7dnasncDI0qI',
                        function=Function(
                            arguments='{"query":"2024 
台灣總統大選當選者","cx":"<搜尋引擎 ID>","api_key":"<搜尋引擎金鑰>","num":3}',
                            name='search_google'
                        ),
                        type='function'
                    )
                ],
                annotations=[]
            )
        )
    ],
    created=1745684350,
    model='gpt-3.5-turbo-0125',
    object='chat.completion',
    service_tier='default',
    system_fingerprint=None,
    usage=CompletionUsage(
        completion_tokens=87,
        prompt_tokens=251,
        total_tokens=338,
        completion_tokens_details=CompletionTokensDetails(
            accepted_prediction_tokens=0,
            audio_tokens=0,
            reasoning_tokens=0,
            rejected_prediction_tokens=0
        ),
        prompt_tokens_details=PromptTokensDetails(
            audio_tokens=0,
            cached_tokens=0
        )
    )
)

回應內容中的 content=None 表示此回應並非實際的回覆 (無內容), 所以 finish_reason 欄不是一般的 'stop' 而是 'tool_calls', 表示模型需要外部工具函式協助, 需要呼叫的函式則是放在 tool_calls 串列中, 每一個串列元素都包裹了一個 Function 物件, 代表一個工具函式 (用串列存放表示可以呼叫多個函式), 函式名稱放在 Function 物件的 name 屬性, 而所需的參數放在 arguments 屬性中. 

我們的應用程式只要將此 tool_calls 回應中的函式名稱與參數取出來後呼叫此函式取得搜尋結果, 再將此函式傳回值與原本的提示詞, 以及上面的 tool_calls 中的回應訊息 message, 用 user 角色一併送回模型做第二次詢問, 它就會參考裡面的搜尋結果做出實際的回應內容.

首先取出 ChatCompletionMessage 物件, 也就是上面 ChatCompletion 物件 reply 的 message 屬性 : 

>>> reply_message=reply.choices[0].message   
>>> type(reply_message)   
<class 'openai.types.chat.chat_completion_message.ChatCompletionMessage'>

然後從回應訊息取出其 tool_calls 屬性, 其值為一個串列, 其元素均為包裹了 Function 物件的 ChatCompletionMessageToolCall 物件 :

>>> tool_calls=reply_message.tool_calls    
>>> tool_calls    
[ChatCompletionMessageToolCall(id='call_LUtiMc54O2NfP1eBmdGahsYt', function=Function(arguments='{"query":"2024 台灣總統大選當選者","cx":"<搜尋引擎 ID>","api_key":"<搜尋引擎金鑰>","num":3}', name='search_google'), type='function')]
>>> type(tool_calls)   
<class 'list'>  
>>> type(tool_calls[0])   
<class 'openai.types.chat.chat_completion_message_tool_call.ChatCompletionMessageToolCall'>
>>> len(tool_calls)   
1

串列長度為 1 表示模型要求呼叫一個指定函式 (空串列表示不需要呼叫外部函式), 利用 tool_calls 物件的 function.name 屬性可取得模型回應我們要去呼叫的外部函式名稱 (字串) :

>>> function_name=tool_calls[0].function.name   
>>> function_name     
'search_google'   

參數則是一個 JSON 格式字串 :

>>> tool_calls[0].function.arguments   
'{"query":"2024 台灣總統大選當選者","cx":"<搜尋引擎 ID>","api_key":"<搜尋引擎金鑰>","num":3}'

可以用 json.loads() 將它轉成 Python 字典,  :

>>> import json     
>>> args=json.loads(tool_calls[0].function.arguments)  
>>> args    
{'query': '2024 台灣總統大選當選者', 'cx': '<搜尋引擎 ID>', 'api_key': '<搜尋引擎金鑰>', 'num': 3}

如果參數毋須修改的話, 只要在 args 前面加上 ** 將參數拆開傳給外部函式即可. 但在此處無法這樣做, 因為 args 裡面的 cx 與 api_key 是模型自己幻想編造出來的無法使用, 這兩個參數必須個人去申請才有效, 參考 :


所以 args 字典裡面的 cx 與 api_key 必須用上面我們透過 dotenv 從環境變數中讀取的值 (變數 custom_search_key 與 cx) 去修改才可以用來傳給外部函式 search_google() : 

>>> args['api_key']=custom_search_key   
>>> args['cx']=cx    
>>> args     
{'query': '2024 台灣總統大選當選人', 'cx': '<我的搜尋引擎ID>', 'api_key': '<我的搜尋引擎金鑰>', 'num': 3}

這樣參數字典中的 cx 與 api_key 就是有效可用的了, 只要將 args 字典用 ** 拆開傳給函式 search_google() 即可. 如果要從模型傳回的函式名稱去呼叫, 可以呼叫內建函式 gloabals(), 它會傳回全域變數字典, 呼叫其 get() 方法並傳入函式名稱字串即可取得函式物件的參考 : 

>>> func=globals().get(function_name)    
>>> func     
<function search_google at 0x000002460701F0A0>

這樣就可以呼叫外部函式 func 了, 它會將 :

>>> results=func(**args)    # 呼叫外部函式 

當然直接呼叫 search_google() 也可以 :

>>> results=search_google(**args) 
>>> len(results)  
3

可見搜尋結果有 3 筆, 檢視最後一筆 :

>>> pprint(results[2])   
{
    'kind': 'customsearch#result',
    'title': '第16屆中華民國總統選舉- 維基百科,自由嘅百科全書',
    'htmlTitle': '第16屆中華民國<b>總統選舉</b>- 維基百科,自由嘅百科全書',
    'link': 
'https://zh-yue.wikipedia.org/wiki/%E7%AC%AC16%E5%B1%86%E4%B8%AD%E8%8F%AF%E6%B0
%91%E5%9C%8B%E7%B8%BD%E7%B5%B1%E9%81%B8%E8%88%89',
    'displayLink': 'zh-yue.wikipedia.org',
    'snippet': 
'賴清德在選舉內得取四成票數,侯友宜獲得三成,柯文哲擭兩成。賴清德總統選舉獲得勝
預計會喺2024年5月20號宣誓就職。 中華民國第16任總統、副總統\xa0...',
    'htmlSnippet': 
'賴清德在選舉內得取四成票數,侯友宜獲得三成,柯文哲擭兩成。賴清德<b>總統選舉</b
預計會喺<b>2024</b>年5月20號宣誓就職。 
中華民國第16任總統、副總統&nbsp;...',
    'formattedUrl': 'https://zh-yue.wikipedia.org/wiki/第16屆中華民國總統選舉',
    'htmlFormattedUrl': 
'https://zh-yue.wikipedia.org/wiki/第16屆中華民國<b>總統</b>選舉',
    'pagemap': {
        'metatags': [
            {
                'referrer': 'origin',
                'og:image': 
'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/%E6%9F%AF%E6%96%87%E
5%93%B2_IMG_9322-1_%2814300234412%29_%28cropped%29.jpg/1200px-%E6%9F%AF%E6%96%8
7%E5%93%B2_IMG_9322-1_%2814300234412%29_%28cropped%29.jpg',
                'theme-color': '#eaecf0',
                'og:image:width': '1200',
                'og:type': 'website',
                'viewport': 'width=device-width, initial-scale=1.0, 
user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0',
                'og:title': '第16屆中華民國總統選舉 - 
維基百科,自由嘅百科全書',
                'og:image:height': '1501',
                'format-detection': 'telephone=no'
            }
        ]
    }
}

search_google() 的傳回值是字典串列, 其中有參考價值的鍵是 title 與 snippet, 因此利用迴圈將這兩個鍵之值串成字串, 這個搜尋結果字串稍後要放進角色為 tool 的訊息字典中的 content 鍵 :  

>>> web_msg=[]   
>>> for result in results: 
    web_msg.append(f'標題: {result["title"]}')
    web_msg.append(f'描述: {result["snippet"]}') 
>>> content='\n'.join(web_msg)   

接下來要將上面第一次詢問得到的回應訊息 reply_message (是一個 ChatCompletionMessage 物件) 直接放進查詢訊息串列 messages 中 :

>>> messages.append(reply_message)  

然後製作一個包含 tool_code_id (呼叫外部工具 id), role (角色, 固定為 'tool'), name (函式名稱), 以及 content (外部函式的搜尋結果字串) 四個鍵的字典 : 

>>> results_dict={
    "tool_call_id": tool_calls[0].id,
    "role": "tool",
    "name": function_name,
    "content": content} 

同樣將其加入 messages 串列中 : 

>>> messages.append(results_dict)      
>>> messages   
[{'role': 'system', 'content': '你是一個繁體中文AI助理'}, {'role': 'user', 'content': '2024台灣總統大選是誰當選?'}, ChatCompletionMessage(content=None, refusal=None, role='assistant', audio=None, function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_RYkrPwyTkaCC92abOT0GXXdh', function=Function(arguments='{"query":"2024台灣總統大選當選人","cx":"3395206c2d90694f9","api_key":"AIzaSyA-J_1DWGJHfDcltPm3U0onRMy8-OnlCU0","num":3}', name='search_google'), type='function')], annotations=[]), {'tool_call_id': 'call_RYkrPwyTkaCC92abOT0GXXdh', 'role': 'tool', 'name': 'search_google', 'content': '標題: 2024年中華民國總統選舉- 維基百科,自由的百科全書\n描述: 本次選舉候選人共有三組,依登記號次分別為台灣民眾黨推薦的黨主席柯文哲及全國不分區立法委員吳欣盈(通稱「柯吳配」或「柯盈配」)、民主進步黨推薦的副總統兼黨主席賴清德\xa0...\n標題: 台灣大選2024:賴清德當選總統民進黨未能控制立法院- BBC News ...\n描述: Jan 13, 2024 ... 1月13日,台灣舉行2024年總統選舉與立法委員選舉,執政黨民進黨候選人賴清德以超過558萬票勝選。\n標題: 第16屆中華民國總統選舉- 維基百科,自由嘅百科全書\n描述: 賴清德在選舉內得取四成票數,侯友宜獲得三成,柯文哲擭兩成。賴清德總統選舉獲得勝利,將預計會喺2024年5月20號宣誓就職。 中華民國第16任總統、副總統\xa0...'}]

最後用此包含搜尋結果的訊息字典串列 messages 向模型提出第二次詢問 :

>>> second_reply=client.chat.completions.create(
    model=model, 
    messages=messages)

就會得到正確的答案了 : 

>>> print(second_reply.choices[0].message.content)   
根據搜尋結果,2024年台灣總統大選賴清德當選,成功連任總統。

不用再用那不十分確定的回應模板真是太好了! 

不過有一個問題, 如果問 GPT 模型它確知的問題, 但攜帶 tools 與 tool_choice='auto' 參數時它會怎麼做? 例如問它一定知道的 2016 年台灣總統大選結果 : 

>>> messages=[
    {'role': 'system', 'content': '你是一個繁體中文AI助理'},
    {'role': 'user', 'content': '2016台灣總統大選是誰當選?'}
    ]
>>> reply=client.chat.completions.create(
    model=model, 
    messages=messages)   
>>> print(reply.choices[0].message.content)    
2016年台灣總統大選當選的是蔡英文,她是台灣歷史上第一位女性總統。

因為這是遠早於訓練資料截止日的事件, 所以模型回應了正確答案. 

如果改成攜帶 tools 與 tool_choice='auto' 參數去問 gpt-3.5-turbo 模型 : 

>>> reply=client.chat.completions.create(
    model=model, 
    messages=messages,
    tools=tools,  
    tool_choice='auto'   
    ) 
>>> print(reply.choices[0].message.content)   
None  

奇怪, 它居然沒直接回應它確知的答案?  tool_choice='auto' 不是叫它自動判斷是否知道答案嗎? 明明知道為何沒回答? 檢視完整的 reply 內容發現它居然要求 call-out 協助 : 

>>> pprint(reply)   
ChatCompletion(
    id='chatcmpl-BQuZvSWqz21zYXzf0mJP7XEJiCfDA',
    choices=[
        Choice(
            finish_reason='tool_calls',
            index=0,
            logprobs=None,
            message=ChatCompletionMessage(
                content=None,
                refusal=None,
                role='assistant',
                audio=None,
                function_call=None,
                tool_calls=[
                    ChatCompletionMessageToolCall(
                        id='call_MHkxV5UqX2YdyIbHPivHfvkm',
                        function=Function(
                            arguments='{"query":"2016台灣總統大選結果","cx":"24
91495a0","api_key":"AIzaSyA2woHvIIooLIlLWVd3zphLptQ_8WBdgSk","num":3}',
                            name='search_google'
                        ),
                        type='function'
                    )
                ],
                annotations=[]
            )
        )
    ],
    created=1745753715,
    model='gpt-3.5-turbo-0125',
    object='chat.completion',
    service_tier='default',
    system_fingerprint=None,
    usage=CompletionUsage(
        completion_tokens=80,
        prompt_tokens=251,
        total_tokens=331,
        completion_tokens_details=CompletionTokensDetails(
            accepted_prediction_tokens=0,
            audio_tokens=0,
            reasoning_tokens=0,
            rejected_prediction_tokens=0
        ),
        prompt_tokens_details=PromptTokensDetails(
            audio_tokens=0,
            cached_tokens=0
        )
    )
)

我問 ChatGPT 為何會這樣, 原來是 OpenAI 在設計 Function calling 功能時採取了較保守的思維, 如果啟用 tools 參數且 tool_choice='auto' 的話, 即使所問的問題是它知道的知識, 也會讓模型優先考慮呼叫外掛工具, 這樣可以取得即時或更準確的資料. 

在開啟 tools 功能情況下, 如果要讓模型在它確知答案時直接回答不要去 call out 外掛, 那麼就不要指定 tool_choice 參數, 但我測試發現這並不一定, 大多數時候會直接回答, 但偶而還是會要求 call out, 例如 : 

>>> messages=[
    {'role': 'system', 'content': '你是一個繁體中文AI助理'},
    {'role': 'user', 'content': '2016台灣總統大選是誰當選?'}
    ]
>>> reply=client.chat.completions.create(
    model=model, 
    messages=messages,
    tools=tools
    )    
>>> print(reply.choices[0].message.content)   
2016年台灣總統大選中,蔡英文當選為台灣第14任總統。
>>> reply=client.chat.completions.create(
    model=model, 
    messages=messages,
    tools=tools)  
>>> print(reply.choices[0].message.content)   
None   
>>> reply=client.chat.completions.create(
    model=model, 
    messages=messages,
    tools=tools)   
>>> print(reply.choices[0].message.content)   
2016年台灣總統大選中,蔡英文當選為台灣第14任總統。
>>> reply=client.chat.completions.create(
    model=model, 
    messages=messages,
    tools=tools)
>>> print(reply.choices[0].message.content)   
2016年台灣總統大選中,蔡英文當選為台灣第14任總統。

連續詢問四次, 有三次直接回應答案, 有一次回答 None, 也就是尋求外部函式協助. 

綜合以上測試可知, 我們可以在每次詢問時都開啟 tools 功能 (但不傳送 tool_choice 參數), 然後利用回應訊息 reply.choices[0].message.content 的值是否為 None 來判斷是否需要呼叫外部函式協助.

根據上面測試結果, 我改寫了如下之 Function calling 版命令列聊天機器人 (無記憶) :

# cli_chatbot_function_calling_1.py
from openai import OpenAI, APIError
import requests
import json
from dotenv import load_dotenv
import os

def ask_gpt(messages, tools=[], model='gpt-3.5-turbo'):
    try:
        reply=client.chat.completions.create(
            model=model, 
            messages=messages,
            tools=tools
            )
        return reply
    except APIError as e:
        return e.message

def search_google(query, cx, api_key, num=3, gl='tw', lr='lang_zh_TW'):
    params={
        'q': query,
        'key': api_key,
        'cx': cx,
        'gl': gl,  
        'lr': lr,          
        'num': num          
        }
    url='https://www.googleapis.com/customsearch/v1'
    r=requests.get(url, params=params)
    data=r.json()
    return data.get('items', [])

# 載入環境變數
load_dotenv()
openai_api_key=os.environ.get('OPENAI_API')
custom_search_key=os.environ.get('GOOGLE_CUSTOM_SEARCH_API')
cx=os.environ.get('SEARCH_ENGINE_ID')
# 設定外部工具函式串列
tools=[{
    "type": "function",
    "function": {
        "name": "search_google",
        "description": "用 Google Custom Search API 取得搜尋結果",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "要搜尋的關鍵字"
                    },
                "cx": {
                    "type": "string",
                    "description": "Google Custom Search 搜尋引擎 ID"
                    },
                "api_key": {
                    "type": "string",
                    "description": "Google Custom Search API 金鑰"
                    },
                "num": {
                    "type": "integer",
                    "description": "回傳的搜尋結果筆數(最多 10 筆)",
                    "default": 3
                    },
                "gl": {
                    "type": "string",
                    "description": "地理區域 (預設 'tw' 台灣)",
                    "default": "tw"
                    },
                "lr": {
                    "type": "string",
                    "description": "語言 (預設 'lang_zh_TW' 繁體中文)",
                    "default": "lang_zh_TW"
                    }
                },
            "required": ["query", "cx", "api_key"]
            }
        }
    }]
# 建立 OpenAI 物件
client=OpenAI(api_key=openai_api_key)
# 設定 GPT 系統角色
sys_role=input('請設定 GPT 角色 : ')
if sys_role.strip() == '': 
    sys_role='你是繁體中文AI助理'
print(sys_role)

# 聊天機器人 (無窮迴圈)
while True: 
    prompt=input('You : ')  # 輸入提示詞
    if prompt.strip() == '':  # 按 Enter 跳出迴圈結束聊天
        print('GPT : 再見。')
        break
    messages=[{'role': 'system', 'content': sys_role},
              {'role': 'user', 'content': prompt}]  # 初始化訊息
    # 第一次查詢 : 確定是否需要搜尋網路
    reply=ask_gpt(messages, tools=tools)
    reply_message=reply.choices[0].message  # 取出回應訊息
    if reply_message.content == None:  # 內容為空表示需要搜尋網路
        messages.append(reply_message)  # 回應放入 messages 串列
        tool_calls=reply_message.tool_calls  # 取得外部工具函式物件
        function_name=tool_calls[0].function.name  # 取得函式名稱
        args=json.loads(tool_calls[0].function.arguments) # 取得參數
        args['api_key']=custom_search_key  # 設定搜尋引擎金鑰
        args['cx']=cx  # 設定搜尋引擎ID
        func=globals().get(function_name)  # 取得函式參考
        results=func(**args)  # 呼叫外部函式, 也可以用 search_google(**args)
        web_msg=[]  # 用來儲存搜尋結果
        for result in results: 
            web_msg.append(f'標題: {result["title"]}')
            web_msg.append(f'描述: {result["snippet"]}') 
        content='\n'.join(web_msg)  # 搜尋結果串接為字串
        results_dict={  # 製作 JSON 格式的搜尋結果字典
            "tool_call_id": tool_calls[0].id,
            "role": "tool",
            "name": function_name,
            "content": content
            }
        messages.append(results_dict)  # 將搜尋結果字典放進 messages         
        # 第二次查詢 : 根據所提供的網路搜尋結果回答
        reply2=ask_gpt(messages, tools=tools)
        response=reply2.choices[0].message.content
    else:  # 不需搜尋網路, 直接回應答案
        response=reply_message.content
    print(f'GPT : {response}') 

注意, 此處的 ask_gpt() 直接將呼叫 client.chat.completions.create() 的結果傳回以便分析模型是否需要呼叫外部工具函式. 

結果如下 : 

>>> %Run cli_chatbot_function_calling_1.py   
請設定 GPT 角色 : 
你是繁體中文AI助理
You : 2024台灣總統大選是誰當選?
GPT : 根據搜尋結果,2024年台灣總統大選中,民進黨候選人賴清德以超過558萬票勝選,成為當選總統。
You : 2016台灣總統大選是誰當選?
GPT : 2016年台灣總統大選的當選者是蔡英文。她在此次選舉中成為台灣史上首位女性最高統治者,也是中華民國首位女性、未婚、擁有這一榮銜的領導人。
You : 2024 台灣金曲歌王是誰?
GPT : 根據搜索結果,2024年台灣金曲歌王是 MC HotDog。
You : 
GPT : 再見。

嗯, 看起來只要有呼叫外部函式的, 回應都會以 "根據搜索結果" 開頭, 否則就直接回應. 

沒有留言 :