三点水网站建设合同,医院网站推广渠道,大气简约企业网站模板免费下载,服务器网站绑定域名Spring AI 1.1.0在 Tool 调用时#xff0c;很难让开发者监听开始调用Tool和结束调用Tool。这篇文章就是为了解决该问题。
Spring AI 1.1.0工具调用监控#xff1a;基于方法引用的最优雅强类型 Tool Callback 方案。
1. Spring AI 在 Tool 调用上的一个现实问题
Spring AI …Spring AI 1.1.0在 Tool 调用时很难让开发者监听开始调用Tool和结束调用Tool。这篇文章就是为了解决该问题。Spring AI 1.1.0工具调用监控基于方法引用的最优雅强类型 Tool Callback 方案。1. Spring AI 在 Tool 调用上的一个现实问题Spring AI 提供了非常方便的工具调用机制只要在 Bean 方法上加上Tool注解然后在对话时把这些 Bean 传给ChatClient模型就可以自行决定何时调用这些工具。典型的代码大概是这样FluxString flux chatClient.prompt() .user(帮我查一下北京现在的天气并顺便点评一下今天适合做什么运动) .tools(weatherTool) // WeatherTool 里有若干 Tool 方法 .stream() .content();这个能力很好用但有一个明显的缺口调用侧几乎感知不到工具的生命周期。通常会想做这些事在工具调用前告诉用户“正在调用某某工具”例如天气服务、OCR 服务。在工具成功返回后记录结果或者推送一段辅助说明到前端。在工具异常时做降级、兜底甚至区分是网络问题还是业务问题。只对某几个关键工具做这些监控而不是所有工具。Spring AI 自带的ToolCallback更偏向底层能力对上层业务来说缺少一个“强类型、按方法粒度订阅工具调用”的入口。2. 核心思路用方法引用绑定到具体工具方法目标很简单调用侧用方法引用声明自己关心哪个工具方法例如new ToolCallObserver(weatherTool::getCurrentWeather) { ... }在工具真正被调用时能够精确知道调用的是哪一个Tool方法Method对象。对应的目标对象是谁具体的 Bean 实例。调用前、调用后、调用异常的节点都能获得通知。最终生成一组ToolCallback[]交给 Spring AI 的ChatClient使用。Spring AI 自身已经提供了从 Bean 到ToolCallback[]的能力ToolCallbacks.from(...)所以这里只需要在它外面加一层观察逻辑即可。3. 原理从方法引用还原 Method 和目标对象关键在于“方法引用”这件事。对于下面这种写法SerializableFunctionString, String fn weatherTool::getCurrentWeather;编译器会生成一个“可序列化的 lambda 类”这个类实现了目标函数式接口这里是SerializableFunction。内部持有若干“捕获变量”其中一个就是weatherTool这个 Bean 实例。实现了一个writeReplace()方法用于序列化时返回SerializedLambda。通过反射调用这个writeReplace()可以拿到java.lang.invoke.SerializedLambda对象里面包含implClass真实实现类例如dev.w0fv1.ai_tool_callback.WeatherTool。implMethodName方法名例如getCurrentWeather。implMethodSignature方法签名。capturedArgs[n]捕获到的实例参数例如第 0 个就是weatherTool实例。这样就可以做到根据implClass implMethodName ( implMethodSignature)还原为真实的Method对象。根据capturedArgs[0]拿到当时绑定的 Bean 实例。接下来只要在工具真正执行时把“当次实际调用的Method”和“我们事先从方法引用解析出的Method”做一次对比就能知道这次是不是要通知对应的观察者。整个过程不依赖其他信息也不会受到方法重命名或参数变化的影响——编译器会负责在方法引用处校验签名。4. 配套代码下面给出一个完整的实现组合SerializableFunction为了让方法引用成为“可序列化 lambda”。ToolCallObserverRegistry管理所有工具观察者并生成 Spring AI 所需的ToolCallback[]。在上层业务中如何使用。4.1 SerializableFunction可序列化的函数式接口你要监控的Tool的方法需要实现一个可序列化的函数接口没有它我们无法从lambda中得到object和method。比如下面的是一个传入一个参数有返回的方法的函数式接口。你可以提前写一个到多个传入参数的接口。// 文件路径src/main/java/dev/w0fv1/ai_tool_callback/SerializableFunction.java package dev.w0fv1.ai_tool_callback; import java.io.Serializable; import java.util.function.Function; /** * 可序列化的 Function用于支持方法引用解析 Method。 */ FunctionalInterface public interface SerializableFunctionT, R extends FunctionT, R, Serializable { }以后有更多签名需求可以按同样模式定义SerializableConsumer、SerializableBiFunction等。4.2 ToolCallObserverRegistry工具调用观察注册中心直接复制到你的仓库里就行。// 文件路径src/main/java/dev/w0fv1/ai_tool_callback/ToolCallObserverRegistry.java package dev.w0fv1.ai_tool_callback; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.support.ToolCallbacks; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.definition.ToolDefinition; import org.springframework.ai.tool.metadata.ToolMetadata; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import java.io.Serializable; import java.lang.invoke.SerializedLambda; import java.lang.reflect.Method; import java.util.*; /** * 专门管理 Tool 调用监听的注册中心 * 1. 保存所有 ToolCallObserver强类型方法引用。 * 2. 在工具执行前/后/异常时分发事件。 * 3. 根据方法引用推导出需要暴露给 Spring AI 的 ToolCallback[]。 */ Slf4j public class ToolCallObserverRegistry { /** * 工具调用观察者抽象类 * - 持有一个方法引用 T methodRef必须可序列化。 * - 提供 before/after/error 钩子。 */ public static abstract class ToolCallObserverT extends Serializable implements Serializable { private final T methodRef; protected ToolCallObserver(T methodRef) { this.methodRef methodRef; } public T methodRef() { return methodRef; } public void before(ToolDefinition toolDefinition, Object[] args) { } public void after(ToolDefinition toolDefinition, Object[] args, Object result) { } public void error(ToolDefinition toolDefinition, Object[] args, Throwable throwable) { } } private final ListToolCallObserver? observers new ArrayList(); public void addObserver(ToolCallObserver? observer) { if (observer ! null) { this.observers.add(observer); } } public ListToolCallObserver? getObservers() { return Collections.unmodifiableList(observers); } // 对外暴露的通知入口由内部包装 ToolCallback 调用 public void notifyBefore(ToolDefinition toolDefinition, Object target, Method invokedMethod, Object[] args) { for (ToolCallObserver? observer : observers) { if (!matches(observer, target, invokedMethod)) { continue; } try { observer.before(toolDefinition, args); } catch (Exception e) { log.warn(ToolCallObserver before 执行异常, tool{}, toolDefinition.name(), e); } } } public void notifyAfter(ToolDefinition toolDefinition, Object target, Method invokedMethod, Object[] args, Object result) { for (ToolCallObserver? observer : observers) { if (!matches(observer, target, invokedMethod)) { continue; } try { observer.after(toolDefinition, args, result); } catch (Exception e) { log.warn(ToolCallObserver after 执行异常, tool{}, toolDefinition.name(), e); } } } public void notifyError(ToolDefinition toolDefinition, Object target, Method invokedMethod, Object[] args, Throwable throwable) { for (ToolCallObserver? observer : observers) { if (!matches(observer, target, invokedMethod)) { continue; } try { observer.error(toolDefinition, args, throwable); } catch (Exception e) { log.warn(ToolCallObserver error 执行异常, tool{}, toolDefinition.name(), e); } } } // 构造 Spring AI 用的 ToolCallback[]只包含需要监控的 Tool /** * 基于当前注册的 ToolCallObserver(methodRef)推导出对应的工具实例 * 并构造一组可直接给 ChatClient 使用的 Spring AI ToolCallback[]。 * * 注意 * - 只包含“需要监控”的工具。 * - 不需要监控的工具直接用 ToolCallbacks.from(otherBeans...) 单独生成再合并。 */ public ToolCallback[] buildSpringToolCallbacks() { if (observers.isEmpty()) { return new ToolCallback[0]; } // 1. 从所有 methodRef 中解析出绑定的工具实例对象 ListObject toolBeans new ArrayList(); for (ToolCallObserver? observer : observers) { Object fn observer.methodRef(); if (fn null) { continue; } Object target resolveTargetFromLambda(fn); if (target null) { continue; } boolean exists false; for (Object bean : toolBeans) { if (bean target) { exists true; break; } } if (!exists) { toolBeans.add(target); } } if (toolBeans.isEmpty()) { return new ToolCallback[0]; } return buildObservedCallbacks(this, toolBeans.toArray()); } // 内部实现lambda 解析 ToolCallback 包装 private boolean matches(ToolCallObserver? observer, Object target, Method invokedMethod) { Object fn observer.methodRef(); if (fn null) { // 未指定 methodRef认为匹配所有工具 return true; } Method keyMethod resolveMethodFromLambda(fn); if (keyMethod null) { return false; } return keyMethod.equals(invokedMethod); } /** * 从方法引用 / lambda 中解析底层 Method。 */ private static Method resolveMethodFromLambda(Object lambda) { try { Method writeReplace lambda.getClass().getDeclaredMethod(writeReplace); writeReplace.setAccessible(true); Object serializedForm writeReplace.invoke(lambda); if (!(serializedForm instanceof SerializedLambda)) { return null; } SerializedLambda sl (SerializedLambda) serializedForm; String implClassName sl.getImplClass().replace(/, .); String implMethodName sl.getImplMethodName(); Class? implClass Class.forName(implClassName); Method[] methods implClass.getDeclaredMethods(); for (Method m : methods) { if (!m.getName().equals(implMethodName)) { continue; } m.setAccessible(true); return m; } return null; } catch (Exception e) { log.warn(解析方法引用失败(获取 Method)lambda{}, lambda, e); return null; } } /** * 从方法引用 / lambda 中解析绑定实例对象。 * * 仅对绑定实例方法引用有效例如weatherTool::getCurrentWeather */ private static Object resolveTargetFromLambda(Object lambda) { try { Method writeReplace lambda.getClass().getDeclaredMethod(writeReplace); writeReplace.setAccessible(true); Object serializedForm writeReplace.invoke(lambda); if (!(serializedForm instanceof SerializedLambda)) { return null; } SerializedLambda sl (SerializedLambda) serializedForm; int capturedCount sl.getCapturedArgCount(); if (capturedCount 0) { return null; } return sl.getCapturedArg(0); } catch (Exception e) { log.warn(解析方法引用失败(获取 target)lambda{}, lambda, e); return null; } } /** * 根据 toolBeans 构建带监控的 ToolCallback[]。 */ private static ToolCallback[] buildObservedCallbacks(ToolCallObserverRegistry registry, Object... toolBeans) { Assert.notNull(toolBeans, toolBeans 不能为空); if (toolBeans.length 0) { return new ToolCallback[0]; } // 1. 基础 ToolCallback 数组Spring AI 官方工具 ToolCallback[] baseCallbacks ToolCallbacks.from(toolBeans); if (registry.getObservers().isEmpty()) { // 没有观察者就不包裹直接返回 return baseCallbacks; } // 2. 建立 toolName - (target, method) 映射 MapString, TargetMethod toolNameIndex buildToolNameIndex(toolBeans); ToolCallback[] observed new ToolCallback[baseCallbacks.length]; for (int i 0; i baseCallbacks.length; i) { ToolCallback base baseCallbacks[i]; String toolName base.getToolDefinition().name(); TargetMethod tm toolNameIndex.get(toolName); if (tm null) { log.warn(未在 toolBeans 中找到名称为 {} 的工具方法将不进行调用观察, toolName); observed[i] base; continue; } observed[i] new ObservingToolCallback( base, registry, tm.target(), tm.method() ); } return observed; } private static MapString, TargetMethod buildToolNameIndex(Object[] toolBeans) { MapString, TargetMethod index new HashMap(); for (Object bean : toolBeans) { Class? userClass ClassUtils.getUserClass(bean); Method[] methods userClass.getMethods(); for (Method method : methods) { Tool ann AnnotatedElementUtils.findMergedAnnotation(method, Tool.class); if (ann null) { continue; } String name ann.name(); if (name null || name.isBlank()) { name method.getName(); } if (index.containsKey(name)) { throw new IllegalStateException(检测到重复的工具名称: name 请确保所有 Tool 的 name 唯一); } index.put(name, new TargetMethod(bean, method)); } } return index; } private record TargetMethod(Object target, Method method) { } /** * 静态内置类对 Spring AI 的 ToolCallback 进行包装 * 在调用前/后/异常时把事件转发给 ToolCallObserverRegistry。 */ private static class ObservingToolCallback implements ToolCallback { private final ToolCallback delegate; private final ToolCallObserverRegistry registry; private final Object target; private final Method method; private ObservingToolCallback(ToolCallback delegate, ToolCallObserverRegistry registry, Object target, Method method) { this.delegate delegate; this.registry registry; this.target target; this.method method; } Override public ToolDefinition getToolDefinition() { return delegate.getToolDefinition(); } Override public ToolMetadata getToolMetadata() { return delegate.getToolMetadata(); } Override public String call(String toolInput) { ToolDefinition def delegate.getToolDefinition(); Object[] args new Object[]{toolInput}; registry.notifyBefore(def, target, method, args); try { String result delegate.call(toolInput); registry.notifyAfter(def, target, method, args, result); return result; } catch (RuntimeException e) { registry.notifyError(def, target, method, args, e); throw e; } } Override public String call(String toolInput, ToolContext toolContext) { ToolDefinition def delegate.getToolDefinition(); Object[] args new Object[]{toolInput}; registry.notifyBefore(def, target, method, args); try { String result delegate.call(toolInput, toolContext); registry.notifyAfter(def, target, method, args, result); return result; } catch (RuntimeException e) { registry.notifyError(def, target, method, args, e); throw e; } } } }4.3 使用示例注册观察者 挂到 ChatClient假设有一个闲聊服务在这里想监控“天气查询工具”的调用情况// 文件路径src/main/java/dev/w0fv1/ai_tool_callback/ConversationService.java package dev.w0fv1.ai_tool_callback; import dev.w0fv1.ai_tool_callback.ToolCallObserverRegistry; import dev.w0fv1.ai_tool_callback.WeatherTool; import dev.w0fv1.ai_tool_callback.SerializableFunction; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.tool.ToolCallback; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import java.util.List; Slf4j Service public class ConversationService { private final ChatClient chatClient; private final WeatherTool weatherTool; public ConversationService(OpenAiChatModel textQuality, WeatherTool weatherTool) { this.chatClient ChatClient.builder(textQuality).build(); this.weatherTool weatherTool; } public FluxString chat(String userInput) { // 1. 创建工具调用观察注册中心并注册一个针对 WeatherTool.getCurrentWeather 的观察者 ToolCallObserverRegistry registry new ToolCallObserverRegistry(); registry.addObserver( new ToolCallObserverRegistry.ToolCallObserverSerializableFunctionString, String(weatherTool::getCurrentWeather) { Override public void before(org.springframework.ai.tool.definition.ToolDefinition toolDefinition, Object[] args) { log.info(开始执行工具{}, args{}, toolDefinition.name(), args); } Override public void after(org.springframework.ai.tool.definition.ToolDefinition toolDefinition, Object[] args, Object result) { log.info(工具执行完成{}, result{}, toolDefinition.name(), result); } Override public void error(org.springframework.ai.tool.definition.ToolDefinition toolDefinition, Object[] args, Throwable throwable) { log.error(工具执行异常{}, toolDefinition.name(), throwable); } } ); // 2. 由注册中心生成“需要监控”的 ToolCallback[] ToolCallback[] observedCallbacks registry.buildSpringToolCallbacks(); // 3. 构造请求并挂上这些回调 ChatClient.ChatClientRequestSpec spec chatClient.prompt() .user(userInput); return spec .toolCallbacks(observedCallbacks) .stream() .content(); } }如果还有一些工具完全不想监控可以额外用ToolCallback[] plain ToolCallbacks.from(otherToolBean1, otherToolBean2); // 然后合并 observedCallbacks plain 再传给 ChatClient5. 小结整个方案的关键点利用“可序列化方法引用 SerializedLambda”从调用侧的xxx::method还原到具体的Method Bean 实例。把“谁关心哪个工具方法”抽象成ToolCallObserverRegistryToolCallObserver。使用 Spring AI 官方提供的ToolCallbacks.from(...)作为基础再在外面包一层ObservingToolCallback做调用前/后/异常通知。所有 API 都围绕方法引用展开和业务代码天然对齐代码改动在编译期就能暴露问题。最终效果是在不入侵 Spring AI 核心机制的前提下为工具调用加上了一条强类型、可订阅、可扩展的“旁路”上层可以非常精细地监控和控制每一次 Tool 调用的生命周期。