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

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

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

<!--more-->

## OpenAI Chat Completions API

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

### 基础调用

```bash
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": "解释什么是幂等性。"}
    ]
  }'
```

一个普通响应大致如下：

```json
{
  "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：

```bash
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 本身不保存对话状态。下一轮请求必须再次发送需要保留的历史消息。

```bash
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. **检索历史记录**：完整记录放在外部存储中，只检索当前问题需要的片段。

一个常见的消息布局是：

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

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

```bash
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` 消息发回。

```bash
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
      }
    }]
  }'
```

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

```json
{
  "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 和工具结果：

```bash
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 文本：

```json
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": "台北目前 28°C，多云。"
    },
    "finish_reason": "stop"
  }]
}
```

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

### 结构化输出

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

```bash
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` 字符串中：

```json
{
  "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` 中。

```bash
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` 会关闭输出缓冲。下面省略了 `created`、`model` 等重复字段；同一次请求中的 chunk 使用相同的 `id`：

```text
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](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create) 为准。

## OpenAI Responses API

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

### 基础调用

```bash
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：

```json
{
  "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` 中找到 `type` 为 `message` 的 Item，再读取其中的 `output_text`。

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

### 多轮消息：三种管理方式

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

#### 方式一：应用保存完整 Item

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

```bash
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`

```bash
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：

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

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

```json
{
  "id": "conv_123",
  "object": "conversation",
  "metadata": {"topic": "地理问答"}
}
```

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

```bash
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`：

```bash
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：

```json
{
  "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 列表：

```bash
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：

```json
{
  "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 指南](https://developers.openai.com/api/docs/guides/compaction)。

### 函数工具和内置工具

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

```bash
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：

```json
{
  "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` 返回结果：

```bash
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：

```json
{
  "id": "resp_124",
  "status": "completed",
  "output": [{
    "type": "message",
    "role": "assistant",
    "status": "completed",
    "content": [{
      "type": "output_text",
      "text": "台北目前 28°C，多云。",
      "annotations": []
    }]
  }]
}
```

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

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

### 结构化输出

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

```bash
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` 的文本返回：

```json
{
  "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 结构。

```bash
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 对象中与流式过程无关的字段：

```text
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.delta` 的 `delta`。`response.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_text`、`input_image`、文件等内容的 Item 数组。输出也可能包含文本之外的 Item。是否支持某种模态仍由模型和工具决定。

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

```bash
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_id` 或 `file_url`。

生产代码至少要检查：

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

Responses 的完整状态模型和字段见 [Responses API Reference](https://developers.openai.com/api/reference/resources/responses/methods/create)，多轮状态见 [Conversation state 指南](https://developers.openai.com/api/docs/guides/conversation-state)。

## Anthropic Messages API

Anthropic Messages API 的入口是 `POST /v1/messages`。一次请求生成一条 assistant message；message 的 `content` 不是固定字符串，而是由 `text`、`tool_use` 等 content block 组成的数组。

### 基础调用

```bash
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` 是请求中的显式输出上限，不代表模型一定生成到这个长度。

典型响应如下：

```json
{
  "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 中主要使用 `user` 和 `assistant` 两种角色。

### 多轮消息

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

```bash
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。适合在以下时机调用：

- 长文档首次进入上下文时。
- 工具定义很多或工具结果很大时。
- 接近应用自己设置的压缩阈值时。
- 需要在调用前估算成本时。

```bash
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：

```json
{
  "input_tokens": 18
}
```

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

### 服务端 compaction

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

```bash
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，后面仍可继续包含普通文本：

```json
{
  "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_reason` 为 `compaction`。

按当前官方文档，该版本默认在 150,000 个输入 token 时触发，可配置的最低阈值是 50,000。默认值、beta 名称和支持模型都可能变化，使用前应再次确认 [Anthropic Compaction 文档](https://platform.claude.com/docs/en/build-with-claude/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 的内容返回。

```bash
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`：

```json
{
  "role": "assistant",
  "content": [{
    "type": "tool_use",
    "id": "toolu_123",
    "name": "get_weather",
    "input": {"city": "台北"}
  }],
  "stop_reason": "tool_use"
}
```

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

```bash
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：

```json
{
  "role": "assistant",
  "content": [{
    "type": "text",
    "text": "台北目前 28°C，多云。"
  }],
  "stop_reason": "end_turn"
}
```

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

### 结构化输出

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

```bash
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` 字符串中：

```json
{
  "role": "assistant",
  "content": [{
    "type": "text",
    "text": "{\"title\":\"数据库连接经常超时\",\"priority\":\"high\"}"
  }],
  "stop_reason": "end_turn"
}
```

符合 schema 的 JSON 会出现在 text block 中，应用仍要取出字符串并反序列化。工具输入如果也需要严格符合 schema，可以在工具定义中启用 `strict: true`。字段形态可能随 API 版本变化，升级前应对照 [Structured outputs 文档](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)。

### 流式输出

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

```bash
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` 中重复同一个类型：

```text
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` 后再解析。

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

### 多模态和停止原因

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

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

```bash
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_type` 和 `data`。

常见 `stop_reason` 包括：

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

应用要根据停止原因进入不同分支，而不是只判断 `content` 是否非空。完整列表和处理建议见 [Anthropic Stop reasons 文档](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons)。

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

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

```mermaid
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 用量和工具执行结果。
- 把关键业务状态放在数据库中，模型上下文只作为工作记忆。

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

## 参考资料

- [OpenAI Chat Completions API Reference](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create)
- [OpenAI Responses API Reference](https://developers.openai.com/api/reference/resources/responses/methods/create)
- [OpenAI Conversation state](https://developers.openai.com/api/docs/guides/conversation-state)
- [OpenAI Compaction](https://developers.openai.com/api/docs/guides/compaction)
- [OpenAI Function calling](https://developers.openai.com/api/docs/guides/function-calling)
- [OpenAI Streaming API responses](https://developers.openai.com/api/docs/guides/streaming-responses)
- [OpenAI Structured outputs](https://developers.openai.com/api/docs/guides/structured-outputs)
- [Anthropic Working with messages](https://platform.claude.com/docs/en/build-with-claude/working-with-messages)
- [Anthropic Context windows](https://platform.claude.com/docs/en/build-with-claude/context-windows)
- [Anthropic Compaction](https://platform.claude.com/docs/en/build-with-claude/compaction)
- [Anthropic Tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)
- [Anthropic Streaming Messages](https://platform.claude.com/docs/en/build-with-claude/streaming)
- [Anthropic Structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)

## 总结

| 维度 | 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` |

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

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

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

