OpenAI對話機器人API最小範例及進階用法

湯沂達(Yi-Dar, Tang)
18 min readSep 19, 2023

--

OpenAI引起了2023大語言模型的風潮,其中openai.ChatCompletion 是在與GPT3.5、GPT4模型溝通時最常使用的函式,這篇文章會介紹基礎用法以及如何使用GPT幫使用者去呼叫函數,以及進階的函數寫法。

這篇文章涵蓋了以下

  • chat.completions最小範例:用程式化的方式得到ChatGPT介面的輸出結果
  • 自建 tools:讓GPT幫你填充指定格式的輸出,讓GPT能去跟定義好的服務互動
  • 複雜點的tool:能填充更為複雜的格式
  • 特化型文本:讓openai輸出同時含有解釋以及json的輸出的結果
  • 個人觀點

程式碼位置:
https://colab.research.google.com/gist/mistake0316/f6dfca300760a45507727ecb0ee826aa/openai_example_greater_than_1.ipynb

閱讀門檻:有用過ChatGPT、看得懂簡單python
使用門檻:有OpenAI_API_Key(需要使用信用卡支付😿)

此篇文章更新於2023十一月,來服從openai pip ≥ 1的版本
技術迭代迅速
也許日後會有更好的方式來取代下方例子

舊版程式碼撰於2023九月
該版本程式碼位於(openai pip版本 < 1)https://colab.research.google.com/gist/mistake0316/fce3b6a0ff051b3257a69f35f9d95908/openai-chatcompletion-api-function_call.ipynb

最小範例

官方介紹:https://platform.openai.com/docs/guides/gpt/chat-completions-api

相信點開這篇文的人都已經使用過ChatGPT了。
如果想要自己嘗試使用程式化的方式去呼叫GPT的話,OpenAI已經有提供非常簡易的接口去串接,如下方所示

要讓GPT的語言模型去生成新的輸出,需要給定要使用哪個模型(model),以及歷史紀錄(messages),其中根據是使用者還是GPT的輸出而給定role是user還是assistant,content則是給對應的文本。

import openai
client = openai.Client(api_key="sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
model = "gpt-3.5-turbo"
messages_simple_chat = [
{"role":"user", "content":"要怎麼發表會引起大眾關注的文章?\n請簡短回答"},
{"role":"assistant", "content":"要發表引起大眾關注的文章,可以從以下幾個方向入手:\n1. **話題熱點:** 選擇當前社會、科技或文化熱門話題作為主題,以引起讀者興趣。\n2. **獨特觀點:** 提出獨特、挑戰性的觀點或見解,突顯與眾不同的思考。\n3. **情感共鳴:** 故事化的方式能引起情感共鳴,吸引讀者關注並引起共鳴。\n4. **數據和研究:** 使用可信的數據和研究結果支持文章觀點,增加文章的可信度。\n5. **引用專家:** 引用權威專家的觀點和意見,提升文章的可信度和影響力。\n6. **生動描寫:** 用生動的語言描寫場景、人物或情境,讓讀者身臨其境。\n7. **題綱明確:** 文章開頭即表明主題和立場,吸引讀者繼續閱讀以了解更多。\n8. **多媒體內容:** 添加圖片、視頻或其他多媒體元素,提升文章的視覺吸引力。\n\n總之,引起大眾關注的文章需要獨特性、情感共鳴、可靠的支持和生動的敘述,以及與當下話題相關的內容。"},
{"role":"user", "content":"好喔"},
]
result_simple_chat = client.chat.completions.create(
model=model,
messages = messages_simple_chat,
)
print(result_simple_chat.choices[0].message)

OpenAI會回傳新的訊息,若要將其存到歷史紀錄以供未來使用,則將回傳訊息以及下一則用戶訊息到messages後再呼叫一次chat.completion即可。

而要讓新增訊息自動化,可以創建一個函數去自動更新

自建 tools

當然,一個純粹的大型語言模型可能會有一些毛病,下列是一些對編程人員最淺而易見的

  1. 沒辦法取得實時資料,或要使用者自行輸入
  2. 純文字輸出跟傳統的編程的結構化格式不統一
  3. 有時候會亂幻想(譬如數學式算錯、產生的SQL跟真實的資料庫的欄位無法對上)
  4. 沒有媒介去跟既有的服務對接

chat.completion中的tools是能解決上述問題的方法之一。

使用者可以透過OpenAI定義的溝通協定,達到以下

  1. 可以創建函式去要求去網路、資料庫中搜尋資料
  2. 可以強制要求輸出必須滿足某種格式,而後可以變成結構化資料,方便輸入到資料庫
  3. 可以提供真實的引擎來執行邏輯
  4. 可以提供真實的接口去與既有服務互動

其中 function_call 有兩種做法,一種是讓 GPT 自行決定要不要呼叫函數,另一種是直接強制ChatGPT去填充指定函數的參數

官方範例:
https://cookbook.openai.com/examples/how_to_call_functions_with_chat_models

要讓GPT去呼叫函數可以參考OpenAI官方範例,大致概念如下

  1. 該範例提供一個取得一些取得當前狀況的函數
  2. GPT會根據對話自行決定要呼叫函式或者純粹對話 (function_call設定為”auto”)
  3. 若呼叫該函式,用戶端需實際去執行程式碼,並將實際得到的結果傳給GPT
  4. GPT會根據函數結果以及歷史紀錄去繼續對話

與範例不同,如果要強制讓GPT去呼叫函式可以仿造下圖 (定義好一個funcion的格式,tool_choice設定為{“type”:”function”, “function”: {“name”: “function_name”})}

openai的模型要照著json_schema的格式輸出,可以參考下圖的定義

輸出會是result.choices[0].message.tool_calls[…].function.arguments,該輸出是json格式,能夠非常輕易用配合的程式去串接。

system_message_TRPG = """\
你是一個TRPG的主持人
你要根據骰子的點數來敘述故事

通常情況下:
* 骰子總點數對為1點時發生對玩家極端糟糕的事件
* 骰子總點數對為2~3點時發生對玩家有點糟糕的事件
* 骰子總點數對為4~5點時發生對玩家有點好的事件
* 骰子總點數對為6點時發生對玩家極端好的事件
"""
round_status = """\
村民當前血量為 10,力量為1,敏捷為1
哥布林當前血量為 5, 力量為2,敏捷為5

這回合是村民與哥布林對戰。
玩家擲出骰子為1點
"""
messages_TRPG = [
{"role":"system", "content":system_message_TRPG},
{"role":"user", "content":round_status}
]


dungeon_master_story_tellerfunction_definition = {
"name" : "dungeon_master_story_teller",
"description": "請填入該回合故事情節。",
"parameters":{
"type":"object",
"properties":{
"怪物行動":{
"type":"string",
"description":"怪物採取的行動,請戲劇化的敘述",
},
"玩家行動":{
"type":"string",
"description":"玩家採取的行動,請戲劇化的敘述",
},
"故事":{
"type":"string",
"description":"這回合的故事,請詳盡敘述",
},
},
"required": ["怪物行動", "玩家行動", "故事"]
}
}

tool_choice_TRPG = {
"type":"function",
"function": {"name": "dungeon_master_story_teller"}
}

TRPG_result = client.chat.completions.create(
model=model,
messages=messages_TRPG,
tools=[
{"type":"function", "function":dungeon_master_story_tellerfunction_definition}
],
tool_choice=tool_choice_TRPG,
)

for ith_tool, tool in enumerate(TRPG_result.choices[0].message.tool_calls, 1):
print(f"# {ith_tool}-th tool")
print(tool.function.arguments)

複雜點的tool

在上面那個例子中,我們定義的輸出非常簡單,只有三個字串

function_call使用json schema的定義方式,json schema能支援非常複雜的邏輯

下面的例子是個希望輸出為一個array,並且array裡面的每個物件都是一個有三個屬性的object

openai 提供的 json schema資源: https://json-schema.org/understanding-json-schema/

system_message_scriptwriter = """\
你是一個編劇,你要根據使用者輸入編寫一個至少3分鐘的劇本
"""
content_scriptwriter = """\
請你描述一個大學食堂阿姨與翹課生的對話故事

食堂阿姨想要勸翹課生回去上課
蹺班生覺得大學通識課都的在教一些用不到的東西
"""
messages_scriptwriters = [
{"role":"system", "content":system_message_scriptwriter},
{"role":"user", "content":content_scriptwriter}
]

scriptwriterfunction_definition = {
"name" : "movie_script",
"parameters":{
"type":"object",
"properties":{
"場景":{
"type":"array",
"minItems": 5,
"maxItems": 20,
"items":{
"type":"object",
"properties":{
"角色":{"type": "string"},
"對白":{"type": "string"},
"表情":{"type": "string"},
},
"required":["角色", "對白", "表情"],
},

}
},
"required":["場景"],
},
}


tool_choice_scriptwriters = {
"type":"function",
"function": {"name": "movie_script"}
}

scriptwriters_result = client.chat.completions.create(
model=model,
messages=messages_scriptwriters,
tools=[
{"type":"function", "function":scriptwriterfunction_definition}
],
tool_choice=tool_choice_scriptwriters,
)

for ith_tool, tool in enumerate(scriptwriters_result.choices[0].message.tool_calls, 1):
print(f"# {ith_tool}-th tool")
print(tool.function.arguments)

特化型文本

OpenAI Prompt Engineering: 給模型時間”思考”

https://platform.openai.com/docs/guides/prompt-engineering/#:~:text=Give%20the%20model%20time%20to%20%22think%22

透過預先要求,可以讓openai輸出同時含有解釋以及json的輸出的結果

個人覺得這方法有以下優點

  • 體感輸出品質會比較高,因為模型有先思考後再給出json結果
  • 給予適合人類閱讀的資訊
  • 同時也產生適合資料庫的輸出
  • 建構方法能夠塞給非OpenAI的模型使用
travel_plan_schema = {
"type":"array",
"minItems": 3,
"items":{
"type":"object",
"properties":{
"活動內容":{"type": "string"},
"停留時間":{"type": "string"},
"大致敘述":{"type": "string"},
"重點項目":{
"type": "array",
"description":"如風景、購買項目、等等",
"items":{"type":"string"}
},
},
"required":["活動內容", "停留時間", "大致敘述","重點項目"],
},
}
system_message_travel_plan = f"""\
你是一個專業的地方導遊
你的任務是計畫每天的行程

公司的資料庫格式如下:
```json
{json.dumps(travel_plan_schema, indent=2,ensure_ascii=False)}
```

你的輸出必須滿足以下格式:
1. 對於使用者的問題做一個完整的企劃敘述,必須非常生動
2. 輸出一個滿足資料庫格式的結構化資料 (必須被"```json\n...\n```"包圍)
"""
travel_plan_content = "幫我規劃一個在帶初次約會對象去迪化街的總共三小時的行程"
messages_travel_plan = [
{"role":"system", "content":system_message_travel_plan},
{"role":"user", "content":travel_plan_content},
]

travel_plan_result = client.chat.completions.create(
model="gpt-4-1106-preview", # 這邊若用 gpt-3.5-turbo 很不穩定
messages=messages_travel_plan,
)
print(travel_plan_result.choices[0].message.content)

def extract_json(text, schema):
l = text.find("```json")+7
r = text.rfind("```")
J = json.loads(
text[l:r]
)
validate(J, schema) # 要確認輸出與要求的資料庫格式吻合,否則會raise error
return J

extracted_json = extract_json(
travel_plan_result.choices[0].message.content,
travel_plan_schema,
)
print(extracted_json)

個人觀點

大語言模型創造了許許多多的可能性,個人看到的目前的狀況如下

跟傳統方法比較

該方法能夠用一步到位的無標籤方式取來代一些傳統的方法,像是文本分類問題(如:文章:[商業/教育/政治],評論:[正面/負面/不好不壞],複雜邏輯結論),但如果是已經存在許久且能取得標籤的問題,個人還是認為傳統的方法比起大語言模型會是使用更少的運算資源且表現的更好。

對於編程人員而言

個人認為大語言模型幫編程人員幫編程人員解鎖了新的可能性,用大語言模型能夠讓終端使用者更靠近核心邏輯,個人目前見到最好的例子有兩個:

  • Zapier
    Zapier本業是要建立不同應用程式之間的接口(像google文件、google日曆、多社群發文、寄信等等),他們可以透過大型語言模型去讓使用者更願意去使用他們的街口。
    一個簡單的技術可行性應用:在google日曆中有對應會議,且透過其他服務得到對應會議的每個發言人的逐字稿,透過大語言模型輕易的將該會議去產出指定格式的會議摘要,然後寄信給所有參與會議的人。
  • ChatGPT Advanced Data Analysis (ChatGPT Code Interpreter)
    先前,編程人員都在嘗試將邏輯轉換為程式碼,而現在,連非編成人員都可以用ChatGPT Advanced Data Analysis高度客製化的幹到這件事情。
    PAPAYA提供了一個非常值得參考的影片,在該影片中他
    * 對csv做修改、處理
    * 產生圖表
    * 產生影片
    * 等等
    可以想像,要做到這麼高度客製化的事情,那原先的流程會是要一步步的搜索不同的服務,然後手動的將那些邏輯的輸入輸出串在一起。
    個人認為有了這工具後,編程人員要嘗試更貼近核心演算法或創意。不複雜的的ETL[擷取(extract)、轉換(transform) 、載入(load)],只要你寫對了正確的問法,那大語言模型能幫你寫出能用的程式碼。我相信目前而言大語言模型對複雜邏輯或高效能的核心演算法很難表現得比人還好;而不複雜的問題能被高效處理了,那編程人員就能夠花更多時間去發想創意,創造出更貼近終端使用者的應用。

限制

  • 運算很貴
  • 輸出不穩定
  • 輸入、輸出長度有限制
  • 需要人力將問題轉換成敘述給語言模型的文字
  • OpenAI不開源 :(

個人結語

我想現階段而言,大語言模型就像昂貴的租賃噴射背包

被經過良好訓練的人使用,祂能夠幹出一些非常特別、有效、且非大語言模型不可的事情

但是如果只是要從大樓的1樓移動到3樓,那為什麼不是走樓梯或搭電梯?

--

--

湯沂達(Yi-Dar, Tang)

Self supervised writer. Ask question to myself. Transfer the meaningful path as an article.