理解 OpenAI 响应 API 中的项与消息的区别
解释了 OpenAI 的 Responses API 如何将模型输出重新组织为条目而非消息,以及为何这种变化对工具调用和智能体工作流具有重要意义。
聊天补全:以消息为核心
长期以来,聊天补全一直是与大型语言模型交流的首选格式。典型的使用流程如下:
response = client.chat.completions.create(
model="...",
messages=[
{
"role": "user",
"content": "Explain RAG"
}
]
)
用户发送一组消息,模型则返回下一条助手回复。流程相当简单直接。而 Responses API 从一开始就有所不同。
response = client.responses.create(
model="...",
input="Explain RAG"
)
表面上看,这似乎只是端点不同,用 input 代替了 messages。但真正的区别在于每种 API 所采用的底层抽象方式。聊天补全以 消息 为核心,而 Responses API 则是以 项目与回复 为组织结构。一旦引入工具和智能体,这种区别就会变得愈发重要。
Chat Completions 功能下的对话实际上只是一组消息对象的集合。
messages = [
{
"role": "system",
"content": "Act as a helpful AI assistant."
},
{
"role": "user",
"content": "What is a vector database?"
}
]
每条消息都包含一个角色标识以及相应内容。
常见的角色包括:
systemuserassistant
其流程可以这样理解:
Messages (list)-> Model -> Assistant Message
模型会接收当前的对话内容,然后生成下一条助手消息。最简单的对话循环结构如下:
# Human message appended to the messages list
messages.append({
"role": "user",
"content": user_input
})
response = client.chat.completions.create(
model="...",
messages=messages
)
# AI message appended to the messages list
messages.append(
response.choices[0].message
)
您的应用程序负责记录消息历史,无论是在内存中、数据库中还是其他您选择的存储方式。每条新的用户消息都会被添加到该列表中,完整的列表会被发送给模型,模型的回复又会依次被添加回去。这种模式与聊天界面的工作方式完全契合。
User Message (str)->
Message History (list[dict])->
Model (llm)->
Assistant Message (str)->
Message History (list[dict])
对于纯文本生成及常见的聊天式应用场景,这种架构已经足够使用。但一旦基于大语言模型的应用需要执行超出简单对话的功能时,其局限性就会显现出来。
大语言模型应用并非仅仅是聊天应用
以这样的请求为例:
查询人工智能领域的最新进展,筛选出重要的内容并生成摘要,然后将该摘要发送到我的邮箱。
要完成这一请求,模型必须发起网络搜索并接入电子邮件服务。
这样一来,执行流程就不再仅仅是用户发消息、助手回复消息这么简单了。
User Request ->
Model ->
Web Search ->
Search Results ->
Summarize (Model)->
Send Mail ->
Final Response
更为复杂的应用可能需要将多种不同的工具串联起来使用。
此时,该模型已不再是单纯的文本生成器,而是更广泛执行流程中的主动参与者。它输出的内容中有一些根本不会传递给最终用户——这些内容仅用于内部处理。模型可能会调用某个工具,而该工具调用的结果可能还需要进一步处理,甚至可能触发另一次工具调用。只有当所有这些步骤都完成之后,模型才会生成最终答案,而且即便如此,这个答案也未必是以文本形式呈现的。
这一流程已无法再简化为:
Messages (list)-> Model -> Assistant Message
现在出现了需要作为上下文部分被保留的中间输出和应用程序级操作,而这正是仅以消息为基础的抽象方式显得过于狭隘的地方。
并非所有模型输出都是消息
一旦模型与工具相连,它输出的很多内容其实是函数调用而非对话文本。
以这个例子为例:
Function Call
Name: get_weather
Arguments:
{
"city": "Bengaluru"
}
这并非供用户阅读的内容——而是发给应用程序的指令。你的代码会运行相应的函数并将结果反馈给模型,而这个结果可能呈现给用户,也可能不会。
实际上,模型在运行过程中至少可以产生两种不同的输出类型:
- 函数调用
- 消息
将这两者统称为“助手消息”并不能准确反映执行过程中的实际情况。这种差异正是Responses API旨在解决的核心设计问题。
Responses API:不同的抽象方式
Responses API并非以消息交换为核心组织结构,而是围绕响应这一概念构建的,该响应可以整合多个输出项。
response = client.responses.create(
model="...",
input="Explain LangGraph"
)
print(response.output)
# response.output is a list of output items.
对于纯文本提示,其输出可能仅为一条消息。但对于涉及智能体或工具的场景,一个响应可以包含多种不同类型的元素。该结构的简化示意图如下:
Response
| Reasoning Item
| Function Call Item
| Message Item
在这种模型中,消息仅是多种可能的输出类型之一,并非响应所代表的全部内容。
区别:
区别:
Messages (list)-> Model -> Assistant Message
聊天补全
Input -> Model -> Response
Response:
| Output Item
| Output Item
| Output Item
Responses API
在 Chat Completions 中,建模的对象是对话内容,而 Responses API 则将模型执行视为由多个输出项构成的响应。对于简单的文本补全任务,这种区别几乎无关紧要;但一旦涉及到工具、具备推理能力的模型或代理工作流,这种区别就变得非常重要了。
消息与项
这两种 API 之间的真正差异体现在它们各自响应的结构上。
在 Chat Completions 中,一切都以消息为中心。
print(response.choices[0].message.content)
# The generated text is inside the assistant message.
# response
# | choices
# | message
# | content
Responses API 的输出组织方式则有所不同。
print(response.output)
# A simplified structure:
# response
# | output
# | reasoning
# | function_call
# | message
# The important difference is that output is not a list of messages,
# but output items.
消息只是其中一种项;函数调用是另一种项;而支持推理的模型还可能生成推理项。这改变了人们对模型实际返回内容的认知方式。
Chat Completions
Model Output = Assistant Message
Responses API
Model Output = List of Output Items
- The structure which is useful for tool calling.
工具调用作为输出项
以一个用于报告天气的简单函数为例(这是随处可见的经典示例)。
def get_weather(city: str):
return f"The weather in {city} is 28°C"
# The weather is ofcoure hardcoded.
你可以将此函数作为工具定义暴露给模型。
tools = [
{
"type": "function",
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string"
}
},
"required": ["city"]
}
}
]
该工具定义会随你的请求一同被发送。
response = client.responses.create(
model="...",
tools=tools,
input="What is the weather in Bengaluru?"
)
从这里开始,模型有两条可能的处理路径。
Option 1: Generate a message
Option 2: Call get_weather
由于函数的实际天气数据并未内嵌在其定义中,模型需要实时数据,因此它会发出函数调用指令而非直接给出答案。
Function Call Item
name: get_weather
arguments:
{
"city": "Bengaluru"
}
你可以通过遍历响应中的输出项来找到这个函数调用。
for item in response.output:
if item.type == "function_call":
print(item.name)
print(item.arguments)
执行该操作后你会得到:
get_weather
{"city":"Bengaluru"}
此时模型尚未生成最终答案,它只是请求执行某个操作;实际运行函数的任务则由你的应用程序来完成。
result = get_weather("Bengaluru")
该结果需要被传回模型。
函数调用输出
你可以使用类型为function_call_output的元素来表示工具的输出结果。
tool_output = {
"type": "function_call_output",
"call_id": item.call_id,
"output": result
}
call_id字段将此输出与请求它的具体函数调用关联起来。
Function Call
| call_id: call_123
Application Executes Tool
Function Call Output
| call_id: call_123
当同时调用多个工具时,比如同时查询两个不同城市的天气状况,这种匹配就变得至关重要了。
基本工具执行循环
基于工具构建的应用程序通常会遵循一个重复的循环结构。
User Input ->
Model ->
Response Output Items ->
Check for Function Calls ->
Execute Functions ->
Create Function Call Outputs ->
Model ->
Final Response
以下是大致对应的代码形式。
response = client.responses.create(
model="...",
input=user_input,
tools=tools
)
while True:
function_calls = [
item
for item in response.output
if item.type == "function_call"
]
if not function_calls:
break
tool_outputs = []
for call in function_calls:
result = execute_tool(
call.name,
call.arguments
)
tool_outputs.append({
"type": "function_call_output",
"call_id": call.call_id,
"output": result
})
response = client.responses.create(
model="...",
previous_response_id=response.id,
input=tool_outputs,
tools=tools
)
只要模型持续返回函数调用,这个循环就会不断重复。一旦它停止请求工具调用,响应中就会包含模型的最终输出结果。
Model ->
Function Call ->
Tool Result ->
Model ->
Function Call ->
Tool Result ->
Model ->
Message
这个循环是大多数工具调用代理实现的基础。
结论
Responses API并非只是Chat Completions的换壳接口,它提供了一种更准确地反映现代大语言模型应用在涉及工具使用、推理及多种输出类型时的运作方式的结构。对于简单的对话场景,Chat Completions依然是可靠的选择。但一旦工作流开始偏向代理化模式,Responses API则更为合适——消息负责对话处理,而项目则负责执行操作,这就是其核心理念。
相关阅读
- 了解AI智能体:目标、工具、记忆与智能体循环 —— 以通俗易懂的方式讲解AI智能体与聊天机器人的区别,涵盖核心组件、决策循环、自主程度以及实际应用场景。
- AI智能体的结构化约束机制:ResolveFlow流程解析 —— 阐述基于LangGraph的智能体如何通过代码级检查而非提示指令来实现推理与执行的分离,同时介绍在此过程中出现的一个检索错误。