一、Submit Tool Outputs方法详解
- 适用场景剖析:在OpenAI Assistant API的应用中,
submit_tool_outputs
方法具有特定的使用场景。当我们创建的Assistant对象配备了外部工具,并且模型根据用户输入判断需要调用这些工具时,在手动调用工具并获得参数及返回结果后,submit_tool_outputs
方法就发挥作用了。它将工具调用的结果添加到线程中,使得模型能够基于这些结果继续完成后续任务。如果在应用中没有定义外部工具,仅仅是让模型生成常规回复,那就无需使用该方法来启用流式输出。 - 重要性解读:在构建功能丰富的AI agent时,往往需要为其配备各种复杂的工具。
submit_tool_outputs
方法在这一过程中扮演着关键角色,它是实现利用外部工具增强AI agent功能的重要环节。掌握该方法的使用,对于开发高效、智能的应用至关重要。
二、前期准备工作
- 定义外部函数:为了演示
submit_tool_outputs
方法在函数调用中的流式传输,我们编写了一个模拟获取实时天气的外部函数get_current_weather
。这个函数根据输入的地点(如北京、上海、广州)返回相应的温度信息,如果输入的地点不在预设列表中,则返回未知温度。函数接收location
(表示地点)和unit
(表示温度单位,默认值为华氏度)两个参数。
def get_current_weather(location, unit="celsius"):
if "beijing" in location.lower():
return json.dumps({"location": "Beijing", "temperature": "15", "unit": unit})
elif "shanghai" in location.lower():
return json.dumps({"location": "Shanghai", "temperature": "20", "unit": unit})
elif "guangzhou" in location.lower():
return json.dumps({"location": "Guangzhou", "temperature": "25", "unit": unit})
else:
return json.dumps({"location": location, "temperature": "unknown"})
- 编写JSON Schema表示:为了让大模型清楚何时以及如何调用外部函数
get_current_weather
,我们需要定义一个JSON Schema表示get_weather_desc
。在这个定义中,明确了类型为function
,函数名为get_current_weather
,描述了函数的功能是获取给定地点的当前天气。同时,详细定义了参数,包括location
(必填,字符串类型,用于指定城市和州)和unit
(字符串类型,取值为celsius
或fahrenheit
)。
get_weather_desc = {
"type": "function",
"function": {
"name": "get_current_weather",
"description": "获取给定地点的当前天气",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市和州,例如北京"
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
}
}
- 建立函数名与函数对象的映射:为了方便在模型识别到需要调用外部函数时动态执行函数,我们创建了一个
available_functions
字典,将函数名get_current_weather
映射到对应的函数对象,这样在后续操作中可以快速调用函数并获取执行结果。
available_functions = {
"get_current_weather": get_current_weather
}
三、搭建AI agent框架
- 实例化客户端与创建Assistant对象:首先,我们需要实例化OpenAI客户端,这是与OpenAI服务进行交互的基础。然后创建Assistant对象,为其设置名称、指令、使用的模型,并通过
tools
参数传递get_weather_desc
,使Assistant能够识别并使用外部工具。这里设置名称为“你是一个实时天气小助理”,指令为“你可以调用工具,获取到当前的实时天气,再给出最终的回复”,模型选择gpt-4o-mini-2024-07-18
。
from openai import OpenAI
client = OpenAI()
assistant = client.assistants.create(
name="你是一个实时天气小助理",
instructions="你可以调用工具,获取到当前的实时天气,再给出最终的回复",
model="gpt-4o-mini-2024-07-18",
tools=[get_weather_desc]
)
- 创建Thread对象并添加消息:接下来创建Thread对象实例,它用于管理对话线程。然后通过
messages
向Thread中添加用户的问题,这里我们添加的问题是“北京现在的天气怎么样?”
thread = client.threads.create()
message = client.threads.messages.create(
thread_id=thread.id,
role="user",
content="北京现在的天气怎么样?"
)
- 执行运行并监控事件流:执行运行时,设置
stream=True
开启流式传输,以便实时获取运行过程中的事件流信息。在运行过程中,我们会观察到一系列事件,如ThreadRunCreated
(表示创建运行事件流)、ThreadRunQueued
(进入队列状态)、ThreadRunInProgress
(执行状态)等。与未使用外部工具时不同,加入外部工具函数后,会出现ThreadRunStepDelta
事件,并且最终运行状态会停留在ThreadRunRequiresAction
,此时模型并未直接回答问题,而是等待进一步操作。
run = client.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
stream=True
)
四、应对ThreadRunRequiresAction事件
- 提取关键信息:当遇到
ThreadRunRequiresAction
事件时,我们通过event.data
获取相关数据,从中提取Function Info
(包含运行的详细信息)、Required Action
(需要执行的具体动作信息)和Tool Calls
(工具调用信息),并进行打印展示,以便了解当前的运行状态和需要执行的操作。
#后续实际处理
for event in run:
if event.event == 'thread.run.requires_action':
function_info = event.data
print("Function Info:")
print(function_info)
required_action = function_info.required_action
print("Required Action:")
print(required_action)
tool_calls = required_action.submit_tool_outputs.tool_calls
print("Tool Calls:")
print(tool_calls)
- 执行外部函数:利用循环遍历
tool_calls
,依次获取每个工具调用的id
、函数名和参数。然后借助available_functions
字典,根据函数名动态执行函数,并将参数传递进去获取执行结果。将每个工具调用的结果添加到tool_outputs
列表中,以便后续提交。
tool_outputs = []
for tool_call in tool_calls:
tool_id = tool_call.id
function = tool_call.function
function_name = function.name
function_args = json.loads(function.arguments)
function_result = available_functions[function_name](**function_args)
print("Function Result for {}:{}".format(function_name, function_result))
tool_outputs.append({"tool_call_id": tool_id, "output": function_result})
print("Tool Outputs:")
print(tool_outputs)
五、运用submit_tool_outputs提交结果
- 提交结果并继续流程:在获取到工具调用的结果后,使用
client.threads.runs.submit_tool_outputs
方法提交结果。在调用该方法时,需要指定thread_id
(与之前创建的线程一致)、run_id
(上一个运行的ID)以及tool_outputs
(包含工具调用结果的列表),同时设置stream=True
开启流式传输,这样结果会被提交到当前线程并再次进入队列执行后续任务。
run_tools = client.threads.runs.submit_tool_outputs(
thread_id=thread.id,
run_id=function_info.id,
tool_outputs=tool_outputs,
stream=True
)
- 处理新的事件流数据:在新的事件流中,通过判断
event_tool.event
的类型来处理不同阶段的数据。当event_tool.event
为thread.message.delta
时,提取并打印模型回复的增量信息;当event_tool.event
为thread.message.completed
时,提取并打印完整的模型回复信息。
for event_tool in run_tools:
if event_tool.event == 'thread.message.delta':
text = event_tool.data.delta.content[0].text.value
print("Delta Text Output:", text)
print("------")
elif event_tool.event == 'thread.message.completed':
full_text = event_tool.data.content[0].text.value
print("Completed Message Text:", full_text)
print("------")
六、总结与未来展望
import json
# 导入OpenAI库,用于与OpenAI的API进行交互
from openai import OpenAI
# 定义一个名为get_current_weather的函数,用于获取指定地点的当前天气状况
def get_current_weather(location, unit="celsius"):
# 如果输入的地点中包含“beijing”(不区分大小写)
if "beijing" in location.lower():
# 返回北京的天气信息,包括地点、温度和单位,以JSON字符串形式
return json.dumps({"location": "Beijing", "temperature": "15", "unit": unit})
# 如果输入的地点中包含“shanghai”(不区分大小写)
elif "shanghai" in location.lower():
# 返回上海的天气信息,包括地点、温度和单位,以JSON字符串形式
return json.dumps({"location": "Shanghai", "temperature": "20", "unit": unit})
# 如果输入的地点中包含“guangzhou”(不区分大小写)
elif "guangzhou" in location.lower():
# 返回广州的天气信息,包括地点、温度和单位,以JSON字符串形式
return json.dumps({"location": "Guangzhou", "temperature": "25", "unit": unit})
else:
# 如果输入的地点不在上述列表中,返回未知温度的信息,包括地点和“unknown”温度,以JSON字符串形式
return json.dumps({"location": location, "temperature": "unknown"})
# 定义JSON Schema表示,用于描述get_current_weather函数的相关信息,以便大模型理解如何调用
get_weather_desc = {
"type": "function", # 表明这是一个函数的描述
"function": {
"name": "get_current_weather", # 函数名称
"description": "获取给定地点的当前天气", # 函数功能描述
"parameters": {
"type": "object", # 参数是一个对象
"properties": {
"location": { # location参数
"type": "string", # 类型为字符串
"description": "城市和州,例如北京" # 参数描述
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} # unit参数,类型为字符串,取值为"celsius"或"fahrenheit"
},
"required": ["location"] # location参数是必填的
}
}
}
# 创建一个字典,将函数名映射到具体的函数对象,方便后续动态调用函数
available_functions = {
"get_current_weather": get_current_weather
}
# 实例化OpenAI客户端,用于与OpenAI的服务进行通信
client = OpenAI()
# 创建一个Assistant对象
assistant = client.beta.assistants.create(
name="你是一个实时天气小助理", # Assistant的名称
instructions="你可以调用工具,获取到当前的实时天气,再给出最终的回复", # 指令,告知Assistant的任务
model="gpt-4o-mini-2024-07-18", # 使用的模型
tools=[get_weather_desc] # 传递get_weather_desc,让Assistant知道有哪些可用工具及其使用方式
)
# 创建一个Thread对象,用于管理对话线程
thread = client.beta.threads.create()
# 向Thread中添加一条用户消息,询问北京现在的天气情况
message = client.beta.threads.messages.create(
thread_id=thread.id, # 指定消息所属的线程ID
role="user", # 消息角色为用户
content="北京现在的天气怎么样?" # 消息内容
)
# 执行第一次Run,开启流式传输,以便实时获取运行过程中的事件流信息
run = client.beta.threads.runs.create(
thread_id=thread.id, # 指定线程ID
assistant_id=assistant.id, # 指定Assistant ID
stream=True # 开启流式传输
)
# 初始化一个列表,用于存储外部函数调用的输出结果
tool_outputs = []
# 遍历run中的事件
for event in run:
# 如果事件类型是thread.run.step.delta,表示模型正在生成调用外部函数的参数
if event.event == 'thread.run.step.delta':
print("Delta Event Arguments:")
# 打印模型生成的用于调用外部函数的参数
print(event.data.delta.step_details.tool_calls[0].function.arguments)
print("------")
# 如果事件类型是thread.run.requires_action,表示模型需要调用外部函数来处理用户请求
elif event.event == 'thread.run.requires_action':
# 获取事件中的数据,包含运行相关信息
function_info = event.data
print("Function Info:")
# 打印运行相关信息
print(function_info)
# 获取需要执行的动作信息
required_action = function_info.required_action
print("Required Action:")
# 打印需要执行的动作信息
print(required_action)
# 获取工具调用信息
tool_calls = required_action.submit_tool_outputs.tool_calls
print("Tool Calls:")
# 打印工具调用信息
print(tool_calls)
# 遍历每个工具调用
for tool_call in tool_calls:
# 获取工具调用的ID
tool_id = tool_call.id
# 获取工具调用对应的函数
function = tool_call.function
# 获取函数名称
function_name = function.name
# 解析函数调用的参数
function_args = json.loads(function.arguments)
# 通过available_functions字典动态调用函数,并传入参数,获取函数执行结果
function_result = available_functions[function_name](**function_args)
print("Function Result for {}:{}".format(function_name, function_result))
# 将工具调用的ID和输出结果添加到tool_outputs列表中
tool_outputs.append({"tool_call_id": tool_id, "output": function_result})
print("Tool Outputs:")
# 打印所有工具调用的输出结果
print(tool_outputs)
# 使用submit_tool_outputs方法提交工具输出结果,继续处理对话流程
run_tools = client.beta.threads.runs.submit_tool_outputs(
thread_id=thread.id, # 指定线程ID
run_id=function_info.id, # 指定上一次运行的ID
tool_outputs=tool_outputs, # 传递工具调用的输出结果
stream=True # 开启流式传输
)
# 遍历新的事件流
for event_tool in run_tools:
# 如果事件类型是thread.message.delta,表示模型正在逐步生成回复
if event_tool.event == 'thread.message.delta':
# 提取模型回复的增量信息
text = event_tool.data.delta.content[0].text.value
print("Delta Text Output:", text)
print("------")
# 如果事件类型是thread.message.completed,表示模型完成了回复
elif event_tool.event == 'thread.message.completed':
# 提取模型的完整回复
full_text = event_tool.data.content[0].text.value
print("Completed Message Text:", full_text)
print("------")
-
两次Run操作核心要点:在流式传输且涉及函数调用的过程中,会进行两次Run操作。第一次Run主要负责生成函数参数并手动执行外部函数,第二次Run则通过
run_id
与第一次Run绑定,提取函数调用结果,并基于此生成对用户问题的完整模型响应,这是实现连贯、准确回复的关键。 -
课程回顾与后续展望:全面介绍了OpenAI Assistant API的多种调用方法和应用技巧,涵盖了普通调用、生命周期、内置工具使用、外部工具函数调用以及流式输出等重要内容。OpenAI预计近期会推出Assistant API的新版本,鉴于其以往更新的风格,新版本可能会带来颠覆性的变化,值得我们密切关注。后续课程将介绍基于Assistant API构建的异步流式传输AI agent开发框架,该框架具有高扩展性,在满足生产环境使用API条件的情况下,可便捷地接入实际生产环境,为开发者提供更强大的开发支持。