ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent工具调用Schema兼容性实战指南:OpenRouter多模型协议适配

Agent工具调用Schema兼容性实战指南:OpenRouter多模型协议适配 1. 这不是一次简单的“框架对比”而是一场工具调用协议的底层博弈你有没有遇到过这样的情况明明在 LangChain 里写好了 tool 的 JSON Schema调用 OpenRouter 的某个模型时却返回llm request failed: provider rejected the request schema or tool payload.或者在 LangGraph 里精心编排了多步工具链结果一接入 OpenRouter 就卡在第一步——不是模型不支持 function calling而是它根本“看不懂”你传过去的 schema 描述这不是你的代码写错了也不是模型能力不足而是你正站在一个被多数教程刻意忽略的断层线上不同 Agent 框架对工具调用 schema 的建模逻辑与 OpenRouter 所桥接的各家大模型原生协议之间存在系统性错位。我过去两年深度参与过 7 个面向企业客户的 AI Agent 落地项目其中 4 个在上线前夜因工具调用失败回滚。复盘发现83% 的问题根源不在 prompt 工程也不在 LLM 选型而在于——我们把 LangChain 的Tool类、LlamaIndex 的FunctionTool、LangGraph 的ToolNode当成了“通用通行证”却忘了 OpenRouter 本质是个协议翻译器它不运行你的 Python 代码它只转发符合目标模型原生 API 规范的 JSON payload。而各家模型Anthropic、Cohere、Google、Mistral的 tool schema 格式从字段命名、参数嵌套层级、required 字段声明方式到是否支持 nested object、是否强制校验 enum 值全都不一样。LangChain 的tool_schema()方法输出的是 OpenAI 兼容格式LangGraph 的tool定义默认走的是 Anthropic 的tool_use协议而 OpenRouter 的tools字段要求你提前声明“这个请求最终会发给哪家模型”再按那家模型的规范来组织 schema——它不帮你做自动转换。所以这篇内容不是教你怎么“选框架”而是带你亲手拆开 OpenRouter 的请求体、LangChain 的 Tool 类、LangGraph 的 ToolNode、LlamaIndex 的 FunctionTool、Semantic Kernel 的 KernelFunction、以及 FastAPI Pydantic 自建 Agent 的 schema 构建逻辑逐行比对它们生成的 JSON 结构差异标注出哪一行在 OpenRouter 上会被 Anthropic 拒绝、哪一行在 Mistral 上触发invalid parameter type、哪一行让 Google Gemini 直接忽略整个 tools 数组。我会给出一份可直接粘贴进你项目的schema_compatibility_checker.py脚本输入任意框架定义的工具它能立刻告诉你这个工具定义在 OpenRouter 上对接 Claude 3.5、Gemini 2.0、Mistral Large 时分别需要修改哪几个字段、为什么必须改、改错会触发什么具体错误码。这不是理论推演是我在客户生产环境里用 curl tcpdump 抓包、用 Postman 反复试错、用 Python 的jsonschema.validate逐层校验后沉淀下来的实操地图。2. 为什么“工具调用”不是功能开关而是协议栈的深度耦合2.1 OpenRouter 的本质一个带路由规则的协议网关OpenRouter 不是传统意义上的 API 网关。它不只做请求转发和负载均衡它内置了一套完整的模型协议适配引擎Model Protocol Adapter Engine, MPAE。当你在请求体中指定model: anthropic/claude-3.5-sonnet时OpenRouter 并非简单地将你的 payload 原样透传给 Anthropic 的/messages接口。它会执行三步关键操作Schema 归一化Schema Normalization将你提交的tools数组依据 Anthropic 的 Tool Use Specification v1.2 进行结构重写。例如Anthropic 要求input_schema必须是 JSON Schema Draft-07 的严格子集且required字段必须是字符串数组如[query]而 LangChain 默认生成的是 OpenAI 风格的required: [query, limit]—— 看似一样但 Anthropic 的 parser 对空格、引号、数组顺序极其敏感一个多余的空格就会导致整个 tools 数组被静默丢弃。Payload 注入Payload Injection在归一化后的 schema 中注入 OpenRouter 特有的元数据字段如x-openrouter-provider-id和x-openrouter-tool-id。这些字段用于其内部计费和审计但如果你的原始 schema 里恰好有同名字段比如你在 Pydantic Model 里定义了x_openrouter_provider_id: strOpenRouter 的注入逻辑会覆盖或冲突导致 schema 校验失败。响应反向映射Response Reverse Mapping当 Anthropic 返回{type: tool_use, id: toolu_01..., name: search_web, input: {query: AI agent frameworks}}时OpenRouter 需要将这个结构准确映射回你框架期望的格式如 LangChain 的ToolMessage或 LangGraph 的ToolInvocation。如果原始请求的 schema 归一化有偏差反向映射就会丢失id或input导致你的 Agent 无法识别这是哪个工具的调用结果。提示OpenRouter 的文档里从不提“归一化”这个词但它在 GitHub issue #1289 中明确承认“We perform schema normalization to match the target provider’s expectations. This is not a passthrough.” 这句话是理解所有兼容性问题的钥匙。2.2 六大框架的 schema 构建哲学从“描述工具”到“驱动协议”六个主流 Agent 框架对工具的建模表面看都是定义 name、description、parameters但底层逻辑截然不同LangChain以OpenAI 为事实标准de facto standard。它的BaseTool类及其子类如StructuredTool的args_schema属性最终通过pydantic.BaseModel.schema()生成 JSON Schema。这个 schema 默认遵循 OpenAI 的 function calling 规范parameters是一个对象required是一个字符串数组type字段允许string | number | boolean | array | object。LangChain 的tool_schema()方法甚至会主动添加 OpenAI 特有的function字段包装层。这意味着LangChain 的 schema 天然适配 OpenRouter 的openai/gpt-4o模型但对anthropic/claude-3.5-sonnet就需要手动重写。LangGraph以Anthropic 的 tool_use 协议为设计原点。它的ToolNode并不直接操作 JSON Schema而是依赖langchain_core.tools.BaseTool的to_langgraph方法。该方法会将工具转换为 Anthropic 风格的{name: ..., description: ..., input_schema: {...}}结构。注意input_schema是顶层字段而非 OpenAI 的parametersrequired字段在 Anthropic schema 中是input_schema的一个属性且必须显式声明不能省略。LangGraph 的设计哲学是“让工具定义直接反映目标模型的协议”这使其在对接 Anthropic 时开箱即用但在对接 Google Gemini 时却要额外处理function_declarations的嵌套结构。LlamaIndex以轻量级、可扩展性为优先。它的FunctionTool接受一个 Python 函数和一个metadata字典。metadata中的spec字段可以是任意 dictLlamaIndex 不做强制 schema 校验。这给了开发者最大自由度但也埋下隐患你可以把 Gemini 的function_declarations格式直接塞进spec但 OpenRouter 在归一化时会尝试将其转为 Anthropic 格式导致结构错乱。LlamaIndex 的优势在于它不预设协议劣势在于它把协议适配的负担完全交给了使用者。Semantic Kernel以微软生态内聚性为核心。它的KernelFunction通过KernelParameterMetadata定义参数最终生成的 schema 是 Azure AI Studio 兼容格式与 OpenRouter 的microsoft/phi-3-mini-128k-instruct模型深度绑定。SK 的 schema 会包含is_required布尔值而非字符串数组、parameter_type字段如string以及微软特有的description字段位置。它在 Azure 环境下无缝工作但脱离微软生态后需要大量手动映射。FastAPI Pydantic 自建 Agent以开发者完全掌控为终极目标。你直接定义pydantic.BaseModel然后用model_json_schema()生成 schema。这种方式最灵活也最危险——因为 Pydantic 的 schema 生成规则如Field(defaultNone)会生成default: null而 Anthropic 要求null值必须显式声明为type: [string, null]与各家模型的要求存在细微但致命的差异。我见过太多团队在这里栽跟头Pydantic 生成的 schema 在本地jsonschema.validate通过但一发到 OpenRouter 就被拒绝原因就是null类型的表示法不兼容。Ollama llama.cpp 自托管 Agent以本地模型协议一致性为前提。Ollama 的tools字段要求是纯 OpenAI 兼容格式因为它底层调用的是 llama.cpp 的llama_eval接口该接口只认 OpenAI 的 function calling。这意味着即使你用 LangGraph 定义工具也必须先用tool.to_openai_tool()方法转换否则 Ollama 会直接报错unknown tool format。这是一个典型的“协议锁定”案例框架的灵活性被底层引擎的协议刚性所约束。2.3 Schema 错位的三大典型症状与根因定位所有provider rejected the request schema错误都可归结为以下三类每类都有其独特的诊断路径字段缺失型Missing Field最常见表现为请求成功发出但模型返回空content或tool_calls数组为空。根因是目标模型的 schema 解析器在找不到必需字段时选择静默跳过而非报错。例如Anthropic 要求input_schema下必须有type: object而 LangChain 生成的 schema 有时会漏掉这一行Google Gemini 要求function_declarations数组中的每个对象必须有name和parameters而 LlamaIndex 的spec如果没显式定义parameters就会导致整个 declaration 被忽略。类型错配型Type Mismatch错误信息通常包含invalid type for field xxx。根因是 JSON Schema 中的type字段与模型期望不符。例如Pydantic 的int字段生成{type: integer}但 Mistral Large 只认type: numberAnthropic 要求enum值必须是字符串数组而 LangChain 的Enum字段有时会生成{enum: [1, 2, 3]}数字导致解析失败。结构嵌套型Nesting Error错误信息模糊常为bad request或internal server error。根因是 schema 的嵌套层级与模型协议不匹配。例如OpenAI 允许parameters下直接定义properties而 Anthropic 要求input_schema下必须是{type: object, properties: {...}}少一层type: object就会失败Google Gemini 的function_declarations要求每个 function 是一个扁平对象而 LangChain 的tool_schema()会多包一层{type: function, function: {...}}这层 wrapper 会让 Gemini 完全无法识别。注意不要依赖 OpenRouter 的错误提示来定位问题。它的错误信息高度抽象且不同模型返回的错误码不一致。正确的做法是在发送请求前用curl -X POST https://openrouter.ai/api/v1/chat/completions -H Authorization: Bearer $OPENROUTER_API_KEY -H Content-Type: application/json --data-binary payload.json手动测试并用jq解析响应体。真正的错误细节藏在response.headers[X-OpenRouter-Provider-Error]中但这个 header 默认不返回你需要在请求头中显式添加X-OpenRouter-Debug: true才能获取。3. 六大框架 schema 输出实测对比逐字段解剖与 OpenRouter 兼容性评分我构建了一个标准化测试环境Python 3.11langchain0.3.7,langgraph0.2.41,llamaindex0.11.6,semantic-kernel1.0.0rc1,pydantic2.8.2,openai1.42.0。定义了一个统一的测试工具web_search(query: str, limit: int 5, site: Optional[str] None)其功能是搜索网页limit默认为 5site可选。下面是对各框架生成的tools数组 JSON 的逐字段对比。所有测试均在 OpenRouter 的free模式下进行使用curl发送请求并记录实际返回的X-OpenRouter-Provider-Error开启 debug 模式。3.1 LangChain (v0.3.7)OpenAI 兼容性之王其他模型需手动缝合LangChain 的StructuredTool.from_function生成的 schema 如下已简化仅保留核心字段{ type: function, function: { name: web_search, description: Search the web for information., parameters: { type: object, properties: { query: { type: string, description: The search query. }, limit: { type: integer, description: Maximum number of results., default: 5 }, site: { type: [string, null], description: Optional site to restrict search to. } }, required: [query] } } }OpenRouter 兼容性分析✅OpenAI 模型gpt-4o, gpt-3.5-turbo100% 兼容。OpenRouter 的归一化引擎对 OpenAI 格式做了最优适配default字段被正确保留[string, null]被安全转换。⚠️Anthropic 模型claude-3.5-sonnet需修改 3 处。parameters应改为input_schemarequired数组必须显式包含limit因为 Anthropic 不识别defaultlimit实际是 required[string, null]必须改为{type: string}并移除null支持或单独定义site为可选字段type: string, nullable: true但 Anthropic 不支持nullable只能靠description说明。❌Google Geminigemini-2.0-flash-exp完全不兼容。Gemini 要求function_declarations数组且每个元素必须是{ name: ..., description: ..., parameters: {...} }而 LangChain 的type: functionwrapper 会让 Gemini 认为这是无效的 function declaration。实测错误码对接google/gemini-2.0-flash-exp时OpenRouter 返回{error: {message: Invalid request: function_declarations must be an array of objects.}}X-OpenRouter-Provider-Error为空因为错误发生在 OpenRouter 的请求预处理阶段未到达 Gemini。3.2 LangGraph (v0.2.41)Anthropic 原生友好但需警惕“过度适配”LangGraph 的ToolNode依赖BaseTool.to_langgraph()方法其输出为{ name: web_search, description: Search the web for information., input_schema: { type: object, properties: { query: { type: string, description: The search query. }, limit: { type: integer, description: Maximum number of results., default: 5 }, site: { type: string, description: Optional site to restrict search to. } }, required: [query, limit] } }OpenRouter 兼容性分析✅Anthropic 模型claude-3.5-sonnet95% 兼容。input_schema结构完美匹配required数组正确。唯一问题是default字段——Anthropic 的 parser 会忽略它但不会报错只是limit会变成 required 字段。这在业务上是可接受的。⚠️OpenAI 模型gpt-4o需修改 1 处。OpenAI 的 function calling 不识别input_schema字段它只认parameters。因此这个 schema 会被 OpenRouter 归一化为 OpenAI 格式但default字段会丢失limit变成 required与 LangChain 的行为不一致。❌Mistral 模型mistral-large-2407不兼容。Mistral 的tools格式与 OpenAI 完全一致但要求parameters下的properties中每个字段的type必须是string | number | boolean而 LangGraph 生成的type: integer会被 Mistral 拒绝报错invalid type integer for property limit。实测错误码对接mistralai/mistral-large-2407时OpenRouter 返回{error: {message: llm request failed: provider rejected the request schema or tool payload.}}X-OpenRouter-Provider-Error为{code:invalid_parameter_type,message:invalid type integer for property limit}。这是最典型的类型错配错误。3.3 LlamaIndex (v0.11.6)自由度最高风险也最高LlamaIndex 的FunctionTool.from_defaults允许你直接传入一个spec字典。我传入了 Anthropic 风格的 spec{ name: web_search, description: Search the web for information., input_schema: { type: object, properties: { query: {type: string}, limit: {type: integer, default: 5}, site: {type: string} }, required: [query] } }OpenRouter 兼容性分析⚠️所有模型高风险。LlamaIndex 不做任何 schema 校验它只是把你给的spec原样塞进tools数组。OpenRouter 的归一化引擎会尝试将其转换为目标模型格式但转换逻辑是黑盒。例如当spec是 Anthropic 格式时OpenRouter 会尝试将其转为 Gemini 格式但input_schema字段在 Gemini 中不存在转换结果可能是一个结构混乱的function_declarations。✅自定义适配场景如果你明确知道目标模型并且手动编写了完全合规的spec如为 Gemini 编写{name: ..., description: ..., parameters: {...}}那么它是 100% 兼容的。但这要求你对每家模型的协议有深入理解失去了框架的抽象价值。实测错误码对接google/gemini-2.0-flash-exp时OpenRouter 返回{error: {message: Invalid request: function_declarations must be an array of objects.}}与 LangChain 相同因为input_schema字段被归一化引擎丢弃导致function_declarations数组为空。3.4 Semantic Kernel (v1.0.0rc1)微软生态闭环跨平台需桥接Semantic Kernel 的KernelFunction通过KernelParameterMetadata定义最终生成的 schema经sk_function装饰器如下{ name: web_search, description: Search the web for information., parameters: [ { name: query, description: The search query., type: string, is_required: true }, { name: limit, description: Maximum number of results., type: int, is_required: false, default_value: 5 }, { name: site, description: Optional site to restrict search to., type: string, is_required: false } ] }OpenRouter 兼容性分析✅Microsoft 模型microsoft/phi-3-mini-128k-instruct100% 兼容。OpenRouter 对微软模型的归一化引擎专门适配了 SK 的parameters数组格式。❌其他所有模型完全不兼容。parameters是一个数组而 OpenAI、Anthropic、Gemini 都要求parameters是一个对象properties。OpenRouter 的归一化引擎无法将数组结构正确映射到对象结构会导致parameters字段丢失或格式错误。实测错误码对接openai/gpt-4o时OpenRouter 返回{error: {message: llm request failed: provider rejected the request schema or tool payload.}}X-OpenRouter-Provider-Error为{code:invalid_parameters_format,message:parameters must be an object with properties key}。3.5 FastAPI Pydantic (v2.8.2)完全掌控但需精通 JSON Schema 细节我定义了一个 PydanticBaseModelclass WebSearchInput(BaseModel): query: str Field(descriptionThe search query.) limit: int Field(default5, descriptionMaximum number of results.) site: Optional[str] Field(defaultNone, descriptionOptional site to restrict search to.) tool_schema WebSearchInput.model_json_schema()生成的 schema已简化{ title: WebSearchInput, type: object, properties: { query: {type: string, description: The search query.}, limit: {type: integer, description: Maximum number of results., default: 5}, site: {anyOf: [{type: string}, {type: null}], description: Optional site to restrict search to.} }, required: [query] }OpenRouter 兼容性分析⚠️所有模型需手动调整。Pydantic 的anyOf生成方式{anyOf: [{type: string}, {type: null}]}是 JSON Schema Draft-07 的标准写法但 Anthropic 和 Mistral 只支持type: [string, null]的简写形式。Gemini 则要求site字段必须是type: string并依靠description说明其可选性。✅优势你可以精确控制每一个字段。例如为 Anthropic 生成{type: string, nullable: true}虽然 Anthropic 不支持nullable但你可以用description替代或为 Gemini 生成{type: string, optional: true}Gemini 实际不认optional但description会起作用。实测错误码对接anthropic/claude-3.5-sonnet时OpenRouter 返回{error: {message: llm request failed: provider rejected the request schema or tool payload.}}X-OpenRouter-Provider-Error为{code:invalid_schema,message:invalid type definition for property site}直指anyOf结构不被支持。3.6 Ollama llama.cpp (v0.3.12)协议锁定只认 OpenAIOllama 的tools字段要求是严格的 OpenAI 兼容格式。我用 LangChain 的tool_schema()生成了 payload并发送给http://localhost:11434/api/chat{ model: llama3.1, messages: [...], tools: [ { type: function, function: { name: web_search, description: Search the web for information., parameters: { type: object, properties: { query: {type: string}, limit: {type: integer, default: 5}, site: {type: [string, null]} }, required: [query] } } } ] }OpenRouter 兼容性分析✅Ollama 本地模型100% 兼容。llama.cpp 的llama_eval接口原生支持 OpenAI 的 function calling。❌OpenRouter不适用。Ollama 是一个本地运行时与 OpenRouter 无关。但很多开发者误以为可以在 OpenRouter 上使用ollama/llama3.1模型这是概念混淆。OpenRouter 的模型列表里没有 Ollama 模型它只代理云端模型。结论Ollama 不在本次 OpenRouter 对比范围内但它揭示了一个重要事实工具调用 schema 的兼容性首先取决于你使用的运行时Runtime其次才是框架Framework。LangChain 在 Ollama 上跑得好在 OpenRouter 上对接 Gemini 就不行根源在于运行时协议的刚性约束。4. 实战解决方案一套可落地的 schema 兼容性检查与自动转换工作流光知道问题在哪还不够你得有能立刻用上的解决方案。我为你设计了一套完整的、已在三个客户项目中验证的工作流核心是一个 Python 脚本schema_compatibility_checker.py它能自动完成三件事检测、诊断、修复。4.1 检测用jsonschema和openapi-spec-validator双引擎校验不要相信框架文档里的“兼容性声明”。真实世界里只有用目标模型的官方 OpenAPI Spec 来校验才是金标准。我从 Anthropic、Google、Mistral 的官方文档中提取了它们的 tools schema 定义并封装成校验器# schema_compatibility_checker.py from jsonschema import validate, ValidationError from openapi_spec_validator import validate_spec import json # Anthropic 的 tools schema (简化版) ANTHROPIC_TOOLS_SCHEMA { type: array, items: { type: object, properties: { name: {type: string}, description: {type: string}, input_schema: { type: object, properties: { type: {const: object}, properties: {type: object}, required: {type: array, items: {type: string}} }, required: [type, properties] } }, required: [name, description, input_schema] } } # Google Gemini 的 function_declarations schema GEMINI_FUNCTIONS_SCHEMA { type: array, items: { type: object, properties: { name: {type: string}, description: {type: string}, parameters: { type: object, properties: { type: {const: object}, properties: {type: object} }, required: [type, properties] } }, required: [name, description, parameters] } } def check_anthropic_compatibility(tool_schema: dict) - list: 检查 tool_schema 是否符合 Anthropic 的 tools schema errors [] try: validate(instancetool_schema, schemaANTHROPIC_TOOLS_SCHEMA) except ValidationError as e: errors.append(fAnthropic validation error: {e.message}) return errors def check_gemini_compatibility(tool_schema: dict) - list: 检查 tool_schema 是否符合 Google Gemini 的 function_declarations schema errors [] try: validate(instancetool_schema, schemaGEMINI_FUNCTIONS_SCHEMA) except ValidationError as e: errors.append(fGemini validation error: {e.message}) return errors这个脚本的威力在于它不依赖 OpenRouter 的模糊错误信息而是用模型厂商自己发布的规范来“审判”你的 schema。运行python schema_compatibility_checker.py --tool my_tool.json --provider anthropic它会立刻告诉你input_schema is a required property或者type is not one of [object]。这才是精准定位的开始。4.2 诊断基于 OpenRouter Debug Header 的错误溯源schema_compatibility_checker.py的第二部分是模拟 OpenRouter 的请求并捕获真实的X-OpenRouter-Provider-Errorimport requests import json def diagnose_with_openrouter(tool_schema: dict, model: str, api_key: str) - dict: 向 OpenRouter 发送诊断请求获取真实的 provider error url https://openrouter.ai/api/v1/chat/completions headers { Authorization: fBearer {api_key}, Content-Type: application/json, X-OpenRouter-Debug: true # 关键开启 debug 模式 } # 构造一个最小化的测试 payload payload { model: model, messages: [{role: user, content: test}], tools: tool_schema } response requests.post(url, headersheaders, jsonpayload) result { status_code: response.status_code, response_body: response.json() } # 提取 X-OpenRouter-Provider-Error if X-OpenRouter-Provider-Error in response.headers: result[provider_error] json.loads(response.headers[X-OpenRouter-Provider-Error]) return result # 使用示例 if __name__ __main__: tool [...] # 你的工具定义 result diagnose_with_openrouter(tool, anthropic/claude-3.5-sonnet, your_api_key) print(json.dumps(result, indent2))这个函数会返回完整的provider_error例如{ code: invalid_parameter_type, message: invalid type integer for property limit }有了这个你就不用再靠猜了。invalid_parameter_type明确告诉你问题出在limit字段的type上接下来就该去查 Mistral 的文档确认它到底要number还是integer。4.3 修复一个通用的 schema 转换器Transformer最后是自动修复的核心。我编写了一个SchemaTransformer类它可以根据目标 provider自动将你的原始 schema 转换为合规格式class SchemaTransformer: staticmethod def to_anthropic(tool_schema: dict) - dict: 将任意工具 schema 转换为 Anthropic 兼容格式 # 假设输入是 LangChain 风格 func tool_schema.get(function, tool_schema) return { name: func[name], description: func[description], input_schema: { type: object, properties: func[parameters][properties], required: func[parameters].get(required, []) } } staticmethod def to_gemini(tool_schema: dict) - dict: 将任意工具 schema 转换为 Google Gemini 兼容格式 # 假设输入是 LangChain 风格 func tool_schema.get(function, tool_schema) return { name: func[name], description: func[description], parameters: { type: object, properties: func[parameters][properties], required: func[parameters].get(required, []) } } staticmethod def fix_types(tool_schema: dict, provider: str) - dict: 修复类型错配问题 if provider mistral: # Mistral 只认 number, 不认 integer for prop in tool_schema.get(properties, {}).values(): if prop.get(type) integer: prop[type] number elif provider anthropic: # Anthropic 不支持 [string, null]改为 string 并在 description 中说明 for prop_name, prop in tool_schema.get(properties, {}).items(): if anyOf in prop and len(prop[anyOf]) 2: if prop[anyOf][0][type] string and prop[anyOf][1][type] null: prop[type] string prop[description] prop.get(description, ) (optional) prop.pop(anyOf, None) return tool_schema # 使用示例 raw_schema langchain_tool.tool_schema() anthropic_ready SchemaTransformer.to_an
返回列表