首页 / 文章

用Python将OpenAI的聊天循环移植到Claude:仅一页修改内容

将无状态消息循环迁移到 Anthropic 的 Messages API:顶层系统参数、必需的 max_tokens 值、内容块以及严格的角色切换机制。

发布者:REACTAPP.TOP阅读约 4 分钟867 词

来源: 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。