处理不同 LLM Provider 的 Tool Schema 差异,靠的不是”写一个万能格式”,而是三层适配:先用统一 Schema 定义工具,再按 Provider 翻译成原生格式,最后对不兼容的关键字做定点清理。OpenClaw 按这套思路落地:API 适配器负责协议层翻译,toolSchemaProfile 做 Schema 归一化,unsupportedToolSchemaKeywords 处理端点级缺口。
一、差异到底差在哪里
OpenAI 用 tools 参数配 JSON Schema,Anthropic 用 tooluse 块,Gemini 用 functiondeclarations。三家的格式看着相似,细节处处不同:
| 差异点 | OpenAI | Anthropic | Gemini |
|---|---|---|---|
| 声明方式 | tools 数组内嵌 JSON Schema | tools 数组内嵌自有描述结构 | function_declarations |
| 结果回传 | tool role 消息 | tool_result 独立消息对象 | function_response |
| 约束支持 | 标准 JSON Schema 关键字 | 部分关键字支持有限 | 递归 Schema 支持有限 |
| 嵌套层级 | 支持多层嵌套 | 嵌套深度受限 | 递归/深层嵌套受限 |
差异横跨参数格式、嵌套深度、特殊类型约束与结果回传四层,例如有的端点不接受 pattern、minLength,有的对 maxLength 阈值敏感。
二、处理差异的三条路线
第一条,逐 Provider 各写一套工具定义,最直接但重复维护。第二条,统一中间格式加适配器翻译,开发者只写一份 Schema,运行时按 Provider 转换,这是主流做法。第三条,请求出口统一清理不兼容关键字,作兜底。OpenClaw 三样都用,分工明确。
接一个新 Provider,按下面四步走:
- 在 providers 节点下声明 provider 路由(api、baseUrl、apiKey),挂上模型;
- 在模型的 compat 块里声明能力(supportsTools、contextWindow 等);
- 能力声明解决不了的格式差异,用 toolSchemaProfile 选归一化 profile;
- profile 覆盖不到的缺口,用 unsupportedToolSchemaKeywords 定点移除关键字。
三、OpenClaw 的三层适配
3.1 API 适配器:协议层翻译
models.providers.*.api 决定协议层怎么转换。openai-completions 走 /v1/chat/completions,anthropic-messages 走 Anthropic 消息协议,google-generative-ai 与 google-vertex 走 Gemini 系,bedrock-converse-stream 走 AWS Bedrock。部分端点还能开 requiresOpenAiAnthropicToolPayload,把 OpenAI 形状的工具调用显式转成 Anthropic 系 payload。
3.2 toolSchemaProfile:Schema 归一化
toolSchemaProfile 内置两个 profile:llamacpp 和 gemini。llamacpp 针对 llama-server 把工具参数编译成 GBNF 语法的场景,移除 pattern 以及 ≥2000 的 maxLength 值,覆盖 cron 工具的 65536 长度限制。内置的 llama-cpp、ollama、lmstudio provider 自动应用清理器,自定义端点必须显式声明。
3.3 unsupportedToolSchemaKeywords:关键字级精修
profile 是定点缓解,不是对所有 JSON Schema 约束的完整兼容。剩下的缺口交给 unsupportedToolSchemaKeywords:Schema 发出前移除端点拒绝的关键字。旧版本没有 profile 时的兜底写法:
compat: {
unsupportedToolSchemaKeywords: [
"pattern", "patternProperties", "format", "propertyNames",
"uniqueItems", "contains", "minContains", "maxContains",
"minLength", "maxLength"
]
}
与 profile 的区别:它无条件移除所有列出项,llamacpp profile 只动 ≥2000 的 maxLength,更精准。
四、一个可跑的配置示例
把本地 llama-server 接成自定义 provider,是最典型的 Schema 适配场景:
{
agents: {
defaults: { model: { primary: "my-llamacpp/qwen35" } }
},
models: {
mode: "merge",
providers: {
"my-llamacpp": {
baseUrl: "http://127.0.0.1:8080/v1",
apiKey: "llamacpp-no-key",
api: "openai-completions",
models: [
{
id: "qwen35",
name: "Qwen3.5 (llama-server)",
contextWindow: 8192,
maxTokens: 2048,
compat: {
supportsTools: true,
toolSchemaProfile: "llamacpp"
}
}
]
}
}
}
}
漏掉 toolSchemaProfile,qwen35 的 cron 工具可能因 pattern 或超长 maxLength 被 llama-server 拒掉;补一行就解决。
五、两个落地注意点
内置 provider 的 compat 由 provider 目录管理,别往 config 里抄,openclaw doctor –fix 会自动清掉匹配的遗留覆盖。能力判定按端点而非内置 provider ID:Moonshot 原生端点就在共享 openai-completions 传输上声明了 streaming usage 兼容。自建适配照同一条原则:compat 只声明你真实验证过的能力。
常见问题(FAQ)
Q1:统一 Schema 用哪种格式定义?
推荐 OpenAI-compatible JSON Schema 作统一格式,其余 Provider 由适配器翻译。
Q2:toolSchemaProfile 和 unsupportedToolSchemaKeywords 怎么分工?
profile 做定点归一化,保留可用约束;后者无条件移除关键字,作兜底。
Q3:自定义 Provider 必须写 compat 吗?
必须。能力声明决定工具开关与格式转换,漏写会导致工具调用失败。