2

OpenAI Assistants-API简明教程

 9 months ago
source link: https://zxs.io/article/1943
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

OpenAI Assistants-API简明教程

  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的几个核心对象。

1701001380187694247.jpg
对象 作用
Assistant 助手,可以使用指定模型根据的一个实体,如果把助手比作某个人的化,这里就是指具备某些能力的一个具体的人
Thread 没有合适的翻译,这里就不翻译了,可以认为这个是和助手的沟通的上下文对话信息, 就好比你和某宝客服沟通,整个对话就可以认为是一个Thread
Run 也没有合适的翻译,可以认为是你向助手发起一次对话,整个对话响应的过程及工程中的状态变化,就可以当成一个run,一个run里不仅仅可以有模型的回复,还可以有函数调用、代码解释器调用、文件召回……
Run Step Run各个步骤的详情,可以看到整个助手的运行过程,主要是方便问题排查和助手优化

  知道了这些概念,我们就可以着手实现自己的Assistant了,为了能更好理解整个Assistant的开发流程,我们还是用一个具体的示例来完成整个功能的开发。假设我们需要开发一个花店财务助手,它的主要功能是根据我们每天卖出去的花,统计成本和收入,最后将收入和成本保存到数据库里。

  这里我提前准备了一个excel表格(flower_prices.xlsx),来记录所有花的成本和售价(虚构数据、不代表真实价格)。

1701001417680234795.jpeg

  下面正式开始我们花店财务助手的开发和使用。

创建助手

  这里首先需要将我们的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有以下的一些状态。

1701001463345146906.jpg
具体的状态和含义如下表: 状态 定义
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的使用示例我也会上传到这个仓库,有兴趣可以关注下。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK