黑8决心将对 OpenAI API 的学习应用到更多实际场景中,以展示新时代技术的巨大潜力。在接下来的日子里,他不断探索和尝试,将 API 中的各种功能融入到不同的生活场景中,取得了一系列令人瞩目的成果。
首先,他将 OpenAI API 中的文本生成功能应用到了日常写作中。通过简单的 function calling,他可以请求 API 生成文章大纲、段落或完整的文章。例如,在准备一个关于项目的解决方案时,他使用 API 生成了一份生动而具有说服力的文章,引起了人们的关注和共鸣。
接着,他将 API 中的图像识别功能应用到了家庭生活中。通过 function calling,他可以上传家里的照片并请求 API 分析其中的内容。比如,当他的女儿画了一幅画作时,他使用 API 对画作进行分析,并得到了有关主题、色彩和风格的反馈,从而为女儿提供更专业的指导和建议。
此外,他还将 API 中的语言理解功能应用到了教育领域。通过 function calling,他可以向 API 提出问题并获得详细的解答,帮助孩子们更好地理解学习内容。比如,当他的儿子遇到数学难题时,他可以使用 API 进行智能辅导,为儿子提供个性化的学习支持。
随着时间的推移,黑8不断完善和拓展自己的应用案例,将 OpenAI API 的功能发挥到了极致。他的努力和成就得到了革委会的认可和赞赏,成为了新时代技术应用的典范。通过 function calling,他不仅改变了自己的生活方式,还为身边的人们带来了更多便利和惊喜,展示了科技的力量和魅力。
1. 接口(Interface)
两种常用接口:
- 人机交互接口,User Interface(UI)
- 应用程序编程接口,Application Programming Interface(API)
2. 接口的进化(Interface evolution)
UI进化的趋势:越来越适应人的习惯,越来越自然
- 命令行:Command Line Interface(CLI) Dos,Unix/Linux shell,Windows Power Shell
- 图形界面:Graphical User Interface(GUI)Windows,MacOS,IOS,Android
- 语言界面:Conversational User Interface(CUL) Natural Language User Interface(NLUI)
- 脑机接口:Brain Computer Interface(BCI)
API: - 从本地到远程,从同步到异步,媒介发生很多变化,但本质一直没有变:程序员的约定
- 现在开始进化为自然语言接口,Natural Language Interface(NLI)
3. 自然语言接口(Natural Language Interface)
最自然的接口就是自然语言接口:
以前因为计算机处理不好自然语言,所以才有了那么多编程语言、接口、协议、界面风格。而且每一次进化,都是为了“更自然”,现在自然的终极时刻到来,大模型赋予计算机以“人类灵魂”,能和我们进行友好的沟通。
为什么要大模型连接外部世界?
大模型有两大缺陷:
- 并非知晓一切
- 训练数据不可能什么都有。垂直、非公开数据欠缺
- 不知道最新信息。大模型训练周期较长,且更新一次耗资巨大,还有越训练越傻的风险。中间会有延时、空档期。GPT-3.5 ,GPT-4 发布日期分别为:2022.1 和 2023.4
- 没有“真逻辑”
表现出来的逻辑、推理,是文本的统计概率,而不是真正的逻辑
所以:大模型需要连接真实世界,对接真逻辑系统。
比如计算加法:
- 把100以内的所有加法算式都训练给大模型,它就能回答100以内的加法
- 如果超出100的加法,就不一定能算对了
- 大模型并不懂加法,只是记住了100以内的加法算式的统计概率
- 它是用字面意义做加法
4. OpenAI-Actions 接外部世界
第一次尝试用Plugins连接真实世界,产品不成功,原因:
- 不在“场景”中,不能提供端到端的服务
- 缺少“强Agetn”调度,只能手工选择3个plugin,使用成本太高
第二次尝试升级为Actions,内置到GPTs中,解决了落地场景问题
Actions工作流程:
4.1 开发一个Action步骤
- 创建一个应用的API接口
- 以OpenAPI YAML或JSON格式记录API
- 在ChatGPT UI中将schema传给GPT
示例:
openapi: 3.0.1
info:title: TODO Actiondescription: An action that allows the user to create and manage a TODO list using a GPT.version: 'v1'
servers:- url: https://example.com
paths:/todos:get:operationId: getTodossummary: Get the list of todosresponses:"200":description: OKcontent:application/json:schema:$ref: '#/components/schemas/getTodosResponse'
components:schemas:getTodosResponse:type: objectproperties:todos:type: arrayitems:type: stringdescription: The list of todos.
4.2 Function Calling扩展
OpenAI GPTs
- 无需编程,就能定制个性对话机器人的平台
- 可以放入自己的知识库,实现 RAG(后面会讲)
- 可以通过 actions 对接专有数据和功能
- 内置 DALL·E 3 文生图和 Code Interpreter 能力
- 只有 ChatGPT Plus 会员可以使用
字节跳动 Coze
- 免费使用 GPT-4 等 OpenAI 的服务
- 只有英文界面,但其实对中文更友好
- Prompt 优化功能更简单直接
Dify
- 开源,中国公司开发
- 功能最丰富
- 可以本地部署,支持非常多的大模型
- 有 GUI,也有 API
有这类无需开发的工具,为什么还要学大模型开发技术呢?
- 它们都无法针对业务需求做极致调优
- 它们和其它业务系统的集成不是特别方便
Function Calling 技术可以把自己开发的大模型应用和其它业务系统连接。
4.3 Function Calling 的机制
注意: 接口里叫 tools
,是从 functions
改的
4.3 Function Calling示例
调用本地函数
# 初始化
from openai import OpenAI
from dotenv import load_dotenv, find_dotenv
import os
import json_ = load_dotenv(find_dotenv())client = OpenAI()# 定义一个函数
def get_completion(messages, model="gpt-3.5-turbo"):response = client.chat.completions.create(model=model,messages=messages,temperature=0.7, # 模型输出的随机性,0 表示随机性最小tools=[{ # 用 JSON 描述函数。可以定义多个。由大模型决定调用谁。也可能都不调用"type": "function","function": {"name": "sum","description": "加法器,计算一组数的和","parameters": {"type": "object","properties": {"numbers": {"type": "array","items": {"type": "number"}}}}}}],)return response.choices[0].message
from math import *prompt = "1+2+3...+99+100"messages = [{"role": "system", "content": "你是一个超级计算器"},{"role": "user", "content": prompt}
]
response = get_completion(messages)# 把大模型的回复加入到对话历史中
print(response)
messages.append(response)print('=====GPT回复=====', response)# 如果返回的是函数调用结果,则打印出来
if (response.tool_calls is not None):# 是否要调用 sumtool_call = response.tool_calls[0]print("=====函数调用=====", tool_call)if (tool_call.function.name == "sum"):# 调用 sumargs = json.loads(tool_call.function.arguments)result = sum(args["numbers"])# 打印函数调用结果print("=====函数返回=====", result)# 把函数调用结果加入到对话历史中messages.append({"tool_call_id": tool_call.id, # 用于标识函数调用的 ID"role": "tool","name": "sum","content": str(result) # 数值 result 必须转成字符串})# 再次调用大模型print("=====最终回复=====")print(get_completion(messages).content)
逻辑:
- 定义调用OpenAI gpt-3.5-turbo模型的函数,增加tools参数,tools参数为JSON数组,JSON内容:type:function和function的JSON对象,function:描述name、description、parameters等
- 定义提示词,第一次将提示词传给大模型gpt-3.5-turbo,模型根据提示词判断是否调用函数并做出响应
- 如果需要调用函数,返回数据将含有重要参数:tool_calls,包含 ID和function描述,function中包含:arguments(参数)、name(参数名称)、type(类型function)
- 将上步返回的结果:ID和调用参数的结果(result),追加到历史会话中,作为新的提示词。
- 第二次传给大模型gpt-3.5-turbo
- 大模型gpt-3.5-turbo根据历史提示词和调用函数的结果,给出新响应
- Function Calling 中的函数与参数的描述也是一种 Prompt
- 这种 Prompt 也需要调优,否则会影响函数的调用、参数的准确性,甚至让 GPT 产生幻觉
调用多个函数
定义返回奇、偶数函数,模型本身也具有区分能力,这里为了演示多函数调用,所以定义了两个具体函数
# 定义一个函数
def get_completion(messages, model="gpt-3.5-turbo"):response = client.chat.completions.create(model=model,messages=messages,temperature=0.7, # 模型输出的随机性,0 表示随机性最小tools=[{ # 用 JSON 描述函数。可以定义多个。由大模型决定调用谁。也可能都不调用"type": "function","function": {"name": "getEvenNumber","description": "获得所有的偶数","parameters": {"type": "object","properties": {"numbers": {"type": "array","items": {"type": "number"}}}}}}, {"type": "function","function": {"name": "getOddNumber","description": "获得所有的奇数","parameters": {"type": "object","properties": {"numbers": {"type": "array","items": {"type": "number"}}}}}}],)return response.choices[0].message
# 返回所有偶数
def getEvenNumber(nums):if 0 == len(nums):return []new_nums = []for n in nums:if n % 2 == 0:new_nums.append(n)return new_nums# 返回所有奇数
def getOddNumber(nums):if 0 == len(nums):return []new_nums = []for n in nums:if n % 2 != 0:new_nums.append(n)return new_nums
prompt = "1,2,3,4,5,6,7,8,9,10"messages = [{"role": "system", "content": "你是一个超级数学家,返回所有偶数"},{"role": "user", "content": prompt}
]
response = get_completion(messages)# 把大模型的回复加入到对话历史中
messages.append(response)print('=====GPT回复=====')
print(response)# 如果返回的是函数调用结果,则打印出来
while response.tool_calls is not None:for tool_call in response.tool_calls:# 是否要调用 函数args = json.loads(tool_call.function.arguments)result = Nonefunction_name = ""if tool_call.function.name == "getEvenNumber":# 调用 getEvenNumber result = getEvenNumber(args["numbers"])function_name = "getEvenNumber"# 打印函数调用结果print("=====函数返回=====", result)elif tool_call.function.name == "getOddNumber":# 调用 getOddNumberresult = getOddNumber(args["numbers"])function_name = "getOddNumber"print("=====函数返回=====", result)# 把函数调用结果加入到对话历史中messages.append({"tool_call_id": tool_call.id, # 用于标识函数调用的 ID"role": "tool","name": function_name,"content": str(result) # 数值 result 必须转成字符串})response = get_completion(messages)messages.append(response) # 把大模型的回复加入到对话中print("=====最终回复=====")
print(response.content)
返回偶数结果:
返回奇数结果:
Function Calling 获取JSON
def get_completion(messages, model="gpt-3.5-turbo"):response = client.chat.completions.create(model=model,messages=messages,temperature=0, # 模型输出的随机性,0 表示随机性最小tools=[{"type": "function","function": {"name": "splitEvenAndOdd","description": "区分奇、偶数","parameters": {"type": "object","properties": {"even": {"type": "string","description": "奇数"},"odd": {"type": "string","description": "偶数"}}}}}],)return response.choices[0].messageprompt = "1,3,2,5,4,6,7,9,8,10"
messages = [{"role": "system", "content": "你是一个数学老师"},{"role": "user", "content": prompt}
]
response = get_completion(messages)
print("====GPT回复====")
print(response)
args = json.loads(response.tool_calls[0].function.arguments)
print("====函数参数====")
print(args)
查询数据库
从SQLite数据库获得数据
def get_table_names(conn):"""返回所有表名."""table_names = []tables = conn.execute("SELECT name FROM sqlite_master WHERE type='table';")for table in tables.fetchall():table_names.append(table[0])return table_namesdef get_column_names(conn, table_name):"""返回所有列名."""column_names = []columns = conn.execute(f"PRAGMA table_info('{table_name}');").fetchall()for col in columns:column_names.append(col[1])return column_namesdef get_database_info(conn):"""返回一个包含数据库中每个表的名字和列的字典列表。"""table_dicts = []for table_name in get_table_names(conn):columns_names = get_column_names(conn, table_name)table_dicts.append({"table_name": table_name, "column_names": columns_names})return table_dictsdatabase_schema_dict = get_database_info(conn)
database_schema_string = "\n".join([f"Table: {table['table_name']}\nColumns: {', '.join(table['column_names'])}"for table in database_schema_dict # 构造表名和列名字符数组]
)
和之前一样,我们为函数调用定义一个函数规范,让模型API生成参数。请注意:我们要插入数据库的模型到函数规范中。让模型知道这一点很重要:
tools = [{"type": "function","function": {"name": "ask_database","description": "使用这个函数回答用户关于音乐的问题. 输入为格式化好的SQL语句.","parameters": {"type": "object","properties": {"query": {"type": "string","description": f"""通过SQL查询提取用户问题的答案。SQL语句应该使用这个数据库模式:{database_schema_string}查询结果应该以纯文本形式返回,而不是JSON格式。""",}},"required": ["query"],},}}
]
执行SQL查询
messages = []
messages.append({"role": "system", "content": "通过chinook音乐数据库生成SQL查询来回答用户问题"})
messages.append({"role": "user", "content": "谁是按曲目数量排名前五的艺术家?"})
# 传入prompt及tools
chat_response = chat_completion_request(messages, tools)
assistant_message = chat_response.choices[0].message
assistant_message.content = str(assistant_message.tool_calls[0].function)
messages.append({"role": assistant_message.role, "content": assistant_message.content})
if assistant_message.tool_calls:results = execute_function_call(assistant_message)messages.append({"role": "function", "tool_call_id": assistant_message.tool_calls[0].id, "name": assistant_message.tool_calls[0].function.name, "content": results})
pretty_print_conversation(messages)
messages.append({"role": "user", "content": "歌曲最多的专辑叫什么名字?"})
chat_response = chat_completion_request(messages, tools)
assistant_message = chat_response.choices[0].message
assistant_message.content = str(assistant_message.tool_calls[0].function)
messages.append({"role": assistant_message.role, "content": assistant_message.content})
if assistant_message.tool_calls:results = execute_function_call(assistant_message)messages.append({"role": "function", "tool_call_id": assistant_message.tool_calls[0].id, "name": assistant_message.tool_calls[0].function.name, "content": results})
pretty_print_conversation(messages)
4.5. Function Calling 注意事项
gpt-3.5-turbo
是gpt-3.5-turbo-1106
的别名gpt-4
和gpt-4-1106-preview
是两个不同的模型- OpenAI 针对 Function Calling 做了 fine-tuning,以尽可能保证函数调用参数的正确
- 函数声明是消耗 token 的。要在功能覆盖、节约上下文tokens之间找到平衡点
- Function Calling 不仅可以调用读函数,也能调用写函数。但官方强烈建议,在写之前,一定要有人做确认
5. NLP 算法工程师视角
- 模型砍大面,规则修细节
- 一个模型搞不定的问题,拆成多个解决
- 评估算法的准确率(所以要先有测试集,否则别问「能不能做」)
- 评估 bad case 的影响面
- 算法的结果永远不是100%正确的,建立在这个假设基础上推敲产品的可行性