第 17 章 Provider 抽象 —— 模型无关的 Agent
Agent 不应绑定单一模型供应商;Provider 抽象层的核心职责是将"选哪个模型"与"Agent 怎么用模型"彻底解耦,使上层业务代码对底层 LLM API 完全透明。
17.1 Provider 抽象层的必要性
模型能力迭代速度极快——一个季度前的 SOTA 可能已被更便宜的替代品超越。如果 Agent 代码中硬编码了某个供应商的 SDK 调用方式、认证逻辑和响应格式,就会产生以下技术债务:
- 供应商锁定(Vendor Lock-in)。切换模型需要改动调用链的每一层,从 HTTP 请求构造、SSE 解析到 tool_use 回传。
- 无法多模型并行评估。生产环境经常需要 A/B 测试不同模型的效果,缺乏抽象层意味着每个模型都要写一套适配代码。
- 故障恢复困难。主模型限流或宕机时,没有统一接口就无法自动切换到备选模型。
- 推理参数碎片化。同一个"控制思考深度"的语义,Anthropic 用
thinking.budgetTokens,OpenAI 用reasoningEffort,Google 用thinkingConfig.thinkingBudget——没有翻译层就必须到处写 if-else。
Provider 抽象层的设计目标:上层代码只操作统一的 Model 接口,一切供应商差异在抽象层内部消化。
17.2 各系统的 Provider 设计
17.2.1 OpenCode:AI SDK + 多供应商注册表
OpenCode 的 Provider 体系是目前开源 Coding Agent 中最完整的实现,其设计分三层。
第一层:models.dev 注册表。所有模型的元数据(context window、cost、capabilities、reasoning 支持)统一存储在远程 models.dev/api.json,本地缓存并定时刷新。每个模型描述符包含:
interface ModelDescriptor {
id: string // 模型 ID,如 "claude-sonnet-4-6"
name: string // 显示名称
reasoning: boolean // 是否支持推理模式
tool_call: boolean // 是否支持 tool calling
cost: {
input: number // 每 M token 价格(美元)
output: number
cache_read?: number
cache_write?: number
}
limit: {
context: number // 上下文窗口
output: number // 最大输出 token
}
provider?: {
npm: string // 对应的 AI SDK 包名
api?: string // 自定义 API 端点
}
}第二层:Bundled Provider SDK 映射。OpenCode 在启动时将所有支持的 SDK 工厂函数注册到一个静态映射表中:
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
"@ai-sdk/anthropic": createAnthropic,
"@ai-sdk/openai": createOpenAI,
"@ai-sdk/google": createGoogleGenerativeAI,
"@ai-sdk/google-vertex": createVertex,
"@ai-sdk/amazon-bedrock": createAmazonBedrock,
"@ai-sdk/azure": createAzure,
"@openrouter/ai-sdk-provider": createOpenRouter,
"@ai-sdk/xai": createXai,
"@ai-sdk/mistral": createMistral,
"@ai-sdk/groq": createGroq,
"@ai-sdk/deepinfra": createDeepInfra,
"@ai-sdk/openai-compatible": createOpenAICompatible,
"@ai-sdk/gateway": createGateway,
// ... 共 18+ 供应商
}根据模型描述符中的 npm 字段,自动选择对应的 SDK 工厂创建 provider 实例。这使得新增供应商只需在注册表中加一行映射。
第三层:Custom Loader 钩子。部分供应商需要特殊处理(认证方式、区域路由、模型发现),通过 CUSTOM_LOADERS 实现:
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
"amazon-bedrock": async () => ({
autoload: true,
options: { region, credentialProvider },
async getModel(sdk, modelID) {
// 自动添加跨区域推理前缀(us.、eu.、apac.)
return sdk.languageModel(`${regionPrefix}.${modelID}`)
},
}),
openai: async () => ({
autoload: false,
async getModel(sdk, modelID) {
return sdk.responses(modelID) // 默认使用 Responses API
},
}),
"github-copilot": async () => ({
autoload: false,
async getModel(sdk, modelID) {
// 根据模型是否支持 Responses API 选择接口
// 判断依据为 shouldUseCopilotResponsesApi() 内部的能力检测,
// 而非固定的模型代际划分(各模型的 API 支持范围可能随版本更新变化)
return shouldUseCopilotResponsesApi(modelID)
? sdk.responses(modelID)
: sdk.chat(modelID)
},
}),
}这种三层架构的关键优势:上层代码只需指定 providerID + modelID,Provider 层自动完成 SDK 选择、认证注入、API 端点路由和区域适配。
17.2.2 Codex:OpenAI Responses API + ChatGPT 登录 + API Key
Codex 的 Provider 设计走了一条与 OpenCode 截然不同的路——强绑定 OpenAI 生态,但提供了灵活的认证层和协议代理。
Responses API Proxy。Codex 内置了一个用 Rust 实现的最小化 HTTP 代理服务器,其唯一职责是:接收本地请求 → 注入 Authorization header → 转发到 OpenAI POST /v1/responses → 流式透传响应。
struct ForwardConfig {
upstream_url: Url, // 默认 https://api.openai.com/v1/responses
host_header: HeaderValue,
}
fn forward_request(client: &Client, auth_header: &'static str,
config: &ForwardConfig, req: Request) -> Result<()> {
// 仅允许 POST /v1/responses
let allow = method == Method::Post && url_path == "/v1/responses";
if !allow { return respond_403(); }
// 替换 Authorization,透传其余 header
headers.insert(AUTHORIZATION, auth_header_value);
headers.insert(HOST, config.host_header.clone());
// 流式转发响应
let upstream_resp = client.post(config.upstream_url.clone())
.headers(headers).body(body).send()?;
req.respond(response);
}API Key 安全处理。Codex 对 API Key 的内存管理达到了近乎偏执的程度:通过 read(2) 系统调用直接从 stdin 读取(绕过 Rust 标准库的 BufReader 以避免内存残留),读取后立即用 mlock(2) 锁定包含密钥的内存页防止被 swap 到磁盘,中间变量用 zeroize 清零。
双认证模式。Codex 支持两种认证方式:
- ChatGPT 账号登录:通过浏览器 OAuth 流或 Device Code 流获取 token,使用 ChatGPT Plus/Pro/Enterprise 配额
- API Key:直接使用 OpenAI API Key,按量计费
虽然 Codex 不支持多供应商切换,但其 --upstream-url 参数允许将代理指向任意兼容 OpenAI Responses API 的端点,实现了有限的供应商可替换性。
17.2.3 mini-codex:环境变量驱动的轻量实现
mini-codex 展示了 Provider 抽象的最简形式——通过环境变量切换端点,用模型名前缀推断参数。
struct LlmConfig {
api_key: String, // MINI_CODEX_API_KEY 或 OPENAI_API_KEY
base_url: String, // MINI_CODEX_BASE_URL 或 OPENAI_BASE_URL
model: String, // 任意模型名
reasoning_effort: String, // low/medium/high
enable_thinking: bool, // 针对国产模型的思考模式开关
}模型族检测通过前缀匹配实现:
fn model_family(model: &str) -> &str {
let normalized = model.trim().to_ascii_lowercase();
if normalized.starts_with("gpt") || normalized.starts_with("o1")
|| normalized.starts_with("o3") || normalized.starts_with("o4") {
"openai"
} else if normalized.starts_with("gemini") { "gemini" }
else if normalized.starts_with("qwen") { "qwen" }
else if normalized.starts_with("deepseek") { "deepseek" }
else if normalized.starts_with("kimi") || normalized.starts_with("moonshot") { "moonshot" }
// ... 覆盖 doubao、hunyuan、glm、yi 等国产模型
else { "other" }
}然后根据模型族决定发送哪些参数:
fn supports_reasoning_effort(family: &str) -> bool {
matches!(family, "openai" | "gemini")
}
fn supports_enable_thinking(family: &str) -> bool {
matches!(family, "qwen" | "deepseek" | "moonshot" | "doubao" | "hunyuan" | "glm" | "yi")
}这种方式的优点是零依赖、即改即用——只要目标模型兼容 OpenAI Chat Completions API,改两个环境变量就能切换。缺点是无法处理差异化的认证协议和流式格式。
17.2.4 DeepAgents:LangChain init_chat_model() 统一接入
DeepAgents 利用 LangChain 生态的 init_chat_model() 实现了一行代码切换模型的能力:
from langchain.chat_models import init_chat_model
from langchain_core.language_models import BaseChatModel
def resolve_model(model: str | BaseChatModel) -> BaseChatModel:
if isinstance(model, BaseChatModel):
return model # 已初始化的实例直接使用
if model.startswith("openai:"):
return init_chat_model(model, use_responses_api=True)
return init_chat_model(model) # "provider:model" 格式自动路由init_chat_model 内部维护了一个 provider → ChatModel 类的注册表,支持的格式包括:
| 输入格式 | 解析行为 |
|---|---|
"openai:gpt-5" | 创建 ChatOpenAI(model="gpt-5") |
"anthropic:claude-sonnet-4-6" | 创建 ChatAnthropic(model_name="claude-sonnet-4-6") |
"google-genai:gemini-2.5-pro" | 创建 ChatGoogleGenerativeAI(model="gemini-2.5-pro") |
"ollama:llama3" | 创建 ChatOllama(model="llama3") |
DeepAgents 的 create_deep_agent() 函数接受 model 参数为 str | BaseChatModel | None,默认使用 claude-sonnet-4-6:
def create_deep_agent(model: str | BaseChatModel | None = None, ...):
model = get_default_model() if model is None else resolve_model(model)子 Agent 也可以使用不同模型——主 Agent 用 Claude,子 Agent 用 GPT-5,只需在 SubAgent spec 中指定 "model": "openai:gpt-5" 即可。
17.2.5 四系统对比
| 维度 | OpenCode | Codex | mini-codex | DeepAgents |
|---|---|---|---|---|
| 抽象粒度 | SDK 工厂 + 注册表 | 协议代理 | 环境变量 | LangChain 统一接口 |
| 支持供应商数 | 18+ | 1 (OpenAI) | 理论无限* | LangChain 生态全覆盖 |
| 新增供应商成本 | 注册表加一行 | 改 upstream URL | 改环境变量 | pip install 一个包 |
| 认证管理 | 多策略 (OAuth, API Key, IAM) | ChatGPT + API Key | 环境变量 | 各 SDK 自行处理 |
| 模型元数据 | models.dev 远程注册表 | 内置目录 | 无 | 无 |
*mini-codex 支持任何兼容 OpenAI Chat Completions API 的端点。
17.3 流式协议差异
LLM API 的流式输出没有统一标准。三大供应商的 SSE(Server-Sent Events)协议在事件类型、分片粒度和 tool_use 表示上存在显著差异。
17.3.1 Anthropic SSE 协议
Anthropic 使用细粒度的事件类型来标记内容流的每个阶段:
event: message_start
data: {"type":"message_start","message":{"id":"msg_01X","model":"claude-sonnet-4-6",...}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01X","name":"read_file","input":{}}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"path\":\"/src"}}
event: content_block_stop
data: {"type":"content_block_stop","index":1}
event: message_stop
data: {"type":"message_stop"}关键特征:
- content_block 索引机制:每个输出块(text、tool_use、thinking)都有唯一 index,delta 事件通过 index 关联到对应的块
- tool_use 的 input 以 JSON 片段流式传输:
partial_json需要在客户端累积拼接后才能解析 - thinking 内容作为独立 content_block:
type: "thinking",与正文内容通过 index 区分
17.3.2 OpenAI SSE 协议
OpenAI 的 Chat Completions API 使用 choices[].delta 结构流式传输:
data: {"id":"chatcmpl-X","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"}}]}
data: {"id":"chatcmpl-X","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_X","type":"function","function":{"name":"shell","arguments":""}}]}}]}
data: {"id":"chatcmpl-X","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"cmd\":"}}]}}]}
data: [DONE]Responses API 格式不同,使用扁平化的事件流:
event: response.output_item.added
data: {"type":"function_call","id":"fc_X","name":"shell","arguments":""}
event: response.output_item.content.delta
data: {"delta":"{\"cmd\":\"ls\"}"}
event: response.output_item.done
data: {"type":"function_call","id":"fc_X","name":"shell","arguments":"{\"cmd\":\"ls\"}"}
event: response.completed
data: {"id":"resp_X","status":"completed","usage":{...}}关键特征:
- tool_calls 嵌套在 delta 中:通过
tool_calls[].index区分多个并行 tool call - arguments 是 JSON 字符串的增量片段:需要逐段拼接
[DONE]标记结束(Chat Completions)或response.completed事件(Responses API)
17.3.3 Google 流式格式
Google 的 Gemini API 不使用 SSE,而是返回 JSON 数组的流式分片:
[{
"candidates": [{
"content": {
"parts": [{"text": "Hello"}],
"role": "model"
}
}]
},
{
"candidates": [{
"content": {
"parts": [{"functionCall": {"name": "read_file", "args": {"path": "/src"}}}],
"role": "model"
}
}]
}]关键特征:
- 无事件类型区分:每个 chunk 都是完整的 candidate 结构
- tool call 以
functionCall形式出现在 parts 中:与文本 part 同级 - thinking 通过
thought: true属性标记
17.3.4 统一抽象层的处理策略
OpenCode 的做法是在 AI SDK 层统一消化差异——每个供应商的 SDK 包内部实现各自的 SSE 解析器,对外暴露统一的 streamText() / generateText() 接口。上层代码只处理标准化后的事件:
// 上层代码完全不感知底层是 Anthropic SSE 还是 OpenAI SSE
const result = streamText({
model: provider.languageModel("claude-sonnet-4-6"),
messages,
tools,
maxTokens: ProviderTransform.maxOutputTokens(model),
})
for await (const chunk of result.textStream) {
// 统一的文本流
}但统一抽象并不能消除所有差异。OpenCode 的 ProviderTransform.message() 函数需要在发送消息前做供应商特定的规范化处理:
- Anthropic:拒绝空内容消息,需要过滤掉
content === ""的消息 - Mistral:要求
toolCallId恰好 9 个字母数字字符,需要截断/补齐 - Claude:
toolCallId只允许[a-zA-Z0-9_-],需要正则替换非法字符 - interleaved thinking 模型(如 DeepSeek、Qwen):reasoning 内容需要从标准 content 中抽取,转存到
providerOptions.openaiCompatible.reasoning_content
function normalizeMessages(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
if (model.api.npm === "@ai-sdk/anthropic") {
msgs = msgs.filter(msg => msg.content !== "")
}
if (model.providerID === "mistral") {
// toolCallId 截断到 9 字符,不足补 0
part.toolCallId = part.toolCallId
.replace(/[^a-zA-Z0-9]/g, "")
.substring(0, 9)
.padEnd(9, "0")
}
// ...
}17.4 故障转移与熔断
17.4.1 重试策略
各系统的重试策略差异明显。
mini-codex:固定间隔重试,最多 7 次,间隔 1.5 秒。不区分可重试和不可重试错误,所有失败都重试:
const API_RETRIES: usize = 7;
const API_RETRY_DELAY_MS: u64 = 1_500;
for attempt in 1..=API_RETRIES {
match call_api() {
Ok(result) => return Ok(result),
Err(err) => {
print_api_error(&format!("{err:#}"));
if attempt < API_RETRIES {
std::thread::sleep(Duration::from_millis(API_RETRY_DELAY_MS));
}
}
}
}OpenCode:细粒度错误分类,区分可重试和不可重试错误:
function parseAPICallError(input: { providerID: ProviderID; error: APICallError }) {
// 上下文溢出 → 不重试,触发上下文压缩
if (isOverflow(message) || error.statusCode === 413) {
return { type: "context_overflow", message }
}
// OpenAI 的 404 有时是误报 → 标记为可重试
if (providerID.startsWith("openai") && error.statusCode === 404) {
return { type: "api_error", isRetryable: true }
}
// 其他错误使用 SDK 自带的重试判断
return { type: "api_error", isRetryable: error.isRetryable }
}OpenCode 维护了一组覆盖 15+ 供应商的上下文溢出检测正则:
const OVERFLOW_PATTERNS = [
/prompt is too long/i, // Anthropic
/exceeds the context window/i, // OpenAI
/input token count.*exceeds the maximum/i, // Google
/maximum prompt length is \d+/i, // xAI
/reduce the length of the messages/i, // Groq
/maximum context length is \d+ tokens/i, // OpenRouter, DeepSeek, vLLM
/context window exceeds limit/i, // MiniMax
// ... 15+ 正则
]17.4.2 SSE 超时保护
OpenCode 实现了 SSE 流的读超时保护。对于流式响应,每次 reader.read() 调用都设置了超时:
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
const reader = res.body.getReader()
const body = new ReadableStream({
async pull(ctrl) {
const part = await new Promise((resolve, reject) => {
const id = setTimeout(() => {
const err = new Error("SSE read timed out")
ctl.abort(err)
reader.cancel(err)
reject(err)
}, ms)
reader.read().then(
(part) => { clearTimeout(id); resolve(part) },
(err) => { clearTimeout(id); reject(err) },
)
})
if (part.done) { ctrl.close(); return }
ctrl.enqueue(part.value)
},
})
return new Response(body, { headers: res.headers, status: res.status })
}这种设计防止了模型 hang 住不返回数据时的无限等待。
17.4.3 流式错误检测
Codex 的 Responses API 会在 SSE 流中发送错误事件。OpenCode 的错误解析器能识别这些流中错误:
function parseStreamError(input: unknown): ParsedStreamError | undefined {
if (body.type !== "error") return
switch (body?.error?.code) {
case "context_length_exceeded":
return { type: "context_overflow", message: "Input exceeds context window" }
case "insufficient_quota":
return { type: "api_error", message: "Quota exceeded", isRetryable: false }
case "usage_not_included":
return { type: "api_error",
message: "Upgrade to Plus: https://chatgpt.com/explore/plus",
isRetryable: false }
}
}17.4.4 熔断策略设计要点
虽然现有系统没有完整实现熔断器(Circuit Breaker),但从错误处理代码中可以提炼出实用的熔断策略:
┌─────────────┐ 连续 N 次失败 ┌─────────────┐ 探活成功 ┌─────────────┐
│ CLOSED │ ─────────────────→ │ OPEN │ ──────────→ │ HALF-OPEN │
│ (正常调用) │ │ (拒绝请求) │ │ (试探性调用) │
└─────────────┘ └─────────────┘ └─────────────┘
↑ ↑ │
│ 探活失败 │ 成功 │
└──────────────────────────────────┘←────────────────────────┘关键参数:
- 失败阈值 N:连续 N 次 5xx / timeout 后触发熔断(推荐 N=3~5)
- 冷却时间:熔断后暂停该 provider 的请求(推荐 30~60 秒)
- 探活间隔:定期发送轻量请求检测 provider 是否恢复
- 降级策略:主 provider 熔断后自动切换到预配置的备选 provider
速率限制(Rate Limit)的检测同样重要。429 响应通常携带 Retry-After header,合理的实现应该解析该 header 并排队等待:
if (error.statusCode === 429) {
const retryAfter = error.responseHeaders?.["retry-after"]
const delayMs = retryAfter ? parseInt(retryAfter) * 1000 : 60_000
await sleep(delayMs)
// 重试或切换到备选 provider
}17.5 推理参数管理
17.5.1 reasoning_effort vs enable_thinking
现代推理模型的"思考深度"控制参数因供应商而异,Provider 抽象层的职责之一就是翻译这些差异。
OpenAI 系(GPT-o 系列、GPT-5 系列):使用 reasoning_effort 字段,取值范围为 none | minimal | low | medium | high | xhigh,直接影响模型花在"思考"上的计算量。
Anthropic:使用 thinking 配置对象。传统模式下是二元开关 + 预算:
{
"thinking": {
"type": "enabled",
"budgetTokens": 16000
}
}新一代模型(Opus 4.6、Sonnet 4.6)支持自适应模式:
{
"thinking": {
"type": "adaptive"
},
"effort": "high"
}Google:使用 thinkingConfig 对象。Gemini 2.5 用 token 预算:
{
"thinkingConfig": {
"includeThoughts": true,
"thinkingBudget": 16000
}
}Gemini 3.x 改用等级制:
{
"thinkingConfig": {
"includeThoughts": true,
"thinkingLevel": "high"
}
}国产模型(Qwen、DeepSeek、Kimi、Doubao、Hunyuan、GLM、Yi):使用 enable_thinking: true 布尔开关,部分还需要 chat_template_args 包装:
{ "chat_template_args": { "enable_thinking": true } }OpenCode 的 ProviderTransform.options() 函数集中处理了这些差异,根据 model.providerID 和 model.api.npm 组合,为每个模型生成正确的推理参数:
function options(input: { model: Provider.Model; sessionID: string }) {
const result: Record<string, any> = {}
// OpenAI: store=false + reasoningEffort
if (model.providerID === "openai") {
result["store"] = false
if (model.api.id.includes("gpt-5")) {
result["reasoningEffort"] = "medium"
result["reasoningSummary"] = "auto"
}
}
// Google: thinkingConfig
if (model.api.npm === "@ai-sdk/google") {
if (model.capabilities.reasoning) {
result["thinkingConfig"] = { includeThoughts: true }
if (model.api.id.includes("gemini-3")) {
result["thinkingConfig"]["thinkingLevel"] = "high"
}
}
}
// 阿里云 DashScope:必须显式开启 enable_thinking
if (model.providerID === "alibaba-cn" && model.capabilities.reasoning) {
result["enable_thinking"] = true
}
return result
}17.5.2 Variants:统一的思考深度挡位
OpenCode 引入了 Variants 概念——将不同供应商的推理参数映射到统一的挡位名称(low | medium | high | max),使用户可以在不了解底层参数差异的情况下调节思考深度:
function variants(model: Provider.Model): Record<string, Record<string, any>> {
switch (model.api.npm) {
case "@ai-sdk/openai":
// low → reasoningEffort: "low", high → reasoningEffort: "high"
return Object.fromEntries(
efforts.map(e => [e, { reasoningEffort: e, reasoningSummary: "auto" }])
)
case "@ai-sdk/anthropic":
// high → budgetTokens: 16000, max → budgetTokens: 31999
return {
high: { thinking: { type: "enabled", budgetTokens: 16000 } },
max: { thinking: { type: "enabled", budgetTokens: 31999 } },
}
case "@ai-sdk/google":
// high → thinkingBudget: 16000, max → thinkingBudget: 24576
return {
high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
max: { thinkingConfig: { includeThoughts: true, thinkingBudget: 24576 } },
}
}
}17.5.3 计费差异
不同供应商的 token 计价模型存在三个维度的差异:
维度一:token 类别。部分供应商将 thinking token 单独计价:
- Anthropic:input + output(thinking token 计入 output)
- OpenAI:input + output(reasoning token 计入 output,通过
include: ["reasoning.encrypted_content"]可缓存推理内容以降低后续调用成本) - Google:input + output(thoughts token 不计费)
维度二:缓存折扣。Provider 抽象层需要跟踪 cache hit 率以优化成本:
cost: {
input: 3.0, // $3 / M tokens
output: 15.0, // $15 / M tokens
cache_read: 0.3, // 缓存读取 = 输入价 × 10%
cache_write: 3.75, // 缓存写入 = 输入价 × 125%
}OpenCode 的 applyCaching() 函数自动在消息中插入缓存控制标记,不同供应商使用不同的标记格式:
const providerOptions = {
anthropic: { cacheControl: { type: "ephemeral" } },
bedrock: { cachePoint: { type: "default" } },
openaiCompatible: { cache_control: { type: "ephemeral" } },
copilot: { copilot_cache_control: { type: "ephemeral" } },
}维度三:上下文长度定价。部分模型在超过特定上下文长度后加价:
experimentalOver200K: {
input: 6.0, // 超过 200K 上下文后 input 加倍
output: 30.0, // 超过 200K 上下文后 output 加倍
cache: { read: 0.6, write: 7.5 },
}17.5.4 模型特定的采样参数
不同模型对 temperature、top_p、top_k 的最优值不同。OpenCode 的 ProviderTransform 为每个模型族维护了推荐值:
function temperature(model: Provider.Model) {
const id = model.id.toLowerCase()
if (id.includes("qwen")) return 0.55
if (id.includes("claude")) return undefined // 不设置,使用 API 默认值
if (id.includes("gemini")) return 1.0
if (id.includes("kimi-k2") && id.includes("thinking")) return 1.0
if (id.includes("kimi-k2")) return 0.6
return undefined
}
function topP(model: Provider.Model) {
if (id.includes("qwen")) return 1
if (id.includes("minimax-m2") || id.includes("gemini")) return 0.95
return undefined
}这些看似微小的参数差异,在大规模生产使用中会显著影响输出质量和一致性。Provider 抽象层的价值之一就是将这些经验知识编码为可复用的配置。
graph TB
subgraph "Agent 业务层"
A[Agent Core] --> B[统一 Model 接口]
end
subgraph "Provider 抽象层"
B --> C{Provider 路由}
C --> D[参数翻译<br/>ProviderTransform]
C --> E[认证管理<br/>ProviderAuth]
C --> F[错误处理<br/>ProviderError]
D --> G[Variant 映射]
D --> H[消息规范化]
D --> I[缓存控制注入]
end
subgraph "SDK 适配层"
C --> J["@ai-sdk/anthropic"]
C --> K["@ai-sdk/openai"]
C --> L["@ai-sdk/google"]
C --> M["@ai-sdk/amazon-bedrock"]
C --> N["@ai-sdk/openai-compatible"]
C --> O["... 18+ SDKs"]
end
subgraph "LLM API"
J --> P["Anthropic API"]
K --> Q["OpenAI API"]
L --> R["Google API"]
M --> S["AWS Bedrock"]
N --> T["Any OpenAI-Compatible"]
end