Tool Schema 设计:为什么你的 Agent 总是调用错工具?

把 Agent 从概念做成可靠系统

模糊请求经过清晰 Schema 后匹配到正确工具

导语

上一篇,我们走完了一次 Tool Calling:应用提供工具定义,模型返回 Tool Call,Runtime 校验并执行,再把结果送回模型。

其中有一个环节被刻意略过了:模型怎样知道该选哪个工具,参数又该怎么填?

假设一个开发 Agent 同时拥有两个工具:

search_code
search_logs

用户说:

帮我找出 INVALID_SIGNATURE 是从哪里产生的。

它应该搜代码,还是搜运行日志?如果工具描述都只写“搜索内容”,模型只能猜。

Tool Schema 就是模型看到的操作说明书。它用工具名、用途描述和参数约束,回答三件事:

  1. 这个工具是做什么的;
  2. 什么情况下应该用它;
  3. 调用时需要提供哪些结构化参数。

Schema 设计不好,强模型也可能选错工具、漏传参数或扩大查询范围。本文会用一组“代码搜索与日志搜索”工具,完整演示怎样把模糊能力改造成清晰接口。


一、先看一个看似能用的坏 Schema

const searchTool = {
  name: 'search',
  description: '搜索信息',
  parameters: {
    type: 'object',
    properties: {
      input: { type: 'string' }
    }
  }
}

这份定义没有语法错误,却几乎没有帮助模型做决策。

模型不知道:

  • 搜索的是源码、日志、文档还是互联网;
  • input 是关键词、正则表达式还是一段自然语言;
  • 搜索范围在哪里;
  • 参数是不是必填;
  • 结果为空时意味着什么。

更麻烦的是,如果工具实现允许 input 同时表达动作和参数,它就会成为一个万能入口:

search({ input: "在生产日志里找错误并顺便删除重复记录" })

Runtime 很难对这种自由文本做可靠校验和细粒度授权。

一个能通过 JSON Schema 校验的定义,不一定是一个适合模型使用的 Tool Schema。


二、工具名应该表达一个清晰动作

工具名是模型做选择时最先看到的信号之一。

相比:

github
data_tool
handle
execute

下面这些名字更容易形成稳定边界:

get_pull_request
search_application_logs
read_file
create_review_draft

实用的命名习惯是“动词 + 对象”:

  • get:获取一个已知对象;
  • list:列出一组对象;
  • search:根据条件查找未知位置;
  • create:创建新资源;
  • update:修改已有资源;
  • delete:删除资源。

这不是为了追求英文整齐,而是让动作的副作用和返回预期更明显。

例如 get_order 与 update_order 应该分开。前者可以自动读取,后者可能需要确认。如果合成 order_tool,权限系统还要再次解析参数才能判断风险。

名字不要承担全部解释

不要为了写清边界,造出过长的名字:

search_staging_application_logs_by_exact_error_code_only

工具名负责识别,完整条件交给 description 和参数 schema。三者要协作,而不是把所有信息压进名字。


三、Description 要写选择边界,不是宣传语

下面这种描述信息量很低:

强大、智能、快速地搜索日志。

模型真正需要知道的是:什么时候用、能查什么、不能做什么。

例如:

查询指定环境中的应用运行日志。
适用于根据时间范围、服务名、请求 ID 或错误码定位运行时问题。
不用于搜索源码;搜索源码请使用 search_code。
该工具只读,不会修改日志或服务状态。

这段描述提供了四类信号:

  • 能力:查询应用日志;
  • 触发场景:定位运行时问题;
  • 反边界:不搜索源码;
  • 副作用:只读。

当两个工具容易混淆时,互相写清“不要在什么情况下使用”很有效。但不必给每个工具堆十条反例;只有真实存在选择冲突时才添加。


四、参数名要让模型知道“该填什么”

继续改造日志工具:

const searchApplicationLogs = {
  name: 'search_application_logs',
  description: [
    '查询指定环境中的应用运行日志。',
    '用于根据服务、时间范围、请求 ID 或错误码定位运行时问题。',
    '不用于搜索源码;搜索源码请使用 search_code。',
    '该工具只读。'
  ].join(' '),
  parameters: {
    type: 'object',
    properties: {
      environment: {
        type: 'string',
        enum: ['staging', 'production'],
        description: '要查询的部署环境'
      },
      service: {
        type: 'string',
        description: '服务名,例如 auth-api'
      },
      query: {
        type: 'string',
        minLength: 1,
        description: '日志查询表达式,可包含错误码或请求 ID'
      },
      startTime: {
        type: 'string',
        format: 'date-time',
        description: '查询起始时间,ISO 8601 格式'
      },
      endTime: {
        type: 'string',
        format: 'date-time',
        description: '查询结束时间,ISO 8601 格式'
      },
      limit: {
        type: 'integer',
        minimum: 1,
        maximum: 200,
        default: 50,
        description: '最多返回多少条日志'
      }
    },
    required: [
      'environment',
      'service',
      'query',
      'startTime',
      'endTime'
    ],
    additionalProperties: false
  }
}

这里每个字段只表达一个概念。

不要把几个概念塞进一个字符串:

{
  "filter": "staging auth-api last 30 minutes limit 50"
}

这种格式看起来省字段,实际上把解析工作重新推给模型或工具实现。拆成结构化参数后,Runtime 才能检查时间范围、环境和最大返回条数。


五、required、默认值和 null 不是一回事

JSON Schema 中,在 properties 里声明字段,并不自动代表它必填。必填字段要放入 required

{
  "properties": {
    "service": { "type": "string" }
  },
  "required": ["service"]
}

还要区分三种状态:

字段缺失
字段存在但值为 null
字段存在且使用默认语义

如果 schema 只允许 string,传入 null 并不等同于省略字段。

默认值也不要只写在描述里。更稳妥的做法是:

  • schema 用 default 告诉读者和工具系统推荐值;
  • Runtime 或工具实现真正补齐默认值;
  • 日志记录补齐后的最终参数。

不同校验器不一定会自动应用 default,不能假设“写了 default 就一定改写输入”。


六、Enum、范围和格式是可执行边界

能枚举的值,不要让模型自由拼写:

{
  "environment": {
    "type": "string",
    "enum": ["staging", "production"]
  }
}

相比任意字符串,它可以拦截:

prod
online
正式环境
production-eu-secret

数值也应该有合理范围:

{
  "limit": {
    "type": "integer",
    "minimum": 1,
    "maximum": 200
  }
}

否则模型可能请求返回十万条日志,既慢又占满上下文。

格式约束能表达日期时间、URI 等常见结构,但要确认你使用的校验器是否实际启用了对应 format 检查。Schema 是契约,Runtime 使用的验证行为才是最终事实。


七、为什么建议关闭额外字段

JSON Schema 默认允许未声明的额外属性。也就是说,只写 properties 时,这类输入可能仍会通过:

{
  "service": "auth-api",
  "query": "INVALID_SIGNATURE",
  "deleteAfterRead": true
}

对于边界明确的工具,通常可以设置:

{
  "additionalProperties": false
}

这样模型多传字段时,Runtime 会明确拒绝,而不是静默忽略或把未知字段传给下游。

不过复杂 schema 使用 allOf 等组合关键字时,additionalProperties 的作用域容易产生意外。不要机械添加后就结束;要用真实样例验证合法和非法输入。


八、一个工具应该做多大一件事

工具太大,模型难选择,权限也难控制:

github({ action, payload })

工具太碎,模型又需要在几十个近似动作中犹豫:

get_issue_title
get_issue_body
get_issue_author
get_issue_labels

更合适的边界通常对应一个可理解、可授权、可测试的业务动作:

get_issue
list_issue_comments
create_issue_comment
update_issue_labels

判断是否应该拆分,可以问四个问题:

  1. 不同动作的副作用是否不同?
  2. 是否需要不同权限或确认策略?
  3. 参数和错误类型是否明显不同?
  4. 模型是否经常只需要其中一部分能力?

任意两项差异很大时,拆开通常更清晰。

但这不是绝对公式。最终要用真实任务集测试,而不是只凭接口美感判断。


九、不要让模型填写系统已经知道的信息

假设用户已在产品里打开仓库 acme/web-app,服务端也知道当前账号和组织。

这时工具参数未必需要再次暴露:

{
  "userId": "?",
  "tenantId": "?",
  "accessToken": "?",
  "owner": "?",
  "repo": "?"
}

可信信息应从执行上下文注入:

async function execute(
  args: SearchCodeArgs,
  context: ToolContext
) {
  return codeSearch.search({
    tenantId: context.tenantId,
    repository: context.currentRepository,
    query: args.query
  })
}

这样既减少模型出错,也避免它把一次会话里的资源标识带到另一位用户或另一个租户。

原则是:模型只填写完成语义动作所需、且确实需要它判断的参数;身份、凭证和已确定的资源上下文由应用提供。


十、Schema 需要怎样测试

Schema 不是写完看着合理就结束。至少要准备三组测试。

选择测试

给模型一组真实用户请求,检查它是否选对工具:

“找出这段函数在哪里被调用” → search_code
“查 request_id=abc 的线上错误” → search_application_logs

还要加入容易混淆的反例。

参数测试

检查正常、边界和非法参数:

缺少必填字段
时间范围颠倒
limit 超过上限
多出未知字段
environment 使用未允许值

JSON Schema 负责结构约束;像“startTime 必须早于 endTime”这样的跨字段业务规则,通常仍需要额外代码验证。

权限测试

同一份合法参数,在不同用户和环境下可能得到不同结果:

开发者查询 staging → 允许
开发者查询 production → 需要额外权限
外部用户查询内部服务 → 拒绝

Schema 不能替代授权测试。


十一、前端和后端如何共同使用 Schema

后端把 Schema 用于模型提示、参数校验和工具路由。前端也可以从同一份元数据中获得帮助:

  • 展示即将执行的工具名称;
  • 把关键参数转成确认摘要;
  • 为人工修正参数生成表单;
  • 在提交前进行基础校验。

但不要直接把原始 Schema 无脑渲染给用户。query 对模型可能很清楚,对普通用户却需要显示成“日志查询条件”。产品层可以维护安全、可本地化的展示元数据。

同一份底层契约可以服务多端,但模型描述、开发者文档和用户界面不必使用完全相同的文案。


十二、用一组工具检查是否理解

现在设计一个订单查询 Agent,已有三个工具:

get_order
list_fulfillment_events
cancel_order

试着回答:

  1. 为什么不能合成一个 order_tool
  2. cancel_order 的描述应该怎样明确副作用?
  3. 订单所属租户应该由模型传入吗?
  4. reason 是必填、可空还是可省略?依据是什么?
  5. 哪些非法参数可以由 JSON Schema 拒绝,哪些仍需业务代码检查?

如果你能根据权限、动作边界和业务语义回答,而不只是堆更多字段,就已经掌握了 Schema 设计的核心。


十三、这一篇真正要记住的事

Tool Schema 不只是“把 TypeScript 类型翻译成 JSON”。它同时服务三个目标:

  • 帮模型在相似能力中选对工具;
  • 把自然语言意图约束成可校验参数;
  • 给 Runtime 留下权限、测试和审计的清晰边界。

设计时优先检查:

名字是否表达明确动作
描述是否说明使用场景与反边界
字段是否一项一义
必填、枚举、范围和额外字段是否受控
可信身份是否来自服务端上下文
读写动作是否因权限和副作用而拆分

Schema 解决的是“模型怎样提出一份清楚的调用请求”。下一篇继续看另一半:工具执行完后,怎样把结果返回给模型,才不会让它误判、浪费上下文或陷入重试。


下一篇预告

下一篇:Tool Result 设计:Agent 如何看懂工具返回?


参考资料

  • OpenAI API — Function calling:
    https://developers.openai.com/api/docs/guides/function-calling
  • JSON Schema — Object:
    https://json-schema.org/understanding-json-schema/reference/object
  • Model Context Protocol — Tools:
    https://modelcontextprotocol.io/specification/2025-06-18/server/tools
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值