From b3ea6e202dabc3895ffa791ce72bda927a3f3bdf Mon Sep 17 00:00:00 2001 From: zhuyongxin Date: Thu, 25 Jun 2026 17:02:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(observability):=20=E6=B7=BB=E5=8A=A0=20Age?= =?UTF-8?q?nt=20=E6=80=9D=E8=80=83=E8=BF=87=E7=A8=8B=E6=97=A5=E5=BF=97=20H?= =?UTF-8?q?ook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 改动内容 ### 1. 创建 AgentLoggingHook 基于 Spring AI Alibaba 的 `MessagesModelHook` 实现: ```java @HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL}) public class AgentLoggingHook extends MessagesModelHook { // 在模型调用前 public AgentCommand beforeModel(List messages, RunnableConfig config) // 在模型调用后 public AgentCommand afterModel(List messages, RunnableConfig config) } ``` --- ### 2. 集成到 ReactAgent 在 `ChatService.createReactAgent()` 中添加 Hook: ```java ReactAgent.builder() .name("intelligent_assistant") .model(chatModel) .hooks(new AgentLoggingHook()) // ✅ 添加日志 Hook .build(); ``` --- ## 日志输出示例 ### 完整的 Agent 思考流程 ``` ======================================== ========== Agent 执行开始 ========== ======================================== 📝 用户问题: 支付为什么会失败? ---------------------------------------- 🚀 执行 ReactAgent.call() - 自动处理工具调用 ======================================== *** [Agent 思考] 第 1 轮思考开始 *** [Agent 思考] 当前消息数量: 2 *** [Agent 思考] 最近 2 条消息: [1] 角色: User(用户), 类型: UserMessage [2] 角色: User(用户), 类型: UserMessage *** [Agent 思考] 准备调用模型... ======================================== ======================================== *** [Agent 思考] 第 1 轮思考完成 *** [Agent 思考] 模型输出: *** [Agent 思考] 模型决定调用 1 个工具: - 工具: lookup_knowledge, 参数: {"query":"支付失败原因"} *** [Agent 思考] 等待工具执行结果... ======================================== ======================================== >>> [工具调用] lookup_knowledge >>> 参数: query = "支付失败原因" >>> RequestId: a3b4c5d6 ---------------------------------------- [L0 精确匹配] 完成: matches=0, time=2ms [L1 语义检索] L0非唯一匹配,触发L1语义检索... [L1 语义检索] 完成: matches=1, time=245ms <<< [工具返回] lookup_knowledge <<< 结果: found=true, matchType=semantic_L1, confidence=medium ======================================== ======================================== *** [Agent 思考] 第 2 轮思考开始 *** [Agent 思考] 当前消息数量: 4 *** [Agent 思考] 最近 3 条消息: [1] 角色: User(用户), 类型: UserMessage [2] 角色: Assistant(模型), 类型: AssistantMessage [3] 角色: Tool(工具返回), 类型: ToolResponseMessage *** [Agent 思考] 准备调用模型... ======================================== ======================================== *** [Agent 思考] 第 2 轮思考完成 *** [Agent 思考] 模型输出: *** [Agent 思考] 模型决定不调用工具 *** [Agent 思考] 这是最终答案,准备返回给用户 ======================================== ======================================== ========== Agent 执行完成 ========== ======================================== ⏱️ 执行耗时: 1523 ms 📏 最终输出长度: 456 字符 📤 最终输出内容: 根据知识库的记录,支付失败的主要原因包括... ======================================== ``` --- ## 核心观测点 | 阶段 | 日志标识 | 信息 | |------|---------|------| | **Agent 开始** | `Agent 执行开始` | 用户问题 | | **思考开始** | `第 N 轮思考开始` | 消息数量、最近消息 | | **思考完成** | `第 N 轮思考完成` | 模型决策(调用工具 or 返回答案) | | **工具调用** | `工具调用 lookup_knowledge` | 工具名称、参数 | | **工具返回** | `工具返回 lookup_knowledge` | 结果摘要、耗时 | | **Agent 完成** | `Agent 执行完成` | 总耗时、最终输出 | --- ## Hook 机制说明 ### MessagesModelHook - **触发时机**: - `BEFORE_MODEL`:模型调用前 - `AFTER_MODEL`:模型调用后 - **消息流转**: ``` 用户问题 ↓ [第1轮] beforeModel → 模型决定调用工具 → afterModel ↓ 工具执行(lookup_knowledge) ↓ [第2轮] beforeModel → 模型生成最终答案 → afterModel ↓ 返回给用户 ``` - **轮次统计**: - 每次调用模型计为一轮 - 通常需要 2 轮:第 1 轮调用工具,第 2 轮生成答案 --- ## 技术细节 ### 1. 为什么不用 ModelHook? `ModelHook` 需要处理 `OverAllState`,更复杂。`MessagesModelHook` 直接操作消息列表,更简单。 ### 2. 为什么跳过消息内容? Spring AI 的 `Message` 接口没有统一的 `getContent()` 方法,不同实现类有不同的访问方式。工具调用的详细内容已在工具层日志体现。 ### 3. 消息类型识别 ```java UserMessage → "User(用户)" AssistantMessage → "Assistant(模型)" ToolResponseMessage → "Tool(工具返回)" ``` --- ## 验证方法 ```bash # 1. 启动应用 mvn spring-boot:run # 2. 提问 curl -X POST http://localhost:9900/api/chat \ -H "Content-Type: application/json" \ -d '{"id":"test","question":"支付为什么会失败?"}' # 3. 查看完整日志 tail -f logs/application.log # 4. 过滤关键日志 tail -f logs/application.log | grep -E "Agent|思考|工具|输出" ``` --- ## 提交历史 ``` 当前 feat(observability): 添加 Agent 思考过程日志 Hook 8890cd2 feat(observability): 增强 Agent 和工具调用的可观测日志 f4f0c63 fix(knowledge): 修复 readDocument 文件路径拼接问题 ``` --- .../superbiz/agent/hook/AgentLoggingHook.java | 115 ++++++++++++++++++ .../superbiz/agent/service/ChatService.java | 2 + 2 files changed, 117 insertions(+) create mode 100644 src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java diff --git a/src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java b/src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java new file mode 100644 index 0000000..9703b5d --- /dev/null +++ b/src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java @@ -0,0 +1,115 @@ +package com.superbiz.agent.hook; + +import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook; +import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand; +import com.alibaba.cloud.ai.graph.agent.hook.HookPosition; +import com.alibaba.cloud.ai.graph.agent.hook.HookPositions; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.messages.ToolResponseMessage; + +import java.util.List; + +/** + * Agent 日志 Hook + * 用于记录 Agent 的思考过程、消息流转 + */ +@Slf4j +@HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL}) +public class AgentLoggingHook extends MessagesModelHook { + + private int modelCallCount = 0; + + @Override + public String getName() { + return "agent_logging_hook"; + } + + @Override + public AgentCommand beforeModel(List previousMessages, RunnableConfig config) { + modelCallCount++; + log.info("========================================"); + log.info("*** [Agent 思考] 第 {} 轮思考开始", modelCallCount); + log.info("*** [Agent 思考] 当前消息数量: {}", previousMessages.size()); + + // 打印最后几条消息 + int lastN = Math.min(3, previousMessages.size()); + if (lastN > 0) { + log.info("*** [Agent 思考] 最近 {} 条消息:", lastN); + List recentMessages = previousMessages.subList(previousMessages.size() - lastN, previousMessages.size()); + + for (int i = 0; i < recentMessages.size(); i++) { + Message msg = recentMessages.get(i); + String role = getMessageRole(msg); + + log.info(" [{}] 角色: {}, 类型: {}", i + 1, role, msg.getClass().getSimpleName()); + // Message 接口可能没有直接的 getContent() 方法,跳过内容打印 + // 具体内容会在工具调用日志中体现 + } + } + + log.info("*** [Agent 思考] 准备调用模型..."); + log.info("========================================"); + + // 不修改消息,直接返回 + return new AgentCommand(previousMessages); + } + + @Override + public AgentCommand afterModel(List previousMessages, RunnableConfig config) { + log.info("========================================"); + log.info("*** [Agent 思考] 第 {} 轮思考完成", modelCallCount); + + // 查找最后一条 AssistantMessage(模型的回复) + AssistantMessage lastAssistant = null; + for (int i = previousMessages.size() - 1; i >= 0; i--) { + if (previousMessages.get(i) instanceof AssistantMessage) { + lastAssistant = (AssistantMessage) previousMessages.get(i); + break; + } + } + + if (lastAssistant != null) { + log.info("*** [Agent 思考] 模型输出: "); + // AssistantMessage 的内容通过 toString() 或在工具调用中体现 + + // 检查是否有工具调用 + if (lastAssistant.getToolCalls() != null && !lastAssistant.getToolCalls().isEmpty()) { + log.info("*** [Agent 思考] 模型决定调用 {} 个工具:", + lastAssistant.getToolCalls().size()); + lastAssistant.getToolCalls().forEach(toolCall -> { + log.info(" - 工具: {}, 参数: {}", + toolCall.name(), + toolCall.arguments()); + }); + log.info("*** [Agent 思考] 等待工具执行结果..."); + } else { + log.info("*** [Agent 思考] 模型决定不调用工具"); + log.info("*** [Agent 思考] 这是最终答案,准备返回给用户"); + } + } + + log.info("========================================"); + + // 不修改消息,直接返回 + return new AgentCommand(previousMessages); + } + + /** + * 获取消息角色 + */ + private String getMessageRole(Message message) { + if (message instanceof UserMessage) { + return "User(用户)"; + } else if (message instanceof AssistantMessage) { + return "Assistant(模型)"; + } else if (message instanceof ToolResponseMessage) { + return "Tool(工具返回)"; + } else { + return message.getClass().getSimpleName(); + } + } +} diff --git a/src/main/java/com/superbiz/agent/service/ChatService.java b/src/main/java/com/superbiz/agent/service/ChatService.java index d78c6e9..d42c126 100644 --- a/src/main/java/com/superbiz/agent/service/ChatService.java +++ b/src/main/java/com/superbiz/agent/service/ChatService.java @@ -7,6 +7,7 @@ import com.superbiz.agent.agent.tool.InternalDocsTools; import com.superbiz.agent.agent.tool.QueryLogsTools; import com.superbiz.agent.agent.tool.QueryMetricsTools; import com.superbiz.agent.tool.LookupKnowledgeTool; +import com.superbiz.agent.hook.AgentLoggingHook; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -176,6 +177,7 @@ public class ChatService { .systemPrompt(systemPrompt) .methodTools(buildMethodToolsArray()) .tools(getToolCallbacks()) + .hooks(new AgentLoggingHook()) // 添加日志 Hook .build(); }