Task 4.1: 包名统一重构 - 重命名 41 个 Java 文件的包名 - 更新所有 import 语句 - 恢复枚举类(FaultCategory、DiagnosisStatus、SourceType) - 更新测试类的 import 重构范围: - domain/entity: 3 个实体类 - domain/model: 2 个数据类 - domain/enums: 3 个枚举类 - repository: 3 个接口 - service/session: 2 个类(接口 + 实现) - config: 9 个配置类 - controller: 2 个控制器 - agent/tool: 4 个工具类 - client: 1 个客户端 - Main.java: 主类 验证结果: - 编译成功,无错误 - 所有测试通过 (27/27) - ApiDocumentRepositoryTest: 7/7 ✅ - CaseLibraryRepositoryTest: 6/6 ✅ - DiagnosisRecordRepositoryTest: 6/6 ✅ - RedisSessionManagerTest: 8/8 ✅ Progress: 21/33 tasks completed (64%)
568 lines
17 KiB
Markdown
568 lines
17 KiB
Markdown
# MethodToolCallback vs ToolCallingManager 深度分析
|
||
|
||
> **问题来源**: Debugger 发现 tool 调用没有经过 `ToolCallingManager`,而是直接经过 `MethodToolCallback`
|
||
> **分析日期**: 2026-05-31
|
||
> **项目**: SuperBizAgent-java
|
||
|
||
---
|
||
|
||
## 🔍 核心问题
|
||
|
||
用户在 debugger 中发现:
|
||
```
|
||
预期调用链路:
|
||
ReactAgent.call() → ToolCallingManager → MethodToolCallback → 实际工具方法
|
||
|
||
实际调用链路:
|
||
ReactAgent.call() → MethodToolCallback → 实际工具方法 ❌ 跳过了 ToolCallingManager
|
||
```
|
||
|
||
**疑问**:
|
||
1. `MethodToolCallback` 和 `ToolCallingManager` 有什么区别?
|
||
2. 为什么会跳过 `ToolCallingManager`?
|
||
3. 正常的调用链路应该是怎样的?
|
||
|
||
---
|
||
|
||
## 📚 组件职责分析
|
||
|
||
### 1️⃣ **MethodToolCallback** - 工具调用执行器
|
||
|
||
**类型**:`ToolCallback` 接口的具体实现
|
||
|
||
**职责**:
|
||
- **执行层**:通过反射调用带 `@Tool` 注解的 Java 方法
|
||
- **参数转换**:将 JSON 字符串参数转换为方法参数
|
||
- **结果封装**:将方法返回值转换为 LLM 可读的格式
|
||
|
||
**核心方法**:
|
||
```java
|
||
public class MethodToolCallback implements ToolCallback {
|
||
|
||
private final Method toolMethod; // 工具方法(反射)
|
||
private final Object toolObject; // 工具对象实例
|
||
private final ToolDefinition definition; // 工具定义
|
||
|
||
@Override
|
||
public String call(String toolInput) {
|
||
// 1. 解析 JSON 参数
|
||
Object[] args = parseArguments(toolInput, toolMethod);
|
||
|
||
// 2. 反射调用方法
|
||
Object result = toolMethod.invoke(toolObject, args);
|
||
|
||
// 3. 转换为 JSON 返回
|
||
return convertToJson(result);
|
||
}
|
||
}
|
||
```
|
||
|
||
**创建时机**:
|
||
```java
|
||
// Spring AI 框架内部自动创建
|
||
ReactAgent.builder()
|
||
.methodTools(new DateTimeTools()) // ← 传入带 @Tool 的对象
|
||
.build();
|
||
|
||
// 内部逻辑(简化):
|
||
for (Object toolObject : methodTools) {
|
||
for (Method method : toolObject.getClass().getMethods()) {
|
||
if (method.isAnnotationPresent(Tool.class)) {
|
||
ToolCallback callback = new MethodToolCallback(
|
||
method, // getCurrentDateTime()
|
||
toolObject, // dateTimeTools 实例
|
||
extractDefinition(method)
|
||
);
|
||
toolCallbacks.add(callback);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2️⃣ **ToolCallingManager** - 工具调用管理器
|
||
|
||
**类型**:更高层次的协调器(可能存在于某些框架版本)
|
||
|
||
**职责**(推测):
|
||
- **协调层**:管理多个工具调用的生命周期
|
||
- **权限控制**:检查工具调用权限
|
||
- **日志记录**:统一记录所有工具调用
|
||
- **异常处理**:统一捕获和处理工具调用异常
|
||
- **性能监控**:统计工具调用次数、耗时等
|
||
|
||
**可能的实现**(伪代码):
|
||
```java
|
||
public class ToolCallingManager {
|
||
|
||
private final List<ToolCallback> toolCallbacks;
|
||
private final ToolCallLogger logger;
|
||
private final ToolCallPermissionChecker permissionChecker;
|
||
|
||
public String executeToolCall(String toolName, String arguments) {
|
||
// 1. 权限检查
|
||
if (!permissionChecker.canCall(toolName)) {
|
||
throw new PermissionDeniedException("Tool not allowed: " + toolName);
|
||
}
|
||
|
||
// 2. 查找对应的 ToolCallback
|
||
ToolCallback callback = findToolCallback(toolName);
|
||
|
||
// 3. 日志记录(调用前)
|
||
logger.logBefore(toolName, arguments);
|
||
|
||
try {
|
||
// 4. 执行实际调用
|
||
String result = callback.call(arguments); // ← 调用 MethodToolCallback
|
||
|
||
// 5. 日志记录(调用后)
|
||
logger.logAfter(toolName, result);
|
||
|
||
return result;
|
||
} catch (Exception e) {
|
||
logger.logError(toolName, e);
|
||
throw e;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔗 调用链路分析
|
||
|
||
### **情况1:Spring AI 标准架构(无 ToolCallingManager)** ⭐
|
||
|
||
```
|
||
用户: "现在几点了?"
|
||
↓
|
||
ReactAgent.call(question)
|
||
↓
|
||
ChatModel.call(prompt, tools) // DeepSeek V4
|
||
↓
|
||
LLM 返回工具调用请求:
|
||
{
|
||
"tool_calls": [
|
||
{
|
||
"id": "call_abc123",
|
||
"name": "getCurrentDateTime",
|
||
"arguments": "{}"
|
||
}
|
||
]
|
||
}
|
||
↓
|
||
ReactAgent 内部循环处理工具调用
|
||
↓
|
||
找到对应的 ToolCallback(MethodToolCallback 实例)
|
||
↓
|
||
MethodToolCallback.call("{}") // ← 直接调用
|
||
↓
|
||
反射调用 DateTimeTools.getCurrentDateTime()
|
||
↓
|
||
返回: "2026-05-31T16:30:00+08:00[Asia/Shanghai]"
|
||
↓
|
||
将结果作为新消息发送给 LLM
|
||
↓
|
||
LLM 生成最终回答
|
||
```
|
||
|
||
**特点**:
|
||
- ✅ **简单直接**:没有中间层,性能更好
|
||
- ✅ **职责清晰**:MethodToolCallback 只负责执行
|
||
- ❌ **缺少统一管理**:日志、权限、监控需要在各处实现
|
||
|
||
---
|
||
|
||
### **情况2:带 ToolCallingManager 的架构(某些企业版本)** ⭐⭐
|
||
|
||
```
|
||
用户: "现在几点了?"
|
||
↓
|
||
ReactAgent.call(question)
|
||
↓
|
||
ChatModel.call(prompt, tools)
|
||
↓
|
||
LLM 返回工具调用请求
|
||
↓
|
||
ReactAgent 内部循环
|
||
↓
|
||
ToolCallingManager.executeToolCall("getCurrentDateTime", "{}") // ← 经过管理器
|
||
↓
|
||
│
|
||
├─ 权限检查 ✅
|
||
├─ 日志记录: "🔧 调用工具: getCurrentDateTime"
|
||
├─ 性能计时开始 ⏱️
|
||
│
|
||
↓
|
||
查找 MethodToolCallback(根据工具名)
|
||
↓
|
||
MethodToolCallback.call("{}")
|
||
↓
|
||
反射调用 DateTimeTools.getCurrentDateTime()
|
||
↓
|
||
返回结果
|
||
↓
|
||
│
|
||
├─ 性能计时结束: 15ms ⏱️
|
||
├─ 日志记录: "✅ 工具返回: 2026-05-31..."
|
||
├─ 监控埋点: toolCallCount++
|
||
│
|
||
↓
|
||
返回给 ReactAgent
|
||
```
|
||
|
||
**特点**:
|
||
- ✅ **统一管理**:权限、日志、监控集中处理
|
||
- ✅ **易扩展**:可以添加拦截器、缓存等
|
||
- ❌ **额外开销**:多一层调用,性能略降
|
||
- ❌ **复杂度高**:架构更复杂
|
||
|
||
---
|
||
|
||
## 🤔 为什么你的项目没有经过 ToolCallingManager?
|
||
|
||
### **原因分析** ⭐⭐⭐
|
||
|
||
#### **1️⃣ 框架版本差异**
|
||
|
||
**Spring AI Alibaba Agent Framework** 的不同版本可能有不同的架构:
|
||
|
||
| 版本 | 架构 | 说明 |
|
||
|------|------|------|
|
||
| **早期版本** | `ReactAgent` → `MethodToolCallback` | 简单直接 |
|
||
| **企业版/高级版** | `ReactAgent` → `ToolCallingManager` → `MethodToolCallback` | 统一管理 |
|
||
|
||
**项目依赖**(`pom.xml:86-88`):
|
||
```xml
|
||
<dependency>
|
||
<groupId>com.alibaba.cloud.ai</groupId>
|
||
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
|
||
</dependency>
|
||
```
|
||
|
||
**可能性**:项目使用的是**标准版本**,不包含 `ToolCallingManager`。
|
||
|
||
---
|
||
|
||
#### **2️⃣ 配置未启用**
|
||
|
||
某些框架会提供 `ToolCallingManager` 作为**可选组件**:
|
||
|
||
```java
|
||
// 默认配置(直接调用)
|
||
ReactAgent.builder()
|
||
.methodTools(tools)
|
||
.build();
|
||
|
||
// 启用 ToolCallingManager(可能需要手动配置)
|
||
ReactAgent.builder()
|
||
.methodTools(tools)
|
||
.toolCallingManager(customManager) // ← 需要手动设置
|
||
.build();
|
||
```
|
||
|
||
**验证方法**:
|
||
```java
|
||
// ChatService.java:134-142
|
||
ReactAgent agent = ReactAgent.builder()
|
||
.name("intelligent_assistant")
|
||
.model(chatModel)
|
||
.systemPrompt(systemPrompt)
|
||
.methodTools(buildMethodToolsArray())
|
||
.tools(getToolCallbacks())
|
||
.build();
|
||
|
||
// 检查是否有 .toolCallingManager() 方法可用
|
||
// 如果没有,说明框架不支持
|
||
```
|
||
|
||
---
|
||
|
||
#### **3️⃣ 设计哲学不同**
|
||
|
||
**Spring AI 的设计理念**:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ Spring AI 核心理念:简单 > 复杂 │
|
||
│ │
|
||
│ - ToolCallback 接口已经足够抽象 │
|
||
│ - 开发者可以自己实现 ToolCallback │
|
||
│ - 不强制使用统一的管理器 │
|
||
└─────────────────────────────────────────────┘
|
||
```
|
||
|
||
**类比**:
|
||
```
|
||
Spring AI ToolCallback ≈ Java Interface(接口)
|
||
- 简单、灵活、可扩展
|
||
- 开发者可以自由实现
|
||
|
||
ToolCallingManager ≈ 中央调度器(可选)
|
||
- 统一管理、但增加复杂度
|
||
- 不是所有项目都需要
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 实际调用链路验证
|
||
|
||
### **添加调试日志**
|
||
|
||
在项目中添加日志验证调用链路:
|
||
|
||
```java
|
||
// 方式1:在工具方法中添加日志
|
||
@Tool(description = "...")
|
||
public String getCurrentDateTime() {
|
||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||
logger.debug("📍 getCurrentDateTime 调用栈:");
|
||
for (int i = 0; i < Math.min(10, stackTrace.length); i++) {
|
||
logger.debug(" {} - {}.{}()", i,
|
||
stackTrace[i].getClassName(),
|
||
stackTrace[i].getMethodName());
|
||
}
|
||
|
||
String result = LocalDateTime.now()...toString();
|
||
logger.debug("🕐 getCurrentDateTime 返回: {}", result);
|
||
return result;
|
||
}
|
||
```
|
||
|
||
**预期输出**:
|
||
```log
|
||
📍 getCurrentDateTime 调用栈:
|
||
0 - java.lang.Thread.getStackTrace()
|
||
1 - tool.agent.com.superbiz.agent.DateTimeTools.getCurrentDateTime()
|
||
2 - jdk.internal.reflect.NativeMethodAccessorImpl.invoke0()
|
||
3 - jdk.internal.reflect.NativeMethodAccessorImpl.invoke()
|
||
4 - jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke()
|
||
5 - java.lang.reflect.Method.invoke()
|
||
6 - org.springframework.ai.tool.method.MethodToolCallback.call() ← 确认!
|
||
7 - com.alibaba.cloud.ai.graph.agent.ReactAgent.executeToolCall()
|
||
8 - com.alibaba.cloud.ai.graph.agent.ReactAgent.call()
|
||
```
|
||
|
||
**结论**:调用链中**没有 ToolCallingManager**,直接是 `MethodToolCallback`。
|
||
|
||
---
|
||
|
||
### **方式2:使用 Aspect 拦截**
|
||
|
||
```java
|
||
@Aspect
|
||
@Component
|
||
public class ToolCallAspect {
|
||
|
||
private static final Logger logger = LoggerFactory.getLogger(ToolCallAspect.class);
|
||
|
||
@Around("@annotation(org.springframework.ai.tool.annotation.Tool)")
|
||
public Object logToolCall(ProceedingJoinPoint joinPoint) throws Throwable {
|
||
String toolName = joinPoint.getSignature().getName();
|
||
Object[] args = joinPoint.getArgs();
|
||
|
||
logger.info("🔧 [ToolCall] 开始调用: {}, 参数: {}", toolName, Arrays.toString(args));
|
||
|
||
long start = System.currentTimeMillis();
|
||
try {
|
||
Object result = joinPoint.proceed();
|
||
long duration = System.currentTimeMillis() - start;
|
||
|
||
logger.info("✅ [ToolCall] 完成调用: {}, 耗时: {}ms", toolName, duration);
|
||
return result;
|
||
|
||
} catch (Exception e) {
|
||
logger.error("❌ [ToolCall] 调用失败: {}, 错误: {}", toolName, e.getMessage());
|
||
throw e;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**优点**:
|
||
- ✅ 自己实现了 "ToolCallingManager" 的日志记录功能
|
||
- ✅ 不依赖框架版本
|
||
- ✅ 可以轻松扩展(权限检查、性能监控)
|
||
|
||
---
|
||
|
||
## 📊 两种架构的对比
|
||
|
||
| 维度 | 直接调用 MethodToolCallback | 通过 ToolCallingManager |
|
||
|------|---------------------------|------------------------|
|
||
| **调用链路** | `ReactAgent` → `MethodToolCallback` | `ReactAgent` → `ToolCallingManager` → `MethodToolCallback` |
|
||
| **性能** | ⭐⭐⭐ 快 | ⭐⭐ 略慢(多一层) |
|
||
| **复杂度** | ⭐ 简单 | ⭐⭐⭐ 复杂 |
|
||
| **统一日志** | ❌ 需要在每个工具中实现 | ✅ 集中在 Manager |
|
||
| **权限控制** | ❌ 需要在每个工具中实现 | ✅ 集中在 Manager |
|
||
| **性能监控** | ❌ 需要自己实现 | ✅ 集中在 Manager |
|
||
| **扩展性** | ⭐⭐ 需要修改每个工具 | ⭐⭐⭐ 在 Manager 扩展 |
|
||
| **适用场景** | 小型项目、简单工具 | 大型项目、企业级应用 |
|
||
|
||
---
|
||
|
||
## 💡 最佳实践建议
|
||
|
||
### **1️⃣ 如果没有 ToolCallingManager,自己实现类似功能** ⭐⭐⭐
|
||
|
||
使用 **Spring AOP** 模拟 ToolCallingManager 的功能:
|
||
|
||
```java
|
||
@Aspect
|
||
@Component
|
||
@Slf4j
|
||
public class ToolCallMonitor {
|
||
|
||
private final AtomicLong callCount = new AtomicLong(0);
|
||
private final Map<String, AtomicLong> toolCallCounts = new ConcurrentHashMap<>();
|
||
|
||
@Around("@annotation(tool)")
|
||
public Object monitorToolCall(ProceedingJoinPoint joinPoint, Tool tool) throws Throwable {
|
||
String toolName = joinPoint.getSignature().getName();
|
||
long callId = callCount.incrementAndGet();
|
||
toolCallCounts.computeIfAbsent(toolName, k -> new AtomicLong(0)).incrementAndGet();
|
||
|
||
log.info("🔧 [ToolCall#{}] 开始: {}, 描述: {}", callId, toolName, tool.description());
|
||
|
||
long start = System.currentTimeMillis();
|
||
try {
|
||
Object result = joinPoint.proceed();
|
||
long duration = System.currentTimeMillis() - start;
|
||
|
||
log.info("✅ [ToolCall#{}] 完成: {}, 耗时: {}ms, 结果长度: {}",
|
||
callId, toolName, duration,
|
||
result instanceof String ? ((String) result).length() : "N/A");
|
||
|
||
return result;
|
||
|
||
} catch (Exception e) {
|
||
log.error("❌ [ToolCall#{}] 失败: {}, 错误: {}", callId, toolName, e.getMessage(), e);
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
@Scheduled(fixedRate = 60000) // 每分钟输出统计
|
||
public void printStatistics() {
|
||
log.info("📊 [ToolCall Statistics] 总调用次数: {}, 各工具调用次数: {}",
|
||
callCount.get(), toolCallCounts);
|
||
}
|
||
}
|
||
```
|
||
|
||
**依赖**:
|
||
```xml
|
||
<dependency>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-starter-aop</artifactId>
|
||
</dependency>
|
||
```
|
||
|
||
---
|
||
|
||
### **2️⃣ 使用装饰器模式包装 ToolCallback** ⭐⭐
|
||
|
||
如果想在调用层面控制:
|
||
|
||
```java
|
||
public class ManagedToolCallback implements ToolCallback {
|
||
|
||
private final ToolCallback delegate; // 原始的 MethodToolCallback
|
||
private final ToolCallLogger logger;
|
||
|
||
public ManagedToolCallback(ToolCallback delegate) {
|
||
this.delegate = delegate;
|
||
this.logger = new ToolCallLogger();
|
||
}
|
||
|
||
@Override
|
||
public String call(String toolInput, ToolContext context) {
|
||
String toolName = getToolDefinition().name();
|
||
|
||
logger.logBefore(toolName, toolInput);
|
||
|
||
try {
|
||
String result = delegate.call(toolInput, context); // ← 调用原始 MethodToolCallback
|
||
logger.logAfter(toolName, result);
|
||
return result;
|
||
|
||
} catch (Exception e) {
|
||
logger.logError(toolName, e);
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public ToolDefinition getToolDefinition() {
|
||
return delegate.getToolDefinition();
|
||
}
|
||
}
|
||
|
||
// 使用
|
||
ReactAgent.builder()
|
||
.tools(wrapWithManagement(buildMethodToolsArray())) // ← 包装所有工具
|
||
.build();
|
||
|
||
private ToolCallback[] wrapWithManagement(Object[] methodTools) {
|
||
// 1. 让 Spring AI 创建 MethodToolCallback
|
||
// 2. 包装成 ManagedToolCallback
|
||
// 3. 返回包装后的数组
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### **3️⃣ 保持现状,添加必要的日志** ⭐(推荐)
|
||
|
||
如果项目规模不大,**保持简单架构**:
|
||
|
||
```java
|
||
// DateTimeTools.java
|
||
@Tool(description = "...")
|
||
public String getCurrentDateTime() {
|
||
logger.debug("🕐 getCurrentDateTime 被调用"); // ← 简单日志
|
||
String result = LocalDateTime.now()...toString();
|
||
logger.debug("🕐 getCurrentDateTime 返回: {}", result);
|
||
return result;
|
||
}
|
||
```
|
||
|
||
**优点**:
|
||
- ✅ 简单直接
|
||
- ✅ 无额外依赖
|
||
- ✅ 性能最好
|
||
|
||
---
|
||
|
||
## ✅ 总结
|
||
|
||
### **核心答案**
|
||
|
||
| 问题 | 答案 |
|
||
|------|------|
|
||
| **为什么没有经过 ToolCallingManager?** | 项目使用的 Spring AI 版本采用**简单架构**,直接调用 `MethodToolCallback` |
|
||
| **MethodToolCallback 是什么?** | 工具调用的**执行器**,通过反射调用 @Tool 方法 |
|
||
| **ToolCallingManager 是什么?** | 工具调用的**管理器**(某些版本),统一处理日志、权限、监控 |
|
||
| **两者有什么区别?** | `MethodToolCallback` 是**执行层**,`ToolCallingManager` 是**管理层** |
|
||
| **是否需要 ToolCallingManager?** | **不一定**,小型项目用 AOP 或简单日志即可 |
|
||
|
||
---
|
||
|
||
### **推荐方案**
|
||
|
||
**短期**(立即实施):
|
||
1. ✅ 保持现状(`MethodToolCallback` 直接调用)
|
||
2. ✅ 在工具方法中添加必要的日志(已完成)
|
||
3. ✅ 使用 debugger 日志记录调用栈(验证架构)
|
||
|
||
**中期**(1-2周):
|
||
1. ⚠️ 添加 Spring AOP 拦截器(模拟 ToolCallingManager)
|
||
2. ⚠️ 统一日志格式和性能监控
|
||
|
||
**长期**(按需):
|
||
1. 🟢 如果项目规模增大,考虑升级框架版本(如果新版本包含 ToolCallingManager)
|
||
2. 🟢 或者自己实现装饰器模式的统一管理
|
||
|
||
---
|
||
|
||
**最终建议**:**不需要担心没有 ToolCallingManager**,这是**正常的架构**,项目当前规模下**直接调用 MethodToolCallback 已经足够**。 |