前言
- 用自然语言连接系统的认知,面向未来思考系统间的集成
- 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 描述函数。可以定义多个。由大模型决定调用谁。也可能都不调用
)
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 描述函数。可以定义多个。由大模型决定调用谁。也可能都不调用
)
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 描述函数。可以定义多个。由大模型决定调用谁。也可能都不调用
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 版之前完全一样