JSON Schema 在 Tool 定义中的最佳实践——让 AI 准确理解工具
JSON Schema 在 Tool 定义中的最佳实践——让 AI 准确理解工具
上个月帮一个团队排查一个很诡异的问题:他们的 AI 助手在查询数据库时,有时候传进来的日期格式是 2024-01-15,有时候是 01/15/2024,有时候甚至是「2024年1月15日」。后端程序直接崩,因为日期解析失败了。
我看了他们的工具定义,问题一眼就看出来了:
{
"name": "queryOrdersByDate",
"description": "按日期查询订单",
"parameters": {
"type": "object",
"properties": {
"date": {
"type": "string",
"description": "日期"
}
}
}
}description 就写了「日期」两个字。AI 不知道要什么格式,只能猜。猜出来的结果当然五花八门。
这不是 AI 的问题,是工具描述写得太烂了。
这篇文章要讲的就是:Tool Schema 怎么写才能让 AI 准确理解并正确调用,以及一些我在实际项目里踩过的坑。
一、Tool Schema 的本质是什么
先说清楚背景。当你给 LLM 配置工具时,工具定义通常是 JSON Schema 格式。LLM 在生成工具调用时,会把你的描述当作「说明书」来理解:这个工具是干什么的、需要什么参数、参数的约束是什么。
这个「说明书」写得好不好,直接决定了 AI 调得准不准。
JSON Schema 本来是用来做数据验证的,但在 Tool Calling 里,它同时承担了两个职责:
- 类型约束:确保参数格式正确(字符串、数字、枚举等)
- 语义说明:告诉 AI 这个参数的含义、取值范围、使用场景
第二点才是最关键的,也是最容易被忽视的。
二、好的 vs 坏的 Tool Schema 对比
我把几个典型场景的好坏对比列出来,直接看代码差异。
2.1 日期参数
坏的写法:
{
"name": "queryOrdersByDate",
"description": "按日期查询订单",
"parameters": {
"type": "object",
"properties": {
"date": {
"type": "string",
"description": "日期"
}
},
"required": ["date"]
}
}好的写法:
{
"name": "queryOrdersByDate",
"description": "按指定日期查询该天的所有订单列表,只查当天数据,不支持范围查询",
"parameters": {
"type": "object",
"properties": {
"date": {
"type": "string",
"description": "查询日期,格式必须为 ISO 8601 标准:YYYY-MM-DD,例如 2026-04-24。不支持其他日期格式",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$",
"examples": ["2026-04-24", "2025-12-31"]
}
},
"required": ["date"]
}
}差异在哪里:
- 工具的
description说清楚了「只查当天、不支持范围」,AI 不会误用 - 参数的
description明确了格式是YYYY-MM-DD,举了例子 - 加了
pattern正则约束,模型生成时会参考这个约束 - 加了
examples,对 AI 的参数生成有很强的引导作用
2.2 枚举参数
坏的写法:
{
"name": "filterOrdersByStatus",
"description": "按状态过滤订单",
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "订单状态"
}
}
}
}好的写法:
{
"name": "filterOrdersByStatus",
"description": "按指定状态筛选订单,每次只能筛选一种状态,需要多状态时请多次调用",
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "订单状态枚举值。PENDING_PAYMENT=待付款,PENDING_SHIPMENT=待发货,SHIPPED=已发货,COMPLETED=已完成,CANCELLED=已取消,REFUNDING=退款中",
"enum": ["PENDING_PAYMENT", "PENDING_SHIPMENT", "SHIPPED", "COMPLETED", "CANCELLED", "REFUNDING"]
}
},
"required": ["status"]
}
}枚举参数必须用 enum 字段列出所有合法值,同时在 description 里解释每个值的含义(用中文对应英文枚举值),让 AI 在理解用户意图后能选到正确的枚举值。
2.3 嵌套对象参数
坏的写法:
{
"name": "createOrder",
"description": "创建订单",
"parameters": {
"type": "object",
"properties": {
"order": {
"type": "object",
"description": "订单信息"
}
}
}
}好的写法:
{
"name": "createOrder",
"description": "创建一笔新订单,订单创建成功后会自动触发库存锁定,请确保商品库存充足再调用此接口",
"parameters": {
"type": "object",
"properties": {
"order": {
"type": "object",
"description": "订单详细信息",
"properties": {
"customerId": {
"type": "string",
"description": "客户ID,格式为 C+ 6位数字,如 C001234。可通过 searchCustomer 工具获取"
},
"items": {
"type": "array",
"description": "订单商品列表,至少包含一个商品,最多 50 个商品",
"minItems": 1,
"maxItems": 50,
"items": {
"type": "object",
"properties": {
"productId": {
"type": "string",
"description": "商品ID,格式为 P+ 6位数字,如 P001234"
},
"quantity": {
"type": "integer",
"description": "购买数量,必须大于0,单次最多购买 999 件",
"minimum": 1,
"maximum": 999
}
},
"required": ["productId", "quantity"]
}
},
"deliveryAddress": {
"type": "object",
"description": "收货地址",
"properties": {
"province": {
"type": "string",
"description": "省份,如「上海市」「广东省」,使用完整名称"
},
"city": {
"type": "string",
"description": "城市,如「上海市」「广州市」"
},
"district": {
"type": "string",
"description": "区/县,如「浦东新区」「天河区」"
},
"street": {
"type": "string",
"description": "详细街道地址,精确到门牌号"
}
},
"required": ["province", "city", "street"]
},
"remark": {
"type": "string",
"description": "订单备注,可选,最长 200 字符,用于填写特殊要求",
"maxLength": 200
}
},
"required": ["customerId", "items", "deliveryAddress"]
}
},
"required": ["order"]
}
}嵌套对象的描述要递归到最细粒度,每个字段都要有说明。特别是:
- 字段格式(
C+ 6位数字) - 字段来源(
可通过 searchCustomer 工具获取)——这个提示很重要,能引导 AI 先查客户再创建订单 - 边界值(
minItems: 1, maxItems: 50)
2.4 可选参数的处理
可选参数容易被忽视,但它的描述方式会影响 AI 是否「乱填」。
坏的写法:
{
"pageSize": {
"type": "integer",
"description": "分页大小"
}
}好的写法:
{
"pageSize": {
"type": "integer",
"description": "每页返回的记录数,可选,默认为 20,有效范围 1-100。数字越大返回越慢,建议不超过 50",
"default": 20,
"minimum": 1,
"maximum": 100
}
}注意:可选参数不要放在 required 数组里,同时在 description 里说明默认值,避免 AI 每次都填一个随机数。
三、Java/Spring AI 中的 Schema 定义方式
Spring AI 支持用注解来定义工具,注解会自动生成 JSON Schema,但你需要在注解里写清楚描述。
3.1 注解方式(推荐)
@Component
public class OrderManagementTools {
@Tool(description = "按指定日期查询当天的订单列表,只支持精确日期查询,不支持日期范围")
public List<OrderDTO> queryOrdersByDate(
@ToolParam(description = "查询日期,格式为 YYYY-MM-DD,如 2026-04-24")
@Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}")
String date,
@ToolParam(description = "订单状态过滤,可选。合法值:PENDING_PAYMENT/PENDING_SHIPMENT/SHIPPED/COMPLETED/CANCELLED,不传则返回所有状态")
@Nullable String status,
@ToolParam(description = "分页页码,从1开始,默认为1")
@Min(1) Integer page,
@ToolParam(description = "每页记录数,有效范围1-50,默认20")
@Min(1) @Max(50) Integer pageSize
) {
// 实现逻辑
return orderRepository.findByDateAndStatus(date, status, page, pageSize);
}
@Tool(description = """
创建新订单。
前置条件:
1. customerId 必须是已存在的客户ID,可通过 searchCustomer 工具查询
2. 所有商品的 productId 必须是有效商品,可通过 searchProduct 工具确认
3. 创建前建议通过 checkInventory 工具确认库存充足
注意:此操作不可撤销,请确认参数正确后再调用
""")
public OrderCreateResult createOrder(
@ToolParam(description = "客户ID,格式为 C+6位数字")
String customerId,
@ToolParam(description = "商品列表,每项包含 productId(商品ID)和 quantity(购买数量,1-999)")
List<OrderItemParam> items,
@ToolParam(description = "收货地址,需包含省份、城市、详细地址")
DeliveryAddress address,
@ToolParam(description = "订单备注,可选,最长200字")
@Nullable @Size(max = 200) String remark
) {
// 实现逻辑
return orderService.createOrder(customerId, items, address, remark);
}
}3.2 手动构建 JSON Schema(精细控制)
如果注解方式满足不了你的需求(比如需要 oneOf、anyOf 等复杂约束),可以手动构建:
@Service
public class ToolSchemaBuilder {
/**
* 构建一个带复杂约束的 Tool Schema
*/
public JsonNode buildQueryTool() {
ObjectMapper mapper = new ObjectMapper();
ObjectNode schema = mapper.createObjectNode();
schema.put("name", "queryData");
schema.put("description", "多维度数据查询工具,支持按时间范围或特定状态查询,两种方式二选一");
ObjectNode params = schema.putObject("parameters");
params.put("type", "object");
ObjectNode props = params.putObject("properties");
// 时间范围查询(oneOf 约束)
ObjectNode timeRange = props.putObject("timeRange");
timeRange.put("type", "object");
timeRange.put("description", "时间范围查询,startDate 和 endDate 必须同时提供");
// 嵌套属性
ObjectNode timeProps = timeRange.putObject("properties");
ObjectNode startDate = timeProps.putObject("startDate");
startDate.put("type", "string");
startDate.put("description", "开始日期,YYYY-MM-DD 格式,如 2026-01-01");
startDate.put("pattern", "^\\d{4}-\\d{2}-\\d{2}$");
ObjectNode endDate = timeProps.putObject("endDate");
endDate.put("type", "string");
endDate.put("description", "结束日期,YYYY-MM-DD 格式,必须晚于或等于 startDate,最大范围90天");
endDate.put("pattern", "^\\d{4}-\\d{2}-\\d{2}$");
// required 在嵌套对象里
ArrayNode timeRequired = timeRange.putArray("required");
timeRequired.add("startDate");
timeRequired.add("endDate");
// 状态枚举
ObjectNode statusFilter = props.putObject("statusFilter");
statusFilter.put("type", "string");
statusFilter.put("description", "按状态筛选:ACTIVE=活跃,INACTIVE=不活跃,SUSPENDED=暂停");
ArrayNode enumValues = statusFilter.putArray("enum");
enumValues.add("ACTIVE");
enumValues.add("INACTIVE");
enumValues.add("SUSPENDED");
// 限制:timeRange 和 statusFilter 二选一(在 description 里说明)
// JSON Schema 的 oneOf 模型有时候 LLM 不完全遵守,description 更可靠
return schema;
}
}3.3 Schema 验证工具
写完 Schema 后,最好做一次验证,确保生成的 Schema 是合法的:
@Component
public class ToolSchemaValidator {
private final JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(
SpecVersion.VersionFlag.V7
);
/**
* 验证工具定义的参数是否符合 JSON Schema 规范
*/
public ValidationResult validateToolSchema(String schemaJson) {
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode schemaNode = mapper.readTree(schemaJson);
// 检查必填字段
List<String> errors = new ArrayList<>();
if (!schemaNode.has("name")) errors.add("缺少 name 字段");
if (!schemaNode.has("description")) errors.add("缺少 description 字段");
if (schemaNode.has("description") &&
schemaNode.get("description").asText().length() < 10) {
errors.add("description 描述太短,请提供详细说明(至少10字符)");
}
// 检查所有参数是否有 description
validatePropertiesHaveDescription(
schemaNode.path("parameters").path("properties"),
"",
errors
);
return new ValidationResult(errors.isEmpty(), errors);
} catch (Exception e) {
return new ValidationResult(false, List.of("Schema 解析失败: " + e.getMessage()));
}
}
private void validatePropertiesHaveDescription(
JsonNode properties, String path, List<String> errors) {
if (properties.isMissingNode()) return;
properties.fields().forEachRemaining(entry -> {
String fieldPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
JsonNode field = entry.getValue();
if (!field.has("description") || field.get("description").asText().isBlank()) {
errors.add("参数 [" + fieldPath + "] 缺少 description");
}
// 递归检查嵌套对象
if ("object".equals(field.path("type").asText())) {
validatePropertiesHaveDescription(
field.path("properties"), fieldPath, errors
);
}
});
}
public record ValidationResult(boolean valid, List<String> errors) {}
}四、不同描述方式的调用准确率测试
我做了一个测试,对比四种不同质量的 Tool Schema,在相同的 50 个测试问题下的调用准确率。
测试工具:日期范围查询接口(需要传 startDate、endDate、status 三个参数)
测试问题示例:
- 「查一下上个月已完成的订单」
- 「最近两周发货的有多少」
- 「给我看一下三月份退款中的订单」
Level 1(42% 准确率):
{
"startDate": {"type": "string"},
"endDate": {"type": "string"},
"status": {"type": "string"}
}AI 经常传错格式,比如 startDate: "上个月" 或 status: "完成"。
Level 2(68% 准确率):
{
"startDate": {"type": "string", "description": "开始日期"},
"endDate": {"type": "string", "description": "结束日期"},
"status": {"type": "string", "description": "订单状态"}
}格式错误减少了,但枚举值还是会猜,比如传 "completed" 而不是 "COMPLETED"。
Level 3(87% 准确率):
{
"startDate": {
"type": "string",
"description": "查询开始日期,ISO 8601 格式 YYYY-MM-DD",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"endDate": {
"type": "string",
"description": "查询结束日期,ISO 8601 格式 YYYY-MM-DD,必须晚于等于 startDate",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"status": {
"type": "string",
"description": "订单状态,不传则查询所有状态",
"enum": ["PENDING_PAYMENT", "PENDING_SHIPMENT", "SHIPPED", "COMPLETED", "CANCELLED", "REFUNDING"]
}
}Level 4(96% 准确率): 在 Level 3 基础上加上枚举值的中文含义解释、加上 examples、工具整体的 description 更详细:
{
"name": "queryOrdersByDateRange",
"description": "按日期范围查询订单,支持可选的状态过滤。日期范围最大90天,超过会报错",
"parameters": {
...
"status": {
"type": "string",
"description": "订单状态过滤,可选。PENDING_PAYMENT=待付款,PENDING_SHIPMENT=待发货,SHIPPED=已发货,COMPLETED=已完成,CANCELLED=已取消,REFUNDING=退款中。不传此参数则返回所有状态的订单",
"enum": ["PENDING_PAYMENT", "PENDING_SHIPMENT", "SHIPPED", "COMPLETED", "CANCELLED", "REFUNDING"]
},
"startDate": {
...
"examples": ["2026-04-01", "2026-01-15"]
}
}
}从 42% 到 96%,差距非常大。而且这 96% 的准确率,是在没有任何额外 Prompt 工程的情况下实现的,纯靠 Schema 质量提升。
五、常见的 Schema 反模式
5.1 工具功能过于宽泛
// 坏的
{
"name": "doQuery",
"description": "执行查询",
"parameters": {
"query": {"type": "string", "description": "查询内容"}
}
}这种「万能工具」设计会让 AI 不知道传什么,结果完全不可预测。工具应该职责单一,每个工具做一件具体的事。
5.2 参数名用缩写或非标准命名
// 坏的
{
"dt": {"type": "string", "description": "日期"},
"pg": {"type": "integer", "description": "页码"},
"sts": {"type": "string", "description": "状态"}
}AI 理解参数名也有语义理解,清晰的参数名(date、page、status)比缩写更容易让 AI 正确赋值。
5.3 混淆输入和输出字段
// 坏的——把输出字段也放进了参数定义
{
"orderId": {"type": "string", "description": "订单ID,由系统自动生成"},
"createTime": {"type": "string", "description": "创建时间,系统自动填充"}
}只有输入参数才应该出现在 parameters 里,输出字段不要放进去,会让 AI 困惑要不要传这些值。
5.4 description 里写实现细节而不是使用说明
// 坏的
{
"description": "调用 OrderRepository.findByDateAndStatusIn() 方法查询数据库 t_order 表"
}
// 好的
{
"description": "查询指定日期范围内指定状态的订单,返回分页结果"
}AI 不需要知道你怎么实现的,它需要知道的是「这个工具能做什么、适合什么场景调用」。
六、动态 Schema 生成(高级用法)
在一些企业场景里,工具的参数可能会根据业务配置动态变化。比如不同的客户有不同的枚举值,这时候需要动态生成 Schema:
@Service
public class DynamicToolSchemaService {
@Autowired
private OrderStatusConfigRepository statusConfigRepo;
/**
* 根据客户配置动态生成工具 Schema
*/
public Map<String, Object> buildOrderQuerySchema(String tenantId) {
// 从数据库加载该租户的订单状态配置
List<OrderStatusConfig> statusConfigs = statusConfigRepo.findByTenantId(tenantId);
// 动态构建枚举值和描述
List<String> enumValues = statusConfigs.stream()
.map(OrderStatusConfig::getCode)
.toList();
String enumDescription = statusConfigs.stream()
.map(c -> c.getCode() + "=" + c.getDisplayName())
.collect(Collectors.joining(","));
return Map.of(
"name", "queryOrders",
"description", "查询租户 " + tenantId + " 的订单,支持按状态过滤",
"parameters", Map.of(
"type", "object",
"properties", Map.of(
"status", Map.of(
"type", "string",
"description", "订单状态,可选。" + enumDescription,
"enum", enumValues
),
"date", Map.of(
"type", "string",
"description", "查询日期,YYYY-MM-DD 格式",
"pattern", "^\\d{4}-\\d{2}-\\d{2}$"
)
)
)
);
}
}动态 Schema 要注意:
- 枚举值变更后要及时刷新(不要缓存太久)
- Schema 里的描述要跟着业务语言走,不要用技术代码
七、工具之间的关联说明
当你有多个工具时,它们之间可能存在调用顺序关系。好的 Schema 会在 description 里说明这种关系:
@Tool(description = "搜索客户信息。在创建订单前必须先调用此工具获取客户ID,不能直接猜测或填写客户ID")
public CustomerDTO searchCustomer(
@ToolParam(description = "客户手机号或姓名,支持模糊匹配") String keyword
) { ... }
@Tool(description = "创建订单。调用前必须先通过 searchCustomer 工具获取有效的 customerId,以及通过 searchProduct 工具确认商品存在")
public OrderCreateResult createOrder(
@ToolParam(description = "客户ID,必须是通过 searchCustomer 工具查询到的有效ID,格式为 C+6位数字") String customerId,
@ToolParam(description = "商品列表") List<OrderItemParam> items
) { ... }这种「前置条件」描述方式,能有效引导 AI 按正确顺序调用工具,避免它直接猜一个客户ID去创建订单。
八、Schema 维护的工程建议
8.1 统一 Schema 的审查入口
在 CI 流程里加一个 Schema 质量检查步骤:
@Test
void toolSchemaQualityCheck() {
// 获取所有 @Tool 注解的方法
ApplicationContext ctx = SpringApplication.run(Application.class);
Map<String, Object> toolBeans = ctx.getBeansWithAnnotation(Component.class);
List<String> schemaErrors = new ArrayList<>();
for (Object bean : toolBeans.values()) {
for (Method method : bean.getClass().getMethods()) {
Tool toolAnnotation = method.getAnnotation(Tool.class);
if (toolAnnotation == null) continue;
// 检查工具描述长度
if (toolAnnotation.description().length() < 20) {
schemaErrors.add(String.format(
"工具 %s.%s 的 description 太短(%d字符),至少需要20字符",
bean.getClass().getSimpleName(),
method.getName(),
toolAnnotation.description().length()
));
}
// 检查所有参数都有 @ToolParam 注解
for (Parameter param : method.getParameters()) {
if (param.getAnnotation(ToolParam.class) == null &&
!param.getName().equals("this")) {
schemaErrors.add(String.format(
"工具 %s.%s 的参数 %s 缺少 @ToolParam 注解",
bean.getClass().getSimpleName(),
method.getName(),
param.getName()
));
}
}
}
}
if (!schemaErrors.isEmpty()) {
fail("Tool Schema 质量检查失败:\n" + String.join("\n", schemaErrors));
}
}8.2 Schema 变更要做版本管理
工具 Schema 的变更会影响 AI 的行为,要像接口文档一样对待:
- 每次修改记录变更原因
- 不能随意删除或重命名参数(AI 可能有缓存的上下文)
- 枚举值的增删需要评估对已有对话的影响
九、小结
Tool Schema 写得好不好,是 AI 应用质量的地基。我见过太多团队花了大量时间调 Prompt,却忽视了最基础的 Schema 质量。
一个好的 Tool Schema 应该:
- 工具 description:说清楚工具的用途、适用场景、前置条件、注意事项
- 参数 description:说清楚格式要求、取值范围、与其他参数的关系
- 枚举参数:必须用
enum列出合法值,并在描述里解释每个值的含义 - 可选参数:说明默认值,让 AI 知道不传时的行为
- 嵌套对象:递归到最细字段,每个字段都有描述
从 42% 到 96% 的准确率提升,不需要更好的模型,只需要更好的工具描述。这是性价比最高的 AI 应用质量提升方式。
