LLM Provider 能力设计
1. 结论
LLM 层不应该继续以 OpenAI Chat Completions 的 message + tool_calls 作为公共模型。建议把三种 API 当成三个独立的协议适配器:
openai-completions:POST /v1/chat/completionsopenai-responses:POST /v1/responsesanthropic-messages:POST /v1/messages
公共层只统一 Agent 真正依赖的语义:指令、按顺序排列的内容块、函数工具、结构化输出、停止原因、用量、错误和流式生命周期。协议特有能力通过各自的 typed options 和 opaque provider context 保留,不用 map[string]any 把差异偷偷穿透公共接口。
三个 adapter 内部必须直接使用对应的官方 Go SDK:两个 OpenAI adapter 统一使用 github.com/openai/openai-go/v3,Anthropic adapter 使用 github.com/anthropics/anthropic-sdk-go。adapter 不自行实现 HTTP client、SSE parser、重试器或 wire protocol。
首期支持边界:文本、客户端函数工具、并行工具调用、流式输出、JSON Schema 结构化输出、usage、stop reason、取消和可诊断错误。OpenAI hosted tools、Anthropic server tools、图像、文件、音频、batch、background mode 暂不纳入公共能力。
3. 三种协议的能力差异
| 维度 | OpenAI Chat Completions | OpenAI Responses | Anthropic Messages |
|---|---|---|---|
| 输入主模型 | messages[] |
typed input Items |
messages[],content 为 blocks |
| 系统指令 | system/developer message |
top-level instructions 或 input message |
top-level system |
| 输出主模型 | choices[].message |
typed output[] Items |
assistant content[] blocks |
| 函数调用 | assistant tool_calls[] |
function_call Item |
tool_use block |
| 函数结果 | tool role message |
function_call_output Item,用 call_id 关联 |
后续 user message 中的 tool_result block |
| 流式协议 | choices/chunk/delta | typed SSE events | typed SSE events + block index |
| 对话状态 | 客户端重放 transcript | 手工 Item 重放、previous_response_id 或 Conversations |
客户端重放 transcript |
| 推理上下文 | 能力有限且随模型变化 | reasoning Items;工具循环时必须保留 | thinking/redacted thinking blocks;签名必须原样保留 |
| 结构化输出 | response_format |
text.format |
output_config.format |
| 停止信息 | finish_reason |
response status、incomplete details、output item status | stop_reason、stop_sequence |
| 用量 | completion usage;流式需请求 usage | response usage | message_start + 累计 message_delta.usage |
| 存储语义 | 由请求及账号行为决定 | Responses 默认存储,需显式决定 store |
API 本身是 stateless conversation |
关键事实:
- Responses 的输出不只是 message;reasoning、function call、function output 都是独立 Item。只读取文本会破坏工具循环和推理连续性。
- 使用
previous_response_id时,上一次 top-levelinstructions不会自动继承,稳定指令必须每次重发。 - Claude 的 tool result 不是独立
toolrole,而是 user content 中的 block。thinking 开启时,最后一条 assistant message 的 thinking block 必须完整且未修改地回传。 - Claude 流中工具参数通过
input_json_delta.partial_json分片;Responses 通过response.function_call_arguments.delta分片;Chat Completions 则通过 choice delta 分片。公共层不能让 Agent 分别处理三种聚合规则。 - Claude 可能在 HTTP 200 后通过 SSE 发送 error,并且官方明确允许未来新增事件类型。adapter 必须容忍未知事件,同时不能吞掉已知错误。
4. 设计原则
4.1 统一语义,不统一 wire shape
公共类型表达“模型说了什么、要求调用什么、为什么停止、花了多少 token”。每个 adapter 使用官方 SDK 构造请求、消费 SDK 暴露的流式事件和聚合器,再转换为公共领域类型。
不把 OpenAI 的 choices、Responses 的 Items 或 Claude 的 content_block_delta 暴露给 Agent。
4.2 不以最小公分母丢数据
可移植的内容进入公共字段;为了下一轮正确回放但不适合公共建模的数据进入 ProviderContext。典型内容包括:
- Responses reasoning Item、encrypted reasoning content、原生 output Item ID;
- Claude thinking/redacted thinking block、signature;
- provider 原生但调用方不应解析的关联信息。
ProviderContext 与生成它的 adapter、model、provider 配置作用域绑定。切换 adapter、model、base URL 或账号信任域,或重建/裁剪上下文时必须丢弃不再对应的 context,禁止跨 provider 回放。
4.3 公共参数必须有稳定语义
只有三种协议都能合理映射的参数进入公共 Request。无法稳定映射的能力放入 adapter-specific options,并由 adapter 严格解码、校验未知字段。
例如 max_output_tokens、stop sequences、tool choice、parallel tools、JSON Schema output 可以公共化;Responses hosted tools、Claude thinking budget、prompt caching marker 不应伪装成公共参数。
4.4 完成态是权威数据
delta 只用于低延迟展示。工具执行、session 持久化、usage 和 stop reason 一律使用最终 Response,避免调用方自行拼接不完整 JSON。
4.5 SDK-first
三个 adapter 都遵循以下硬性约束:
- 只通过官方 Go SDK 发起同步和流式请求;
- 使用 SDK 提供的 request/response/event union 和 accumulator,不复制官方 wire struct;
- 使用 SDK 的 base URL、header、context cancellation 和 error 机制;
- adapter 只负责公共类型与 SDK 类型之间的双向转换,以及项目统一的 validation、error mapping 和 telemetry;
- SDK 暂未暴露的 API 能力先不支持,不以手写 JSON、HTTP 或 SSE 绕过 SDK;需要时先升级 SDK 并补兼容测试。
为便于单元测试,可以在 adapter 内部定义最小的 SDK service wrapper interface,但生产实现必须委托官方 SDK,不能出现第二套协议客户端。
4.6 OpenAI-compatible dialect
openai-completions 和 openai-responses 表示 wire protocol,不代表所有兼容厂商具有完全相同的参数语义。MiniMax、DeepSeek、Qwen、Kimi 等扩展通过 dialect + model capability 映射,详细调研见 OpenAI-Compatible Thinking 方言调研。
公共层可以定义 ThinkingConfig 的 mode、effort、budget、history preservation 等稳定语义;dialect mapper 负责转换为目标字段。不支持的组合必须在请求前拒绝,不能静默忽略或假装生效。
5. 公共 API
以下代码用于约束接口形状,不是逐字实现要求。
package llmtype Client interface {Generate(ctx context.Context, req Request) (Response, error)Stream(ctx context.Context, req Request) (Stream, error)Capabilities() Capabilities
}type Stream interface {Recv() (Event, error)Close() error // idempotent cancel; producer owns end-of-stream
}
Generate 和 Stream 必须产生同构的最终 Response。Stream 的固定契约:
- 恰好一个
response_start; - 零到多个内容 delta 事件;
- 恰好一个
response_done,其中携带完整Response; - 随后
Recv返回io.EOF。
SDK 建立流之前返回的错误由 Stream 直接返回;SDK 流迭代过程中返回的错误由 Recv 映射为 typed *APIError,不会伪装为 EOF。
5.1 Request
type Request struct {Instructions []TextBlockMessages []MessageTools []ToolDefinitionToolChoice ToolChoiceParallelTools *boolOutputFormat *JSONSchemaFormatMaxOutputTokens *intTemperature *float64StopSequences []stringProviderOptions json.RawMessage
}
设计说明:
- model、API key、base URL 属于 client 配置,不在每次请求中重复。
Instructions与 transcript 分离。adapter 可映射到developer/systemmessage、Responsesinstructions或 Claudesystem。- 可选标量使用 pointer,区分“调用方未指定”和零值。
ProviderOptions由当前 adapter 解码为它自己的具名结构;禁止逐字段SetExtraFields。
5.2 Message 和内容块
type Role stringconst (RoleUser Role = "user"RoleAssistant Role = "assistant"
)type Message struct {Role RoleBlocks []BlockProviderContext *ProviderContext
}type Block struct {Type BlockTypeText stringToolCall *ToolCallToolResult *ToolResultRefusal *Refusal
}type ToolCall struct {ID stringName stringArguments json.RawMessage
}type ToolResult struct {ToolCallID stringContent stringIsError bool
}type ProviderContext struct {Adapter stringModel stringScope string // stable provider-config identity; never an API keyData json.RawMessage
}
约束:
- block 顺序有语义,不能把所有 text 合并后再把 tools 放到末尾。
- assistant message 可包含 text、tool call、refusal;user message 可包含 text、tool result。
- 完成态
ToolCall.Arguments必须是合法 JSON。流式过程中的半截 JSON 只存在于 delta event。 ToolCall.ID始终表示工具结果使用的关联 ID:Responses 映射call_id,另外的 output item ID 只保存在ProviderContext。ProviderContext不发送给 UI,不进入 prompt 文本,不允许业务层修改。- 如果 session 落盘保存
ProviderContext,需要把它视为敏感模型上下文,采用与会话内容相同或更高等级的访问控制;其中可能包含加密 reasoning 或签名材料。
5.3 Response
type Response struct {ID stringModel stringMessage MessageStopReason StopReasonStopDetail stringUsage UsageRequestID string
}type Usage struct {InputTokens int64OutputTokens int64ReasoningTokens int64CacheReadTokens int64CacheWriteTokens int64TotalTokens int64ProviderBreakdown json.RawMessage
}
公共 StopReason 建议限定为:
stop:自然完成;tool_calls:需要客户端执行工具;length:达到输出或上下文限制;content_filter:拒绝或安全拦截;pause:provider 要求继续同一 turn;error:生成失败;unknown:新值尚未映射。
StopDetail 保留原始值,例如 finish_reason=length、stop_reason=pause_turn 或 Responses incomplete reason,便于诊断且不污染控制流。
5.4 Stream events
type Event struct {Type EventTypeIndex intTextDelta stringToolCall *ToolCallDeltaReasoning *ReasoningSummaryDeltaResponse *Response // only response_done
}type ToolCallDelta struct {ID stringName stringArgumentsDelta stringDone bool
}
首期事件集合:
response_starttext_deltatool_call_starttool_call_deltatool_call_donereasoning_summary_deltaresponse_done
原生 ping、keepalive 和没有公共语义的中间事件不向上冒泡。未知事件记录 debug 日志后跳过;未知的终止状态必须映射为 unknown 并保留原值。
5.5 Tools
type ToolDefinition struct {Name stringDescription stringInputSchema json.RawMessageSchemaMode SchemaMode
}
SchemaMode 首期提供 best_effort 和 strict。迁移默认使用 best_effort,以保持现有 MCP tool schema 的兼容性;不能依赖 Responses 与 Chat Completions 不同的隐式 strict 默认值。
启用 strict 前统一校验 schema:对象应设置 additionalProperties: false,属性 required/nullable 规则满足目标 API 的严格模式约束。adapter 不应静默降级用户明确要求的 strict。
ToolChoice 公共语义为 auto、none、required 和指定函数。若目标模型或 provider options 与选择冲突,例如 Claude extended thinking 搭配强制指定工具,adapter 在发请求前返回 UnsupportedOptionError。
6. Provider 配置与注册
type Config struct {Adapter stringDialect stringAPIKey stringBaseURL stringModel stringHeaders map[string]stringDefaults RequestDefaultsOptions json.RawMessage
}
Dialect 只描述同一协议下的扩展语义,不注册成新的顶层 adapter。例如 Adapter=openai-completions, Dialect=deepseek 仍由 OpenAI Completions adapter 和 openai-go/v3 执行。
注册 key 使用协议名,而不是厂商简称:
| Key | 默认 Base URL | 说明 |
|---|---|---|
openai-completions |
https://api.openai.com/v1 |
OpenAI 及明确兼容 Chat Completions 的第三方 |
openai-responses |
https://api.openai.com/v1 |
只有明确兼容 Responses 的服务才能使用 |
anthropic-messages |
https://api.anthropic.com |
Anthropic Messages wire protocol |
兼容迁移期可以保留 openai alias,解析为 openai-completions 并记录一次 deprecation warning。不要根据 base URL 或 model 名自动猜协议。
建议包布局:
internal/llm/client.gotypes.gostream.goerrors.gocapabilities.goprovider/registry.goopenai/completions/responses/anthropic/messages/
SDK 依赖关系固定为:
| Adapter | 官方 Go SDK | SDK 服务 |
|---|---|---|
openai-completions |
github.com/openai/openai-go/v3 |
client.Chat.Completions |
openai-responses |
github.com/openai/openai-go/v3 |
client.Responses |
anthropic-messages |
github.com/anthropics/anthropic-sdk-go |
client.Messages |
OpenAI 两个 adapter 可以复用 SDK client 构造、鉴权配置和 error mapping,但 request/response converter 必须分开。Anthropic 必须使用其官方 Messages SDK,不能经过 OpenAI compatibility endpoint,也不能自行实现 Messages wire protocol。
6.1 Capabilities
ProviderMeta 可以声明协议的最大能力集合,用于 Manager UI;它不能代替请求校验,因为具体能力还受 model、base URL 和账户开关影响。
type Capabilities struct {Streaming boolFunctionTools boolParallelTools boolStructuredOutput boolReasoningSummary boolProviderState bool
}
adapter 必须在发送前执行 ValidateRequest。UI 中的 capabilities 是提示,运行时 validation 才是权威结果。不要维护一份很快过期的完整 model capability 硬编码表;只有确知且需要阻止的协议级约束才写入代码。
7. 三个 Adapter 的映射规则
7.1 openai-completions
- 使用
openai-go/v3的client.Chat.Completions.New和NewStreaming。 - 标准流式字段使用 SDK 的
ChatCompletionAccumulator;厂商扩展从同一 SDK chunk 的JSON.ExtraFields/RawJSON()提取,不能指望 accumulator 自动保留未知字段。 Instructions转为前置 developer/system messages;保留输入顺序。- text block 转为 message content。
- assistant tool call blocks 合并为 assistant
tool_calls。 - user tool result blocks 展开为
toolrole messages。 - streaming 使用 accumulator 按 tool call index 聚合 ID、name 和 arguments。
- 请求流式 usage 时显式启用对应 stream option;最终 usage 只写入
response_done。 - 只接受第一个 choice。公共 API 不支持
n > 1;用户若需要多候选应发多个请求。 finish_reason映射为公共 stop reason,并保留原值。
7.2 openai-responses
- 使用
openai-go/v3的client.Responses同步和流式 API,以及 SDK 定义的 Responses event union。 Instructions每次请求都发送 top-levelinstructions。- messages 和 blocks 转为 typed input Items;tool call result 转为
function_call_output,使用 call ID 关联。 - 输出遍历全部
response.output,不能只使用output_texthelper。 - text 处理
response.output_text.delta;工具参数处理response.function_call_arguments.delta/done;完成以response.completed或相应终止事件收口。 - 原生 reasoning 和需要回放的 output Items 写入 assistant message 的
ProviderContext。 - 首期默认
store: false+ 客户端上下文重放,避免接入 Responses 后无意改变数据保留语义。 - 如启用 reasoning,必须保留并回放官方要求的 reasoning Items/encrypted content,尤其是带工具调用的 response。
- 后续可增加显式
state_mode=server,使用previous_response_id;它不能与任意裁剪后的本地 transcript 混用,且每次仍需重发 instructions。 - structured output 映射到
text.format,不能复用 Chat Completions 的response_formatwire 字段。
7.3 anthropic-messages
- 使用
anthropic-sdk-go的client.Messages同步和流式 API,并使用 SDK 的 message accumulation 能力获得完成态 Message。 Instructions合并为 top-levelsystemblocks,不生成 system role message。- transcript 只产生 user/assistant roles。工具结果放进 user content 的
tool_resultblocks,并保持 provider 要求的 block 顺序。 - assistant 输出按 content block 原顺序转换;
tool_use.input完成后编码为json.RawMessage。 - streaming 按 block index 处理
content_block_start/delta/stop;工具参数聚合input_json_delta.partial_json。 - thinking/signature/redacted thinking 不作为语音文本输出,完整写入
ProviderContext,下一工具步骤原样回放。 message_startusage 与message_delta的累计 usage 合并,不能把多个累计值相加。- SSE error 转为
APIError;ping 忽略;未知事件容忍。 max_tokens是 Messages 请求必填项。公共配置应提供明确的max_output_tokens默认值,adapter 启动时校验其大于零。- structured output 映射到
output_config.format。
8. ProviderContext 与 Session
不要让 adapter 暗中维护整段会话状态。Agent 的 session/context builder 仍是对话历史的唯一事实来源,否则 session 裁剪、摘要和 memory 注入会与 provider 私有状态分叉。
推荐做法:
- 每个完成的 assistant
Message同时保存公共 blocks 和该次响应的ProviderContext。 - context builder 裁剪某条 message 时,其 provider context 一起裁剪。
- adapter 构建下一请求时,优先使用匹配当前 adapter/model/scope 的 context 还原该 assistant turn;不匹配时退回公共 blocks。
- 切换 model、adapter、base URL 信任域后清空所有 provider context。
- 修改 assistant blocks 后必须清空对应 context,避免“显示内容”和“真实回放内容”不一致。
这比只保存一个 previous_response_id 更符合当前架构:本地 session 可以被 memory service 重组,且工具循环需要可恢复、可测试、可迁移。
首期不建议启用 Responses server-managed state。等本地 transcript 模式稳定后,再把它作为显式策略加入,并定义清晰的 fork、过期、删除和数据保留行为。
9. 错误模型与可观测性
type APIError struct {Adapter stringStatusCode intType stringCode stringMessage stringRequestID stringRetryable boolCause error
}
至少区分:
- 本地 request validation;
- 鉴权/权限;
- rate limit;
- provider overloaded;
- context length;
- content policy/refusal;
- SDK transport/stream decode;
- context cancellation/deadline;
- provider protocol violation。
日志与指标统一记录 adapter、model、request ID、首 token 延迟、总耗时、stop reason、input/output/cache/reasoning tokens、tool call 数和错误分类。API key、完整 prompt、tool arguments、tool result、ProviderContext 不进入普通日志。
retry 只放在明确安全的阶段:建立响应前的可重试错误可以按策略重试;已经输出 text delta 或 tool call 后默认不自动重试,避免重复播报或重复执行有副作用的工具。
11. 测试策略与验收标准
三个 adapter 共享一套 contract test。通过 httptest.Server 配置官方 SDK 的 base URL,返回官方协议形状的本地 HTTP/SSE fixtures,以同时验证“公共类型转换 + SDK 行为 + adapter 归一化”,不依赖真实 API key。只 mock adapter 自己定义的最小 SDK service wrapper 时,仍需保留上述 SDK 集成 fixture,避免 mock 与 SDK 实际类型漂移。
必测场景:
- 同步纯文本;
- 流式纯文本与 UTF-8 分片;
- 单工具调用;
- 多个并行工具调用;
- text 与 tool call blocks 交错;
- 工具参数跨多个 delta,完成后 JSON 合法;
- tool result 和 tool error 回传;
- stop、tool calls、length、refusal、unknown stop reason;
- usage 与 cache/reasoning 明细;
- JSON Schema structured output;
- 建连前错误与流内错误;
- ctx cancel、deadline、调用方提前 Close;
- 未知 SSE event 不破坏后续事件;
- provider context 在下一工具步骤无损回放;
- adapter/model 不匹配时 context 被拒绝或清除;
- sync 与 stream 聚合后的最终 Response 等价。
12. 明确不做的事情
- 不做一个自动猜测任意厂商兼容程度的“万能 OpenAI provider”。
- 不把 Responses hosted tools 与本地
internal/tools混成同一种执行路径。 - 不把 Claude thinking 当作普通 assistant text 送入 TTS 或 UI。
- 不允许未经校验的
map[string]any覆盖公共请求字段。 - 不自行实现 OpenAI 或 Anthropic 的 HTTP client、SSE parser、wire structs 和 retry policy。
- 不绕过官方 SDK 使用裸 JSON 调用 SDK 尚未支持的能力。
- 不把 MiniMax、DeepSeek、Qwen、Kimi 的 thinking 扩展当成同一套 wire 参数;必须经过 dialect/model validation。
- 不在首期提供跨 adapter 的 server-side conversation migration。
- 不为所有模型硬编码能力表;协议校验和 provider 返回错误优先。
13. 官方资料
- OpenAI, Migrate to the Responses API
- OpenAI, Function calling
- OpenAI, Chat Completions API reference
- OpenAI, Responses API reference
- Anthropic, Messages API reference
- Anthropic, Streaming Messages
- Anthropic, Define tools
- Anthropic, Extended thinking
- OpenAI, openai-go
- Anthropic, anthropic-sdk-go