面向想要对接大模型 API 的开发者,系统讲解两大主流 AI API 的协议设计、请求/响应格式、工具调用机制及核心差异。


1. 概述:两套协议的定位

当前 AI 应用开发中,最主流的两套 API 协议是:

  • OpenAI Chat Completions APIPOST /v1/chat/completions,GPT 系列模型的标准接口,也是行业事实标准(大量第三方模型兼容此格式)
  • Anthropic Messages APIPOST /v1/messages,Claude 系列模型的标准接口,设计更严格、结构更清晰

用 Java 类比:OpenAI 的协议像是 Servlet 规范——宽松、灵活、兼容性强;Anthropic 的协议像是 Spring WebFlux 的严格类型约束——更规范但限制更多。


2. 认证方式

OpenAI

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer sk-xxxxxxxx" \
  -H "Content-Type: application/json"

使用标准的 Bearer Token 认证,API Key 以 sk- 开头。

Anthropic

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: sk-ant-xxxxxxxx" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json"

使用自定义 Header x-api-key 认证,且必须指定 anthropic-version 版本号。

维度OpenAIAnthropic
认证方式Authorization: Bearer <key>x-api-key: <key>
版本控制URL 路径(/v1/Header(anthropic-version
Key 前缀sk-sk-ant-

3. 请求体格式

3.1 OpenAI 请求

{
  "model": "gpt-4o",
  "messages": [
    { "role": "system", "content": "你是一个有帮助的助手" },
    { "role": "user", "content": "你好,请介绍一下自己" }
  ],
  "temperature": 0.7,
  "max_tokens": 1000
}

3.2 Anthropic 请求

{
  "model": "claude-sonnet-4-20250514",
  "system": "你是一个有帮助的助手",
  "messages": [
    {
      "role": "user",
      "content": [{ "type": "text", "text": "你好,请介绍一下自己" }]
    }
  ],
  "temperature": 0.7,
  "max_tokens": 1024
}

3.3 关键差异

维度OpenAIAnthropic
系统提示(System Prompt)放在 messages 数组中,role: "system"独立的顶层 system 字段
消息角色systemuserassistanttooluserassistant
content 格式字符串(简单场景)或数组(多模态)始终是数组,每个元素有 type 字段
max_tokens可选,有默认值必填,不传会报错
消息顺序约束无强制交替严格交替,且第一条必须是 user
停止词stopstop_sequences
特有参数presence_penaltyfrequency_penaltytop_k

Anthropic 的严格约束是初学者最容易踩的坑:

// ❌ 错误:content 不能是字符串
{ "role": "user", "content": "你好" }

// ✅ 正确:content 必须是数组
{ "role": "user", "content": [{ "type": "text", "text": "你好" }] }
// ❌ 错误:第一条消息不能是 assistant
"messages": [
  { "role": "assistant", "content": [...] },
  { "role": "user", "content": [...] }
]

// ✅ 正确:必须以 user 开头,严格交替
"messages": [
  { "role": "user", "content": [...] },
  { "role": "assistant", "content": [...] },
  { "role": "user", "content": [...] }
]

注:Anthropic Python SDK 在传入字符串时会自动包装为数组,但直接调用 HTTP API 时必须手动处理。


4. 响应体格式

4.1 OpenAI 响应

{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "created": 1677652288,
  "model": "gpt-4o",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "你好!我是一个AI助手,很高兴为你服务。"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 20,
    "completion_tokens": 15,
    "total_tokens": 35
  }
}

4.2 Anthropic 响应

{
  "id": "msg_01XFDUDYJgAACzvnptvVoYEL",
  "type": "message",
  "role": "assistant",
  "model": "claude-sonnet-4-20250514",
  "content": [
    {
      "type": "text",
      "text": "你好!我是一个AI助手,很高兴为你服务。"
    }
  ],
  "stop_reason": "end_turn",
  "usage": {
    "input_tokens": 20,
    "output_tokens": 15
  }
}

4.3 响应结构对比

维度OpenAIAnthropic
回复内容位置choices[0].message.contentcontent[0].text
嵌套层级深(choices → message → content)浅(顶层 content 数组)
content 类型字符串数组(每个元素有 type)
结束原因finish_reason(值:stoplengthtool_callsstop_reason(值:end_turnmax_tokenstool_use
token 统计prompt_tokens + completion_tokensinput_tokens + output_tokens
支持多选项是(n 参数,多个 choices)否(始终返回一个结果)
判断依据存在 choices 字段存在 "type": "message"

用 Java 类比:OpenAI 的响应像是 List<Result> 包装(即使只有一个结果也用数组),Anthropic 的响应更像直接返回单个对象。

4.4 为什么响应体要设计成数组?

初学者常有疑问:既然大模型通常只返回一条回复,为什么 OpenAI 用 choices[] 数组、Anthropic 用 content[] 数组?这两个数组的设计动机其实完全不同。

OpenAI 的 choices[]:多候选方案

OpenAI 的请求参数中有一个 n 参数(默认为 1),表示"让模型一次生成 n 个独立的回复候选"。当 n > 1 时,choices 数组中就会有多个元素:

// 请求:n=3,让模型生成 3 个候选回复
{
  "model": "gpt-4o",
  "n": 3,
  "messages": [{"role": "user", "content": "给我的咖啡店起个名字"}]
}

// 响应:choices 中有 3 个独立的完整回复
{
  "choices": [
    { "index": 0, "message": { "content": "「晨光咖啡」" }, "finish_reason": "stop" },
    { "index": 1, "message": { "content": "「慢时光」" }, "finish_reason": "stop" },
    { "index": 2, "message": { "content": "「豆语」" }, "finish_reason": "stop" }
  ]
}

每个 choice 是一个完整且独立的回复,它们之间互不关联。这个设计适用于需要多样性的场景,比如让模型提供多个创意方案供用户挑选。即使 n=1(绝大多数情况),响应仍然是数组格式,只是数组中只有一个元素。

Anthropic 的 content[]:多内容块组合

Anthropic 的设计思路完全不同。它不支持 n 参数(永远只返回一条回复),但一条回复内部可以包含多个不同类型的内容块

// 当模型同时返回文本 + 工具调用时
{
  "content": [
    { "type": "text", "text": "我来帮你查一下天气。" },
    { "type": "tool_use", "id": "toolu_01A", "name": "get_weather", "input": {"city": "北京"} }
  ]
}
// 当模型返回多段文本(如思考过程 + 最终回答)时
{
  "content": [
    { "type": "thinking", "thinking": "用户问的是..." },
    { "type": "text", "text": "答案是42。" }
  ]
}

这些内容块共同组成一条回复的不同部分,它们是有序的、互相关联的。用 Java 类比:OpenAI 的 choicesList<Response>(多个独立回复),Anthropic 的 contentList<ContentBlock>(一个回复内的多个组成部分)。

对比总结
维度OpenAI choices[]Anthropic content[]
设计目的支持一次请求返回多个候选回复支持一条回复包含多种内容类型
触发条件设置 n > 1 参数模型决定混合输出(文本+工具调用等)
元素关系互相独立,可任选其一有序组合,共同构成完整回复
常见场景创意生成、A/B 测试工具调用、思维链、多模态输出
默认情况n=1,数组中只有 1 个元素纯文本时,数组中只有 1 个 text 块
实际开发中的处理方式
# OpenAI:通常只取第一个 choice
reply = response.choices[0].message.content

# 如果用了 n>1,可以遍历所有候选
for choice in response.choices:
    print(f"候选 {choice.index}: {choice.message.content}")

# Anthropic:需要遍历 content 块,按类型分别处理
for block in response.content:
    if block.type == "text":
        print(block.text)
    elif block.type == "tool_use":
        # 执行工具调用
        result = call_tool(block.name, block.input)

一句话总结:OpenAI 的数组是"多个平行世界的回答",Anthropic 的数组是"一个回答的多个组成零件"。


5. 流式响应(Streaming)

两者都支持 Server-Sent Events(SSE)流式输出,但事件格式不同。

5.1 OpenAI 流式

请求时加 "stream": true,响应为 SSE 事件流:

data: {"id":"chatcmpl-abc","choices":[{"delta":{"role":"assistant"},"index":0}]}

data: {"id":"chatcmpl-abc","choices":[{"delta":{"content":"你"},"index":0}]}

data: {"id":"chatcmpl-abc","choices":[{"delta":{"content":"好"},"index":0}]}

data: [DONE]

关键点:增量内容在 choices[0].delta.content 中,流结束标志是 data: [DONE]

5.2 Anthropic 流式

请求时加 "stream": true,响应为带类型的 SSE 事件流:

event: message_start
data: {"type":"message_start","message":{"id":"msg_01...","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"usage":{"input_tokens":20,"output_tokens":0}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"你"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"好"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_stop
data: {"type":"message_stop"}

关键点:Anthropic 的流式事件有明确的生命周期(message_start → content_block_start → delta… → content_block_stop → message_stop),结构更清晰但解析更复杂。

维度OpenAIAnthropic
增量字段choices[0].delta.contentdelta.text(在 content_block_delta 事件中)
结束标志data: [DONE]event: message_stop
事件类型无(全是 data:有明确的 event: 类型标签
生命周期扁平(一连串 delta)分层(message → content_block → delta)

6. 工具调用(Tool Use / Function Calling)

工具调用是构建 AI Agent 的核心能力。两者的设计思路差异最大。

6.1 工具定义

OpenAI

{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "获取指定城市的天气",
        "parameters": {
          "type": "object",
          "properties": {
            "city": { "type": "string", "description": "城市名称" }
          },
          "required": ["city"]
        }
      }
    }
  ],
  "tool_choice": "auto"
}

Anthropic

{
  "tools": [
    {
      "name": "get_weather",
      "description": "获取指定城市的天气",
      "input_schema": {
        "type": "object",
        "properties": {
          "city": { "type": "string", "description": "城市名称" }
        },
        "required": ["city"]
      }
    }
  ],
  "tool_choice": { "type": "auto" }
}

差异:OpenAI 多一层 function 包装和 type: "function" 声明;Anthropic 更扁平,参数 schema 字段名为 input_schema(而非 parameters)。

6.2 模型返回工具调用

OpenAI 模型决定调用工具时:

{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "call_abc123",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"city\":\"北京\"}"
        }
      }]
    },
    "finish_reason": "tool_calls"
  }]
}

注意:arguments 是 JSON 字符串(不是对象),需要自己 JSON.parse()

Anthropic 模型决定调用工具时:

{
  "content": [
    { "type": "text", "text": "好的,我来查询北京的天气。" },
    {
      "type": "tool_use",
      "id": "toolu_01AbC",
      "name": "get_weather",
      "input": { "city": "北京" }
    }
  ],
  "stop_reason": "tool_use"
}

注意:input 直接是 JSON 对象(不需要额外解析),且工具调用和文本回复可以共存在同一个 content 数组中。

6.3 回传工具执行结果

这是两者差异最大的地方。

OpenAI:使用独立的 tool 角色:

{
  "messages": [
    { "role": "user", "content": "北京天气怎么样?" },
    {
      "role": "assistant",
      "content": null,
      "tool_calls": [{ "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"北京\"}" } }]
    },
    {
      "role": "tool",
      "tool_call_id": "call_abc123",
      "content": "{\"temperature\": \"22°C\", \"condition\": \"晴\"}"
    }
  ]
}

Anthropic:工具结果放在 user 消息中,类型为 tool_result

{
  "messages": [
    { "role": "user", "content": [{ "type": "text", "text": "北京天气怎么样?" }] },
    {
      "role": "assistant",
      "content": [
        { "type": "text", "text": "好的,我来查询北京的天气。" },
        { "type": "tool_use", "id": "toolu_01AbC", "name": "get_weather", "input": { "city": "北京" } }
      ]
    },
    {
      "role": "user",
      "content": [{
        "type": "tool_result",
        "tool_use_id": "toolu_01AbC",
        "content": "{\"temperature\": \"22°C\", \"condition\": \"晴\"}"
      }]
    }
  ]
}

6.4 工具调用对比总结

维度OpenAIAnthropic
工具调用位置message.tool_calls 数组content 数组中的 tool_use
参数格式JSON 字符串(需 parse)JSON 对象(直接可用)
结果回传角色独立的 role: "tool"放在 role: "user" 中,类型为 tool_result
关联 ID 字段tool_call_idtool_use_id
结束标志finish_reason: "tool_calls"stop_reason: "tool_use"
文本与工具共存不能(content 为 null)可以(同一 content 数组中)

Anthropic 的设计哲学是:一切都是 content block。文本是 content block,工具调用是 content block,工具结果也是 content block。这种统一的结构在处理复杂的多工具调用场景时更一致,但初学者需要适应"工具结果伪装成 user 消息"这个设计。


7. 多模态(图片输入)

7.1 OpenAI

{
  "model": "gpt-4o",
  "messages": [{
    "role": "user",
    "content": [
      { "type": "text", "text": "这张图片里有什么?" },
      {
        "type": "image_url",
        "image_url": {
          "url": "https://example.com/image.jpg"
        }
      }
    ]
  }]
}

支持 URL 和 base64 两种方式,base64 格式为 data:image/jpeg;base64,/9j/4AAQ...

7.2 Anthropic

{
  "model": "claude-sonnet-4-20250514",
  "messages": [{
    "role": "user",
    "content": [
      {
        "type": "image",
        "source": {
          "type": "base64",
          "media_type": "image/jpeg",
          "data": "/9j/4AAQ..."
        }
      },
      { "type": "text", "text": "这张图片里有什么?" }
    ]
  }],
  "max_tokens": 1024
}
维度OpenAIAnthropic
图片类型标识"type": "image_url""type": "image"
URL 支持直接传 URL需要通过 "type": "url" 的 source
Base64 格式data URI 字符串结构化对象(media_type + data 分开)
支持格式JPEG、PNG、GIF、WebPJPEG、PNG、GIF、WebP

8. Python SDK 使用示例

8.1 OpenAI SDK

from openai import OpenAI

client = OpenAI(api_key="sk-xxx")

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "你是一个有帮助的助手"},
        {"role": "user", "content": "你好"}
    ],
    temperature=0.7,
    max_tokens=1000
)

# 获取回复
print(response.choices[0].message.content)

8.2 Anthropic SDK

import anthropic

client = anthropic.Anthropic(api_key="sk-ant-xxx")

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    system="你是一个有帮助的助手",
    messages=[
        {"role": "user", "content": "你好"}
    ],
    temperature=0.7,
    max_tokens=1024
)

# 获取回复
print(response.content[0].text)

注意:使用 SDK 时,Anthropic SDK 会自动处理 content 的数组包装,你可以直接传字符串。但理解底层协议格式对于调试和理解 Agent 框架源码至关重要。


9. Agent Loop 中的协议应用

理解这两套协议对于构建 AI Agent 至关重要。Agent 的核心循环是:

用户输入 → 调用 LLM → 模型决定调用工具 → 执行工具 → 把结果回传给 LLM → 模型生成最终回复

用代码表示(以 Anthropic 为例):

import anthropic

client = anthropic.Anthropic()
messages = []
tools = [...]  # 工具定义

# Agent Loop
def agent_loop(user_input: str) -> str:
    messages.append({"role": "user", "content": user_input})

    while True:
        # 1. 调用模型
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            system="你是一个有帮助的助手,可以使用工具完成任务。",
            messages=messages,
            tools=tools,
            max_tokens=4096
        )

        # 2. 把模型回复加入历史
        messages.append({"role": "assistant", "content": response.content})

        # 3. 判断是否需要调用工具
        if response.stop_reason == "tool_use":
            # 找到所有工具调用
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    # 4. 执行工具
                    result = execute_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })

            # 5. 把工具结果回传(作为 user 消息)
            messages.append({"role": "user", "content": tool_results})
            # 继续循环,让模型处理工具结果

        else:
            # 6. 模型生成了最终回复,退出循环
            final_text = ""
            for block in response.content:
                if hasattr(block, "text"):
                    final_text += block.text
            return final_text

这就是你正在学习的 s01_agent_loop/code.py 的核心逻辑。理解了协议格式,就能理解为什么 Agent 代码要这样组织消息。


10. 设计哲学对比总结

维度OpenAIAnthropic
设计风格宽松灵活,向后兼容严格规范,结构清晰
消息模型messages 是统一的时序容器,角色丰富分离系统指令与对话,角色精简
content 设计简单场景用字符串,复杂场景用数组统一用数组,一切皆 content block
工具调用独立角色 + 独立字段嵌入 content 数组,保持消息交替
行业地位事实标准,大量第三方兼容独立协议,生态较小但设计更现代
适合场景快速接入、多模型切换深度集成、Agent 开发

对于 Agent 开发者的建议:两套协议都要熟悉。OpenAI 格式是行业通用语言,很多开源框架(LangChain、LlamaIndex)默认使用;Anthropic 格式在 Agent 场景下的 tool_use 设计更优雅,Claude 模型在长上下文和工具调用方面表现出色。实际项目中,可以使用 LiteLLM 或 Portkey 等代理层统一两套协议。


参考来源

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐