OpenAI在11月6号的开发者大会上,除了公布了gpt4-v、gpt-4-turbo等新模型外,还有一个assistants-api,基于assistants-api开发者可以构建自己的AI助手,目前assistants-api有三类的工具可以用。首先就是之前大火的代码解释器(Code Interpreter),这个在chatgpt-plus会员上线的时候大火了一把。其次就是文件检索(Retrieval),利用Retrieval你可以在assistants中外挂自己的知识库。还有就是函数调用了,这个就不在多说了。assistants-api目前还处于beta版本,但从OpenAI的规划来看,后续应该是会支持DALLE3、gpt4-v甚至是plugin的,我们可以期待下。
使用assistants-api和使用chat-api有啥区别?首先就是chat api只能使用模型的chat能力的,而且如果你之前使用过,就会发现chat对话历史都需要自行维护,很不方便。而assistants-api除了chat的能力外,它还可以调用强大的解释器(Code Interpreter),还可以调用外部函数(Functions Calling), 而且还可以外挂自己的知识库(Retrieval),主要你还不需要维护对话历史,只需要关注对话本身即可。 如果后续assistants-api支持了plugin、DALLE3和gpt4-v之后,你完全可以认为它就是一个api版本的chatGPT-Plus,当然功能可以可以完全定制,相信看到这里你肯定也蠢蠢欲动,想定制自己的AI助手了。
在正式开始开发之前,我们先来了解下Assistants-API的几个核心对象。
对象 | 作用 |
Assistant | 助手,可以使用指定模型根据的一个实体,如果把助手比作某个人的化,这里就是指具备某些能力的一个具体的人 |
Thread | 没有合适的翻译,这里就不翻译了,可以认为这个是和助手的沟通的上下文对话信息, 就好比你和某宝客服沟通,整个对话就可以认为是一个Thread |
Run | 也没有合适的翻译,可以认为是你向助手发起一次对话,整个对话响应的过程及工程中的状态变化,就可以当成一个run,一个run里不仅仅可以有模型的回复,还可以有函数调用、代码解释器调用、文件召回…… |
Run Step | Run各个步骤的详情,可以看到整个助手的运行过程,主要是方便问题排查和助手优化 |
知道了这些概念,我们就可以着手实现自己的Assistant了,为了能更好理解整个Assistant的开发流程,我们还是用一个具体的示例来完成整个功能的开发。假设我们需要开发一个花店财务助手,它的主要功能是根据我们每天卖出去的花,统计成本和收入,最后将收入和成本保存到数据库里。
这里我提前准备了一个excel表格(flower_prices.xlsx),来记录所有花的成本和售价(虚构数据、不代表真实价格)。
下面正式开始我们花店财务助手的开发和使用。
创建助手
这里首先需要将我们的flower_prices.csv转成Assistant能使用的file,使用如下代码即可:
from openai import OpenAI
client = OpenAI(base_url='https://thales.xindoo.xyz/openai/v1/')
# 将文件上传至openAI保存
file = client.files.create(
file=open("flower_prices.csv", "rb"),
purpose='assistants'
)
接下来我们定义保存账单信息的function,具体可以参考下我上篇博客OpenAI的多函数调用
# 定义保存账单的方法
def save_bill(totalCost, totalIncome):
'''保存总成本和总的收入'''
print(totalCost, totalIncome)
return "success"
function = {
"type": "function",
"function": {
"name": "save_bill",
"description": "保存总成本和总的收入",
"parameters": {
"type": "object",
"properties": {
"totalCost": {
"type": "number",
"description": "总成本",
},
"totalIncome": {
"type": "number",
"description": "总收入",
}
},
"required": ["totalCost", "totalIncome"],
},
}
}
available_functions = { "save_bill": save_bill}
创建助手(assistant)
这里需要调用API将所有的开关、文件和函数调用信息都传给OpenAI,创建一个属于我们自己的assistant。
# 创建助手,将code_interpreter,retrieval,function都开启
assistant = client.beta.assistants.create(
name="花店财务助手",
description="按照每种花的售出量,统计成本和收入,计算出总利润",
model="gpt-4-1106-preview",
tools=[{"type": "code_interpreter"}, {"type": "retrieval"}, function],
file_ids=[file.id]
)
创建Thread
如上文讲到Thread是用户和Assistant对话的上下文信息,用户和Assistant初次对话肯定是需要创建上下文的,代码和很简单,如下:
# 创建对话Thread
thread = client.beta.threads.create(
messages=[
{
"role": "user",
"content": "我卖出去了红玫瑰3支、郁金香2支、百合6支,计算下总成本和总收入,给出具体的计算过程"
}
]
)
这里看到Thead并没有和Assistant关联到一起,猜测这里只是在本地代码里创建了一个Thread对象,实际上在OpenAI那边还没有任何操作,这个可能是OpenAI利用懒加载来减轻对服务端的压力。
创建Run
# 创建Run
run = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id
)
创建Run的方法也很简单,可以看到只需要传thread_id和assistant_id两个参数即可,而这两个id都是字符串,尤其是assistant_id 你都是可以在OpenAI网站后台看到的,相信这里大家已经猜到了,Assistant和Thread不用每次都创建新的。
# 从Run中获取结果
run = client.beta.threads.runs.retrieve(
thread_id=thread.id,
run_id=run.id
)
获取run的状态
Run创建好之后,需要让OpenAI运行起来,这里就需要调用Retrieve方法,来获取Run的运行结果,这里如果你打印出run的话,你可能会看到类似的信息。
Run(id='run_A9phobcoIOG3euibElksTu8a', assistant_id='asst_hW7NrPZP8q8KvE9oiuceg5mM', cancelled_at=None, completed_at=None, created_at=1700400089, expires_at=1700400689, failed_at=None, file_ids=['file-uhMIBtm4BPXlJlY1UzGIPlGn'], instructions=None, last_error=None, metadata={}, model='gpt-4-1106-preview', object='thread.run', required_action=None, started_at=1700400089, status='in_progress', thread_id='thread_nvsTyK6DQdmKoVxOseSSKZF4', tools=[ToolAssistantToolsCode(type='code_interpreter'), ToolAssistantToolsRetrieval(type='retrieval'), ToolAssistantToolsFunction(function=FunctionDefinition(name='save_bill', parameters={'type': 'object', 'properties': {'totalCost': {'type': 'number', 'description': '总成本'}, 'totalIncome': {'type': 'number', 'description': '总收入'}}, 'required': ['totalCost', 'totalIncome']}, description='保存总成本和总的收入'), type='function')])
这里获取到的是run的最新状态,有可能run还没有执行完,所以可能需要一直循环调取,等待run的状态发生变化。Run有以下的一些状态。
具体的状态和含义如下表:
状态 | 定义 |
queued | 当Runs首次创建或者调用了retrive获取状态后,就会变成queued等待运行。正常情况下,很快就会变成in_progress状态。 |
in_progress | 说明run正在执行中,这时候可以调用run step来查看具体的执行过程 |
completed | 执行完成,可以获取Assistant返回的消息了,也可以继续想Assistant提问了 |
requires_action | 如果Assistant需要执行函数调用,就会转到这个状态,然后你必须按给定的参数调用指定的方法,之后run才可以继续运行 |
expired | 当没有在expires_at之前提交函数调用输出,run将会过期。另外,如果在expires_at之前没获取输出,run也会变成expired状态 |
cancelling | 当你调用client.beta.threads.runs.cancel(run_id=run.id, thread_id=thread.id)方法后,run就会变成cancelling,取消成功后就会变成callcelled状态 |
cancelled | Run已成功取消。 |
failed | 运行失败,你可以通过查看Run中的last_error对象来查看失败的原因。 |
这里需要特别注意requires_action状态,这个是需要要求代码本地去执行一些函数的,执行完成后将结果返回给Assistant,之后run才能继续运行下去。
run触发函数调用
如果run.status是requires_action,我们需要调用本地工具,当然现在只有函数调用,然后将函数调用的结果返给Assistant,方便它继续执行,代码如下:
if run.status == 'requires_action':
tool_outputs=[]
# 调用并保存所有函数调用的结果
for call in run.required_action.submit_tool_outputs.tool_calls:
if call.type != "function":
continue
# 获取真实函数
function = available_functions[call.function.name]
output = {
"tool_call_id": call.id,
"output": function(**call.function.arguments),
}
tool_outputs.append(output)
# 将函数调用的结果回传给Assistant
run = client.beta.threads.runs.submit_tool_outputs(
thread_id=thread.id,
run_id=run.id,
tool_outputs=tool_outputs
)
获取Assistant的消息
接下来我只需要轮询retrive接口,获取run的最新状态,如果状态是completed,就可以读取Assistant的返回结果了。
# 获取run的最新状态。
run = client.beta.threads.runs.retrieve(
thread_id=thread.id,
run_id=run.id
)
if run.status == 'completed':
messages = client.beta.threads.messages.list(
thread_id=thread.id
)
print(messages)
这里注意下messages是倒序排列的,所以最新的消息是在最上面的。
发起新信息
上面的流程是从Assistant创建到发起首次消息的流程,如果我们需要紧接着之前的流程继续对话,只需要在thread中添加新的消息,然后然后创建并执行run即可,代码如下:
# 添加新消息
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="另外还有2支向日葵,补充下这份账单"
)
# 创建run
run = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id
)
# 获取执行结果
run = client.beta.threads.runs.retrieve(
thread_id=thread.id,
run_id=run.id
)
结语
以上就是Assistants-API整体的开发流程,了解了这些流程后,大家可以很容易构建出像ChatGPT-Plus的私人助理。当然Assistants-API目前还是在beta阶段,有很多功能不完善,比如不支持流式返回、不支持图片生成、不支持插件调用……,甚至run的状态还需要轮询来获取……。另外我在写本文demo的时候,发现Retrivel的文本内容召回成功率非常低,导致账单计算成功率很低(也可能是我给的文本格式的问题)。还有就是code_interpreter运行成功率也很低,经常出现运行不起来的情况,难怪还是beta版本,只能期待后续官方能优化下。
另外有些像assistant、thread、run、run step的查看和管理的接口我这里没有讲到,具体大家可自行查阅下官网文档。 如果大家需要试用Assistants-API的话,也可以先到官方https://platform.openai.com/assistants 先行体验,试用完成后可以再将页面配置完整翻译成代码,然后再嵌入到自己的应用中。
完整的代码我已上传至Github上https://github.com/xindoo/openai-examples/blob/main/flower_assistant.ipynb,后续OpenAI其他API的使用示例我也会上传到这个仓库,有兴趣可以关注下。