13 min read
三种 LLM 接口:Chat Completions、Responses 与 Messages

14 天系列的补篇,不是 Day N。相关阅读:什么是 Agent Harness · Day 03:LLM Chat Loop

三种 LLM 接口:Chat Completions、Responses 与 Messages 汇入同一套 Agent Harness

写集成模型的时候,最常见的困惑不是“调哪个模型”,而是“同一件事,为什么三种 SDK 写法完全不一样”。

同样是“让模型读一个文件然后总结”,OpenAI 叫你拼 messages 数组,过几天又出了个 input + items 的用法;Anthropic 让你往 content 里塞 block。如果不弄清这三个接口背后的设计逻辑,写出来的 harness 要么深度绑定某一家,要么在翻译层里反复打补丁。

这篇文章把三个接口按同一个骨架拆开对比:

  • Chat Completions(OpenAI,/v1/chat/completions
  • Responses(OpenAI,/v1/responses
  • Messages(Anthropic,/v1/messages

对比的维度是六个:请求结构、工具调用循环、流式协议、结构化输出、状态管理、计费与缓存。最后给出选型建议。

一、为什么会有三种接口

三个接口不是同一代产品,理解这一点比背字段更重要。

Chat Completions 诞生于 2023 年 GPT-3.5 时代。它把“对话”抽象成一条 messages 数组,每条消息带一个 role。设计上它做对了一件事——把“系统提示词、用户输入、历史、工具结果”全部压进同一个数组,用一个 role 字段区分。这让它成为事实上的兼容标准:DeepSeek、vLLM、Ollama、几乎所有本地推理服务都实现了 /chat/completions

但它的缺点同样来自这个设计:一切状态都在客户端。每一次请求都要把全部历史重发一遍,工具调用要自己维护 tool 消息对,流式输出只有 token 增量的概念,没有“结构化单元”的概念。

Anthropic Messages 诞生于 2023 年底(Claude 2.1 时代),是 Anthropic 对大模型接口的另一种回答。它的核心抽象是 content block:一段对话里每个 turn 的 content 不再是一段字符串,而是一组有类型的块——文本、图片、PDF、tool_usetool_resultthinking。系统提示词也独立成顶层字段,而不是塞进消息数组。

OpenAI Responses 诞生于 2025 年 3 月。它是 OpenAI 对“agent 需要什么”的直接回应:把之前散落在 Chat Completions(对话)、Assistants(工具 + 状态)、Batch/Multimodal 的能力收敛成一个统一 primitives。它的核心抽象是 itemmessagefunction_callfunction_call_outputreasoning 都是同级的 item,永续存在 output / input 数组里;还引入了 previous_response_id 服务端状态。

一句话概括三者的关系:

接口 核心抽象 状态归谁 诞生背景
Chat Completions message(带 role 的字符串) 客户端全量维护 GPT-3.5,把 chat 做成标准
Messages content block(有类型的块) 客户端全量维护 Claude,把多模态/工具做成块
Responses item(message / function_call / reasoning) 服务端可选托管 GPT-5 时代,为 agent 重构

二、先看一张总览表

下面这张表是三者的“三分钟速览”,后文逐项展开:

维度 Chat Completions Responses Anthropic Messages
端点 POST /v1/chat/completions POST /v1/responses POST /v1/messages
系统提示词 messages 里的 system role 顶层 instructions 字段 顶层 system 字段
对话输入 messages[] input(字符串或 item 数组) messages[]
角色/类型 role: system/user/assistant/tool item type: message/function_call/function_call_output/reasoning role: user/assistant + block type
工具定义 嵌套 function 字段 扁平 name/parameters 扁平 input_schema
工具回传 role:"tool" + tool_call_id function_call_output item + call_id user 消息里的 tool_result block
最大输出 max_completion_tokens max_output_tokens max_tokens必填
流式协议 SSE,delta 增量 SSE,item 级事件 SSE,content_block 级事件
结构化输出 response_format.json_schema text.format output_config.format(2026 官方支持)
内置工具 无(只有自定义 function) 有(web_search / file_search / code_interpreter 等) 有(web_search / computer use 等)
服务端状态 previous_response_id + store 无(有 prompt caching)
显式缓存控制 无(自动缓存) 无(自动缓存) cache_control 断点

三、请求结构:对话从哪里开始

3.1 系统提示词

同样是“你是编码助手”,三家的写法分别是:

// Chat Completions:system 是 messages 里的一条
const res = await client.chat.completions.create({
  model: "gpt-5.5",
  messages: [
    { role: "system", content: "You are a concise coding assistant." },
    { role: "user", content: "Summarize the repository." },
  ],
});
// Responses:instructions 是顶层字段
const res = await client.responses.create({
  model: "gpt-5.5",
  instructions: "You are a concise coding assistant.",
  input: "Summarize the repository.",
});
// Anthropic Messages:system 是顶层字段,messages 里没有 system role
const res = await client.messages.create({
  model: "claude-opus-4-8",
  max_tokens: 2048,
  system: "You are a concise coding assistant.",
  messages: [{ role: "user", content: "Summarize the repository." }],
});

这里有一个值得注意的差异:Anthropic 不接受 system 出现在 messages,OpenAI 的 Chat Completions 则把 system 当成 messages 的一部分、按位置生效。Responses 更进一步——instructions 是顶层字符串(不是数组),语义上更像“跨回合的恒定指令”,配合 previous_response_id 每次重传即可在保有历史时替换它。

对 harness 的启示:system prompt、项目规则、工具描述在 Anthropic 里有一个天然的分层位置(system 字段可以做 cache_control 缓存断点),在 OpenAI 里则要自己拼字符串。上下文构建器(Context Builder)输出的结构,最后映射到哪个接口的哪个字段,是不同的。

3.2 对话历史

Chat Completions 和 Anthropic Messages 都是无状态的:客户端要自己保存全部历史,每次请求整体重发。区别在于 Anthropic 要求遵守交替规则——user/assistant 消息必须严格交替,连续两条相同 role 会报错;而 OpenAI 对相邻消息比较宽容(虽然官方也建议合并)。

Responses 是三者里唯一有服务端状态的。三种用法按复杂度递进:

  1. input 传字符串:最简单的一次性问答。
  2. input 传 item 数组:把上一轮 response.output 原样接回来,附上新的 function_call_output / 用户消息。
  3. previous_response_id:只传新增内容,历史由服务端托管(配合 store: true 或自动保存)。此时 instructions 可以继续覆盖,input 里缺的上下文模型也能看到。

第三种的代价是:开了服务端状态,解析后的输入/输出 token 会分别计一次费(后面讲 usage 时再说),而且状态在服务端的保留时长有限。对追求可复现、可审计的 harness 来说,“把整个会话明文放进每次请求”反而更透明。

3.3 多模态输入

同一张图片,三种数据结构:

// Chat Completions:content 是数组,图片是 image_url
{ role: "user", content: [
  { type: "text", text: "What is in this image?" },
  { type: "image_url", image_url: { url: "data:image/png;base64,..." } },
] }
// Responses:input 图片部分叫 input_image
{ type: "message", role: "user", content: [
  { type: "input_text", text: "What is in this image?" },
  { type: "input_image", image_url: "data:image/png;base64,..." },
] }
// Anthropic:image block,base64 必须显式声明 media_type
{ role: "user", content: [
  { type: "text", text: "What is in this image?" },
  { type: "image", source: { type: "base64", media_type: "image/png", data: "..." } },
] }

Anthropic 还有另外两家没有的文件类型:document block(原生 PDF 解析)。另外注意细节:Responses 的输入部分叫 input_text/input_image,输出部分叫 output_text/output_image,命名本身就是“对话方向”的提示。

四、工具调用:三种循环

工具调用是三个接口差异最大、也是 harness 最关心的地方。我们用同一个 read_file 工具走完三种“定义 → 调用 → 回传”的循环。

4.1 工具定义

// Chat Completions:type 里嵌套 function 字段(历史包袱)
const tools = [{
  type: "function",
  function: {
    name: "read_file",
    description: "Read a UTF-8 text file from the workspace.",
    parameters: {
      type: "object",
      properties: {
        path: { type: "string", description: "Workspace-relative path." },
      },
      required: ["path"],
      additionalProperties: false,
    },
  },
}];
// Responses:扁平结构,name 直接在顶层
const tools = [{
  type: "function",
  name: "read_file",
  description: "Read a UTF-8 text file from the workspace.",
  parameters: {
    type: "object",
    properties: {
      path: { type: "string", description: "Workspace-relative path." },
    },
    required: ["path"],
    additionalProperties: false,
  },
}];
// Anthropic:input_schema 代替 parameters
const tools = [{
  name: "read_file",
  description: "Read a UTF-8 text file from the workspace.",
  input_schema: {
    type: "object",
    properties: {
      path: { type: "string", description: "Workspace-relative path." },
    },
    required: ["path"],
  },
}];

第一眼看是“同一件事的三种写法”,但背后有两个结构性的区别:

  • Chat Completions 的 function 嵌套是历史痕迹——早期为了兼容 deprecated 的 functions 参数而保留的。JSON Schema 的 additionalProperties:false 只在 OpenAI 侧有意义(配合 strict 模式),Anthropic 的 schema 校验更宽松。
  • 内置工具:Responses 和 Messages 都带内置工具(网页搜索、代码解释器等),其中 Responses 的自定义工具类型也叫 function,但内置的 web_searchfile_searchcode_interpreter 直接 {type:"web_search"} 就能开,不用写 schema。Chat Completions 没有内置工具,全部要自己实现。

4.2 模型说“我要读文件”

模型决定调工具时,三个接口通知你的位置完全不同:

// Chat Completions:在 message.tool_calls 里
const message = res.choices[0].message;
message.tool_calls; // [{ id: "call_abc", type: "function",
                    //    function: { name: "read_file", arguments: '{"path":"README.md"}' } }]
message.content;    // null —— 文本和工具调用互斥
// Responses:output 里是一个 function_call item
res.output.find((item) => item.type === "function_call");
// { id: "fc_123", call_id: "call_xyz", type: "function_call",
//   name: "read_file", arguments: '{"path":"README.md"}' }
res.output_text;    // 顶层便捷属性,本轮所有文本拼接
// Anthropic:content 里是一个 tool_use block
res.content.find((block) => block.type === "tool_use");
// { type: "tool_use", id: "toolu_123", name: "read_file", input: { path: "README.md" } }
res.stop_reason;    // "tool_use" —— 模型主动停住等结果

三个细节值得记住:

  1. Anthropic 的 input 已经是对象{"path":"README.md"}),OpenAI 两侧都要自己 JSON.parse(arguments)
  2. Chat Completions 里 content 会是 null——一旦走工具调用,这一轮的文本就没了。所有“直接读 content 拼 UI”的代码都会在这里踩空。
  3. 响应里的标识符名称不同:Response item 同时有 idcall_id,回传时引用的是 call_id;Anthropic 用 tool_use block 的 id;Chat Completions 用 tool_calls[i].id

4.3 结果回传

执行完 read_file,把结果还给模型:

// Chat Completions:push 一条 role:"tool" 的消息
messages.push(message);                                   // 原样存 assistant
messages.push({ role: "tool", tool_call_id: call.id, content: fileText });

// Responses:把上一轮 output 接回 input,再追加 function_call_output item
await client.responses.create({
  model,
  instructions: "...",
  input: [
    ...res.output,
    { type: "function_call_output", call_id: call.call_id, output: fileText },
  ],
});

// Anthropic:新增一条 user 消息,里面放 tool_result block
await client.messages.create({
  model,
  max_tokens: 2048,
  system: "...",
  messages: [
    { role: "user", content: "Summarize the repository." },
    { role: "assistant", content: res.content },          // tool_use 原样回传
    { role: "user", content: [{ type: "tool_result", tool_use_id: use.id, content: fileText }] },
  ],
});

三种做法的本质是同一个协议:assistant 说要调工具 → 系统执行 → 把结果以“工具产出”的身份放回对话。差异在于回传的载体:

Chat Completions Responses Anthropic
回传载体 消息 role item type content block
引用字段 tool_call_id call_id tool_use_id
历史里占几条 2 条(assistant + tool) 2 个 item(function_call + output) 2 条(assistant + user)
工具结果失败 content 里写错误文本 output 字段写错误文本 is_error: true 显式标记

Anthropic 多做了一层正确性检查:tool_use block 所在的 assistant 消息必须原样回传。assistant 可以同时带文本和 tool_use,但文本必须在 tool_use 前面;紧接着的 user 消息必须带对应的 tool_result,且 tool_use / tool_result 不能交错。这保证了你不能偷偷改掉模型的调用。

4.4 完整的两轮循环

把上面的片段拼成完整可运行的工具循环(第一轮:读文件;第二轮:模型输出总结):

// ---------- Chat Completions ----------
const messages = [
  { role: "system", content: "You are a coding assistant." },
  { role: "user", content: "Read README.md and summarize it." },
];

let res = await client.chat.completions.create({ model, messages, tools });
const first = res.choices[0].message;

if (first.tool_calls?.length) {
  const call = first.tool_calls[0];
  const fileText = await readFile(JSON.parse(call.function.arguments).path);
  messages.push(first);
  messages.push({ role: "tool", tool_call_id: call.id, content: fileText });
  res = await client.chat.completions.create({ model, messages, tools });
}
console.log(res.choices[0].message.content);
// ---------- Responses ----------
let res = await client.responses.create({
  model, instructions: "You are a coding assistant.",
  input: "Read README.md and summarize it.", tools,
});

const call = res.output.find((i) => i.type === "function_call");
if (call) {
  const fileText = await readFile(JSON.parse(call.arguments).path);
  res = await client.responses.create({
    model, instructions: "You are a coding assistant.",
    input: [
      ...res.output,
      { type: "function_call_output", call_id: call.call_id, output: fileText },
    ],
    tools,
  });
}
console.log(res.output_text);
// ---------- Anthropic Messages ----------
const conversation = [{ role: "user", content: "Read README.md and summarize it." }];

let res = await client.messages.create({ model, max_tokens: 2048, system: "You are a coding assistant.", messages: conversation, tools });
const use = res.content.find((b) => b.type === "tool_use");

if (use) {
  const fileText = await readFile(use.input.path);
  conversation.push({ role: "assistant", content: res.content });
  conversation.push({ role: "user", content: [{ type: "tool_result", tool_use_id: use.id, content: fileText }] });
  res = await client.messages.create({ model, max_tokens: 2048, system: "You are a coding assistant.", messages: conversation, tools });
}
console.log(res.content.map((b) => (b.type === "text" ? b.text : "")).join(""));

可以看到:三者做同一件事的代码量几乎一样。真正的差异不在“能不能做”,而在“建模方式”——messages / items / content blocks 三种心智模型。如果你的 harness 只绑定其中一种,切换供应商就是要重写 agent loop。

4.5 tool_choice:谁能决定用哪个工具

tool_choice 三个接口都有,但语义不完全对齐:

// Chat Completions
tool_choice: "auto" | "none" | "required" | { type: "function", function: { name: "read_file" } }

// Responses
tool_choice: "auto" | "none" | "required" | { type: "function", name: "read_file" }

// Anthropic
tool_choice: { type: "auto" } | { type: "any" } | { type: "tool", name: "read_file" }

注意 Anthropic 的 any(必调工具但模型自选)对应 OpenAI 的 required(同样语义)——同一个概念,OpenAI 用字符串,Anthropic 用对象。写兼容层时这是最常见的边界条件之一。

五、流式协议:三种 SSE

流式输出是区分“接口设计是否 agent-ready”的最明显标尺。三者都是 SSE,但粒度完全不同。

Chat Completions:token 增量。 每个 chunk 是 choices[0].delta 的一块:

data: {"choices":[{"delta":{"role":"assistant","content":"Re"}}]}
data: {"choices":[{"delta":{"content":"ading README"}}]}
data: {"choices":[{"delta":{"content":"..."}}]}
data: {"choices":[{"delta":{},"finish_reason":"stop"}]}
data: [DONE]

工具调用时增量会变成 delta.tool_calls 的 fragment(带 index),你要在客户端自己按 index 聚合 arguments 字符串。没有“这条消息完了”的边界事件。

Anthropic Messages:content block 生命周期。 事件围绕 block 的开、续、停展开:

event: message_start
event: content_block_start   {"content_block":{"type":"text","text":""}}
event: content_block_delta   {"delta":{"type":"text_delta","text":"Reading"}}
event: content_block_delta   {"delta":{"type":"text_delta","text":" README"}}
event: content_block_stop
event: message_delta         {"delta":{"stop_reason":"end_turn"},"usage":{...}}
event: message_stop

工具参数走 input_json_delta,thinking 走 thinking_delta。每一个 block 都有明确的开始和结束,天然形成“内容单元”。

Responses:item 级事件。 事件名直接描述“发生了什么”:

event: response.created
event: response.output_item.added       {"output_index":0,"item":{"type":"message",...}}
event: response.content_part.added
event: response.output_text.delta       {"delta":"Re"}
event: response.output_text.delta       {"delta":"ading"}
event: response.output_text.done
event: response.content_part.done
event: response.output_item.done
event: response.completed                {"response":{...完整对象...}}

对 agent harness 来说,Responses 的流式协议有非常实际的好处:事件本身就可以直接变成 transcriptoutput_item.added / done 的边界对应一次工具调用的生命周期,function_call_arguments.delta 对应参数流式到达。Claude Code / Codex 这类产品的“步骤可视化”,在 Responses 上几乎是零成本还原的;Chat Completions 的 token 增量则需要额外维护状态机才能拼出“哪一次调用、哪个参数、什么时候完成”。

Anthropic 的 block 生命周期和 Responses 的 item 生命周期,本质是同一个思想(有边界的结构化单元),只有命名习惯不同:Anthropic 更“消息为中心”,OpenAI 更“行为为中心”。

六、结构化输出

让模型“必须返回符合 schema 的 JSON”,三家现在的做法都收敛到了 constrained decoding(约束解码),但字段名完全不同:

// Chat Completions:response_format 嵌套 json_schema
await client.chat.completions.create({
  model, messages,
  response_format: {
    type: "json_schema",
    json_schema: { name: "summary", strict: true, schema: { /* JSON Schema */ } },
  },
});
// Responses:放在 text.format,因为"文本输出"本身是一个可配置的 item
await client.responses.create({
  model, instructions, input,
  text: { format: { type: "json_schema", name: "summary", strict: true, schema: { /* JSON Schema */ } } },
});
// Anthropic Messages:2026 年官方支持的 output_config(SDK 里也叫 output_format)
const res = await client.messages.create({
  model: "claude-sonnet-4-6", max_tokens: 1024, messages,
  output_config: {
    format: { type: "json_schema", schema: { /* JSON Schema */ } },
  },
});
// SDK 把解析好的结果暴露在 res.parsed_output

注意 Anthropic 在写法上确实和 OpenAI 有别:OpenAI 两个端点在响应体里给你原始 JSON 字符串(自己 parse),而 Anthropic 的 SDK 直接把解析结果暴露在 parsed_output 字段。另外 Anthropic 还有一条历史路径——先用 tool use 强制 schema:把“输出”定义成一个没有实际用途的 output 工具,模型必须调用它、参数就是你的结构。这条路在 2026 年之前是 Anthropic 唯一官方保证 schema 合规的方式,现在被 output_config 取代了,但大量现存代码还在用。

三家共同的限制也值得写进 harness 的文档:strict / constrained 模式下 JSON Schema 有格式约束(不支持部分 anyOf/oneOfdefault 等),字段名会被约束为合法标识符风格,additionalProperties:false 必须显式声明。不同厂商对“schema 不合法”的反应不同:OpenAI 直接 400,Anthropic 会在日志里警告。

七、状态管理与多轮对话

这是 Responses 和另外两家拉开差距的地方。

Chat Completions / Anthropic Messages:无状态,每轮全量重发。Anthropic 通过 prompt caching(cache_control)把这个昂贵的行为变便宜——system、工具定义、历史可以打上缓存断点,命中后按 cache_read_input_tokens 计数而不是全价 input_tokens。OpenAI 也自动做 prompt caching(到一定 token 量起,记在 prompt_tokens_details.cached_tokens),只是不可控。

Responsesprevious_response_id 让“上下文”从客户端搬到了服务端:

// 第一轮不写 previous_response_id
const r1 = await client.responses.create({ model, instructions, input: "Tell me about this repo." });

// 第二轮只传新增内容 + 引用上一轮
const r2 = await client.responses.create({
  model,
  previous_response_id: r1.id,
  input: "What about the build scripts?",
});

代价与边界:

  1. 计费:开启状态托管后,对话的输入 token 会“解析一次、再编码一次”,单轮成本变高;服务端历史也有保留时长上限。
  2. 可复现性:服务端状态是隐式的,transcript / 审计要另想办法;而全量明文发送的请求天然可复现、可重放。
  3. 跨模型:状态是 OpenAI 服务端的概念,切到 Anthropic 或本地模型时 previous_response_id 不存在,你的 harness 必须回到“全量重发”路径。

对 harness 的结论很直接:写兼容层时,把“全量重发”当作基准路径,把 previous_response_id 当作优化项,而不是反过来。

八、思考与推理

推理型模型的输出,三个接口也都给了显式位置:

请求参数 响应里的形态
Chat Completions reasoning_effort: "low"/"medium"/"high" 思考过程不进 content,只在 usage.completion_tokens_details.reasoning_tokens 计数
Responses reasoning: { effort, summary: "auto"/"concise"/"detailed" } output 里的 reasoning item,默认只有摘要,全文要 include 指定
Anthropic Messages Opus 4.8+:thinking: { type: "adaptive" };更早模型:type: "enabled" + budget_tokens content 里的 thinking block(含 signature,续对话必须回传)

几个关键差异:

  • Anthropic 的 thinking 内容默认就在 content 里(可以关),OpenAI 的推理内容默认不进对话、只给摘要或 reasoning tokens 计数。
  • Anthropic 开 thinking 有限制:temperature / top_p / top_k 必须保持默认。Opus 4.8 只接受 type: "adaptive",旧的 enabled + budget_tokens 会 400。stop_reason: "pause_turn" 是 web search 这类 server tool 暂停回合,不是 thinking 预算耗尽。
  • Responses 的 reasoning 摘要默认不回显,要在 include 里点名才返回——这是为了省 token,但因为太隐蔽,很多人第一次接 Responses 会以为推理被隐藏了。
  • Responses 做 function calling 时,上一轮 output 里的 reasoning item 必须原样回传。滤掉之后常见报错是 function_call was provided without its required reasoning item

九、usage 与缓存

三个 usage 对象长得像,字段含义不同,混着读会算错账:

// Chat Completions
usage: {
  prompt_tokens: 1200, completion_tokens: 150, total_tokens: 1350,
  prompt_tokens_details: { cached_tokens: 1000 },          // 自动缓存
  completion_tokens_details: { reasoning_tokens: 80 },     // 推理 token
}

// Responses —— 注意名字变成 input/output
usage: {
  input_tokens: 1200, output_tokens: 150, total_tokens: 1350,
  input_tokens_details: { cached_tokens: 1000 },
  output_tokens_details: { reasoning_tokens: 80 },
}
// 开 previous_response_id 状态托管时,还会有 parse 阶段的额外计费项

// Anthropic —— 缓存是显式的
usage: {
  input_tokens: 1200, output_tokens: 150,
  cache_creation_input_tokens: 0,   // 写缓存
  cache_read_input_tokens: 1000,    // 读缓存
}

结论:OpenAI 的缓存是黑盒自动的,Anthropic 的缓存是白盒可控的。前者省心但不可预测,后者要在“打多少断点 / 放哪个断点”上做工程权衡——断点打错位置,缓存命中率上不去,费用反而更高。

十、各自的坑

按踩坑频率排序,每个接口最容易出问题的点:

Chat Completions

  • message.content 在工具调用时为 null,任何无判断的 content! 都会炸。
  • 工具定义里那个多余的 function 嵌套,写错一层(把 function 漏掉)是最常见的报错来源之一。
  • 流式工具调用要自己按 index 聚合参数 JSON,聚合错误很难定位。
  • max_tokensmax_completion_tokens 并存过渡期,混用容易产生奇怪报错。

Responses

  • input 的 item 类型多(message / function_call / function_call_output / reasoning / web_search_call…)。手动续轮时应把上一轮 output(含 reasoning)原样接回去,再追加 function_call_output;滤掉 reasoning 反而会 400。
  • 用过 previous_response_id 后忘记处理指令覆盖,或反之,状态行为不符合预期。
  • 服务端状态让计费多一档,费用模型和 Chat Completions 不一样,预算对比要换算。
  • 事件流里 response.output_text.doneresponse.output_item.done 的时序关系需要仔细对待(文本完成 ≠ item 完成)。

Anthropic Messages

  • max_tokens 必填,漏了直接 400——这是从 OpenAI 系迁过来第一脚就会踩的。
  • messages 里的 role 严格交替,user/user 或 assistant/assistant 连续会报错,多轮拼接时必须 merge 相邻消息。
  • tool_use 所在的 assistant 消息必须原样回传,不能改写内容(会校验)。
  • 图片必须 base64 + 显式 media_type,“图片太大/格式不对”的错误不像 OpenAI 会帮你兜。
  • thinking 与 temperature 互斥,开了 thinking 又调温度会报错。

十一、选型建议

站在 2026 年的时点,直接给结论:

选 Chat Completions 当兼容基线,不代表永远只调 Chat Completions。

现实是:本地模型、DeepSeek、vLLM、Ollama、中间层服务几乎全部实现了 /chat/completions,它是事实上的“最低公分母”。你的 harness 如果不打算深度绑定某一家,ModelProvider 接口的第一实现应该基于它——这也是这个系列里 Day 03 的默认 provider 走 OpenAI-compatible 端点的原因。

面向 agent 功能选 Responses。 如果产品的主战场是 OpenAI 模型,且你需要内置工具(web search / file search / code interpreter)、服务端状态、或“流式事件即 transcript”,Responses 是唯一把这些做成第一公民的接口。OpenAI 官方也已明确:新能力只会优先落到 Responses,Assistants API 正在被官方引导迁移,新项目不该再以它为起点。

选 Anthropic Messages 当“高质量多模态 + 可控成本”的选项。 长上下文、PDF 原生解析、显式缓存断点、thinking 全程可见,这些对 coding agent 很值钱;代价是接口形态离 OpenAI 系最远,写适配层时注意 tool_use 回传和消息交替规则。

一张需求表收尾:

你的需求 推荐
兼容最多模型 / 本地部署 / 中间层服务 Chat Completions(OpenAI-compatible 端点)
只用 OpenAI 生态,要多轮状态、内置工具 Responses
Claude 生态、长上下文 + 可控缓存成本 Anthropic Messages
多供应商可插拔的 harness 内部统一抽象(message + tool_call + tool_result),每个接口只写一个 adapter
流式事件要做成可审计的 transcript Responses 或 Anthropic(都是单元级事件)

小结

三个接口的差异,归根到底是三句话:

  • Chat Completions 把一切做成“带 role 的消息”——简单、兼容、状态在你手里。
  • Anthropic Messages 把一切做成“有类型的 content block”——结构化、严格、多模态和工具是第一公民。
  • Responses 把一切做成“item”并把状态交给服务端——为 agent 而生的统一 primitives。

它们不是三个版本的同一 API,而是三种不同的心智模型。能同时用这三种接口的思维写 harness,才不会在供应商之间反复重写 agent loop。

如果你在写自己的 provider 抽象层,建议下一步读系列里的 Day 03:LLM Chat Loop,看一个不绑定任何供应商的 ModelProvider 接口长什么样。

Comments

  • Loading comments…

Comments are posted immediately and emailed to the site owner. No account needed.