大语言模型接口协议

llminferenceopenai apianthropic api

目录

大语言模型的主流接口协议,主要有 Anthropic Messages API、OpenAI Chat Completions API 和 OpenAI Responses API,下面我们对比一下这三种 API,并探讨一下在考虑到 KV cache 复用的情况下如何更好的使用协议。

1 Anthropic Messages API v.s. OpenAI Chat Completions API

方面OpenAI Chat CompletionsAnthropic Messages
鉴权Authorization: Bearerx-api-key,外加必填的 anthropic-version 头
系统提示词作为 role: "system"(或 developer)消息放进 messages独立的顶层 system 字段,messages 里只有 user/assistant
max_tokens可选必填
消息内容字符串,或 parts 数组(text、image_url 等)字符串,或 content blocks 数组(text、image、document、tool_use、tool_result、thinking 等)
响应结构choices[].message,可用 n 一次生成多个候选单个 content 数组,没有多候选
提示词缓存基本是自动的通过 cache_control 显式标记缓存断点
流式输出每个 chunk 都是 data: {…},里面的 choices[0].delta 带增量文本或增量的工具参数,最后以 data: [DONE] 结束带类型的事件流:message_start → content_block_start → 若干 content_block_delta → content_block_stop →(下一个块)→ message_delta(带 stop_reason 和用量)→ message_stop
工具定义包在 {“type”: “function”, “function”: {name, description, parameters}} 里更扁平,{name, description, input_schema} 定义一个工具
工具调用返回结果返回的 message 里带 tool_calls,其中 arguments 是一个 JSON 字符串,需要自己再 parse 一次返回的 content 里出现 tool_use 块,input 直接是 JSON 对象。
工具调用结果回传工具结果用一条 role: “tool” 的消息回传,靠 tool_call_id 对应工具结果放在一条 user 消息里,作为 tool_result 块,靠 tool_use_id 对应
字段不同结束原因:finish_reason:stop / length / tool_calls,用量字段:prompt_tokens / completion_tokens结束原因:stop_reason:end_turn / max_tokens / tool_use / stop_sequence,用量字段:input_tokens / output_tokens

其中,两者在系统提示词、工具调用、流式输出上的不同较大。

对于 Anthropic 协议,顶层 system 字段可以是 text block 数组,存放多段系统提示词。但是如果在对话中途出现的 system 消息,OpenAI 允许在 messages 任意位置插入 role: “system” 的消息,对于 Anthropic 则需要特殊处理,常见处理有两种:

  • 提到顶层合并。 这是 LiteLLM 这类转换网关的默认做法:把所有 system 消息抽出来拼进顶层 system。好处是简单,坏处有两个:一是丢失了位置语义,原本“第 20 轮之后才生效”的指令变成了从头就有;二是每出现一条新的中途 system,顶层 system 就变了,整个前缀缓存失效。
  • 原地转成 user 消息。 把中途的系统指令包一层标签,塞进当前位置的 user 消息里,比如 …。Claude Code 就大量使用这种方式来注入动态提醒。它保留了位置语义,也不会动前缀。

对于两个协议在工具调用、流式输出上的不同,可以使用下面的请求是实验:

方面OpenAI Chat CompletionsAnthropic Messages
工具调用(模型发起调用)
curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-4.1-mini",
    "messages": [{"role": "user", "content": "北京现在天气怎么样?"}],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "查询某个城市的当前天气",
        "parameters": {
          "type": "object",
          "properties": {"city": {"type": "string"}},
          "required": ["city"]
        }
      }
    }]
  }'
curl https://api.anthropic.com/v1/messages \
  -H "Content-Type: application/json" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "claude-sonnet-5-5",
    "max_tokens": 512,
    "messages": [{"role": "user", "content": "北京现在天气怎么样?"}],
    "tools": [{
      "name": "get_weather",
      "description": "查询某个城市的当前天气",
      "input_schema": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"]
      }
    }]
  }'
工具调用(回传工具结果)
curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-4.1-mini",
    "messages": [
      {"role": "user", "content": "北京现在天气怎么样?"},
      {"role": "assistant", "content": null, "tool_calls": [{
        "id": "call_demo123", "type": "function",
        "function": {"name": "get_weather", "arguments": "{\"city\": \"北京\"}"}
      }]},
      {"role": "tool", "tool_call_id": "call_demo123",
       "content": "{\"temp_c\": 22, \"condition\": \"晴\"}"}
    ],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "查询某个城市的当前天气",
        "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
      }
    }]
  }'
curl https://api.anthropic.com/v1/messages \
  -H "Content-Type: application/json" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "claude-sonnet-5-5",
    "max_tokens": 512,
    "messages": [
      {"role": "user", "content": "北京现在天气怎么样?"},
      {"role": "assistant", "content": [
        {"type": "tool_use", "id": "toolu_demo123", "name": "get_weather", "input": {"city": "北京"}}
      ]},
      {"role": "user", "content": [
        {"type": "tool_result", "tool_use_id": "toolu_demo123",
         "content": "{\"temp_c\": 22, \"condition\": \"晴\"}"}
      ]}
    ],
    "tools": [{
      "name": "get_weather",
      "description": "查询某个城市的当前天气",
      "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
    }]
  }'
流式输出
curl -N https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-4.1-mini",
    "stream": true,
    "messages": [{"role": "user", "content": "数到5,每个数字一行。"}]
  }'
curl -N https://api.anthropic.com/v1/messages \
  -H "Content-Type: application/json" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "claude-sonnet-5-5",
    "max_tokens": 256,
    "stream": true,
    "messages": [{"role": "user", "content": "数到5,每个数字一行。"}]
  }'

2 OpenAI Chat Completions API vs OpenAI Responses API

OpenAI 官方把 Responses (/v1/responses) 描述为 Chat Completions (/v1/chat/completions) 的演进版本,带来了更简洁的接口和面向 agent 的原语,并表示 Chat Completions 仍会继续支持,但所有新项目都推荐使用 Responses。

两者的区别有:

方面Chat CompletionsResponses
端点POST /v1/chat/completionsPOST /v1/responses
输入字段messages(消息数组)input(字符串,或 item 数组)
系统提示词system / developer 角色消息顶层 instructions,或 developer 角色 item
输出长度上限max_tokens(推理模型须用 max_completion_tokens)max_output_tokens
输出结构choices[].messageoutput[],由多种类型的 item 平铺组成
多个候选支持 n不支持
取文本choices[0].message.contentSDK 提供 output_text;原始 JSON 需在 output 中找 message item
对话状态无状态,每次传完整历史可无状态,也可用 previous_response_id 或 Conversations API
工具定义{"type":"function","function":{name, parameters}}{"type":"function","name":...,"parameters":...}
strict 默认值(函数调用是否采用严格模式)关闭开启
模型发起调用message.tool_calls[],finish_reason: "tool_calls"function_call item(带 call_id)
回传工具结果role: "tool" 消息 + tool_call_idfunction_call_output item + call_id
推理强度reasoning_effortreasoning.effort
推理内容无标准字段reasoning item,可选摘要和加密内容,能跨轮回传
结构化输出response_formattext.format
流式data: chunk + delta,以 [DONE] 结束类型化事件,如 response.output_text.delta
内置工具无网页搜索、文件搜索、远程 MCP 等
用量字段prompt_tokens / completion_tokensinput_tokens / output_tokens

3 考虑 KV cache 下的协议使用

了解大模型的人都知道,KV cache 的复用能提升推理速度和吞吐。上述 API request 中的 JSON 最终都会被 chat template 渲染成一条线性的 token 序列,而 KV cache 只认这条序列的前缀看是否能够复用,考虑到这一点,我们看一下在处理 system、tools 以及复制历史对话时要注意什么。

3.1 chat template 里,system 和 tools 放在哪

主流做法几乎一致:tools 和 system 都放在序列最开头,通常 tools 会被直接并进 system 区块。以 Qwen 系列的模板为例,一次带工具的请求渲染出来大致是:

<|im_start|>system
你是一个编程助手。

# Tools
You may call one or more functions...
<tools>
{"type": "function", "function": {"name": "get_weather", ...}}
</tools>
...调用格式说明...<|im_end|>
<|im_start|>user
北京现在天气怎么样?<|im_end|>
<|im_start|>assistant
<tool_call>
{"name": "get_weather", "arguments": {"city": "北京"}}
</tool_call><|im_end|>
<|im_start|>user
<tool_response>
{"temp_c": 22}
</tool_response><|im_end|>
<|im_start|>assistant

可以看到:工具定义序列化成 JSON 文本,追加在 system 内容后面。Anthropic 闭源,看不到模板,但官方文档里写明了缓存前缀的顺序是 tools → system → messages。

3.2 从 KV cache 复用角度推出一些实用原则

前缀缓存的规则很简单:从第一个 token 开始逐个比对,遇到第一个不同的 token,从那里往后全部重算。在 chat template 中,tools 和 system 放在最前面,后面是历史信息。

由此我们可以推出一些实践原则:

  1. 不要在 system 里放动态内容,对于 Anthropic 协议,把中途 system 提到顶层合并,等于每次都在改前缀,是缓存不友好的做法,推荐使用原地转成 user 消息
  2. 工具列表保持稳定、有序。 中途增删工具、调整顺序、修改描述,都会导致全量重算。
  3. 序列化要确定。 同样的工具,如果 JSON key 的顺序不同、空格不同,渲染出来就是不同的 token。自己拼请求或写网关时,要保证序列化结果每次一致
  4. 历史消息尽量不要被改写。

3.3 如何践行实践原则

3.3.1 支持按需加载工具

如何保证工具列表保持稳定、有序?如果中途增删工具、修改描述怎么处理?答案是支持按需加载工具。

最典型的就是 Anthropic API 的 tool search tool 配合 defer_loading:

在工具列表里放一个 tool search 工具,其他工具全部标记 defer_loading: true,这样 Claude 一开始只能看到搜索工具。带 defer_loading: true 的工具在计算缓存 key 之前就会从渲染后的 tools 区块里剥离,根本不出现在 system prompt 前缀中;当搜索命中某个延迟工具并返回 tool_reference 时,这个工具的完整定义会在对话正文的那个位置内联展开(定义被渲染在对应 tool_result 的位置),而不是插进前缀。

3.3.2 保证序列化一致

如何保证工具的序列化一致?首先要明确进到 template 前序列化在哪里发生,然后盯以下几处:

  • 工具列表的顺序。最简单的办法是发请求前按名字排序。
  • 每个工具内部 key 的顺序。 确保 schema 来源是确定的:Python 3.7+ 的 dict 和 JS 的对象都保留插入顺序,只要构造过程确定就没问题;JS 中整数形式的 key 会被自动提前,要留意。如果你没法控制来源,可以统一做一次规范化(递归排序 key),只要每次都这样做,结果就是一致的。
  • 中文转义。 ensure_ascii=True 会把“北京”变成 \u5317\u4eac,token 完全不同。在自己拼字符串(比如方案 A 里把 schema 写进 tool result)的地方要统一设置。
  • 历史工具调用的参数不要重新序列化。 OpenAI 格式里 arguments 是字符串,模板一般会原样输出。拿到模型生成的 arguments 后,回传时要原样放回,不要 json.loads 再 json.dumps,否则空格和 key 顺序可能和模型当初生成的不一致,会从这一轮开始缓存失效。
  • 内容本身不能有动态值。 工具描述里别出现时间、版本号、计数这类每次都会变的东西。

3.3.3 是否删掉历史轮次的思考内容

有些模型比如 Qwen3、deepseek 需要删除历史轮次的思考内容(它不是删除所有历史 assistant 轮的思考,而是以最后一条 user 消息为界。在这条 user 消息之后的 assistant 轮会保留思考,之前的才删除。这样设计是因为,在一次工具调用循环中,模型需要看到自己之前的推理才能接着做下去)。一旦新的 user message 出现,思考都会被删掉,与历史消息不同,这一整段工具调用历史(工具返回的文件内容、搜索结果等往往非常长)都要重算。

如果你自己部署这类模型做 agent,可以考虑两个方向:

  • 保留全部思考。 改模板或者使用模型提供的选项,让历史轮的思考始终保留。序列只追加、不改写,缓存命中最好,模型也能看到完整的推理链。代价是上下文增长得更快。较新的、面向 agent 的模型越来越倾向于这个方向:Anthropic 也要求在工具调用循环中把 thinking 块原样回传,较新的 Claude 模型默认会在上下文里保留历史轮的思考块。
  • 一直删除。 上下文更省,但要接受每个 user 回合开始时重算一次上个回合的工具循环。如果单个回合内的工具步数不多,这个代价是可以接受的。
← 全部文章