docs: 重构文档结构,分离学习笔记和 MVP 架构设计
**变更概述:** - 将 MVP 架构设计文档独立到项目根目录 `mvp/` - 整理 `docs/` 为纯学习和分析文档目录 - 按类型分类:learning(学习)、analysis(分析)、reports(报告)、guides(指南) **目录结构:** ``` mvp/ # MVP 架构设计(独立) ├── README.md # 数据库设计总览 ├── architecture/ # 架构文档 │ ├── agent-architecture-mvp.md │ ├── implementation-plan.md │ └── ... └── tables/ # 数据表设计 docs/ # 学习和分析文档 ├── learning/ # 学习笔记(00-08 编号) ├── analysis/ # 分析笔记 + 重构计划 ├── reports/ # 临时报告 └── guides/ # 指南文档 ``` **详细变更:** - docs/README.md → mvp/README.md(数据库设计入口) - docs/architecture/ → mvp/architecture/(架构设计) - docs/tables/ → mvp/tables/(数据表设计) - docs/学习笔记-*.md → docs/learning/07-*.md, 08-*.md - docs/项目学习路径.md → docs/learning/00-*.md - docs/功能分析报告.md → docs/analysis/ - docs/修复报告-*.md → docs/reports/ - docs/日志配置*.md → docs/guides/ 或 docs/reports/ - docs/design/ → docs/analysis/(问题分析和重构计划)
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
# 当前分片策略问题分析与根因
|
||||
|
||||
> 基于 `DocumentChunkServiceTest` 可视化测试的运行结果
|
||||
> 配置:`maxSize=800, overlap=100`(默认) / 可视化测试使用 `maxSize=300/200, overlap=50/30`
|
||||
|
||||
---
|
||||
|
||||
## 问题总览
|
||||
|
||||
| # | 问题 | 严重程度 | 根因归类 |
|
||||
|---|------|----------|----------|
|
||||
| 1 | 标题独立成空壳块 | 中 | 标题分割逻辑 |
|
||||
| 2 | 有序列表被拆散 | 高 | 段落级切割 + 缺少结构感知 |
|
||||
| 3 | 英文块 token 密度远低于中文块 | 高 | 字符计数代替 token 计数 |
|
||||
| 4 | 硬截断点在语义转折处无特殊处理 | 中 | 仅依赖 maxSize 触发 |
|
||||
| 5 | overlap 窗口对中文句号后截取命中率低 | 低 | 句子校准逻辑覆盖不全 |
|
||||
|
||||
---
|
||||
|
||||
## 问题 1:标题独立成空壳块
|
||||
|
||||
### 现象
|
||||
|
||||
运维文档 `maxSize=300` 下,H1 标题产生了一个只有 14 字符的分块:
|
||||
|
||||
```
|
||||
Chunk #0
|
||||
│ Title: CPU高负载问题排查指南
|
||||
│ Range: [0→14] (14字符)
|
||||
│ Content:
|
||||
│ │ # CPU高负载问题排查指南
|
||||
```
|
||||
|
||||
紧随其后的 `## 问题现象` 被分到下一个块。14 字符的块没有任何可检索的实质内容。
|
||||
|
||||
### 根因
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:71-83
|
||||
while (matcher.find()) {
|
||||
// 保存上一个章节
|
||||
if (lastEnd < matcher.start()) {
|
||||
String sectionContent = content.substring(lastEnd, matcher.start()).trim();
|
||||
if (!sectionContent.isEmpty()) { // ← 条件:content 非空
|
||||
sections.add(new Section(currentTitle, sectionContent, lastEnd));
|
||||
}
|
||||
}
|
||||
currentTitle = matcher.group(2).trim();
|
||||
lastEnd = matcher.start();
|
||||
}
|
||||
```
|
||||
|
||||
`splitByHeadings()` 遍历标题时,`lastEnd` 指向当前标题起始位置,`matcher.start()` 是下一个标题的起始位置。当 H1 后紧跟 H2(中间只有 `#` 行本身的内容),`content.substring(lastEnd, matcher.start())` 取出的是 **H1 标题行本身 + H1 标题行和 H2 之间的空白**。
|
||||
|
||||
关键问题:
|
||||
- H1 标题行被当作上一个 section 的 "content" 保存(因为中间文本不为空——标题行本身是文本)
|
||||
- 但实质上标题不应该独立成为一个可检索的分块
|
||||
|
||||
### 影响
|
||||
|
||||
- 向量库中出现大量无效向量(仅含标题、无实质内容)
|
||||
- 检索时可能召回标题块,Agent 得不到有用信息
|
||||
- 浪费 Milvus 存储空间
|
||||
|
||||
---
|
||||
|
||||
## 问题 2:有序列表被拆散
|
||||
|
||||
### 现象
|
||||
|
||||
排查步骤 1-4 在 Chunk #2,第 5 步被单独踢到 Chunk #3:
|
||||
|
||||
```
|
||||
Chunk #2 → 1. 登录服务器... 2. 使用 ps... 3. 查看应用日志... 4. 检查数据库...
|
||||
Chunk #3 → 5. 检查JVM内存...
|
||||
```
|
||||
|
||||
Agent 调用工具拿到 Chunk #2 时,排查步骤不完整,可能漏掉关键操作。
|
||||
|
||||
### 根因
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:174-188
|
||||
private List<String> splitByParagraphs(String content) {
|
||||
List<String> paragraphs = new ArrayList<>();
|
||||
String[] parts = content.split("\n\n+"); // ← 双换行分割
|
||||
for (String part : parts) {
|
||||
String trimmed = part.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
paragraphs.add(trimmed);
|
||||
}
|
||||
}
|
||||
return paragraphs;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:132-148
|
||||
if (currentChunk.length() > 0 &&
|
||||
currentChunk.length() + paragraph.length() > chunkConfig.getMaxSize()) {
|
||||
// 触发切分——不关心这个段落属于什么语义结构
|
||||
String overlap = getOverlapText(chunkContent);
|
||||
currentChunk = new StringBuilder(overlap);
|
||||
}
|
||||
currentChunk.append(paragraph).append("\n\n");
|
||||
```
|
||||
|
||||
两层根因:
|
||||
1. `splitByParagraphs()` 只认 `\n\n+` 作为段落分割符,不识别 **有序列表**(`1. \n2. \n3.` 之间通常是单换行)
|
||||
2. `chunkSection()` 走到字符上限就切,完全不感知"这是一个列表的第几项"——列表项之间的语义强关联被忽略
|
||||
|
||||
### 影响
|
||||
|
||||
- 排查步骤、操作指南类文档的完整性被破坏
|
||||
- RAG 检索召回不完整的步骤列表,Agent 据此操作可能导致遗漏
|
||||
- 这是运维场景的致命问题——运维文档大量使用列表
|
||||
|
||||
---
|
||||
|
||||
## 问题 3:英文块 token 密度远低于中文块
|
||||
|
||||
### 现象
|
||||
|
||||
可视化测试数据:
|
||||
|
||||
```
|
||||
中文: 218字符 → 2个分块(约218 tokens,密度 ~1.0 token/字符)
|
||||
英文: 602字符 → 3个分块(约150 tokens,密度 ~0.25 token/字符)
|
||||
```
|
||||
|
||||
同样 `maxSize=200`,英文 602 字符装了 150 token 还产生 3 个分块;中文 218 字符装了 218 token 只产生 2 个分块。中文块的实际 token 负担是英文的 **~4x**。
|
||||
|
||||
### 根因
|
||||
|
||||
```java
|
||||
// DocumentChunkConfig.java:18
|
||||
private int maxSize = 800; // 字符数上限
|
||||
|
||||
// DocumentChunkService.java:132-133
|
||||
if (currentChunk.length() + paragraph.length() > chunkConfig.getMaxSize()) {
|
||||
// 这里比的是 Java String.length() — 字符数,不是 token 数
|
||||
```
|
||||
|
||||
Java 的 `String.length()` 对每个 Unicode 字符(包括中文)都返回 1。但 LLM tokenizer 对中文和英文的 token 化效率完全不同:
|
||||
|
||||
```
|
||||
"这是中文" → 4 字符 → ~4 tokens (1:1)
|
||||
"This is English" → 15 字符 → ~4 tokens (3.75:1)
|
||||
```
|
||||
|
||||
用字符数作为切割上限,相当于:
|
||||
- 中文块:可以装 800 token(甚至更多)
|
||||
- 英文块:只能装 ~200 token
|
||||
|
||||
LLM 上下文窗口是按 token 计费的,这种偏差意味着**中文知识库的 RAG 开销是英文的 4 倍**。
|
||||
|
||||
### 影响
|
||||
|
||||
- LLM 调用成本不可预测(中英混排时波动大)
|
||||
- 中文知识库的上下文窗口利用率极易超标
|
||||
- 无法对 prompt 的 token 预算做精确控制
|
||||
|
||||
---
|
||||
|
||||
## 问题 4:硬截断在语义转折处无特殊处理
|
||||
|
||||
### 现象
|
||||
|
||||
同问题 2 的根因延伸。当前逻辑:
|
||||
|
||||
```
|
||||
段落1 + 段落2 + 段落3 + ... + 段落N → 总字符数 < maxSize → 继续追加
|
||||
→ 总字符数 > maxSize → 立刻切
|
||||
```
|
||||
|
||||
不考虑「段落 N 和段落 N+1 是否属于同一语义单元」。两个语义上需要绑定的段落恰好越过 maxSize 边界就会被拆散。
|
||||
|
||||
### 根因
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:132
|
||||
if (currentChunk.length() + paragraph.length() > chunkConfig.getMaxSize()) {
|
||||
```
|
||||
|
||||
触发条件只有一个——字符数。不缺以下信号:
|
||||
- 相邻段落的语义相似度(可用 embedding 计算)
|
||||
- 当前缓冲区是否处于列表/表格/代码块内部
|
||||
- 当前位置是否是 Markdown 层级的自然边界(如 `##` 标题前)
|
||||
|
||||
### 影响
|
||||
|
||||
- 切出来的分块边界在语义上不可预测
|
||||
- 同一主题的内容可能跨越两个分块,召回时只能拿到一半上下文
|
||||
|
||||
---
|
||||
|
||||
## 问题 5:overlap 句子校准对中文覆盖不全
|
||||
|
||||
### 现象
|
||||
|
||||
测试用的中文句子边界校准场景中,文档字数不足 `maxSize=100`,未触发切分。但即便触发,当前校准逻辑存在盲区:
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:203-206
|
||||
int lastSentenceEnd = Math.max(
|
||||
overlap.lastIndexOf('。'), // 只有三个终止符
|
||||
Math.max(overlap.lastIndexOf('?'), overlap.lastIndexOf('!'))
|
||||
);
|
||||
```
|
||||
|
||||
### 根因
|
||||
|
||||
中文句子终止符不止 `。?!` 三种:
|
||||
|
||||
| 终止符 | 是否覆盖 | 遗漏场景 |
|
||||
|--------|----------|----------|
|
||||
| `。` | ✅ | — |
|
||||
| `?` | ✅ | — |
|
||||
| `!` | ✅ | — |
|
||||
| `;`(分号) | ❌ | 长复句的语义断点 |
|
||||
| `:`(冒号) | ❌ | 列表/说明的引入点 |
|
||||
| `……` | ❌ | 省略号表示语义未尽 |
|
||||
| `\n`(换行) | ❌ | 中文短句常用换行代替标点 |
|
||||
|
||||
阈值逻辑也有盲区:
|
||||
|
||||
```java
|
||||
if (lastSentenceEnd > overlapSize / 2) {
|
||||
// only apply if sentence boundary is in the LATER half of overlap
|
||||
}
|
||||
```
|
||||
|
||||
如果句子边界在重叠区的前半段(即离截断点不到 overlap/2),直接退回原始截取——但实际上即使在前半段,也比随机截取更好。
|
||||
|
||||
### 影响
|
||||
|
||||
- 中文内容的重叠窗口可能从句子中间截取
|
||||
- 新分块的"种子"文本不完整,影响该块的语义完整性
|
||||
|
||||
---
|
||||
|
||||
## 根因总结
|
||||
|
||||
所有 5 个问题的根源收敛到两点:
|
||||
|
||||
### 根因 A:切割触发器只有一个维度——字符数
|
||||
|
||||
```
|
||||
currentChunk.length() + paragraph.length() > maxSize → 切!
|
||||
```
|
||||
|
||||
这个条件不知道:
|
||||
- 这个"paragraph"是列表项还是普通段落?(问题 2)
|
||||
- 中文还是英文?(问题 3)
|
||||
- 和上一条内容语义紧密还是已经转移话题?(问题 4)
|
||||
- 这个位置是在 Markdown 结构树上的什么层级?(问题 1)
|
||||
|
||||
### 根因 B:文档结构感知仅限于正则标题
|
||||
|
||||
```java
|
||||
Pattern headingPattern = Pattern.compile("^(#{1,6})\\s+(.+)$", Pattern.MULTILINE);
|
||||
```
|
||||
|
||||
这是唯一的结构感知入口。正则比 AST 脆弱,无法区分:
|
||||
- 代码块内的 `#` 注释 vs 真正的 Markdown 标题
|
||||
- 列表项 vs 段落
|
||||
- 代码块 vs 正文
|
||||
- 表格 vs 正文
|
||||
|
||||
---
|
||||
|
||||
## 修复优先级建议
|
||||
|
||||
| 优先级 | 问题 | 对策 | 改动量 |
|
||||
|--------|------|------|--------|
|
||||
| P0 | 问题 3(中英 token 密度) | 字符计数 → token 计数 | ~10 行 |
|
||||
| P0 | 问题 2(列表拆散) | 增加列表结构感知 | ~30 行 |
|
||||
| P1 | 问题 1(标题空壳) | 标题与下一个 H2 之间内容为空时合并 | ~15 行 |
|
||||
| P1 | 问题 4(硬截断) | 语义相似度辅助决策切点 | ~30 行 |
|
||||
| P2 | 问题 5(句子校准覆盖) | 增加终止符 + 降低阈值条件 | ~5 行 |
|
||||
|
||||
最终方案:替换为 Spring AI `TokenTextSplitter`,同时保留本项目特有的 `title` 元数据传播能力(因为 `TokenTextSplitter` 也不感知 Markdown 标题)。
|
||||
@@ -0,0 +1,200 @@
|
||||
# Plan: 分片策略第 4 步重构
|
||||
|
||||
> 分支: `refactor/rag-chunking-strategy`
|
||||
> 状态: 规划中
|
||||
> 范围: 仅改 `DocumentChunkService.chunkSection()` 一个方法
|
||||
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
经 debug 确认,当前分片流程的 1/2/3 步逻辑正确:
|
||||
|
||||
```
|
||||
第1步 chunkDocument() → splitByHeadings(content) ✅ 保持不变
|
||||
第2步 for each Section → 循环章节 ✅ 保持不变
|
||||
第3步 chunkSection() 入口 → 容量短路判断 + splitByParagraphs ✅ 保持不变
|
||||
第4步 chunkSection() 累积循环 → 段落累积 + 字符触发切分 ❌ 需重构
|
||||
```
|
||||
|
||||
**第 4 步的两个核心问题:**
|
||||
|
||||
| 问题 | 现象 |
|
||||
|------|------|
|
||||
| A. 丢失顺序 | `trim()` + 手工拼接 `\n\n` 导致 `currentStartIndex` 漂移 |
|
||||
| B. 结构无感知 | 有序列表项被拆散到不同分块(排查步骤 1-4 在一块,第 5 步在另一块) |
|
||||
|
||||
---
|
||||
|
||||
## 目标
|
||||
|
||||
改造 `chunkSection()` 的段落累积循环,使其:
|
||||
|
||||
1. **不丢顺序** — 用原始文本索引替代手工拼装的 `currentStartIndex`
|
||||
2. **感知列表结构** — 有序/无序列表项之间不在中间切断
|
||||
3. **Token 感知** — 用启发式 token 估算替代纯字符计数(为后续 Spring AI TokenTextSplitter 做准备)
|
||||
4. **软边界** — 在接近上限时查找语义安全切点,而非硬截断
|
||||
|
||||
---
|
||||
|
||||
## 不改的部分
|
||||
|
||||
| 组件 | 理由 |
|
||||
|------|------|
|
||||
| `splitByHeadings()` | 标题分割正确,正则够用 |
|
||||
| `getOverlapText()` | 句子校准逻辑保留,作为安全网 |
|
||||
| `DocumentChunk` 数据结构 | 字段完备,无需新增 |
|
||||
| `DocumentChunkConfig` | 增加 `maxTokens` 字段,保留原字段兼容 |
|
||||
| `VectorIndexService` | 消费者改动延后到下一阶段 |
|
||||
|
||||
---
|
||||
|
||||
## 改动方案
|
||||
|
||||
### 改动 1: `DocumentChunkConfig` — 增加 token 配置
|
||||
|
||||
```java
|
||||
// 新增字段
|
||||
private int maxTokens = 500; // token 上限(中文约500字,英文约2000字符)
|
||||
private int maxTokensHard = 600; // 硬上限(maxTokens × 1.2)
|
||||
|
||||
// 保留原字段作为向后兼容
|
||||
private int maxSize = 800; // 保留但标记 @Deprecated
|
||||
```
|
||||
|
||||
### 改动 2: `chunkSection()` — 改造累积循环
|
||||
|
||||
**当前逻辑(伪代码):**
|
||||
|
||||
```
|
||||
for each paragraph:
|
||||
if length + paragraph > maxSize → 切分 → 从 overlap 开始新块
|
||||
append paragraph + "\n\n"
|
||||
```
|
||||
|
||||
**新逻辑(伪代码):**
|
||||
|
||||
```
|
||||
for each paragraph:
|
||||
currentTokens = estimateTokens(buffer)
|
||||
paraTokens = estimateTokens(paragraph)
|
||||
|
||||
if currentTokens + paraTokens > maxTokens:
|
||||
if isInUnbreakableContext(buffer, paragraph):
|
||||
if currentTokens + paraTokens > maxTokensHard:
|
||||
→ 必须切(硬上限保护)
|
||||
else:
|
||||
→ 不切,继续累积(容忍超出,保护列表完整性)
|
||||
else:
|
||||
→ 切分(段落边界 = 安全切点)
|
||||
→ 从 overlap 开始新块
|
||||
else:
|
||||
→ 不切,继续累积
|
||||
|
||||
append paragraph + "\n\n"
|
||||
```
|
||||
|
||||
### 改动 3: 新增 `estimateTokens()` — 启发式 token 估算
|
||||
|
||||
```java
|
||||
/**
|
||||
* 启发式 token 估算(无需外部依赖)
|
||||
* 中文: ~1 字符/token
|
||||
* 英文/数字: ~4 字符/token
|
||||
* 标点/空白: 忽略
|
||||
*/
|
||||
private int estimateTokens(String text) {
|
||||
int tokens = 0;
|
||||
for (char c : text.toCharArray()) {
|
||||
if (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|
||||
|| Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A) {
|
||||
tokens += 1; // 中文字符 1:1
|
||||
} else if (Character.isWhitespace(c)) {
|
||||
// 空白字符不计
|
||||
} else {
|
||||
tokens += 1; // 非中文凑 4 个算 1 token(简化)
|
||||
}
|
||||
}
|
||||
// 非中文部分 / 4
|
||||
return tokens;
|
||||
}
|
||||
```
|
||||
|
||||
### 改动 4: 新增 `isInUnbreakableContext()` — 结构感知
|
||||
|
||||
```java
|
||||
/**
|
||||
* 判断当前段落是否属于不可中断的结构
|
||||
* 返回 true = 不能在当前位置切分
|
||||
*/
|
||||
private boolean isInUnbreakableContext(String buffer, String nextParagraph) {
|
||||
// 有序列表: "1. " "2. " "3. " 格式
|
||||
if (nextParagraph.matches("^\\d{1,2}\\.\\s.*")) {
|
||||
// 前一个段落也是列表项 → 不切
|
||||
String lastLine = getLastNonEmptyLine(buffer);
|
||||
if (lastLine.matches("^\\d{1,2}\\.\\s.*|.*\\n\\d{1,2}\\.\\s.*")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 无序列表: "- " 或 "* " 格式
|
||||
if (nextParagraph.matches("^[-*]\\s.*")) {
|
||||
String lastLine = getLastNonEmptyLine(buffer);
|
||||
if (lastLine.matches("^[-*]\\s.*|.*\\n[-*]\\s.*")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 代码块: ``` 内部不切
|
||||
if (buffer.contains("```") && countOccurrences(buffer, "```") % 2 == 1) {
|
||||
return true; // 在未闭合的代码块内 → 不切
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
### 改动 5: 修复 index 漂移
|
||||
|
||||
```java
|
||||
// 当前问题:用手工拼装的 chunkContent.length() 推算 offset
|
||||
// String chunkContent = currentChunk.toString().trim(); ← trim 丢字符
|
||||
// currentStartIndex = currentStartIndex + chunkContent.length() - overlap.length(); ← 漂移
|
||||
|
||||
// 改为:用段落在原始文档中的实际位置
|
||||
// 对每个 paragraph 记录其在 section.content 中的 offset,切分时直接使用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 改动文件清单
|
||||
|
||||
| 文件 | 改动 | 行数变化 |
|
||||
|------|------|----------|
|
||||
| `config/DocumentChunkConfig.java` | +2 字段 | +8 |
|
||||
| `service/DocumentChunkService.java` | 改造 `chunkSection()` + 3 个新方法 | ~+50 / -20 |
|
||||
| `test/.../DocumentChunkServiceTest.java` | 新增列表结构感知 + token 估算用例 | +40 |
|
||||
|
||||
总计改动约 80 行,仅影响一个核心方法。
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
| # | 用例 | 预期 |
|
||||
|---|------|------|
|
||||
| 1 | 有序列表(5 项,每项 50 字符,maxTokens=180) | 5 项不拆散,容忍略超上限 |
|
||||
| 2 | 有序列表(20 项,超 maxTokensHard) | 在硬上限处切,但不在列表项中间切 |
|
||||
| 3 | 纯段落(10 段,每段 100 字符,maxTokens=300) | 在段落边界切 |
|
||||
| 4 | 中文 800 字 vs 英文 3200 字符 | 分块数接近 |
|
||||
| 5 | H1→空的→H2(标题空壳) | 仍有(不在本次修复范围) |
|
||||
| 6 | 原有测试:空文档、短文档、标题分割、重叠、chunkIndex | 全部通过 |
|
||||
|
||||
---
|
||||
|
||||
## 后续阶段
|
||||
|
||||
| 阶段 | 内容 | 依赖 |
|
||||
|------|------|------|
|
||||
| **Phase 1(本次)** | 改 `chunkSection()` — token + 列表感知 | 无 |
|
||||
| Phase 2 | 标题空壳问题修复(`splitByHeadings` 合并相邻空 section) | Phase 1 |
|
||||
| Phase 3 | 可选:切换到 Spring AI `TokenTextSplitter` | Phase 1/2 |
|
||||
| Phase 4 | 语义相似度辅助切点决策 | Phase 1 |
|
||||
| Phase 5 | Markdown AST 解析替代正则 | 低优先级 |
|
||||
@@ -0,0 +1,337 @@
|
||||
# SuperBizAgent-java 功能分析报告
|
||||
|
||||
> 分析日期:2026-05-30
|
||||
> 分析站点:http://localhost:9900
|
||||
> 分析工具:Playwright MCP
|
||||
|
||||
---
|
||||
|
||||
## 📊 项目概览
|
||||
|
||||
这是一个**智能 OnCall 助手**系统,基于 Spring AI + DeepSeek V4 Flash + BGE-M3 向量化 + Zilliz Cloud (Milvus) 构建的 AIOps 平台。
|
||||
|
||||
**核心定位**:为运维/SRE 团队提供 7×24 小时智能告警分析和问题诊断能力。
|
||||
|
||||
---
|
||||
|
||||
## ✨ 核心功能模块
|
||||
|
||||
### 1️⃣ 智能对话系统
|
||||
|
||||
**界面特点**:
|
||||
- 清爽的聊天界面
|
||||
- 左侧:会话管理(新建对话、近期对话列表)
|
||||
- 右侧:对话区域 + AI Ops 快捷按钮
|
||||
|
||||
**能力列表**:
|
||||
|
||||
| 能力 | 说明 | 工具支持 |
|
||||
|------|------|---------|
|
||||
| 📅 时间与日期 | 获取当前日期和时间 | ✅ |
|
||||
| 🌤️ 天气查询 | 查询天气信息 | ⚠️ 当前工具集未配置 |
|
||||
| 📚 内部知识库搜索 | 搜索公司文档、流程、最佳实践、技术指南 | ✅ RAG (Milvus) |
|
||||
| ⚠️ Prometheus 告警查询 | 查询监控系统告警信息 | ✅ |
|
||||
| 📋 腾讯云日志查询 | 查询 CLS 日志(系统指标、应用日志、慢查询、系统事件) | ✅ |
|
||||
|
||||
**交互特性**:
|
||||
- 流式对话响应(SSE)
|
||||
- Markdown 渲染支持
|
||||
- 代码高亮(highlight.js)
|
||||
- 快速/标准模式切换
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ AI Ops 自动化分析 ⭐️
|
||||
|
||||
**触发方式**:点击右上角橙色 "AI Ops" 按钮
|
||||
|
||||
**功能流程**:
|
||||
```mermaid
|
||||
graph LR
|
||||
A[点击 AI Ops] --> B[SSE 流式响应]
|
||||
B --> C[读取 Prometheus 告警]
|
||||
C --> D[关联多源数据]
|
||||
D --> E[LLM 根因分析]
|
||||
E --> F[生成结构化报告]
|
||||
```
|
||||
|
||||
**实际分析案例**(从 2026-05-30 12:57:36 响应提取):
|
||||
|
||||
```markdown
|
||||
📋 告警分析报告
|
||||
|
||||
活跃告警清单:
|
||||
┌─────────────────┬──────────┬──────────────────┬──────────────────────┬──────┐
|
||||
│ 告警名称 │ 级别 │ 目标服务 │ 首次触发时间 │ 状态 │
|
||||
├─────────────────┼──────────┼──────────────────┼──────────────────────┼──────┤
|
||||
│ HighCPUUsage │ WARN │ payment-service │ 2026-05-30 12:32:40 │ 活跃 │
|
||||
│ HighMemoryUsage │ CRITICAL │ order-service │ 2026-05-30 12:42:40 │ 活跃 │
|
||||
│ SlowResponse │ WARN │ user-service │ 2026-05-30 12:47:40 │ 活跃 │
|
||||
└─────────────────┴──────────┴──────────────────┴──────────────────────┴──────┘
|
||||
|
||||
🔍 告警根因分析1 - HighMemoryUsage (order-service) — CRITICAL
|
||||
|
||||
症状描述:
|
||||
- JVM 堆内存使用率持续攀升:3.4GB → 3.8GB(4GB上限),当前 91%
|
||||
- 近 10 分钟内触发 15 次 Full GC,平均耗时 850ms,内存回收效果越来越差
|
||||
- OOM Killer 已触发杀死进程(退出码 137,Pod 已重启 3 次)
|
||||
- 数据库连接池耗尽:active=50/50,waiting=23 个线程
|
||||
- 消息队列 order-process-queue 积压 15,823 条消息
|
||||
|
||||
日志证据:
|
||||
```java
|
||||
2026-05-30 20:45:52 FATAL order-service:
|
||||
java.lang.OutOfMemoryError: Java heap space
|
||||
at com.example.order.service.OrderService.processLargeOrder(OrderService.java:156)
|
||||
StackTrace: OrderService.processLargeOrder
|
||||
-> OrderRepository.findByCondition
|
||||
-> HikariPool.getConnection
|
||||
```
|
||||
|
||||
根因结论:
|
||||
order-service 的 `OrderService.processLargeOrder()` 方法存在内存泄漏。
|
||||
该方法在处理大批量订单时,将过多数据加载到 JVM 堆中未及时释放,导致:
|
||||
→ JVM 堆内存持续膨胀至满 → 频繁 Full GC 但无法回收 → OutOfMemoryError
|
||||
→ OOM Killer 杀死进程 → Pod 重启(已 3 次)
|
||||
→ 数据库连接在 OOM 过程中未能正常归还连接池 → 连接池耗尽
|
||||
→ 消息队列消费进程也被 OOM/Kill 影响 → 队列积压 1.5 万+ 条消息
|
||||
|
||||
🔍 告警根因分析2 - HighCPUUsage (payment-service) — WARN
|
||||
|
||||
症状描述:
|
||||
- CPU 使用率 92%,进程全部为 Java,线程数 245
|
||||
- 1 分钟负载 3.82,5 分钟负载 3.65(4 核容器已严重过载)
|
||||
- Redis 连接持续超时(重试 3 次仍失败)
|
||||
|
||||
系统指标:
|
||||
```
|
||||
2026-05-30 20:57:52 WARN payment-service:
|
||||
CPU使用率 92%, 线程数 245, load_1m=3.82, load_5m=3.65 (4核)
|
||||
```
|
||||
```
|
||||
|
||||
**分析深度**:
|
||||
- ✅ 自动关联告警、日志、指标、系统事件
|
||||
- ✅ 提取关键证据(OOM 日志、堆栈跟踪、系统事件)
|
||||
- ✅ 推理根因链路(内存泄漏 → Full GC → OOM → Pod 重启 → 连接池耗尽)
|
||||
- ✅ 识别级联影响(消息队列积压)
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ 会话管理
|
||||
|
||||
- **新建对话**:快速开始新一轮交互
|
||||
- **近期对话列表**:保留历史会话
|
||||
- **删除对话**:清理无用会话
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 后端技术栈
|
||||
|
||||
根据 `devflow/projects/2026-05-29-chatmodel-abstraction` 文档分析:
|
||||
|
||||
| 组件 | 技术选型 | 说明 |
|
||||
|------|---------|------|
|
||||
| **Chat 模型** | DeepSeek V4 Flash | Spring AI 原生 starter |
|
||||
| **Embedding** | SiliconFlow BGE-M3 | OpenAI 兼容模式,1024 维向量 |
|
||||
| **向量数据库** | Zilliz Cloud (Milvus) | 存储知识库向量,collection: `biz` |
|
||||
| **Web 框架** | Spring Boot | - |
|
||||
| **AI 框架** | Spring AI 1.1.7 | ChatModel/EmbeddingModel 抽象 |
|
||||
| **MCP 工具集成** | ToolCallbackProvider | 可选(支持 enabled: false) |
|
||||
| **日志服务** | 腾讯云 CLS | 系统指标、应用日志、慢查询、系统事件 |
|
||||
| **监控系统** | Prometheus | 告警查询 |
|
||||
|
||||
**架构亮点**(2026-05-29 重构成果):
|
||||
- ✅ 面向 Spring AI 抽象接口编程(`ChatModel`、`EmbeddingModel`)
|
||||
- ✅ yml 配置驱动模型路由(`ModelRoutingConfig`)
|
||||
- ✅ 多厂商并存(DeepSeek + SiliconFlow)
|
||||
- ✅ 换模型只需改配置,无需改代码
|
||||
|
||||
### 前端技术栈
|
||||
|
||||
根据浏览器分析:
|
||||
|
||||
| 组件 | 技术 |
|
||||
|------|------|
|
||||
| **UI 风格** | 简洁对话式界面 |
|
||||
| **渲染** | Markdown + highlight.js (代码高亮) |
|
||||
| **通信** | SSE (Server-Sent Events) 流式响应 |
|
||||
| **图标** | 自定义 SVG 图标 |
|
||||
| **响应式** | 左侧固定 240px,右侧自适应 |
|
||||
|
||||
### API 端点
|
||||
|
||||
根据网络请求分析:
|
||||
|
||||
| 端点 | 方法 | 说明 | 响应格式 |
|
||||
|------|------|------|---------|
|
||||
| `/api/ai_ops` | POST | AI Ops 自动化分析 | `text/event-stream` |
|
||||
| `/api/chat` | POST | 普通对话(推测) | `text/event-stream` |
|
||||
| `/api/sessions` | GET | 会话管理(推测) | JSON |
|
||||
|
||||
**SSE 数据格式**(从日志提取):
|
||||
```
|
||||
event:message
|
||||
data:{"type":"content","data":"正在读取告警并拆解任务...\n"}
|
||||
|
||||
event:message
|
||||
data:{"type":"content","data":"📋 **告警分析报告**\n\n"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 发现的问题
|
||||
|
||||
### 1. favicon 404
|
||||
```
|
||||
[ERROR] Failed to load resource: the server responded with a status of 404 ()
|
||||
@ http://localhost:9900/favicon.ico:0
|
||||
```
|
||||
**影响**:浏览器标签页无图标,控制台 1 条错误
|
||||
|
||||
**建议**:添加 `src/main/resources/static/favicon.ico`
|
||||
|
||||
### 2. CDN 资源加载失败
|
||||
```
|
||||
[GET] https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/es/highlight.min.js
|
||||
=> [FAILED] net::ERR_BLOCKED_BY_ORB
|
||||
```
|
||||
**影响**:代码高亮功能可能失效
|
||||
|
||||
**建议**:
|
||||
- 方案 1:下载 highlight.js 到本地 `/static/js/`
|
||||
- 方案 2:更换 CDN(unpkg、cdnjs)
|
||||
- 方案 3:改用非 ES Module 版本
|
||||
|
||||
---
|
||||
|
||||
## 💡 核心价值主张
|
||||
|
||||
### 🎯 解决的痛点
|
||||
|
||||
| 传统运维 | 智能 OnCall 助手 |
|
||||
|---------|---------------|
|
||||
| 凌晨告警,登录多个系统查看 | AI Ops 一键获取根因报告 |
|
||||
| 翻查日志、指标,手动关联 | 自动关联多数据源,提取证据 |
|
||||
| 新人不熟悉排查流程 | 内置最佳实践,知识库搜索 |
|
||||
| 人工推理耗时 15-30 分钟 | LLM 推理 3 分钟内完成 |
|
||||
|
||||
### 🚀 典型使用场景
|
||||
|
||||
**场景 1:凌晨告警快速响应**
|
||||
```
|
||||
03:15 收到 PagerDuty 告警
|
||||
→ 打开 localhost:9900
|
||||
→ 点击 "AI Ops"
|
||||
→ 3 分钟内获得根因 + 修复建议 + 证据链
|
||||
→ 执行修复并记录
|
||||
```
|
||||
|
||||
**场景 2:知识库查询**
|
||||
```
|
||||
"搜索一下发布流程的文档"
|
||||
→ RAG 从 Milvus 检索相关文档
|
||||
→ DeepSeek 生成友好回答
|
||||
```
|
||||
|
||||
**场景 3:日志关联分析**
|
||||
```
|
||||
"order-service 最近有 OOM 吗?"
|
||||
→ 查询腾讯云 CLS 系统事件日志
|
||||
→ 关联应用日志
|
||||
→ 提取关键堆栈跟踪
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📂 相关代码文件
|
||||
|
||||
推荐查看以下关键文件了解实现细节:
|
||||
|
||||
```
|
||||
src/main/java/org/example/controller/ChatController.java # 对话 API
|
||||
src/main/java/org/example/service/AiOpsService.java # AI Ops 核心逻辑
|
||||
src/main/java/org/example/service/ChatService.java # 聊天服务
|
||||
src/main/java/org/example/service/RagService.java # RAG 知识库
|
||||
src/main/java/org/example/service/VectorEmbeddingService.java # 向量化
|
||||
src/main/java/org/example/config/ModelRoutingConfig.java # 模型路由
|
||||
src/main/java/org/example/config/SiliconFlowEmbeddingConfig.java # BGE-M3 配置
|
||||
src/main/resources/application.yml # 配置中心
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔬 测试验证
|
||||
|
||||
### 已验证功能
|
||||
|
||||
| 功能 | 测试结果 |
|
||||
|------|---------|
|
||||
| 页面加载 | ✅ 正常 |
|
||||
| AI Ops 自动分析 | ✅ 正常(56s 完成分析) |
|
||||
| 对话交互 | ✅ 正常(流式响应) |
|
||||
| Markdown 渲染 | ✅ 正常(标题、列表、代码块、引用) |
|
||||
| 会话管理 | ✅ 正常(新建、删除) |
|
||||
|
||||
### 冒烟测试套件
|
||||
|
||||
根据 `src/test/java/org/example/service/` 存在以下测试:
|
||||
|
||||
```
|
||||
ChatAndEmbeddingSmokeTest.java # Chat + Embedding 冒烟测试(5/5 ✅)
|
||||
FullPipelineSmokeTest.java # 全流程冒烟测试(5/5 ✅)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 未来改进建议
|
||||
|
||||
### 功能增强
|
||||
1. **告警自动修复**:从根因分析 → 生成修复脚本 → 执行(需人工确认)
|
||||
2. **历史告警学习**:建立告警-根因知识库,加速后续分析
|
||||
3. **多租户支持**:不同团队隔离数据
|
||||
4. **移动端适配**:PWA,支持推送通知
|
||||
|
||||
### 性能优化
|
||||
1. **缓存热点查询**:Prometheus 告警缓存 5 分钟
|
||||
2. **流式响应优化**:SSE 心跳保持连接
|
||||
3. **向量检索加速**:Milvus IVF_FLAT → HNSW
|
||||
|
||||
### 可观测性
|
||||
1. **分析耗时追踪**:各环节耗时(告警查询、日志查询、LLM 推理)
|
||||
2. **准确率监控**:根因分析准确率
|
||||
3. **用户反馈**:👍/👎 评价系统
|
||||
|
||||
---
|
||||
|
||||
## 📊 技术指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| **响应时间** | AI Ops 分析 56s(含多源数据查询 + LLM 推理) |
|
||||
| **向量维度** | 1024(BGE-M3) |
|
||||
| **知识库规模** | Milvus collection `biz`(具体条数未知) |
|
||||
| **并发能力** | SSE 流式,支持多用户(未压测) |
|
||||
| **模型** | DeepSeek V4 Flash(快速模式) |
|
||||
|
||||
---
|
||||
|
||||
## 🎓 总结
|
||||
|
||||
**SuperBizAgent-java** 是一个生产级的智能运维助手,核心亮点在于:
|
||||
|
||||
1. **真正的 AI Ops**:不是简单的告警查询,而是多源数据关联 + LLM 根因推理
|
||||
2. **工程化良好**:Spring AI 抽象、配置驱动、模型可切换
|
||||
3. **用户体验优秀**:流式响应、Markdown 渲染、一键分析
|
||||
4. **可扩展性强**:MCP 工具集成、RAG 知识库、多厂商模型
|
||||
|
||||
**适用场景**:中大型公司 SRE/运维团队的 7×24 小时智能值守。
|
||||
|
||||
---
|
||||
|
||||
> 🔗 相关文档:
|
||||
> - [ChatModel 抽象重构记录](../devflow/projects/2026-05-29-chatmodel-abstraction/brief.md)
|
||||
> - [技术决策](../devflow/projects/2026-05-29-chatmodel-abstraction/decisions.md)
|
||||
> - [验收记录](../devflow/projects/2026-05-29-chatmodel-abstraction/acceptance.md)
|
||||
Reference in New Issue
Block a user