把 openai 升到 1.x 后,原来跑得好好的 ChatCompletion.create 突然报 AttributeError,这是最近迁移时最常见的现象。你手里可能既有旧版脚本,也有新环境里的 SDK v1 依赖。读完这篇,你能用最小改动把现有调用迁移到 OpenAI 兼容端点,并且拿到一个可以直接复制运行的 Python 脚本。
场景与前置条件
先说清楚这篇教程适合什么场景。你现在遇到的通常不是模型本身的问题,而是 openai 这个库的调用方式变了。0.28 升级到 1.0 以后,openai.ChatCompletion 模块被移除,所有聊天补全都要走 client.chat.completions.create。与此同时,如果你想把调用迁移到兼容 OpenAI 协议的端点,不需要重写业务代码,只要改两处:Base URL 和 API Key。本文的示例就在千木的 OpenAI 兼容端点上跑通。
动手之前,你手里应该已经有一个能发出请求的旧脚本,或者至少装好了 Python 3.8 以上环境。本文不会从零解释什么是 chat completions,只解决 SDK v1 带来的接口变化和端点迁移问题。如果你还在 0.28,先升级到 1.x,因为旧版的模块名在新代码里根本找不到。这个库的 v1 版本要求 Python 3.7 以上,但建议用 3.8 之后的版本,避免类型标注相关的兼容问题。网络方面,只要你的机器能访问目标 Base URL 即可,没有额外依赖。
环境准备
先装依赖。终端执行 pip install openai,会把 openai 升级到 1.x。如果你之前锁定了 0.28,建议先 pip uninstall openai 再重装,避免残留旧模块。安装完成后,命令行里可用 python -c 'import openai; print(openai.version)' 确认版本。v1 系列最明显的变化是客户端实例化方式,不再有全局 api_key 设置。
密钥不要硬编码进脚本。最稳妥的做法是写进环境变量,然后让 SDK 在初始化时从 os.environ 里读取。这样即使脚本上传到 Git,密钥也不会暴露。很多迁移后报 401 的案例,都是因为把密钥写死在了本地,结果换到服务器后环境变量没设置。你的平台会给你一个 sk_live_ 开头的 Key,这个 Key 只在创建时显示一次。把导出环境变量的命令加到 shell 配置文件里,例如 export 变量名=sk_live_xxx,然后用 source 生效。不要在脚本里写 api_key='sk_live_xxx',这会让排错更麻烦。OpenAI Python SDK v1 对环境变量的读取方式是显式的,你必须把密钥传入 OpenAI 构造函数。
使用环境变量而不是硬编码的另一个原因是,SDK v1 的日志和报错只会显示请求元信息,不会把环境变量内容打出来。如果硬编码,密钥一旦被复制到剪贴板或者粘贴到聊天记录,泄露面会大很多。环境变量还方便在不同项目之间切换 Key:只要改 shell 配置重新 source,不用动 Python 代码。这个习惯在迁移到任何兼容端点时都适用。
最小可跑示例
python
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ['SILVAMUX_API_KEY'],
base_url='https://www.silvamux.com/api/v1'
)
resp = client.chat.completions.create(
model='minimax-m2.5',
messages=[
{'role': 'user', 'content': '用一句话解释什么是迁移学习'}
]
)
print(resp.choices[0].message.content)
这段代码可以直接保存成 migrate_demo.py 运行。先看导入,from openai import OpenAI 是 v1 里的标准写法,旧版写 from openai import ChatCompletion 会直接找不到类。OpenAI(api_key=..., base_url=...) 把密钥和 Base URL 传给客户端,这是迁移的重点:不管官方还是兼容端点,都由 base_url 决定请求发到哪。代码里的 Base URL 一个字符都不能少,尤其要注意 https 前缀和 /api/v1 路径。少一个斜杠会请求到错误路径,表现为 404。
model 参数填的是 minimax-m2.5。这个值不能根据展示名自己拼,必须从模型广场详情页复制。很多刚迁移的人会把它写成自己猜的版本号,结果收到 model_not_found。messages 结构和旧版基本一致,但传参位置变了,从 openai.ChatCompletion.create 的命名参数变成 client.chat.completions.create 的命名参数。print 那一行打印的是 choices0.message.content,v1 的响应对象没有变化,还是这个取法。另外注意,初始化 OpenAI 时没有传 organization 参数。旧版 SDK 会用 openai.organization 做隔离,v1 里大部分开发者不再需要它。如果你原来的代码有 organization,迁移时可以先去掉,观察请求是否正常。很多人把 organization 和 base_url 一起改,结果排查时把两个变量搞混。
逐步扩展
最小示例只打印完整回复。真实项目里你经常需要流式输出,或者在请求头里带上自定义 ID 方便排错。这两个能力可以叠加,不需要额外安装包。
python
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ['SILVAMUX_API_KEY'],
base_url='https://www.silvamux.com/api/v1',
default_headers={'X-Request-Id': 'REQ-migrate-001'}
)
stream = client.chat.completions.create(
model='minimax-m2.5',
messages=[{'role': 'user', 'content': '用 Python 写一个 for 循环打印列表'}],
stream=True
)
for chunk in stream:
content = chunk.choices[0].delta.content if chunk.choices else None
if content:
print(content, end='', flush=True)
改动只有三处。一是 OpenAI 构造函数里加了 default_headers,X-Request-Id 可以自己设置,系统会沿用。格式按 REQ-xxxx 这类写,排查问题时把它提供给平台侧,能快速定位请求。二是 create 方法里 stream=True,告诉 SDK 走流式返回。三是最下面的循环,每次 chunk 先判断 chunk.choices 是否为空再取 delta.content。兼容端点可能会在流结束前返回一个 choices 为空但带着 usage 的 chunk,直接下标访问会抛 IndexError。这个判断是小细节,但能避开大量偶发报错。
流式输出迁移后,很多人的客户端还是只依赖 data: DONE 判断完成。这个习惯要改,因为末尾那个 usage chunk 可能不包含 choices。正确做法是以流对象结束为准,迭代完就说明本轮响应结束了。这段代码里没有打印 usage,若要拿 token 用量,可以在最后一次迭代的 usage 字段读取,但不要假设每个 chunk 都有 choices。Python 的流式迭代本身是惰性的,不会一次性把全部内容拉到内存里,这也是 v1 接口下较稳妥的处理方式。
排错
报错一:AttributeError: module 'openai' has no attribute 'ChatCompletion'。直接原因:你在 SDK v1 环境里调用了旧版模块。修法:把 openai.ChatCompletion.create 改成 client.chat.completions.create,用 OpenAI 类先实例化客户端。这个错误在迁移过程中出现频率最高,因为网上很多旧教程还在用老写法。
报错二:openai.AuthenticationError: Error code: 401。直接原因:密钥没有传到客户端,典型原因是没有设置环境变量,或者环境变量名写成了别的。修法:在终端重新 export 环境变量,确认值是 sk_live_ 开头;脚本里保持从 os.environ 读取,不要临时硬编码,也不要改环境变量名的大小写。
报错三:openai.APIConnectionError: Connection error.。直接原因:Base URL 写错。常见是把 www 写成了其他子域,或者漏掉了路径部分。修法:回到代码示例,把 Base URL 按原样复制。尤其是路径不能省,只写域名会请求到首页而不是 API 入口,返回的内容根本不是 JSON。
报错四:IndexError: list index out of range,发生在流式 for 循环里。直接原因:某个 chunk 的 choices 为空,代码直接取 chunk.choices0 导致越界。修法:先判断 chunk.choices 是否存在且非空,再取 delta.content。这个在上述扩展循环里已经处理。
常见问题
问:迁移后旧版 ChatCompletion 代码还能跑吗?
答:openai 1.x 提供了旧版兼容层,但不是所有方法都完整。长期看建议按新接口重写,因为后续小版本会逐渐移除旧垫片。旧写法能跑只是暂时的,排错时代码路径也不直观。
问:模型参数为什么不能自己拼版本号?
答:模型调用名是平台规范后的 alias,必须从模型广场详情页取。例如 minimax-m2.5 这个 alias 和你印象中的上游命名可能不同。自己拼的版本号如果不在平台列表里,会收到 404 或模型不存在错误,排查起来很费时间。
问:流式输出里为什么会出现空 choices 的 chunk?
答:一些兼容端点会在正常内容之后多发一个只带 usage 的用量块,choices 是空数组。这是协议行为,不代表出错。客户端要以流结束为准,不要看到 choices 为空就认为请求失败。
以上示例在千木SilvaMux的 OpenAI 兼容端点上验证,模型调用名为 minimax-m2.5。具体模型调用名以控制台模型广场为准,可查阅开发者文档获取最新列表和错误码定义。
