更新时间:2026-07-08。
本文聊聊 Agent 客户端”发一条消息”时背后的 HTTP 请求链路:请求打到哪、body 里都带了什么、header 又是干什么用的,顺便说清楚 OpenAI-compatible 和 Claude / Anthropic 原生链路的区别。公开接口部分以 OpenAI、Anthropic / Claude、vLLM、LiteLLM 官方文档为准;Ducc、Clawdbot 是公司内部网关的例子,具体数值以实际抓包和网关日志为准。

从一条消息说起

我们平时在 Agent 客户端里敲一句话回车,看起来是”发了一句话给模型”,但客户端不会真的把这句话原样丢过去。它会先把用户输入、系统/开发者指令、历史上下文、工具定义、运行约束、要不要流式返回这些东西,一起拼成一个结构化的 JSON,然后按当前 provider 的协议发一个 HTTP 请求出去。

这张时序图里最值得注意的是第 4 步:客户端这时已经把首跳 URL 拼好了,headersbody 一起 POST 出去。

再往后走,无论是本地代理、模型网关还是最终的上游服务,做的都是鉴权、路由、模型名映射、协议转换、流式事件转换这些”转发和翻译”的工作——真正决定”这条链路是什么协议”的信息,已经在第一跳的 URL 和 body 里定下来了。

所以排查一条链路的时候,思路可以简化成一句话:URL 决定请求先打到哪里、调用哪个 endpoint;headers 负责鉴权和来源识别;body 里的 model、input/messages、tools、stream 决定协议形态和能力。不要只看模型名,也不要只看客户端叫什么——模型名可以被网关随便映射,接口形态却不会跟着变。

URL 里藏着什么

URL 决定的是”请求先到哪、走哪套 API”。举个例子:

http://127.0.0.1:25817/codex/by-provider/openai/v1/responses

这条 URL 里 /codex/by-provider/openai 是本地代理自己加的 path 路由前缀,跟 provider 的 API 本身没关系;真正的 provider endpoint 是后面的 /v1/responses。如果没有本地代理或网关插在中间,URL 里通常就不会有这段多余的路径。

也有些接口天生长得不一样,比如 Gemini 这类”资源动作型”API,直接把模型名塞进了 URL path:

POST /v1beta/models/{model}:generateContent

这种就不是 OpenAI Chat / Responses 那套形态了。如果哪个兼容网关要接 Gemini,通常得把 OpenAI 风格的 body 转换成 Gemini 这种资源动作请求。

body 里藏着什么

body 承载的是协议真正的”内容”:model 决定请求哪个模型(OpenAI / Claude 风格基本都把 model 放 body 里);inputmessages 直接暴露了协议形态——这一个字段的区别就能看出是 Responses 还是 Chat/Messages;tools / tool_choice 声明工具,但声明了不代表模型一定会调用;stream 决定是不是要走 SSE,但光看这个字段还不够,还要看返回的事件格式客户端能不能解析。

排查一条请求,按这个顺序走

真正抓包排查时,我一般是这么一层层剥的:先看首跳 URL 的 scheme、host、port、有没有多余的 path 前缀;再看 endpoint 到底是 /v1/responses/v1/chat/completions 还是 /v1/messages;然后看 body 的形状——input 对应 Responses,messages 配合 OpenAI 风格 path 对应 Chat,messages 配合 /v1/messages 才是 Claude Messages;确认完形态之后再去看 body 里的 model 字段,这才是客户端真正发出去的模型名;接着看 header,包括鉴权方式、anthropic-version、有没有自定义 header、User-Agent 是什么;如果中间有网关,还要翻一下网关日志,看它是不是改写了模型名、换了 key、转了协议、转了 stream;最后看响应,普通 JSON 能不能正常解析,SSE 的 event/chunk 是不是客户端预期的格式。整个过程走完,再去对照网关文档确认它到底支持哪个 endpoint——很多时候网关自己说”OpenAI-compatible”,但支持程度天差地别,不能只看这四个字就下结论。

三种常见的发消息接口

把 HTTP 请求拆开之后,顺着 endpoint 和 body 字段就能认出 provider 风格。OpenAI 系主要是两套:Responses APIChat Completions API;Anthropic / Claude 原生链路则集中在 Messages API 一套上。判断协议形态时,endpoint、body 核心字段和响应结构比模型名靠谱得多。

OpenAI Responses API

请求地址是 POST https://api.openai.com/v1/responses,最小请求大概是这样

POST /v1/responses HTTP/1.1
Host: api.openai.com
Authorization: Bearer <OPENAI_API_KEY>
Content-Type: application/json
{
  "model": "gpt-5.5",
  "input": "你好"
}

model 选模型,input 是用户输入(可以是字符串,也可以是结构化的 input item 列表),instructions 相当于系统说明,tools / tool_choice 管工具调用,previous_response_idconversation 用来做状态延续,stream 决定要不要流式返回,max_output_tokens 限制输出长度,reasoning 是推理模型相关配置,text.format 控制输出是纯文本还是结构化格式,metadata / store 用于追踪和保存输出。

Responses API 更像是一个”Agent 工作流”接口,而不只是简单的聊天补全——它把工具调用、推理、结构化输出、文件/图片输入和状态延续都塞进了同一套响应对象里。

OpenAI Chat Completions API

请求地址是 POST https://api.openai.com/v1/chat/completions

POST /v1/chat/completions HTTP/1.1
Host: api.openai.com
Authorization: Bearer <OPENAI_API_KEY>
Content-Type: application/json
{
  "model": "gpt-5.5",
  "messages": [
    { "role": "user", "content": "你好" }
  ]
}

messages 是对话历史列表,常见 role 有 systemdeveloperuserassistanttooltools / tool_choice 同样管工具调用;stream / stream_options 管流式返回及其附加选项(比如 usage);temperature / top_p 管采样,但部分推理模型或兼容网关可能不支持;max_completion_tokens 限制输出 token,不过老接口和很多兼容网关里仍然更常见 max_tokensresponse_format 控制输出是文本、JSON object 还是 JSON schema。

典型响应长这样,主对象是 chat.completion,内容在 choices[].message 里:

{
  "id": "chatcmpl_xxx",
  "object": "chat.completion",
  "choices": [{
    "index": 0,
    "message": { "role": "assistant", "content": "你好!" },
    "finish_reason": "stop"
  }],
  "usage": { "prompt_tokens": 12, "completion_tokens": 6, "total_tokens": 18 }
}

Responses 和 Chat Completions 怎么选

两者都是 OpenAI 家的接口,区别不在”能不能聊天”,而在设计定位和数据形态上。

输入字段上,Responses 用的是 input(字符串或结构化 item 列表),Chat Completions 用的是 messages(角色化的对话历史列表)。

状态延续是两者最大的实质差异:Chat Completions 完全无状态,每次请求都要传完整的 messages 历史,由客户端自己维护上下文;Responses 支持 previous_response_id(或 conversation 对象)复用上一轮 response,服务端帮你接续状态,不用每次全量回传历史。

定位也不同。Chat Completions 是经典的”聊天补全”接口,历史最久、兼容性最广,几乎所有 OpenAI-compatible 网关都优先支持它。Responses 更像”Agent 工作流”接口,把工具调用、推理(reasoning)、结构化输出(text.format)、文件/图片输入和状态延续都塞进同一套响应对象里,是 OpenAI 目前主推的、面向 Agent 场景的新一代接口。

输出上限参数名也不一样:Responses 用 max_output_tokens,Chat Completions 用 max_completion_tokens(老接口和很多兼容网关里更常见 max_tokens);返回结构上 Responses 主对象是 response、内容在 output[] / output_text,Chat Completions 主对象是 chat.completion、内容在 choices[].message

总结:Chat Completions 稳定、无状态、兼容性最广;Responses 面向 Agent 场景、带状态延续和更丰富能力。新项目尤其是做 Agent 的,官方现在更推荐 Responses;但要接第三方兼容网关,Chat Completions 目前支持度依然最稳。

Anthropic Messages API

Claude API base_url 是 https://api.anthropic.com

POST /v1/messages HTTP/1.1
Host: api.anthropic.com
x-api-key: <ANTHROPIC_API_KEY>
anthropic-version: 2023-06-01
Content-Type: application/json

Messages API 请求地址是 POST https://api.anthropic.com/v1/messages

{
  "model": "claude-opus-4-8",
  "max_tokens": 1024,
  "messages": [
    { "role": "user", "content": "你好" }
  ]
}

跟前两者比,Claude Messages 有几个明显不同的地方:

  • max_tokens 是必传参数,官方基础示例都会带;
  • 这个接口本身是无状态的,通常每次要传完整历史,没有类似 previous_response_id 的状态延续机制;
  • system 是顶层字段而不是塞进 messages 列表(新模型也支持中途插入 system message,但要遵守位置规则);
  • header 里必须带 anthropic-version,这是官方强制要求的,鉴权可以用 x-api-keyAuthorization
  • 如果要开某个 beta 能力,还需要加 anthropic-beta header。

返回对象的主类型是 message,内容在 content[] 数组里:

{
  "id": "msg_xxx",
  "type": "message",
  "role": "assistant",
  "content": [{ "type": "text", "text": "你好!" }],
  "model": "claude-opus-4-8",
  "stop_reason": "end_turn",
  "usage": { "input_tokens": 12, "output_tokens": 6 }
}

三种接口怎么快速区分

说了这么多细节,真正抓包时最快的判断方法就是看 endpoint + 输入字段这两样:

维度OpenAI ResponsesOpenAI Chat CompletionsAnthropic Messages
endpoint/v1/responses/v1/chat/completions/v1/messages
输入字段inputmessagesmessages
输出上限max_output_tokensmax_completion_tokens / max_tokensmax_tokens(必传)
鉴权头Authorization: BearerAuthorization: Bearerx-api-key + anthropic-version
返回主对象responsechat.completionmessage
状态延续支持(previous_response_id无状态无状态

一句话记法:看到 /v1/responsesinput,是 Responses;看到 /v1/chat/completionsmessages,是 OpenAI Chat;看到 /v1/messagesmessages + max_tokens + anthropic-version,是 Claude Messages。三者里 Responses 和 Chat Completions 都是 OpenAI 家的,光看 messages 这个字段区分不了 Chat 和 Claude,一定要结合 endpoint 一起看。

OpenAI-compatible 到底兼容了什么

很多网关会说自己”OpenAI-compatible”,但这其实不是一种模型能力,而是一种接口兼容承诺:服务端尽量把 URL、header、请求字段、响应结构做得跟 OpenAI API 一样或很像,这样用 OpenAI SDK 或者 OpenAI 风格的客户端,只需要改一下 base_urlapi_keymodel 三个东西,就能请求到根本不是 OpenAI 的上游模型。

这种网关的核心价值,是把客户端看到的”一种协议”,翻译成后面很多个上游实际需要的”很多种协议”。但要注意,兼容的只是协议外观,不代表所有 endpoint 和参数都真的可用:

举个具体例子,下面这段 Python 代码看起来是在调用 OpenAI: ```python from openai import OpenAI

client = OpenAI(
api_key=“anything-or-proxy-key”,
base_url=“http://localhost:4000/v1
)

resp = client.chat.completions.create(
model=“glm-4.7-internal”,
messages=[{“role”: “user”, “content”: “你好”}],
)

但它实际发出去的请求是:
```http
POST http://localhost:4000/v1/chat/completions
Content-Type: application/json
Authorization: Bearer <proxy-key>
{
  "model": "glm-4.7-internal",
  "messages": [{ "role": "user", "content": "你好" }]
}

客户端仍然是按 OpenAI Chat Completions 的格式在发请求,重点不是”用了 OpenAI 模型”,而是网关收到这个请求之后,会自己转成真实上游需要的协议——可能转给 GLM、Claude、Gemini、本地 vLLM,或者公司内部的模型服务。

这里有个关键点:兼容是按 endpoint 算的,不是网关说自己兼容就全都兼容。/v1/chat/completions 几乎是所有兼容网关都会优先支持的,最成熟;/v1/responses 相对新,Agent 客户端用得越来越多,但很多中转网关支持并不完整;/v1/models/v1/embeddings 也算常见;
/v1/images/generations/v1/audio/transcriptions 这类多模态接口就要看具体网关了,图片和语音模型经常是单独路由的。

vLLM 官方文档把自己的在线服务称为 OpenAI-Compatible Server,列出的能力包括 Chat、Responses、Embeddings、Audio Transcriptions;LiteLLM Proxy 文档同样列了 /chat/completions/embeddings/models 这些代理 endpoint。这基本就是”兼容”这个词的真实含义:客户端这边看到的协议长得像 OpenAI,但上游模型完全可以不是 OpenAI。

实际踩坑的地方通常集中在几类:网关只兼容 Chat 接口,/v1/responses 一调就 404 或者报字段错误,这是因为网关压根没实现新接口;某些参数名字不兼容,比如 max_completion_tokensreasoning、新版 tools 格式,上游或网关根本不认识;模型返回了工具调用但客户端解析失败,这是因为 OpenAI、Anthropic、Gemini 三家的工具调用结构本身就长得不一样,网关转换不完整就会出这种问题;非流式请求正常,一开流式就挂,通常是 SSE 的 chunk/event 类型转换没做全;base_url 该不该带 /v1,不同 SDK 和网关的约定不统一,容易拼出 /v1/v1/chat/completions 这种重复路径;还有模型名看起来很像 OpenAI 的模型,但实际上网关早就把它映射到内部模型了。所以判断一个网关是否真的”兼容”,不能只看它的营销话术,得实际去抓 endpoint、body、响应结构和流式事件。

常用接口:不只是发消息

前面讲的都是”发一条消息”这个动作对应的接口。但真实接入一个 provider 时,还会遇到模型列表、向量、图片、音频、文件、批处理等一堆配套接口。这里没有把 OpenAI 和 Anthropic 做成一张横向对照表,因为两家的接口族本身就不是一一对应的关系,硬凑对照表反而会误导人。

OpenAI 这边是按 endpoint 分别认能力的:

这张图里最容易搞混的是”图片”相关的两件事:图片输入(比如让模型看一张图再回答)走的是 Responses / Chat 的多模态输入能力,不需要单独的接口;图片生成才是真正独立的一套链路,对应 /v1/images/generations。像 gptimage2 这种请求属于后者,跟日常的”发消息”接口是两回事,别搞混了。

Anthropic / Claude 这边的接口相对集中:核心的模型调用就是 Messages API 这一套,批处理、token 计数、模型列表、文件能力都是围绕 Messages 展开的配套接口:

有个点容易被忽略:Files、Agents、Sessions 这些目前都是 beta 能力,官方文档也明确说过 Anthropic 不提供自己的 embedding 模型。所以如果在 Claude 链路里看到”需要向量”的场景,得先搞清楚这个向量化到底是走 Voyage、走 OpenAI embeddings、走公司内部向量服务,还是走了某个兼容网关转发出去的——反正肯定不是 Claude 自己算的。

还要提一句,OpenAI-compatible 网关千万别混进”OpenAI 原生接口”或者”Anthropic 原生接口”这两类里去理解。它可能对外暴露的是 /v1/chat/completions/v1/models/v1/embeddings 这些 OpenAI 风格的路径,但背后真实的上游完全可能是 Claude、GLM、Gemini、vLLM 或者某个内部模型服务。排查的时候脑子里始终要把三层名字分开看:客户端请求的 model 名,网关对外暴露的 alias,以及背后真实调用的上游 model/deployment——这三个经常不是一回事。

案例对照:Ducc 和 Clawdbot

拿两个真实观测到的例子来对照一下前面讲的框架。结论很直接:Ducc 走的是 CC / Claude 原生 Messages 形态,Clawdbot 走的是 OpenAI-compatible Chat Completions 形态。

Ducc 这边,协议是标准的 Anthropic Messages(POST /v1/messages),scheme 是 https://,请求体里的 model 字段是 Claude Opus 4.7[1m],输入字段是 messages 配合 max_tokens。它没有额外的自定义 header,值得一提的是设置 CLAUDE_CODE_ATTRIBUTION_HEADER=0 可以关闭归因头;User-Agent 是 Claude Code / baidu-cc 客户端的默认值。排查这条链路时重点看 /v1/messagesanthropic-version(或者内部网关的等价替代)以及 Claude 那套 stream events 格式。

Clawdbot 这边则完全是另一套:走的是 OpenAI Chat(POST /v1/chat/completions),内部 api 标识直接就是 api: openai-completions,scheme 是 http://(本机或内网首跳,不代表最终上游就是明文 HTTP,具体要看代理转发日志)。请求体里的 model 是 glm-4.7-internal,输入字段也是 messages。它带了一个很明显的自定义 header:comate_custom_header: {"username":"zhangzhen40","source":"openclaw"},用来做来源和用户归因;User-Agent 是 clawdbot / openclaw 客户端的标识。排查这条链路要看 /v1/chat/completions、标准 OpenAI chat body 格式、comate_custom_header 这个自定义头,以及网关做了什么模型映射。

两者对比下来正好印证了前面反复强调的判断原则:光看”发的是中文消息”或者”用的是 Claude 系模型”完全分不出协议形态,必须落到 endpoint、body 字段和 header 这几个具体的技术细节上才能判断清楚。

参考资料


相关笔记