前言
- 用自然語言連接系統的認知,面向未來思考系統間的集成
- GPTs 是如何連接外部世界的
- 用 Function Calling(函數調用) 把大模型和業務連接起來
一、接口介紹
1. 接口 (Interface)
兩種常見接口:
- 人機交互接口,User Interface, 簡稱 UI
- 應用程序編程接口,Application Programming Interface, 簡稱 API
接口能「通」的關鍵,是兩邊都要遵守約定。
- 人要按照 UI 的設計來操作。UI 的設計要符合人的習慣
- 程序要按照 API 的設計來調用。API 的設計要符合程序慣例
2. 接口的進化
UI進化的趨勢是:越來越適應人的習慣,越來越自然
- 命令行,Command Line Interface, 簡稱 CLI (DOS、Unix/Linux shell, Windows Power Shell)
- 圖形界面,Graphical User Interface, 簡稱 GUl (Windows、MacOS、iOS、Android)
- 語言界面,Conversational User Interface, 簡稱CUI,或 Natural-Language User Interface,簡稱LUI ← 我們在這里
-
腦機接口,Brain-Computer Interface, 簡稱 BCI
API
- 從本地到遠程,從同步到異步,媒介發生很多變化,但本質一直沒變:程序員的約定
- 現在,開始進化到自然語言接口,Natural-Language Interface, 簡稱 NLI(自然語言與自然語言直接進行傳遞/對接/操作)
3. 自然語言接口 (Natural Language Interface,簡稱 NLI)
NLI是我們在 《以ChatGPT 為代表的「大模型」會是多大的技術革命?》一文中提出的概念。
用戶操作習慣的遷移,會逼所有軟件,都得提供「自然語言界面 (NaturalLanguage lnterface, 簡稱 NLI) 」。這是我生造的詞,指的是以自然語言為輸入的接口。
不僅用戶界面要 NLI, API也要NLI化。這是因為用戶發出的宏觀指令,往往不會是一個獨立軟件能解決的,它需要很多軟件、設備的配合。
一種實現思路是,入口Al(比如 Siri、小愛同學,機器人管家) 非常強大,能充分了解所有軟件和設備的能力,且能準確地把用戶任務拆解和分發下去。這對入口 Al 的要求非常高。
另一種實現思路是,入口 AI 收到自然語言指令,把指令通過 NLI廣播出去(也可以基于某些規則做有選擇的廣播,保護用戶隱私),由各個軟件自主決策接不接這個指令,接了要怎么做,該和誰配合。
......
當 NLI 成為事實標準,那么互聯網上軟件、服務的互通性會大幅提升,不再受各種協議、接口的限制。
最自然的接口,就是自然語言接口:
以前因為計算機處理不對自然語言,所以有了那么多編程語言,那么多接口,那么多協議,那么多界面風格。而且,它們每一次進化,都是為了「更自然」。現在,終極的自然,到來了。我們終于可以把計算機當人看了!
二、大模型連接外部世界
OpenAl 是如何用自然語言連接一切的呢?
ChatGPT 能聽懂自然語言,但是怎么和我們的業務系統進行連接呢?
方式1:我們可以通過提示詞來控制大模型輸出JSON格式數據,然后再與我們系統來產生連接。但是這種方式存在很多不穩定性,可控性不好。
方式2:使用 OpenAI推出的 Function Calling 技術,它可以讓大語言模型和一切產生連接。
為什么要大模型連接外部世界?
大模型兩大缺陷:
-
并非知曉一切
A. 訓練數據不可能什么都有。垂直、非公開數據必有欠缺
B. 不知道最新信息。大模型的訓練周期很長,且更新一次耗資巨大,還有越訓越傻的風險。所以 ta 不可能實時訓練。GPT-3.5 的知識截至 2022年1月,GPT-4是2023年4月。 - 沒有「真邏輯」。它表現出的邏輯、推理,是訓練文本的統計規律,而不是真正的邏輯。(大模型本質是基于統計規律/概率去猜下一個字或詞)
所以:大模型需要連接真實世界,并對接真邏輯系統,才能補全缺陷產生真正的價值!
比如算加法:
1.把 100 以內所有加法算式都訓練給大模型,ta 就能回答 100以內的加法算式
2.如果問 ta 更大數字的加法,就不一定對了
3.因為 ta 并不懂「加法」,只是記佳了100 以內的加法算式的統計規律
4.Ta 沒有真邏輯,相當于是用字面意義做數學
三、Plugins / Actions 的發展
Plugins 是大模型連接真實世界第一次嘗試,但產品很不成功
1. Plugins 開發
- Actions 是 Plugins 的升級,是 GPTs 產品的一部分。
- 可能是史上最容易開發的 plugin。只需要定義兩個文件:
yourdomain.com/.well-known/ai-plugin.json
:描述插件的基本信息
openai.yaml
:描述插件的 API(Swagger 生成的文檔) - 配置文件中,description 的內容非常重要,決定了 ChatGPT 會不會調用你的插件,調用得是否正確。
- 而 OpenAI 那邊,更簡單,沒有任何人和你對接。是 AI 和你對接!AI 閱讀上面兩個文件,就知道該怎么調用你了。(自然語言對接接口 NLI)
2. Plugins 缺陷
- 缺少「強 Agent」調度,只能手工選三個 plugin,使用成本太高。(解決此問題,相當于 App Store + Siri,可挑戰手機操作系統地位)
- 不在「場景」中,不能提供端到端一攬子服務。(解決此問題,就是全能私人助理了,人類唯一需要的軟件)
- 開銷大。(至少兩次 GPT-4 生成,和一次 Web API 調用)
第二次嘗試:升級為 Actions,內置到 GPTs 中,解決了落地場景問題。
3. 升級為 Actions
“Add actions” 功能是 GPTs 中的一個高級功能,允許用戶將自定義聊天GPT與第三方API集成,以便執行特定動作或檢索數據。
什么是 GPTs?GPTs 是 OpenAI 推出的自定義 GPT,即用戶可以自定義聊天機器人,并發布到 OpenAI 的應用商店。
如:我們自定義的聊天機器人「小瓜 GPT」 ,通過在 GPTs 中添加 actions 接入了高德地圖API,具備回答位置相關的問題:https://chat.openai.com/g/g-DxRsTzzep-xiao-gua
注意:需要升級開通 GPT-4 后,才能使用 GPTs(即自定義聊天機器人的功能)
GPTs 這樣解決問題:
- 每個 GPT 有一個場景,比如「寫代碼」「教小孩數學」「某某人的化身」
- 被 GPT 綁定的 Actions 被自動調用,縮小了 agent 調度的難度
- GPT-4 提速又降價
作為開發者,我們:
- 可以開發 Actions,搭建自己的 GPTs
- 還可以使用 Assistants API,脫離 ChatGPT 做獨立智能應用
4. Actions 的工作流程:
- 人向OpenAI發起一個對話,這個對話是會觸發 action 的 prompt
如:
prompt1:中關村附近的聯通營業廳有哪些?
prompt1 會觸發某個action
prompt2:附近的聯通營業廳有哪些?
prompt2 不會觸發某個action
- OpenAI會理解我們發起的對話內容prompt,從里面提取關鍵信息生成對 action 的調用參數。
- 然后去調用外部的API,并返回調用結果給 OpenAI
- 最后 OpenAI 會根據 外部API調用結果內容 再結合 我們提問的內容生成回答。
思考:GPT 怎么把 prompt 和 API 功能做匹配的?
5. Actions 開發對接
Actions 官方文檔:https://platform.openai.com/docs/actions
把 API 對接到 GPTs 里,只需要配置一段 API 描述信息:
openapi: 3.1.0
info:
title: 高德地圖
description: 獲取 POI 的相關信息
version: 'v1.0.0'
servers:
- url: https://restapi.amap.com/v5/place
paths:
/text:
get:
description: 根據POI名稱,獲得POI的經緯度坐標
operationId: get_location_coordinate
parameters:
- name: keywords
in: query
description: POI名稱,必須是中文
required: true
schema:
type: string
- name: region
in: query
description: POI所在的區域名,必領是中文
required: false
schema:
type: string
deprecated: false
/around:
get:
description: 搜索給定坐標附近的POI
operationId: search_nearby_pois
parameters:
- name: keywords
in: query
description: 目標POI的關鍵字
required: true
schema:
type: string
- name: location
in: query
description: 中心點的經度和緯度,用逗號分隔
required: false
schema:
type: string
deprecated: false
components:
schemas: {}
這里的所有name、description 都是prompt,決定了 GPT 會不會調用你的 APl,調用得是否正確。
還需要配置 APl Key 來滿足權限要求。
思考:為什么不干脆整個描述文件都用自然語言寫?非要用結構化的 JSON 或
YAML?
是為了提高準確度,為了防止幻覺,為了避免歧義,保證穩定性,所以使用明確的結構化方式來表示。
四、GPTs 與它的平替們
1. OpenAI GPTs,GPTs的好處:
- 無需編程,就能定制個性對話機器人的平臺
- 可以放入自己的知識庫,實現 RAG (后面會講)
- 可以通過 actions 對接專有數據和功能
- 內置 DALLE3 文生圖和 Code Interpreter 能力
- 只有 ChatGPT Plus 會員可以使用
沒有 ChatGPT Plus 會員,推薦兩款平替:
2. 字節跳動 Coze
- 可以
免科學上網,免費使用 GPT-4 等 OpenAl的服務!大羊毛! - 只有英文界面,但其實對中文更友好
- Prompt 優化功能更簡單直接
- 「iOS編程助手」的提示詞:下面是系統幫我們優化后的提示詞,是MarkDown格式,OpenAI對MarkDown格式支持比較友好。
# 角色
你是一位資深的iOS程序員,擅長Objective-C語言開發。你有著豐富的iOS開發經驗,可以針對用戶在iOS開發中遇到的問題提供專業解答和代碼示例。
## 技能
### 技能1: 問題解答
- 根據用戶的問題,給出具體的解決方案。
- 如有需要,提供Objective-C語言的代碼示例助其理解。
### 技能2: 代碼優化
- 針對用戶提供的Objective-C代碼片段,給出優化建議。
- 提供優化后的代碼示例。
### 技能3: iOS開發知識分享
- 根據用戶的疑問,分享相關的iOS開發知識。
- 幫助用戶理解iOS開發的核心概念和最佳實踐。
## 約束條件
- 只回答和解決與iOS開發相關的問題。
- 提供的代碼示例只使用Objective-C語言。
- 應答始于對問題的清晰解答,如果涉及代碼,應該提供代碼示例。
3. Dify
- 開源,中國公司開發
- 功能最豐富
- 可以本地部署,支持非常多的大模型
- 有GUI,也有API
有這類無需開發的工具,為什么還要學大模型開發技術呢?
- 它們都無法針對業務需求做極致調優
- 它們和其它業務系統的集成不是特別方便
五、Function calling
Function calling(函數調用)技術:是一種大模型連接到外部的工具。
官方介紹
在 API 調用中,您可以描述函數,并讓模型智能地選擇輸出包含調用一個或多個函數的參數的 JSON 對象。聊天完成 API 不會調用該函數;相反,模型會生成 JSON,您可以使用它來調用代碼中的函數。
最新的模型 (gpt-3.5-turbo-0125
和 gpt-4-turbo-preview
) 經過訓練,可以檢測何時應該調用函數(取決于輸入),并使用比以前的模型更緊密地遵循函數簽名的 JSON 進行響應。
Function calling 的工作流程
- 用戶向我們的應用程序發起提問;
- 我們的應用程序會把 用戶的問題(prompt) 和 我們自己提供的函數(function)定義 一并給大模型,大模型會分析判斷這個 prompt,是否需要調用某個函數,以及調用函數所需要的哪些參數,這個過程大模型會返回函數調用參數;(NLU過程 )
這一步是利用大模型把 prompt + function定義 解析成函數的調用,告訴我們要調用哪個函數,以及調用函數的參數是什么
- 我們的應用程序拿到大模型返回的參數,就去調用我們的函數;
- 我們的應用程序將函數調用的結果 再給 大模型;(NLG過程)
- 大模型會把 函數調用結果 再結合 prompt,生成自然語言的回答,并返回給我們的應用程序。
Function Calling 完整的官方接口文檔:https://platform.openai.com/docs/guides/function-calling
值得一提:接口里叫 tools,是從 functions 改的。這是一個很有趣的指向
示例1:調用本地函數
需求:實現一個回答問題的 Al。題目中如果有加法,必須能精確計算。
- 封裝的通用代碼
from openai import OpenAI
from dotenv import load_dotenv, find_dotenv
from math import *
import json
# 加載 .env 到環境變量
_ = load_dotenv(find_dotenv())
client = OpenAI()
# 打印優美的JSON
def print_json(data):
"""
打印參數。如果參數是有結構的(如字典或列表),則以格式化的 JSON 形式打印;
否則,直接打印該值。
"""
if hasattr(data, 'model_dump_json'):
data = json.loads(data.model_dump_json())
if (isinstance(data, (list, dict))):
print(json.dumps(data, indent=4, ensure_ascii=False))
else:
print(data)
# 調用大模型方法
def get_completion(messages, tools, model="gpt-3.5-turbo-0125"):
# tools 里定義了函數,大模型會解析 prompt,智能判斷調用哪個函數,也可能不調用,也可能調錯
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=0.7, # 模型輸出的隨機性,0表示隨機性最小
tools=tools, # 用 JSON 描述函數??梢远x多個。由大模型決定調用誰。也可能都不調用
)
message = response.choices[0].message
print("=====大模型回復=====")
print_json(message)
return message
- 調用大模型代碼
# 提示詞
prompt = "Tell me the sum of 1, 2, 3, 4, 5, 6, 7, 8, 9, 10." # 求和結果:55
prompt1 = "桌上有2個蘋果, 四個桃子和3本書, 一共有幾個水果?" # 求和結果: 2 + 4 = 6
prompt2 = "1+2+3...+99+100" # 求和結果:5050
prompt3 = "1024 乘以 1024 是多少?" # tools 里沒有定義乘法,會怎樣? 求和結果:可能會出現幻覺,結果不一定正確
prompt4 = "太陽從哪邊升起?" # 不需要算加法,會怎樣? 求和結果:不會調用函數,返回的結果 tool_calls 是空的
# 對話歷史list
messages = [
{"role": "system", "content": "你是一個數學家,你能幫我算一下嗎?"},
{"role": "user", "content": prompt}
]
# 定義函數
tools = [{
"type": "function",
"function": {
"name": "sum",
"description": "加法器,計算一組數的和",
"parameters": {
"type": "object",
"properties": {
"numbers": {
"type": "array",
"items": {
"type": "number"
}
}
}
}
}
}]
# 1.調用大模型(將 prompt + function定義 傳給大模型),返回函數調用參數
res_message = get_completion(messages, tools)
# 記錄對話歷史,以便后續進行多輪對話
messages.append(res_message)
# 2.獲取函數調用參數
if (res_message.tool_calls is not None):
tool_call = res_message.tool_calls[0]
if tool_call.function.name == "sum":
# 解析參數,獲取 numbers 的值
args = json.loads(tool_call.function.arguments) # 將 JSON字符串 轉成 字典(Python對象)
print("====解析出函數參數====")
print_json(args)
# 3.調用函數求和
result = sum(args["numbers"])
print(f'調用了加法器,計算結果是:{result}')
# 4.將函數調用結果 和 歷史會話 傳給大模型
messages.append(
{
"tool_call_id": tool_call.id, # 用于標識函數調用的 ID
"role": "tool", # 用于標識是函數調用的結果
"name": "sum", # 用于標識是哪個函數調用的結果
"content": str(result) # 數值 result 必須轉成字符串
}
)
# 重新調用大模型
res_message = get_completion(messages, tools)
- 輸出結果:
=====大模型回復=====
{
"content": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_UhHsmflCKbYn8inSXHtSCtRQ",
"function": {
"arguments": "{\"numbers\":[1,2,3,4,5,6,7,8,9,10]}",
"name": "sum"
},
"type": "function"
}
]
}
====解析出函數參數====
{
"numbers": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10
]
}
調用了加法器,計算結果是:55
=====大模型回復=====
{
"content": "The sum of 1, 2, 3, 4, 5, 6, 7, 8, 9, and 10 is 55.",
"role": "assistant",
"function_call": null,
"tool_calls": null
}
注意:
1.Function Calling 中的函數與參數的描述也是一種Prompt
2.這種 Prompt 也需要調優,否則會影響函數的調用、參數的準確性,甚至讓 GPT 產生幻覺
示例2:多Function 調用
需求:查詢某個地點附近的酒店、餐廳、景點等信息。即,查詢某個 POI附近的 POl。
- 封裝的通用代碼
from openai import OpenAI
from dotenv import load_dotenv, find_dotenv
import requests
import json
# 加載 .env 到環境變量
_ = load_dotenv(find_dotenv())
client = OpenAI()
# 打印優美的JSON
def print_json(data):
"""
打印參數。如果參數是有結構的(如字典或列表),則以格式化的 JSON 形式打印;
否則,直接打印該值。
"""
if hasattr(data, 'model_dump_json'):
data = json.loads(data.model_dump_json())
if (isinstance(data, (list, dict))):
print(json.dumps(data, indent=4, ensure_ascii=False))
else:
print(data)
# 調用大模型方法
def get_completion(messages, tools, model="gpt-3.5-turbo-0125"):
# tools 里定義了函數,大模型會解析 prompt,智能判斷調用哪個函數,也可能不調用,也可能調錯
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=0, # 模型輸出的隨機性,0表示隨機性最小
seed=1024, # 隨機種子保持不變,temperature 和 prompt 不變的情況下,輸出就會不變
tool_choice="auto", # 選擇函數調用的策略。auto為默認值,表示由大模型自動決定是否調用函數
tools=tools, # 用 JSON 描述函數??梢远x多個。由大模型決定調用誰。也可能都不調用
)
message = response.choices[0].message
print("=====大模型回復=====")
print_json(message)
return message
# 高德地圖開發者密鑰
amap_key = "005deb4aeb1f8cfdd28fb5fdd6badf25"
# 根據POI名稱, 獲得POI的經緯度坐標
def get_location_coordinate(location, city):
url = "https://restapi.amap.com/v5/place/text"
params = {
"key": amap_key,
"keywords": location,
"city": city,
"output": "json"
}
response = requests.get(url, params=params)
data = response.json()
if "pois" in data and data["pois"]:
return data["pois"][0]
else:
return None
# 搜索給定坐標附近的poi
def search_nearly_pois(longitude, latitude, keyword):
url = "https://restapi.amap.com/v5/place/around"
params = {
"key": amap_key,
"location": f"{longitude},{latitude}",
"keywords": keyword,
"output": "json"
}
response = requests.get(url, params=params)
data = response.json()
ans = "" # 用于存儲結果
if "pois" in data and data["pois"]:
pois = data["pois"]
for i in range(min(3, len(pois))):
name = pois[i]["name"]
address = pois[i]["address"]
distance = pois[i]["distance"]
ans += f"{name}\n{address}\n距離: {distance}米\n\n"
return ans
- 調用大模型代碼
# 提示詞
prompt = "我想在北京五道口附近喝咖啡,給我推薦幾個"
prompt1 = "我到北京出差,給我推薦三里屯的酒店,和五道口附近的咖啡"
# 對話歷史list
messages = [
{"role": "system", "content": "你是一個地圖通,你可以找到任何地址。"},
{"role": "user", "content": prompt1}
]
# 定義函數
tools = [{
"type": "function",
"function": {
"name": "get_location_coordinate",
"description": "根據POI名稱, 獲得POI的經緯度坐標",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "POI名稱, 必須是中文"
},
"city": {
"type": "string",
"description": "POI所在的城市名, 必須是中文"
}
},
"required": ["location", "city"]
}
}
},
{
"type": "function",
"function": {
"name": "search_nearly_pois",
"description": "搜索給定坐標附近的poi",
"parameters": {
"type": "object",
"properties": {
"longitude": {
"type": "string",
"description": "中心點的經度"
},
"latitude": {
"type": "string",
"description": "中心點的緯度"
},
"keyword": {
"type": "string",
"description": "目標poi的關鍵詞"
}
},
"required": ["longitude", "latitude", "keyword"]
}
}
}]
# 1.調用大模型(將 prompt + function定義 傳給大模型),返回函數調用參數
res_message = get_completion(messages, tools)
# 記錄對話歷史,以便后續進行多輪對話
messages.append(res_message)
# 2.獲取函數調用參數
while (res_message.tool_calls is not None):
for tool_call in res_message.tool_calls:
# 解析參數
args = json.loads(tool_call.function.arguments) # 將 JSON字符串 轉成 字典(Python對象)
# 3.調用外部函數
if tool_call.function.name == "get_location_coordinate":
result = get_location_coordinate(**args)
print ("Call: get_location_coordinate")
print_json(result)
elif tool_call.function.name == "search_nearly_pois":
result = search_nearly_pois(**args)
print ("Call: search_nearly_pois")
print_json(result)
messages.append(
{
"tool_call_id": tool_call.id, # 用于標識函數調用的 ID
"role": "tool", # 用于標識是函數調用的結果
"name": tool_call.function.name, # 用于標識是哪個函數調用的結果
"content": str(result) # 數值 result 必須轉成字符串
}
)
# 重新調用大模型
res_message = get_completion(messages, tools)
if res_message.content is None: # 如果大模型返回的是 None,就將其置為空字符串(解決OpenAI的一個 400 bug)
res_message.content = ""
messages.append(res_message) # 把大模型的回復加入到対活中
- 問1:我想在北京五道口附近喝咖啡,給我推薦幾個
=====大模型回復=====
{
"content": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_DgUaQN9Tc9MHMSeyuEwWZT9f",
"function": {
"arguments": "{\"location\":\"五道口\",\"city\":\"北京\"}",
"name": "get_location_coordinate"
},
"type": "function"
}
]
}
Call: get_location_coordinate
{
"parent": "",
"address": "(在建)13A號線;13號線",
"distance": "",
"pcode": "110000",
"adcode": "110108",
"pname": "北京市",
"cityname": "北京市",
"type": "交通設施服務;地鐵站;地鐵站",
"typecode": "150500",
"adname": "海淀區",
"citycode": "010",
"name": "五道口(地鐵站)",
"location": "116.337742,39.992894",
"id": "BV10006886"
}
=====大模型回復=====
{
"content": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_XzzqdcbJKAlswCFh4et1DsLQ",
"function": {
"arguments": "{\"longitude\":\"116.337742\",\"latitude\":\"39.992894\",\"keyword\":\"咖啡\"}",
"name": "search_nearly_pois"
},
"type": "function"
}
]
}
Call: search_nearly_pois
瑞幸咖啡(五道口地鐵站店)
荷清路與成府路交叉口華清嘉園1號樓二層1-2號
距離: 97米
八號橋咖啡(華清嘉園東區店)
五道口華清嘉園12號(五道口地鐵站B南口步行150米)
距離: 120米
星巴克(北京五道口購物中心店)
成府路28號1層101-10B及2層201-09號
距離: 122米
=====大模型回復=====
{
"content": "以下是在北京五道口附近的幾家咖啡店推薦:\n\n1. 瑞幸咖啡(五道口地鐵站店)\n地址:荷清路與成府路交叉口華清嘉園1號樓二層1-2號\n距離地鐵站:97米\n\n2. 八號橋咖啡(華清嘉園東區店)\n地址:五道口華清嘉園12號(五道口地鐵站B南口步行150米)\n距離地鐵站:120米\n\n3. 星巴克(北京五道口購物中心店)\n地址:成府路28號1層101-10B及2層201-09號\n距離地鐵站:122米\n\n您可以選擇其中一家前往享受咖啡時光。祝您喝咖啡愉快!",
"role": "assistant",
"function_call": null,
"tool_calls": null
}
- 問2:我到北京出差,給我推薦三里屯的酒店,和五道口附近的咖啡
=====大模型回復=====
{
"content": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_1B6LSeBa4UuOI3qJBYwcQKcN",
"function": {
"arguments": "{\"location\": \"三里屯\", \"city\": \"北京\"}",
"name": "get_location_coordinate"
},
"type": "function"
},
{
"id": "call_R7qgVvUtEzUqMW8cv82kOYfH",
"function": {
"arguments": "{\"location\": \"五道口\", \"city\": \"北京\"}",
"name": "get_location_coordinate"
},
"type": "function"
}
]
}
Call: get_location_coordinate
{
"parent": "",
"address": "朝陽區",
"distance": "",
"pcode": "110000",
"adcode": "110105",
"pname": "北京市",
"cityname": "北京市",
"type": "地名地址信息;熱點地名;熱點地名",
"typecode": "190700",
"adname": "朝陽區",
"citycode": "010",
"name": "三里屯",
"location": "116.455294,39.937492",
"id": "B0FFF5BER7"
}
Call: get_location_coordinate
{
"parent": "",
"address": "(在建)13A號線;13號線",
"distance": "",
"pcode": "110000",
"adcode": "110108",
"pname": "北京市",
"cityname": "北京市",
"type": "交通設施服務;地鐵站;地鐵站",
"typecode": "150500",
"adname": "海淀區",
"citycode": "010",
"name": "五道口(地鐵站)",
"location": "116.337742,39.992894",
"id": "BV10006886"
}
=====大模型回復=====
{
"content": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_w9H3DtZas1gpiw2ukK6idQuQ",
"function": {
"arguments": "{\"longitude\": \"116.455294\", \"latitude\": \"39.937492\", \"keyword\": \"酒店\"}",
"name": "search_nearly_pois"
},
"type": "function"
},
{
"id": "call_UlYak22SajR1JENOipRYsZWX",
"function": {
"arguments": "{\"longitude\": \"116.337742\", \"latitude\": \"39.992894\", \"keyword\": \"咖啡\"}",
"name": "search_nearly_pois"
},
"type": "function"
}
]
}
Call: search_nearly_pois
北京瑜舍
三里屯路11號三里屯太古里北區
距離: 47米
THE OPPOSITE HOUSE(三里屯太古里北區店)
三里屯路11號院三里屯太古里北區L1層
距離: 46米
北京三里屯太古里亞朵X酒店
東直門外大街12號
距離: 384米
Call: search_nearly_pois
瑞幸咖啡(五道口地鐵站店)
荷清路與成府路交叉口華清嘉園1號樓二層1-2號
距離: 97米
八號橋咖啡(華清嘉園東區店)
五道口華清嘉園12號(五道口地鐵站B南口步行150米)
距離: 120米
星巴克(北京五道口購物中心店)
成府路28號1層101-10B及2層201-09號
距離: 122米
=====大模型回復=====
{
"content": "在北京,我找到了以下地點:\n\n### 三里屯附近的酒店:\n1. 北京麗舍酒店\n 地址:三里屯路11號三里舍太古里北區\n 距離:47米\n\n2. THE OPPOSITE HOUSE(三里舍太古里北區店)\n 地址:三里舍路11號院三里舍太古里北區L1層\n 距離:46米\n\n3. 北京三里舍太古里亞杜X酒店\n 地址:東直門外大街12號\n 距離:384米\n\n### 五道口附近的咖啡店:\n1. 瑞幸咖啡(五道口地鐵站店)\n 地址:荷清路與成府路交叉口華清嘉園1號樓2單元1-2號\n 距離:97米\n\n2. 八號橋咖啡(華清嘉園東區店)\n 地址:五道口華清嘉園12號(五道口地鐵站B南口步行150米)\n 距離:120米\n\n3. 星巴克(北京五道口購物中心店)\n 地址:成府路28號1號樓101-10B及2號樓201-09號\n 距離:122米\n\n希望這些信息對您有幫助!",
"role": "assistant",
"function_call": null,
"tool_calls": null
}
示例3:用 Function Calling 獲取 JSON 結構
備注:Function calling 生成 JSON 的穩定性比較高。
需求:從一段文字中抽取聯系人姓名、地址和電話
- 調用大模型代碼
# 提示詞
prompt = "幫我寄給張三, 地址是浙江省杭州市濱江區浦沿街道, 電話151xxxxxxxx。"
# 對話歷史list
messages = [
{"role": "system", "content": "你是一個聯系人錄入員。"},
{"role": "user", "content": prompt}
]
# 定義函數
tools=[{
"type": "function",
"function": {
"name": "add_contact",
"description": "添加聯系人",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "聯系人姓名"
},
"address": {
"type": "string",
"description": "聯系人地址"
},
"tel": {
"type": "string",
"description": "聯系人電話"
},
}
}
}
}]
# 1.調用大模型(將 prompt + function定義 傳給大模型),返回函數調用參數
res_message = get_completion(messages, tools)
# 解析出函數參數
if (res_message.tool_calls is not None):
tool_call = res_message.tool_calls[0]
if tool_call.function.name == "add_contact":
# 解析參數,獲取 numbers 的值
args = json.loads(tool_call.function.arguments) # 將 JSON字符串 轉成 字典(Python對象)
print("====解析出函數參數====")
print_json(args)
- 輸出結果:
=====大模型回復=====
{
"content": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_YPXDY8JJHJUCNlwjEJXnq8DP",
"function": {
"arguments": "{\"name\":\"張三\",\"address\":\"浙江省杭州市濱江區浦沿街道\",\"tel\":\"151xxxxxxxx\"}",
"name": "add_contact"
},
"type": "function"
}
]
}
====解析出函數參數====
{
"name": "張三",
"address": "浙江省杭州市濱江區浦沿街道",
"tel": "151xxxxxxxx"
}
示例 4:通過 Function Calling 查詢數據庫
需求:從訂單表中查詢各種信息,比如某個用戶的訂單數量、某個商品的銷量、某個用戶的消費總額等等。
示例 5:用 Function Calling 實現多表查詢
示例 6:Stream 模式
流式(stream)輸出不會一次返回完整 JSON 結構,所以需要拼接后再使用。
- 完整代碼
from openai import OpenAI
from dotenv import load_dotenv, find_dotenv
import json
# 加載 .env 到環境變量
_ = load_dotenv(find_dotenv())
client = OpenAI()
# 打印優美的JSON
def print_json(data):
"""
打印參數。如果參數是有結構的(如字典或列表),則以格式化的 JSON 形式打??;
否則,直接打印該值。
"""
if hasattr(data, 'model_dump_json'):
data = json.loads(data.model_dump_json())
if (isinstance(data, (list, dict))):
print(json.dumps(data, indent=4, ensure_ascii=False))
else:
print(data)
# 調用大模型方法
def get_completion(messages, tools, model="gpt-3.5-turbo-0125"):
# tools 里定義了函數,大模型會解析 prompt,智能判斷調用哪個函數,也可能不調用,也可能調錯
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=0, # 模型輸出的隨機性,0表示隨機性最小
tools=tools, # 用 JSON 描述函數??梢远x多個。由大模型決定調用誰。也可能都不調用
stream=True # 啟動流式輸出
)
# print("====大模型回復====")
# print_json(response) # <openai.Stream object at 0x1097cd4f0>
return response
# 提示詞
# prompt = "1+2+3"
prompt = "你是誰"
# 對話歷史list
messages = [
{"role": "system", "content": "你是一個小學數學老師,你要教學生加法"},
{"role": "user", "content": prompt}
]
# 定義函數
tools = [{
"type": "function",
"function": {
"name": "sum",
"description": "計算一組數的加和",
"parameters": {
"type": "object",
"properties": {
"numbers": {
"type": "array",
"items": {
"type": "number"
}
}
}
}
}
}]
# 1.調用大模型(將 prompt + function定義 傳給大模型),返回函數調用參數
res_message = get_completion(messages, tools)
print("====Streaming 流式輸出====")
# 需要把 stream 里的 token 拼起來,才能得到完整的 call
function_name, args, text = "", "", ""
for msg in res_message:
# print_json(msg)
delta = msg.choices[0].delta
if delta.tool_calls:
if not function_name:
function_name = delta.tool_calls[0].function.name
args_delta = delta.tool_calls[0].function.arguments
print(args_delta) # 打印每次得到的數據
args = args + args_delta
elif delta.content:
text_delta = delta.content
print(text_delta) # 打印每次得到的數據
text = text + text_delta
print("====完成,最終輸出====")
if function_name or args:
print(function_name)
print_json(args)
if text:
print(text)
-
prompt = "1+2+3"
的輸出結果:
====Streaming 流式輸出====
{"
numbers
":[
1
,
2
,
3
]}
====完成,最終輸出====
sum
{"numbers":[1,2,3]}
-
prompt = "你是誰"
的輸出結果:
====Streaming 流式輸出====
我
是
一個
小
學
數
學
老
師
,
我
可以
幫
助
你
學
習
數
學
。
你
有
什
么
問題
需要
幫
忙
嗎
?
====完成,最終輸出====
我是一個小學數學老師,我可以幫助你學習數學。你有什么問題需要幫忙嗎?
六、Function Calling的注釋事項
- 只有
gpt-3.5-turbo-0125
和gpt-4-turbo-preview
可用本次課介紹的方法。 - OpenAI 針對 Function Calling 做了 fine-tuning,以盡可能保證函數調用參數的正確。
- 函數聲明是消耗 token 的。要在功能覆蓋、省錢、節約上下文窗口之間找到最佳平衡。
- Function Calling 不僅可以調用讀函數,也能調用寫函數。但官方強烈建議,在寫之前(對真實世界會產生影響的操作,如:發送電子郵件、在線發布內容、購買等),一定要有人做確認。
- 不保證不出錯,包括不保證 json 格式正確。但比純靠 prompt 控制,可靠性是大了很多。
七、支持 Function Calling 的國產大模型
百度文心大模型
MiniMax
- 這是個公眾不大知道,但其實挺強的大模型,尤其角色扮演能力
- 如果你曾經在一個叫 Glow 的 app 流連忘返,那么你已經用過它了
- 應該是最早支持 Function Calling 的國產大模型
- Function Calling 的 API 和 OpenAI 1106 版之前完全一樣,但其它 API 有很大的特色
ChatGLM3-6B
- 最著名的國產開源大模型,生態最好
- 早就使用 tools 而不是 function 來做參數,其它和 OpenAI 1106 版之前完全一樣
訊飛星火 3.0
- 和 OpenAI 1106 版之前完全一樣