feat(observability): 在 Hook 中输出模型返回的文本内容
## 改动内容
### 增强 AgentLoggingHook.afterModel()
在模型调用完成后,提取并输出模型返回的文本内容:
```java
@Override
public AgentCommand afterModel(List<Message> messages, RunnableConfig config) {
// 查找最后一条 AssistantMessage
AssistantMessage lastAssistant = ...;
// 提取文本内容
String textContent = extractTextContent(lastAssistant);
log.info("*** [Agent 思考] 模型返回文本: {}", textContent);
// 检查工具调用
if (hasToolCalls) {
log.info("*** [Agent 思考] 模型决定调用 N 个工具");
} else {
log.info("*** [Agent 思考] 这是最终答案");
}
}
```
---
### extractTextContent() 实现
通过反射提取 AssistantMessage 的文本内容:
```java
private String extractTextContent(AssistantMessage message) {
try {
// 尝试获取 text 或 content 字段
Field textField = message.getClass().getDeclaredField("text");
textField.setAccessible(true);
Object value = textField.get(message);
return value != null ? value.toString() : null;
} catch (NoSuchFieldException e) {
// 尝试 content 字段
try {
Field contentField = message.getClass().getDeclaredField("content");
// ...
} catch (NoSuchFieldException ex) {
// 字段不存在,返回 null
}
}
}
```
**为什么用反射?**
- Spring AI 的 `AssistantMessage` 没有公开的 `getText()` 或 `getContent()` 方法
- 不同版本可能使用 `text` 或 `content` 字段
- 反射可以兼容不同实现
---
## 日志输出示例
### 第 1 轮:模型决定调用工具
```
========================================
*** [Agent 思考] 第 1 轮思考完成
*** [Agent 思考] 模型返回文本: 我需要查询知识库来了解支付失败的原因
*** [Agent 思考] 模型决定调用 1 个工具:
- 工具: lookup_knowledge, 参数: {"query":"支付失败原因"}
*** [Agent 思考] 等待工具执行结果...
========================================
```
---
### 第 2 轮:模型返回最终答案
```
========================================
*** [Agent 思考] 第 2 轮思考完成
*** [Agent 思考] 模型返回文本: 根据知识库的记录,支付失败的主要原因包括:
1. ERR_TIMEOUT - 支付网关响应超时,通常是网络问题或第三方服务不稳定
2. ERR_INVALID_SIGNATURE - 签名验证失败,检查密钥配置
3. ERR_INSUFFICIENT_BALANCE - 账户余额不足
... (已截断,总长度: 1234)
*** [Agent 思考] 模型决定不调用工具
*** [Agent 思考] 这是最终答案,准备返回给用户
========================================
```
---
## 关键观测点
| 轮次 | 模型输出内容 | 决策 |
|------|-------------|------|
| **第 1 轮** | 模型的推理过程(通常很短) | 决定调用工具 |
| **第 2 轮** | 模型的最终答案(完整回复) | 不调用工具 |
---
## 内容截断策略
- 长度 ≤ 500:完整输出
- 长度 > 500:截断前 500 字符,显示总长度
```
模型返回文本: 根据知识库的记录,支付失败的主要原因包括...
(前 500 字符)
... (已截断,总长度: 1234)
```
---
## 异常处理
如果反射失败(字段不存在或访问被拒绝):
```java
catch (Exception e) {
log.debug("无法提取 AssistantMessage 文本内容: {}", e.getMessage());
return null;
}
```
日志输出:
```
*** [Agent 思考] 模型返回文本: (无法提取)
```
不会中断程序,只是跳过文本输出。
---
## 完整的思考流程日志
```
📝 用户问题: 支付为什么会失败?
*** [Agent 思考] 第 1 轮思考开始
*** [Agent 思考] 准备调用模型...
*** [Agent 思考] 第 1 轮思考完成
*** [Agent 思考] 模型返回文本: 我需要查询知识库
*** [Agent 思考] 模型决定调用 1 个工具:
- 工具: lookup_knowledge, 参数: {"query":"支付失败"}
>>> [工具调用] lookup_knowledge
<<< [工具返回] lookup_knowledge
<<< 结果: found=true
*** [Agent 思考] 第 2 轮思考开始
*** [Agent 思考] 准备调用模型...
*** [Agent 思考] 第 2 轮思考完成
*** [Agent 思考] 模型返回文本: 根据知识库的记录,支付失败...
*** [Agent 思考] 模型决定不调用工具
*** [Agent 思考] 这是最终答案,准备返回给用户
⏱️ 总耗时: 1523 ms
📏 输出长度: 456 字符
```
---
## 提交历史
```
当前 feat(observability): 在 Hook 中输出模型返回的文本内容
7c8758d refactor(observability): 简化 ChatService 日志,避免与 Hook 重复
b3ea6e2 feat(observability): 添加 Agent 思考过程日志 Hook
```
This commit is contained in:
@@ -73,8 +73,14 @@ public class AgentLoggingHook extends MessagesModelHook {
|
||||
}
|
||||
|
||||
if (lastAssistant != null) {
|
||||
log.info("*** [Agent 思考] 模型输出: <AssistantMessage>");
|
||||
// AssistantMessage 的内容通过 toString() 或在工具调用中体现
|
||||
// 打印模型返回的文本内容
|
||||
String textContent = extractTextContent(lastAssistant);
|
||||
if (textContent != null && !textContent.isEmpty()) {
|
||||
log.info("*** [Agent 思考] 模型返回文本: {}",
|
||||
textContent.length() > 500
|
||||
? textContent.substring(0, 500) + "... (已截断,总长度: " + textContent.length() + ")"
|
||||
: textContent);
|
||||
}
|
||||
|
||||
// 检查是否有工具调用
|
||||
if (lastAssistant.getToolCalls() != null && !lastAssistant.getToolCalls().isEmpty()) {
|
||||
@@ -98,6 +104,43 @@ public class AgentLoggingHook extends MessagesModelHook {
|
||||
return new AgentCommand(previousMessages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取 AssistantMessage 的文本内容
|
||||
*/
|
||||
private String extractTextContent(AssistantMessage message) {
|
||||
try {
|
||||
// 尝试通过反射获取 text 或 content 字段
|
||||
// Spring AI 的 AssistantMessage 通常有 text 字段
|
||||
java.lang.reflect.Field textField = null;
|
||||
try {
|
||||
textField = message.getClass().getDeclaredField("text");
|
||||
} catch (NoSuchFieldException e) {
|
||||
try {
|
||||
textField = message.getClass().getDeclaredField("content");
|
||||
} catch (NoSuchFieldException ex) {
|
||||
// 字段不存在
|
||||
}
|
||||
}
|
||||
|
||||
if (textField != null) {
|
||||
textField.setAccessible(true);
|
||||
Object value = textField.get(message);
|
||||
return value != null ? value.toString() : null;
|
||||
}
|
||||
|
||||
// 如果找不到字段,尝试 toString()
|
||||
String toString = message.toString();
|
||||
if (toString != null && !toString.startsWith("AssistantMessage@")) {
|
||||
return toString;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.debug("无法提取 AssistantMessage 文本内容: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息角色
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user