Commit Graph
146 Commits
Author SHA1 Message Date
aruo 72a3dbf8c5 feat: add aiops payload query augmentation 2026-07-05 12:56:20 +08:00
aruo 674dd27a48 feat: add rag post-reindex acceptance 2026-07-05 12:29:24 +08:00
aruo c7e2fc2ee2 feat: include breadcrumb in embedding text 2026-07-05 12:15:27 +08:00
aruo 1bfe1a17b4 docs: add rag refactor story 2026-07-05 12:09:32 +08:00
aruo 9dd6823fe7 docs: add rag retrieval quality report 2026-07-05 11:29:16 +08:00
aruo 9376448804 docs: add rag vectorstore interview notes 2026-07-05 11:13:50 +08:00
aruo f2bae0382c fix: align vectorstore live retrieval 2026-07-05 10:53:45 +08:00
aruo 5c71f5fc79 feat: integrate spring ai vectorstore fallback 2026-07-05 10:20:29 +08:00
aruo b9ec07de57 feat: add spring ai retrieval sidecar 2026-07-05 03:22:03 +08:00
aruo 5197712719 feat: add rag evidence postprocess blocks 2026-07-05 03:03:18 +08:00
aruo 4a94c14feb feat: treat l0 retrieval as domain hint 2026-07-05 02:18:40 +08:00
aruo 9a2a44d1b5 test: add rag retrieval baseline 2026-07-05 02:02:27 +08:00
aruo 79feed3314 Merge branch 'emdash/shy-items-fry-f4zze' into refactor/mvp1.0
# Conflicts:
#	mvp/issues/README.md
2026-07-05 01:42:42 +08:00
aruo 98155ae1d8 Merge branch 'aiops-trace-scope' into refactor/mvp1.0 2026-07-05 01:42:23 +08:00
aruo 2609c5a5ab docs: consolidate rag refactor issues 2026-07-05 01:40:08 +08:00
aruo bf5286c8f4 docs: add interview project materials 2026-07-05 01:39:37 +08:00
aruo 26e12a8d6b Archive MVP demo interview runbook 2026-07-05 01:34:04 +08:00
aruo cbef3ddd3c Add MVP demo interview runbook 2026-07-05 01:25:20 +08:00
aruo 69deb15330 Add diagnosis eval baseline diff 2026-07-05 00:59:53 +08:00
aruo 4c7c53b024 Expand diagnosis eval fixtures 2026-07-05 00:27:57 +08:00
aruo ca5c61fabf Add diagnosis eval harness 2026-07-04 23:51:43 +08:00
aruo 23ee05c7c3 feat: add traceable scoped AIOps diagnosis 2026-07-04 22:57:28 +08:00
aruo dc6cd32a67 Harden evidence trace semantics 2026-07-04 22:36:30 +08:00
zhuyongxin 246c99b954 add query sql script 2026-07-04 20:14:57 +08:00
zhuyongxin f01866c1a2 refactor: use sequential agent for chat workflow 2026-07-03 18:03:06 +08:00
zhuyongxin 6919092b83 feat: archive mvp demo trace acceptance 2026-07-03 16:25:00 +08:00
zhuyongxin 5b827fe90e fix: avoid low-confidence supervisor retry by default 2026-07-03 15:32:09 +08:00
zhuyongxin b0f288ae36 use supervisor agent for complex chat 2026-07-03 14:10:55 +08:00
zhuyongxin 1ff7f09d25 fix chat session traces and document paths 2026-07-03 13:53:16 +08:00
zhuyongxin fd89d84fc0 docs: add MVP review issue 2026-07-03 11:21:24 +08:00
zhuyongxin 9050487307 feat: add chat verifier agent 2026-07-03 10:54:33 +08:00
zhuyongxin 4f5316d473 chore(cleanup): 清理临时文件和已归档标记
- .gitignore 添加 *.stackdump 和 NUL 规则
- 移除 bash.exe.stackdump 跟踪
- 移除已归档的 .archive-ready 旧标记
2026-07-01 18:32:12 +08:00
zhuyongxin 2a7164288f chore(docs): 补充 ISS-001 架构设计文档到 mvp
- 新增 mvp/architecture/session-dedup-knowledge-map.md
- 更新 mvp/README.md 文档导航
- ISS-001 issue 关联架构文档
2026-07-01 18:28:19 +08:00
zhuyongxin a1c896ebda chore(docs): 归档 ISS-001 session-dedup-knowledge-map + ISS-002 mvp 文档
- 移动 session-dedup-knowledge-map OpenSpec 到 archive 目录
- 提交 ISS-001 遗留的 devflow 档案文件
- 更新 ISS-002 状态为已修复
- 新增 mvp/architecture/action-memory-relevance.md 设计文档
- 更新 mvp/README.md 文档导航
- 更新 devflow/index.md OpenSpec 链接指向 archive
2026-07-01 18:27:04 +08:00
zhuyongxin e438df4355 feat(knowledge): Executor 行动记忆 + 归一化质量等级解决 ISS-002 重复检索
- RetrievedDocTracker 升级为域级+文档级双层记录(Map<sessionId, Map<domain, Set<filePath>>>)
- LookupKnowledgeTool 新增 Min-Max 归一化层(BGE-M3 L2 距离→[0,1] similarity)
- 三等级 relevanceLevel:PRECISE / HIGHLY_RELEVANT / REFERENCE + completenessHint 兜底信号
- LookupResult 新增 relevanceLevel、completenessHint、retrievedDomainsThisSession
- Executor prompt 重写:4 条检索约束 + 合法出口不查全不追责,重复检索才惩罚
- 入库可观测性:V010 迁移 + retrieval_details JSON 扩展
- 归档 executor-action-memory-relevance change
2026-07-01 18:24:41 +08:00
zhuyongxin e4f37cb9e6 fix(knowledge): 修复循环依赖 + 归档 session-dedup-knowledge-map + 记录 ISS-002
- KnowledgeIndexService: 域级生成从 @PostConstruct 移到 @EventListener(ApplicationReadyEvent),解决 KnowledgeIndexService ↔ KnowledgeDomainService 循环依赖
- devflow 归档: evidence.md + acceptance.md(含运行验证结果)
- devflow/index.md: session-dedup-knowledge-map 状态改为 archived
- openspec .archive-ready 标记
- mvp/issues/ISS-002: Executor 无约束重复调用 lookup_knowledge
2026-07-01 14:26:59 +08:00
zhuyongxin 354ffc1947 feat(feedback): 补提交 feedback 相关源码(漏提交的新建文件) 2026-07-01 10:57:49 +08:00
zhuyongxin bb44140901 feat(knowledge): 会话级去重 + 知识域地图注入 Planner 解决 ISS-001 重复检索
- RetrievedDocTracker: sessionId → Set<filePath> 会话级去重,LookupKnowledgeTool Step 5 过滤已检索文档
- KnowledgeDomainService: 域级聚合,LLM 生成 when_to_retrieve,构建 knowledge map YAML
- DocumentFieldEnricher: 上传时 LLM 补全 covers + whenToRetrieve(含同域文档排除上下文)
- KnowledgeDomain entity + V009 迁移: 域级元数据持久化,避免重启重复 LLM 调用
- ChatService: 注入 knowledge map 到 Planner prompt,会话结束时清理去重状态
- KnowledgeIndexService: 手写 JSON 解析替换为 Jackson ObjectMapper,启动时补建缺失域记录
- chat-planner-prompt: 新增知识库检索规则(按域 when_to_retrieve 判断,每域最多一次检索)
- doc-field-enricher-prompt / domain-summary-prompt: 外部化 LLM 提示词
2026-07-01 10:47:46 +08:00
zhuyongxin 2a796da490 feat(feedback): 置信度评分与用户反馈机制 & 归档 confidence-feedback change 2026-06-30 18:01:06 +08:00
zhuyongxin 3ffa5cc366 Merge branch 'emdash/afraid-geese-carry-h5718' into refactor/mvp1.0
# Conflicts:
#	devflow/index.md
2026-06-26 17:36:59 +08:00
zhuyongxin e3f20b1f06 chore: 归档 session-storage change
- 创建 devflow 项目档案(brief/evidence/decisions/acceptance)
- 更新 devflow/index.md 索引
- 移动 OpenSpec 到 archive
2026-06-26 17:33:56 +08:00
zhuyongxin 9b52afce07 docs: 合并 .docs/mvp 到根 mvp 目录并更新文档
- 删除 .docs/mvp 目录,内容合并到根目录 mvp/
- 更新 session-storage-design.md 实现变更记录
- 修复引用路径
2026-06-26 17:29:27 +08:00
zhuyongxin a3abe3f7a2 refactor(session): 清理代码 & RunnableConfig 传 sessionId
- AgentLoggingHook 改为从 config.metadata 读取 sessionId(线程安全)
- 移除 AgentLoggingHook 调试用的 metadata 日志
- TokenTrackingChatModel 日志降为 debug
- SessionContextHolder 移除未使用的 setAgentName/getAgentName
- ChatService 清理无用 import
- 修复 stream 路径下 ThreadLocal NPE
2026-06-26 17:28:30 +08:00
zhuyongxin 0d9cce75f9 feat(session): 会话存储体系实现 & Chat多Agent路由
- 新增诊断会话(diagnosis_session/agent_step/tool_invocation)三表
- AgentLoggingHook 持久化 agent_step,记录决策链和耗时
- LookupKnowledgeTool 写入 tool_invocation,记录L0/L1检索质量
- TokenTrackingChatModel 捕获真实token用量
- Chat接口支持意图路由:简单问题单Agent,复杂问题多Agent(Planner+Executor)
- Prompt外置到 src/main/resources/prompts/
- 删除旧 diagnosis_record 表及相关文件
- 新增SessionContextHolder(ThreadLocal传递sessionId)
- QuestionComplexity 复杂度判断工具
- 测试覆盖三张新表的Repository
2026-06-26 16:22:05 +08:00
zhuyongxin a74ccea5be feat(knowledge): breadcrumb分块上下文 & LookupKnowledgeTool日志优化
- DocumentChunk新增breadcrumb字段,分块时构建完整标题层级路径
- DocumentChunkService splitByHeadings维护标题层级栈算法
- VectorIndexService 将breadcrumb写入Milvus metadata
- LookupKnowledgeTool日志替换为结构化摘要,替代原始MD预览
- L0返回策略:唯一匹配用正文摘要,多匹配+L1有结果仅元数据(不读文件)
- 新增buildCompactSummary / buildMetadataOnlySummary方法
- 安装frontend-design skill
- 创建mvp/文档目录(架构设计+会话存储方案)
- 更新测试适配新逻辑
2026-06-26 13:56:10 +08:00
zhuyongxin a1876286fd 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 重复
```
2026-06-25 17:41:37 +08:00
zhuyongxin 934d8eee29 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
```
2026-06-25 17:27:48 +08:00
zhuyongxin 7c8758d7fa refactor(observability): 简化 ChatService 日志,避免与 Hook 重复
## 改动内容

### 修改前:重复的日志

```java
// ChatService.executeChat()
logger.info("========== Agent 执行开始 ==========");
logger.info("📝 用户问题: {}", question);
logger.info("🚀 执行 ReactAgent.call() - 自动处理工具调用");

// ... Agent 执行 ...

logger.info("========== Agent 执行完成 ==========");
logger.info("⏱️  执行耗时: {} ms", duration);
logger.info("📤 最终输出内容:");
// 打印完整答案
```

**问题**:
- 与 `AgentLoggingHook` 的日志重复
- 日志过于冗长
- Hook 已经覆盖了 Agent 的思考过程

---

### 修改后:精简的日志

```java
// ChatService.executeChat() - 只保留最外层框架
logger.info("========================================");
logger.info("📝 用户问题: {}", question);

// ... Agent 执行(Hook 负责内部日志)...

logger.info("⏱️  总耗时: {} ms", duration);
logger.info("📏 输出长度: {} 字符", answer.length());
logger.info("========================================");
```

**优势**:
- 职责清晰:ChatService 只记录最外层信息
- 避免重复:思考过程由 Hook 负责
- 更简洁:减少噪音日志

---

## 日志分层

| 层级 | 负责类 | 职责 |
|------|--------|------|
| **外层框架** | `ChatService` | 用户问题、总耗时、输出长度 |
| **思考过程** | `AgentLoggingHook` | 每轮思考、模型决策、消息流转 |
| **工具执行** | `LookupKnowledgeTool` | L0/L1 检索、工具参数/返回 |

---

## 日志输出对比

### 修改前(重复冗长)

```
========================================
========== Agent 执行开始 ==========      ← ChatService
========================================
📝 用户问题: 支付为什么会失败?          ← ChatService
----------------------------------------
🚀 执行 ReactAgent.call()               ← ChatService

*** [Agent 思考] 第 1 轮思考开始         ← Hook
...
*** [Agent 思考] 第 1 轮思考完成         ← Hook

>>> [工具调用] lookup_knowledge         ← Tool
...
<<< [工具返回] lookup_knowledge         ← Tool

*** [Agent 思考] 第 2 轮思考开始         ← Hook
...
*** [Agent 思考] 第 2 轮思考完成         ← Hook

========================================
========== Agent 执行完成 ==========      ← ChatService (重复)
========================================
⏱️  执行耗时: 1523 ms                   ← ChatService
📤 最终输出内容:                         ← ChatService
根据知识库的记录...(完整答案)          ← ChatService (太长)
========================================
```

---

### 修改后(清晰简洁)

```
========================================
📝 用户问题: 支付为什么会失败?          ← ChatService (简洁)

*** [Agent 思考] 第 1 轮思考开始         ← Hook
...
*** [Agent 思考] 第 1 轮思考完成         ← Hook

>>> [工具调用] lookup_knowledge         ← Tool
...
<<< [工具返回] lookup_knowledge         ← Tool

*** [Agent 思考] 第 2 轮思考开始         ← Hook
...
*** [Agent 思考] 第 2 轮思考完成         ← Hook
*** [Agent 思考] 这是最终答案            ← Hook (已说明)

⏱️  总耗时: 1523 ms                     ← ChatService (简洁)
📏 输出长度: 456 字符                    ← ChatService (摘要)
========================================
```

**改进**:
- ✅ 去掉重复的"开始/完成"标记
- ✅ 不再打印完整答案(通过 HTTP 响应已返回)
- ✅ Hook 已说明"这是最终答案"
- ✅ 日志更紧凑,信噪比更高

---

## 设计原则

### 1. 单一职责

- **ChatService**:顶层编排,只记录执行框架
- **Hook**:Agent 内部状态,记录思考过程
- **Tool**:工具执行细节,记录检索过程

### 2. 避免重复

- 不在多处打印相同信息
- Hook 已说明"最终答案",ChatService 不再重复

### 3. 信息密度

- 关键信息:保留(用户问题、耗时、长度)
- 冗余信息:删除(重复标题、完整答案)

---

## 查看日志

```bash
# 完整日志
tail -f logs/application.log

# 只看框架
tail -f logs/application.log | grep "📝\|⏱️\|📏"

# 只看思考过程
tail -f logs/application.log | grep "Agent 思考"

# 只看工具调用
tail -f logs/application.log | grep "工具调用\|工具返回"
```

---

## 提交历史

```
当前 refactor(observability): 简化 ChatService 日志,避免与 Hook 重复
b3ea6e2 feat(observability): 添加 Agent 思考过程日志 Hook
8890cd2 feat(observability): 增强 Agent 和工具调用的可观测日志
```
2026-06-25 17:24:59 +08:00
zhuyongxin b3ea6e202d feat(observability): 添加 Agent 思考过程日志 Hook
## 改动内容

### 1. 创建 AgentLoggingHook

基于 Spring AI Alibaba 的 `MessagesModelHook` 实现:

```java
@HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL})
public class AgentLoggingHook extends MessagesModelHook {

    // 在模型调用前
    public AgentCommand beforeModel(List<Message> messages, RunnableConfig config)

    // 在模型调用后
    public AgentCommand afterModel(List<Message> 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 思考] 模型输出: <AssistantMessage>
*** [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 思考] 模型输出: <AssistantMessage>
*** [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 文件路径拼接问题
```
2026-06-25 17:02:13 +08:00
zhuyongxin 8890cd2806 feat(observability): 增强 Agent 和工具调用的可观测日志
## 改动内容

### 1. ChatService - Agent 执行日志

在 `executeChat` 方法中添加:

```
========================================
========== Agent 执行开始 ==========
========================================
📝 用户问题: 支付为什么会失败?
----------------------------------------
🚀 执行 ReactAgent.call() - 自动处理工具调用
========================================
========== Agent 执行完成 ==========
========================================
⏱️  执行耗时: 1523 ms
📏 最终输出长度: 456 字符
----------------------------------------
📤 最终输出内容:
根据知识库的记录,支付失败的主要原因是...
========================================
```

**关键信息**:
- 用户问题
- 执行耗时
- 最终输出长度和内容

---

### 2. LookupKnowledgeTool - 工具调用详细日志

```
========================================
>>> [工具调用] lookup_knowledge
>>> 参数: query = "支付为什么会失败?"
>>> RequestId: a3b4c5d6
----------------------------------------
[L0 精确匹配] 完成: matches=0, time=3ms
[置信度判断] highConfidence=false, reason=多个或零个匹配
[L1 语义检索] L0非唯一匹配,触发L1语义检索...
[L1 语义检索] 完成: matches=1, time=245ms
[L1 语义检索] 找到文档:
  - [1] 文档ID: doc-123, 相似度得分: 0.82
----------------------------------------
<<< [工具返回] lookup_knowledge
<<< 结果: found=true, matchType=semantic_L1, confidence=medium
<<< 总耗时: 248ms (L0=3ms, L1=245ms)
<<< 返回内容长度: 1234 字符
<<< 内容预览: ## 支付网关错误码定义...
========================================
```

**关键信息**:
- 工具名称和参数
- L0/L1 执行时间和结果
- 匹配文档列表
- 返回结果摘要

---

## 日志格式说明

### 符号约定

- `>>>` - 工具调用(入参)
- `<<<` - 工具返回(出参)
- `***` - Agent 思考过程(暂未实现)
- `📝` - 用户输入
- `📤` - Agent 输出
- `⏱️` - 性能指标

### 日志级别

- `INFO` - 关键节点和结果
- `DEBUG` - 详细的中间状态(已设置但默认不显示)

---

## 使用场景

### 1. 调试工具调用

```bash
# 查看工具调用详情
grep "工具调用\|工具返回" logs/application.log

# 输出示例
>>> [工具调用] lookup_knowledge
>>> 参数: query = "ERR_TIMEOUT"
<<< [工具返回] lookup_knowledge
<<< 结果: found=true, matchType=exact_L0, confidence=high
```

### 2. 性能分析

```bash
# 查看执行耗时
grep "执行耗时\|总耗时" logs/application.log

# 输出示例
⏱️  执行耗时: 1523 ms
<<< 总耗时: 248ms (L0=3ms, L1=245ms)
```

### 3. L0/L1 验证

```bash
# 查看检索路径
grep "L0精确匹配\|L1语义检索" logs/application.log

# 示例 - L0 命中
[L0 精确匹配] 完成: matches=1, time=3ms
[L0 精确匹配] 找到文档:
  - [1] 标题: 支付网关错误码定义, 路径: api/payment-errors.md
[L1 语义检索] L0唯一匹配,跳过L1检索

# 示例 - L1 命中
[L0 精确匹配] 完成: matches=0, time=2ms
[L1 语义检索] L0非唯一匹配,触发L1语义检索...
[L1 语义检索] 完成: matches=1, time=245ms
```

---

## 后续优化

### 可能的增强(未实现)

由于阿里云 ReactAgent 不支持内置监听器,以下功能暂时无法实现:

- ❌ Agent 思考过程实时监听(`onStateUpdate`)
- ❌ 工具调用前拦截(`onToolCall`)
- ❌ 工具返回后拦截(`onToolResponse`)

如需这些功能,需要:
1. 包装每个工具,统一添加日志
2. 或使用支持监听器的 Agent 框架

当前实现已满足基本可观测需求。

---

## 验证

```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 | grep -E "Agent|工具|输出"
```
2026-06-25 16:16:46 +08:00