用Python将OpenAI的聊天循环移植到Claude:仅一页修改内容
将无状态消息循环迁移到 Anthropic 的 Messages API:顶层系统参数、必需的 max_tokens 值、内容块以及严格的角色切换机制。
来源: Part 3 — Call the Claude API in Python (same messages, system on the side)
任何曾经为 OpenAI 编写过对话循环的人,只需大约一页的修改就能将其迁移到 Anthropic 的 Claude 上。两家服务的基本结构相同:每次调用都会完整发送带有角色标签的 messages 列表,回复则附加在其后。只需修改一个元素,且明确知道该修改哪个元素会很有帮助。
相同的列表,不同的系统结构
Claude 的 Messages API 接受你已熟悉的对话格式:包含 role 和 content 字段的字典列表,服务器端无状态,每次都会完整重新发送。角色名称及其含义完全一致,均为 user 和 assistant。
唯一的结构差异在于system并非一条消息。OpenAI将指令放在列表的第一行,而Anthropic则将其提升为顶层system=参数,与列表一同存在。列表中仅包含实际的对话内容,即严格交替出现的用户和助手的发言。
import anthropic
client = anthropic.Anthropic() # key from env
resp = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=300,
system="You are a concise Python assistant.",
messages=[
{
"role": "user",
"content": "Why is system a parameter "
"here, not a message?",
},
],
)
print(resp.content[0].text)
在开始上述操作之前,还有两个较小的差异需要注意:
max_tokens必须被设置。 OpenAI 允许在没有输出上限的情况下发送请求,而 Claude 要求每次调用都必须指定该值。这虽然有点烦人,但有助于控制成本,因为只有在确定回复长度后才能发起请求。- 回复以内容块的形式呈现。 你得到的不是单个字符串,而是一个列表,纯文本位于
resp.content[0].text中。虽然现在看来有些多余,但由于后续的工具调用也采用相同结构,这一设计还是有其必要的。
设置与单次交互
使用 pip install anthropic 安装 SDK,然后从 Anthropic 控制台导出密钥(以 sk-ant- 开头)并将其设置为 ANTHROPIC_API_KEY。客户端会自动读取该密钥。
将其放在 OpenAI 调用旁边,两者之间的差异仅在于有四处修改:方法从 client.chat.completions.create 变为 client.messages.create,系统行被移至 system= 位置,必须设置输出上限,且文本读取路径从 choices[0].message.content 改为 content[0].text。关于 SDK 的说明:当前的 anthropic 包不支持在 messages.create 中使用 temperature= 参数,因此包含该参数的旧代码示例会报错。这些示例中使用的是默认采样方式,完全没问题。
令牌计数分别存储在 resp.usage.input_tokens 和 resp.usage.output_tokens 中。实际上它们是同一个计数器,只是字段名称略有不同,因此每次操作后都值得记录下来。
Claude 的历史记录循环
这是从 OpenAI 版本移植过来的 ask() 函数。追加后重新发送的逻辑没有变化,只有中间的调用方式有所调整。
msgs = []
def ask(text: str) -> str:
msgs.append(
{"role": "user", "content": text}
)
resp = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=300,
system="You are a concise assistant.",
messages=msgs, # full history
)
reply = resp.content[0].text
msgs.append(
{"role": "assistant", "content": reply}
)
return reply
print(ask("Define a context window."))
print(ask("Now for a five-year-old."))
print(ask("Which answer was shorter?"))
对话中的第三个问题依然会引发循环问题,因为只有当之前的回答都存储在msgs中并被重新发送时,该问题才会出现。同样的经济性原则也适用:每轮对话的输入令牌数量都会增加,因此较长的对话每次调用所需的成本也会更高。
Claude对消息顺序的要求更为严格,消息必须在用户和助手之间交替发送。如果连续出现两条用户消息——这种情况在重试失败请求时很容易发生——Claude会拒绝该调用,而OpenAI则可以容忍。这可以视为管理对话历史记录的系统的默认规则。
运行方式
该脚本会先运行一轮对话,然后再运行三轮对话,每次调用后都会记录输入和输出令牌的数量。如果缺少ANTHROPIC_API_KEY,它会立即停止并给出明确提示。此外,该脚本会发起实时计费请求,因此请勿在任何CI流程中运行它。
pip install -r requirements.txt
export ANTHROPIC_API_KEY="sk-ant..."
python examples/part03_claude.py
为何端口迁移如此简单,以及你能获得什么
由于两家提供商都提供相同的接口契约——即一个无状态端点,每次调用都会接收完整的对话内容并仅回复一次——因此改动其实很小。不同之处在于具体实现细节,比如系统提示语的位置、回复对象的结构以及所需参数等。LangChain 对这些复杂细节进行了抽象处理,其 ChatOpenAI 和 ChatAnthropic 类就是为了替代你之前需要手动处理的四行差异代码而存在的。了解这些抽象层所隐藏的内容,才是正确使用它与被其束缚之间的区别。
这还有实际的好处。通过一个ask()接口调用两个提供者,你可以对模型进行A/B测试,在API出现故障时实现自动切换,或将简单问题发送给一个提供者、复杂问题发送给另一个提供者,而无需修改应用程序代码。Google的Gemini API也采用相同的对话结构,只是角色名称有所变化:助手被称作model。