Agent 工程体系 · 第 9/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。
Agent 工具 Schema:命名、描述、参数、枚举和可发现性
Agent 的工具调用不是“模型输出一段 JSON,程序再把它转成函数调用”这么简单。模型能否选对工具、填对参数、避免危险操作,首先取决于工具如何被声明。
这里的 工具 Schema,指描述工具能力、输入结构和取值约束的机器可读契约。它通常以 JSON Schema 或其受限子集表达,并由模型侧、编排器和工具执行器共同消费。
一个完整的工具调用链至少包含四个角色:
- 模型:根据用户目标选择工具并生成调用参数。
- Schema:告诉模型“有哪些工具、什么时候用、参数长什么样”。
- 工具执行器:解析、校验、授权并实际执行调用。
- 外部系统:数据库、HTTP 服务、文件系统或业务 API。
OpenAI 的 Function Calling 将工具定义为模型可以使用的功能,工具输入通常由 JSON Schema 描述;MCP 则通过 tools/list 暴露工具列表,并为每个工具提供名称、描述和 inputSchema。(developers.openai.com)
因此,Schema 不是注释,也不是仅供 IDE 补全的类型声明。它是模型决策空间的一部分:
如果 Schema 只保证“JSON 能解析”,却没有表达业务语义,模型仍然可能稳定地产生错误调用。
一、先建立工具 Schema 的完整边界
以一个“查询订单”的工具为例:
{
"name": "commerce_get_order",
"description": "根据订单号查询当前用户有权访问的订单状态、金额和物流摘要。仅用于查询,不会修改订单。",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单号,例如 ORD-20260901-001。不要填写商品名称、手机号或自然语言描述。"
},
"include_items": {
"type": ["boolean", "null"],
"description": "是否返回订单中的商品明细。传 null 表示使用系统默认值。"
}
},
"required": ["order_id", "include_items"],
"additionalProperties": false
},
"strict": true
}
这个定义包含五个不同层次:
| 层次 | 作用 |
|---|---|
name |
工具的机器可调用标识 |
description |
工具的能力边界、适用场景和副作用 |
parameters |
输入对象的结构 |
参数 description |
每个字段的业务含义和填写方式 |
enum、type、required 等 |
输入值域和结构约束 |
其中,name 解决“调用谁”,description 解决“什么时候调用”,参数 Schema 解决“传什么”,枚举解决“只能传哪些离散值”,可发现性解决“模型是否能在当前上下文中看到并理解这个工具”。
这几个概念不能互相替代:
- 名字清楚但没有描述,模型不知道适用边界。
- 描述详细但参数宽松,模型可能填出无法执行的值。
- 参数严格但工具没有被加载,模型根本无法选择它。
- 工具已被发现但名称冲突,模型可能选择错误的同名能力。
二、命名:名称是路由键,不是营销标题
2.1 工具名称的两个使用者
工具名称至少同时服务于两个系统:
- 模型用它区分工具;
- 执行器用它查找处理函数。
因此工具名称必须同时满足:
名称不是给人看的展示标题。它更接近 RPC 方法名或 API 路由。
推荐使用:
<领域>_<动作>_<对象>
例如:
crm_get_customer
crm_list_open_orders
billing_create_refund
shipping_get_tracking
OpenAI 的文档也建议使用 namespace 将相关工具按领域分组,例如 crm、billing 和 shipping,以帮助模型在多个系统之间区分相似能力。(developers.openai.com)
2.2 动作必须表达真实副作用
下面两组名称看起来都能工作,但语义风险不同:
order_update
order_cancel
order_delete
order_preview_cancellation
order_submit_cancellation
order_confirm_cancellation
order_update 把多个可能的业务动作压缩成了一个模糊入口。模型可能把“修改收货地址”“取消订单”和“修改备注”都映射到同一个工具。
如果工具会产生外部副作用,动作名应尽量直接表达副作用:
create:创建资源;update:修改资源;delete:删除资源;send:发送消息;refund:退款;approve:审批;preview:只计算或预览,不提交;confirm:确认已经展示给用户的动作。
不能把有副作用的工具伪装成查询工具:
get_order
内部却执行:
def get_order(order_id):
mark_order_as_viewed(order_id)
trigger_customer_notification(order_id)
return load_order(order_id)
这会破坏模型对工具行为的预测。工具名称和描述应当让模型能够区分:
2.3 名称冲突会造成“语义路由错误”
假设工具列表同时存在:
get_customer
以及:
get_customer
但分别来自 CRM 和客服系统。
如果协议层允许名称重复,执行器必须额外依赖 server、namespace 或工具来源进行路由;如果协议层或客户端假设名称全局唯一,则后注册的工具可能覆盖先注册的工具。
更稳妥的形式是:
crm_get_customer
support_get_customer
错误示例:
do_it
handle_request
query
process
这些名称缺少对象和动作,模型需要从描述中猜测全部语义,工具数量一多,选择错误会显著增加。
三、描述:不是“介绍”,而是模型的决策规则
3.1 工具描述回答四个问题
一个可用的工具描述至少需要回答:
- 它做什么?
- 什么时候应该使用?
- 什么时候不应该使用?
- 它是否产生副作用?
推荐结构:
[动作对象] + [适用条件] + [关键输入] + [返回内容] + [副作用/限制]
例如:
根据订单号查询当前用户有权访问的订单状态、金额和物流摘要。
仅用于读取;不会取消、修改或支付订单。
当用户询问订单进度、订单状态或物流信息时使用。
如果用户只提供商品名称而没有订单号,先向用户索要订单号。
相比之下,下面的描述信息不足:
查询订单
它没有说明:
- 是否只能查询当前用户的订单;
- 是否支持商品名称;
- 是否返回物流;
- 是否会改变状态;
- 缺少订单号时是否可以调用;
- 是否适合处理取消订单。
3.2 描述中的“何时不用”很重要
工具选择本质上是一个分类问题。对于用户请求 ,模型需要在工具集合 中选择:
描述不仅提高目标工具的匹配概率,也应该降低相邻工具的匹配概率。
例如有两个工具:
{
"name": "calendar_find_free_slots",
"description": "查找参与者共同空闲的时间段。只读,不会创建或修改日历事件。"
}
{
"name": "calendar_create_event",
"description": "创建日历事件并邀请参与者。会产生外部副作用;只有在用户明确确认时间、标题和参与者后使用。"
}
如果第一个工具只写“查询日历”,第二个工具只写“创建事件”,模型仍可能在用户说“帮我安排明天下午三点开会”时直接创建事件。描述中的副作用条件可以把“查找时间”和“提交事件”分成两个不同阶段。
3.3 描述不能承担硬约束
以下写法是常见误解:
{
"name": "billing_refund",
"description": "金额必须小于等于 1000 元"
}
如果 Schema 没有表达金额类型和范围,执行器仍然可能收到:
{
"amount": 999999
}
描述是自然语言提示,不是可靠的业务验证。硬约束应同时出现在 Schema 和执行器中:
{
"amount": {
"type": "number",
"minimum": 0.01,
"maximum": 1000,
"description": "退款金额,单位为人民币元。"
}
}
即使模型遵守了 Schema,执行器仍需根据真实账户余额、订单状态和授权主体再次校验。Schema 约束的是输入形状,不等于授权,也不等于业务规则的完整实现。
四、参数 Schema:从 JSON 形状到业务语义
4.1 根节点通常是对象
工具调用参数通常是一个 JSON 对象:
{
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
}
它表达的是:
其中 是一次工具调用的参数对象。
例如:
{
"type": "object",
"properties": {
"query": {
"type": "string",
"minLength": 1
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 50
}
},
"required": ["query", "limit"],
"additionalProperties": false
}
合法输入:
{
"query": "退款政策",
"limit": 10
}
非法输入:
{
"query": "",
"limit": 100,
"debug": true
}
失败原因分别是:
query不满足minLength: 1;limit超过maximum: 50;debug不在properties中,且不允许额外字段。
4.2 required 表达结构必需,不表达业务必填的全部含义
在普通 JSON Schema 中,字段是否出现在对象中由 required 决定:
{
"properties": {
"units": {
"type": "string"
}
},
"required": []
}
表示 units 可以缺失。
但在 OpenAI Function Calling 的 strict mode 中,要求 properties 中的所有字段都标记为 required,可选语义可以通过允许 null 表达。additionalProperties 也需要设为 false。(developers.openai.com)
因此:
{
"type": "object",
"properties": {
"units": {
"type": ["string", "null"],
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["units"],
"additionalProperties": false
}
表达的是:
units这个键必须出现;- 值可以是
"celsius"; - 值可以是
"fahrenheit"; - 值也可以是
null; - 但不能省略该键。
这是“字段存在但值可为空”,不是“字段可以省略”。
错误写法:
{
"properties": {
"units": {
"type": ["string", "null"],
"enum": ["celsius", "fahrenheit"]
}
},
"required": []
}
在普通 JSON Schema 中它可能有效,但在要求 strict mode 的调用接口中可能被拒绝,或者被降级为非严格调用。OpenAI 文档明确区分了严格和 best-effort 调用,并说明不同 API 的默认行为可能不同。(developers.openai.com)
4.3 additionalProperties: false 的意义
如果没有额外字段限制:
{
"type": "object",
"properties": {
"order_id": {
"type": "string"
}
}
}
以下输入可能仍通过基础结构校验:
{
"order_id": "ORD-001",
"is_admin": true,
"force": true
}
执行器如果直接把整个对象传给下游系统,可能出现:
- 下游 SDK 忽略未知字段;
- 下游 API 将未知字段解释为特殊选项;
- 业务代码未来新增字段后意外激活旧调用;
- 攻击者利用“看似无害”的额外参数绕过边界。
所以严格工具调用一般采用:
"additionalProperties": false
但要注意,它只禁止 Schema 中未声明的键,并不自动阻止参数值中的恶意字符串。例如:
{
"path": "../../etc/passwd"
}
字段名合法,不代表业务语义安全。
五、枚举:把开放文本压缩为有限状态空间
5.1 枚举的形式化含义
enum 表示字段值属于有限集合:
例如:
{
"status": {
"type": "string",
"enum": ["pending", "paid", "cancelled"],
"description": "订单状态过滤条件。"
}
}
允许值集合是:
{"pending", "paid", "cancelled"}
以下值都不合法:
"Pending"
"已支付"
"success"
"all"
除非它们被明确加入枚举集合。
5.2 枚举值应使用稳定机器值
推荐:
{
"sort": {
"type": "string",
"enum": ["created_at_asc", "created_at_desc"],
"description": "结果排序方式。"
}
}
不推荐:
{
"sort": {
"type": "string",
"enum": ["按创建时间升序", "按创建时间降序"]
}
}
机器值应该:
- 稳定;
- 无歧义;
- 与后端状态机一致;
- 不依赖语言;
- 不包含容易混淆的同义词。
展示文本可以放在描述中,或由应用层做本地化映射:
SORT_LABELS = {
"created_at_asc": "按创建时间升序",
"created_at_desc": "按创建时间降序",
}
5.3 不要把枚举集合做得过大
枚举不是把所有可能文本罗列出来:
{
"city": {
"type": "string",
"enum": [
"杭州",
"上海",
"北京",
"广州",
"深圳",
"..."
]
}
}
城市数量可能动态变化。把动态数据硬编码到 Schema 会带来:
- Schema 体积膨胀;
- 工具定义频繁变化;
- 模型上下文成本增加;
- 旧 Schema 与真实数据不一致。
更合理的拆分是:
search_city(query: string)
create_delivery(order_id: string, city_id: string)
第一个工具负责发现动态资源,第二个工具只接受稳定的 city_id。
5.4 枚举不能代替跨字段约束
以下 Schema:
{
"type": "object",
"properties": {
"payment_method": {
"type": "string",
"enum": ["card", "bank_transfer"]
},
"card_number": {
"type": ["string", "null"]
}
},
"required": ["payment_method", "card_number"],
"additionalProperties": false
}
允许这种逻辑上矛盾的输入:
{
"payment_method": "card",
"card_number": null
}
也允许:
{
"payment_method": "bank_transfer",
"card_number": "4111111111111111"
}
如果使用的 Schema 方言和调用平台支持条件组合,可以用 oneOf 表达互斥结构:
{
"type": "object",
"oneOf": [
{
"properties": {
"payment_method": {
"const": "card"
},
"card_number": {
"type": "string",
"minLength": 12
}
},
"required": ["payment_method", "card_number"],
"additionalProperties": false
},
{
"properties": {
"payment_method": {
"const": "bank_transfer"
},
"card_number": {
"type": "null"
}
},
"required": ["payment_method", "card_number"],
"additionalProperties": false
}
]
}
但不能假定所有模型接口都完整支持 JSON Schema。OpenAI 文档明确说明 strict mode 只支持 JSON Schema 的一部分。(developers.openai.com)
生产中常见取舍是:
- Schema 表达模型和接口都稳定支持的约束;
- 执行器补充跨字段业务校验;
- 对无法表达的条件返回结构化错误,而不是静默修正。
六、可发现性:工具存在不等于模型能使用
6.1 可发现性的定义
可发现性是指模型在需要完成某个目标时,能够在当前上下文中看到、区分并正确理解某个工具的能力。
它包含三个条件:
其中:
- :工具对模型可见;
- :工具描述和 Schema 足以区分它;
- :工具当前可调用,例如权限、租户、环境和生命周期都满足。
只有名称和描述写得好,但工具没有传给模型,,模型仍不可能调用它。
6.2 全量暴露与按需发现
当工具数量较少时,可以把全部工具定义放进模型请求:
用户请求
↓
加载全部工具 Schema
↓
模型选择工具
↓
执行器调用
工具数量较多时,全量暴露会造成:
- 上下文变长;
- 相似工具增多;
- 描述之间发生竞争;
- Schema 修改影响整个请求;
- 工具选择不稳定。
OpenAI 的工具搜索能力允许延迟加载较少使用的工具;官方文档说明,在工具很多或 Schema 很大时,可以先暴露工具搜索入口,再按需加载工具,且该能力只适用于支持它的模型版本。(developers.openai.com)
按需发现的基本流程是:
sequenceDiagram
participant U as 用户
participant M as 模型
participant D as 工具目录
participant X as 执行器
participant S as 外部系统
U->>M: 提出目标
M->>D: 搜索相关工具
D-->>M: 返回名称、描述和 Schema
M->>X: 生成工具调用
X->>X: 解析、校验、授权
X->>S: 执行业务操作
S-->>X: 返回结果或错误
X-->>M: 工具结果
M-->>U: 生成最终回答
关键点是:工具搜索本身只是发现机制,不是授权机制。目录返回了工具,不代表当前用户被允许执行它。
6.3 MCP 的工具发现模型
在 MCP 中,客户端可以通过 tools/list 请求服务器提供的工具列表。返回结果包含工具数组,并且支持分页游标;工具调用参数应符合工具的 inputSchema。(modelcontextprotocol.io)
抽象后的发现响应类似:
{
"tools": [
{
"name": "crm_get_customer",
"description": "根据客户 ID 查询当前租户内可访问的客户资料。",
"inputSchema": {
"type": "object",
"properties": {
"customer_id": {
"type": "string"
}
},
"required": ["customer_id"],
"additionalProperties": false
}
}
],
"nextCursor": "opaque-next-page-token"
}
这里需要区分两个层次:
- MCP 的
tools/list是协议层的工具发现; - Agent 运行时是否把这些工具交给模型,是宿主应用的编排决策。
一个 MCP Server 可以暴露 100 个工具,但宿主应用只给模型加载其中 5 个。反过来,宿主应用也可能把多个 MCP Server 的工具合并后再做 namespace 处理。
6.4 动态工具列表必须有缓存失效策略
工具发现通常不是每一轮都重新获取。否则模型请求会反复承担发现开销。
可以为工具目录维护:
(server_id, tenant_id, permission_scope, schema_version)
作为缓存键,而不能只用:
(server_id)
因为工具列表可能依赖:
- 当前租户;
- 当前用户;
- OAuth scope;
- 环境;
- 功能开关;
- 工具版本。
如果权限变化后仍复用旧缓存,模型可能看到用户已经无权使用的工具。即使执行器最终拒绝调用,也会导致不必要的错误回合,并可能暴露工具名称和业务能力。
MCP 的新版本为列表和资源读取结果增加了 ttlMs 与 cacheScope,帮助客户端判断缓存时长和缓存是否可以跨用户共享。(blog.modelcontextprotocol.io)
七、一个完整工具定义:从错误版本到严格版本
假设需求是:根据订单号查询物流信息,并允许指定返回语言。
7.1 错误版本
{
"name": "query",
"description": "查询物流",
"parameters": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"lang": {
"type": "string"
}
}
}
}
这个版本有五类问题:
query无法区分订单查询、客户查询和商品查询;id没有说明是订单号、物流单号还是数据库 ID;lang没有取值范围;- 没有
required,模型可能遗漏关键参数; - 没有
additionalProperties: false,额外字段可能进入执行器。
7.2 改进版本
{
"type": "function",
"name": "shipping_get_tracking",
"description": "根据订单号查询当前用户有权访问的物流状态、承运商和最近一条轨迹。仅用于读取,不会修改订单或联系承运商。当用户询问订单配送进度、物流状态或预计送达时间时使用。",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "平台订单号,例如 ORD-20260901-001。不要填写物流单号、商品名称或手机号。",
"pattern": "^ORD-[0-9]{8}-[0-9]{3,}$"
},
"language": {
"type": ["string", "null"],
"enum": ["zh-CN", "en-US"],
"description": "物流状态的返回语言。传 null 表示使用用户会话语言。"
}
},
"required": ["order_id", "language"],
"additionalProperties": false
},
"strict": true
}
这里的每条约束都有明确作用:
shipping_get_tracking消除工具路由歧义;- 工具描述区分“查询物流”和“修改订单”;
order_id的描述避免把物流单号传进来;pattern约束订单号的基本格式;language使用枚举限制离散取值;null表达可选语义,同时满足 strict mode 对字段存在性的要求;additionalProperties: false阻止未声明字段;strict: true要求调用更可靠地符合 Schema,而不是仅做 best-effort 生成。(developers.openai.com)
7.3 执行器仍然必须二次校验
模型返回:
{
"order_id": "ORD-20260901-001",
"language": "zh-CN"
}
执行器至少要经过以下状态:
stateDiagram-v2
[*] --> Received: 收到工具调用
Received --> Decoded: JSON 解码成功
Received --> Rejected: JSON 非法
Decoded --> SchemaValidated: Schema 校验成功
Decoded --> Rejected: 类型/枚举/额外字段错误
SchemaValidated --> Authorized: 身份和权限校验成功
SchemaValidated --> Rejected: 授权失败
Authorized --> Executing: 调用物流系统
Executing --> Succeeded: 返回结果
Executing --> Failed: 超时/网络/业务错误
Succeeded --> [*]
Failed --> [*]
Rejected --> [*]
Schema 校验只证明:
参数结构合法
它不能证明:
订单存在
订单属于当前用户
当前用户有权查看物流
订单已发货
物流服务可用
因此执行器不能写成:
handler(**json.loads(arguments))
而应当在边界处重新验证:
def execute_shipping_get_tracking(arguments, user):
args = decode_json(arguments)
validate_schema(args, TRACKING_SCHEMA)
order = order_repository.get(args["order_id"])
if order is None:
return tool_error("ORDER_NOT_FOUND", "订单不存在")
if order.owner_id != user.id:
return tool_error("FORBIDDEN", "无权访问该订单")
return tracking_service.get_tracking(
order_id=order.id,
language=args["language"] or user.locale,
)
这里 validate_schema 防止结构错误,业务查询和权限判断防止语义越权。两者缺一不可。
八、参数描述必须解释“值的语义”,而不仅是类型
下面两个字段在 JSON Schema 层面完全相同:
{
"type": "integer"
}
但业务语义可能完全不同:
{
"timeout": {
"type": "integer",
"description": "超时时间,单位为秒,范围 1 到 30。"
}
}
{
"timeout": {
"type": "integer",
"description": "任务截止时间距当前时间的秒数,不是 HTTP 请求超时。"
}
}
如果只写类型,模型可能把毫秒当秒,或者把绝对时间戳当成相对时长。
参数描述应该明确:
- 单位:秒、毫秒、元、分、件;
- 标识类型:订单号、资源 ID、用户名还是展示名称;
- 时间基准:UTC、用户时区还是服务器时区;
- 是否允许空值;
- 默认值如何解释;
- 字符串是否需要编码;
- 是否支持自然语言;
- 是否有格式示例;
- 不应传什么。
例如:
{
"start_time": {
"type": "string",
"format": "date-time",
"description": "查询起始时间,必须使用 ISO 8601 格式并带时区,例如 2026-09-01T00:00:00+08:00。不要传 Unix 时间戳。"
}
}
示例不是装饰。对于格式复杂但合法空间很大的字段,示例可以显著缩小模型的解释空间。
九、Schema 演化:新增字段不一定向后兼容
Schema 版本变化至少影响三方:
- 生成调用的模型;
- 校验调用的执行器;
- 处理工具结果的编排器。
9.1 新增必填字段是破坏性变更
旧版本:
{
"required": ["order_id"]
}
新版本:
{
"required": ["order_id", "region"]
}
旧模型仍然可能只生成:
{
"order_id": "ORD-001"
}
结果是调用失败。
更兼容的做法是先新增可为空字段:
{
"properties": {
"region": {
"type": ["string", "null"],
"enum": ["cn-east", "cn-south"]
}
},
"required": ["order_id", "region"]
}
执行器对 null 使用默认路由,待所有调用方完成迁移后,再考虑收紧约束。
9.2 删除枚举值也是破坏性变更
旧版本:
"enum": ["pending", "paid", "cancelled"]
新版本删除 "pending" 后,旧缓存、重试消息或延迟任务仍可能携带该值。
如果状态已经不再支持,更安全的迁移方式是:
- 保留旧值一段兼容窗口;
- 描述中标记不再生成;
- 执行器把旧值映射到明确错误或迁移状态;
- 等待旧调用完全消失后再删除。
9.3 名称重命名不是普通重构
将:
billing_refund
改成:
billing_create_refund
会影响:
- 模型工具选择;
- 执行器路由表;
- 日志和指标聚合;
- 录制回放数据;
- 审计规则;
- 用户确认策略。
可以采用双注册迁移:
billing_refund # 旧名称,标记 deprecated
billing_create_refund # 新名称
旧名称的执行器内部转发到新实现,但不要同时让模型长期看到两个语义相同的工具,否则会增加选择竞争。迁移完成后,从模型可见列表中移除旧名称,但保留一段时间的后端兼容路由。
十、并行工具调用会放大 Schema 的副作用风险
某些支持的模型可能在一次响应中产生多个工具调用;OpenAI 文档说明可以使用 parallel_tool_calls: false 禁止这种行为,使一轮中最多产生零个或一个工具调用。(developers.openai.com)
如果工具都是只读操作,并行调用通常比较简单:
get_weather(Hangzhou)
get_weather(Shanghai)
get_weather(Beijing)
但以下工具不能简单并行:
debit_account()
create_invoice()
send_email()
cancel_order()
原因不是 Schema 无法表达,而是工具之间存在状态依赖:
如果模型同时生成三个调用,执行器必须知道:
- 是否允许并发;
- 是否需要顺序;
- 前一步失败后是否执行后一步;
- 是否允许重试;
- 是否具有幂等键;
- 用户是否需要在副作用前确认。
因此 Schema 中可以通过描述提示模型:
仅在扣款已成功且用户确认发票信息后调用。
不能与 payment_debit 并行执行。
但真正的顺序控制必须由编排器和执行器保证,而不能依赖模型遵守自然语言。对于高风险副作用工具,关闭并行调用通常比事后补救更容易验证。
十一、错误的 Schema 设计会如何失败
11.1 把多个动作合并为一个工具
{
"name": "manage_order",
"description": "管理订单",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"order_id": {
"type": "string"
},
"payload": {}
}
}
}
问题在于:
action的可能值不受约束;- 不同动作需要不同参数;
payload失去结构;- 模型需要自行推断参数组合;
- 执行器容易出现“先解析,再猜业务”的代码。
应拆成多个工具:
order_get
order_preview_cancel
order_submit_cancel
order_update_address
工具数量增加了,但每个工具的决策边界更清晰。
11.2 把动态查询做成巨大枚举
{
"product_id": {
"type": "string",
"enum": ["p001", "p002", "p003", "..."]
}
}
这会让 Schema 成为过期的商品缓存。动态资源应通过搜索或列表工具发现,不能要求工具定义实时承载业务数据库。
11.3 只使用自然语言约束
{
"amount": {
"type": "number",
"description": "请填写合理的金额,不能太大。"
}
}
“合理”“不能太大”不是可验证条件。应该把可计算条件写成:
{
"amount": {
"type": "number",
"minimum": 0.01,
"maximum": 1000
}
}
无法形式化的部分,再由执行器做业务校验。
11.4 把用户身份放进模型参数
错误设计:
{
"name": "get_account",
"parameters": {
"type": "object",
"properties": {
"user_id": {
"type": "string"
}
},
"required": ["user_id"]
}
}
如果身份已经由会话、令牌或执行上下文确定,就不应让模型决定 user_id。否则模型可能根据用户文本填入另一个人的 ID。
更安全的设计是:
{
"name": "get_my_account",
"parameters": {
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
}
}
执行器从可信上下文取得用户身份:
def get_my_account(args, auth_context):
return account_service.get(auth_context.subject_id)
这体现了一个重要边界:
身份、租户、权限和审计主体通常应来自可信执行上下文,而不是模型生成的 JSON。
十二、如何验证一个工具 Schema 是否真的可用
可以把验证分成四层。
12.1 结构验证
检查:
- 根节点是否为对象;
properties是否为对象;required是否引用已声明字段;- 字段类型是否有效;
enum中的值是否符合类型;- strict mode 所需的约束是否完整;
- 是否禁止未声明字段。
12.2 语义验证
逐个回答:
- 工具名是否包含领域和动作;
- 描述是否说明适用和不适用场景;
- 参数名是否能区分 ID、名称和自然语言;
- 单位和时区是否明确;
- 副作用是否显式;
- 是否存在两个功能高度重叠的工具。
12.3 对抗性输入验证
至少测试:
{}
{"order_id": null}
{"order_id": "ORD-001", "unexpected": true}
{"order_id": "../../etc/passwd"}
{"order_id": "ORD-001", "language": "中文"}
这些输入分别检验缺字段、类型错误、额外字段、路径注入式值和枚举越界。
12.4 模型选择验证
构造覆盖相邻能力的用户请求:
帮我查订单 ORD-001 的物流。
帮我取消订单 ORD-001。
帮我找出最近三个月还没发货的订单。
我想知道 ORD-001 能不能取消,但先不要真的取消。
预期分别应路由到:
shipping_get_tracking
order_submit_cancel
order_list_unshipped
order_preview_cancel
如果模型在这些边界请求中经常混淆,优先检查名称和描述是否表达了动作差异,而不是立刻增加更多系统提示。
十三、规范保证、实现行为和工程建议要分开
规范或接口保证
这类内容来自协议或 API 约束,例如:
- 工具定义包含名称、描述和参数 Schema;
- MCP 通过
tools/list暴露工具; - MCP 工具调用参数应符合
inputSchema; - OpenAI strict mode 对
additionalProperties和required有明确要求; - 某些模型和配置支持并行工具调用。(developers.openai.com)
常见实现行为
这类内容并非所有平台都保证:
- 模型通常会利用工具描述进行选择;
- 更具体的描述通常比泛化描述更容易区分相邻工具;
- 工具数量增大后,全量 Schema 往往更难管理;
- 动态工具列表通常需要缓存和失效机制。
这些行为应通过评测验证,而不是当作协议承诺。
工程建议
这类内容属于系统设计取舍:
- 使用 namespace 避免跨系统名称冲突;
- 对高风险工具关闭并行调用;
- 让身份和租户来自可信上下文;
- 把硬约束放进 Schema,把授权和业务状态放进执行器;
- 为 Schema、名称和枚举设计兼容迁移策略。
它们不是单靠 Schema 就能实现的能力。
结语:Schema 是模型与系统之间的可计算边界
一个成熟的 Agent 工具 Schema 不是“函数签名加几行描述”,而是同时承担了四项工作:
- 命名建立稳定的工具路由;
- 描述定义模型的选择语境;
- 参数和枚举压缩输入空间;
- 可发现性决定工具是否在正确的时间、以正确的权限进入模型上下文。
可以用下面的判定式检查工具设计是否完整:
Schema 主要解决前两项的一部分;授权、超时、幂等、错误信封和状态一致性,则必须由工具执行器继续完成。
所以,工具 Schema 的目标不是让模型“看起来会调用函数”,而是让模型在一个明确、有限、可验证的行动空间中做选择。
系列导航与关联阅读
- 系列入口:Agent 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:Agent 结构化输出:JSON Schema、严格解析、修复和版本兼容
- 下一篇:Agent 工具执行器:严格解码、授权、超时、幂等和错误信封
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论