NOTE / 2026.08.19
主流大模型 API 协议:从 OpenAI、Claude、Gemini 到 DeepSeek、GLM、Kimi 与 Qwen
从消息、流式事件、工具调用和思考上下文出发,比较国内外主流大模型 API 协议,并沿着 Pi 源码追踪一次请求如何被统一、路由和还原。
接入大模型时,最容易混淆的不是某个字段,而是三个层次:厂商、模型和协议。
Chat Completions 不是一家模型厂商。它是 OpenAI 定义的一套文本生成 API 形式,核心端点是 POST /v1/chat/completions。由于大量平台兼容了这套形式,人们才会说某个服务“兼容 OpenAI”或“提供 Chat Completions 接口”。
反过来,一家厂商也不一定只有一套协议。仅讨论通用文本生成和 Agent 场景,OpenAI 就同时维护 Chat Completions 与 Responses;通义千问同时提供 OpenAI 兼容的 Chat Completions、OpenAI 兼容的 Responses、Anthropic 兼容的 Messages,以及原生 DashScope。模型厂商与 API 协议不是一一对应关系。
本文以 2026 年 8 月 19 日的公开文档为准,重点讨论消息、流式输出、思考内容和工具调用。Embedding、图像、语音、Realtime、Batch 等独立 API 不在范围内。文中的模型名称容易变化,协议结构比某个具体模型 ID 更值得长期关注。
先分清四个概念
| 概念 | 回答的问题 | 示例 |
|---|---|---|
| 厂商 Provider | 请求发给谁,谁负责鉴权和计费? | OpenAI、Anthropic、Google、DeepSeek、智谱、月之暗面、阿里云 |
| 模型 Model | 实际运行哪个模型? | GPT、Claude、Gemini、DeepSeek、GLM、Kimi、Qwen 系列 |
| 协议 Protocol | HTTP 请求和响应长什么样? | OpenAI Responses、OpenAI Chat Completions、Anthropic Messages、Gemini GenerateContent |
| SDK | 用什么客户端构造和解析协议? | OpenAI SDK、Anthropic SDK、Google Gen AI SDK、厂商自有 SDK |
同一个模型可能通过多个平台和协议被调用;同一个协议也可能承载多个厂商的模型。一个 OpenAI SDK 实例指向 DeepSeek 的 Base URL,调用的仍然是 DeepSeek 模型,只是请求外形采用了 OpenAI Chat Completions。
这张图表达了两个关键事实:
- OpenAI、Anthropic 和 Google 都有自己的原生协议。
- DeepSeek、GLM、Kimi、Qwen 等国内平台大量提供兼容协议,但兼容通常是“主要结构兼容”,不是“所有语义完全相同”。
一套模型协议到底规定了什么
只看请求 URL,很难理解协议。对聊天和 Agent 应用,至少要检查下面八个部分:
| 层面 | 需要确认的内容 |
|---|---|
| 端点 | 路径、版本、区域、是否区分流式端点 |
| 鉴权 | Bearer Token、专用请求头、API 版本头 |
| 输入 | system 指令放在哪里,历史消息如何表达,多模态内容如何分块 |
| 输出 | 文本、思考、工具调用、拒绝和引用如何表示 |
| 流式 | SSE 事件是通用 chunk,还是有明确的事件类型和生命周期 |
| 工具 | 工具 Schema、调用 ID、参数增量、结果回传格式 |
| 状态 | 客户端是否必须重放完整历史,服务端能否用响应 ID 延续上下文 |
| 用量与错误 | Token 分类、缓存命中、停止原因、限流和协议错误 |
主流协议的骨架可以先用一张表概括:
| 协议 | 主要端点 | 核心输入 | 核心输出 | 流式特征 | 对话状态 |
|---|---|---|---|---|---|
| OpenAI Responses | /v1/responses |
instructions、input、tools |
output[] 类型化条目 |
response.output_text.delta 等类型化事件 |
可重放输入,也可用 previous_response_id |
| OpenAI Chat Completions | /v1/chat/completions |
messages[]、tools[] |
choices[].message |
choices[].delta,通常以 [DONE] 收尾 |
客户端重放消息历史 |
| Anthropic Messages | /v1/messages |
顶层 system、messages[]、tools[] |
content[] 内容块 |
有名称的 SSE 事件和内容块生命周期 | 客户端重放消息历史 |
| Gemini GenerateContent | :generateContent |
systemInstruction、contents[].parts[] |
candidates[].content.parts[] |
独立 :streamGenerateContent 端点,可用 SSE |
客户端重放 contents |
下面逐一展开。
OpenAI:为什么会同时有 Responses 和 Chat Completions
“OpenAI 有两套 API”需要加一个范围限定:OpenAI 的完整平台当然不止两个端点;但在通用文本生成、推理和工具调用范围内,Responses 与 Chat Completions 是最常遇到的两套接口。
Chat Completions:以消息数组为中心
Chat Completions 的核心抽象是“给定一组消息,补出 assistant 的下一条消息”。
POST /v1/chat/completions
Authorization: Bearer $OPENAI_API_KEY
Content-Type: application/json
{
"model": "MODEL_ID",
"messages": [
{"role": "system", "content": "回答要简洁"},
{"role": "user", "content": "北京今天多少度?"}
],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
}],
"stream": true
}
非流式文本位于 choices[0].message.content。如果模型要调用工具,assistant 消息会携带 tool_calls,其中 function.arguments 是 JSON 字符串,而不是已经验证过的对象。应用必须解析并校验参数,再用 role: "tool"、tool_call_id 把执行结果送回下一次请求。
流式响应通常是一系列 SSE 数据帧。文本从 choices[0].delta.content 累积;工具名和参数也可能被拆成多个 delta;最后根据 finish_reason 判断是正常结束、达到长度限制,还是等待工具调用。
这套协议结构直观、生态最大,因此成为事实上的兼容层。它的代价是所有输出都被压在 choices 与 delta 结构中,处理复杂推理、多种工具和更多类型化输出时,扩展空间不如 Responses 清晰。
Responses:以类型化输入输出项为中心
Responses 不再把一切都理解成一条聊天消息,而是把输入和输出拆成不同类型的 item:
POST /v1/responses
Authorization: Bearer $OPENAI_API_KEY
Content-Type: application/json
{
"model": "MODEL_ID",
"instructions": "回答要简洁",
"input": [{
"role": "user",
"content": [{
"type": "input_text",
"text": "北京今天多少度?"
}]
}],
"tools": [{
"type": "function",
"name": "get_weather",
"description": "查询天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
},
"strict": true
}],
"stream": true
}
输出可能包含 message、reasoning、function_call 等不同 item。工具结果不是一条 role: "tool" 消息,而是:
{
"type": "function_call_output",
"call_id": "call_xxx",
"output": "{\"temperature\": 28}"
}
流式事件也更细,例如:
response.output_item.added:开始一个输出 item;response.output_text.delta:文本增量;response.function_call_arguments.delta:工具参数增量;response.output_item.done:某个 item 完成;response.completed:整个响应完成。
Responses 还可以用 previous_response_id 引用上一轮响应,减少应用手工拼装历史的负担。OpenAI 当前对推理、工具调用和多轮工作流优先推荐 Responses,重要原因之一是它能在轮次之间延续推理上下文。
两套接口怎么选
| 场景 | 更合适的选择 |
|---|---|
| 新建 OpenAI 原生 Agent、需要推理和多种工具 | Responses |
| 已有大量 Chat Completions 代码 | 可以继续使用,再评估迁移 |
| 需要接入大量 OpenAI 兼容厂商 | Chat Completions 仍是最大公约数 |
需要 previous_response_id、类型化事件或托管工具 |
Responses |
Responses 不是简单地把 messages 改名为 input。它改变了输出对象、工具回传、流式事件和状态管理方式,因此适配层必须单独实现。
Anthropic Messages:内容块和事件生命周期
Anthropic 的核心端点是 POST https://api.anthropic.com/v1/messages。典型请求头包括:
x-api-key: $ANTHROPIC_API_KEY
anthropic-version: 2023-06-01
content-type: application/json
它与 OpenAI Chat Completions 有三处结构差异最值得注意。
第一,system 指令位于请求顶层,而不是 messages 中的一条 system 消息。
第二,消息内容天然是块数组。文本、思考、工具调用和工具结果都有各自的块类型:
{
"model": "MODEL_ID",
"max_tokens": 2048,
"system": "回答要简洁",
"messages": [{
"role": "user",
"content": [{"type": "text", "text": "北京今天多少度?"}]
}],
"tools": [{
"name": "get_weather",
"description": "查询天气",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}]
}
模型调用工具时返回 tool_use 块。应用执行后,在下一条 user 消息中放入 tool_result 块,并用 tool_use_id 关联。
第三,Anthropic 的 SSE 是有名称的状态机事件,而不是只有通用 data chunk:
message_start
content_block_start
content_block_delta
content_block_stop
message_delta
message_stop
文本增量是 text_delta,工具参数可能通过 input_json_delta 分片到达,思考和签名也有独立 delta。解析器需要按 index 维护每个内容块的状态,不能把所有帧都当作字符串拼接。
Gemini GenerateContent:一切都是 Part
Gemini 原生 REST 入口把模型 ID 放在 URL 中:
POST https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent
x-goog-api-key: $GEMINI_API_KEY
content-type: application/json
它的基本单元不是 message content block,而是 Content.parts:
{
"systemInstruction": {
"parts": [{"text": "回答要简洁"}]
},
"contents": [{
"role": "user",
"parts": [{"text": "北京今天多少度?"}]
}],
"tools": [{
"functionDeclarations": [{
"name": "get_weather",
"description": "查询天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}]
}]
}
返回值位于 candidates[].content.parts[]。工具请求是 functionCall part,结果是 functionResponse part。流式调用使用独立的 :streamGenerateContent 方法,REST 场景可增加 alt=sse。
Gemini 还有一个容易被通用适配层破坏的字段:thoughtSignature。它是模型生成的不可解释签名,可能附着在思考、文本或工具调用 part 上。后续请求需要原样带回;即使可见文本为空,也不能轻易丢掉携带签名的 part。
国内主流厂商:大多兼容 OpenAI,但都有扩展
国内模型平台采用 OpenAI 兼容协议,主要是为了复用 SDK、IDE 和 Agent 生态。真正接入时仍要逐项核对,尤其是思考模式、工具调用、最大输出字段和流式用量。
| 厂商 | 典型入口 | 主要协议形态 | 关键扩展或差异 |
|---|---|---|---|
| DeepSeek | https://api.deepseek.com/chat/completions |
OpenAI Chat Completions 兼容,也提供 Anthropic 兼容入口 | thinking、reasoning_effort、reasoning_content;严格工具模式位于 Beta 入口 |
| 智谱 GLM | https://open.bigmodel.cn/api/paas/v4/chat/completions |
OpenAI Chat Completions 风格 | thinking、clear_thinking、reasoning_content、tool_stream |
| Kimi / Moonshot | https://api.moonshot.cn/v1/chat/completions |
OpenAI Chat Completions 兼容 | partial、thinking.keep、reasoning_content、cached_tokens |
| 通义千问 / 百炼 | 地域或 Workspace 专属 Base URL | OpenAI Chat、OpenAI Responses、Anthropic Messages、原生 DashScope | 四套接口能力不完全相同,地域 URL 和支持参数需要按文档选择 |
DeepSeek:兼容结构之外,必须保存思考上下文
DeepSeek 的消息角色、choices、tool_calls 和 SSE chunk 基本沿用 Chat Completions。思考模式增加:
{
"thinking": {"type": "enabled"},
"reasoning_effort": "high"
}
思考内容与最终答案平级,分别位于 reasoning_content 和 content。流式响应也分别通过 delta.reasoning_content 与 delta.content 到达。
最重要的规则发生在工具调用之后:如果某轮思考产生了工具调用,后续请求必须完整回传该 assistant 消息的 reasoning_content。遗漏会导致 400。严格遵循 JSON Schema 的工具调用目前需要使用 Beta Base URL,并为所有函数设置 strict: true。
因此,“能用 OpenAI SDK 发出请求”不等于“只替换 Base URL 就完成了可靠适配”。如果中间层只保存可见答案和工具调用,丢掉 reasoning_content,普通聊天可能正常,Agent 多步工具链却会失败。
智谱 GLM:交错思考与保留式思考
GLM 的标准对话补全同样采用 messages、tools、tool_calls 和 role: "tool"。鉴权是 Bearer Token,流式输出使用 SSE。
它在思考上下文上提供了更细的控制:
{
"thinking": {
"type": "enabled",
"clear_thinking": false
},
"reasoning_effort": "high"
}
clear_thinking: false 表示保留式思考,适合 Coding 和 Agent 场景。使用交错思考加工具时,应用要把完整、未修改的 reasoning_content 与工具结果一起回传。部分模型还通过 tool_stream 控制 Function Call 参数是否流式输出。
工具选择目前应按官方文档的能力约束处理。例如文档中的通用 Function Calling 说明只支持 tool_choice: "auto",不能因为 OpenAI Schema 接受更多枚举值,就假设 GLM 端点也全部接受。
Kimi:Chat 兼容入口与 Coding 入口不是同一协议
Moonshot 开放平台的标准入口是 OpenAI Chat Completions 兼容协议。它支持:
tool_calls与严格工具定义;reasoning_content;stream_options.include_usage;- 用量中的
cached_tokens; - 在最后一条 assistant 消息上设置
partial: true,预填输出前缀; prompt_cache_key等缓存相关字段。
思考模式下,多轮请求需要按模型规则保留历史 reasoning_content。SSE 的总体外形仍是 chat.completion.chunk,通常以 data: [DONE] 结束。
但 Kimi For Coding 是另一个接入面。它面向编码订阅场景,可以采用 Anthropic Messages 兼容协议。这意味着“Kimi 使用哪套协议”没有脱离具体入口的唯一答案:Moonshot 标准 API 通常走 OpenAI Chat 兼容,Coding 订阅入口可以走 Anthropic Messages 兼容。
Qwen:多协议平台最典型的例子
阿里云百炼当前为文本生成明确列出四种接口:
- OpenAI 兼容 Chat Completions;
- OpenAI 兼容 Responses;
- Anthropic 兼容 Messages;
- 原生 DashScope。
选择兼容协议可以复用既有客户端;选择原生协议通常能获得更完整的参数和能力。即使选择 Responses,百炼文档也明确说明:它只处理文档列出的参数,未列出的 OpenAI 参数可能被忽略,部分能力和行为与 OpenAI 原生 Responses 不同。
此外,百炼越来越多地使用地域和 Workspace 专属域名。生产配置不能只复制一个网上常见 Base URL,还要确认 API Key 所属地域、Workspace、协议路径和模型可用范围。
工具调用:四套协议描述的是同一个闭环
不同协议的字段名差异很大,但应用层真正需要完成的是同一个五步循环:
对应关系如下:
| 语义 | OpenAI Chat | OpenAI Responses | Anthropic Messages | Gemini |
|---|---|---|---|---|
| 系统指令 | system/developer 消息 | instructions |
顶层 system |
systemInstruction |
| 工具定义 | tools[].function.parameters |
tools[].parameters |
tools[].input_schema |
functionDeclarations[].parameters |
| 模型请求工具 | assistant.tool_calls[] |
function_call item |
tool_use block |
functionCall part |
| 调用参数 | JSON 字符串 | JSON 字符串及其 delta | 对象,流中可能是 partial JSON | 对象 |
| 工具结果 | role: tool + tool_call_id |
function_call_output + call_id |
user 中的 tool_result + tool_use_id |
functionResponse |
| 停止信号 | finish_reason: tool_calls |
output item 与响应状态 | stop_reason: tool_use |
finishReason 与 part 类型 |
工具参数来自模型输出,即使开启了 Schema 约束,也不应直接执行。应用至少要做三件事:按调用 ID 拼完参数、验证 JSON 与业务 Schema、检查工具权限。工具执行结果还必须与原调用 ID 精确关联,否则下一轮模型无法判断哪个结果属于哪个请求。
流式协议不是“把字符串一点点返回”
对纯文本演示,把 SSE 看成字符流似乎够用;一旦加入思考和工具调用,这种实现很快失效。
一个可靠的流解析器至少要维护:
- 当前响应是否已经开始;
- 当前 content item 或 block 的索引和类型;
- 文本、思考和工具参数各自的累积缓冲区;
- 工具调用 ID、名称和 partial JSON;
- 最终停止原因和 usage;
- 错误、中断与不完整输出状态。
例如 Anthropic 的 input_json_delta 和 OpenAI 的 function_call_arguments.delta 都可能从任意字符边界切开 JSON。每个 delta 到达时直接 JSON.parse 并不可靠,必须先累积,完成后再做最终解析与校验。
这也是统一事件层的价值:UI 和 Agent 循环不必理解每家 SSE 的细节,只接收“文本开始、文本增量、工具调用完成、整个响应完成”这样的稳定事件。
Pi 如何统一这些协议
下面结合 Pi 源码追踪一次请求。代码分析固定在仓库 earendil-works/pi 的 58302d3 提交, 避免后续重构导致行号和结论漂移。
Pi 没有强迫所有厂商在 HTTP 层长得一样。它在厂商协议之上定义内部消息和事件,再由每个适配器负责双向翻译。
第一层:统一内部语义,而不是统一厂商 JSON
packages/ai/src/types.ts 定义了稳定的领域模型:
interface Context {
systemPrompt?: string;
messages: Message[];
tools?: Tool[];
}
type Message =
| UserMessage
| AssistantMessage
| ToolResultMessage;
assistant 内容进一步拆成 text、thinking 和 toolCall。工具结果是独立的 toolResult 消息。用量也被归一成输入、输出、缓存读取、缓存写入、推理 Token 和成本。
更关键的是统一的
AssistantMessageEvent:
start
text_start → text_delta → text_end
thinking_start → thinking_delta → thinking_end
toolcall_start → toolcall_delta → toolcall_end
done | error
上层 TUI、日志和 Agent 循环只依赖这套事件。OpenAI 的 response.output_text.delta、Anthropic 的 text_delta 和 Gemini part 文本,最终都会变成同一种 text_delta。
第二层:Agent 消息先变成 LLM 上下文
Coding Agent 还包含 shell 执行、压缩摘要、自定义消息等上层类型。它们先由
convertToLlm()
转换成 pi-ai 能理解的消息。
随后
streamAssistantResponse()
完成这条链:
let messages = context.messages;
messages = await transformContext(messages);
const llmMessages = await convertToLlm(messages);
const llmContext = {
systemPrompt: context.systemPrompt,
messages: llmMessages,
tools: context.tools,
};
const response = await streamFunction(model, llmContext, options);
这里有意把“Agent 的长期上下文管理”和“厂商 HTTP 协议转换”分开。摘要、裁剪和业务消息转换发生在前者;system、messages、contents 或 input 的具体组装发生在后者。
第三层:Provider 与 API 实现是两个对象
Pi 的 Provider 负责:
- 厂商 ID 与显示名称;
- Base URL;
- API Key 或 OAuth;
- 模型目录;
- 应该使用哪一个 API 实现。
API 实现负责:
- 把内部
Context转成厂商请求; - 调用 SDK;
- 把厂商流解析成统一事件;
- 统一停止原因、用量和错误。
createProvider()
既支持一个 Provider 共享单一协议实现,也支持按 model.api 分派到多个实现。这正是“厂商”和“协议”解耦在代码中的落点。
第四层:每套原生协议有独立适配器
| Pi API ID | 主要适配器 | 关键职责 |
|---|---|---|
openai-responses |
openai-responses.ts |
构造 Responses input/tools,解析类型化 response 事件 |
openai-completions |
openai-completions.ts |
构造 Chat messages,解析 choices[].delta,处理兼容厂商差异 |
anthropic-messages |
anthropic-messages.ts |
维护内容块生命周期,解析文本、思考、签名和 partial JSON |
google-generative-ai |
google-generative-ai.ts |
解析 Gemini parts、functionCall、thoughtSignature、usage |
OpenAI Responses 适配器有一个值得注意的选择:它设置 store: false,由 Pi 重放自己的完整上下文,而不是依赖服务端保存的上一轮状态。这样做让会话持久化、分支、切换模型和跨厂商回放仍由 Pi 控制;代价是适配器必须正确保存并重放推理签名等隐藏状态。
Responses 的函数调用还可能同时拥有 call_id 和输出 item ID。Pi 在内部组合这两个标识,回传时再拆开,避免把“调用关联 ID”和“输出条目 ID”混为一谈。相关逻辑位于
openai-responses-shared.ts。
Anthropic 适配器则按照 content_block_start、content_block_delta、content_block_stop 更新当前块。遇到 input_json_delta 时,它先累积工具参数,再在块结束时生成统一 ToolCall。
Gemini 转换器会特别保留 thoughtSignature。即使文本为空,只要携带签名,part 就不能丢弃;否则模型的推理链可能在后续工具轮次中断。对应实现见
google-shared.ts。
第五层:OpenAI 兼容不是一个布尔值
如果兼容性只有“是”或“否”,DeepSeek、GLM、Kimi 和 Qwen 都可以共用一个简单 Base URL 配置。Pi 的实现不是这样。
OpenAICompletionsCompat
把差异拆成多项能力,例如:
- 是否支持
store; - 是否支持 developer role;
- 最大输出字段是
max_tokens还是max_completion_tokens; - 流式响应是否包含 usage;
- 工具是否支持
strict; - 工具结果是否必须带名称;
- 思考开关采用 OpenAI、DeepSeek、Z.AI、Qwen 还是其他格式;
- 是否要求 assistant 历史始终携带
reasoning_content。
detectCompat()
会依据 provider ID 与 Base URL 识别 DeepSeek、Z.AI 和 Moonshot 等端点。以当前代码为例:
- DeepSeek 使用
thinking: {type},并要求回放 assistant 的reasoning_content; - Z.AI 使用自己的
thinking形式; - DeepSeek、Z.AI、Moonshot 使用
max_tokens; - 非标准端点默认不会收到 OpenAI 专属的
store和 developer role。
源码快照和最新厂商文档之间还暴露出一个现实问题:协议能力会持续变化。本文核对的 Kimi Chat 文档已经把 max_tokens 标为弃用、推荐 max_completion_tokens,工具定义中也列出了 strict;而这个 Pi 提交中的 Moonshot 回退规则仍选择 max_tokens,并默认关闭 strict。模型目录中的显式 compat 可以覆盖 URL 推断,但生产接入仍应以目标模型的实测和当期文档为准。适配器不是写完一次就永久正确,兼容元数据也需要随上游协议演进。
这比“捕获 400 后删除一个字段再试”更可靠,因为兼容规则是显式、可检查的模型元数据和适配策略。
国内厂商在 Pi 中的实际映射
Pi 的 Provider factory 很短,但信息密度很高:
| Pi Provider | Base URL | API 实现 |
|---|---|---|
| DeepSeek | https://api.deepseek.com |
openai-completions |
| Moonshot AI CN | https://api.moonshot.cn/v1 |
openai-completions |
| Kimi For Coding | https://api.kimi.com/coding |
anthropic-messages |
| Z.AI Coding CN | https://open.bigmodel.cn/api/coding/paas/v4 |
openai-completions |
| Qwen Token Plan CN | Token Plan OpenAI 兼容入口 | openai-completions |
这里必须避免过度概括:
- Pi 的 Z.AI 内置项面向 Coding Plan 端点,不等于智谱所有标准 API 都固定使用这一个 URL;
- Pi 的 Qwen 内置项面向 Token Plan,并没有在这里把百炼的四套协议全部注册一遍;
- Moonshot 标准 API 与 Kimi For Coding 是两个 Provider,协议不同;
- 任何新的兼容端点仍应按它实际支持的字段配置,而不是只改厂商名称。
第六层:跨模型切换时,隐藏状态不能乱带
思考签名通常只对产生它的厂商、协议和模型有效。把 Gemini 的 thoughtSignature 发送给 Anthropic,或者把 OpenAI 的加密 reasoning item 发送给另一个模型,轻则被忽略,重则请求失败。
Pi 的
transformMessages()
使用 provider、api 和 model 三者判断是否仍是同一个模型:
- 同模型回放时保留不透明签名;
- 跨模型时丢弃不可复用的 redacted thinking;
- 可见思考文本降级为普通文本;
- 移除 Gemini 工具调用上的
thoughtSignature; - 规范化不被目标协议接受的工具调用 ID;
- 如果历史中存在孤立的工具调用,补一个明确失败的合成工具结果,保持消息序列合法。
这段逻辑体现了一个通用原则:统一层应该保存足够多的厂商元数据,但只能在有效范围内重放它。
第七层:Agent 循环只处理 ToolCall 和 ToolResult
适配器完成流解析后,Agent 不再关心上游是 tool_calls、tool_use 还是 functionCall。
Agent 主循环
从 assistant 内容中筛出统一 toolCall。随后它:
- 找到本地工具;
- 验证参数;
- 执行安全钩子;
- 根据工具声明选择顺序或并行执行;
- 生成统一
ToolResultMessage; - 把结果加入上下文,再进入下一轮模型请求。
参数验证与阻断逻辑位于
prepareToolCall()。
如果模型因为长度上限截断了工具参数,主循环不会尝试执行残缺调用,而是把整批调用标记为失败。
于是一次 DeepSeek 天气工具调用的完整路径是:
AgentMessage[]
→ convertToLlm()
→ Context
→ DeepSeek Provider
→ openai-completions buildParams()
→ POST /chat/completions
→ delta.reasoning_content / delta.tool_calls
→ thinking_delta / toolcall_delta / toolcall_end
→ 参数验证与本地工具执行
→ ToolResultMessage
→ role: tool + tool_call_id
→ 连同 assistant reasoning_content 发起下一轮
→ 最终 text_delta / done
这条链路同时解释了为什么统一接口不能只保存最终文本:Agent 的正确性依赖工具 ID、参数分片、停止原因、思考签名和工具结果的完整往返。
设计多厂商接入层时,应该学 Pi 的什么
第一,内部模型应描述业务语义,而不是复制某一家 JSON。ToolCall 和 ToolResult 是稳定语义,tool_calls、tool_use、functionCall 只是线上的不同编码。
第二,Provider 与协议实现要解耦。鉴权、Base URL、模型目录属于厂商;消息转换和流解析属于协议。这样才能表达“一家厂商多种协议”和“多家厂商共用兼容协议”。
第三,流式输出应归一成有生命周期的事件,而不是只暴露文本回调。工具参数、思考内容和错误都需要独立事件。
第四,兼容性应是能力矩阵,不是单个布尔值。字段名称、严格 Schema、usage、思考格式和消息角色都可能不同。
第五,不透明签名也是上下文。看不懂不代表可以丢掉,更不代表可以跨模型复制。
第六,工具调用必须由 Agent 循环闭环。模型只提出调用;参数校验、权限、执行、错误编码和结果回传都属于应用责任。
实际选型建议
如果只接 OpenAI 且新建复杂 Agent,优先从 Responses 开始;如果重点是跨厂商覆盖,OpenAI Chat Completions 仍然是最实用的公共入口,但需要兼容性配置。
如果深度使用 Claude 的思考、内容块和工具流,直接实现 Anthropic Messages,通常比经由兼容代理更清晰。Gemini 的多模态 Part 与 thoughtSignature 也值得使用原生适配器,不宜强行压成纯文本 Chat。
接入 DeepSeek、GLM、Kimi 或 Qwen 时,至少用真实流式工具调用验证下面这份清单:
- Base URL、地域和 API Key 是否匹配;
- system/developer role 是否支持;
- 最大输出字段用哪一个;
- 思考开关和 effort 如何表达;
reasoning_content是否必须回放;- 工具参数是否流式、是否保证 JSON Schema;
tool_choice支持哪些值;- usage 在哪个 chunk 出现;
- 缓存 Token 如何统计;
- 中断、长度截断和内容过滤分别返回什么停止原因。
只验证一次“你好”远远不够。最小的协议验收用例应包含:多轮对话、流式文本、单工具、并行工具、错误工具结果、思考加工具、长参数分片、主动中断和跨模型恢复。
结语
大模型 API 的真正难点不是发出一个 HTTP 请求,而是在多轮、流式、思考和工具调用中保持语义完整。
Chat Completions 是 OpenAI 定义并被广泛兼容的一套协议,不是厂商;Responses 是 OpenAI 面向新一代推理和 Agent 工作流的另一套协议;Anthropic Messages 与 Gemini GenerateContent 又各自建立了内容块和 Part 模型。DeepSeek、GLM、Kimi 和 Qwen 降低了迁移成本,但也通过思考、缓存、工具和多入口扩展了兼容协议。
Pi 的实现给出了一个清楚的工程答案:先建立稳定的内部消息、工具和事件模型,再把每套厂商协议放在边缘翻译。这样,上层 Agent 只处理“模型说了什么、要调用什么工具、工具返回了什么”,而不必在业务循环里到处判断 choices、content_block 或 parts。
理解这层边界之后,面对任何新厂商都可以先问三个问题:它真正实现了哪套协议,扩展或删减了哪些语义,现有适配层能否完整保存一次 Agent 往返所需的状态。答案比“是否兼容 OpenAI”更有价值。
参考资料
- OpenAI:模型与 Responses 迁移指南
- OpenAI:Chat API Reference
- Anthropic:Messages API
- Anthropic:Streaming Messages
- Anthropic:API Versioning
- Google:GenerateContent 与 StreamGenerateContent
- Google:Function Calling
- Google:Thought Signatures
- DeepSeek:Chat Completion
- DeepSeek:Thinking Mode
- DeepSeek:Tool Calls
- 智谱:对话补全
- 智谱:思考模式
- 智谱:工具调用
- Kimi:创建对话补全
- Kimi:工具调用
- 阿里云百炼:文本生成模型 API 参考
- 阿里云百炼:OpenAI 兼容 Responses
- 阿里云百炼:Anthropic 兼容 Messages
- Pi:
packages/ai文档
DISCUSSION