ᕕ( ᐛ )ᕗ Jimyag's Blog

OpenAI Chat Completions、Responses 与 Anthropic Messages API 详解

OpenAI Chat Completions、OpenAI Responses 和 Anthropic Messages 都能完成模型调用,但状态模型并不相同:Chat Completions 以聊天消息为中心;Responses 把消息、推理和工具调用统一建模为 Item;Anthropic Messages 则在消息内部使用不同类型的 content block。这些差异会直接影响应用如何保存多轮状态、压缩上下文、执行工具和处理流式事件。

下面分别从最小请求讲起,再讨论多轮消息、上下文管理、工具调用、结构化输出、流式响应和常见边界,最后对比三套 API 的状态协议。

本文讨论的是直接调用 HTTP API 的行为,不是 ChatGPT 或 Claude 网页产品中的聊天记录和记忆功能。示例中的 OPENAI_MODEL_IDCLAUDE_MODEL_ID 需要替换为账号当前可用、并且支持对应能力的模型。

OpenAI Chat Completions API

Chat Completions 的入口是 POST /v1/chat/completions。请求的核心是 messages,响应的核心是 choices。如果应用已经把一段对话保存成按时间排列的消息列表,这套接口最容易理解。

基础调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "messages": [
      {"role": "developer", "content": "回答要准确、简洁。"},
      {"role": "user", "content": "解释什么是幂等性。"}
    ]
  }'

一个普通响应大致如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
  "id": "chatcmpl_...",
  "object": "chat.completion",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "幂等性是指同一个操作执行一次或多次,结果保持一致。"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 28,
    "completion_tokens": 22,
    "total_tokens": 50
  }
}

最常读取的字段是:

  • choices[0].message:模型生成的完整 assistant 消息。
  • choices[0].message.content:普通文本内容。
  • choices[0].message.tool_calls:模型要求应用执行的工具调用。
  • choices[0].finish_reason:本次生成结束的原因。
  • usage:输入和输出 token 用量。

不要只保存 message.content。一条 assistant message 还可能包含工具调用、拒绝信息或其他结构化内容,后续多轮对话可能需要原样带回。

消息角色和内容

常用角色包括:

  • developer:应用开发者给模型的稳定规则。新模型通常优先使用该角色。
  • system:系统级指令,旧模型和部分兼容实现仍常用。
  • user:用户输入。
  • assistant:模型以前的回复。
  • tool:应用执行工具后的结果。

简单文本可以直接写进 content。需要传入图片或其他多模态内容时,content 可以改为由多个 content part 组成的数组。具体支持的内容类型取决于模型,不能只根据接口名称判断模型是否支持图片、音频或文件。

下面的请求同时发送文字和图片 URL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "messages": [{
      "role": "user",
      "content": [
        {"type": "text", "text": "描述这张图片。"},
        {
          "type": "image_url",
          "image_url": {"url": "https://example.com/image.jpg"}
        }
      ]
    }]
  }'

响应仍然是普通 assistant message,图片不会改变 choices[].message.content 的读取方式。

多轮消息

Chat Completions 本身不保存对话状态。下一轮请求必须再次发送需要保留的历史消息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "messages": [
      {"role": "developer", "content": "回答要准确、简洁。"},
      {"role": "user", "content": "法国首都是哪里?"},
      {"role": "assistant", "content": "法国首都是巴黎。"},
      {"role": "user", "content": "它位于哪条河沿岸?"}
    ]
  }'

第二次请求中的 assistant 消息来自第一次响应。示例直接写出该消息,是为了展示最终发送给 API 的完整请求体。

生产环境一般不会把 messages 只放在进程内存中,而是会保存到数据库。建议同时保存:

  • 原始角色和完整内容。
  • assistant 的完整消息对象。
  • 工具调用 ID 及其执行结果。
  • 使用的模型、请求时间和 token 用量。
  • 应用自己的会话 ID、用户 ID 和租户 ID。

这样既能继续对话,也能审计模型实际看到了什么。

上下文窗口和压缩

每次请求中的 developer/system 指令、历史消息、工具定义、工具结果和当前问题都会占用上下文窗口。随着轮数增加,请求会越来越大,成本和延迟也会上升,最终还可能超过模型的上下文限制。

Chat Completions 没有对应 Responses API 服务端 compaction 的内置机制。应用需要自行选择一种策略:

  1. 滑动窗口:只保留最近若干轮。实现简单,但较早的约束和事实会丢失。
  2. 摘要旧消息:让模型把较早对话压缩成一条摘要,再保留最近几轮原文。
  3. 提取结构化状态:把用户偏好、任务状态和关键事实保存为 JSON,需要时重新注入。
  4. 检索历史记录:完整记录放在外部存储中,只检索当前问题需要的片段。

一个常见的消息布局是:

1
2
3
4
5
developer 指令
较早对话的摘要
必须长期保留的结构化事实
最近几轮原始消息
当前用户消息

例如,应用已经把较早对话压缩成摘要,只保留最近一轮原文时,可以发送:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "messages": [
      {"role": "developer", "content": "回答要准确、简洁。"},
      {
        "role": "developer",
        "content": "历史摘要:用户正在排查数据库连接超时;已经确认连接池上限为 20,下一步检查慢查询。"
      },
      {"role": "user", "content": "刚才的慢查询日志里出现了锁等待。"},
      {"role": "assistant", "content": "请先确认锁等待涉及的事务和持续时间。"},
      {"role": "user", "content": "事务持续了 45 秒,应该先查什么?"}
    ]
  }'

这里第二条 developer message 是应用生成并信任的摘要,不应直接塞入未经处理的用户文本。

摘要是一种有损压缩,不能直接替代所有历史。压缩时至少要注意:

  • 不要把稳定的 developer/system 指令混进可随意改写的摘要。
  • 不要拆散一组 assistant tool_calls 和对应的 tool 结果。
  • 订单号、资源 ID、金额等精确数据更适合结构化保存,而不是只写进自然语言摘要。
  • 摘要生成失败时,不要覆盖最后一份可用上下文。
  • 在发送请求前估算 token,并为模型输出预留空间。

如果应用需要服务端管理对话状态或自动压缩上下文,Responses API 通常更合适。

工具调用

Chat Completions 使用 tools 声明函数。模型不会真正执行函数,只会生成调用请求;应用负责验证参数、执行函数,再把结果作为 tool 消息发回。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "messages": [
      {"role": "user", "content": "台北今天天气如何?"}
    ],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "查询指定城市的天气",
        "parameters": {
          "type": "object",
          "properties": {"city": {"type": "string"}},
          "required": ["city"],
          "additionalProperties": false
        },
        "strict": true
      }
    }]
  }'

模型决定调用工具时,响应中的关键部分如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "call_123",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"city\":\"台北\"}"
        }
      }]
    },
    "finish_reason": "tool_calls"
  }]
}

应用读取 call_123 和参数,执行 get_weather 后,第二次请求需要同时带回原始 assistant message 和工具结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "messages": [
      {"role": "user", "content": "台北今天天气如何?"},
      {
        "role": "assistant",
        "content": null,
        "tool_calls": [{
          "id": "call_123",
          "type": "function",
          "function": {
            "name": "get_weather",
            "arguments": "{\"city\":\"台北\"}"
          }
        }]
      },
      {
        "role": "tool",
        "tool_call_id": "call_123",
        "content": "{\"temperature\":28,\"condition\":\"多云\"}"
      }
    ]
  }'

第二次响应回到普通 assistant 文本:

1
2
3
4
5
6
7
8
9
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": "台北目前 28°C,多云。"
    },
    "finish_reason": "stop"
  }]
}

一次响应可能包含多个 tool_calls。应用应逐个按调用 ID 返回结果,并把完整的 assistant 工具调用消息保留在历史中。工具参数来自模型,必须像外部输入一样做 schema 校验、权限检查和超时限制。

结构化输出

如果结果要交给程序消费,可以使用 response_format 约束输出,而不是提示模型“只返回 JSON”。JSON Schema 模式的基本形态如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "messages": [{
      "role": "user",
      "content": "把下面的问题整理成工单:数据库连接经常超时。"
    }],
    "response_format": {
      "type": "json_schema",
      "json_schema": {
        "name": "ticket",
        "strict": true,
        "schema": {
          "type": "object",
          "properties": {
            "title": {"type": "string"},
            "priority": {
              "type": "string",
              "enum": ["low", "medium", "high"]
            }
          },
          "required": ["title", "priority"],
          "additionalProperties": false
        }
      }
    }
  }'

响应中的结构化结果仍位于 assistant message 的 content 字符串中:

1
2
3
4
5
6
7
8
9
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": "{\"title\":\"数据库连接经常超时\",\"priority\":\"high\"}"
    },
    "finish_reason": "stop"
  }]
}

应用需要对 content 再做一次 JSON 反序列化,得到工单对象。

结构化输出保证的是模型输出符合受支持的 schema,不代表字段内容一定符合业务事实。应用仍然要检查拒答、截断和业务约束。

流式输出

设置 stream=true 后,服务端通过 Server-Sent Events 返回 chat.completion.chunk。文本不是一次返回,而是出现在 choices[].delta.content 中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
curl -N https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "messages": [
      {"role": "user", "content": "写一段关于台北雨夜的短文。"}
    ],
    "stream": true
  }'

curl -N 会关闭输出缓冲。下面省略了 createdmodel 等重复字段;同一次请求中的 chunk 使用相同的 id

1
2
3
4
5
6
7
8
9
data: {"id":"chatcmpl_123","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl_123","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"台北"},"finish_reason":null}]}

data: {"id":"chatcmpl_123","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"的雨夜。"},"finish_reason":null}]}

data: {"id":"chatcmpl_123","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]

把每个 delta.content 按顺序拼接,最终得到「台北的雨夜。」。finish_reason 出现在结束 chunk,而不是每个文本 chunk 中。

原始 SSE 流最终通常以 data: [DONE] 结束。最后一个 chunk 还可能给出 finish_reason;如果请求启用了流式 usage 选项,usage 可能出现在单独的尾部 chunk 中。

工具调用也可以流式返回,但 delta.tool_calls[].function.arguments 只是参数字符串的一部分。应用必须按 choice、tool call 的 index 和 ID 累积片段,等调用结束后再解析完整 JSON,不能对每个 delta 单独执行 json.loads,更不能在参数尚未完整时执行工具。

结束原因和错误处理

常见 finish_reason 包括正常结束、达到输出上限、工具调用和内容过滤等情况。应用不应把所有非空文本都当作完整答案:

  • 达到长度上限时,JSON 或代码可能只生成了一半。
  • 存在 tool_calls 时,应进入工具执行循环。
  • 请求超时后重试可能产生重复工具操作,工具应尽量幂等。
  • HTTP 429 和 5xx 可以退避重试;参数错误和权限错误不应盲目重试。

完整字段以 Chat Completions API Reference 为准。

OpenAI Responses API

Responses API 的入口是 POST /v1/responses。OpenAI 建议新项目优先考虑它。它不再假设一次模型运行只产生一条 assistant message,而是用 output[] 保存多个有类型的 Item,例如消息、推理和工具调用。

基础调用

1
2
3
4
5
6
7
8
curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "instructions": "回答要准确、简洁。",
    "input": "解释什么是幂等性。"
  }'

典型响应可能同时包含 reasoning Item 和 message Item:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "id": "resp_...",
  "object": "response",
  "status": "completed",
  "output": [
    {"type": "reasoning", "id": "rs_...", "summary": []},
    {
      "type": "message",
      "id": "msg_...",
      "role": "assistant",
      "status": "completed",
      "content": [
        {"type": "output_text", "text": "幂等性是指……", "annotations": []}
      ]
    }
  ],
  "usage": {}
}

使用原始 HTTP 响应时,需要在 output 中找到 typemessage 的 Item,再读取其中的 output_text

需要实现 Agent、审计工具调用或保存多轮上下文时,应遍历完整 response.output,并根据每个 Item 的 type 处理。不能假设 output[0] 一定是文本消息。

多轮消息:三种管理方式

Responses 提供三种常见的对话状态管理方式。

方式一:应用保存完整 Item

应用把上一轮的所有 response.output 追加到下一轮 input,再加入新的 user message。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "instructions": "回答要准确、简洁。",
    "store": false,
    "input": [
      {
        "role": "user",
        "content": [{"type": "input_text", "text": "法国首都是哪里?"}]
      },
      {
        "type": "message",
        "id": "msg_123",
        "role": "assistant",
        "status": "completed",
        "content": [{
          "type": "output_text",
          "text": "法国首都是巴黎。",
          "annotations": []
        }]
      },
      {
        "role": "user",
        "content": [{"type": "input_text", "text": "它位于哪条河沿岸?"}]
      }
    ]
  }'

示例为了便于阅读,只展示了一条 assistant message。实际应用必须把第一次响应的全部 output Item 原样插入第二次请求,而不只是最终文本。推理模型可能需要上一轮的 reasoning Item 或加密推理内容来保持推理连续性,工具调用 Item 也必须和结果配对。

方式二:使用 previous_response_id

1
2
3
4
5
6
7
8
9
curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "instructions": "回答要准确、简洁。",
    "previous_response_id": "resp_123",
    "input": "它位于哪条河沿岸?"
  }'

其中 resp_123 来自上一轮响应的 id

OpenAI 会关联前一条 Response。需要注意:

  • 前一轮顶层 instructions 不会因为设置了 previous_response_id 而自动继承,稳定指令应在后续请求中重新发送。
  • 关联历史并不意味着历史输入免费,链中的历史 token 仍会计入输入用量。
  • 应用仍应保存 response ID,并明确处理响应保留、删除和数据治理要求。

方式三:使用 Conversations API

需要让一段长期对话跨会话、设备或任务持续存在时,可以创建 conversation,再让多个 Response 关联同一个 conversation。它适合服务端托管持久对话状态;如果应用必须精确控制每个历史 Item 的裁剪和跨供应商迁移,手动管理通常更透明。

先创建 conversation:

1
2
3
4
5
6
curl https://api.openai.com/v1/conversations \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "metadata": {"topic": "地理问答"}
  }'

响应会返回一个持久 conversation ID:

1
2
3
4
5
{
  "id": "conv_123",
  "object": "conversation",
  "metadata": {"topic": "地理问答"}
}

后续调用把 conversation 指向同一个 ID:

1
2
3
4
5
6
7
8
curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "conversation": "conv_123",
    "input": "法国首都是哪里?"
  }'

下一轮继续使用 conversation: "conv_123",只把 input 换成「它位于哪条河沿岸?」。此前写入 conversation 的 Item 会由服务端参与后续请求。

三种方式不应在没有明确状态边界的情况下混用。应用最好为一段会话固定一种主策略,并把 provider conversation ID、response ID 和本地会话 ID 分开保存。

上下文压缩

Responses 支持服务端 compaction。它会在上下文达到阈值时,把较早内容压缩成一个可继续传回 API 的 compaction Item,以减少后续请求实际携带的上下文。

自动服务端压缩

在请求中配置 context_management

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "store": false,
    "input": [
      {"role": "user", "content": "继续完成当前任务。"}
    ],
    "context_management": [{
      "type": "compaction",
      "compact_threshold": 100000
    }]
  }'

达到阈值时,普通 Response 的 output 中会多出一个不透明的 compaction Item:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "id": "resp_123",
  "object": "response",
  "output": [
    {
      "id": "cmp_123",
      "type": "compaction",
      "encrypted_content": "gAAAAAB...="
    },
    {
      "id": "msg_123",
      "type": "message",
      "role": "assistant",
      "status": "completed",
      "content": [{
        "type": "output_text",
        "text": "继续处理当前任务。",
        "annotations": []
      }]
    }
  ]
}

encrypted_content 不是给人阅读的摘要。应用应保存整个 Item,并在下一轮原样传回。

达到阈值后,本次响应的 output 会包含 compaction Item。手动维护 input_items 时,应把完整 response.output 追加回上下文。最新 compaction Item 已经代表它之前的压缩状态,因此应用可以丢弃该 Item 之前的旧 Item,再继续追加新的输入。

如果使用 previous_response_id 链接状态,不要再自行删除链中的 Item;让服务端按链和 compaction 配置管理上下文即可。

独立压缩接口

应用也可以显式调用 POST /v1/responses/compact,把当前上下文压缩为下一轮可以直接使用的规范 Item 列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
curl https://api.openai.com/v1/responses/compact \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "input": [
      {"role": "user", "content": "分析这份较长的会话记录。"},
      {"role": "assistant", "content": "这里是前一轮输出。"},
      {"role": "user", "content": "保留关键约束并压缩上下文。"}
    ]
  }'

独立压缩接口返回 response.compaction 对象。下面的响应节选保留了一个 user Item 和一个加密 compaction Item:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "id": "resp_123",
  "object": "response.compaction",
  "output": [
    {
      "id": "msg_001",
      "type": "message",
      "status": "completed",
      "content": [{
        "type": "input_text",
        "text": "分析这份较长的会话记录。"
      }],
      "role": "user"
    },
    {
      "id": "cmp_001",
      "type": "compaction",
      "encrypted_content": "gAAAAAB...="
    }
  ]
}

响应中的 output 就是下一轮的规范上下文。下一次调用 /v1/responses 时,应先原样放入这组 Item,再追加新的 user Item。

不要自行拆解、改写或只摘取 compaction Item 中看得懂的部分。官方文档将压缩结果定义为后续请求的规范输入。自动 compaction 与 store=false 也可以组合,用于由客户端维护状态的场景。

压缩仍然是有损处理。业务关键数据最好保存在应用自己的结构化状态中,不要只依赖模型生成的上下文摘要。详细行为见 OpenAI Compaction 指南

函数工具和内置工具

Responses 的函数定义比 Chat Completions 更扁平:函数名、描述和参数直接放在 function tool 上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "input": "台北今天天气如何?",
    "tools": [{
      "type": "function",
      "name": "get_weather",
      "description": "查询指定城市的天气",
      "parameters": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"],
        "additionalProperties": false
      },
      "strict": true
    }]
  }'

模型决定调用函数时,会在 output 中返回独立的 function_call Item:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "id": "resp_123",
  "status": "completed",
  "output": [{
    "type": "function_call",
    "id": "fc_123",
    "call_id": "call_123",
    "name": "get_weather",
    "arguments": "{\"city\":\"台北\"}",
    "status": "completed"
  }]
}

应用解析参数并执行函数后,可通过 previous_response_id 返回结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "previous_response_id": "resp_123",
    "input": [{
      "type": "function_call_output",
      "call_id": "call_123",
      "output": "{\"temperature\":28,\"condition\":\"多云\"}"
    }]
  }'

模型拿到 function_call_output 后,会产生新的 message Item:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "id": "resp_124",
  "status": "completed",
  "output": [{
    "type": "message",
    "role": "assistant",
    "status": "completed",
    "content": [{
      "type": "output_text",
      "text": "台北目前 28°C,多云。",
      "annotations": []
    }]
  }]
}

function_callfunction_call_output 通过 call_id 关联。应用要支持一次返回多个调用,并限制工具可访问的资源范围。

Responses 还支持 web search、file search、code interpreter、computer use、remote MCP 等 OpenAI 托管工具。托管工具由服务端执行,具体可用性、参数和费用取决于模型与工具。客户端函数则始终由应用自己执行。

结构化输出

Responses 把结构化输出配置放在 text.format 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "input": "把下面的问题整理成工单:数据库连接经常超时。",
    "text": {
      "format": {
        "type": "json_schema",
        "name": "ticket",
        "strict": true,
        "schema": {
          "type": "object",
          "properties": {
            "title": {"type": "string"},
            "priority": {
              "type": "string",
              "enum": ["low", "medium", "high"]
            }
          },
          "required": ["title", "priority"],
          "additionalProperties": false
        }
      }
    }
  }'

响应中的 JSON 仍然作为 output_text 的文本返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "status": "completed",
  "output": [{
    "type": "message",
    "role": "assistant",
    "status": "completed",
    "content": [{
      "type": "output_text",
      "text": "{\"title\":\"数据库连接经常超时\",\"priority\":\"high\"}",
      "annotations": []
    }]
  }]
}

应用找到 output_text 后,再对其中的 text 做 JSON 反序列化。

这和 Chat Completions 的 response_format 不是同一个字段路径。迁移时如果只替换 endpoint 而保留原请求体,会得到参数错误。

流式输出

设置 stream=true 后,Responses 返回带语义类型的事件,而不是只有一个通用 delta 结构。

1
2
3
4
5
6
7
8
curl -N https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "input": "写一段关于台北雨夜的短文。",
    "stream": true
  }'

Responses 的 SSE 同时包含 event 行和 JSON data 行。下面省略了 Response 对象中与流式过程无关的字段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
event: response.created
data: {"type":"response.created","response":{"id":"resp_123","status":"in_progress","output":[]}}

event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_123","output_index":0,"content_index":0,"delta":"台北"}

event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_123","output_index":0,"content_index":0,"delta":"的雨夜。"}

event: response.output_text.done
data: {"type":"response.output_text.done","item_id":"msg_123","output_index":0,"content_index":0,"text":"台北的雨夜。"}

event: response.completed
data: {"type":"response.completed","response":{"id":"resp_123","status":"completed"}}

文本界面通常只拼接 response.output_text.deltadeltaresponse.output_text.done 给出完整文本,response.completed 则表示整个 Response 已完成;工具和其他 Item 可能在文本完成前后继续产生事件。

常见事件包括:

  • response.created:Response 已创建。
  • response.output_item.added:新增一个 output Item。
  • response.content_part.added:新增一个内容部分。
  • response.output_text.delta:文本增量。
  • response.function_call_arguments.delta:函数参数字符串增量。
  • response.output_item.done:一个 Item 完成。
  • response.completed:整个 Response 完成。
  • error:流内错误。

事件集合会随启用的工具和输出类型变化。实现时应按事件 type 分派,并忽略暂时不认识但不影响当前功能的事件,而不是假定每个事件都有 delta

函数参数和结构化输出在流式阶段同样可能是不完整 JSON。应在对应 Item 完成后解析;如果只是把文本实时展示给用户,可以只消费 response.output_text.delta,同时在结束时保存完整 Response。

多模态、状态和完成判断

Responses 的 input 可以是字符串,也可以是包含 input_textinput_image、文件等内容的 Item 数组。输出也可能包含文本之外的 Item。是否支持某种模态仍由模型和工具决定。

下面的请求同时发送文字和图片 URL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "OPENAI_MODEL_ID",
    "input": [{
      "role": "user",
      "content": [
        {"type": "input_text", "text": "描述这张图片。"},
        {
          "type": "input_image",
          "image_url": "https://example.com/image.jpg"
        }
      ]
    }]
  }'

文字结果仍然出现在 message Item 的 output_text 中。传文件时使用 input_file,并按来源提供 file_idfile_url

生产代码至少要检查:

  • response.status 是否为 completed
  • 是否存在 errorincomplete_details
  • output 中是否有函数或托管工具调用。
  • usage 是否符合预算。
  • store 设置是否符合数据保留要求。

Responses 的完整状态模型和字段见 Responses API Reference,多轮状态见 Conversation state 指南

Anthropic Messages API

Anthropic Messages API 的入口是 POST /v1/messages。一次请求生成一条 assistant message;message 的 content 不是固定字符串,而是由 texttool_use 等 content block 组成的数组。

基础调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "CLAUDE_MODEL_ID",
    "max_tokens": 1024,
    "system": "回答要准确、简洁。",
    "messages": [
      {"role": "user", "content": "解释什么是幂等性。"}
    ]
  }'

Claude API 使用 x-api-key,并要求 anthropic-version header。max_tokens 是请求中的显式输出上限,不代表模型一定生成到这个长度。

典型响应如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "id": "msg_...",
  "type": "message",
  "role": "assistant",
  "content": [
    {"type": "text", "text": "幂等性是指……"}
  ],
  "model": "CLAUDE_MODEL_ID",
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 25,
    "output_tokens": 20
  }
}

即使只请求文本,也应遍历 content 并根据 block 的 type 处理,不要长期依赖 content[0].text。启用工具或其他能力后,第一块不一定是文本。

Anthropic 的 system 是顶层字段,不是 messages 中的一条 system role message。Messages 中主要使用 userassistant 两种角色。

多轮消息

Messages API 默认是无状态的。每次请求都要重新发送希望 Claude 看到的历史消息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "CLAUDE_MODEL_ID",
    "max_tokens": 1024,
    "system": "回答要准确、简洁。",
    "messages": [
      {"role": "user", "content": "法国首都是哪里?"},
      {
        "role": "assistant",
        "content": [{"type": "text", "text": "法国首都是巴黎。"}]
      },
      {"role": "user", "content": "它位于哪条河沿岸?"}
    ]
  }'

第二次请求中的 assistant content 来自第一次响应。示例把 content block 完整写出,是为了强调下一轮应保存并带回整个 content 数组。

API 允许把连续同角色消息组合起来,但应用最好保持清晰的 user/assistant 轮次,并完整保存 content blocks。只把 Claude 的文本拼成一个字符串,会丢失 tool_use、compaction 等后续需要的结构。

上下文窗口和 token 统计

顶层 system 指令、所有历史消息、工具定义、工具结果、图片和文档都会占用上下文窗口。max_tokens 又会占用输出预算,因此应用不能等到请求已经超限才处理历史。

Anthropic 提供 token counting API,可以在真正生成之前估算同一组 system、messages 和 tools 的输入 token。适合在以下时机调用:

  • 长文档首次进入上下文时。
  • 工具定义很多或工具结果很大时。
  • 接近应用自己设置的压缩阈值时。
  • 需要在调用前估算成本时。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
curl https://api.anthropic.com/v1/messages/count_tokens \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "CLAUDE_MODEL_ID",
    "system": "回答要准确、简洁。",
    "messages": [
      {"role": "user", "content": "分析这段较长的对话。"}
    ]
  }'

token counting 响应只返回估算后的输入 token 数,不会生成 assistant message:

1
2
3
{
  "input_tokens": 18
}

响应中的 usage.input_tokensusage.output_tokens 用于记录实际计费量。缓存读写等能力还可能增加更细的 usage 字段,不能假定 usage 永远只有两个数字。

服务端 compaction

Anthropic 当前提供服务端 compaction。该能力使用 beta header,把较早的对话压缩成 compaction content block,并在下一轮用这个 block 替代它之前的历史内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: compact-2026-01-12" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "max_tokens": 4096,
    "messages": [
      {"role": "user", "content": "继续完成这段较长会话中的任务。"}
    ],
    "context_management": {
      "edits": [{
        "type": "compact_20260112",
        "trigger": {"type": "input_tokens", "value": 100000}
      }]
    }
  }'

触发压缩后,assistant content 的开头会出现 compaction block,后面仍可继续包含普通文本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "content": [
    {
      "type": "compaction",
      "content": "<summary>用户正在实现一个天气服务,已经确定接口和错误处理策略……</summary>"
    },
    {
      "type": "text",
      "text": "接下来可以实现缓存层。"
    }
  ],
  "stop_reason": "end_turn"
}

下一轮请求必须把包括该 block 在内的完整 assistant content 放回 messages,然后再追加新的 user message。如果启用了 pause_after_compaction,响应只包含 compaction block,且 stop_reasoncompaction

按当前官方文档,该版本默认在 150,000 个输入 token 时触发,可配置的最低阈值是 50,000。默认值、beta 名称和支持模型都可能变化,使用前应再次确认 Anthropic Compaction 文档

compaction 的关键行为是:

  • 触发后,响应 content 中会出现 compaction block。
  • 应用要像保存 text 和 tool block 一样,保存完整 assistant content。
  • 下一轮再次提交该 block 时,API 会丢弃它之前已经被压缩的 content blocks。
  • 可以提供自定义压缩指令,但它会替换默认摘要提示,不是简单追加一句要求。
  • 可以设置 pause_after_compaction,让请求在完成压缩后暂停,应用检查或持久化状态后再继续。

服务端 compaction 之外,应用仍可自行摘要、滑动窗口或提取结构化状态。和 Chat Completions 一样,精确业务数据不应只存在于自然语言摘要中。

工具调用

Anthropic 使用 tools 声明工具,参数 schema 字段名为 input_schema。Claude 请求执行工具时,会返回 tool_use block;应用执行后,用 tool_result block 作为下一条 user message 的内容返回。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "CLAUDE_MODEL_ID",
    "max_tokens": 1024,
    "messages": [
      {"role": "user", "content": "台北今天天气如何?"}
    ],
    "tools": [{
      "name": "get_weather",
      "description": "查询指定城市的天气",
      "input_schema": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"],
        "additionalProperties": false
      }
    }]
  }'

模型决定调用工具时,会返回 tool_use block,并把 stop_reason 设为 tool_use

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "role": "assistant",
  "content": [{
    "type": "tool_use",
    "id": "toolu_123",
    "name": "get_weather",
    "input": {"city": "台北"}
  }],
  "stop_reason": "tool_use"
}

应用执行函数后,需要把原始 assistant content 和 tool_result 一起带回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "CLAUDE_MODEL_ID",
    "max_tokens": 1024,
    "messages": [
      {"role": "user", "content": "台北今天天气如何?"},
      {
        "role": "assistant",
        "content": [{
          "type": "tool_use",
          "id": "toolu_123",
          "name": "get_weather",
          "input": {"city": "台北"}
        }]
      },
      {
        "role": "user",
        "content": [{
          "type": "tool_result",
          "tool_use_id": "toolu_123",
          "content": "{\"temperature\":28,\"condition\":\"多云\"}"
        }]
      }
    ],
    "tools": [{
      "name": "get_weather",
      "description": "查询指定城市的天气",
      "input_schema": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"]
      }
    }]
  }'

下一条响应会重新回到 text block:

1
2
3
4
5
6
7
8
{
  "role": "assistant",
  "content": [{
    "type": "text",
    "text": "台北目前 28°C,多云。"
  }],
  "stop_reason": "end_turn"
}

tool_use.idtool_result.tool_use_id 必须匹配。一次消息可能要求多个工具调用,应该在同一条 user message 中返回对应的多个结果。工具失败时可以在 result 中标记错误,而不是伪造一个正常结果。

结构化输出

Anthropic 当前使用 output_config.format 指定 JSON Schema:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "CLAUDE_MODEL_ID",
    "max_tokens": 1024,
    "messages": [{
      "role": "user",
      "content": "把下面的问题整理成工单:数据库连接经常超时。"
    }],
    "output_config": {
      "format": {
        "type": "json_schema",
        "schema": {
          "type": "object",
          "properties": {
            "title": {"type": "string"},
            "priority": {
              "type": "string",
              "enum": ["low", "medium", "high"]
            }
          },
          "required": ["title", "priority"],
          "additionalProperties": false
        }
      }
    }
  }'

JSON 结果位于普通 text block 的 text 字符串中:

1
2
3
4
5
6
7
8
{
  "role": "assistant",
  "content": [{
    "type": "text",
    "text": "{\"title\":\"数据库连接经常超时\",\"priority\":\"high\"}"
  }],
  "stop_reason": "end_turn"
}

符合 schema 的 JSON 会出现在 text block 中,应用仍要取出字符串并反序列化。工具输入如果也需要严格符合 schema,可以在工具定义中启用 strict: true。字段形态可能随 API 版本变化,升级前应对照 Structured outputs 文档

流式输出

设置 stream=true 后,Anthropic 通过 SSE 返回 message 和 content block 的生命周期事件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
curl -N https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "CLAUDE_MODEL_ID",
    "max_tokens": 1024,
    "messages": [
      {"role": "user", "content": "写一段关于台北雨夜的短文。"}
    ],
    "stream": true
  }'

一次文本响应的 SSE 形态如下。为了突出事件顺序,示例省略了 model 等不影响拼接的字段。Anthropic 会在 event 行中写事件名,并在 data.type 中重复同一个类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
event: message_start
data: {"type":"message_start","message":{"id":"msg_123","type":"message","role":"assistant","content":[],"stop_reason":null,"usage":{"input_tokens":25,"output_tokens":1}}}

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_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":15}}

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

index 把同一个 content block 的 text_delta.text 依次拼接,就能得到最终文本。最后的 message_delta 提供停止原因和累计 usage,message_stop 表示整条 Message 完成。

如果直接处理原始事件,常见顺序是:

  1. message_start
  2. 一个或多个 content_block_start
  3. 每个 block 的若干 content_block_delta
  4. content_block_stop
  5. message_delta
  6. message_stop

文本增量的 delta 类型是 text_delta。工具参数使用 input_json_delta,其中的 partial_json 只是 JSON 字符串片段,应该按 content block 的 index 累积,等 content_block_stop 后再解析。

流中还可能出现 pingerror 和新增的未知事件类型。客户端应该容忍 ping 和暂时不关心的事件,同时对流内 error 做明确处理。网络连接关闭不等于模型正常完成;需要看到 message_stop,并保存由各个事件汇总出的最终 Message。

多模态和停止原因

Messages 的 user content 可以由多个 block 组成,例如文本、图片和 PDF 文档。内容可以直接用 base64、URL,或使用 Files API 支持的文件引用;具体方式和限制取决于内容类型及模型。

下面的请求同时发送文字和图片 URL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "CLAUDE_MODEL_ID",
    "max_tokens": 1024,
    "messages": [{
      "role": "user",
      "content": [
        {
          "type": "image",
          "source": {
            "type": "url",
            "url": "https://example.com/image.jpg"
          }
        },
        {"type": "text", "text": "描述这张图片。"}
      ]
    }]
  }'

响应中的图片描述仍然位于 text block。使用 base64 时,把 source.type 改为 base64,并提供 media_typedata

常见 stop_reason 包括:

  • end_turn:模型正常结束当前轮次。
  • max_tokens:达到请求的输出上限,结果可能不完整。
  • stop_sequence:命中了自定义停止序列。
  • tool_use:模型要求应用执行工具。
  • pause_turn:服务端工具或长任务暂时暂停,应按文档继续该轮。
  • refusal:模型拒绝请求。
  • model_context_window_exceeded:生成到模型上下文允许的边界。

应用要根据停止原因进入不同分支,而不是只判断 content 是否非空。完整列表和处理建议见 Anthropic Stop reasons 文档

三套 API 都需要的应用层控制

接口不同,但可靠的生产实现通常都需要同一个控制循环:

  flowchart TD
    A[接收用户输入] --> B[加载指令和会话状态]
    B --> C[调用模型 API]
    C --> D{模型是否请求工具}
    D -- 是 --> E[校验权限和参数]
    E --> F[幂等执行工具]
    F --> G[追加工具结果]
    G --> C
    D -- 否 --> H{上下文是否需要压缩}
    H -- 是 --> I[压缩历史并保留关键状态]
    H -- 否 --> J[保存完整响应]
    I --> J
    J --> K[向用户返回结果]

生产实现还需要处理以下共同问题:

  • 保存供应商返回的完整结构,不只保存最终文本。
  • 把模型输出和工具参数都当作不可信输入。
  • 为工具设置权限、超时、并发限制和幂等键。
  • 对 429、连接中断和 5xx 使用带抖动的指数退避;不要无条件重试业务错误。
  • 流式展示和最终持久化分开处理,结束后保存完整响应,而不是只保存已经打印的 delta。
  • 记录模型、请求 ID、停止原因、token 用量和工具执行结果。
  • 把关键业务状态放在数据库中,模型上下文只作为工作记忆。

如果要同时支持多个供应商,不要强行把所有原始对象压成一个只有 rolecontent 的最小结构。更稳妥的做法是保留原始 provider payload,再在应用控制层定义少量统一动作,例如“追加用户输入”“执行工具”“读取最终文本”和“记录用量”。

参考资料

总结

维度 OpenAI Chat Completions OpenAI Responses Anthropic Messages
Endpoint POST /v1/chat/completions POST /v1/responses POST /v1/messages
核心输出 choices[].message output[] Item content[] block
多轮状态 应用重发 messages 手动 Item、previous_response_id 或 Conversations 应用重发 messages
上下文压缩 应用自行摘要、裁剪或检索 自动 compaction 或 /responses/compact beta 服务端 compaction,也可应用自行处理
工具请求 tool_calls function_call Item tool_use block
工具结果 tool message function_call_output Item tool_result block
结构化输出 response_format text.format output_config.format
流式文本 choices[].delta.content response.output_text.delta content_block_delta / text_delta

选择时可以按现有系统的状态模型来判断:

  • 已经稳定使用 messageschoicestool_calls 的 OpenAI 应用,可以继续使用 Chat Completions。
  • 新的 OpenAI 应用,尤其需要服务端对话状态、自动压缩、推理 Item 或托管工具时,优先使用 Responses。
  • 直接调用 Claude 时使用 Anthropic Messages,并围绕 content block、无状态多轮和 stop_reason 设计控制循环。

多供应商适配的边界应放在状态协议,而不是几个文本字段。先完整保存各自的消息、Item 或 content block,再在应用层统一工具执行、重试、预算和持久化;这能避免过早的统一抽象丢失供应商特有的状态。

#OpenAI #Anthropic #LLM #API #AI Agent