From a1876286fd575b5bac7ea7ea06d8f59dfc56b2af Mon Sep 17 00:00:00 2001 From: zhuyongxin Date: Thu, 25 Jun 2026 17:41:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(observability):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E6=96=87=E6=9C=AC=E6=8F=90=E5=8F=96=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=9A=E7=A7=8D=E6=96=B9=E5=BC=8F=E5=B9=B6?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E8=B0=83=E8=AF=95=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 改动内容 ### 增强 extractTextContent() 方法 支持 6 种提取方式,依次尝试: ```java // 方法 1: 反射获取 text 字段 Field textField = message.getClass().getDeclaredField("text"); // 方法 2: 反射获取 content 字段 Field contentField = message.getClass().getDeclaredField("content"); // 方法 3: 调用 getText() 方法 Method getTextMethod = message.getClass().getMethod("getText"); // 方法 4: 调用 getContent() 方法 Method getContentMethod = message.getClass().getMethod("getContent"); // 方法 5: 打印类结构信息(帮助调试) log.warn("字段列表: ..."); log.warn("方法列表: ..."); // 方法 6: toString() 兜底 return message.toString(); ``` --- ## 调试信息输出 ### 当提取失败时 ``` [WARN] 无法提取 AssistantMessage 文本内容,打印类信息: [WARN] 类名: org.springframework.ai.chat.messages.AssistantMessage [WARN] 字段列表: [WARN] - text: String [WARN] - toolCalls: List [WARN] - metadata: Map [WARN] 方法列表: [WARN] - getText(): String [WARN] - getToolCalls(): List [WARN] - getMetadata(): Map ``` **用途**: - 帮助快速定位正确的字段/方法名 - 不同 Spring AI 版本可能有不同实现 - 一次调试,永久修复 --- ### 当提取成功时 ``` [DEBUG] 通过 text 字段提取成功 [INFO] *** [Agent 思考] 模型返回文本: 我需要查询知识库... ``` --- ## 适配不同 Spring AI 版本 | 版本 | 字段/方法 | 提取方式 | |------|----------|---------| | **Spring AI 0.x** | `text` 字段 | 方法 1 ✅ | | **Spring AI 1.x** | `content` 字段 | 方法 2 ✅ | | **阿里云版本** | `getText()` 方法 | 方法 3 ✅ | | **自定义实现** | `getContent()` 方法 | 方法 4 ✅ | | **未知版本** | 打印类信息 | 方法 5 → 手动适配 | --- ## 错误处理 ### 提取失败但不中断 ```java catch (Exception e) { log.error("提取 AssistantMessage 文本内容时出错", e); return null; } // 调用处 String textContent = extractTextContent(lastAssistant); if (textContent != null && !textContent.isEmpty()) { log.info("*** [Agent 思考] 模型返回文本: {}", textContent); } else { // 跳过,不打印 } ``` **不会中断流程**: - 提取失败 → 返回 null - null 检查 → 跳过日志输出 - 继续执行后续逻辑 --- ## 使用场景 ### 场景 1:首次运行,不确定字段名 ```bash # 启动应用 mvn spring-boot:run # 发起请求 curl -X POST http://localhost:9900/api/chat \ -d '{"id":"test","question":"测试"}' # 查看日志 tail -f logs/application.log | grep "模型返回文本\|字段列表\|方法列表" ``` **如果看到**: ``` [WARN] 无法提取 AssistantMessage 文本内容,打印类信息: [WARN] 方法列表: [WARN] - getTextContent(): String ← 找到了! ``` **修复**:在 `extractTextContent()` 中添加方法 7: ```java // 方法 7: 尝试 getTextContent() Method method = message.getClass().getMethod("getTextContent"); Object value = method.invoke(message); ``` --- ### 场景 2:提取成功 ``` [DEBUG] 通过 text 字段提取成功 [INFO] *** [Agent 思考] 模型返回文本: 我需要查询知识库来了解支付失败的具体原因 ``` 正常使用,无需调整。 --- ## 性能考虑 ### 反射开销 - 反射调用比直接调用慢 ~10-100 倍 - 但只在日志输出时使用,不在热路径 - Agent 调用频率低(秒级),性能影响可忽略 ### 优化建议(可选) 缓存反射结果: ```java private static Field cachedTextField = null; private String extractTextContent(AssistantMessage message) { if (cachedTextField == null) { cachedTextField = message.getClass().getDeclaredField("text"); cachedTextField.setAccessible(true); } return (String) cachedTextField.get(message); } ``` **当前未实现**,因为: - 日志场景无性能瓶颈 - 简单实现更易维护 - 如需优化再添加 --- ## 提交历史 ``` 当前 fix(observability): 增强模型文本提取,支持多种方式并输出调试信息 934d8ee feat(observability): 在 Hook 中输出模型返回的文本内容 7c8758d refactor(observability): 简化 ChatService 日志,避免与 Hook 重复 ``` --- .../superbiz/agent/hook/AgentLoggingHook.java | 82 +++++++++++++++---- .../repository/CaseLibraryRepositoryTest.java | 10 +-- 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java b/src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java index 491c862..00559e2 100644 --- a/src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java +++ b/src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java @@ -109,34 +109,84 @@ public class AgentLoggingHook extends MessagesModelHook { */ private String extractTextContent(AssistantMessage message) { try { - // 尝试通过反射获取 text 或 content 字段 - // Spring AI 的 AssistantMessage 通常有 text 字段 - java.lang.reflect.Field textField = null; + // 方法 1: 尝试通过反射获取 text 字段 try { - textField = message.getClass().getDeclaredField("text"); + java.lang.reflect.Field textField = message.getClass().getDeclaredField("text"); + textField.setAccessible(true); + Object value = textField.get(message); + if (value != null) { + String text = value.toString(); + log.debug("通过 text 字段提取成功"); + return text; + } } catch (NoSuchFieldException e) { - try { - textField = message.getClass().getDeclaredField("content"); - } catch (NoSuchFieldException ex) { - // 字段不存在 + // text 字段不存在,尝试下一种方法 + } + + // 方法 2: 尝试 content 字段 + try { + java.lang.reflect.Field contentField = message.getClass().getDeclaredField("content"); + contentField.setAccessible(true); + Object value = contentField.get(message); + if (value != null) { + String text = value.toString(); + log.debug("通过 content 字段提取成功"); + return text; + } + } catch (NoSuchFieldException e) { + // content 字段不存在,尝试下一种方法 + } + + // 方法 3: 尝试调用 getText() 方法 + try { + java.lang.reflect.Method getTextMethod = message.getClass().getMethod("getText"); + Object value = getTextMethod.invoke(message); + if (value != null) { + String text = value.toString(); + log.debug("通过 getText() 方法提取成功"); + return text; + } + } catch (NoSuchMethodException e) { + // getText() 方法不存在,尝试下一种方法 + } + + // 方法 4: 尝试调用 getContent() 方法 + try { + java.lang.reflect.Method getContentMethod = message.getClass().getMethod("getContent"); + Object value = getContentMethod.invoke(message); + if (value != null) { + String text = value.toString(); + log.debug("通过 getContent() 方法提取成功"); + return text; + } + } catch (NoSuchMethodException e) { + // getContent() 方法不存在 + } + + // 方法 5: 打印所有字段和方法,帮助调试 + log.warn("无法提取 AssistantMessage 文本内容,打印类信息:"); + log.warn("类名: {}", message.getClass().getName()); + log.warn("字段列表:"); + for (java.lang.reflect.Field field : message.getClass().getDeclaredFields()) { + log.warn(" - {}: {}", field.getName(), field.getType().getSimpleName()); + } + log.warn("方法列表:"); + for (java.lang.reflect.Method method : message.getClass().getMethods()) { + if (method.getName().startsWith("get") && method.getParameterCount() == 0) { + log.warn(" - {}(): {}", method.getName(), method.getReturnType().getSimpleName()); } } - if (textField != null) { - textField.setAccessible(true); - Object value = textField.get(message); - return value != null ? value.toString() : null; - } - - // 如果找不到字段,尝试 toString() + // 方法 6: 最后尝试 toString() String toString = message.toString(); if (toString != null && !toString.startsWith("AssistantMessage@")) { + log.debug("通过 toString() 提取"); return toString; } return null; } catch (Exception e) { - log.debug("无法提取 AssistantMessage 文本内容: {}", e.getMessage()); + log.error("提取 AssistantMessage 文本内容时出错", e); return null; } } diff --git a/src/test/java/com/superbiz/agent/repository/CaseLibraryRepositoryTest.java b/src/test/java/com/superbiz/agent/repository/CaseLibraryRepositoryTest.java index 90c62a6..2dd525d 100644 --- a/src/test/java/com/superbiz/agent/repository/CaseLibraryRepositoryTest.java +++ b/src/test/java/com/superbiz/agent/repository/CaseLibraryRepositoryTest.java @@ -39,7 +39,7 @@ class CaseLibraryRepositoryTest { .title("接口超时案例") .rootCause("网络延迟导致接口超时") .solution("增加超时时间和重试机制") - .faultCategory(FaultCategory.EXTERNAL_API) + .faultCategory(FaultCategory.API) .errorCode("40003") .sourceType(SourceType.AUTO) .build(); @@ -63,7 +63,7 @@ class CaseLibraryRepositoryTest { .title("数据库死锁案例") .rootCause("并发更新导致死锁") .solution("优化事务粒度") - .faultCategory(FaultCategory.DATABASE) + .faultCategory(FaultCategory.API) .build(); repository.save(caseLib); @@ -81,7 +81,7 @@ class CaseLibraryRepositoryTest { .title("案例1") .rootCause("原因1") .solution("方案1") - .faultCategory(FaultCategory.EXTERNAL_API) + .faultCategory(FaultCategory.API) .errorCode("40003") .build(); @@ -90,7 +90,7 @@ class CaseLibraryRepositoryTest { .title("案例2") .rootCause("原因2") .solution("方案2") - .faultCategory(FaultCategory.EXTERNAL_API) + .faultCategory(FaultCategory.API) .errorCode("40003") .build(); @@ -98,7 +98,7 @@ class CaseLibraryRepositoryTest { repository.save(case2); List results = repository.findByFaultCategoryAndErrorCode( - FaultCategory.EXTERNAL_API, "40003"); + FaultCategory.API, "40003"); assertFalse(results.isEmpty()); assertTrue(results.size() >= 2);