> ## Documentation Index
> Fetch the complete documentation index at: https://docs.playapi.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 工具选择与并行控制

> 使用 tool_choice 和 parallel_tool_calls 控制工具轮次，核实 strict 的转换限制。

## 选择策略与协议形状

| 意图 | Chat Completions | Responses |
| - | - | - |
| 由模型决定 | `tool_choice: auto` | `tool_choice: auto` |
| 请求使用工具 | `tool_choice: required` | `tool_choice: required` |
| 指定函数 | `type: function` 与嵌套 `function.name` | `type: function` 与同层 `name` |
| 禁止本轮工具 | `tool_choice: none` | `tool_choice: none` |
| 请求一轮最多一个函数调用 | `parallel_tool_calls: false` | `parallel_tool_calls: false` |

字段需要目标模型和渠道支持。Chat 的并行开关为可选布尔指针，显式 false 不等于省略；当前 Chat ↔ Responses 转换会映射布尔值及指定函数形状。Chat → Claude 会将 required 映射为 any、指定函数映射为 tool，并将关闭并行映射为 disable\_parallel\_tool\_use；这不是所有模型都接受所有组合的保证。

`parallel_tool_calls` 控制模型单轮输出，不替应用调度并发、不限制多个独立 HTTP 请求，也不表示网关执行本地函数。应用仍应检查实际调用数量、参数、名称与关联 ID。要求一个调用时收到多个，应先停止验证；不要先执行一部分再报错。内置搜索等服务端工具有独立限制。

## strict 与转换边界

原生 Chat 的函数声明可携带 `function.strict`，原生 Responses 使用同层 `strict`。当前两个方向的 Chat / Responses 函数工具转换都只复制名称、描述和参数，没有复制 strict 字段。原生请求、转换请求和上游默认严格策略因此不能视为等价；参数 schema 被保留也不证明严格模式生效。

这是函数工具声明的限制，不要与最终文本的 `response_format.json_schema.strict` / `text.format.strict` 混淆。无论 strict 是否保留，都要在应用侧校验业务参数。OpenRouter 的 require\_parameters 路由偏好、allowed\_tools、工具搜索和托管执行不能由这几个字段推导为项目通用功能。

## 有界的只读工具示例

脚本支持 `API_TOOL_POLICY=forced|required|auto|none`，默认 forced。第一轮按所选策略请求本地演示库存，始终发送 parallel\_tool\_calls=false；收到合法调用后回传匹配的 tool\_call\_id，第二轮切换到 none 请求最终答案。最多两次模型请求、一次本地只读计算，没有自动重试。

forced / required 缺少调用视为示例失败；auto 可以直接回答；none 收到调用会失败。所有参数必须通过检查才执行固定的演示查询，不动态分派函数，也不连接真实库存系统。保留完整助手消息用于续聊，不打印私有推理。第二轮失败不会重做调用；生产中会改变数据的工具仍需独立权限、去重和恢复设计。

输出包含回答和实际工具调用数；模型是否正确引用库存值需另行验收。示例依赖模型支持工具选择，不能通过 HTTP 200 认定策略已生效。

源码：`relaykit/dto/openai_request.go`、`relaykit/relayconvert/internal/oai_chat/to_oai_responses_req.go`、`relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go`、`relaykit/relayconvert/internal/shared/claude/tool_choice.go`。参见 [工具调用](/zh/guides/tool-calling)、[OpenRouter 客户端工具](https://openrouter.ai/docs/guides/features/tool-calling) 和 [OpenAI 工具说明](https://developers.openai.com/api/docs/guides/function-calling)。

## 可运行示例

在文档目录运行；复制脚本时一并保留 `http-client.mjs`，配置见 [REST 运行时](/zh/guides/http-runtime)。

```sh theme={null}
export API_ROOT_URL='https://gateway.example.com'
export API_KEY='YOUR_API_KEY'
export API_MODEL='YOUR_MODEL_ID'
export API_TOOL_POLICY='forced'
node examples/tool-policy.mjs
```

```javascript theme={null}
import { requestJSON } from './http-client.mjs';

try {
  const model = process.env.API_MODEL;
  const policy = process.env.API_TOOL_POLICY || 'forced';
  if (!model?.trim() || !['auto', 'required', 'forced', 'none'].includes(policy)) throw new Error('Invalid configuration.');
  const tools = [{ type: 'function', function: {
    name: 'get_stock', description: 'Read stock from the local demonstration catalog.', strict: true,
    parameters: { type: 'object', properties: { sku: { type: 'string', enum: ['NOTEBOOK'] } },
      required: ['sku'], additionalProperties: false },
  } }];
  const messages = [{ role: 'user', content: 'How many NOTEBOOK items are available? Use get_stock if permitted; otherwise say the stock is unknown.' }];
  let completed = false;
  let callCount = 0;
  for (let round = 0; round < 2; round++) {
    const toolChoice = round === 1 ? 'none' : policy === 'forced'
      ? { type: 'function', function: { name: 'get_stock' } } : policy;
    const result = await requestJSON('/v1/chat/completions', {
      model, messages, tools, tool_choice: toolChoice, parallel_tool_calls: false,
    });
    const choice = result.choices?.[0];
    const message = choice?.message;
    if (!Array.isArray(result.choices) || result.choices.length !== 1 || choice?.index !== 0
      || message?.role !== 'assistant' || message.refusal != null || message.function_call != null) {
      throw new Error('Invalid assistant result.');
    }
    const calls = message.tool_calls ?? [];
    if (!Array.isArray(calls) || calls.length > 1) throw new Error('Expected at most one call.');
    if (!calls.length) {
      if ((round === 0 && ['required', 'forced'].includes(policy)) || choice.finish_reason !== 'stop'
        || typeof message.content !== 'string' || !message.content.trim()) throw new Error('Missing final answer or required call.');
      console.log(JSON.stringify({ answer: message.content, tool_calls: callCount }));
      completed = true;
      break;
    }
    if (round !== 0 || policy === 'none' || choice.finish_reason !== 'tool_calls') throw new Error('Tool policy violated.');
    const call = calls[0];
    if (call?.type !== 'function' || typeof call.id !== 'string' || !call.id.trim()
      || call.function?.name !== 'get_stock' || typeof call.function.arguments !== 'string'
      || call.function.arguments.length > 4096) throw new Error('Invalid tool call.');
    const args = JSON.parse(call.function.arguments);
    if (!args || Array.isArray(args) || Object.keys(args).length !== 1 || args.sku !== 'NOTEBOOK') {
      throw new Error('Invalid catalog arguments.');
    }
    // Only this read-only fixture is executable. Preserve the original assistant message for replay.
    messages.push(message, { role: 'tool', tool_call_id: call.id, content: JSON.stringify({ sku: args.sku, available: 8 }) });
    callCount++;
  }
  if (!completed) throw new Error('No final answer within the example budget.');
} catch {
  console.error('Tool policy example failed. Check model support, call policy, arguments, and completion. No automatic retry.');
  process.exitCode = 1;
}
```

[Read in English](/en/guides/tool-controls)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.