fix(observability): 增强模型文本提取,支持多种方式并输出调试信息
## 改动内容
### 增强 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 重复
```
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CaseLibrary> results = repository.findByFaultCategoryAndErrorCode(
|
||||
FaultCategory.EXTERNAL_API, "40003");
|
||||
FaultCategory.API, "40003");
|
||||
|
||||
assertFalse(results.isEmpty());
|
||||
assertTrue(results.size() >= 2);
|
||||
|
||||
Reference in New Issue
Block a user