Spring AI @ToolParam 扩展注解改造实践
如何在 Spring AI 框架基础上扩展
@ToolParam注解,添加example和defaultValue支持,让 AI 模型更好地理解和使用工具参数。
前言
在使用 Spring AI 框架开发 MCP(Model Context Protocol)服务器时,我们发现框架提供的 @ToolParam 注解虽然能够描述参数的基本信息(描述和是否必填),但缺少示例值和默认值的支持。这对于 AI 模型理解参数格式和生成正确的调用参数来说,是一个不小的遗憾。
本文将详细介绍如何在不修改 Spring AI 框架源码的前提下,通过扩展注解和自定义 Schema 生成器的方式,为工具参数添加 example 和 defaultValue 支持。
目录
背景与需求
现状分析
Spring AI 框架提供了 @ToolParam 注解,用于描述工具方法的参数信息:
java
@Tool(name = "orderDetail", description = "查询订单详情")
public String orderDetail(
@ToolParam(description = "订单号", required = true) Long orderNo
) {
return "订单详情";
}
生成的 JSON Schema 如下:
json
{
"type": "object",
"properties": {
"orderNo": {
"type": "integer",
"description": "订单号"
}
},
"required": ["orderNo"]
}
问题与痛点
在实际使用中,我们发现以下问题:
- 缺少示例值:AI 模型无法直观了解参数的格式和内容
- 缺少默认值:无法为可选参数提供默认值,增加模型调用的复杂度
- 参数理解困难:特别是对于复杂对象参数,模型难以准确理解每个字段的期望值
需求目标
我们希望扩展 @ToolParam 注解,使其支持:
- ✅ example:参数的示例值,帮助 AI 模型理解参数格式
- ✅ defaultValue:参数的默认值,简化模型调用
- ✅ 向后兼容 :完全兼容现有的
@ToolParam注解 - ✅ 零侵入:不修改 Spring AI 框架源码
技术方案设计
方案选型
经过分析,我们确定了以下技术方案:
方案一:直接修改框架源码 ❌
优点:实现简单,直接修改框架代码即可
缺点:
- 框架升级时需要重新修改
- 维护成本高
- 不符合开闭原则
方案二:扩展注解 + 自定义生成器 ✅
优点:
- 完全独立,不依赖框架修改
- 易于维护和升级
- 可以灵活扩展
缺点:
- 需要理解框架内部实现
- 实现复杂度稍高
最终选择:方案二
架构设计
我们的扩展方案包含以下核心组件:
┌─────────────────────────────────────────┐
│ @ExtendedToolParam 注解 │
│ (扩展的注解,添加 example/defaultValue) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ ExtendedJsonSchemaGenerator │
│ (扩展的 Schema 生成器) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ ExtendedToolDefinitions │
│ (扩展的工具定义生成器) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ ExtendedMethodToolCallbackProvider │
│ (扩展的工具回调提供者) │
└─────────────────────────────────────────┘
核心流程
1. 工具方法注册
↓
2. ExtendedMethodToolCallbackProvider 扫描 @Tool 方法
↓
3. ExtendedToolDefinitions.from() 生成工具定义
↓
4. ExtendedJsonSchemaGenerator.generateForMethodInput() 生成 Schema
↓
5. 解析 @ExtendedToolParam 注解,提取 example 和 defaultValue
↓
6. 生成包含 examples 和 default 的 JSON Schema
核心实现
第一步:创建扩展注解
首先,我们创建了 @ExtendedToolParam 注解:
java
package com.echronos.mcp.annotation;
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtendedToolParam {
String description() default "";
boolean required() default true;
String example() default ""; // 新增:示例值
String defaultValue() default ""; // 新增:默认值
}
设计要点:
- 完全兼容
@ToolParam的所有属性 - 添加
example和defaultValue两个新属性 - 支持在方法参数和类字段上使用
第二步:实现扩展的 Schema 生成器
这是整个方案的核心部分。我们需要在框架生成的 Schema 基础上,添加 examples 和 default 字段。
设计思路
- 复用框架能力 :先调用框架的
JsonSchemaGenerator.generateForMethodInput()生成基础 Schema - 解析和增强:解析生成的 Schema,遍历所有参数
- 提取注解信息 :检查
@ExtendedToolParam注解,提取example和defaultValue - 递归处理:对于复杂对象参数,递归处理其字段上的注解
核心代码实现
java
public final class ExtendedJsonSchemaGenerator {
private static final ObjectMapper OBJECT_MAPPER = JsonParser.getObjectMapper();
public static String generateForMethodInput(Method method,
JsonSchemaGenerator.SchemaOption... schemaOptions) {
// 1. 使用框架方法生成基础 Schema
String baseSchema = JsonSchemaGenerator.generateForMethodInput(method, schemaOptions);
try {
// 2. 解析为基础 Schema
ObjectNode schema = (ObjectNode) OBJECT_MAPPER.readTree(baseSchema);
if (!schema.has("properties")) {
return baseSchema;
}
ObjectNode properties = (ObjectNode) schema.get("properties");
// 3. 遍历所有参数,添加 example 和 defaultValue
for (int i = 0; i < method.getParameterCount(); i++) {
String parameterName = method.getParameters()[i].getName();
Type parameterType = method.getGenericParameterTypes()[i];
// 跳过 ToolContext 类型参数
if (parameterType instanceof Class<?> parameterClass
&& ClassUtils.isAssignable(parameterClass, ToolContext.class)) {
continue;
}
if (!properties.has(parameterName)) {
continue;
}
ObjectNode parameterNode = (ObjectNode) properties.get(parameterName);
// 4. 添加 example 和 defaultValue
addExampleAndDefaultValue(parameterNode, method, i, parameterType);
}
return schema.toPrettyString();
} catch (Exception e) {
// 如果解析失败,返回原始 Schema
return baseSchema;
}
}
}
关键实现:参数处理逻辑
java
private static void addExampleAndDefaultValue(ObjectNode parameterNode,
Method method, int parameterIndex, Type parameterType) {
Parameter parameter = method.getParameters()[parameterIndex];
// 优先检查 @ExtendedToolParam
ExtendedToolParam extendedAnnotation = parameter.getAnnotation(ExtendedToolParam.class);
if (extendedAnnotation != null) {
addExample(parameterNode, extendedAnnotation.example());
addDefaultValue(parameterNode, extendedAnnotation.defaultValue());
// 如果是复杂对象类型,递归处理字段
if (parameterType instanceof Class<?>) {
addExampleAndDefaultFromClassFields(parameterNode, (Class<?>) parameterType);
}
return;
}
// 检查 @ToolParam
ToolParam toolParamAnnotation = parameter.getAnnotation(ToolParam.class);
if (toolParamAnnotation != null) {
if (parameterType instanceof Class<?>) {
addExampleAndDefaultFromClassFields(parameterNode, (Class<?>) parameterType);
}
return;
}
// ⚠️ 关键修复:即使参数上没有注解,也要处理嵌套对象的字段
// 这样可以确保复杂对象参数中的字段注解能够被正确处理
if (parameterType instanceof Class<?>) {
Class<?> clazz = (Class<?>) parameterType;
if (!clazz.isPrimitive() && clazz != String.class
&& !Number.class.isAssignableFrom(clazz)
&& clazz != Boolean.class && !clazz.isEnum()) {
addExampleAndDefaultFromClassFields(parameterNode, clazz);
}
}
}
递归处理嵌套对象
java
private static void addExampleAndDefaultFromClassFields(ObjectNode parameterNode, Class<?> clazz) {
if (!parameterNode.has("properties")) {
return;
}
ObjectNode properties = (ObjectNode) parameterNode.get("properties");
List<Field> fields = getAllDeclaredFields(clazz);
for (Field field : fields) {
String fieldName = field.getName();
if (!properties.has(fieldName)) {
continue;
}
ObjectNode fieldNode = (ObjectNode) properties.get(fieldName);
ExtendedToolParam extendedAnnotation = field.getAnnotation(ExtendedToolParam.class);
if (extendedAnnotation != null) {
addExample(fieldNode, extendedAnnotation.example());
addDefaultValue(fieldNode, extendedAnnotation.defaultValue());
}
// 递归处理嵌套对象
if (fieldNode.has("properties")) {
addExampleAndDefaultFromType(fieldNode, field.getGenericType());
}
}
}
Example 和 DefaultValue 的处理
java
private static void addExample(ObjectNode node, String example) {
if (!StringUtils.hasText(example)) {
return;
}
try {
// 尝试解析为 JSON 值(支持 JSON 格式的字符串)
Object exampleValue = OBJECT_MAPPER.readValue(example, Object.class);
ArrayNode examplesArray = OBJECT_MAPPER.createArrayNode();
examplesArray.add(OBJECT_MAPPER.valueToTree(exampleValue));
node.set("examples", examplesArray);
} catch (Exception e) {
// 如果解析失败,直接作为字符串
ArrayNode examplesArray = OBJECT_MAPPER.createArrayNode();
examplesArray.add(example);
node.set("examples", examplesArray);
}
}
private static void addDefaultValue(ObjectNode node, String defaultValue) {
if (!StringUtils.hasText(defaultValue)) {
return;
}
try {
// 尝试解析为 JSON 值
Object defaultVal = OBJECT_MAPPER.readValue(defaultValue, Object.class);
node.set("default", OBJECT_MAPPER.valueToTree(defaultVal));
} catch (Exception e) {
// 如果解析失败,直接作为字符串
node.put("default", defaultValue);
}
}
设计亮点:
- 支持 JSON 格式的字符串(如
"\"上海\"")和普通字符串(如"上海") - 自动处理解析失败的情况,确保健壮性
- 使用 JSON Schema 标准的
examples数组格式
第三步:扩展工具定义生成器
为了让扩展的 Schema 生成器生效,我们需要创建一个扩展的工具定义生成器:
java
public class ExtendedToolDefinitions {
public static ToolDefinition from(Method method) {
// 使用框架的 ToolDefinitions 获取基础定义
ToolDefinition baseDefinition = ToolDefinitions.from(method);
// 使用扩展生成器重新生成 Schema(包含 example 和 defaultValue)
String extendedSchema = ExtendedJsonSchemaGenerator.generateForMethodInput(method);
// 创建新的工具定义,使用扩展的 Schema
return ToolDefinition.builder()
.name(baseDefinition.name())
.description(baseDefinition.description())
.inputSchema(extendedSchema) // 使用扩展的 Schema
.build();
}
}
第四步:扩展工具回调提供者
最后,我们需要创建一个扩展的工具回调提供者,使用我们的扩展定义生成器:
java
public class ExtendedMethodToolCallbackProvider implements ToolCallbackProvider {
private final List<Object> toolObjects;
@Override
public ToolCallback[] getToolCallbacks() {
return this.toolObjects.stream()
.map(toolObject -> Stream
.of(ReflectionUtils.getDeclaredMethods(...))
.filter(toolMethod -> toolMethod.isAnnotationPresent(Tool.class))
.map(toolMethod -> MethodToolCallback.builder()
.toolDefinition(ExtendedToolDefinitions.from(toolMethod)) // 使用扩展的定义生成器
.toolMetadata(ToolMetadata.from(toolMethod))
.toolMethod(toolMethod)
.toolObject(toolObject)
.build())
.toArray(ToolCallback[]::new))
.flatMap(Stream::of)
.toArray(ToolCallback[]::new);
}
public static Builder builder() {
return new Builder();
}
}
使用示例
配置扩展的 Provider
在 Spring 配置类中,将所有的 MethodToolCallbackProvider 替换为 ExtendedMethodToolCallbackProvider:
java
@Configuration
public class McpServerConfig {
@Bean("AgentTools")
public ToolCallbackProvider AgentTools(AgentService service) {
return ExtendedMethodToolCallbackProvider.builder()
.toolObjects(service)
.build();
}
// ... 其他工具配置
}
示例一:简单参数
java
@Tool(name = "getUserInfo", description = "获取用户信息")
public String getUserInfo(
@ExtendedToolParam(
description = "用户ID",
required = true,
example = "12345",
defaultValue = "0"
) Long userId
) {
return "用户信息";
}
生成的 Schema:
json
{
"type": "object",
"properties": {
"userId": {
"type": "integer",
"description": "用户ID",
"examples": [12345],
"default": 0
}
},
"required": ["userId"]
}
示例二:复杂对象参数
java
@Data
public class WeatherQueryParam {
@ExtendedToolParam(
description = "地区",
required = true,
example = "上海",
defaultValue = "北京"
)
private String region;
@ExtendedToolParam(
description = "日期",
required = false,
example = "\"2024-01-01\"",
defaultValue = "\"今天\""
)
private String date;
}
@Tool(name = "获取天气", description = "获取某个地区的天气")
public String getWeather(WeatherQueryParam param) {
return "今天50度";
}
生成的 Schema:
json
{
"type": "object",
"properties": {
"param": {
"type": "object",
"properties": {
"region": {
"type": "string",
"description": "地区",
"examples": ["上海"],
"default": "北京"
},
"date": {
"type": "string",
"description": "日期",
"examples": ["2024-01-01"],
"default": "今天"
}
},
"required": ["region"]
}
},
"required": ["param"]
}
示例三:混合使用
@ExtendedToolParam 与 @ToolParam 可以混合使用,@ExtendedToolParam 优先级更高:
java
@Tool(name = "queryOrder", description = "查询订单")
public String queryOrder(
@ToolParam(description = "订单号") Long orderNo, // 使用标准注解
@ExtendedToolParam(
description = "是否包含详情",
example = "true",
defaultValue = "false"
) Boolean includeDetails // 使用扩展注解
) {
return "订单信息";
}
踩坑与解决方案
坑一:参数无注解时嵌套对象字段未被处理
问题描述:
最初实现时,如果方法参数上没有 @ExtendedToolParam 或 @ToolParam 注解,代码不会处理嵌套对象的字段。这导致即使对象字段上有 @ExtendedToolParam 注解,也无法生成 examples 和 default。
问题代码:
java
// ❌ 错误:如果参数没有注解,就不会处理嵌套对象
if (toolParamAnnotation != null) {
if (parameterType instanceof Class<?>) {
addExampleAndDefaultFromClassFields(parameterNode, (Class<?>) parameterType);
}
}
// 如果参数没有注解,这里就结束了,不会处理嵌套对象
解决方案:
java
// ✅ 正确:即使参数没有注解,也要处理嵌套对象
if (toolParamAnnotation != null) {
if (parameterType instanceof Class<?>) {
addExampleAndDefaultFromClassFields(parameterNode, (Class<?>) parameterType);
}
return;
}
// 关键修复:即使参数上没有注解,也要处理嵌套对象的字段
if (parameterType instanceof Class<?>) {
Class<?> clazz = (Class<?>) parameterType;
if (!clazz.isPrimitive() && clazz != String.class
&& !Number.class.isAssignableFrom(clazz)
&& clazz != Boolean.class && !clazz.isEnum()) {
addExampleAndDefaultFromClassFields(parameterNode, clazz);
}
}
坑二:ReflectionUtils API 使用错误
问题描述:
代码中使用了 ReflectionUtils.getAllDeclaredFields(),但该方法在 Spring 的 ReflectionUtils 中并不存在。
问题代码:
java
// ❌ 错误:方法不存在
Field[] fields = ReflectionUtils.getAllDeclaredFields(clazz);
解决方案:
java
// ✅ 正确:使用 doWithFields 收集字段
private static List<Field> getAllDeclaredFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
ReflectionUtils.doWithFields(clazz, fields::add);
return fields;
}
坑三:ToolDefinition API 理解错误
问题描述:
最初以为 ToolDefinition.builder() 使用 parameters(ObjectNode) 方法,但实际使用的是 inputSchema(String) 方法。
问题代码:
java
// ❌ 错误:方法不存在
return ToolDefinition.builder()
.name(baseDefinition.name())
.description(baseDefinition.description())
.parameters(extendedSchemaNode) // 方法不存在
.build();
解决方案:
java
// ✅ 正确:使用 inputSchema 方法
return ToolDefinition.builder()
.name(baseDefinition.name())
.description(baseDefinition.description())
.inputSchema(extendedSchema) // 使用字符串格式
.build();
坑四:example 和 defaultValue 格式处理
问题描述:
用户可能提供不同格式的值:
- 普通字符串:
example = "上海" - JSON 格式字符串:
example = "\"上海\"" - 数字字符串:
example = "123" - 布尔字符串:
example = "true"
解决方案:
java
private static void addExample(ObjectNode node, String example) {
if (!StringUtils.hasText(example)) {
return;
}
try {
// 尝试解析为 JSON 值(支持 JSON 格式)
Object exampleValue = OBJECT_MAPPER.readValue(example, Object.class);
ArrayNode examplesArray = OBJECT_MAPPER.createArrayNode();
examplesArray.add(OBJECT_MAPPER.valueToTree(exampleValue));
node.set("examples", examplesArray);
} catch (Exception e) {
// 如果解析失败,直接作为字符串(兼容普通字符串)
ArrayNode examplesArray = OBJECT_MAPPER.createArrayNode();
examplesArray.add(example);
node.set("examples", examplesArray);
}
}
这样既支持 JSON 格式,也兼容普通字符串,提供了最大的灵活性。
最佳实践
1. example 和 defaultValue 格式建议
字符串类型
java
// ✅ 推荐:直接使用字符串(代码会自动处理)
@ExtendedToolParam(example = "上海", defaultValue = "北京")
// ✅ 也可以:使用 JSON 格式(带引号)
@ExtendedToolParam(example = "\"上海\"", defaultValue = "\"北京\"")
数字类型
java
// ✅ 推荐:直接使用数字字符串
@ExtendedToolParam(example = "12345", defaultValue = "0")
布尔类型
java
// ✅ 推荐:直接使用布尔字符串
@ExtendedToolParam(example = "true", defaultValue = "false")
数组类型
java
// ✅ 必须使用 JSON 数组格式
@ExtendedToolParam(example = "[1, 2, 3]", defaultValue = "[]")
对象类型
java
// ✅ 必须使用 JSON 对象格式
@ExtendedToolParam(
example = "{\"key\": \"value\"}",
defaultValue = "{}"
)
2. 嵌套对象处理
对于复杂对象参数,建议在对象字段上使用 @ExtendedToolParam:
java
@Data
public class UserQueryParam {
@ExtendedToolParam(
description = "用户ID",
required = true,
example = "12345",
defaultValue = "0"
)
private Long userId;
@ExtendedToolParam(
description = "用户信息",
required = false
)
private UserInfo userInfo; // 嵌套对象
}
@Data
public class UserInfo {
@ExtendedToolParam(
description = "用户名",
example = "张三",
defaultValue = "未知"
)
private String name;
}
3. 向后兼容性
- ✅ 现有的
@ToolParam注解完全兼容,无需修改 - ✅ 可以逐步迁移到
@ExtendedToolParam - ✅ 两种注解可以混合使用
效果对比
改造前
json
{
"type": "object",
"properties": {
"region": {
"type": "string",
"description": "地区"
}
},
"required": ["region"]
}
问题:AI 模型不知道 "地区" 应该填什么值,可能生成错误的参数。
改造后
json
{
"type": "object",
"properties": {
"region": {
"type": "string",
"description": "地区",
"examples": ["上海"],
"default": "北京"
}
},
"required": ["region"]
}
优势:
- ✅ AI 模型可以通过
examples了解参数格式 - ✅ 可以使用
default作为默认值 - ✅ 提高工具调用的准确率
性能考虑
优化点
- 缓存机制:可以考虑对生成的 Schema 进行缓存,避免重复生成
- 懒加载:只在需要时才生成扩展的 Schema
- 批量处理:对于大量工具方法,可以考虑批量生成
当前实现
- 每次调用都会重新生成 Schema(简单但可能影响性能)
- 对于工具数量不多的场景,性能影响可忽略
- 如需优化,可以在
ExtendedToolDefinitions中添加缓存
总结与展望
总结
通过本次改造,我们成功实现了:
- ✅ 扩展注解 :创建了
@ExtendedToolParam注解,支持example和defaultValue - ✅ Schema 生成器 :实现了
ExtendedJsonSchemaGenerator,能够生成包含示例和默认值的 Schema - ✅ 工具定义生成器 :实现了
ExtendedToolDefinitions,集成扩展的 Schema 生成器 - ✅ 工具回调提供者 :实现了
ExtendedMethodToolCallbackProvider,替换框架默认实现 - ✅ 向后兼容 :完全兼容现有的
@ToolParam注解 - ✅ 零侵入:不修改 Spring AI 框架源码
技术亮点
- 🎯 设计模式:使用装饰器模式,在框架功能基础上扩展
- 🎯 反射机制:充分利用 Java 反射,动态解析注解
- 🎯 递归处理:支持嵌套对象的字段注解处理
- 🎯 容错机制:完善的异常处理,确保健壮性
实际效果
- 📈 提升准确率:AI 模型调用工具的准确率明显提升
- 📈 降低复杂度:通过默认值简化了模型调用逻辑
- 📈 改善体验:示例值帮助模型更好地理解参数格式
未来展望
- 更多注解属性 :可以考虑添加更多属性,如
min、max、pattern等 - 类型验证:可以添加类型验证逻辑,确保参数类型正确
- 文档生成:可以基于注解自动生成 API 文档
- IDE 支持:可以开发 IDE 插件,提供注解的智能提示
致谢
感谢 Spring AI 团队提供的优秀框架,让我们能够在框架基础上进行扩展,实现自己的需求。
附录
完整代码结构
src/main/java/com/echronos/mcp/
├── annotation/
│ └── ExtendedToolParam.java # 扩展注解
├── tool/
│ ├── ExtendedJsonSchemaGenerator.java # Schema 生成器
│ ├── ExtendedToolDefinitions.java # 工具定义生成器
│ ├── ExtendedMethodToolCallbackProvider.java # 工具回调提供者
│ └── ExtendedJsonSchemaGeneratorUsageExample.java # 使用示例
└── mcp/config/
└── McpServerConfig.java # 配置类