From 934d8eee29215d59a7b7b677426205d6cc64a0c5 Mon Sep 17 00:00:00 2001 From: zhuyongxin Date: Thu, 25 Jun 2026 17:27:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(observability):=20=E5=9C=A8=20Hook=20?= =?UTF-8?q?=E4=B8=AD=E8=BE=93=E5=87=BA=E6=A8=A1=E5=9E=8B=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E7=9A=84=E6=96=87=E6=9C=AC=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 改动内容 ### 增强 AgentLoggingHook.afterModel() 在模型调用完成后,提取并输出模型返回的文本内容: ```java @Override public AgentCommand afterModel(List 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 ``` --- .../superbiz/agent/hook/AgentLoggingHook.java | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java b/src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java index 9703b5d..491c862 100644 --- a/src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java +++ b/src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java @@ -73,8 +73,14 @@ public class AgentLoggingHook extends MessagesModelHook { } if (lastAssistant != null) { - log.info("*** [Agent 思考] 模型输出: "); - // 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; + } + } + /** * 获取消息角色 */