← All articles
Agents Weekly2026 · Issue 17

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

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

YGG 智能体周刊8 min read#skills#engineering#tool-use

一段先讲心智模型

写 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 还管用。

Sources

  1. OpenAI Function Calling 文档
  2. Anthropic Tool Use 文档
  3. Model Context Protocol 规范

Further reading

Editor's note

Curated by the YGG team from ArXiv, HackerNews and public vendor blogs. Human-reviewed.