一、client.beta.threads.messages.create 方法
前文学习了"OpenAI Assistants API 架构",其中 client.beta.threads.messages.create 是向指定对话线程(Thread)添加消息的唯一方式,也是构建对话上下文、实现用户与助手交互的基础。
1、方法核心作用
client.beta.threads.messages.create 的核心功能是:
- 向一个已存在的
Thread(对话线程)中新增一条消息; - 消息可以是「用户发送的内容」(
role="user"),也可以是「手动补充的助手回复」(role="assistant",通常由 Run 自动生成); - 新增的消息会作为线程的上下文,后续调用
threads.runs.create时,助手会基于这些消息生成回复; - 支持附带文件、自定义元数据等扩展能力。
2、方法基本语法
1. 基础调用格式
python
# 最简调用(纯文本用户消息)
message = client.beta.threads.messages.create(
thread_id="线程ID", # 必选
role="user", # 必选
content="用户消息内容" # 必选
)
# 带可选参数的调用(如附件、元数据)
message = client.beta.threads.messages.create(
thread_id="线程ID",
role="user",
content="用户消息内容",
attachments=[{"file_id": "文件ID", "tools": [{"type": "code_interpreter"}]}], # 可选
metadata={"user_id": "12345"} # 可选
)
2. 关键说明
- 该方法是同步调用,调用成功后立即返回新增的消息对象;
- 消息一旦创建,会永久关联到该线程(除非手动调用
threads.messages.delete删除); - 支持的 Python SDK 版本:
openai>=1.0.0(旧版openai<1.0.0语法不同,已淘汰)。
3、完整参数详解
按「必选/可选」分类,结合实际使用场景说明,所有参数的 key 需和方法形参完全一致(也是之前 params 字典的核心依据):
| 参数名 | 类型 | 是否必选 | 取值范围/说明 | 示例 |
|---|---|---|---|---|
thread_id |
字符串 | ✅ 必选 | 目标线程的唯一 ID(创建线程时返回的 id) |
"thread_abc123xyz789" |
role |
字符串 | ✅ 必选 | 消息发送者角色: ✅ user:用户发送的消息(最常用) ✅ assistant:助手回复(通常由 Run 自动生成,手动创建需谨慎) |
"user" |
content |
字符串 / 列表 | ✅ 必选 | 消息内容,分两种格式: 1. 纯文本(推荐):直接传字符串 2. 多类型内容(文本+图片/文件):传列表,每个元素是「内容块」 | 纯文本:"帮我分析这个CSV文件" 多类型:[{"type":"text","text":{"value":"这张图里有什么?"}}, {"type":"image_file","image_file":{"file_id":"file_123"}}] |
attachments |
列表 | ❌ 可选 | 消息附带的文件及绑定的工具,每个元素是字典: - file_id:上传文件的 ID(需先调用 files.create 上传文件) - tools:该文件要使用的工具列表(如 code_interpreter 解析文件、retrieval 检索文件内容) |
[{"file_id":"file_123","tools":[{"type":"code_interpreter"}]}] |
metadata |
字典 | ❌ 可选 | 自定义元数据(键值对),OpenAI 不解析该字段,仅原样存储/返回,可用于关联业务数据(如用户ID、会话ID) | {"user_id": "u_9876", "biz_id": "order_123"} |
file_ids |
列表 | ❌ 可选(已废弃) | 旧版参数,用于关联文件 ID,OpenAI 推荐用 attachments 替代 |
["file_123"] |
4、典型使用示例
结合实际开发场景,给出可直接运行的示例(需替换自己的 API Key/ID):
示例1:添加纯文本用户消息(最常用)
python
import openai
# 初始化客户端
client = openai.OpenAI(api_key="你的API Key")
# 1. 先创建一个线程
thread = client.beta.threads.create()
# 2. 向线程添加纯文本用户消息
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="请问如何用Python计算1到100的和?"
)
# 打印新增消息的核心信息
print(f"消息ID:{message.id}")
print(f"消息角色:{message.role}")
print(f"消息内容:{message.content[0].text.value}")
示例2:添加带文件附件的用户消息(解析Excel/CSV)
python
# 前提:先上传文件(获取file_id)
file = client.files.create(
file=open("数据.csv", "rb"),
purpose="assistants" # 必须指定purpose为assistants
)
# 向线程添加带附件的消息(绑定code_interpreter工具)
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="帮我分析这个CSV文件,计算平均值和中位数",
attachments=[
{"file_id": file.id, "tools": [{"type": "code_interpreter"}]}
]
)
print(f"带附件的消息ID:{message.id}")
print(f"关联的文件ID:{message.attachments[0].file_id}")
示例3:添加多类型内容(文本+图片)
python
# 前提:先上传图片文件
image_file = client.files.create(
file=open("截图.png", "rb"),
purpose="assistants"
)
# 添加文本+图片的消息
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content=[
{"type": "text", "text": {"value": "请描述这张截图里的内容"}},
{"type": "image_file", "image_file": {"file_id": image_file.id}}
]
)
# 提取图片内容块
image_block = [c for c in message.content if c.type == "image_file"][0]
print(f"图片文件ID:{image_block.image_file.file_id}")
5、返回值结构
调用该方法后,会返回一个「Thread Message 对象」(OpenAI 封装的类字典对象),核心字段和 messages.data 元素一致:
python
# 返回值示例(简化版)
{
"id": "msg_789abc", # 消息唯一ID
"object": "thread.message", # 对象类型标记
"created_at": 1735689600, # 创建时间戳(秒)
"thread_id": "thread_123xyz",# 所属线程ID
"role": "user", # 消息角色
"content": [ # 消息内容(列表)
{"type": "text", "text": {"value": "用户消息内容", "annotations": []}}
],
"attachments": [], # 附件列表(无则空)
"metadata": {}, # 元数据(无则空)
"status": "completed", # 消息状态(通常为completed)
"assistant_id": None, # 助手消息才会有值,用户消息为None
"run_id": None # 关联的Run ID,用户消息为None
}
注意事项
- 角色限制 :
- 手动创建消息时,
role建议只使用user;assistant角色的消息通常由threads.runs.create自动生成,手动创建可能导致上下文混乱。
- 手动创建消息时,
- content 格式 :
- 纯文本优先用字符串格式(简洁),多类型内容才用列表格式;
- 列表格式中,
type支持text/image_file,其他类型(如file_citation)由 API 自动生成。
- 文件相关 :
- 上传文件时
purpose必须为assistants,否则无法关联到消息; - 绑定工具时,
code_interpreter用于解析文件(Excel/CSV/代码),retrieval用于检索文本文件内容。
- 上传文件时
- 速率限制 :
- 避免短时间内向同一线程创建大量消息,需遵守 OpenAI 的 API 速率限制(可参考官方文档)。
- 异常处理 :
-
调用时需捕获
openai.APIError(如无效 thread_id、文件不存在),示例:pythontry: message = client.beta.threads.messages.create(...) except openai.APIError as e: print(f"创建消息失败:{e}")
-
二、参数的两星解包
上述程序中client.beta.threads.messages.create(参数),参数是一个复杂的数据结构(是一个字典,字典的键值对需要传递给函数)。通常我们会简化成: client.beta.threads.messages.create(**params)
1、构建它的params 字典
params 是一个Python 字典 ,其核心要求是:字典的 key 必须完全匹配 client.beta.threads.messages.create 方法的参数名 ,value 必须符合该参数的类型和 OpenAI API 规范。
该函数场景,params 的结构分为「必选参数」和「可选参数」两部分,以下是完整说明:
1. 核心结构(示例代码)
python
# 构建的 params 示例(含附件)
params = {
# 必选参数(缺一不可)
"thread_id": "thread_xxxxxxx", # 字符串:消息所属的线程 ID
"role": "user", # 字符串:消息角色,仅支持 "user" 或 "assistant"
"content": "请问1+1等于多少?", # 字符串/列表:消息内容(纯文本用字符串,多类型用列表)
# 可选参数
"attachments": [ # 列表:消息附带的文件/工具配置
{
"file_id": "file_xxxxxxx", # 字符串:上传的文件 ID
"tools": [{"type": "code_interpreter"}] # 列表:绑定的工具(解析文件用)
}
],
# 其他可选参数(扩展用)
"metadata": {"user_id": "123"}, # 字典:自定义元数据(如用户标识、业务ID)
"file_ids": [] # 字符串列表:已废弃(推荐用 attachments)
}
2. 参数详细说明(与前述的表一致)
| Key 名 | 类型 | 是否必选 | 含义 |
|---|---|---|---|
thread_id |
字符串 | 是 | 消息要归属的线程 ID,确保消息进入正确的对话上下文 |
role |
字符串 | 是 | 消息发送者角色: - user:用户发送的消息 - assistant:助手返回的消息(通常由 API 自动生成) |
content |
字符串 / 列表 | 是 | 消息内容: - 纯文本:直接传字符串 - 多类型内容(文本+图片):传列表,例:[{"type":"text","text":{"value":"文本"}}, {"type":"image_file","image_file":{"file_id":"file_xxx"}}] |
attachments |
列表 | 否 | 消息附带的文件及工具配置,每个元素是字典,包含 file_id(文件ID)和 tools(要使用的工具,如 code_interpreter/retrieval) |
metadata |
字典 | 否 | 自定义元数据,可存储业务相关信息(如用户ID、会话ID),OpenAI 不会解析该字段,仅原样返回 |
file_ids |
列表 | 否 | 旧版参数(已推荐用 attachments),用于关联文件 ID |
2、两颗星(**) 字典解包
** 是 Python 的「字典解包运算符」,作用是把字典的键值对拆解成「关键字参数」(key=value)的形式传递给函数。
1. 核心原理(对比两种写法)
写法1:不用 **(硬编码参数)
python
# 如果不用**,需要这样写(繁琐且不灵活)
client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content=user_message,
# 附件需要单独判断,代码冗余
attachments=attachments if file_ids else None
)
写法2:用 **(字典解包)
python
# 先构建字典(动态添加参数)
params = {"thread_id": thread.id, "role": "user", "content": user_message}
if file_ids:
params["attachments"] = [...] # 动态添加附件参数
# 解包字典,等价于上面的硬编码写法
client.beta.threads.messages.create(**params)
2. 使用 ** 的核心优势(贴合函数场景)
-
动态添加参数,代码更简洁 :
函数中
file_ids是可选参数,只有传入时才需要加attachments。用字典解包可以先构建基础参数,再根据条件动态添加,避免写冗长的if-else来拼接函数参数。 -
避免参数冗余,可维护性更高 :
如果后续需要添加新参数(比如
metadata),只需在params字典中加一个key-value,无需修改函数调用行,代码扩展性更好。 -
符合 Python 最佳实践 :
当函数参数较多或参数需要动态生成时,字典解包是标准写法,比硬编码参数更易读、易调试(比如可以先打印
params确认参数是否正确)。
三、既有一星角包又有两星解包
1. * 和 ** 的区别(避免混淆)
| 运算符 | 作用 | 适用对象 | 示例 |
|---|---|---|---|
* |
列表/元组解包,拆成「位置参数」 | 列表、元组 | func(*[1,2,3]) → func(1,2,3) |
** |
字典解包,拆成「关键字参数」 | 字典 | func(**{"a":1,"b":2}) → func(a=1,b=2) |
2、 示例程序
python
# ===================== 第一步:定义一个需要多参数的函数 =====================
# 函数功能:计算商品订单的最终金额
# 位置参数(按顺序传值):product_name(商品名)、quantity(数量)
# 关键字参数(按key传值):unit_price(单价)、discount(折扣,默认1.0即无折扣)
def calculate_order_amount(product_name, quantity, unit_price, discount=1.0):
subtotal = quantity * unit_price # 小计 = 数量 × 单价
final_amount = subtotal * discount # 最终金额 = 小计 × 折扣
print(f"【{product_name}】订单详情:")
print(f"- 数量:{quantity}件 | 单价:{unit_price}元/件")
print(f"- 折扣:{discount}折 | 最终金额:{final_amount}元")
return final_amount
# ===================== 第二步:准备解包用的数据源 =====================
# 1. 列表(用于*解包成位置参数:对应 product_name、quantity)
position_args_list = ["Python编程手册", 5] # 商品名=Python编程手册,数量=5
# 2. 元组(和列表一样,也能被*解包,效果相同)
# position_args_tuple = ("Python编程手册", 5)
# 3. 字典(用于**解包成关键字参数:对应 unit_price、discount)
keyword_args_dict = {
"unit_price": 89.9, # 单价89.9元
"discount": 0.8 # 折扣8折
}
# ===================== 第三步:用解包方式调用函数 =====================
print("=== 方式1:解包调用(* + ** 结合)===")
# *position_args_list:把列表拆成位置参数 → 等价于 "Python编程手册", 5
# **keyword_args_dict:把字典拆成关键字参数 → 等价于 unit_price=89.9, discount=0.8
calculate_order_amount(*position_args_list, **keyword_args_dict)
# ===================== 对比:直接传参(和解包效果完全一致)=====================
print("\n=== 方式2:直接传参(等价于解包)===")
calculate_order_amount("Python编程手册", 5, unit_price=89.9, discount=0.8)
# ===================== 拓展:OpenAI API 场景的解包示例 =====================
print("\n=== 拓展:OpenAI API 中的解包===")
import openai
# 初始化客户端(替换为你的API Key)
client = openai.OpenAI(api_key="你的API Key")
# 1. 准备位置参数(线程ID、角色)→ 用元组存储(*解包)
thread_id = client.beta.threads.create().id # 创建临时线程
position_args = (thread_id, "user") # 对应 thread_id、role 位置参数
# 2. 准备关键字参数(内容、元数据)→ 用字典存储(**解包)
keyword_args = {
"content": "帮我计算5本Python手册的总价",
"metadata": {"order_id": "ORD12345"} # 自定义订单ID
}
# 3. 调用OpenAI API时结合*和**解包
message = client.beta.threads.messages.create(*position_args, **keyword_args)
print(f"创建的消息ID:{message.id}")
print(f"消息内容:{message.content[0].text.value}")
运行结果(示例)
=== 方式1:解包调用(* + ** 结合)===
【Python编程手册】订单详情:
- 数量:5件 | 单价:89.9元/件
- 折扣:0.8折 | 最终金额:359.6元
=== 方式2:直接传参(等价于解包)===
【Python编程手册】订单详情:
- 数量:5件 | 单价:89.9元/件
- 折扣:0.8折 | 最终金额:359.6元
=== 拓展:OpenAI API 中的解包 ===
创建的消息ID:msg_abc123xyz
消息内容:帮我计算5本Python手册的总价
说明
1. * 列表/元组解包(位置参数)
position_args_list = ["Python编程手册", 5]是一个列表,*position_args_list会把它拆成按顺序排列的位置参数 ,等价于直接写"Python编程手册", 5;- 位置参数的核心是「顺序」:列表/元组的第一个元素对应函数的第一个位置参数,第二个对应第二个,不能乱序;
- 元组和列表的解包效果完全一致(
*position_args_tuple和*position_args_list没区别)。
2. ** 字典解包(关键字参数)
keyword_args_dict = {"unit_price": 89.9, "discount": 0.8}是字典,**keyword_args_dict会把它拆成key=value形式的关键字参数 ,等价于直接写unit_price=89.9, discount=0.8;- 关键字参数的核心是「key名匹配」:字典的key必须和函数的参数名完全一致(比如不能把
unit_price写成price),顺序无关。
3. 两种解包结合使用
在函数调用中,* 解包的位置参数必须放在 ** 解包的关键字参数前面(符合 Python 的参数传递规则),比如:
python
# ✅ 正确:位置参数在前,关键字参数在后
calculate_order_amount(*position_args_list, **keyword_args_dict)
# ❌ 错误:关键字参数不能在位置参数前面
# calculate_order_amount(**keyword_args_dict, *position_args_list)
记住要点
*解包:把列表/元组拆成位置参数(按顺序传值,核心是"顺序匹配");**解包:把字典拆成关键字参数(按key传值,核心是"名称匹配");- 实战价值:
client.beta.threads.messages.create(**params),当参数需要动态生成(比如根据用户输入拼接参数列表/字典)时,解包能让代码更简洁、灵活,这也是在 OpenAI API 中用**params的核心原因。