> ## Documentation Index
> Fetch the complete documentation index at: https://opc-43d279b8.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Tool Calling：Schema、运行时与故障定位

> 工具 Schema 设计、strict 约束、并行调用与线上排障面试考点

> 工具调用是 Agent 最基础也最容易翻车的一层：Schema 怎么设计、工具多了怎么选、参数错了怎么定位，这三件事我在线上都踩过。

***

## Q1: 一次 Tool Calling 的完整链路和不变量是什么？

```text theme={null}
① 定义      把工具 Schema 与描述放进模型上下文
     │
② 选择      模型决定是否调用、调用哪个
     │
③ 生成参数  模型按 Schema 生成 arguments（可能流式）
     │
④ 校验      Harness 做 Schema / 权限 / 策略校验
     │
⑤ 执行      调用真实工具（可能并行、可能超时）
     │
⑥ 回灌      把结果作为 tool message 写回上下文
     │
⑦ 再决策    模型继续调用或收尾
```

每一段都有自己的不变量：

| 环节 | 不变量 | 破坏后的表现 |
| - | - | - |
| 定义 | 工具名唯一、描述互斥、Schema 合法 | 不调用、选错、参数乱编 |
| 选择 | 单次可见工具数量可控 | 准确率随工具数下降 |
| 参数 | 生成结果符合 Schema | 运行时 400、类型错误 |
| 校验 | 模型输出永不直接执行 | 越权、注入、路径穿越 |
| 执行 | 写操作幂等、超时可控 | 重复副作用、线程挂死 |
| 回灌 | 结果有界、结构稳定、错误可行动 | 上下文爆炸、循环重试 |
| 再决策 | 有步数上限与卡死检测 | 无限循环、任务半途而废 |

一次真实的请求长这样（OpenAI Chat Completions 风格）：

```json theme={null}
{
  "model": "gpt-4.1",
  "messages": [
    { "role": "user", "content": "帮我查一下 A1024 这个订单到哪了" }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_order",
        "description": "按订单号查询订单状态与物流。仅用于用户自己的订单；统计类问题请用 report_orders。",
        "parameters": {
          "type": "object",
          "properties": {
            "orderId": {
              "type": "string",
              "description": "订单号，格式如 A1024"
            }
          },
          "required": ["orderId"],
          "additionalProperties": false
        },
        "strict": true
      }
    }
  ],
  "tool_choice": "auto",
  "parallel_tool_calls": true
}
```

```json theme={null}
{
  "choices": [
    {
      "finish_reason": "tool_calls",
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_9f2",
            "type": "function",
            "function": {
              "name": "get_order",
              "arguments": "{\"orderId\":\"A1024\"}"
            }
          }
        ]
      }
    }
  ]
}
```

**第一个该看的就是 `finish_reason`。** 它是 `tool_calls` 还是 `stop`，决定了你后面查"选择"还是查"生成"。

***

## Q2: 工具 Schema 应该怎么设计？

Schema 不是类型声明，是**给模型看的接口契约**。分四层设计：

### 第一层：命名

| 原则 | 差 | 好 |
| - | - | - |
| 动词 + 名词 | `order` | `get_order` |
| 带领域前缀 | `search` | `search_orders` |
| 避免泛化 | `query`、`lookup`、`search` | `search_docs`、`lookup_user` |
| 稳定 | 频繁改名 | 名字即 API，改名等于破坏缓存和评测 |

### 第二层：描述

描述要回答五个问题，而不是一句"查询订单"：

```text theme={null}
何时用：用户询问订单状态、物流、金额时
何时不用：统计、报表、批量导出不要用
输入来源：orderId 来自用户原话或会话上下文，不要自行编造
返回什么：状态、金额、物流节点；不返回用户隐私字段
副作用与幂等：只读；重复调用安全
```

### 第三层：结构

```json theme={null}
{
  "type": "object",
  "properties": {
    "orderId": {
      "type": "string",
      "pattern": "^[A-Z][0-9]{4,10}$",
      "description": "订单号，字母开头 + 4-10 位数字，例如 A1024"
    },
    "includeLogistics": {
      "type": "boolean",
      "default": false,
      "description": "是否返回物流节点。默认 false，可减少返回体积"
    }
  },
  "required": ["orderId"],
  "additionalProperties": false
}
```

结构设计经验：

* **扁平优先。** 两层以上的嵌套对象会显著提高参数生成错误率；能用 `userId` + `orderId` 就别传一个 `filter` 对象。
* **枚举优于自由字符串。** `status` 给 `["paid","shipped","refunded"]`，不要让模型猜。
* **数量有边界。** `limit` 写明默认值和最大值，避免一次拉十万条。
* **单位写进字段名或描述。** `amountCents` 比 `amount` 少一类事故。
* **时间用 ISO 8601 并给例子。** 时区必须明确。
* **避免 `anyOf` / `oneOf` / 深层 `$ref`。** 各家对 JSON Schema 子集支持不同，复杂模式在 strict 模式下可能直接编译失败。

### 第四层：负面约束

同一个问题有两个工具都能答，是最常见的选错原因：

```text theme={null}
差：两个工具都"能查"
{ "name": "search", "description": "搜索信息" }
{ "name": "lookup", "description": "查找数据" }

好：用"何时不用"划边界
{
  "name": "web_search",
  "description": "用于获取实时外部信息。不要用于用户已提供的事实，也不要用于内部知识库。"
}
{
  "name": "kb_search",
  "description": "用于查询公司内部知识库。不要用于实时新闻、价格或外部网页。"
}
```

### Schema 演进规则

| 变更 | 是否安全 | 说明 |
| - | - | - |
| 新增可选字段 | 安全 | 老调用方不受影响 |
| 新增必填字段 | 破坏性 | 需要新工具名或版本化 |
| 改字段类型 | 破坏性 | 模型与执行器都会错 |
| 改枚举值 | 半破坏 | 删值破坏，加值通常安全 |
| 改描述 | 行为变化 | 不算破坏，但会改变选择分布，必须回归评测 |

> [!NOTE]
>
> Schema 的目标是"让模型在正确的时机、用正确的参数、只调用一次"，"能通过校验"只是最低要求。

***

## Q3: strict 模式、JSON Mode、结构化输出有什么区别？

三者经常被混为一谈，但约束点完全不同：

| 机制 | 约束什么 | 生效时机 | 典型失败 |
| - | - | - | - |
| JSON Mode | 输出是合法 JSON | 生成时 | 合法 JSON 但字段不对 |
| Structured Outputs | 输出符合给定 Schema | 生成时（约束解码） | Schema 用了不支持的子集 |
| strict tool use | 工具名与参数符合 Schema | 生成时（语法约束采样） | 复杂 Schema 编译失败 |
| 运行时校验 | 参数符合业务规则 | 执行前 | 只能发现，不能预防 |

OpenAI 的函数工具支持 `strict`，Anthropic 的工具定义支持 `strict: true`，其机制是**语法约束采样**（grammar-constrained sampling）：把 Schema 编译成语法，采样时只允许生成合法 token 序列。Anthropic 的文档明确要求所有对象加 `additionalProperties: false`，并只支持 JSON Schema 的一个子集。

```json theme={null}
// Anthropic 风格
{
  "name": "book_flight",
  "description": "预订航班",
  "strict": true,
  "input_schema": {
    "type": "object",
    "properties": {
      "destination": { "type": "string" },
      "passengers": { "type": "integer", "enum": [1, 2, 3, 4] }
    },
    "required": ["destination", "passengers"],
    "additionalProperties": false
  }
}
```

### 三层防线，不要只靠一层

```text theme={null}
strict / 约束解码   → 保证"形状对"
运行时 Schema 校验  → 保证"版本对、子集对"
业务规则校验        → 保证"语义对、权限对、状态对"
```

strict 不能替代后两层：它保证 `passengers` 是整数，但保证不了这个用户有没有权限订 4 张票；它保证 `orderId` 是字符串，但保证不了这个订单属于当前用户。

### 常见坑

* **Schema 太复杂导致被拒。** 深层嵌套、`oneOf`、递归 `$ref` 在部分 Provider 上不支持 strict。
* **strict 打开后模型"变笨"。** 约束越强，模型越难表达"我需要更多信息"，可能直接编一个合法但错误的值。此时应该在工具之外提供澄清路径（Elicitation 或普通提问）。
* **把 JSON Mode 当 Schema。** `{"type":"json_object"}` 只保证是 JSON，不保证字段。
* **忽略 `additionalProperties`。** 不写 `false` 时模型可能塞额外字段，执行器若做宽松解析，会把错误藏起来。

***

## Q4: 工具太多怎么办？动态工具与 tool search 怎么落地？

工具数量与选择准确率并非线性关系：过了某个点会开始明显退化。Anthropic 在 Tool Search 文档里给的经验是：**可用工具超过 30–50 个后选择能力下降**；一个典型的多 Server 配置（GitHub、Slack、Sentry、Grafana、Splunk）光工具定义就能占约 55k tokens。

三种解法，成本依次上升：

| 方案 | 做法 | 优点 | 代价 |
| - | - | - | - |
| 静态裁剪 | 按租户/场景只暴露相关工具 | 简单、可预测 | 需要人工维护映射 |
| 分组 / 命名空间 | `crm.*`、`billing.*`，或按子 Agent 拆分 | 降低单次选择空间 | 多一层路由 |
| Tool Search | 工具定义延迟加载，先检索再展开 | 可扩展到成百上千 | 检索质量成为新瓶颈 |

### Tool Search 的工作方式

以 Anthropic 的实现为例（字段名以官方文档为准）：

```json theme={null}
{
  "tools": [
    { "type": "tool_search_tool_regex_20251119", "name": "tool_search" },
    {
      "name": "search_files",
      "description": "在工作区搜索文件",
      "input_schema": {
        "type": "object",
        "properties": { "query": { "type": "string" } }
      },
      "defer_loading": true
    }
  ]
}
```

* `defer_loading: true` 的工具不进入初始上下文；
* 模型先调用检索工具，服务端返回 `tool_reference` 块（默认最多 5 个），再自动展开成完整定义；
* 至少保留一个非延迟工具（通常就是检索工具本身），否则模型没有入口。

OpenAI 的 Responses API 也提供了对应机制：工具可以设 `deferLoading` 被检索发现，并可用 `allowedCallers` 区分 `DIRECT` 与 `PROGRAMMATIC` 调用，用 namespace 做分组。

### 落地要点

1. **描述就是索引。** 检索质量取决于工具名、描述、参数描述，不是取决于向量库多高级。写描述时把用户可能的说法放进去。
2. **保留一个 always-on 工具。** 检索工具本身、或一个 `ask_user` / `list_capabilities`。
3. **检索结果要可解释。** 记录命中了哪些工具、为什么，否则线上只能看到"模型调了个奇怪的函数"。
4. **工具定义变化会破坏 prompt cache。** 动态挂载要按会话稳定，不要每轮都改工具列表。
5. **评测要覆盖"该检索却没检索"和"检索到错的工具"两类错误。**

### 与 MCP 的关系

MCP 的 `tools/list` 解决"有哪些工具"，Tool Search 解决"这次该让模型看到哪些工具"。二者可以叠加：网关先按租户裁剪 `tools/list`，Harness 再按任务做一次检索。**不要把上千个 MCP 工具直接全量塞进模型上下文**，这是线上最常见的上下文浪费。

***

## Q5: 并行调用什么时候安全？结果怎么配对？

模型在一轮里返回多个 `tool_calls` 就是并行调用。OpenAI 用 `parallel_tool_calls` 控制，Anthropic 在同一轮返回多个 `tool_use` 块。

### 可以并行的条件

```text theme={null}
无数据依赖  AND  只读或幂等  AND  有独立限流预算
```

只要有一条不满足，就应该串行或分批：

| 场景 | 能否并行 | 原因 |
| - | - | - |
| 同时查三个城市的天气 | 可以 | 无依赖、只读 |
| 查用户 → 用 userId 查订单 | 不可以 | 有依赖，必须分轮 |
| 同一账户扣款两次 | 不可以 | 写操作，竞态 |
| 批量给 100 个用户发券 | 分批 | 需要限流与部分失败处理 |
| 读文件 + 读数据库 + 调搜索 | 可以 | 独立资源 |

### 结果配对

```typescript theme={null}
const calls = response.tool_calls ?? [];

const results = await Promise.allSettled(
  calls.map((call) =>
    executeTool(call.function.name, JSON.parse(call.function.arguments)),
  ),
);

const toolMessages = calls.map((call, i) => {
  const r = results[i];
  return {
    role: "tool",
    tool_call_id: call.id, // 必须回填原始 id
    content:
      r.status === "fulfilled"
        ? JSON.stringify(r.value)
        : JSON.stringify(toToolError(r.reason)),
  };
});
```

三个细节：

* **必须用 `tool_call_id` 配对，不能靠顺序。** 并行完成顺序和返回顺序不一定一致。
* **`Promise.allSettled` 而不是 `Promise.all`。** 一个失败不应该丢掉其他结果；模型需要看到"哪些成功、哪些失败"。
* **并行度要有上限。** 例如 `p-limit(5)`，否则 20 个调用会同时打爆下游。

### Provider 差异

| 项目 | OpenAI 风格 | Anthropic 风格 |
| - | - | - |
| 工具定义 | `{type:"function", function:{...}}` | `{name, description, input_schema, strict?}` |
| 并行开关 | `parallel_tool_calls` | 模型自行决定，可返回多个 `tool_use` |
| 结果回灌 | 每个调用一条 `role:"tool"` 消息 | 同一轮所有 `tool_result` 放在**同一条 user 消息**里 |
| 结果顺序 | 按 id 配对 | `tool_result` 必须排在文本之前 |
| 思考块 | 视模型而定 | 回传时不能修改 thinking 块 |

> [!WARNING]
>
> 同一轮多个工具结果必须放在一条 `user` 消息的 `tool_result` 块里。分成多条消息，会被判定为"缺少工具结果"，轻则行为异常，重则直接请求失败。另外 `tool_result` 要排在文本块之前。

***

## Q6: 工具结果回灌时，怎么防止上下文被撑爆？

工具结果是上下文膨胀的第一大来源：一次 `cat` 可能返回几十万字符，一次搜索可能返回 200 条结果。

### 预算视角

给每类工具设一个**结果预算**，而不是等上下文满了再救：

| 工具类型 | 建议上限 | 超出后的策略 |
| - | - | - |
| 单条查询 | 2–4 KB | 只保留决策字段 |
| 列表搜索 | 8–16 KB | 截断 + 总数 + 分页游标 |
| 文件读取 | 16–32 KB | 保留头尾 + 行号范围 |
| 日志 | 8 KB | 只保留错误段 + 时间窗 |
| 二进制/图片 | 不内联 | 返回 resource link 或缩略描述 |

### 截断策略

```typescript theme={null}
function truncateMiddle(text: string, maxBytes = 32_000) {
  if (Buffer.byteLength(text) <= maxBytes) return text;
  const head = text.slice(0, 12_000);
  const tail = text.slice(-12_000);
  return `${head}\n\n...[中间已截断，原文 ${text.length} 字符]...\n\n${tail}`;
}
```

**为什么保留头尾：** 结构化信息通常在开头（JSON 字段名、文件 import），错误详情通常在结尾（堆栈、失败原因）。但截断不是万能的——如果结果是 JSON，截断后就不是合法 JSON 了，所以**结构化结果应该先摘要再序列化**，不要直接截字符串。

```typescript theme={null}
function summarizeOrders(rows: Order[]) {
  return {
    ok: true,
    data: rows.slice(0, 20).map((r) => ({
      id: r.id,
      status: r.status,
      amountCents: r.amountCents,
    })),
    meta: {
      total: rows.length,
      returned: Math.min(rows.length, 20),
      truncated: rows.length > 20,
      nextCursor: rows.length > 20 ? rows[19].id : null,
    },
  };
}
```

### 回灌的五个原则

1. **统一外壳**：`{ ok, data, meta }`，模型不用猜结构。
2. **只给决策字段**：模型不需要 `createdAt` 的内部主键，就别给。
3. **大对象给引用**：返回 URI / 文件路径 / 查询游标，需要时再取。
4. **敏感字段脱敏**：手机号、地址、令牌在回灌前处理。
5. **标明截断**：`truncated: true` 和 `nextCursor`，否则模型会以为看到的是全量。

> [!WARNING]
>
> 截断会悄悄改变模型的世界观：它可能基于不完整的结果下结论，而且不知道自己没看全。所以**截断必须显式**，并在 `meta` 里说明"还有多少没返回、怎么取"。静默截断比不截断更危险。

***

## Q7: 工具报错时应该返回什么？协议错误和业务错误怎么分？

错误信息本身就是给模型的**重试指令**。好的错误要回答三件事：发生了什么、能不能重试、下一步怎么做。

```typescript theme={null}
interface ToolError {
  ok: false;
  code: string; // 机器可判定
  message: string; // 人能看懂
  retryable: boolean; // 是否值得重试
  hint?: string; // 下一步动作
  field?: string; // 哪个参数错了
}
```

### 协议错误 vs 工具执行错误

MCP 规范把错误分成两类，这个分类对任何 Harness 都适用：

| 类型 | 例子 | 返回方式 | 模型能否自愈 |
| - | - | - | - |
| 协议错误 | 未知工具、请求结构不合法、Server 内部错误 | JSON-RPC error | 通常不能 |
| 工具执行错误 | 日期格式错、库存不足、下游 API 失败 | `isError: true` + 内容 | 通常能 |

**关键判断：错误里有没有"模型可以据此改变行为"的信息。** 参数格式错、值越界、资源不存在，都属于可行动错误，必须让模型看到；鉴权失败、代码 bug、数据库宕机，模型看了也只能瞎猜，应该由 Harness 处理并返回简短说明。

### 错误码分类

```text theme={null}
INVALID_ARGUMENT     参数格式/范围错          retryable=true，附 field 与期望格式
NOT_FOUND            资源不存在              retryable=false，提示先查询
PERMISSION_DENIED    权限不足                retryable=false，换方案或上报
CONFLICT             状态冲突（已支付/已取消） retryable=false，提示先读状态
RATE_LIMITED         限流                    retryable=true，工具层退避，对模型透明
UPSTREAM_UNAVAILABLE 下游不可用              retryable=true，有限重试
BUSINESS_RULE        业务规则拒绝            retryable=false，直接告知用户
```

### 实现

```typescript theme={null}
function toToolError(err: unknown): ToolError {
  const e = err as any;
  if (e?.status === 429) {
    return {
      ok: false,
      code: "RATE_LIMITED",
      message: "请求过于频繁",
      retryable: true,
    };
  }
  if (e?.code === "INVALID_ARGUMENT") {
    return {
      ok: false,
      code: "INVALID_ARGUMENT",
      message: e.message,
      retryable: true,
      field: e.field,
      hint: `参数 ${e.field} 应为 ${e.expected}，请修正后重试`,
    };
  }
  if (e?.code === "NOT_FOUND") {
    return {
      ok: false,
      code: "NOT_FOUND",
      message: "目标不存在",
      retryable: false,
      hint: "请先调用 search_orders 确认订单号，再重试",
    };
  }
  return {
    ok: false,
    code: "INTERNAL",
    message: "工具执行失败",
    retryable: false,
    hint: "不要重复调用，请换方案或告知用户",
  };
}
```

### 不要做的事

* **不要把堆栈直接给模型。** 既浪费 token，又可能泄露内部路径。
* **不要返回 `{"error": "操作失败"}`。** 模型只能原样重试。
* **不要把所有错误都标成 retryable。** 会造成重试风暴。
* **不要把业务错误抛成 500。** Harness 会当成基础设施故障，触发错误的重试策略。

***

## Q8: 重试为什么要分层？幂等怎么保证？

### 三层职责

```text theme={null}
┌─────────────────────────────────────────────┐
│ 编排层：任务级降级                            │
│   换工具 / 换模型 / 拆任务 / 人工接管          │
├─────────────────────────────────────────────┤
│ 模型层：语义纠错                              │
│   读 hint 修正参数，最多 2–3 次                │
├─────────────────────────────────────────────┤
│ 工具层：基础设施自愈                          │
│   超时、限流、5xx，指数退避 + jitter，2–3 次    │
└─────────────────────────────────────────────┘
```

| 层 | 该做 | 不该做 |
| - | - | - |
| 工具层 | 瞬时错误自动重试，对模型透明 | 重试参数错误、业务拒绝 |
| 模型层 | 根据结构化错误修正参数 | 对同一错误无限重试 |
| 编排层 | 降级、换路、人工接管 | 在无幂等保障下重派写操作 |

**双重重试是隐形放大器。** 工具层重试 3 次 × 模型层重试 3 次 = 9 次真实调用。写操作必须让其中一层"不重试"，通常是把工具层重试限制在幂等请求上。

### 幂等键设计

```typescript theme={null}
await createOrder({
  userId,
  sku,
  count: 2,
  idempotencyKey: `order:${taskId}:${stepId}:${hash(args)}`,
});
```

要求：

* **稳定**：同一逻辑操作在重试中必须生成同一个键；不能用 `Date.now()` 或随机数。
* **可追溯**：键里带 `taskId` / `stepId`，出问题能反查。
* **服务端去重**：键存在则返回第一次的结果，而不是再执行一次。
* **有 TTL**：幂等记录不能永久增长，通常 24 小时到 7 天。

### 写操作的三种安全模式

| 模式 | 做法 | 适用 |
| - | - | - |
| 幂等键 | 下游支持去重 | 下单、支付、发券 |
| 先查后写 | 执行前查状态，已完成的直接返回 | 状态机类操作 |
| 事务 / Outbox | 本地事务 + 消息队列保证最终一致 | 跨服务写 |

### 卡死检测

最大步数只能防"无限"，防不了"无效重复"。还要检测相同调用与相同结果：

```typescript theme={null}
function isStuck(history: ToolCall[]) {
  const last = history.slice(-4);
  if (last.length < 4) return false;
  return (
    last[0].name === last[2].name &&
    JSON.stringify(last[0].args) === JSON.stringify(last[2].args) &&
    JSON.stringify(last[1].result) === JSON.stringify(last[3].result)
  );
}
```

命中后不要只是"停止"，而要给模型一个明确的收尾指令：`"检测到重复调用，请基于已有信息总结，或说明缺少什么。"`

***

## Q9: 不同 Provider 的 Tool Calling 有哪些实质差异？

| 维度 | OpenAI | Anthropic | Gemini |
| - | - | - | - |
| 工具定义 | `tools:[{type:"function", function:{...}}]` | `tools:[{name, description, input_schema}]` | `tools:[{functionDeclarations:[...]}]` |
| 严格模式 | `strict: true`（受 Schema 子集限制） | `strict: true`（语法约束采样） | `responseSchema` / 受支持子集 |
| 并行控制 | `parallel_tool_calls` | 模型自行返回多个 `tool_use` | `toolConfig.functionCallingConfig.mode` |
| 调用块 | `message.tool_calls[]` | `content[]` 里的 `tool_use` | `functionCall` part |
| 结果回灌 | 每个调用一条 `role:"tool"` | 同一 user 消息里的多个 `tool_result` | `functionResponse` part |
| 强制调用 | `tool_choice:"required"` / 指定函数 | `tool_choice:{type:"tool",name}` | `mode:"ANY"` |
| 禁用调用 | `tool_choice:"none"` | `tool_choice:{type:"none"}` | `mode:"NONE"` |
| 流式参数 | 增量 `arguments` 片段 | `input_json_delta` | 增量参数片段 |
| 思考/推理块 | 视模型与接口 | thinking 块回传时不可修改 | thought signatures（视模型） |
| 工具检索 | `deferLoading` + namespace | Tool Search（`defer_loading` + `tool_reference`） | 需自行实现 |

### 迁移策略

1. **内部维护一份 canonical 工具定义**（name / description / JSON Schema / 副作用等级 / 超时 / 幂等策略），再由 adapter 转成各家格式。
2. **不要依赖某家的错误文案**，把错误归一化成自己的 `ToolError`。
3. **保存 golden trace**：同一组输入下的工具调用序列，换 Provider 后逐条比对，而不是只看最终回答。
4. **注意消息顺序约束**：Anthropic 的 `tool_result` 必须紧跟对应的 `tool_use`，且同轮结果放在一条消息里；OpenAI 的 tool 消息按 `tool_call_id` 配对。

> [!WARNING]
>
> 工具定义通常位于 prompt 前部。工具列表顺序不稳定、描述频繁变化、动态挂载每轮不同，都会破坏 prompt cache，成本和延迟同时上升。**工具列表要确定性排序**（MCP 规范也要求 Server 尽量返回稳定顺序），动态变更要按会话而不是按轮次。

***

## Q10: 怎么评测工具调用质量？

不要只测最终回答。按层拆指标：

| 层级 | 指标 | 测法 |
| - | - | - |
| 选择 | 工具选择准确率 / 误选率 / 漏调率 | 固定 fixture，比对 `tool_calls[].name` |
| 参数 | 参数合法率 / 字段准确率 | Schema 校验 + 关键字段比对 |
| 执行 | 执行成功率 / P95 延迟 / 错误码分布 | 工具层埋点 |
| 恢复 | 注入错误后的自愈率 | 强制返回可修正错误，看下一轮 |
| 任务 | 端到端完成率 / 步数 / 成本 | 任务集 + 自动断言 |
| 安全 | 越权调用率 / 注入成功率 | 红队用例 |

### Fixture 设计

```json theme={null}
{
  "name": "order_tool_selection",
  "model": "fixed-model",
  "temperature": 0,
  "tools": ["get_order", "report_orders", "refund_order"],
  "cases": [
    {
      "input": "A1024 到哪了",
      "expect": { "tool": "get_order", "args": { "orderId": "A1024" } }
    },
    {
      "input": "解释一下什么是闭包",
      "expect": { "tool": null }
    },
    {
      "input": "上个月退款率是多少",
      "expect": { "tool": "report_orders" }
    },
    {
      "input": "把 A1024 退了吧",
      "expect": { "tool": "refund_order", "requiresConfirmation": true }
    }
  ]
}
```

覆盖四类用例：**正例、负例（不该调用）、边界（格式奇怪但合法）、对抗（诱导越权/注入）**。

### 评测的四个陷阱

1. **用 LLM 当裁判评"工具选得对不对"。** 选择对错是可判定的，用规则比对，别引入随机性。
2. **只测单轮。** 真实故障多发生在第 3–5 轮，必须有多步任务集。
3. **改了 Prompt 不重跑基线。** 工具描述变化会改变选择分布，必须回归。
4. **只看平均值。** 某个高频工具 99% 正确、低频高危工具 60% 正确，平均值会掩盖灾难。

***

## Q11: 线上四类症状怎么排？给出可执行的决策树

先统一日志字段，否则后面全是猜：

```json theme={null}
{
  "trace_id": "4bf92f...",
  "span_id": "a1c3...",
  "provider": "openai",
  "model": "gpt-4.1",
  "step": 3,
  "tools_hash": "sha256:7d1e...",
  "tool_count": 14,
  "tool_choice": "auto",
  "finish_reason": "tool_calls",
  "tool_calls": [{ "name": "get_order", "args_bytes": 28 }],
  "validation_error": null,
  "retry_count": 0,
  "prompt_tokens": 8210,
  "completion_tokens": 64,
  "latency_ms": 940
}
```

### 症状 A：完全不触发 `tool_calls`

```text theme={null}
finish_reason 是什么？
├─ stop / end_turn
│   ├─ message.tool_calls 不存在或为空
│   │   ├─ 用户问题是否真的需要工具？ → 用负例 fixture 验证
│   │   ├─ tool_choice 是否被设成 none？ → 看请求体
│   │   ├─ 工具 Schema 是否符合当前 SDK 格式？ → 用 SDK 类型校验
│   │   ├─ 工具数量是否过多、描述是否同质？ → 临时只留 1 个工具对照
│   │   └─ 模型/接口是否支持 tool calling？ → 换基线模型
│   └─ 有 tool_calls 但 Harness 没解析 → 看解析层版本
└─ length
    └─ 输出被截断，模型还没写到工具调用 → 提高 max_tokens
```

**对照实验**：同一问题、同一模型、同一参数，只改一个变量（描述 / 工具数量 / `tool_choice`），观察 `tool_calls` 是否出现。不要凭感觉说"模型变笨了"。

### 症状 B：调用了错误的工具

```text theme={null}
先算混淆矩阵：真实意图 × 实际调用
├─ 两个工具互相误选
│   ├─ 描述是否只写"能做什么"，没写"何时不用"？ → 补负面边界
│   ├─ 名字是否过于泛化（search/lookup/query）？ → 改成动词+领域
│   └─ 参数是否高度重叠？ → 合并工具或拆分场景
├─ 总是选到"最热门"的工具
│   └─ 工具定义顺序/描述长度造成偏置 → 确定性排序，控制描述长度
└─ 选了不存在的工具名
    └─ 历史消息里残留旧工具定义 → 清理上下文，校验 tools_hash
```

量化方法：把工具描述做 embedding，算两两相似度。相似度高的工具对，就是误选的高发区。

### 症状 C：工具对了，参数错

```text theme={null}
validation_error 长什么样？
├─ 类型错（"2" vs 2）
│   ├─ strict 是否开启？ → 开启并补齐 additionalProperties:false
│   └─ Schema 是否用了 provider 不支持的子集？ → 简化
├─ 缺必填字段
│   ├─ 字段是否真的必填？ → 能默认就给默认值
│   └─ 描述是否说明来源？ → 写"来自会话上下文的 userId"
├─ 枚举/格式错
│   ├─ 是否给了 enum？ → 补 enum 与 format
│   └─ 是否给了示例？ → 描述里放一个合法样例
├─ 编造值（orderId 不存在）
│   ├─ 模型是否被要求"必须给出结果"？ → 提供"不确定就询问"的路径
│   └─ 是否缺少查询工具？ → 先 search 再 get
└─ 单位/时区错
    └─ 字段名写单位（amountCents），时间写 ISO 8601 + 时区
```

**能自动修的自动修，不能的交给模型。** 日期格式、数字字符串转数字可以白名单修正；选错用户 ID、猜错订单号必须让模型重来，自动"猜"会把错误藏起来。

### 症状 D：循环停不下来或漏步

```text theme={null}
步数 / 重复调用签名 / 错误分布
├─ 同一工具同一参数重复
│   ├─ 错误信息是否没有 hint？ → 补"下一步怎么做"
│   ├─ 结果是否为空但没说明？ → 返回 NOT_FOUND + 查询建议
│   └─ 是否命中卡死检测？ → 强制收尾
├─ 每轮都换工具但都不成功
│   ├─ 任务是否超出工具能力？ → 编排层降级/人工接管
│   └─ 是否缺少关键工具？ → 补工具或改流程
├─ 漏步（没查就直接写）
│   ├─ 是否缺少前置条件校验？ → 工具层强校验状态
│   └─ 是否把多步任务塞进一个工具？ → 拆分并给显式步骤
└─ 步数正常但结果错
    └─ 回灌结果是否被截断/结构混乱？ → 看 result_bytes 与 truncated 标记
```

### 五分钟排查顺序

1. 看 `finish_reason`，确认是"没选"还是"选了没执行"。
2. 看 `tools_hash` 与 `tool_count`，确认模型看到的工具是否和你以为的一致。
3. 看 `tool_calls[].args` 原文，不要看 Harness 解析后的对象。
4. 看 `validation_error` 与 `retry_count`，判断是契约问题还是运行时问题。
5. 用单变量 fixture 复现，再决定改描述、改 Schema 还是改执行层。

***

## Q12: 深度追问（Tool Calling）

> [accordion] strict 模式能保证工具参数一定正确吗？
>
> **及格回答：** 能，开了 strict 就不会传错类型。
>
> **满分回答：** 只能保证"形状正确"，保证不了"语义正确"。strict 通过语法约束采样，让输出符合 Schema：类型对、必填字段在、枚举值合法。但它不知道这个 `orderId` 是否属于当前用户，也不知道 `passengers: 4` 是否超过该航班的余票。所以生产上要三层：strict 保证形状，运行时 Schema 校验保证版本和子集一致，业务规则校验保证权限与状态正确。另外 strict 对 JSON Schema 子集有限制，复杂 Schema 可能编译失败，需要降级到普通模式并加强事后校验。

> [accordion] 工具数量多了准确率为什么下降？怎么量化？
>
> **及格回答：** 因为模型要选的东西太多，容易选错。
>
> **满分回答：** 两个机制叠加：一是选择空间变大，相似描述之间的区分难度上升；二是所有工具定义都占上下文，真正与任务相关的信息被稀释，而且工具定义通常在前部，会挤占指令和示例的位置。Anthropic 的公开文档给的经验是超过 30–50 个工具后选择能力明显下降，多 Server 场景光定义就可能占约 55k tokens。量化方法是构造"意图 × 工具"的混淆矩阵，统计误选率和漏调率；再把工具描述做 embedding 算相似度，相似度高的工具对通常就是误选高发区。解法优先级是静态裁剪 → 分组/子 Agent → Tool Search。

> [accordion] 并行工具调用有哪些坑？
>
> **及格回答：** 写操作不能并行，其他都可以。
>
> **满分回答：** 四类坑。第一是依赖：后一个调用需要前一个的结果，必须分轮。第二是副作用与竞态：两个写操作并行可能互相覆盖，需要串行、加锁或幂等键。第三是结果配对：必须用 `tool_call_id` 关联，不能靠返回顺序；一个失败不能让其他结果丢失，所以用 `allSettled` 而不是 `all`。第四是资源：并行度要有上限，否则会同时打爆下游限流。协议层还有格式差异：OpenAI 每个结果一条 tool 消息，Anthropic 要求同一轮所有 `tool_result` 放在同一条 user 消息里且排在文本之前。

> [accordion] 工具返回超长结果，你会怎么处理？
>
> **及格回答：** 截断，保留前面一部分。
>
> **满分回答：** 先看结果类型。结构化结果应该先摘要再序列化，只保留决策字段，并带上 `total / returned / truncated / nextCursor`；纯文本可以保留头尾、截断中段，因为结构化信息在头、错误在尾。大文件不要内联，返回路径或 resource link，让模型按需分段读。二进制和图片转成引用或缩略描述。最关键的是**截断必须显式**：静默截断会让模型以为看到了全量，基于不完整信息下结论，比直接报错更危险。另外截断 JSON 字符串会破坏可解析性，所以顺序应该是"先结构化摘要，再考虑截断"。

> [accordion] 模型一直重复调用同一个工具，你怎么修？
>
> **及格回答：** 加最大步数，超了就停。
>
> **满分回答：** 最大步数只是兜底，不是修复。先看它为什么重复：如果错误信息只有"操作失败"，模型没有新信息，只能原样重试；如果结果为空但没说明原因，模型会以为再试一次就有了。正确做法是给结构化错误（code、retryable、hint），明确告诉它"下一步该查什么"或"不要再试"。同时加卡死检测：连续两次相同工具、相同参数、相同结果就判定卡死，注入一条收尾指令，让它基于已有信息总结或说明缺什么。最后从工具设计上解决：补前置查询工具、把多步任务拆开、给缺失状态一个明确的 NOT\_FOUND 分支。

> [accordion] 换模型后工具调用准确率下降，怎么定位？
>
> **及格回答：** 换个 Prompt 试试，不行就回滚模型。
>
> **满分回答：** 用同一套 fixture 做分层对比，不要只看最终回答。分别统计：工具选择准确率、参数合法率、多步任务完成率、平均步数、错误恢复率。如果只有选择下降，可能是新模型对描述更敏感或对工具数量更敏感；如果参数错误多，可能是 strict 支持差异或 Schema 子集差异；如果多步任务下降，可能是并行调用策略或结果消息格式不同（比如 `tool_result` 的位置）；如果错误恢复率下降，可能是新模型对 hint 的利用方式不同。定位到具体层再改，不要整体回滚。同时检查 `tools_hash` 和消息顺序，迁移时常在这两处出错。

> [accordion] 工具结果里包含恶意指令怎么办？
>
> **及格回答：** 在 Prompt 里告诉模型不要听工具结果里的指令。
>
> **满分回答：** 不能只靠 Prompt。工具结果是第三方内容，必须按不可信数据处理：指令只从 system/user 消息进入，工具结果只作为数据；对结果做来源标记和可疑片段过滤；高风险动作（转账、删除、发邮件）强制人工确认，并在确认 UI 展示真实参数；网络出口做限制，凭据不进入上下文，降低"被说服后能造成的破坏"。这就是间接提示注入的防御思路：不追求 100% 识别，而是让被攻陷后的影响面可控。MCP 规范也明确说工具描述和注解应视为不可信，除非来自可信 Server。

> [accordion] 请设计一个工具调用的可观测性方案。
>
> **及格回答：** 记录每次调用的工具名、参数、耗时和是否成功。
>
> **满分回答：** 至少要能回答四个问题：模型看到了什么、决定了什么、执行发生了什么、最后为什么停。所以一条 trace 要包含：`trace_id`、`step`、`provider/model`、`tools_hash` 与 `tool_count`、`tool_choice`、`finish_reason`、`tool_calls[].name/args`（参数存摘要或脱敏）、`validation_error`、`retry_count`、`latency_ms`、`prompt/completion tokens`、`result_bytes` 与 `truncated`。指标上按层拆分：选择准确率、参数合法率、执行成功率、P95 延迟、错误码分布、平均步数、单任务成本。再加两类专项：安全审计（越权、注入命中）和回归（fixture 通过率）。最后把这些字段和用户反馈关联起来，才能从"用户说不好用"定位到"第 3 步选错了工具"。

***

## 延伸阅读与引用参考

* [OpenAI Function Calling](https://platform.openai.com/docs/guides/function-calling)：工具定义、`strict`、`parallel_tool_calls`、工具结果消息格式。
* [OpenAI Responses API 工具参考](https://developers.openai.com/api/reference/)：`deferLoading`、`allowedCallers`、namespace、`strict`。
* [Anthropic Tool Use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)：`tool_use` / `tool_result`、并行调用与消息顺序。
* [Anthropic Strict Tool Use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use)：语法约束采样、`additionalProperties: false`。
* [Anthropic Structured Outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)：`output_config.format` 与约束解码。
* [Anthropic Tool Search Tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool)：`defer_loading`、`tool_reference`、30–50 工具退化经验值。
* [Anthropic Troubleshooting Tool Use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/troubleshooting-tool-use)：按症状分类的排障表。
* [MCP Tools 规范](https://modelcontextprotocol.io/specification/2025-06-18/server/tools)：协议错误与工具执行错误的区分、`outputSchema`、安全要求。
* [Gemini Function Calling](https://ai.google.dev/gemini-api/docs/function-calling)：`functionDeclarations` 与 `functionCallingConfig`。


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