OpenRouter 的工具调用怎么写:跨供应商的统一层在哪
一、真正让人头疼的不是写 tools,是换模型之后
写工具调用本身不难:给一个 JSON 描述,模型回一个 tool_calls,你在本地跑函数,再把结果塞回去。麻烦的是同一份代码换个模型跑会不会照旧——arguments 能不能解析、返回的 function.name 在不在你给的 tools[] 里、参数值合不合你写的 schema。这三种情况并非我们的观察,而是官方文档在讲工具调用成功率时列为三类错误的东西,后面第四节会逐条对上号。
OpenRouter 官方文档《Tool & Function Calling》页(openrouter.ai/docs/guides/features/tool-calling)开篇就把职责说清楚了:LLM 不会自己去调工具,它只是建议调用哪个工具;调用由你在本地完成,结果再交回模型,模型最后把结果整理成回答。同一页写明,OpenRouter 在模型与供应商之间标准化了工具调用接口。
这篇就落在两件事上:工具定义里到底有哪些字段、各自是什么语义;以及跨供应商的差异,官方文档说是在哪几层上处理的。
二、前置条件
- 模型要支持 tools。 文档给出的筛选入口是
openrouter.ai/models?supported_parameters=tools。这是一个会随平台变动的动态列表,本文不抄任何模型清单,请自己去那个入口筛。 - 接口地址。 文档 Python 示例里的
base_url是https://openrouter.ai/api/v1;fetch示例请求的是https://openrouter.ai/api/v1/chat/completions。 - 密钥。 文档示例里用占位符表示密钥值,实际写法请用你自己的
<YOUR_API_KEY>,不要硬编码进仓库。 - 客户端二选一。 文档同时给了 TypeScript SDK(
import { OpenRouter } from '@openrouter/sdk')、Python 里直接用OpenAI客户端把base_url指过来、以及裸fetch三种写法。 - 额外开关这件事。 这一页没有提到工具调用需要单独申请或开通什么;除此之外还有没有别的前置动作,官方文档没有说明这一点,别当成”确认不需要”来用。
关于环境变量:把密钥放进环境变量而不是写在代码里,是通用工程做法,不是 OpenRouter 官方文档的内容。Windows 侧在 PowerShell 里用 $env: 设置当前会话变量、用 setx 写入用户级变量;Linux/macOS 侧用 export。两边的持久化行为不一样,Windows 上 setx 写完要重开终端才生效。
三、三步走:官方文档写明的请求形态
文档把工具调用拆成三步,并给了每一步的请求体骨架。
第一步,带 tools 发推理请求。 工具定义放在顶层 tools 数组里,每个元素的结构是固定的:
{
"type": "function",
"function": {
"name": "search_gutenberg_books",
"description": "Search for books in the Project Gutenberg library",
"parameters": {
"type": "object",
"properties": {
"search_terms": {
"type": "array",
"items": {"type": "string"},
"description": "List of search terms to find books"
}
},
"required": ["search_terms"]
}
}
}
逐字段说一下在改什么:type 目前在示例里都是 function;function.name 是模型回传时用来指路的键,你本地要拿它去查函数映射表(文档示例里叫 TOOL_MAPPING);function.description 是给模型看的用途说明;function.parameters 是一份 JSON Schema,properties 描述每个入参,required 列出必填项。文档在「Function Definition Guidelines」一节里额外演示了 enum 与 default 两个键——enum 限定取值范围,default 写明缺省值,并在参数的 description 里直接给示例值。同一节的建议是名字要具体(示例里用 get_weather_forecast 而不是 weather),描述要写全模型判断”什么时候用、怎么用”所需要的信息。
第二步,本地执行。 模型回来的响应里 finish_reason 是 tool_calls,同时带一个 tool_calls 数组。文档特意提醒:通用的响应处理逻辑里,应当先检查 finish_reason 再去处理工具调用,示例为了简短跳过了这一步。数组里每一项有 id、type、function.name 和 function.arguments,其中 arguments 是一个字符串,不是对象,要自己 JSON.parse / json.loads。
第三步,把结果发回去。 消息数组里要追加两条:模型那条带 tool_calls 的 assistant 消息(文档在示例注释里写了一句 “It’s easy to forget this step!”,这一步确实最容易漏),以及一条 role 为 tool 的消息,带 tool_call_id 和 content:
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "[{\"id\": 4300, \"title\": \"Ulysses\", \"authors\": [{\"name\": \"Joyce, James\"}]}]"
}
这里有一条容易被忽略的硬要求,文档用加粗的 Note 单独写了:tools 参数必须出现在每一次请求里(第一步和第三步都要),原因是 router 要在每一次调用上校验工具 schema。也就是说第二轮不能因为”模型已经知道了”就省掉 tools——这不是模型的需要,是路由层的需要。
tool_choice 控制要不要调、调哪个。API 参考页(openrouter.ai/docs/api_reference/parameters)写明它可以是字符串或对象,取值语义是:none 表示不调任何工具、直接生成消息;auto 表示模型自己在”生成消息”和”调一个或多个工具”之间选;required 表示必须调至少一个工具;传 {"type": "function", "function": {"name": "my_function"}} 则强制调用指定的那个。工具调用页把 auto 标注为默认。
parallel_tool_calls 控制能不能一次抛多个。API 参考页写明它是布尔值,默认 true(这是文档写明的默认值,随版本可能变动,也不等于每个模型的实际表现),设为 false 时函数会顺序调用,且该参数只在提供了 tools 时生效。工具调用页对此的补充是:false 时模型一次只请求一个工具调用。
流式场景下,文档示例是从 data.choices[0].delta.tool_calls 里累积分片,再按 finish_reason 分流处理。文档还给了一个最简 agentic loop:循环里调模型、有 toolCalls 就把工具结果压回消息数组、没有就 break,外面套一个最大迭代次数做兜底(示例里那个上限值只是文档的示例取值,不是推荐配置)。
以上为按官方文档中的参数语义组合的示例,未经实测,以官方文档与 --help 的实际输出为准。
四、跨供应商差异,官方文档说是在三层上处理的
这是本文真正想说清楚的部分。
第一层:请求形状的转换。 API 参考页对 tools 的说明只有一句,但信息量不小:该参数遵循 OpenAI 的工具调用请求形状,对于非 OpenAI 的供应商会做相应转换。也就是说你按上面那套 JSON 写就行,转换在平台侧发生。文档没有说明转换的具体规则,也没有列出哪些字段在转换中可能被丢弃——这一点官方文档没有说明。
第二层:把不支持的供应商排除在候选之外。 供应商路由页(openrouter.ai/docs/guides/routing/provider-selection)写明,默认路由策略下,不支持你请求中某些参数的供应商仍然可能收到这个请求,只是会忽略它不认识的参数——这就是”我明明传了 tools,却什么都没发生”的一种可能来源。把 provider.require_parameters 设为 true(该字段类型为 boolean,文档写明默认 false),请求就压根不会路由到那种供应商。
同一页还有一条更细的规则,叫「Default parameter preferences」:即使 require_parameters 是 false,也有一小组参数被当作软偏好参与供应商选择,tools 正是其中之一(另外两个是 response_format、verbosity)。文档写明的行为是:同一个模型下如果有的供应商支持、有的不支持,请求只会路由到支持的那些;如果这个模型的供应商全都不支持,请求仍然会发到该模型,参数被忽略。文档同时强调,这条偏好不会把模型从候选列表里移除。翻译成人话:tools 天然享有一点优待,但优待不是保证,兜底还得靠 require_parameters。
第三层:按工具调用的成功率重排供应商。 《Auto Exacto》页(openrouter.ai/docs/guides/routing/auto-exacto)写明,这是一个路由步骤,对所有带 tools 的请求默认生效,无需配置,它会用真实运行信号给供应商重新排序,表现差的往后放。信号来源包括吞吐、工具调用成功率、以及 OpenRouter 自有基准测试的结果。
工具调用成功率是怎么算的,这一页写得很实在,也直接影响你的 schema 该怎么写:
- 校验器:工具调用返回的
arguments,会用@cfworker/json-schema对照你在tools[].function.parameters里给的 schema 做校验,并且固定在 JSON Schema Draft 7。 - schema 缺失时从宽:
parameters不存在或编译不通过的工具,会被当作”没有 schema”,一律视为有效。文档自述这样做是为了在调用方 schema 写坏时让指标保持保守。 - 错误分类:每次工具调用被归入三类错误之一或算作有效——
InvalidJson(arguments解析失败)、UnknownName(function.name不在请求的tools[]里)、SchemaMismatch(校验器判定不通过)。 - 统计口径按请求算:一个请求里只要有任意一次工具调用落进上面三类,整个请求就被记为出错;分子分母都是请求数,不是工具调用数。
两条 caveat 也要照实标出来:只在 Draft 2019-09 / 2020-12 才引入的关键字(文档举了 unevaluatedProperties、$dynamicRef)在 Draft 7 下不会被强制执行;正则方面,pattern 与 patternProperties 交给运行时原生的 JavaScript RegExp,没有额外的一致性垫片,所以边缘情况下的语义与 JSON Schema 规范引用的正则方言存在差异。你写 schema 时可以据此避开这两片区域。
不想要这层重排也可以退出。文档给了三种方式,都是”显式按价格排序”:在请求体 provider 对象里把 sort 设为 "price";在模型 slug 后追加 :floor 这个虚拟变体;或者在账号设置里把默认供应商排序设为价格。任意一种都会绕过 Auto Exacto,回到默认的价格加权排序。这里只讲机制,具体排到哪一档、多少钱,本文不写。
五、边界:哪些地方文档没兜底
- 模型能力差异。文档在「Implementation Considerations」里明说,交错思考(Interleaved Thinking,即模型在多次工具调用之间穿插推理)会增加 token 用量与响应延迟,且推理质量取决于模型自身能力、有些模型更适合这种用法。它没有承诺任何模型的工具调用质量。
- 代码示例里的 model slug 如
google/gemini-3-flash-preview、anthropic/claude-sonnet-4.5只是官方文档当时的示例值,平台上有哪些模型随时在变,不要当清单用。 - 文档自身的不一致,得提前知道。 同一页里,JSON 请求体用的是下划线风格的
tool_call_id,而 TypeScript SDK 示例里写的是toolCallId;同样,一处用response_1.tool_calls,agentic loop 那段用的是response.choices[0].message.toolCalls。TypeScript SDK 示例还从arguments里解构出search_params,而它自己的 schema 里定义的键叫search_terms。这几处摆在一起就是不一致,官方文档没有解释哪个才是当前口径——照抄之前请以你所用 SDK 的类型定义为准。 - 转换规则、各供应商对
tool_choice各取值的落实程度、parallel_tool_calls在不同模型上的实际行为,官方文档没有说明这一点。
该平台迭代频繁,文中涉及的字段、默认值与路由行为随版本变动,请以官方文档最新内容为准。
六、怎么确认自己配对了
- 看
finish_reason。 第一轮响应的finish_reason是不是tool_calls。不是的话,说明模型压根没打算调工具,问题多半在description写得太含糊或tool_choice被设成了none。 - 自己先跑一遍 Draft 7 校验。 拿模型返回的
arguments字符串,按你给出的parametersschema、用 Draft 7 校验一遍。三类错误里InvalidJson和SchemaMismatch在本地就能复现,UnknownName则对照tools[]里的名字查。 - 确认每一轮都带上了
tools。 抓一下第三步那个请求的请求体,看tools是不是还在。 - 想排除供应商差异这个变量时,把
provider.require_parameters设为true再跑一次,看现象是否变化。 - 看模型页的 Performance tab。 官方文档写明,Tool Call Error Rate 与吞吐会展示在每个模型页的 Performance 标签页上,Auto Exacto 的基准分数则在「AutoExacto Benchmarks」卡片里。我们没有对这些页面做过查看,只是转述文档写明的位置。
本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。
该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现;
我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。
该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单;
价格、额度与限流的具体数值请以官方定价页与用量说明为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。