第2002篇:Tool Calling的工程化设计——Java函数注解与工具注册体系
2026/4/30大约 6 分钟
第2002篇:Tool Calling的工程化设计——Java函数注解与工具注册体系
适读人群:正在构建AI Agent系统的Java工程师 | 阅读时长:约20分钟 | 核心价值:用注解驱动的方式简化工具注册,让添加新工具变成10分钟的事
重构完第一版Agent框架后,我发现了一个让人头疼的问题:每次要给Agent加新工具,都要手动实现AgentTool接口,写name()、description()、parametersSchema()这些方法,然后把工具注册到工具列表里。
写了三四个工具之后,这个流程已经让我觉得很烦了。
在Java后端世界,Swagger注解、Spring MVC注解、Hibernate注解……我们早就习惯了用注解来描述代码意图,让框架去做重复的事。为什么给Agent添加工具不能一样简单?
后来我设计了一套基于注解的Tool Calling框架,让添加新工具变成这样:
@AgentToolProvider
@Component
public class OrderTools {
@Tool(
name = "query_order",
description = "查询订单信息,包括状态、交货期、客户信息"
)
public String queryOrder(
@ToolParam(name = "order_id", description = "订单ID", required = true) String orderId
) {
// 实现逻辑
return orderService.getOrderInfo(orderId);
}
}一个注解搞定,框架自动扫描并注册。这篇文章就是这套机制的完整实现。
注解定义
先定义两个核心注解:
/**
* 标记一个类包含Agent工具方法
* 标了这个注解的类会被框架扫描,其中@Tool方法会自动注册
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AgentToolProvider {
String value() default ""; // 可选的工具提供者名称
}
/**
* 标记一个方法是Agent工具
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Tool {
String name(); // 工具名称(LLM调用时使用)
String description(); // 工具功能描述(放进Prompt告知LLM)
// 如果工具调用有副作用(如发邮件、修改数据),设为true
// 未来可以用这个字段做"高风险操作需二次确认"的逻辑
boolean hasSideEffects() default false;
}
/**
* 标记工具方法的参数
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ToolParam {
String name(); // 参数名
String description(); // 参数说明
boolean required() default true; // 是否必填
String defaultValue() default ""; // 默认值(仅对可选参数有意义)
// 参数类型约束,会体现在JSON Schema中
String type() default "string"; // string/number/integer/boolean/array/object
String[] enumValues() default {}; // 枚举值(如果有)
}工具注册表的核心
框架的核心是ToolRegistry,它负责扫描注解并把方法包装成可执行的工具:
@Component
@Slf4j
public class ToolRegistry {
// 所有已注册的工具,按名称索引
private final Map<String, RegisteredTool> tools = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 从标注了@AgentToolProvider的Bean中扫描并注册工具
* 在Spring容器初始化完成后调用
*/
@EventListener(ApplicationReadyEvent.class)
public void scanAndRegisterTools(ApplicationReadyEvent event) {
ApplicationContext context = event.getApplicationContext();
// 找到所有标注了@AgentToolProvider的Bean
Map<String, Object> providers = context.getBeansWithAnnotation(AgentToolProvider.class);
for (Map.Entry<String, Object> entry : providers.entrySet()) {
String beanName = entry.getKey();
Object bean = entry.getValue();
registerToolsFromBean(beanName, bean);
}
log.info("工具注册完成,共注册 {} 个工具: {}",
tools.size(), String.join(", ", tools.keySet()));
}
private void registerToolsFromBean(String beanName, Object bean) {
Class<?> clazz = AopUtils.getTargetClass(bean); // 处理AOP代理
for (Method method : clazz.getMethods()) {
Tool toolAnnotation = method.getAnnotation(Tool.class);
if (toolAnnotation == null) continue;
String toolName = toolAnnotation.name();
if (tools.containsKey(toolName)) {
log.warn("工具名 '{}' 已存在(来自{}),跳过重复注册(来自{})",
toolName, tools.get(toolName).getBeanName(), beanName);
continue;
}
// 构建工具的JSON Schema
String schema = buildParametersSchema(method);
RegisteredTool registeredTool = RegisteredTool.builder()
.name(toolName)
.description(toolAnnotation.description())
.hasSideEffects(toolAnnotation.hasSideEffects())
.parametersSchema(schema)
.bean(bean)
.method(method)
.beanName(beanName)
.build();
tools.put(toolName, registeredTool);
log.debug("注册工具: {} (来自 {}.{})", toolName, beanName, method.getName());
}
}
/**
* 根据方法参数注解自动生成JSON Schema
*/
private String buildParametersSchema(Method method) {
Map<String, Object> schema = new LinkedHashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new LinkedHashMap<>();
List<String> required = new ArrayList<>();
Parameter[] params = method.getParameters();
for (Parameter param : params) {
ToolParam paramAnnotation = param.getAnnotation(ToolParam.class);
if (paramAnnotation == null) continue; // 没有注解的参数跳过(如内部注入的参数)
Map<String, Object> propDef = new LinkedHashMap<>();
propDef.put("type", paramAnnotation.type());
propDef.put("description", paramAnnotation.description());
if (!paramAnnotation.defaultValue().isEmpty()) {
propDef.put("default", paramAnnotation.defaultValue());
}
if (paramAnnotation.enumValues().length > 0) {
propDef.put("enum", List.of(paramAnnotation.enumValues()));
}
properties.put(paramAnnotation.name(), propDef);
if (paramAnnotation.required()) {
required.add(paramAnnotation.name());
}
}
schema.put("properties", properties);
if (!required.isEmpty()) {
schema.put("required", required);
}
try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(schema);
} catch (JsonProcessingException e) {
return "{}";
}
}
/**
* 执行指定工具
*/
public String executeTool(String toolName, Map<String, Object> params) {
RegisteredTool tool = tools.get(toolName);
if (tool == null) {
return "Error: 工具 '" + toolName + "' 不存在。" +
"可用工具: " + String.join(", ", tools.keySet());
}
try {
// 把Map参数绑定到方法参数
Object[] args = bindArguments(tool.getMethod(), params);
Object result = tool.getMethod().invoke(tool.getBean(), args);
if (result == null) return "操作成功(无返回值)";
if (result instanceof String) return (String) result;
// 非字符串返回值,序列化为JSON
return objectMapper.writeValueAsString(result);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
log.error("工具 {} 执行抛出异常", toolName, cause);
return "Error: " + (cause != null ? cause.getMessage() : e.getMessage());
} catch (Exception e) {
log.error("工具 {} 执行失败", toolName, e);
return "Error: " + e.getMessage();
}
}
private Object[] bindArguments(Method method, Map<String, Object> params) {
Parameter[] parameters = method.getParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
Parameter param = parameters[i];
ToolParam paramAnnotation = param.getAnnotation(ToolParam.class);
if (paramAnnotation == null) {
args[i] = null;
continue;
}
String paramName = paramAnnotation.name();
Object value = params.get(paramName);
if (value == null && paramAnnotation.required()) {
throw new IllegalArgumentException("必填参数 '" + paramName + "' 未提供");
}
// 类型转换
args[i] = convertType(value, param.getType(), paramAnnotation.defaultValue());
}
return args;
}
private Object convertType(Object value, Class<?> targetType, String defaultValue) {
if (value == null) {
if (!defaultValue.isEmpty()) {
value = defaultValue;
} else {
return null;
}
}
if (targetType.isAssignableFrom(value.getClass())) return value;
String strValue = value.toString();
if (targetType == Integer.class || targetType == int.class) return Integer.parseInt(strValue);
if (targetType == Long.class || targetType == long.class) return Long.parseLong(strValue);
if (targetType == Double.class || targetType == double.class) return Double.parseDouble(strValue);
if (targetType == Boolean.class || targetType == boolean.class) return Boolean.parseBoolean(strValue);
if (targetType == LocalDate.class) return LocalDate.parse(strValue);
return strValue; // 默认转字符串
}
/**
* 获取所有工具的描述信息(用于构建Prompt)
*/
public List<ToolDescription> getAllToolDescriptions() {
return tools.values().stream()
.map(t -> ToolDescription.builder()
.name(t.getName())
.description(t.getDescription())
.parametersSchema(t.getParametersSchema())
.hasSideEffects(t.isHasSideEffects())
.build())
.collect(Collectors.toList());
}
}使用注解定义工具
有了这套框架,定义工具变得非常简洁:
@AgentToolProvider
@Component
@RequiredArgsConstructor
public class OrderTools {
private final OrderService orderService;
private final CustomerService customerService;
@Tool(name = "query_order", description = "查询订单详细信息")
public String queryOrder(
@ToolParam(name = "order_id", description = "订单ID,格式O-YYYYMMDD") String orderId
) {
return orderService.getOrderInfoJson(orderId);
}
@Tool(
name = "update_delivery_date",
description = "修改订单的计划交货日期",
hasSideEffects = true // 标记有副作用
)
public String updateDeliveryDate(
@ToolParam(name = "order_id", description = "订单ID") String orderId,
@ToolParam(name = "new_date", description = "新日期,格式YYYY-MM-DD") String newDate,
@ToolParam(name = "reason", description = "修改原因", required = false) String reason
) {
return orderService.updateDeliveryDate(orderId, LocalDate.parse(newDate), reason);
}
@Tool(name = "list_delayed_orders", description = "列出所有延迟的订单")
public String listDelayedOrders(
@ToolParam(
name = "days_threshold",
description = "延迟天数阈值,只返回延迟超过此天数的订单",
required = false,
defaultValue = "1",
type = "integer"
) Integer daysThreshold
) {
List<Order> delayed = orderService.findDelayedOrders(daysThreshold);
return "共 " + delayed.size() + " 个延迟订单: " +
delayed.stream().map(Order::getId).collect(Collectors.joining(", "));
}
}
@AgentToolProvider
@Component
@RequiredArgsConstructor
public class NotificationTools {
private final EmailService emailService;
private final SmsService smsService;
@Tool(
name = "send_email",
description = "发送邮件给客户或内部人员",
hasSideEffects = true
)
public String sendEmail(
@ToolParam(name = "to", description = "收件人邮箱") String to,
@ToolParam(name = "subject", description = "邮件主题") String subject,
@ToolParam(name = "body", description = "邮件正文") String body
) {
emailService.send(to, subject, body);
return "邮件已发送到 " + to;
}
@Tool(
name = "send_sms",
description = "发送短信通知",
hasSideEffects = true
)
public String sendSms(
@ToolParam(name = "phone", description = "手机号") String phone,
@ToolParam(name = "message", description = "短信内容,不超过70字") String message
) {
if (message.length() > 70) {
return "Error: 短信内容超过70字限制";
}
smsService.send(phone, message);
return "短信已发送到 " + phone;
}
}高风险工具的二次确认机制
对于hasSideEffects=true的工具(如发邮件、修改数据库),我加了一层确认机制:
@Service
@RequiredArgsConstructor
public class SafeToolExecutor {
private final ToolRegistry toolRegistry;
/**
* 执行工具,对高风险操作进行确认
*/
public ToolExecutionResult execute(
String toolName,
Map<String, Object> params,
boolean userConfirmedSideEffects) {
RegisteredTool tool = toolRegistry.findTool(toolName);
if (tool == null) {
return ToolExecutionResult.error("工具不存在: " + toolName);
}
// 如果工具有副作用且用户没有确认
if (tool.isHasSideEffects() && !userConfirmedSideEffects) {
return ToolExecutionResult.needsConfirmation(
"工具 '" + toolName + "' 会产生不可逆的操作: " + tool.getDescription() +
"\n参数: " + params +
"\n是否确认执行?"
);
}
String result = toolRegistry.executeTool(toolName, params);
return ToolExecutionResult.success(result);
}
}这套基于注解的工具注册框架,让我们团队添加新工具的成本从"半天"变成了"10分钟"——写个方法,加几个注解,重启服务,Agent就能用了。
最重要的是,工具的文档(描述、参数说明)和实现代码放在一起,不会出现"文档和实现对不上"的问题。
