#skills#engineering#tool-use

工具调用的 schema 设计:让 LLM 少出错的 7 条工程经验

tool schema 写得不好,LLM 就会频繁参数错、漏参数、类型错。我们这一年给 SCL 和 DataWeaver 写了几十个工具,踩过的坑沉淀成 7 条经验,每条带反例和正例。结论是 schema 不是给人看的文档,是 LLM 学决策的训练样本——这个视角变了,写法就变了。

著者YGG 智能体周刊公開日8 分で読める

一段先讲心智模型

写 tool schema 的时候,大多数人当成在写 API 文档——给后端同事看的。这个心智在 LLM 时代是错的。

LLM 在每次决定"调哪个工具、传什么参数"的时候,唯一能看到的就是你写的 schema。它没有源码、没有测试、没有 changelog。schema 写糊了,它就猜;猜错了,就反复修复浪费 token;修不好,用户就崩。

下面 7 条来自真实项目的反例-正例对照。

1. description 写"什么时候用",不是"做什么"

反例:

{
  "name": "list_users",
  "description": "获取用户列表"
}

正例:

{
  "name": "list_users",
  "description": "当用户问'有哪些用户'/'多少人注册了'/'最近谁加入'/'按公司筛用户'这类问题时调用。返回当前租户下的用户清单,可按 status / created_at / company_id 过滤。不要用于查询单个用户详情——那种用 get_user。"
}

差别在哪?正例告诉 LLM 触发场景的语言模式与相邻工具的边界。LLM 在多个相似工具里选困难症,靠的是 description 里的负面提示。

2. 必填和可选严格区分

不要给一堆可选参数让 LLM 猜该传哪个。它一定会全传,或者全不传,没有中间状态。

反例:搜索接口 12 个参数全 optional。LLM 看不出哪些是核心、哪些是 advanced。

正例:核心 2 个 required(query、limit),其余塞进 filters: object 嵌套对象,并在 description 里说"高级过滤,绝大多数场景不需要"。

3. enum 优于 string

反例:

{ "priority": { "type": "string", "description": "优先级" } }

LLM 会传 "高"、"high"、"P0"、"urgent"、"important"——五种全见过。

正例:

{
  "priority": {
    "type": "string",
    "enum": ["urgent", "normal", "low"],
    "description": "urgent 用于阻塞性问题;normal 是默认;low 用于改进类需求"
  }
}

加 enum 的同时给每个值的语义。LLM 会按语义匹配场景,不会瞎挑。

4. type: object 嵌套时给 example

JSON Schema 的 examples 字段是 LLM 学习嵌套结构的最强信号——比 properties 描述强。

{
  "filters": {
    "type": "object",
    "properties": {
      "status": { "type": "string", "enum": ["active", "archived"] },
      "created_after": { "type": "string", "format": "date-time" }
    },
    "examples": [
      { "status": "active", "created_after": "2026-01-01T00:00:00Z" },
      { "status": "archived" }
    ]
  }
}

我们做过一个对照:同一个嵌套对象 schema,加 examples 后参数错误率从 18% 掉到 4%。这不是玄学,是 LLM 训练数据里见过太多 "examples 字段 = 真实输入长这样" 的模式。

5. 不要重名 tool

反例:同一个 toolset 里有 get_userfetch_user,前者按 id 查,后者按 email 查。LLM 会反复在两者之间犹豫,甚至每次随机挑。

正例:合并成 get_user,参数里 lookup_by: "id" | "email"。或者起反差大的名字 get_user_by_id / find_user_by_email,让 LLM 一眼分清。

写 tool 命名的时候我有个土办法:把所有工具名列出来,盖住 description,自己能不能从名字判断什么时候用哪个。如果你都判断不出来,LLM 也不行。

6. 返回值结构稳定

反例:成功时返字符串 "OK",失败时返 { error: "..." }。LLM 拿到结果不知道怎么处理后续——是当作文本拼回答,还是当作 JSON 解析。

正例:永远返同一个结构。

type ToolResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: ErrorCode; hint: string }

ok 字段是 LLM 决策分支的锚点。在 description 里明确写"调用后先检查 ok 字段"。

7. 错误返回也要结构化

反例:"user not found"

正例:

{
  "ok": false,
  "error": "USER_NOT_FOUND",
  "hint": "确认 user_id 是否正确,或调 list_users 查可用 id",
  "retryable": false
}

三个字段都有用:

  • error 是机器码,LLM 能据此分类(NOT_FOUND vs RATE_LIMITED 后续行为完全不同)
  • hint 是给 LLM 的下一步建议
  • retryable 告诉 LLM 这个错值不值得重试

我们在第 6 篇周刊(5 轮自动修复模式)里讲过,错误是修复链路的输入。错误返回的质量直接决定修复链路的成本。

一段完整 schema 例子

把上面 7 条揉到一起:

const listUsers = {
  name: 'list_users',
  description:
    "当用户问'有哪些用户'/'多少注册'/'按公司筛用户'时调。" +
    '不要用于查单个用户详情(那种用 get_user)。' +
    '返回值检查 ok 字段,ok=true 取 data,ok=false 看 hint。',
  parameters: {
    type: 'object',
    required: ['query', 'limit'],
    properties: {
      query: {
        type: 'string',
        description: '用户名 / email 模糊匹配,留空返回全部',
      },
      limit: {
        type: 'integer',
        minimum: 1,
        maximum: 100,
        description: '默认 20。LLM 不要主动调到 100 除非用户明确要"全部"',
      },
      filters: {
        type: 'object',
        description: '高级过滤。绝大多数场景不需要',
        properties: {
          status: { type: 'string', enum: ['active', 'archived'] },
          created_after: { type: 'string', format: 'date-time' },
        },
        examples: [{ status: 'active' }],
      },
    },
  },
}

MCP 是什么

最近 Anthropic 推的 Model Context Protocol(MCP)把工具协议标准化了——schema 思路和上面 7 条一致,但格式更规范、跨厂商互通。Claude、Cursor、Windsurf 都开始原生支持。如果你在做内部 toolset,直接按 MCP 写,未来切换 LLM 厂商不用重写。

但 MCP 的标准化解决的是"格式"问题,不是"质量"问题。description 写糊、enum 漏掉、example 缺失,再标准的协议也救不了。

通用性

这些经验对 OpenAI、Claude、Gemini、Qwen 都通用。底层逻辑很简单:这些 LLM 都从你给的 schema 学决策。它们对 schema 的"理解力"差异比你以为的小,差异主要在生成长度、推理深度。schema 的好坏对所有家族都成立。

最后一个真心建议:写完 schema,自己拿一段用户提问 + schema 喂给 ChatGPT,看它怎么填参数。它填错的地方,就是你 schema 的 bug。这个土办法比 unit test 还管用。

ArXiv、HackerNews、公開ベンダーブログを YGG チームがキュレーション。人手でレビュー済み。