This commit is contained in:
zhuyongxin
2026-04-30 11:19:46 +08:00
parent 0671ad8bab
commit 80eada415d
42 changed files with 9135 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
.idea/CopilotChatHistory.xml
.idea/CopilotChatTabs.xml
*.iws
*.iml
*.ipr
.idea/*
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
### Application Logs ###
*.log
app.log
logs/
### Upload Files ###
uploads/
### Temp Scripts ###
*.sh
*.py
### docker
/volumes
/server.pid
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+268
View File
@@ -0,0 +1,268 @@
# SuperBizAgent Makefile
# 用于自动化项目初始化和文档向量化
# 配置变量
SERVER_URL = http://localhost:9900
UPLOAD_API = $(SERVER_URL)/api/upload
DOCS_DIR = aiops-docs
HEALTH_CHECK_API = $(SERVER_URL)/milvus/health
DOCKER_COMPOSE_FILE = vector-database.yml
MILVUS_CONTAINER = milvus-standalone
# 颜色输出
GREEN = \033[0;32m
YELLOW = \033[0;33m
RED = \033[0;31m
NC = \033[0m # No Color
.PHONY: help init start stop restart check upload clean up down status wait
# 默认目标:显示帮助信息
help:
@echo "$(GREEN)SuperBizAgent Makefile$(NC)"
@echo ""
@echo "可用命令:"
@echo " $(YELLOW)make init$(NC) - 🚀 一键初始化(启动Docker → 启动服务 → 上传文档)"
@echo " $(YELLOW)make up$(NC) - 启动 Docker Compose(Milvus 向量数据库)"
@echo " $(YELLOW)make down$(NC) - 停止 Docker Compose"
@echo " $(YELLOW)make status$(NC) - 查看 Docker 容器状态"
@echo " $(YELLOW)make start$(NC) - 启动 Spring Boot 服务(后台运行)"
@echo " $(YELLOW)make stop$(NC) - 停止 Spring Boot 服务"
@echo " $(YELLOW)make restart$(NC) - 重启 Spring Boot 服务"
@echo " $(YELLOW)make check$(NC) - 检查服务器是否运行"
@echo " $(YELLOW)make upload$(NC) - 上传 aiops-docs 目录下的所有文档"
@echo " $(YELLOW)make clean$(NC) - 清理临时文件"
@echo ""
@echo "使用示例:"
@echo " 1. 一键初始化: make init"
@echo " 2. 手动启动: make up && make start && make upload"
@echo " 3. 停止服务: make stop && make down"
# 一键初始化:启动Docker → 启动服务 → 检查服务 → 上传文档
init:
@echo "$(GREEN)🚀 开始一键初始化 SuperBizAgent...$(NC)"
@echo ""
@echo "$(YELLOW)步骤 1/4: 启动 Docker Compose(Milvus 向量数据库)$(NC)"
@$(MAKE) up
@echo ""
@echo "$(YELLOW)步骤 2/4: 启动 Spring Boot 服务$(NC)"
@$(MAKE) start
@echo ""
@echo "$(YELLOW)步骤 3/4: 等待服务就绪$(NC)"
@$(MAKE) wait
@echo ""
@echo "$(YELLOW)步骤 4/4: 上传 AIOps 文档到向量数据库$(NC)"
@$(MAKE) upload
@echo ""
@echo "$(GREEN)═══════════════════════════════════════════════════════$(NC)"
@echo "$(GREEN)✅ 初始化完成!所有文档已成功向量化存储到数据库$(NC)"
@echo "$(GREEN)═══════════════════════════════════════════════════════$(NC)"
@echo ""
@echo "$(GREEN)🌐 服务访问地址:$(NC)"
@echo " API 服务: $(SERVER_URL)"
@echo " Attu (Web UI): http://localhost:8000"
@echo ""
@echo "$(YELLOW)💡 提示: 服务正在后台运行,查看日志: tail -f server.log$(NC)"
# 启动 Spring Boot 服务(后台运行)
start:
@echo "$(YELLOW)🚀 启动 Spring Boot 服务...$(NC)"
@if curl -s -f $(HEALTH_CHECK_API) > /dev/null 2>&1; then \
echo "$(GREEN)✅ 服务已经在运行中 ($(SERVER_URL))$(NC)"; \
else \
echo "$(YELLOW)📦 正在启动服务(后台运行)...$(NC)"; \
nohup mvn spring-boot:run > server.log 2>&1 & \
echo $$! > server.pid; \
echo "$(GREEN)✅ 服务启动命令已执行$(NC)"; \
echo "$(YELLOW) PID: $$(cat server.pid)$(NC)"; \
echo "$(YELLOW) 日志文件: server.log$(NC)"; \
fi
# 等待服务器就绪(最多等待 60 秒)
wait:
@echo "$(YELLOW)⏳ 等待服务器就绪...$(NC)"
@max_attempts=60; \
attempt=0; \
while [ $$attempt -lt $$max_attempts ]; do \
if curl -s -f $(HEALTH_CHECK_API) > /dev/null 2>&1; then \
echo "$(GREEN)✅ 服务器已就绪!($(SERVER_URL))$(NC)"; \
exit 0; \
fi; \
attempt=$$((attempt + 1)); \
printf "$(YELLOW) 等待中... [$$attempt/$$max_attempts]$(NC)\r"; \
sleep 1; \
done; \
echo ""; \
echo "$(RED)❌ 服务器启动超时!$(NC)"; \
echo "$(YELLOW)请检查日志: tail -f server.log$(NC)"; \
exit 1
# 检查服务器是否运行
check:
@echo "$(YELLOW)🔍 检查服务器状态...$(NC)"
@if curl -s -f $(HEALTH_CHECK_API) > /dev/null 2>&1; then \
echo "$(GREEN)✅ 服务器运行正常 ($(SERVER_URL))$(NC)"; \
else \
echo "$(RED)❌ 服务器未运行或无法连接!$(NC)"; \
echo "$(YELLOW)请先启动项目: mvn spring-boot:run$(NC)"; \
exit 1; \
fi
# 上传所有文档
upload:
@echo "$(YELLOW)📤 开始上传 $(DOCS_DIR) 目录下的文档...$(NC)"
@if [ ! -d "$(DOCS_DIR)" ]; then \
echo "$(RED)❌ 目录 $(DOCS_DIR) 不存在!$(NC)"; \
exit 1; \
fi
@count=0; \
success=0; \
failed=0; \
for file in $(DOCS_DIR)/*.md; do \
if [ -f "$$file" ]; then \
count=$$((count + 1)); \
filename=$$(basename "$$file"); \
echo "$(YELLOW) [$$count] 上传文件: $$filename$(NC)"; \
response=$$(curl -s -w "\n%{http_code}" -X POST $(UPLOAD_API) \
-F "file=@$$file" \
-H "Accept: application/json"); \
http_code=$$(echo "$$response" | tail -n1); \
body=$$(echo "$$response" | sed '$$d'); \
if [ "$$http_code" = "200" ]; then \
echo "$(GREEN) ✅ 成功: $$filename$(NC)"; \
success=$$((success + 1)); \
else \
echo "$(RED) ❌ 失败: $$filename (HTTP $$http_code)$(NC)"; \
echo "$$body" | head -n 3; \
failed=$$((failed + 1)); \
fi; \
sleep 1; \
fi; \
done; \
echo ""; \
echo "$(GREEN)📊 上传统计:$(NC)"; \
echo " 总计: $$count 个文件"; \
echo " $(GREEN)成功: $$success$(NC)"; \
if [ $$failed -gt 0 ]; then \
echo " $(RED)失败: $$failed$(NC)"; \
fi
# 停止 Spring Boot 服务
stop:
@echo "$(YELLOW)🛑 停止 Spring Boot 服务...$(NC)"
@if [ -f server.pid ]; then \
pid=$$(cat server.pid); \
if ps -p $$pid > /dev/null 2>&1; then \
kill $$pid; \
echo "$(GREEN)✅ 服务已停止 (PID: $$pid)$(NC)"; \
else \
echo "$(YELLOW)⚠️ 进程不存在 (PID: $$pid)$(NC)"; \
fi; \
rm -f server.pid; \
else \
echo "$(YELLOW)⚠️ 未找到 server.pid 文件$(NC)"; \
pkill -f "spring-boot:run" && echo "$(GREEN)✅ 已停止所有 spring-boot 进程$(NC)" || echo "$(YELLOW)⚠️ 没有运行中的 spring-boot 进程$(NC)"; \
fi
# 重启 Spring Boot 服务
restart:
@echo "$(YELLOW)🔄 重启 Spring Boot 服务...$(NC)"
@echo ""
@echo "$(YELLOW)步骤 1/2: 停止服务$(NC)"
@$(MAKE) stop
@echo ""
@echo "$(YELLOW)步骤 2/2: 启动服务$(NC)"
@$(MAKE) start
@echo ""
@$(MAKE) wait
@echo ""
@echo "$(GREEN)✅ 服务重启完成!$(NC)"
# 清理临时文件
clean:
@echo "$(YELLOW)🧹 清理临时文件...$(NC)"
@rm -rf uploads/*.tmp
@rm -f server.pid server.log
@echo "$(GREEN)✅ 清理完成$(NC)"
# 显示文档列表
list-docs:
@echo "$(YELLOW)📚 $(DOCS_DIR) 目录下的文档:$(NC)"
@if [ -d "$(DOCS_DIR)" ]; then \
ls -lh $(DOCS_DIR)/*.md 2>/dev/null || echo "$(RED)没有找到 .md 文件$(NC)"; \
else \
echo "$(RED)目录 $(DOCS_DIR) 不存在$(NC)"; \
fi
# 测试单个文件上传
test-upload:
@echo "$(YELLOW)🧪 测试上传单个文件...$(NC)"
@if [ -f "$(DOCS_DIR)/cpu_high_usage.md" ]; then \
curl -X POST $(UPLOAD_API) \
-F "file=@$(DOCS_DIR)/cpu_high_usage.md" \
-H "Accept: application/json" | jq .; \
else \
echo "$(RED)测试文件不存在$(NC)"; \
fi
# 启动 Docker Compose(智能检测,避免重复启动)
up:
@echo "$(YELLOW)🐳 检查 Docker 容器状态...$(NC)"
@if [ ! -f "$(DOCKER_COMPOSE_FILE)" ]; then \
echo "$(RED)❌ Docker Compose 文件不存在: $(DOCKER_COMPOSE_FILE)$(NC)"; \
exit 1; \
fi
@if docker ps --format '{{.Names}}' | grep -q "^$(MILVUS_CONTAINER)$$"; then \
echo "$(GREEN)✅ Milvus 容器已经在运行中$(NC)"; \
echo "$(YELLOW)📋 当前运行的容器:$(NC)"; \
docker ps --filter "name=milvus" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"; \
else \
echo "$(YELLOW)🚀 启动 Docker Compose...$(NC)"; \
docker-compose -f $(DOCKER_COMPOSE_FILE) up -d; \
echo ""; \
echo "$(YELLOW)⏳ 等待容器启动...$(NC)"; \
sleep 5; \
if docker ps --format '{{.Names}}' | grep -q "^$(MILVUS_CONTAINER)$$"; then \
echo "$(GREEN)✅ Docker Compose 启动成功!$(NC)"; \
echo ""; \
echo "$(GREEN)📋 运行中的容器:$(NC)"; \
docker ps --filter "name=milvus" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"; \
echo ""; \
echo "$(GREEN)🌐 服务访问地址:$(NC)"; \
echo " Milvus: localhost:19530"; \
echo " Attu (Web UI): http://localhost:8000"; \
echo " MinIO: http://localhost:9001 (admin/minioadmin)"; \
else \
echo "$(RED)❌ 容器启动失败,请检查日志: docker-compose -f $(DOCKER_COMPOSE_FILE) logs$(NC)"; \
exit 1; \
fi; \
fi
# 停止 Docker Compose
down:
@echo "$(YELLOW)🛑 停止 Docker Compose...$(NC)"
@if [ ! -f "$(DOCKER_COMPOSE_FILE)" ]; then \
echo "$(RED)❌ Docker Compose 文件不存在: $(DOCKER_COMPOSE_FILE)$(NC)"; \
exit 1; \
fi
@if docker ps --format '{{.Names}}' | grep -q "milvus"; then \
docker-compose -f $(DOCKER_COMPOSE_FILE) down; \
echo "$(GREEN)✅ Docker Compose 已停止$(NC)"; \
else \
echo "$(YELLOW)⚠️ 没有运行中的 Milvus 容器$(NC)"; \
fi
# 查看 Docker 容器状态
status:
@echo "$(YELLOW)📊 Docker 容器状态:$(NC)"
@echo ""
@if docker ps -a --format '{{.Names}}' | grep -q "milvus"; then \
docker ps -a --filter "name=milvus" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"; \
echo ""; \
running=$$(docker ps --filter "name=milvus" --format '{{.Names}}' | wc -l | tr -d ' '); \
total=$$(docker ps -a --filter "name=milvus" --format '{{.Names}}' | wc -l | tr -d ' '); \
echo "$(GREEN)运行中: $$running / $$total$(NC)"; \
else \
echo "$(YELLOW)⚠️ 没有找到 Milvus 相关容器$(NC)"; \
echo "$(YELLOW)提示: 运行 'make docker-up' 启动容器$(NC)"; \
fi
+135
View File
@@ -0,0 +1,135 @@
# CPU使用率过高告警处理方案
## 告警名称
- **告警名**: `HighCPUUsage`
- **告警级别**: 严重
- **触发条件**: CPU使用率持续5分钟超过80%
## 问题描述
当服务器或容器的CPU使用率持续超过80%时,可能导致:
- 应用响应变慢
- 请求超时增加
- 系统负载过高
- 可能触发雪崩效应
## 排查步骤
### 步骤1: 获取当前时间
**工具**: `get_current_time`
**目的**: 确定告警发生的时间范围,用于后续日志查询
### 步骤2: 查询系统日志
**工具**: `query_logs`
**参数要求**:
- **地域**: `ap-guangzhou` (广州)
- **日志主题**: `system-metrics`
- **时间范围**: 最近30分钟
- **查询条件**: `level:ERROR OR cpu_usage:>80`
**查询示例**:
```
地域: ap-guangzhou
日志主题: system-metrics
时间范围: 2024-01-20 14:00:00 到 2024-01-20 14:30:00
查询语句: cpu_usage > 80 AND service_name:*
```
### 步骤3: 分析CPU消耗进程
查看日志中的进程信息,重点关注:
- 进程名称和PID
- CPU占用百分比
- 进程启动时间
- 进程所属服务
### 步骤4: 查询应用日志
**工具**: `query_logs`
**参数要求**:
- **地域**: `ap-guangzhou`
- **日志主题**: `application-logs`
- **时间范围**: 与告警时间一致
- **查询条件**: `level:ERROR OR level:WARN`
## 常见原因分析
### 原因1: 死循环或无限递归
**特征**:
- 单个进程CPU占用接近100%
- 应用日志中有大量重复的错误堆栈
- 内存使用也可能同步增长
**处理方案**:
1. 立即重启受影响的服务实例
2. 查看应用日志定位代码问题
3. 回滚到上一个稳定版本
4. 通知开发团队修复代码bug
### 原因2: 流量突增
**特征**:
- 多个进程CPU使用率均匀升高
- 请求量明显增加
- 响应时间变长但无明显错误
**处理方案**:
1. 检查是否有营销活动或突发流量
2. 启动自动扩容机制
3. 如果是恶意流量,启用限流策略
4. 监控扩容后的CPU使用率
### 原因3: 定时任务重叠执行
**特征**:
- CPU使用率周期性升高
- 特定时间点出现
- 日志中有定时任务执行记录
**处理方案**:
1. 检查定时任务配置
2. 调整任务执行时间,避免重叠
3. 优化任务执行逻辑
4. 增加任务执行互斥锁
### 原因4: 数据库查询慢
**特征**:
- 应用CPU高,但实际业务逻辑简单
- 日志中有慢查询记录
- 数据库连接池占用高
**处理方案**:
1. 查询慢SQL日志
2. 优化SQL语句或添加索引
3. 检查是否有全表扫描
4. 考虑增加缓存层
## 紧急处理措施
### 立即操作(5分钟内)
1. **扩容**: 如果是流量问题,立即增加实例数量
2. **限流**: 如果怀疑恶意流量,启用限流规则
3. **重启**: 如果是单个实例问题,重启该实例
### 短期措施(30分钟内)
1. 分析日志找出根本原因
2. 如果是代码问题,准备回滚
3. 如果是配置问题,调整配置
4. 持续监控CPU使用率变化
### 长期优化
1. 代码性能优化
2. 增加监控告警阈值
3. 完善自动扩缩容策略
4. 定期进行压力测试
## 验证步骤
1. 确认CPU使用率降到正常水平(<60%)
2. 检查应用响应时间恢复正常
3. 确认无新的错误日志产生
4. 观察30分钟确保问题不再复现
## 相关告警
- `HighMemoryUsage`: 内存使用率过高
- `HighLoadAverage`: 系统负载过高
- `SlowResponse`: 响应时间过长
## 联系方式
- **运维团队**: ops-team@company.com
- **开发团队**: dev-team@company.com
- **紧急电话**: 400-xxx-xxxx
+343
View File
@@ -0,0 +1,343 @@
# 磁盘使用率过高告警处理方案
## 告警名称
- **告警名**: `HighDiskUsage`
- **告警级别**: 警告(>80%)/ 严重(>90%)
- **触发条件**: 磁盘使用率持续5分钟超过阈值
## 问题描述
磁盘使用率过高会导致:
- 无法写入新数据
- 日志无法记录
- 应用崩溃
- 数据库损坏
- 系统性能下降
## 排查步骤
### 步骤1: 获取当前时间
**工具**: `get_current_time`
**目的**: 记录告警时间,用于日志查询
### 步骤2: 查询系统磁盘使用情况
**工具**: `query_logs`
**参数要求**:
- **地域**: `ap-guangzhou`
- **日志主题**: `system-metrics`
- **时间范围**: 最近30分钟
- **查询条件**: `disk_usage:>80 OR disk_full:true`
**查询示例**:
```
地域: ap-guangzhou
日志主题: system-metrics
时间范围: [当前时间-30分钟] 到 [当前时间]
查询语句: disk_usage > 80 OR filesystem:full
```
### 步骤3: 查询应用日志
**工具**: `query_logs`
**参数要求**:
- **地域**: `ap-guangzhou`
- **日志主题**: `application-logs`
- **查询条件**: `No space left on device OR disk full`
### 步骤4: 分析磁盘占用
从日志中分析:
- 哪个目录占用空间最大
- 日志文件大小
- 临时文件数量
- 数据文件增长趋势
## 常见原因分析
### 原因1: 日志文件过大
**特征**:
- /var/log 目录占用大量空间
- 应用日志文件持续增长
- 没有日志轮转或轮转失败
- 日志级别设置为DEBUG
**处理方案**:
1. **立即清理**:
```bash
# 查找大日志文件
find /var/log -type f -size +100M
# 清空日志文件(保留文件)
> /var/log/application.log
# 压缩旧日志
gzip /var/log/*.log.1
# 删除旧日志
find /var/log -name "*.log.*" -mtime +7 -delete
```
2. **配置日志轮转**:
- 启用logrotate
- 设置日志保留天数
- 配置日志压缩
- 限制单个日志文件大小
3. **优化日志级别**:
- 生产环境使用INFO或WARN级别
- 避免打印大对象
- 减少不必要的日志
### 原因2: 临时文件堆积
**特征**:
- /tmp 目录占用大量空间
- 大量临时文件未清理
- 文件上传临时目录满
- 缓存文件过多
**处理方案**:
1. **清理临时文件**:
```bash
# 查找大临时文件
find /tmp -type f -size +100M
# 删除7天前的临时文件
find /tmp -type f -mtime +7 -delete
# 清理应用临时目录
rm -rf /app/temp/*
```
2. **定期清理**:
- 设置定时任务清理临时文件
- 应用启动时清理旧临时文件
- 文件处理完成后立即删除
3. **优化临时文件使用**:
- 使用流式处理避免临时文件
- 限制临时文件大小
- 使用内存缓存替代临时文件
### 原因3: 数据文件增长过快
**特征**:
- 数据库文件持续增长
- 业务数据量激增
- 没有数据归档策略
- 历史数据未清理
**处理方案**:
1. **数据清理**:
- 删除过期数据
- 归档历史数据
- 清理测试数据
- 优化数据存储
2. **数据归档**:
- 建立数据归档策略
- 定期归档历史数据
- 使用对象存储保存归档数据
- 实现冷热数据分离
3. **扩容磁盘**:
- 如果是正常业务增长,扩容磁盘
- 使用云盘动态扩容
- 考虑分布式存储
### 原因4: 备份文件占用空间
**特征**:
- 备份目录占用大量空间
- 多个历史备份文件
- 备份文件未压缩
- 备份未转移到其他存储
**处理方案**:
1. **清理旧备份**:
```bash
# 查找备份文件
find /backup -name "*.bak" -mtime +30
# 删除30天前的备份
find /backup -name "*.bak" -mtime +30 -delete
```
2. **优化备份策略**:
- 只保留最近N天的备份
- 压缩备份文件
- 备份到对象存储
- 实现增量备份
3. **备份转移**:
- 将备份转移到专用存储
- 使用云存储服务
- 定期清理本地备份
### 原因5: 应用缓存文件过多
**特征**:
- 缓存目录占用大量空间
- 缓存文件未过期清理
- 缓存策略不合理
- 缓存命中率低
**处理方案**:
1. **清理缓存**:
```bash
# 清理应用缓存
rm -rf /app/cache/*
# 清理Maven缓存
rm -rf ~/.m2/repository
# 清理Docker缓存
docker system prune -a
```
2. **优化缓存策略**:
- 设置缓存过期时间
- 限制缓存大小
- 实现LRU淘汰策略
- 使用Redis等外部缓存
### 原因6: Docker镜像和容器占用
**特征**:
- Docker占用大量磁盘空间
- 大量未使用的镜像
- 停止的容器未清理
- 容器日志过大
**处理方案**:
1. **清理Docker资源**:
```bash
# 清理未使用的镜像
docker image prune -a
# 清理停止的容器
docker container prune
# 清理未使用的卷
docker volume prune
# 清理所有未使用资源
docker system prune -a --volumes
```
2. **限制容器日志**:
- 配置日志驱动
- 限制日志文件大小
- 设置日志轮转
3. **优化镜像**:
- 使用多阶段构建
- 减小镜像体积
- 定期清理旧镜像
## 紧急处理措施
### 立即操作(5分钟内)
1. **快速清理**:
- 删除大日志文件
- 清理临时文件
- 删除不必要的文件
2. **释放空间**:
```bash
# 查找最大的10个文件
du -ah / | sort -rh | head -n 10
# 查找最大的10个目录
du -h --max-depth=1 / | sort -rh | head -n 10
```
3. **紧急扩容**:
- 如果无法快速清理,立即扩容磁盘
### 短期措施(30分钟内)
1. **系统清理**:
- 清理包管理器缓存
- 清理系统日志
- 清理core dump文件
2. **应用优化**:
- 配置日志轮转
- 清理应用缓存
- 删除过期数据
3. **监控设置**:
- 设置磁盘使用率告警
- 监控磁盘增长趋势
- 定期检查大文件
### 长期优化
1. **自动化清理**:
- 设置定时清理任务
- 实现自动日志轮转
- 自动归档历史数据
2. **容量规划**:
- 根据业务增长规划容量
- 预留足够的磁盘空间
- 实现弹性扩容
3. **监控完善**:
- 监控各目录磁盘使用
- 预测磁盘使用趋势
- 提前告警
## 常用命令
### 查看磁盘使用情况
```bash
# 查看磁盘使用率
df -h
# 查看目录大小
du -sh /var/log
# 查找大文件
find / -type f -size +1G
# 查看inode使用情况
df -i
```
### 清理命令
```bash
# 清空文件内容(保留文件)
> /var/log/large.log
# 删除N天前的文件
find /path -mtime +N -delete
# 压缩日志文件
gzip /var/log/*.log
# 清理包管理器缓存
yum clean all # CentOS
apt-get clean # Ubuntu
```
## 验证步骤
1. 确认磁盘使用率降到安全水平(<70%)
2. 检查应用运行正常
3. 验证日志可以正常写入
4. 确认数据库运行正常
5. 持续监控磁盘使用情况
## 预防措施
1. **监控告警**: 设置60%预警,80%严重告警
2. **定期清理**: 每周自动清理临时文件和旧日志
3. **容量规划**: 根据增长趋势提前扩容
4. **日志管理**: 实施统一的日志管理策略
5. **数据归档**: 建立数据归档和清理机制
## 相关告警
- `InodeFull`: inode耗尽
- `DiskIOHigh`: 磁盘IO过高
- `DiskReadOnly`: 磁盘只读
- `FileSystemError`: 文件系统错误
## 联系方式
- **运维团队**: ops-team@company.com
- **存储团队**: storage-team@company.com
- **紧急电话**: 400-xxx-xxxx
## 参考文档
- [磁盘管理最佳实践](internal-docs/disk-management.md)
- [日志管理规范](internal-docs/log-management.md)
- [数据归档策略](internal-docs/data-archiving.md)
+180
View File
@@ -0,0 +1,180 @@
# 内存使用率过高告警处理方案
## 告警名称
- **告警名**: `HighMemoryUsage`
- **告警级别**: 严重
- **触发条件**: 内存使用率持续5分钟超过85%
## 问题描述
当服务器或容器的内存使用率持续超过85%时,可能导致:
- 应用频繁GC(垃圾回收)
- OOM(Out Of Memory)错误
- 应用崩溃或重启
- 系统swap频繁,性能急剧下降
## 排查步骤
### 步骤1: 获取当前时间
**工具**: `get_current_time`
**目的**: 确定告警发生的时间范围
### 步骤2: 查询系统监控日志
**工具**: `query_logs`
**参数要求**:
- **地域**: `ap-guangzhou`
- **日志主题**: `system-metrics`
- **时间范围**: 最近30分钟
- **查询条件**: `memory_usage:>85 OR event:OOM`
**查询示例**:
```
地域: ap-guangzhou
日志主题: system-metrics
时间范围: [当前时间-30分钟] 到 [当前时间]
查询语句: memory_usage > 85 OR oom_kill:true
```
### 步骤3: 查询应用日志
**工具**: `query_logs`
**参数要求**:
- **地域**: `ap-guangzhou`
- **日志主题**: `application-logs`
- **查询条件**: `OutOfMemoryError OR GC overhead`
### 步骤4: 分析内存使用情况
从日志中提取:
- JVM堆内存使用情况
- GC频率和耗时
- 是否有内存泄漏迹象
- 大对象分配记录
## 常见原因分析
### 原因1: 内存泄漏
**特征**:
- 内存使用率持续缓慢上升
- Full GC后内存无法释放
- 应用运行时间越长,内存占用越高
- 日志中有大量GC记录
**处理方案**:
1. **紧急措施**: 重启应用释放内存
2. **获取堆转储**: 在重启前dump内存快照
3. **分析内存泄漏**: 使用MAT工具分析堆转储文件
4. **定位代码问题**: 找出持有大量对象引用的代码
5. **修复并发布**: 修复内存泄漏代码并重新部署
### 原因2: 流量突增导致对象激增
**特征**:
- 内存使用率突然升高
- 与流量增长同步
- GC能够回收大部分内存
- 应用日志显示请求量激增
**处理方案**:
1. **立即扩容**: 增加应用实例数量
2. **启用限流**: 保护系统不被压垮
3. **优化缓存**: 检查缓存策略是否合理
4. **增加内存**: 如果是合理的业务增长,考虑增加内存配置
### 原因3: 缓存配置不当
**特征**:
- 缓存占用内存过大
- 缓存命中率低
- 缓存数据长时间不过期
- 日志中有缓存相关警告
**处理方案**:
1. **调整缓存大小**: 减少缓存容量上限
2. **优化过期策略**: 设置合理的TTL
3. **启用LRU淘汰**: 确保缓存能自动淘汰旧数据
4. **监控缓存指标**: 持续观察缓存命中率和内存占用
### 原因4: 大文件或大数据处理
**特征**:
- 特定时间点内存突增
- 日志中有文件上传或数据导入记录
- 处理完成后内存可以释放
- 与定时任务或用户操作相关
**处理方案**:
1. **优化处理逻辑**: 改为流式处理,避免一次性加载
2. **限制文件大小**: 设置上传文件大小限制
3. **分批处理**: 大数据处理改为分批次进行
4. **异步处理**: 将大数据处理任务放到后台队列
### 原因5: JVM参数配置不合理
**特征**:
- 堆内存配置过小
- 新生代和老年代比例不合理
- GC算法选择不当
- 元空间(Metaspace)溢出
**处理方案**:
1. **调整堆内存**: 增加-Xmx和-Xms参数
2. **优化GC参数**: 根据应用特点选择合适的GC算法
3. **调整代际比例**: 优化-XX:NewRatio参数
4. **增加元空间**: 调整-XX:MetaspaceSize参数
## 紧急处理措施
### 立即操作(5分钟内)
1. **重启高内存实例**: 快速释放内存,恢复服务
2. **扩容**: 增加实例数量,分散内存压力
3. **限流**: 如果是流量问题,启用限流保护
### 短期措施(30分钟内)
1. **分析日志**: 确定内存增长的根本原因
2. **dump内存快照**: 保留现场用于后续分析
3. **调整JVM参数**: 如果是配置问题,临时调整参数
4. **清理缓存**: 手动清理不必要的缓存数据
### 长期优化
1. **代码优化**: 修复内存泄漏,优化对象创建
2. **监控完善**: 增加内存监控指标和告警
3. **压力测试**: 定期进行内存压力测试
4. **容量规划**: 根据业务增长合理规划内存容量
## 验证步骤
1. 确认内存使用率降到正常水平(<70%)
2. 观察GC频率和耗时是否正常
3. 检查应用日志无OOM错误
4. 持续监控30分钟确保稳定
## 预防措施
1. **设置合理的内存告警阈值**: 70%预警,85%严重
2. **定期内存分析**: 每周分析内存使用趋势
3. **代码审查**: 关注大对象创建和集合使用
4. **压力测试**: 上线前进行充分的内存压力测试
## 相关工具命令
### 查看JVM内存使用
```bash
jmap -heap <pid>
```
### 生成堆转储文件
```bash
jmap -dump:format=b,file=heap.hprof <pid>
```
### 查看GC日志
```bash
jstat -gc <pid> 1000
```
## 相关告警
- `HighCPUUsage`: CPU使用率过高(可能伴随频繁GC)
- `FrequentGC`: GC频率过高
- `OOMError`: 内存溢出错误
- `SlowResponse`: 响应时间过长
## 联系方式
- **运维团队**: ops-team@company.com
- **开发团队**: dev-team@company.com
- **紧急电话**: 400-xxx-xxxx
## 参考文档
- [JVM内存管理最佳实践](internal-docs/jvm-memory-best-practices.md)
- [内存泄漏排查指南](internal-docs/memory-leak-troubleshooting.md)
+289
View File
@@ -0,0 +1,289 @@
# 服务不可用告警处理方案
## 告警名称
- **告警名**: `ServiceUnavailable`
- **告警级别**: 紧急
- **触发条件**: 服务健康检查失败或错误率超过50%
## 问题描述
服务不可用是最严重的故障,会导致:
- 用户无法访问服务
- 业务完全中断
- 可能造成经济损失
- 影响公司声誉
## 排查步骤
### 步骤1: 获取当前时间
**工具**: `get_current_time`
**目的**: 记录故障发生时间,用于后续分析
### 步骤2: 查询服务状态日志
**工具**: `query_logs`
**参数要求**:
- **地域**: `ap-guangzhou`
- **日志主题**: `application-logs`
- **时间范围**: 最近15分钟
- **查询条件**: `level:ERROR OR level:FATAL OR status:500`
**查询示例**:
```
地域: ap-guangzhou
日志主题: application-logs
时间范围: [当前时间-15分钟] 到 [当前时间]
查询语句: (level:ERROR OR level:FATAL) AND service_name:*
```
### 步骤3: 查询系统事件日志
**工具**: `query_logs`
**参数要求**:
- **地域**: `ap-guangzhou`
- **日志主题**: `system-events`
- **查询条件**: `event:restart OR event:crash OR event:oom_kill`
### 步骤4: 检查依赖服务状态
**工具**: `query_logs`
**参数要求**:
- **地域**: `ap-guangzhou`
- **日志主题**: `application-logs`
- **查询条件**: `downstream_service OR database OR redis OR mq`
## 常见原因分析
### 原因1: 应用崩溃或无法启动
**特征**:
- 所有实例都无法响应
- 日志中有启动失败记录
- 可能有OOM或其他致命错误
- 健康检查全部失败
**处理方案**:
1. **立即回滚**:
- 如果是新版本发布后出现,立即回滚到上一个稳定版本
- 使用蓝绿部署或金丝雀发布策略
2. **紧急修复**:
- 查看启动日志定位问题
- 检查配置文件是否正确
- 验证依赖服务是否可用
- 修复后重新部署
3. **临时方案**:
- 启用备用服务
- 显示维护页面
- 通知用户服务异常
### 原因2: 数据库连接失败
**特征**:
- 日志中有数据库连接错误
- "Connection refused" 或 "Too many connections"
- 数据库服务异常
- 连接池耗尽
**处理方案**:
1. **检查数据库状态**:
- 确认数据库服务是否运行
- 检查数据库连接数
- 查看数据库错误日志
2. **恢复数据库连接**:
- 重启数据库(如果必要)
- 增加最大连接数
- 释放僵死连接
- 检查网络连通性
3. **应用层处理**:
- 重启应用重建连接池
- 调整连接池配置
- 实现连接重试机制
### 原因3: 依赖服务故障
**特征**:
- 日志中有调用第三方服务失败
- Redis、MQ等中间件不可用
- 网络连接超时
- 级联故障
**处理方案**:
1. **快速降级**:
- 启用熔断机制
- 跳过非关键依赖
- 使用缓存数据
- 返回降级响应
2. **恢复依赖服务**:
- 检查Redis、MQ等服务状态
- 重启故障的依赖服务
- 修复网络连接问题
3. **隔离故障**:
- 防止故障扩散
- 限制重试次数
- 实现超时控制
### 原因4: 配置错误
**特征**:
- 最近有配置变更
- 日志中有配置加载错误
- 特定环境变量缺失
- 启动参数错误
**处理方案**:
1. **回滚配置**:
- 恢复到上一个正确的配置
- 检查配置文件语法
- 验证环境变量
2. **修复配置**:
- 对比正确的配置文件
- 修正错误的配置项
- 重新加载配置
3. **配置管理**:
- 使用配置中心统一管理
- 配置变更需要审批
- 保留配置历史版本
### 原因5: 资源耗尽
**特征**:
- 磁盘空间满
- 文件描述符耗尽
- 端口被占用
- 内存不足导致OOM
**处理方案**:
1. **清理资源**:
- 清理磁盘空间(日志、临时文件)
- 释放文件描述符
- 杀死占用端口的进程
2. **扩容资源**:
- 增加磁盘空间
- 调整系统限制(ulimit)
- 增加内存配置
3. **优化使用**:
- 实现日志轮转
- 及时关闭文件句柄
- 优化内存使用
### 原因6: 网络故障
**特征**:
- 无法访问服务
- 网络连接超时
- 负载均衡器异常
- DNS解析失败
**处理方案**:
1. **检查网络**:
- 测试网络连通性(ping, telnet)
- 检查防火墙规则
- 验证安全组配置
- 检查DNS解析
2. **修复网络**:
- 重启网络服务
- 修正路由配置
- 更新负载均衡器配置
- 切换到备用网络
3. **流量切换**:
- 切换到备用地域
- 使用备用域名
- 启用CDN
## 紧急处理流程
### 第一时间(1分钟内)
1. **确认故障**: 通过监控和日志确认服务确实不可用
2. **启动应急**: 通知相关人员,启动应急响应流程
3. **止损措施**:
- 如果是部分实例故障,摘除故障实例
- 如果是全部故障,启用备用服务或维护页面
### 5分钟内
1. **快速定位**: 查看日志和监控,快速定位问题原因
2. **决策处理**:
- 如果能快速修复,立即修复
- 如果无法快速修复,执行回滚
- 如果是依赖故障,启用降级
### 15分钟内
1. **恢复服务**: 通过回滚、修复或降级恢复服务
2. **验证功能**: 确认核心功能可用
3. **监控观察**: 持续监控服务状态
### 30分钟内
1. **全面验证**: 验证所有功能正常
2. **通知用户**: 如果影响用户,发布公告
3. **初步总结**: 记录故障原因和处理过程
## 验证步骤
1. **健康检查**: 确认所有实例健康检查通过
2. **功能测试**: 测试核心业务功能
3. **性能验证**: 确认响应时间和吞吐量正常
4. **错误率**: 确认错误率降到正常水平(<1%)
5. **持续监控**: 观察30分钟确保稳定
## 预防措施
1. **高可用架构**:
- 多实例部署
- 多地域容灾
- 数据库主从复制
- 关键服务冗余
2. **监控告警**:
- 完善的健康检查
- 实时监控告警
- 自动故障转移
- 异常自动恢复
3. **发布策略**:
- 灰度发布
- 蓝绿部署
- 金丝雀发布
- 快速回滚机制
4. **容错设计**:
- 熔断降级
- 限流保护
- 超时控制
- 重试机制
5. **演练测试**:
- 定期故障演练
- 压力测试
- 混沌工程
- 应急预案演练
## 故障复盘
故障恢复后,必须进行复盘:
1. **故障时间线**: 记录故障发生、发现、处理、恢复的完整时间线
2. **根因分析**: 深入分析故障根本原因
3. **影响评估**: 评估故障影响范围和损失
4. **改进措施**: 制定防止类似故障的改进措施
5. **文档更新**: 更新运维文档和应急预案
## 相关告警
- `HighErrorRate`: 错误率过高
- `HealthCheckFailed`: 健康检查失败
- `AllInstancesDown`: 所有实例下线
- `DatabaseConnectionFailed`: 数据库连接失败
## 紧急联系方式
- **运维值班**: 400-xxx-1111(24小时)
- **开发值班**: 400-xxx-2222(24小时)
- **DBA值班**: 400-xxx-3333(24小时)
- **网络团队**: 400-xxx-4444
- **安全团队**: 400-xxx-5555
## 升级机制
- **15分钟未恢复**: 升级到技术总监
- **30分钟未恢复**: 升级到CTO
- **1小时未恢复**: 启动最高级别应急响应
## 参考文档
- [应急响应流程](internal-docs/emergency-response.md)
- [故障处理手册](internal-docs/incident-handbook.md)
- [高可用架构设计](internal-docs/ha-architecture.md)
+257
View File
@@ -0,0 +1,257 @@
# 服务响应时间过长告警处理方案
## 告警名称
- **告警名**: `SlowResponse`
- **告警级别**: 警告
- **触发条件**: P99响应时间持续5分钟超过3秒
## 问题描述
当服务的响应时间持续超过阈值时,会导致:
- 用户体验下降
- 请求堆积
- 可能触发超时错误
- 影响下游服务
## 排查步骤
### 步骤1: 获取当前时间
**工具**: `get_current_time`
**目的**: 确定告警发生的准确时间
### 步骤2: 查询应用性能日志
**工具**: `query_logs`
**参数要求**:
- **地域**: `ap-guangzhou`
- **日志主题**: `application-logs`
- **时间范围**: 最近30分钟
- **查询条件**: `response_time:>3000 OR slow_query:true`
**查询示例**:
```
地域: ap-guangzhou
日志主题: application-logs
时间范围: [当前时间-30分钟] 到 [当前时间]
查询语句: response_time > 3000 AND level:WARN
```
### 步骤3: 查询数据库慢查询日志
**工具**: `query_logs`
**参数要求**:
- **地域**: `ap-guangzhou`
- **日志主题**: `database-slow-query`
- **时间范围**: 与告警时间一致
- **查询条件**: `query_time:>1000`
### 步骤4: 查询系统资源使用情况
**工具**: `query_logs`
**参数要求**:
- **地域**: `ap-guangzhou`
- **日志主题**: `system-metrics`
- **查询条件**: `cpu_usage OR memory_usage OR disk_io`
## 常见原因分析
### 原因1: 数据库慢查询
**特征**:
- 慢查询日志中有大量记录
- 数据库CPU使用率高
- 特定SQL执行时间长
- 数据库连接池接近满载
**处理方案**:
1. **立即优化**:
- 找出最慢的SQL语句
- 检查是否缺少索引
- 查看执行计划(EXPLAIN)
- 临时添加索引或优化SQL
2. **短期措施**:
- 增加数据库连接池大小
- 启用查询缓存
- 考虑读写分离
3. **长期优化**:
- 定期分析慢查询日志
- 优化数据库表结构
- 建立合适的索引策略
- 考虑分库分表
### 原因2: 外部API调用超时
**特征**:
- 日志中有第三方API调用记录
- 特定接口响应时间长
- 可能有网络超时错误
- 下游服务响应慢
**处理方案**:
1. **设置合理超时**:
- 调整HTTP客户端超时时间
- 避免无限等待
- 设置连接超时和读取超时
2. **实施降级策略**:
- 启用熔断机制
- 返回默认值或缓存数据
- 异步处理非关键调用
3. **优化调用方式**:
- 并行调用多个API
- 使用批量接口减少调用次数
- 增加本地缓存
### 原因3: 代码性能问题
**特征**:
- CPU使用率高
- 特定代码路径执行慢
- 日志中有性能警告
- 无明显外部依赖问题
**处理方案**:
1. **性能分析**:
- 使用APM工具定位慢方法
- 查看火焰图找出热点
- 分析代码执行路径
2. **代码优化**:
- 优化循环和递归
- 减少不必要的对象创建
- 使用更高效的数据结构
- 避免重复计算
3. **异步处理**:
- 非关键逻辑异步执行
- 使用消息队列解耦
- 实现异步响应
### 原因4: 缓存失效或缓存穿透
**特征**:
- 缓存命中率突然下降
- 数据库查询量激增
- 特定时间点响应变慢
- 日志中有缓存miss记录
**处理方案**:
1. **缓存预热**:
- 重启后立即预热热点数据
- 定时刷新缓存
- 避免缓存同时失效
2. **防止缓存穿透**:
- 对空值也进行缓存
- 使用布隆过滤器
- 参数校验防止恶意查询
3. **优化缓存策略**:
- 设置合理的过期时间
- 使用多级缓存
- 实现缓存更新机制
### 原因5: 系统资源不足
**特征**:
- CPU或内存使用率高
- 磁盘IO繁忙
- 网络带宽占用高
- 系统负载过高
**处理方案**:
1. **资源扩容**:
- 增加实例数量(水平扩展)
- 升级实例规格(垂直扩展)
- 优化资源分配
2. **负载均衡**:
- 检查负载均衡配置
- 确保流量均匀分布
- 移除异常实例
3. **限流降级**:
- 启用限流保护
- 降级非核心功能
- 优先保证核心业务
### 原因6: 网络问题
**特征**:
- 跨地域调用延迟高
- 网络丢包率增加
- 特定时间段网络拥堵
- 日志中有网络超时错误
**处理方案**:
1. **网络优化**:
- 使用CDN加速
- 优化网络路由
- 启用HTTP/2或QUIC
2. **就近访问**:
- 部署多地域实例
- 智能DNS解析
- 边缘计算
3. **压缩传输**:
- 启用GZIP压缩
- 优化数据传输格式
- 减少传输数据量
## 紧急处理措施
### 立即操作(5分钟内)
1. **扩容**: 如果是资源不足,立即增加实例
2. **限流**: 保护系统不被压垮
3. **降级**: 关闭非核心功能
4. **重启**: 如果是内存泄漏等问题,重启受影响实例
### 短期措施(30分钟内)
1. **定位根因**: 通过日志分析找出慢的环节
2. **临时优化**:
- 增加缓存
- 优化慢SQL
- 调整超时时间
3. **监控观察**: 持续观察响应时间变化
### 长期优化
1. **性能优化**: 代码层面的性能优化
2. **架构优化**: 引入缓存、消息队列等
3. **监控完善**: 增加更细粒度的性能监控
4. **压力测试**: 定期进行性能压测
## 验证步骤
1. 确认P99响应时间降到正常水平(<1秒)
2. 检查错误率是否下降
3. 观察用户投诉是否减少
4. 持续监控30分钟确保稳定
## 性能优化检查清单
- [ ] 数据库查询是否有索引
- [ ] 是否有N+1查询问题
- [ ] 缓存策略是否合理
- [ ] 外部API调用是否有超时设置
- [ ] 是否有不必要的同步等待
- [ ] 日志级别是否过于详细
- [ ] 是否有大对象序列化
- [ ] 网络调用是否可以批量化
## 相关监控指标
- **响应时间**: P50, P90, P99, P999
- **吞吐量**: QPS, TPS
- **错误率**: 4xx, 5xx错误比例
- **资源使用**: CPU, 内存, 网络, 磁盘IO
- **数据库**: 慢查询数量, 连接池使用率
- **缓存**: 命中率, 失效率
## 相关告警
- `HighCPUUsage`: CPU使用率过高
- `HighMemoryUsage`: 内存使用率过高
- `DatabaseSlowQuery`: 数据库慢查询
- `HighErrorRate`: 错误率过高
- `ServiceTimeout`: 服务超时
## 联系方式
- **运维团队**: ops-team@company.com
- **开发团队**: dev-team@company.com
- **DBA团队**: dba-team@company.com
- **紧急电话**: 400-xxx-xxxx
## 参考文档
- [性能优化最佳实践](internal-docs/performance-optimization.md)
- [数据库优化指南](internal-docs/database-optimization.md)
- [缓存使用规范](internal-docs/cache-best-practices.md)
+180
View File
@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>org.example</groupId>
<artifactId>super-biz-agent</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-ai.version>1.1.0</spring-ai.version>
<spring-ai-alibaba.version>1.1.0.0-RC2</spring-ai-alibaba.version>
<spring-ai-alibaba-extensions.version>1.1.0.0-RC2</spring-ai-alibaba-extensions.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-extensions-bom</artifactId>
<version>${spring-ai-alibaba-extensions.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${spring-ai-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.milvus</groupId>
<artifactId>milvus-sdk-java</artifactId>
<version>2.6.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<!-- 阿里云 DashScope SDK (文本向量化) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.17.0</version>
<exclusions>
<!-- 排除 slf4j-simple,使用 Spring Boot 默认的 Logback -->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- JSON Schema Generator - Spring AI 工具需要 -->
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
<version>4.36.0</version>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-module-jackson</artifactId>
<version>4.36.0</version>
</dependency>
<!-- Spring AI MCP Client - 使用 WebFlux 版本 -->
<!-- 注意:spring-ai-starter-mcp-client-webflux 已经包含了 mcp-annotations,无需单独引入 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>org.example.Main</mainClass>
</configuration>
</plugin>
<!-- Maven 编译器插件,配置 Lombok 注解处理器 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
+11
View File
@@ -0,0 +1,11 @@
package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
@@ -0,0 +1,19 @@
package org.example.agent.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class DateTimeTools {
/** 工具名常量,用于动态构建提示词 */
public static final String TOOL_GET_CURRENT_DATETIME = "getCurrentDateTime";
@Tool(description = "Get the current date and time in the user's timezone")
public String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
@@ -0,0 +1,79 @@
package org.example.agent.tool;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.service.VectorSearchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 内部文档查询工具
* 使用 RAG (Retrieval-Augmented Generation) 从内部知识库检索相关文档
*/
@Component
public class InternalDocsTools {
private static final Logger logger = LoggerFactory.getLogger(InternalDocsTools.class);
/** 工具名常量,用于动态构建提示词 */
public static final String TOOL_QUERY_INTERNAL_DOCS = "queryInternalDocs";
private final VectorSearchService vectorSearchService;
@Value("${rag.top-k:3}")
private int topK = 3; // 默认值
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 构造函数注入依赖
* Spring 会自动注入 VectorSearchService
*/
@Autowired
public InternalDocsTools(VectorSearchService vectorSearchService) {
this.vectorSearchService = vectorSearchService;
}
/**
* 查询内部文档工具
*
* @param query 搜索查询,描述您要查找的信息
* @return JSON 格式的搜索结果,包含相关文档内容、相似度分数和元数据
*/
@Tool(description = "Use this tool to search internal documentation and knowledge base for relevant information. " +
"It performs RAG (Retrieval-Augmented Generation) to find similar documents and extract processing steps. " +
"This is useful when you need to understand internal procedures, best practices, or step-by-step guides " +
"stored in the company's documentation.")
public String queryInternalDocs(
@ToolParam(description = "Search query describing what information you are looking for")
String query) {
try {
// 使用向量搜索服务检索相关文档
List<VectorSearchService.SearchResult> searchResults =
vectorSearchService.searchSimilarDocuments(query, topK);
if (searchResults.isEmpty()) {
return "{\"status\": \"no_results\", \"message\": \"No relevant documents found in the knowledge base.\"}";
}
// 将搜索结果转换为 JSON 格式
String resultJson = objectMapper.writeValueAsString(searchResults);
return resultJson;
} catch (Exception e) {
logger.error("[工具错误] queryInternalDocs 执行失败", e);
return String.format("{\"status\": \"error\", \"message\": \"Failed to query internal docs: %s\"}",
e.getMessage());
}
}
}
@@ -0,0 +1,694 @@
package org.example.agent.tool;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 日志查询工具
* 用于查询 CLS(云日志服务)的日志信息
* 支持 Mock 模式,提供与告警关联的模拟日志数据
*/
@Component
public class QueryLogsTools {
private static final Logger logger = LoggerFactory.getLogger(QueryLogsTools.class);
/** 工具名常量,用于动态构建提示词 */
public static final String TOOL_QUERY_LOGS = "queryLogs";
public static final String TOOL_GET_AVAILABLE_LOG_TOPICS = "getAvailableLogTopics";
private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${cls.mock-enabled:false}")
private boolean mockEnabled;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.of("Asia/Shanghai"));
@jakarta.annotation.PostConstruct
public void init() {
logger.info("✅ QueryLogsTools 初始化成功, Mock模式: {}", mockEnabled);
}
/**
* 获取可用的日志主题列表
* 用于查询前先了解有哪些日志主题可供查询
*/
@Tool(description = "Get all available log topics and their descriptions. " +
"Call this tool first before querying logs to understand what log topics are available. " +
"Returns a list of log topics with their names, descriptions, and example queries.")
public String getAvailableLogTopics() {
logger.info("获取可用的日志主题列表");
try {
List<LogTopicInfo> topics = new ArrayList<>();
// 系统指标日志
LogTopicInfo systemMetrics = new LogTopicInfo();
systemMetrics.setTopicName("system-metrics");
systemMetrics.setDescription("系统指标日志,包含 CPU、内存、磁盘使用率等系统资源监控数据");
systemMetrics.setExampleQueries(List.of(
"cpu_usage:>80",
"memory_usage:>85",
"disk_usage:>90",
"level:WARN AND service:payment-service"
));
systemMetrics.setRelatedAlerts(List.of("HighCPUUsage", "HighMemoryUsage", "HighDiskUsage"));
topics.add(systemMetrics);
// 应用日志
LogTopicInfo applicationLogs = new LogTopicInfo();
applicationLogs.setTopicName("application-logs");
applicationLogs.setDescription("应用日志,包含应用程序的错误日志、警告日志、慢请求日志、下游依赖调用日志等");
applicationLogs.setExampleQueries(List.of(
"level:ERROR",
"level:FATAL",
"http_status:500",
"response_time:>3000",
"slow",
"downstream OR redis OR database OR mq"
));
applicationLogs.setRelatedAlerts(List.of("ServiceUnavailable", "SlowResponse", "HighMemoryUsage"));
topics.add(applicationLogs);
// 数据库慢查询日志
LogTopicInfo dbSlowQuery = new LogTopicInfo();
dbSlowQuery.setTopicName("database-slow-query");
dbSlowQuery.setDescription("数据库慢查询日志,包含执行时间较长的 SQL 查询,可用于分析数据库性能问题");
dbSlowQuery.setExampleQueries(List.of(
"query_time:>2",
"table:orders",
"query_type:SELECT",
"*" // 查询所有慢查询
));
dbSlowQuery.setRelatedAlerts(List.of("SlowResponse", "ServiceUnavailable"));
topics.add(dbSlowQuery);
// 系统事件日志
LogTopicInfo systemEvents = new LogTopicInfo();
systemEvents.setTopicName("system-events");
systemEvents.setDescription("系统事件日志,包含 Kubernetes Pod 重启、OOM Kill、容器崩溃等系统级事件");
systemEvents.setExampleQueries(List.of(
"restart OR crash",
"oom_kill",
"event_type:PodRestart",
"reason:OOMKilled"
));
systemEvents.setRelatedAlerts(List.of("ServiceUnavailable", "HighMemoryUsage"));
topics.add(systemEvents);
// 构建输出
LogTopicsOutput output = new LogTopicsOutput();
output.setSuccess(true);
output.setTopics(topics);
output.setAvailableRegions(List.of("ap-guangzhou", "ap-shanghai", "ap-beijing", "ap-chengdu"));
output.setDefaultRegion("ap-guangzhou");
output.setMessage(String.format("共有 %d 个可用的日志主题。建议使用默认地域 'ap-guangzhou' 或省略 region 参数", topics.size()));
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(output);
} catch (Exception e) {
logger.error("获取日志主题列表失败", e);
return "{\"success\":false,\"message\":\"获取日志主题列表失败: " + e.getMessage() + "\"}";
}
}
/**
* 查询日志
* 从云日志服务查询指定条件的日志
*
* @param region 地域,如 ap-guangzhou
* @param logTopic 日志主题,如 system-metrics, application-logs
* @param query 查询条件,如 level:ERROR OR cpu_usage:>80
* @param limit 返回的日志条数,默认20条
*/
// 有效地域列表
private static final List<String> VALID_REGIONS = List.of(
"ap-guangzhou", "ap-shanghai", "ap-beijing", "ap-chengdu"
);
private static final String DEFAULT_REGION = "ap-guangzhou";
@Tool(description = "Query logs from Cloud Log Service (CLS). " +
"Use this tool to search application logs, system metrics, and other log data. " +
"IMPORTANT: Before calling this tool, you should call getAvailableLogTopics to understand what log topics are available. " +
"Available log topics: " +
"1) 'system-metrics' - System metrics logs (CPU, memory, disk usage, etc. Related to HighCPUUsage, HighMemoryUsage, HighDiskUsage alerts); " +
"2) 'application-logs' - Application logs (error logs, slow request logs, downstream dependency logs. Related to ServiceUnavailable, SlowResponse alerts); " +
"3) 'database-slow-query' - Database slow query logs (SQL queries with long execution time. Related to SlowResponse alerts); " +
"4) 'system-events' - System event logs (Pod restart, OOM Kill, container crash. Related to ServiceUnavailable, HighMemoryUsage alerts). " +
"logTopic (required, one of the above topics or their CLS topicId), " +
"query (optional, defaults to a curated search if empty), " +
"limit (optional, default 20, max 100).")
public String queryLogs(
@ToolParam(description = "地域,可选值: ap-guangzhou, ap-shanghai, ap-beijing, ap-chengdu。默认 ap-guangzhou") String region,
@ToolParam(description = "日志主题,如 system-metrics, application-logs, database-slow-query, system-events,也支持 CLS TopicId") String logTopic,
@ToolParam(description = "查询条件,支持 Lucene 语法,如 level:ERROR OR cpu_usage:>80;为空时返回该主题近 5 条核心日志") String query,
@ToolParam(description = "返回日志条数,默认20,最大100") Integer limit) {
int actualLimit = (limit == null || limit <= 0) ? 20 : Math.min(limit, 100);
String safeQuery = query == null ? "" : query;
try {
List<LogEntry> logEntries;
if (mockEnabled) {
// Mock 模式:返回与告警关联的模拟日志数据
logEntries = buildMockLogs(region, logTopic, safeQuery, actualLimit);
logger.info("使用 Mock 数据,返回 {} 条日志", logEntries.size());
} else {
// 真实模式:调用 CLS API(这里预留接口,后续实现)
return buildErrorResponse("CLS 真实查询尚未实现,请启用 mock 模式进行测试");
}
// 构建成功响应
QueryLogsOutput output = new QueryLogsOutput();
output.setSuccess(!logEntries.isEmpty());
output.setRegion(region);
output.setLogTopic(logTopic);
output.setQuery(safeQuery.isBlank() ? "DEFAULT_QUERY" : safeQuery);
output.setLogs(logEntries);
output.setTotal(logEntries.size());
output.setMessage(logEntries.isEmpty() ? "未找到匹配的日志" : String.format("成功查询到 %d 条日志", logEntries.size()));
String jsonResult = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(output);
logger.info("日志查询完成: 找到 {} 条日志", logEntries.size());
return jsonResult;
} catch (Exception e) {
logger.error("查询日志失败", e);
return buildErrorResponse("查询失败: " + e.getMessage());
}
}
/**
* 构建 Mock 日志数据
* 根据日志主题和查询条件返回与告警关联的模拟数据
*/
private List<LogEntry> buildMockLogs(String region, String logTopic, String query, int limit) {
List<LogEntry> logs = new ArrayList<>();
Instant now = Instant.now();
String safeTopic = logTopic == null ? "system-metrics" : logTopic.toLowerCase();
String normalizedQuery = query == null ? "" : query.toLowerCase();
// 根据日志主题和查询条件生成对应的 mock 数据
switch (safeTopic) {
case "system-metrics":
logs.addAll(buildSystemMetricsLogs(now, normalizedQuery, limit));
break;
case "application-logs":
logs.addAll(buildApplicationLogs(now, normalizedQuery, limit));
break;
case "database-slow-query":
logs.addAll(buildDatabaseSlowQueryLogs(now, normalizedQuery, limit));
break;
case "system-events":
logs.addAll(buildSystemEventsLogs(now, normalizedQuery, limit));
break;
default:
logs.addAll(buildGenericLogs(now, normalizedQuery, limit));
}
if (logs.isEmpty()) {
logs.addAll(buildGenericLogs(now, normalizedQuery, limit));
}
// 限制返回条数
if (logs.size() > limit) {
logs = logs.subList(0, limit);
}
return logs;
}
/**
* 构建系统指标日志(与 CPU、内存、磁盘告警关联)
*/
private List<LogEntry> buildSystemMetricsLogs(Instant now, String query, int limit) {
List<LogEntry> logs = new ArrayList<>();
// CPU 相关日志
if (query.contains("cpu") || query.contains(">80")) {
for (int i = 0; i < 5; i++) {
LogEntry log = new LogEntry();
log.setTimestamp(FORMATTER.format(now.minus(i * 2, ChronoUnit.MINUTES)));
log.setLevel("WARN");
log.setService("payment-service");
log.setInstance("pod-payment-service-7d8f9c6b5-x2k4m");
log.setMessage(String.format("CPU使用率过高: %.1f%%, 进程: java (PID: 1), 线程数: 245", 92.0 - i * 1.5));
log.setMetrics(Map.of(
"cpu_usage", String.format("%.1f", 92.0 - i * 1.5),
"cpu_cores", "4",
"load_average_1m", "3.82",
"load_average_5m", "3.65",
"top_process", "java",
"process_threads", "245"
));
logs.add(log);
}
}
// 内存相关日志
if (query.contains("memory") || query.contains(">85") || query.contains("oom")) {
for (int i = 0; i < 5; i++) {
LogEntry log = new LogEntry();
log.setTimestamp(FORMATTER.format(now.minus(i * 3, ChronoUnit.MINUTES)));
log.setLevel("WARN");
log.setService("order-service");
log.setInstance("pod-order-service-5c7d8e9f1-m3n2p");
log.setMessage(String.format("内存使用率过高: %.1f%%, JVM堆内存: %.1fGB/4GB, GC次数: %d",
91.0 - i * 1.2, 3.8 - i * 0.1, 128 - i * 5));
log.setMetrics(Map.of(
"memory_usage", String.format("%.1f", 91.0 - i * 1.2),
"jvm_heap_used", String.format("%.1fGB", 3.8 - i * 0.1),
"jvm_heap_max", "4GB",
"gc_count", String.valueOf(128 - i * 5),
"gc_time_ms", String.valueOf(1250 + i * 50)
));
logs.add(log);
}
// 添加 GC 警告日志
LogEntry gcLog = new LogEntry();
gcLog.setTimestamp(FORMATTER.format(now.minus(8, ChronoUnit.MINUTES)));
gcLog.setLevel("WARN");
gcLog.setService("order-service");
gcLog.setInstance("pod-order-service-5c7d8e9f1-m3n2p");
gcLog.setMessage("频繁 Full GC 警告: 过去10分钟内发生 15 次 Full GC, 平均耗时 850ms, 建议检查内存泄漏");
gcLog.setMetrics(Map.of(
"full_gc_count", "15",
"avg_gc_time_ms", "850",
"survivor_space", "95%",
"old_gen", "89%"
));
logs.add(gcLog);
}
// 磁盘相关日志
if (query.contains("disk") || query.contains("filesystem")) {
for (int i = 0; i < 3; i++) {
LogEntry log = new LogEntry();
log.setTimestamp(FORMATTER.format(now.minus(i * 5, ChronoUnit.MINUTES)));
log.setLevel("WARN");
log.setService("log-collector");
log.setInstance("node-worker-01");
log.setMessage(String.format("磁盘使用率告警: /data 分区使用率 %.1f%%, 可用空间: %.1fGB",
85.0 + i * 2, 15.0 - i * 2));
log.setMetrics(Map.of(
"disk_usage", String.format("%.1f%%", 85.0 + i * 2),
"disk_available", String.format("%.1fGB", 15.0 - i * 2),
"disk_total", "100GB",
"mount_point", "/data",
"largest_dir", "/data/logs"
));
logs.add(log);
}
}
return logs;
}
/**
* 构建应用日志(与服务不可用、慢响应告警关联)
*/
private List<LogEntry> buildApplicationLogs(Instant now, String query, int limit) {
List<LogEntry> logs = new ArrayList<>();
// ERROR 级别日志
if (query.contains("error") || query.contains("fatal") || query.contains("500")) {
// 数据库连接错误
LogEntry dbError = new LogEntry();
dbError.setTimestamp(FORMATTER.format(now.minus(5, ChronoUnit.MINUTES)));
dbError.setLevel("ERROR");
dbError.setService("order-service");
dbError.setInstance("pod-order-service-5c7d8e9f1-m3n2p");
dbError.setMessage("数据库连接池耗尽: Cannot acquire connection from pool, " +
"active: 50/50, waiting: 23, timeout: 30000ms");
dbError.setMetrics(Map.of(
"error_type", "ConnectionPoolExhaustedException",
"pool_active", "50",
"pool_max", "50",
"waiting_threads", "23"
));
logs.add(dbError);
// OOM 错误
LogEntry oomError = new LogEntry();
oomError.setTimestamp(FORMATTER.format(now.minus(12, ChronoUnit.MINUTES)));
oomError.setLevel("FATAL");
oomError.setService("order-service");
oomError.setInstance("pod-order-service-5c7d8e9f1-m3n2p");
oomError.setMessage("java.lang.OutOfMemoryError: Java heap space at " +
"com.example.order.service.OrderService.processLargeOrder(OrderService.java:156)");
oomError.setMetrics(Map.of(
"error_type", "OutOfMemoryError",
"heap_used", "3.9GB",
"heap_max", "4GB",
"stack_trace", "OrderService.processLargeOrder -> OrderRepository.findByCondition -> HikariPool.getConnection"
));
logs.add(oomError);
// HTTP 500 错误
for (int i = 0; i < 3; i++) {
LogEntry httpError = new LogEntry();
httpError.setTimestamp(FORMATTER.format(now.minus(3 + i, ChronoUnit.MINUTES)));
httpError.setLevel("ERROR");
httpError.setService("user-service");
httpError.setInstance("pod-user-service-8e9f0a1b2-k5j6h");
httpError.setMessage(String.format("HTTP 500 Internal Server Error: /api/v1/users/profile, " +
"耗时: %dms, 错误: Database query timeout", 5200 + i * 300));
httpError.setMetrics(Map.of(
"http_status", "500",
"uri", "/api/v1/users/profile",
"method", "GET",
"duration_ms", String.valueOf(5200 + i * 300),
"error_cause", "QueryTimeoutException"
));
logs.add(httpError);
}
}
// 慢响应相关日志
if (query.contains("response_time") || query.contains("slow") || query.contains(">3000")) {
for (int i = 0; i < 5; i++) {
LogEntry slowLog = new LogEntry();
slowLog.setTimestamp(FORMATTER.format(now.minus(i * 2, ChronoUnit.MINUTES)));
slowLog.setLevel("WARN");
slowLog.setService("user-service");
slowLog.setInstance("pod-user-service-8e9f0a1b2-k5j6h");
slowLog.setMessage(String.format("慢请求警告: %s, 响应时间: %dms, 阈值: 3000ms",
i % 2 == 0 ? "/api/v1/users/profile" : "/api/v1/users/orders",
4200 - i * 150));
slowLog.setMetrics(Map.of(
"uri", i % 2 == 0 ? "/api/v1/users/profile" : "/api/v1/users/orders",
"response_time_ms", String.valueOf(4200 - i * 150),
"threshold_ms", "3000",
"db_time_ms", String.valueOf(3800 - i * 100),
"cache_hit", "false"
));
logs.add(slowLog);
}
}
// 下游服务依赖相关日志
if (query.contains("downstream") || query.contains("redis") ||
query.contains("database") || query.contains("mq")) {
LogEntry redisError = new LogEntry();
redisError.setTimestamp(FORMATTER.format(now.minus(7, ChronoUnit.MINUTES)));
redisError.setLevel("ERROR");
redisError.setService("payment-service");
redisError.setInstance("pod-payment-service-7d8f9c6b5-x2k4m");
redisError.setMessage("Redis 连接超时: 无法连接到 Redis 集群, 节点: redis-cluster-01:6379, 超时: 3000ms");
redisError.setMetrics(Map.of(
"dependency", "redis",
"host", "redis-cluster-01:6379",
"timeout_ms", "3000",
"retry_count", "3"
));
logs.add(redisError);
LogEntry mqError = new LogEntry();
mqError.setTimestamp(FORMATTER.format(now.minus(9, ChronoUnit.MINUTES)));
mqError.setLevel("WARN");
mqError.setService("order-service");
mqError.setInstance("pod-order-service-5c7d8e9f1-m3n2p");
mqError.setMessage("消息队列积压警告: 队列 order-process-queue 积压消息数: 15823, 消费速率下降");
mqError.setMetrics(Map.of(
"dependency", "rabbitmq",
"queue", "order-process-queue",
"pending_messages", "15823",
"consumer_count", "3"
));
logs.add(mqError);
}
return logs;
}
/**
* 构建数据库慢查询日志(与慢响应告警关联)
*/
private List<LogEntry> buildDatabaseSlowQueryLogs(Instant now, String query, int limit) {
List<LogEntry> logs = new ArrayList<>();
// 慢查询日志
LogEntry slowQuery1 = new LogEntry();
slowQuery1.setTimestamp(FORMATTER.format(now.minus(3, ChronoUnit.MINUTES)));
slowQuery1.setLevel("WARN");
slowQuery1.setService("mysql");
slowQuery1.setInstance("mysql-primary-01");
slowQuery1.setMessage("慢查询: SELECT * FROM orders WHERE user_id = ? AND status IN (?, ?, ?) " +
"ORDER BY created_at DESC LIMIT 100, 执行时间: 3.2s, 扫描行数: 1,245,678");
slowQuery1.setMetrics(Map.of(
"query_time_sec", "3.2",
"rows_examined", "1245678",
"rows_returned", "100",
"index_used", "idx_user_id",
"table", "orders",
"query_type", "SELECT"
));
logs.add(slowQuery1);
LogEntry slowQuery2 = new LogEntry();
slowQuery2.setTimestamp(FORMATTER.format(now.minus(6, ChronoUnit.MINUTES)));
slowQuery2.setLevel("WARN");
slowQuery2.setService("mysql");
slowQuery2.setInstance("mysql-primary-01");
slowQuery2.setMessage("慢查询: SELECT u.*, p.* FROM users u LEFT JOIN user_profiles p ON u.id = p.user_id " +
"WHERE u.last_login > ?, 执行时间: 2.8s, 全表扫描");
slowQuery2.setMetrics(Map.of(
"query_time_sec", "2.8",
"rows_examined", "856234",
"rows_returned", "45678",
"index_used", "NONE",
"table", "users, user_profiles",
"query_type", "SELECT",
"warning", "Full table scan detected"
));
logs.add(slowQuery2);
LogEntry slowQuery3 = new LogEntry();
slowQuery3.setTimestamp(FORMATTER.format(now.minus(8, ChronoUnit.MINUTES)));
slowQuery3.setLevel("WARN");
slowQuery3.setService("mysql");
slowQuery3.setInstance("mysql-primary-01");
slowQuery3.setMessage("慢查询: UPDATE orders SET status = ? WHERE created_at < ? AND status = ?, " +
"执行时间: 4.5s, 锁等待时间: 2.1s");
slowQuery3.setMetrics(Map.of(
"query_time_sec", "4.5",
"lock_time_sec", "2.1",
"rows_affected", "23456",
"table", "orders",
"query_type", "UPDATE",
"warning", "High lock contention"
));
logs.add(slowQuery3);
return logs;
}
/**
* 构建系统事件日志(与服务不可用告警关联)
*/
private List<LogEntry> buildSystemEventsLogs(Instant now, String query, int limit) {
List<LogEntry> logs = new ArrayList<>();
// 服务重启事件
if (query.contains("restart") || query.contains("crash") ||
query.contains("oom_kill")) {
LogEntry restartEvent = new LogEntry();
restartEvent.setTimestamp(FORMATTER.format(now.minus(15, ChronoUnit.MINUTES)));
restartEvent.setLevel("WARN");
restartEvent.setService("kubernetes");
restartEvent.setInstance("kube-controller-manager");
restartEvent.setMessage("Pod 重启事件: pod-order-service-5c7d8e9f1-m3n2p, 原因: OOMKilled, " +
"容器退出码: 137, 重启次数: 3");
restartEvent.setMetrics(Map.of(
"event_type", "PodRestart",
"pod", "pod-order-service-5c7d8e9f1-m3n2p",
"reason", "OOMKilled",
"exit_code", "137",
"restart_count", "3",
"namespace", "production"
));
logs.add(restartEvent);
LogEntry oomKillEvent = new LogEntry();
oomKillEvent.setTimestamp(FORMATTER.format(now.minus(16, ChronoUnit.MINUTES)));
oomKillEvent.setLevel("ERROR");
oomKillEvent.setService("kernel");
oomKillEvent.setInstance("node-worker-02");
oomKillEvent.setMessage("OOM Killer 触发: 进程 java (PID: 12345) 被杀死, " +
"内存使用: 3.9GB, 内存限制: 4GB");
oomKillEvent.setMetrics(Map.of(
"event_type", "OOMKill",
"process", "java",
"pid", "12345",
"memory_used", "3.9GB",
"memory_limit", "4GB",
"cgroup", "/kubepods/pod-order-service"
));
logs.add(oomKillEvent);
}
return logs;
}
/**
* 构建通用日志
*/
private List<LogEntry> buildGenericLogs(Instant now, String query, int limit) {
List<LogEntry> logs = new ArrayList<>();
for (int i = 0; i < Math.min(limit, 10); i++) {
LogEntry log = new LogEntry();
log.setTimestamp(FORMATTER.format(now.minus(i, ChronoUnit.MINUTES)));
log.setLevel(i % 3 == 0 ? "ERROR" : (i % 3 == 1 ? "WARN" : "INFO"));
log.setService("generic-service");
log.setInstance("instance-" + i);
log.setMessage("日志消息 #" + i + ", 查询条件: " + query);
log.setMetrics(new HashMap<>());
logs.add(log);
}
return logs;
}
/**
* 构建错误响应
*/
private String buildErrorResponse(String message) {
try {
QueryLogsOutput output = new QueryLogsOutput();
output.setSuccess(false);
output.setMessage(message);
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(output);
} catch (Exception e) {
return String.format("{\"success\":false,\"message\":\"%s\"}", message);
}
}
// ==================== 数据模型 ====================
/**
* 日志条目
*/
@Data
public static class LogEntry {
@JsonProperty("timestamp")
private String timestamp;
@JsonProperty("level")
private String level;
@JsonProperty("service")
private String service;
@JsonProperty("instance")
private String instance;
@JsonProperty("message")
private String message;
@JsonProperty("metrics")
private Map<String, String> metrics;
}
/**
* 日志查询输出
*/
@Data
public static class QueryLogsOutput {
@JsonProperty("success")
private boolean success;
@JsonProperty("region")
private String region;
@JsonProperty("log_topic")
private String logTopic;
@JsonProperty("query")
private String query;
@JsonProperty("logs")
private List<LogEntry> logs;
@JsonProperty("total")
private int total;
@JsonProperty("message")
private String message;
}
/**
* 日志主题信息
*/
@Data
public static class LogTopicInfo {
@JsonProperty("topic_name")
private String topicName;
@JsonProperty("description")
private String description;
@JsonProperty("example_queries")
private List<String> exampleQueries;
@JsonProperty("related_alerts")
private List<String> relatedAlerts;
}
/**
* 日志主题列表输出
*/
@Data
public static class LogTopicsOutput {
@JsonProperty("success")
private boolean success;
@JsonProperty("topics")
private List<LogTopicInfo> topics;
@JsonProperty("available_regions")
private List<String> availableRegions;
@JsonProperty("default_region")
private String defaultRegion;
@JsonProperty("message")
private String message;
}
}
@@ -0,0 +1,303 @@
package org.example.agent.tool;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
/**
* Prometheus 告警查询工具
* 用于查询 Prometheus 的活动告警信息
*/
@Component
public class QueryMetricsTools {
private static final Logger logger = LoggerFactory.getLogger(QueryMetricsTools.class);
/** 工具名常量,用于动态构建提示词 */
public static final String TOOL_QUERY_PROMETHEUS_ALERTS = "queryPrometheusAlerts";
private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${prometheus.base-url}")
private String prometheusBaseUrl;
@Value("${prometheus.timeout:10}")
private int timeout;
@Value("${prometheus.mock-enabled:false}")
private boolean mockEnabled;
private OkHttpClient httpClient;
@jakarta.annotation.PostConstruct
public void init() {
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(Duration.ofSeconds(timeout))
.readTimeout(Duration.ofSeconds(timeout))
.build();
logger.info("✅ QueryMetricsTools 初始化成功, Prometheus URL: {}, Mock模式: {}", prometheusBaseUrl, mockEnabled);
}
/**
* 查询 Prometheus 活动告警
* 该工具从 Prometheus 告警系统检索所有当前活动/触发的告警,包括标签、注释、状态和值
*/
@Tool(description = "Query active alerts from Prometheus alerting system. " +
"This tool retrieves all currently active/firing alerts including their labels, annotations, state, and values. " +
"Use this tool when you need to check what alerts are currently firing, investigate alert conditions, or monitor alert status.")
public String queryPrometheusAlerts() {
logger.info("开始查询 Prometheus 活动告警, Mock模式: {}", mockEnabled);
try {
List<SimplifiedAlert> simplifiedAlerts;
if (mockEnabled) {
// Mock 模式:返回与文档关联的模拟告警数据
simplifiedAlerts = buildMockAlerts();
logger.info("使用 Mock 数据,返回 {} 个模拟告警", simplifiedAlerts.size());
} else {
// 真实模式:调用 Prometheus Alerts API
PrometheusAlertsResult result = fetchPrometheusAlerts();
if (!"success".equals(result.getStatus())) {
return buildErrorResponse("Prometheus API 返回非成功状态: " + result.getStatus(), result.getError());
}
// 转换为简化格式,对于相同的 alertname,只保留第一个
Set<String> seenAlertNames = new HashSet<>();
simplifiedAlerts = new ArrayList<>();
for (PrometheusAlert alert : result.getData().getAlerts()) {
String alertName = alert.getLabels().get("alertname");
// 如果这个 alertname 已经存在,跳过
if (seenAlertNames.contains(alertName)) {
continue;
}
// 标记为已见过
seenAlertNames.add(alertName);
SimplifiedAlert simplified = new SimplifiedAlert();
simplified.setAlertName(alertName);
simplified.setDescription(alert.getAnnotations().getOrDefault("description", ""));
simplified.setState(alert.getState());
simplified.setActiveAt(alert.getActiveAt());
simplified.setDuration(calculateDuration(alert.getActiveAt()));
simplifiedAlerts.add(simplified);
}
}
// 构建成功响应
PrometheusAlertsOutput output = new PrometheusAlertsOutput();
output.setSuccess(true);
output.setAlerts(simplifiedAlerts);
output.setMessage(String.format("成功检索到 %d 个活动告警", simplifiedAlerts.size()));
String jsonResult = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(output);
logger.info("Prometheus 告警查询完成: 找到 {} 个告警", simplifiedAlerts.size());
return jsonResult;
} catch (Exception e) {
logger.error("查询 Prometheus 告警失败", e);
return buildErrorResponse("查询失败", e.getMessage());
}
}
/**
* 构建 Mock 告警数据
* 与 aiops-docs 文档中的告警类型对应:
* - HighCPUUsage: CPU使用率过高
* - HighMemoryUsage: 内存使用率过高
* - HighDiskUsage: 磁盘使用率过高
* - ServiceUnavailable: 服务不可用
* - SlowResponse: 响应时间过长
*/
private List<SimplifiedAlert> buildMockAlerts() {
List<SimplifiedAlert> alerts = new ArrayList<>();
Instant now = Instant.now();
// 告警1: CPU使用率过高 - 持续约25分钟
SimplifiedAlert cpuAlert = new SimplifiedAlert();
cpuAlert.setAlertName("HighCPUUsage");
cpuAlert.setDescription("服务 payment-service 的 CPU 使用率持续超过 80%,当前值为 92%。" +
"实例: pod-payment-service-7d8f9c6b5-x2k4m,命名空间: production");
cpuAlert.setState("firing");
Instant cpuActiveAt = now.minus(25, ChronoUnit.MINUTES);
cpuAlert.setActiveAt(cpuActiveAt.toString());
cpuAlert.setDuration(calculateDuration(cpuActiveAt.toString()));
alerts.add(cpuAlert);
// 告警2: 内存使用率过高 - 持续约15分钟
SimplifiedAlert memoryAlert = new SimplifiedAlert();
memoryAlert.setAlertName("HighMemoryUsage");
memoryAlert.setDescription("服务 order-service 的内存使用率持续超过 85%,当前值为 91%。" +
"JVM堆内存使用: 3.8GB/4GB,可能存在内存泄漏风险。" +
"实例: pod-order-service-5c7d8e9f1-m3n2p,命名空间: production");
memoryAlert.setState("firing");
Instant memoryActiveAt = now.minus(15, ChronoUnit.MINUTES);
memoryAlert.setActiveAt(memoryActiveAt.toString());
memoryAlert.setDuration(calculateDuration(memoryActiveAt.toString()));
alerts.add(memoryAlert);
// 告警3: 响应时间过长 - 持续约10分钟
SimplifiedAlert slowAlert = new SimplifiedAlert();
slowAlert.setAlertName("SlowResponse");
slowAlert.setDescription("服务 user-service 的 P99 响应时间持续超过 3 秒,当前值为 4.2 秒。" +
"受影响接口: /api/v1/users/profile, /api/v1/users/orders。" +
"可能原因:数据库慢查询或下游服务延迟");
slowAlert.setState("firing");
Instant slowActiveAt = now.minus(10, ChronoUnit.MINUTES);
slowAlert.setActiveAt(slowActiveAt.toString());
slowAlert.setDuration(calculateDuration(slowActiveAt.toString()));
alerts.add(slowAlert);
return alerts;
}
/**
* 从 Prometheus API 获取告警数据
*/
private PrometheusAlertsResult fetchPrometheusAlerts() throws Exception {
String apiUrl = prometheusBaseUrl + "/api/v1/alerts";
logger.debug("请求 Prometheus API: {}", apiUrl);
Request request = new Request.Builder()
.url(apiUrl)
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new RuntimeException("HTTP 请求失败: " + response.code());
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, PrometheusAlertsResult.class);
}
}
/**
* 计算从 activeAt 到现在的持续时间
*/
private String calculateDuration(String activeAtStr) {
try {
Instant activeAt = Instant.parse(activeAtStr);
Duration duration = Duration.between(activeAt, Instant.now());
long hours = duration.toHours();
long minutes = duration.toMinutes() % 60;
long seconds = duration.getSeconds() % 60;
if (hours > 0) {
return String.format("%dh%dm%ds", hours, minutes, seconds);
} else if (minutes > 0) {
return String.format("%dm%ds", minutes, seconds);
} else {
return String.format("%ds", seconds);
}
} catch (Exception e) {
logger.warn("解析时间失败: {}", activeAtStr, e);
return "unknown";
}
}
/**
* 构建错误响应
*/
private String buildErrorResponse(String message, String error) {
try {
PrometheusAlertsOutput output = new PrometheusAlertsOutput();
output.setSuccess(false);
output.setMessage(message);
output.setError(error);
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(output);
} catch (Exception e) {
return String.format("{\"success\":false,\"message\":\"%s\",\"error\":\"%s\"}", message, error);
}
}
// ==================== 数据模型 ====================
/**
* Prometheus 告警信息结构
*/
@Data
public static class PrometheusAlert {
private Map<String, String> labels;
private Map<String, String> annotations;
private String state;
private String activeAt;
private String value;
}
/**
* Prometheus 告警查询结果
*/
@Data
public static class PrometheusAlertsResult {
private String status;
private AlertsData data;
private String error;
private String errorType;
}
@Data
public static class AlertsData {
private List<PrometheusAlert> alerts = new ArrayList<>();
}
/**
* 简化的告警信息
*/
@Data
public static class SimplifiedAlert {
@JsonProperty("alert_name")
private String alertName;
@JsonProperty("description")
private String description;
@JsonProperty("state")
private String state;
@JsonProperty("active_at")
private String activeAt;
@JsonProperty("duration")
private String duration;
}
/**
* 告警查询输出
*/
@Data
public static class PrometheusAlertsOutput {
@JsonProperty("success")
private boolean success;
@JsonProperty("alerts")
private List<SimplifiedAlert> alerts;
@JsonProperty("message")
private String message;
@JsonProperty("error")
private String error;
}
}
@@ -0,0 +1,179 @@
package org.example.client;
import io.milvus.client.MilvusServiceClient;
import io.milvus.grpc.DataType;
import io.milvus.param.ConnectParam;
import io.milvus.param.IndexType;
import io.milvus.param.MetricType;
import io.milvus.param.R;
import io.milvus.param.RpcStatus;
import io.milvus.param.collection.*;
import io.milvus.param.index.CreateIndexParam;
import org.example.config.MilvusProperties;
import org.example.constant.MilvusConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* Milvus 客户端工厂类
* 负责创建和初始化 Milvus 客户端连接
*/
@Component
public class MilvusClientFactory {
private static final Logger logger = LoggerFactory.getLogger(MilvusClientFactory.class);
@Autowired
private MilvusProperties milvusProperties;
/**
* 创建并初始化 Milvus 客户端
*
* 简化版本:直接连接并创建 collection
*
* @return MilvusServiceClient 实例
* @throws RuntimeException 如果连接或初始化失败
*/
public MilvusServiceClient createClient() {
MilvusServiceClient client = null;
try {
// 1. 连接到 Milvus
logger.info("正在连接到 Milvus: {}:{}", milvusProperties.getHost(), milvusProperties.getPort());
client = connectToMilvus();
logger.info("成功连接到 Milvus");
// 2. 检查并创建 biz collection(如果不存在)
if (!collectionExists(client, MilvusConstants.MILVUS_COLLECTION_NAME)) {
logger.info("collection '{}' 不存在,正在创建...", MilvusConstants.MILVUS_COLLECTION_NAME);
createBizCollection(client);
logger.info("成功创建 collection '{}'", MilvusConstants.MILVUS_COLLECTION_NAME);
// 创建索引
createIndexes(client);
logger.info("成功创建索引");
} else {
logger.info("collection '{}' 已存在", MilvusConstants.MILVUS_COLLECTION_NAME);
}
return client;
} catch (Exception e) {
logger.error("创建 Milvus 客户端失败", e);
if (client != null) {
client.close();
}
throw new RuntimeException("创建 Milvus 客户端失败: " + e.getMessage(), e);
}
}
/**
* 连接到 Milvus
*/
private MilvusServiceClient connectToMilvus() {
ConnectParam.Builder builder = ConnectParam.newBuilder()
.withHost(milvusProperties.getHost())
.withPort(milvusProperties.getPort())
.withConnectTimeout(milvusProperties.getTimeout(), TimeUnit.MILLISECONDS);
// 如果配置了用户名和密码
if (milvusProperties.getUsername() != null && !milvusProperties.getUsername().isEmpty()) {
builder.withAuthorization(milvusProperties.getUsername(), milvusProperties.getPassword());
}
return new MilvusServiceClient(builder.build());
}
/**
* 检查 collection 是否存在
*/
private boolean collectionExists(MilvusServiceClient client, String collectionName) {
R<Boolean> response = client.hasCollection(HasCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build());
if (response.getStatus() != 0) {
throw new RuntimeException("检查 collection 失败: " + response.getMessage());
}
return response.getData();
}
/**
* 创建 biz collection
*/
private void createBizCollection(MilvusServiceClient client) {
// 定义字段
FieldType idField = FieldType.newBuilder()
.withName("id")
.withDataType(DataType.VarChar)
.withMaxLength(MilvusConstants.ID_MAX_LENGTH)
.withPrimaryKey(true)
.build();
FieldType vectorField = FieldType.newBuilder()
.withName("vector")
.withDataType(DataType.FloatVector) // 改为 FloatVector
.withDimension(MilvusConstants.VECTOR_DIM)
.build();
FieldType contentField = FieldType.newBuilder()
.withName("content")
.withDataType(DataType.VarChar)
.withMaxLength(MilvusConstants.CONTENT_MAX_LENGTH)
.build();
FieldType metadataField = FieldType.newBuilder()
.withName("metadata")
.withDataType(DataType.JSON)
.build();
// 创建 collection schema
CollectionSchemaParam schema = CollectionSchemaParam.newBuilder()
.withEnableDynamicField(false)
.addFieldType(idField)
.addFieldType(vectorField)
.addFieldType(contentField)
.addFieldType(metadataField)
.build();
// 创建 collection
CreateCollectionParam createParam = CreateCollectionParam.newBuilder()
.withCollectionName(MilvusConstants.MILVUS_COLLECTION_NAME)
.withDescription("Business knowledge collection")
.withSchema(schema)
.withShardsNum(MilvusConstants.DEFAULT_SHARD_NUMBER)
.build();
R<RpcStatus> response = client.createCollection(createParam);
if (response.getStatus() != 0) {
throw new RuntimeException("创建 collection 失败: " + response.getMessage());
}
}
/**
* 为 collection 创建索引
*/
private void createIndexes(MilvusServiceClient client) {
// 为 vector 字段创建索引(FloatVector 使用 IVF_FLAT 和 L2 距离)
CreateIndexParam vectorIndexParam = CreateIndexParam.newBuilder()
.withCollectionName(MilvusConstants.MILVUS_COLLECTION_NAME)
.withFieldName("vector")
.withIndexType(IndexType.IVF_FLAT)
.withMetricType(MetricType.L2) // L2 距离(欧氏距离)
.withExtraParam("{\"nlist\":128}")
.withSyncMode(Boolean.FALSE)
.build();
R<RpcStatus> response = client.createIndex(vectorIndexParam);
if (response.getStatus() != 0) {
throw new RuntimeException("创建 vector 索引失败: " + response.getMessage());
}
logger.info("成功为 vector 字段创建索引");
}
}
@@ -0,0 +1,40 @@
package org.example.config;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
import java.time.Duration;
/**
* DashScope API 配置
* 用于配置超时时间等参数
*/
@Configuration
public class DashScopeConfig {
@Value("${spring.ai.dashscope.chat.options.timeout:180000}")
private long timeout;
/**
* 配置 RestClient.Builder,设置超时时间
* Spring AI 会自动使用这个 Bean
*/
@Bean
public RestClient.Builder restClientBuilder() {
// 创建自定义的 OkHttpClient,设置超时时间
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(Duration.ofMillis(timeout))
.readTimeout(Duration.ofMillis(timeout))
.writeTimeout(Duration.ofMillis(timeout))
.callTimeout(Duration.ofMillis(timeout))
.build();
// 创建 RestClient.Builder 并配置 OkHttpClient
return RestClient.builder()
.requestFactory(new OkHttp3ClientHttpRequestFactory(okHttpClient));
}
}
@@ -0,0 +1,32 @@
package org.example.config;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 文档分片配置
*/
@Getter
@Configuration
@ConfigurationProperties(prefix = "document.chunk")
public class DocumentChunkConfig {
/**
* 每个分片的最大字符数
*/
private int maxSize = 800;
/**
* 分片之间的重叠字符数
*/
private int overlap = 100;
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}
public void setOverlap(int overlap) {
this.overlap = overlap;
}
}
@@ -0,0 +1,22 @@
package org.example.config;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Getter
@Configuration
@ConfigurationProperties(prefix = "file.upload")
public class FileUploadConfig {
private String path;
private String allowedExtensions;
public void setPath(String path) {
this.path = path;
}
public void setAllowedExtensions(String allowedExtensions) {
this.allowedExtensions = allowedExtensions;
}
}
@@ -0,0 +1,51 @@
package org.example.config;
import io.milvus.client.MilvusServiceClient;
import org.example.client.MilvusClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import jakarta.annotation.PreDestroy;
/**
* Milvus 配置类
* 负责创建和管理 MilvusServiceClient Bean
*/
@Configuration
public class MilvusConfig {
private static final Logger logger = LoggerFactory.getLogger(MilvusConfig.class);
@Autowired
private MilvusClientFactory milvusClientFactory;
private MilvusServiceClient milvusClient;
/**
* 创建 MilvusServiceClient Bean
*
* @return MilvusServiceClient 实例
*/
@Bean
public MilvusServiceClient milvusServiceClient() {
logger.info("正在初始化 Milvus 客户端...");
milvusClient = milvusClientFactory.createClient();
logger.info("Milvus 客户端初始化完成");
return milvusClient;
}
/**
* 应用关闭时清理资源
*/
@PreDestroy
public void cleanup() {
if (milvusClient != null) {
logger.info("正在关闭 Milvus 客户端连接...");
milvusClient.close();
logger.info("Milvus 客户端连接已关闭");
}
}
}
@@ -0,0 +1,68 @@
package org.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "milvus")
public class MilvusProperties {
private String host = "localhost";
private Integer port = 19530;
private String username = "";
private String password = "";
private String database = "default";
private Long timeout = 10000L;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getDatabase() {
return database;
}
public void setDatabase(String database) {
this.database = database;
}
public Long getTimeout() {
return timeout;
}
public void setTimeout(Long timeout) {
this.timeout = timeout;
}
public String getAddress() {
return host + ":" + port;
}
}
@@ -0,0 +1,38 @@
package org.example.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* Web MVC 配置
* 解决中文乱码问题
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 添加 UTF-8 字符串转换器
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(StandardCharsets.UTF_8);
stringConverter.setWriteAcceptCharset(false); // 不设置 Accept-Charset
converters.add(0, stringConverter);
// 添加 Jackson JSON 转换器,确保 UTF-8 编码
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
jsonConverter.setDefaultCharset(StandardCharsets.UTF_8);
converters.add(1, jsonConverter);
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
@@ -0,0 +1,30 @@
package org.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web MVC 配置
* 配置跨域和静态资源
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.maxAge(3600);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 配置静态资源映射
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/");
}
}
@@ -0,0 +1,38 @@
package org.example.constant;
public class MilvusConstants {
/**
* Milvus 数据库名称
*/
public static final String MILVUS_DB_NAME = "default";
/**
* Milvus 集合名称
*/
public static final String MILVUS_COLLECTION_NAME = "biz";
/**
* 向量维度(豆包 embedding 模型的维度)
*/
public static final int VECTOR_DIM = 1024; // 豆包模型返回1024维向量
/**
* ID字段最大长度
*/
public static final int ID_MAX_LENGTH = 256;
/**
* Content字段最大长度
*/
public static final int CONTENT_MAX_LENGTH = 8192;
/**
* 默认分片数
*/
public static final int DEFAULT_SHARD_NUMBER = 2;
private MilvusConstants() {
// 工具类,禁止实例化
}
}
@@ -0,0 +1,629 @@
package org.example.controller;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import com.alibaba.cloud.ai.graph.NodeOutput;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.streaming.OutputType;
import com.alibaba.cloud.ai.graph.streaming.StreamingOutput;
import lombok.Getter;
import lombok.Setter;
import org.example.service.AiOpsService;
import org.example.service.ChatService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
/**
* 统一 API 控制器
* 适配前端接口需求
*/
@RestController
@RequestMapping("/api")
public class ChatController {
private static final Logger logger = LoggerFactory.getLogger(ChatController.class);
@Autowired
private AiOpsService aiOpsService;
@Autowired
private ChatService chatService;
@Autowired
private ToolCallbackProvider tools;
private final ExecutorService executor = Executors.newCachedThreadPool();
// 存储会话信息
private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<>();
// 最大历史消息窗口大小(成对计算:用户消息+AI回复=1对)
private static final int MAX_WINDOW_SIZE = 6;
/**
* 普通对话接口(支持工具调用)
* 与 /chat_react 逻辑一致,但直接返回完整结果而非流式输出
*/
@PostMapping("/chat")
public ResponseEntity<ApiResponse<ChatResponse>> chat(@RequestBody ChatRequest request) {
try {
logger.info("收到对话请求 - SessionId: {}, Question: {}", request.getId(), request.getQuestion());
// 参数校验
if (request.getQuestion() == null || request.getQuestion().trim().isEmpty()) {
logger.warn("问题内容为空");
return ResponseEntity.ok(ApiResponse.success(ChatResponse.error("问题内容不能为空")));
}
// 获取或创建会话
SessionInfo session = getOrCreateSession(request.getId());
// 获取历史消息
List<Map<String, String>> history = session.getHistory();
logger.info("会话历史消息对数: {}", history.size() / 2);
// 创建 DashScope API 和 ChatModel
DashScopeApi dashScopeApi = chatService.createDashScopeApi();
DashScopeChatModel chatModel = chatService.createStandardChatModel(dashScopeApi);
// 记录可用工具
chatService.logAvailableTools();
logger.info("开始 ReactAgent 对话(支持自动工具调用)");
// 构建系统提示词(包含历史消息)
String systemPrompt = chatService.buildSystemPrompt(history);
// 创建 ReactAgent
ReactAgent agent = chatService.createReactAgent(chatModel, systemPrompt);
// 执行对话
String fullAnswer = chatService.executeChat(agent, request.getQuestion());
// 更新会话历史
session.addMessage(request.getQuestion(), fullAnswer);
logger.info("已更新会话历史 - SessionId: {}, 当前消息对数: {}",
request.getId(), session.getMessagePairCount());
return ResponseEntity.ok(ApiResponse.success(ChatResponse.success(fullAnswer)));
} catch (Exception e) {
logger.error("对话失败", e);
return ResponseEntity.ok(ApiResponse.success(ChatResponse.error(e.getMessage())));
}
}
/**
* 清空会话历史
*/
@PostMapping("/chat/clear")
public ResponseEntity<ApiResponse<String>> clearChatHistory(@RequestBody ClearRequest request) {
try {
logger.info("收到清空会话历史请求 - SessionId: {}", request.getId());
if (request.getId() == null || request.getId().isEmpty()) {
return ResponseEntity.ok(ApiResponse.error("会话ID不能为空"));
}
SessionInfo session = sessions.get(request.getId());
if (session != null) {
session.clearHistory();
return ResponseEntity.ok(ApiResponse.success("会话历史已清空"));
} else {
return ResponseEntity.ok(ApiResponse.error("会话不存在"));
}
} catch (Exception e) {
logger.error("清空会话历史失败", e);
return ResponseEntity.ok(ApiResponse.error(e.getMessage()));
}
}
/**
* ReactAgent 对话接口(SSE 流式模式,支持多轮对话,支持自动工具调用,例如获取当前时间,查询日志,告警等)
* 支持 session 管理,保留对话历史
*/
@PostMapping(value = "/chat_stream", produces = "text/event-stream;charset=UTF-8")
public SseEmitter chatStream(@RequestBody ChatRequest request) {
SseEmitter emitter = new SseEmitter(300000L); // 5分钟超时
// 参数校验
if (request.getQuestion() == null || request.getQuestion().trim().isEmpty()) {
logger.warn("问题内容为空");
try {
emitter.send(SseEmitter.event().name("message").data(SseMessage.error("问题内容不能为空"), MediaType.APPLICATION_JSON));
emitter.complete();
} catch (IOException e) {
emitter.completeWithError(e);
}
return emitter;
}
executor.execute(() -> {
try {
logger.info("收到 ReactAgent 对话请求 - SessionId: {}, Question: {}", request.getId(), request.getQuestion());
// 获取或创建会话
SessionInfo session = getOrCreateSession(request.getId());
// 获取历史消息
List<Map<String, String>> history = session.getHistory();
logger.info("ReactAgent 会话历史消息对数: {}", history.size() / 2);
// 创建 DashScope API 和 ChatModel
DashScopeApi dashScopeApi = chatService.createDashScopeApi();
DashScopeChatModel chatModel = chatService.createStandardChatModel(dashScopeApi);
// 记录可用工具
chatService.logAvailableTools();
logger.info("开始 ReactAgent 流式对话(支持自动工具调用)");
// 构建系统提示词(包含历史消息)
String systemPrompt = chatService.buildSystemPrompt(history);
// 创建 ReactAgent
ReactAgent agent = chatService.createReactAgent(chatModel, systemPrompt);
// 用于累积完整答案
StringBuilder fullAnswerBuilder = new StringBuilder();
// 使用 agent.stream() 进行流式对话
Flux<NodeOutput> stream = agent.stream(request.getQuestion());
stream.subscribe(
output -> {
try {
// 检查是否为 StreamingOutput 类型
if (output instanceof StreamingOutput streamingOutput) {
OutputType type = streamingOutput.getOutputType();
// 处理模型推理的流式输出
if (type == OutputType.AGENT_MODEL_STREAMING) {
// 流式增量内容,逐步显示
String chunk = streamingOutput.message().getText();
if (chunk != null && !chunk.isEmpty()) {
fullAnswerBuilder.append(chunk);
// 实时发送到前端
emitter.send(SseEmitter.event()
.name("message")
.data(SseMessage.content(chunk), MediaType.APPLICATION_JSON));
logger.info("发送流式内容: {}", chunk);
}
} else if (type == OutputType.AGENT_MODEL_FINISHED) {
// 模型推理完成
logger.info("模型输出完成");
} else if (type == OutputType.AGENT_TOOL_FINISHED) {
// 工具调用完成
logger.info("工具调用完成: {}", output.node());
} else if (type == OutputType.AGENT_HOOK_FINISHED) {
// Hook 执行完成
logger.debug("Hook 执行完成: {}", output.node());
}
}
} catch (IOException e) {
logger.error("发送流式消息失败", e);
throw new RuntimeException(e);
}
},
error -> {
// 错误处理
logger.error("ReactAgent 流式对话失败", error);
try {
emitter.send(SseEmitter.event()
.name("message")
.data(SseMessage.error(error.getMessage()), MediaType.APPLICATION_JSON));
} catch (IOException ex) {
logger.error("发送错误消息失败", ex);
}
emitter.completeWithError(error);
},
() -> {
// 完成处理
try {
String fullAnswer = fullAnswerBuilder.toString();
logger.info("ReactAgent 流式对话完成 - SessionId: {}, 答案长度: {}",
request.getId(), fullAnswer.length());
// 更新会话历史
session.addMessage(request.getQuestion(), fullAnswer);
logger.info("已更新会话历史 - SessionId: {}, 当前消息对数: {}",
request.getId(), session.getMessagePairCount());
// 发送完成标记
emitter.send(SseEmitter.event()
.name("message")
.data(SseMessage.done(), MediaType.APPLICATION_JSON));
emitter.complete();
} catch (IOException e) {
logger.error("发送完成消息失败", e);
emitter.completeWithError(e);
}
}
);
} catch (Exception e) {
logger.error("ReactAgent 对话初始化失败", e);
try {
emitter.send(SseEmitter.event()
.name("message")
.data(SseMessage.error(e.getMessage()), MediaType.APPLICATION_JSON));
} catch (IOException ex) {
logger.error("发送错误消息失败", ex);
}
emitter.completeWithError(e);
}
});
return emitter;
}
/**
* AI 智能运维接口(SSE 流式模式)- 自动分析告警并生成运维报告
* 无需用户输入,自动执行告警分析流程
*/
@PostMapping(value = "/ai_ops", produces = "text/event-stream;charset=UTF-8")
public SseEmitter aiOps() {
SseEmitter emitter = new SseEmitter(600000L); // 10分钟超时(告警分析可能较慢)
executor.execute(() -> {
try {
logger.info("收到 AI 智能运维请求 - 启动多 Agent 协作流程");
DashScopeApi dashScopeApi = chatService.createDashScopeApi();
DashScopeChatModel chatModel = DashScopeChatModel.builder()
.dashScopeApi(dashScopeApi)
.defaultOptions(DashScopeChatOptions.builder()
.withModel(DashScopeChatModel.DEFAULT_MODEL_NAME)
.withTemperature(0.3)
.withMaxToken(8000)
.withTopP(0.9)
.build())
.build();
ToolCallback[] toolCallbacks = tools.getToolCallbacks();
emitter.send(SseEmitter.event().name("message").data(SseMessage.content("正在读取告警并拆解任务...\n")));
// 调用 AiOpsService 执行分析流程
Optional<OverAllState> overAllStateOptional = aiOpsService.executeAiOpsAnalysis(chatModel, toolCallbacks);
if (overAllStateOptional.isEmpty()) {
emitter.send(SseEmitter.event().name("message")
.data(SseMessage.error("多 Agent 编排未获取到有效结果"), MediaType.APPLICATION_JSON));
emitter.complete();
return;
}
OverAllState state = overAllStateOptional.get();
logger.info("AI Ops 编排完成,开始提取最终报告...");
// 提取最终报告
Optional<String> finalReportOptional = aiOpsService.extractFinalReport(state);
// 输出最终报告
if (finalReportOptional.isPresent()) {
String finalReportText = finalReportOptional.get();
logger.info("提取到 Planner 最终报告,长度: {}", finalReportText.length());
// 发送分隔线
emitter.send(SseEmitter.event().name("message")
.data(SseMessage.content("\n\n" + "=".repeat(60) + "\n"), MediaType.APPLICATION_JSON));
// 发送完整的告警分析报告
emitter.send(SseEmitter.event().name("message")
.data(SseMessage.content("📋 **告警分析报告**\n\n"), MediaType.APPLICATION_JSON));
int chunkSize = 50;
for (int i = 0; i < finalReportText.length(); i += chunkSize) {
int end = Math.min(i + chunkSize, finalReportText.length());
String chunk = finalReportText.substring(i, end);
emitter.send(SseEmitter.event().name("message")
.data(SseMessage.content(chunk), MediaType.APPLICATION_JSON));
}
// 发送结束分隔线
emitter.send(SseEmitter.event().name("message")
.data(SseMessage.content("\n" + "=".repeat(60) + "\n\n"), MediaType.APPLICATION_JSON));
logger.info("最终报告已完整输出");
} else {
logger.warn("未能提取到 Planner 最终报告");
emitter.send(SseEmitter.event().name("message")
.data(SseMessage.content("⚠️ 多 Agent 流程已完成,但未能生成最终报告。"), MediaType.APPLICATION_JSON));
}
emitter.send(SseEmitter.event().name("message").data(SseMessage.done(), MediaType.APPLICATION_JSON));
emitter.complete();
logger.info("AI Ops 多 Agent 编排完成");
} catch (Exception e) {
logger.error("AI Ops 多 Agent 协作失败", e);
try {
emitter.send(SseEmitter.event().name("message")
.data(SseMessage.error("AI Ops 流程失败: " + e.getMessage()), MediaType.APPLICATION_JSON));
} catch (IOException ex) {
logger.error("发送错误消息失败", ex);
}
emitter.completeWithError(e);
}
});
return emitter;
}
/**
* 获取会话信息
*/
@GetMapping("/chat/session/{sessionId}")
public ResponseEntity<ApiResponse<SessionInfoResponse>> getSessionInfo(@PathVariable String sessionId) {
try {
logger.info("收到获取会话信息请求 - SessionId: {}", sessionId);
SessionInfo session = sessions.get(sessionId);
if (session != null) {
SessionInfoResponse response = new SessionInfoResponse();
response.setSessionId(sessionId);
response.setMessagePairCount(session.getMessagePairCount());
response.setCreateTime(session.createTime);
return ResponseEntity.ok(ApiResponse.success(response));
} else {
return ResponseEntity.ok(ApiResponse.error("会话不存在"));
}
} catch (Exception e) {
logger.error("获取会话信息失败", e);
return ResponseEntity.ok(ApiResponse.error(e.getMessage()));
}
}
// ==================== 辅助方法 ====================
private SessionInfo getOrCreateSession(String sessionId) {
if (sessionId == null || sessionId.isEmpty()) {
sessionId = UUID.randomUUID().toString();
}
return sessions.computeIfAbsent(sessionId, SessionInfo::new);
}
// ==================== 内部类 ====================
/**
* 会话信息
* 管理单个会话的历史消息,支持自动清理和线程安全
*/
private static class SessionInfo {
private final String sessionId;
// 存储历史消息对:[{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
private final List<Map<String, String>> messageHistory;
private final long createTime;
private final ReentrantLock lock;
public SessionInfo(String sessionId) {
this.sessionId = sessionId;
this.messageHistory = new ArrayList<>();
this.createTime = System.currentTimeMillis();
this.lock = new ReentrantLock();
}
/**
* 添加一对消息(用户问题 + AI回复)
* 自动管理历史消息窗口大小
*/
public void addMessage(String userQuestion, String aiAnswer) {
lock.lock();
try {
// 添加用户消息
Map<String, String> userMsg = new HashMap<>();
userMsg.put("role", "user");
userMsg.put("content", userQuestion);
messageHistory.add(userMsg);
// 添加AI回复
Map<String, String> assistantMsg = new HashMap<>();
assistantMsg.put("role", "assistant");
assistantMsg.put("content", aiAnswer);
messageHistory.add(assistantMsg);
// 自动清理:保持最多 MAX_WINDOW_SIZE 对消息
// 每对消息包含2条记录(user + assistant)
int maxMessages = MAX_WINDOW_SIZE * 2;
while (messageHistory.size() > maxMessages) {
// 成对删除最旧的消息(删除前2条)
messageHistory.remove(0); // 删除最旧的用户消息
if (!messageHistory.isEmpty()) {
messageHistory.remove(0); // 删除对应的AI回复
}
}
logger.debug("会话 {} 更新历史消息,当前消息对数: {}",
sessionId, messageHistory.size() / 2);
} finally {
lock.unlock();
}
}
/**
* 获取历史消息(线程安全)
* 返回副本以避免并发修改
*/
public List<Map<String, String>> getHistory() {
lock.lock();
try {
return new ArrayList<>(messageHistory);
} finally {
lock.unlock();
}
}
/**
* 清空历史消息
*/
public void clearHistory() {
lock.lock();
try {
messageHistory.clear();
logger.info("会话 {} 历史消息已清空", sessionId);
} finally {
lock.unlock();
}
}
/**
* 获取当前消息对数
*/
public int getMessagePairCount() {
lock.lock();
try {
return messageHistory.size() / 2;
} finally {
lock.unlock();
}
}
}
/**
* 聊天请求
*/
@Setter
@Getter
public static class ChatRequest {
@com.fasterxml.jackson.annotation.JsonProperty(value = "Id")
@com.fasterxml.jackson.annotation.JsonAlias({"id", "ID"})
private String Id;
@com.fasterxml.jackson.annotation.JsonProperty(value = "Question")
@com.fasterxml.jackson.annotation.JsonAlias({"question", "QUESTION"})
private String Question;
}
/**
* 清空会话请求
*/
@Setter
@Getter
public static class ClearRequest {
@com.fasterxml.jackson.annotation.JsonProperty(value = "Id")
@com.fasterxml.jackson.annotation.JsonAlias({"id", "ID"})
private String Id;
}
// ==================== 内部类 ====================
/**
* 会话信息响应
*/
@Setter
@Getter
public static class SessionInfoResponse {
private String sessionId;
private int messagePairCount;
private long createTime;
}
/**
* 统一聊天响应格式
* 适用于所有普通返回模式的对话接口
*/
@Setter
@Getter
public static class ChatResponse {
private boolean success;
private String answer;
private String errorMessage;
public static ChatResponse success(String answer) {
ChatResponse response = new ChatResponse();
response.setSuccess(true);
response.setAnswer(answer);
return response;
}
public static ChatResponse error(String errorMessage) {
ChatResponse response = new ChatResponse();
response.setSuccess(false);
response.setErrorMessage(errorMessage);
return response;
}
}
/**
* 统一 SSE 流式消息格式
* 适用于所有 SSE 流式返回模式的对话接口
*/
@Setter
@Getter
public static class SseMessage {
private String type; // content: 内容块, error: 错误, done: 完成
private String data;
public static SseMessage content(String data) {
SseMessage message = new SseMessage();
message.setType("content");
message.setData(data);
return message;
}
public static SseMessage error(String errorMessage) {
SseMessage message = new SseMessage();
message.setType("error");
message.setData(errorMessage);
return message;
}
public static SseMessage done() {
SseMessage message = new SseMessage();
message.setType("done");
message.setData(null);
return message;
}
}
@Getter
@Setter
public static class ApiResponse<T> {
private int code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(200);
response.setMessage("success");
response.setData(data);
return response;
}
public static <T> ApiResponse<T> error(String message) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(500);
response.setMessage(message);
return response;
}
}
}
@@ -0,0 +1,154 @@
package org.example.controller;
import org.example.config.FileUploadConfig;
import org.example.dto.FileUploadRes;
import org.example.service.VectorIndexService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
@RestController
public class FileUploadController {
private static final Logger logger = LoggerFactory.getLogger(FileUploadController.class);
@Autowired
private FileUploadConfig fileUploadConfig;
@Autowired
private VectorIndexService vectorIndexService;
@PostMapping(value = "/api/upload", consumes = "multipart/form-data")
public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("文件不能为空");
}
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || originalFilename.isEmpty()) {
return ResponseEntity.badRequest().body("文件名不能为空");
}
String fileExtension = getFileExtension(originalFilename);
if (!isAllowedExtension(fileExtension)) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("不支持的文件格式,仅支持: " + fileUploadConfig.getAllowedExtensions());
}
try {
String uploadPath = fileUploadConfig.getPath();
Path uploadDir = Paths.get(uploadPath).normalize();
if (!Files.exists(uploadDir)) {
Files.createDirectories(uploadDir);
}
// 使用原始文件名,而不是UUID,以便实现基于文件名的去重
Path filePath = uploadDir.resolve(originalFilename).normalize();
// 如果文件已存在,先删除旧文件(实现覆盖更新)
if (Files.exists(filePath)) {
logger.info("文件已存在,将覆盖: {}", filePath);
Files.delete(filePath);
}
Files.copy(file.getInputStream(), filePath);
logger.info("文件上传成功: {}", filePath);
// 文件上传成功后,自动调用向量索引服务
try {
logger.info("开始为上传文件创建向量索引: {}", filePath);
vectorIndexService.indexSingleFile(filePath.toString());
logger.info("向量索引创建成功: {}", filePath);
} catch (Exception e) {
logger.error("向量索引创建失败: {}, 错误: {}", filePath, e.getMessage(), e);
// 注意:即使索引失败,文件上传仍然成功,只是记录错误日志
// 可以根据业务需求决定是否要删除文件或返回错误
}
FileUploadRes response = new FileUploadRes(
originalFilename,
filePath.toString(),
file.getSize()
);
// 使用统一的API响应格式
ApiResponse<FileUploadRes> apiResponse = new ApiResponse<>();
apiResponse.setCode(200);
apiResponse.setMessage("success");
apiResponse.setData(response);
return ResponseEntity.ok(apiResponse);
} catch (IOException e) {
ApiResponse<String> errorResponse = new ApiResponse<>();
errorResponse.setCode(500);
errorResponse.setMessage("文件上传失败: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorResponse);
}
}
/**
* 统一 API 响应格式
*/
public static class ApiResponse<T> {
private int code;
private String message;
private T data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
private String getFileExtension(String filename) {
int lastIndexOf = filename.lastIndexOf(".");
if (lastIndexOf == -1) {
return "";
}
return filename.substring(lastIndexOf + 1).toLowerCase();
}
private boolean isAllowedExtension(String extension) {
String allowedExtensions = fileUploadConfig.getAllowedExtensions();
if (allowedExtensions == null || allowedExtensions.isEmpty()) {
return false;
}
List<String> allowedList = Arrays.asList(allowedExtensions.split(","));
return allowedList.contains(extension.toLowerCase());
}
}
@@ -0,0 +1,50 @@
package org.example.controller;
import io.milvus.client.MilvusServiceClient;
import io.milvus.grpc.ShowCollectionsResponse;
import io.milvus.param.R;
import io.milvus.param.collection.ShowCollectionsParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* Milvus 测试控制器
* 用于测试数据库连接和数据读取
*/
@RestController
@RequestMapping("/milvus")
public class MilvusCheckController {
@Autowired
private MilvusServiceClient milvusClient;
/**
* 简单的健康检查
*/
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> simpleHealth() {
Map<String, Object> result = new HashMap<>();
try {
R<ShowCollectionsResponse> response = milvusClient.showCollections(
ShowCollectionsParam.newBuilder().build()
);
if (response.getStatus() == 0) {
result.put("message", "ok");
result.put("collections", response.getData().getCollectionNamesList());
return ResponseEntity.ok(result);
} else {
result.put("message", response.getMessage());
return ResponseEntity.status(503).body(result);
}
} catch (Exception e) {
result.put("error", e.getMessage());
return ResponseEntity.status(503).body(result);
}
}
}
@@ -0,0 +1,15 @@
package org.example.dto;
import lombok.Data;
/**
* AIOps 请求 DTO
*/
@Data
public class AIOpsRequest {
/**
* 用户请求描述
*/
private String userRequest;
}
@@ -0,0 +1,59 @@
package org.example.dto;
import lombok.Getter;
import lombok.Setter;
/**
* 文档分片
*/
@Setter
@Getter
public class DocumentChunk {
// Getters and Setters
/**
* 分片内容
*/
private String content;
/**
* 分片在原文档中的起始位置
*/
private int startIndex;
/**
* 分片在原文档中的结束位置
*/
private int endIndex;
/**
* 分片序号(从0开始)
*/
private int chunkIndex;
/**
* 分片标题或上下文信息
*/
private String title;
public DocumentChunk() {
}
public DocumentChunk(String content, int startIndex, int endIndex, int chunkIndex) {
this.content = content;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.chunkIndex = chunkIndex;
}
@Override
public String toString() {
return "DocumentChunk{" +
"chunkIndex=" + chunkIndex +
", title='" + title + '\'' +
", contentLength=" + (content != null ? content.length() : 0) +
", startIndex=" + startIndex +
", endIndex=" + endIndex +
'}';
}
}
@@ -0,0 +1,23 @@
package org.example.dto;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class FileUploadRes {
private String fileName;
private String filePath;
private Long fileSize;
public FileUploadRes() {
}
public FileUploadRes(String fileName, String filePath, Long fileSize) {
this.fileName = fileName;
this.filePath = filePath;
this.fileSize = fileSize;
}
}
@@ -0,0 +1,278 @@
package org.example.service;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.agent.flow.agent.SupervisorAgent;
import com.alibaba.cloud.ai.graph.exception.GraphRunnerException;
import org.example.agent.tool.DateTimeTools;
import org.example.agent.tool.InternalDocsTools;
import org.example.agent.tool.QueryLogsTools;
import org.example.agent.tool.QueryMetricsTools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* AI Ops 智能运维服务
* 负责多 Agent 协作的告警分析流程
*/
@Service
public class AiOpsService {
private static final Logger logger = LoggerFactory.getLogger(AiOpsService.class);
@Autowired
private DateTimeTools dateTimeTools;
@Autowired
private InternalDocsTools internalDocsTools;
@Autowired
private QueryMetricsTools queryMetricsTools;
@Autowired(required = false) // Mock 模式下才注册
private QueryLogsTools queryLogsTools;
/**
* 执行 AI Ops 告警分析流程
*
* @param chatModel 大模型实例
* @param toolCallbacks 工具回调数组
* @return 分析结果状态
* @throws GraphRunnerException 如果 Agent 执行失败
*/
public Optional<OverAllState> executeAiOpsAnalysis(DashScopeChatModel chatModel, ToolCallback[] toolCallbacks) throws GraphRunnerException {
logger.info("开始执行 AI Ops 多 Agent 协作流程");
// 构建 Planner 和 Executor Agent
ReactAgent plannerAgent = buildPlannerAgent(chatModel, toolCallbacks);
ReactAgent executorAgent = buildExecutorAgent(chatModel, toolCallbacks);
// 构建 Supervisor Agent
SupervisorAgent supervisorAgent = SupervisorAgent.builder()
.name("ai_ops_supervisor")
.description("负责调度 Planner 与 Executor 的多 Agent 控制器")
.model(chatModel)
.systemPrompt(buildSupervisorSystemPrompt())
.subAgents(List.of(plannerAgent, executorAgent))
.build();
String taskPrompt = "你是企业级 SRE,接到了自动化告警排查任务。请结合工具调用,执行**规划→执行→再规划**的闭环,并最终按照固定模板输出《告警分析报告》。禁止编造虚假数据,如连续多次查询失败需诚实反馈无法完成的原因。";
logger.info("调用 Supervisor Agent 开始编排...");
return supervisorAgent.invoke(taskPrompt);
}
/**
* 从执行结果中提取最终报告文本
*
* @param state 执行状态
* @return 报告文本(如果存在)
*/
public Optional<String> extractFinalReport(OverAllState state) {
logger.info("开始提取最终报告...");
// 提取 Planner 最终输出(包含完整的告警分析报告)
Optional<AssistantMessage> plannerFinalOutput = state.value("planner_plan")
.filter(AssistantMessage.class::isInstance)
.map(AssistantMessage.class::cast);
if (plannerFinalOutput.isPresent()) {
String reportText = plannerFinalOutput.get().getText();
logger.info("成功提取到 Planner 最终报告,长度: {}", reportText.length());
return Optional.of(reportText);
} else {
logger.warn("未能提取到 Planner 最终报告");
return Optional.empty();
}
}
/**
* 构建 Planner Agent
*/
private ReactAgent buildPlannerAgent(DashScopeChatModel chatModel, ToolCallback[] toolCallbacks) {
return ReactAgent.builder()
.name("planner_agent")
.description("负责拆解告警、规划与再规划步骤")
.model(chatModel)
.systemPrompt(buildPlannerPrompt())
.methodTools(buildMethodToolsArray())
.tools(toolCallbacks)
.outputKey("planner_plan")
.build();
}
/**
* 构建 Executor Agent
*/
private ReactAgent buildExecutorAgent(DashScopeChatModel chatModel, ToolCallback[] toolCallbacks) {
return ReactAgent.builder()
.name("executor_agent")
.description("负责执行 Planner 的首个步骤并及时反馈")
.model(chatModel)
.systemPrompt(buildExecutorPrompt())
.methodTools(buildMethodToolsArray())
.tools(toolCallbacks)
.outputKey("executor_feedback")
.build();
}
/**
* 动态构建方法工具数组
* 根据 cls.mock-enabled 决定是否包含 QueryLogsTools
*/
private Object[] buildMethodToolsArray() {
if (queryLogsTools != null) {
// Mock 模式:包含 QueryLogsTools
return new Object[]{dateTimeTools, internalDocsTools, queryMetricsTools, queryLogsTools};
} else {
// 真实模式:不包含 QueryLogsTools(由 MCP 提供日志查询功能)
return new Object[]{dateTimeTools, internalDocsTools, queryMetricsTools};
}
}
/**
* 构建 Planner Agent 系统提示词
*/
private String buildPlannerPrompt() {
return """
你是 Planner Agent,同时承担 Replanner 角色,负责:
1. 读取当前输入任务 {input} 以及 Executor 的最近反馈 {executor_feedback}。
2. 分析 Prometheus 告警、日志、内部文档等信息,制定可执行的下一步步骤。
3. 在执行阶段,输出 JSON,包含 decision (PLAN|EXECUTE|FINISH)、step 描述、预期要调用的工具、以及必要的上下文。
4. 调用任何腾讯云日志/主题相关工具时,region 参数必须使用连字符格式(如 ap-guangzhou),若不确定请省略以使用默认值。
5. 严格禁止编造数据,只能引用工具返回的真实内容;如果连续 3 次调用同一工具仍失败或返回空结果,需停止该方向并在最终报告的结论部分说明"无法完成"的原因。
## 最终报告输出要求(CRITICAL)
当 decision=FINISH 时,你必须:
1. **不要输出 JSON 格式**
2. **直接输出完整的 Markdown 格式报告文本**
3. **报告必须严格遵循以下模板**:
```
# 告警分析报告
---
## 📋 活跃告警清单
| 告警名称 | 级别 | 目标服务 | 首次触发时间 | 最新触发时间 | 状态 |
|---------|------|----------|-------------|-------------|------|
| [告警1名称] | [级别] | [服务名] | [时间] | [时间] | 活跃 |
| [告警2名称] | [级别] | [服务名] | [时间] | [时间] | 活跃 |
---
## 🔍 告警根因分析1 - [告警名称]
### 告警详情
- **告警级别**: [级别]
- **受影响服务**: [服务名]
- **持续时间**: [X分钟]
### 症状描述
[根据监控指标描述症状]
### 日志证据
[引用查询到的关键日志]
### 根因结论
[基于证据得出的根本原因]
---
## 🛠️ 处理方案执行1 - [告警名称]
### 已执行的排查步骤
1. [步骤1]
2. [步骤2]
### 处理建议
[给出具体的处理建议]
### 预期效果
[说明预期的效果]
---
## 🔍 告警根因分析2 - [告警名称]
[如果有第2个告警,重复上述格式]
---
## 📊 结论
### 整体评估
[总结所有告警的整体情况]
### 关键发现
- [发现1]
- [发现2]
### 后续建议
1. [建议1]
2. [建议2]
### 风险评估
[评估当前风险等级和影响范围]
```
**重要提醒**:
- 最终输出必须是纯 Markdown 文本,不要包含 JSON 结构
- 不要使用 "finalReport": "..." 这样的格式
- 直接从 "# 告警分析报告" 开始输出
- 所有内容必须基于工具查询的真实数据,严禁编造
- 如果某个步骤失败,在结论中如实说明,不要跳过
""";
}
/**
* 构建 Executor Agent 系统提示词
*/
private String buildExecutorPrompt() {
return """
你是 Executor Agent,负责读取 Planner 最新输出 {planner_plan},只执行其中的第一步。
- 确认步骤所需的工具与参数,尤其是 region 参数要使用连字符格式(ap-guangzhou);若 Planner 未给出则使用默认区域。
- 调用相应的工具并收集结果,如工具返回错误或空数据,需要将失败原因、请求参数一并记录,并停止进一步调用该工具(同一工具失败达到 3 次时应直接返回 FAILED)。
- 将日志、指标、文档等证据整理成结构化摘要,标注对应的告警名称或资源,方便 Planner 填充"告警根因分析 / 处理方案执行"章节。
- 以 JSON 形式返回执行状态、证据以及给 Planner 的建议,写入 executor_feedback,严禁编造未实际查询到的内容。
输出示例:
{
"status": "SUCCESS",
"summary": "近1小时未见 error 日志,仅有 info",
"evidence": "...",
"nextHint": "建议转向高占用进程"
}
""";
}
/**
* 构建 Supervisor Agent 系统提示词
*/
private String buildSupervisorSystemPrompt() {
return """
你是 AI Ops Supervisor,负责调度 planner_agent 与 executor_agent:
1. 当需要拆解任务或重新制定策略时,调用 planner_agent。
2. 当 planner_agent 输出 decision=EXECUTE 时,调用 executor_agent 执行第一步。
3. 根据 executor_agent 的反馈,评估是否需要再次调用 planner_agent,直到 decision=FINISH。
4. FINISH 后,确保向最终用户输出完整的《告警分析报告》,格式必须严格为:
告警分析报告\n---\n# 告警处理详情\n## 活跃告警清单\n## 告警根因分析N\n## 处理方案执行N\n## 结论。
5. 若步骤涉及腾讯云日志/主题工具,请确保使用连字符区域 ID(ap-guangzhou 等),或省略 region 以采用默认值。
6. 如果发现 Planner/Executor 在同一方向连续 3 次调用工具仍失败或没有数据,必须终止流程,直接输出"任务无法完成"的报告,明确告知失败原因,严禁凭空编造结果。
只允许在 planner_agent、executor_agent 与 FINISH 之间做出选择。
""";
}
}
@@ -0,0 +1,180 @@
package org.example.service;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.exception.GraphRunnerException;
import org.example.agent.tool.DateTimeTools;
import org.example.agent.tool.InternalDocsTools;
import org.example.agent.tool.QueryLogsTools;
import org.example.agent.tool.QueryMetricsTools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* 聊天服务
* 封装 ReactAgent 对话的公共逻辑,包括模型创建、系统提示词构建、Agent 配置等
*/
@Service
public class ChatService {
private static final Logger logger = LoggerFactory.getLogger(ChatService.class);
@Autowired
private InternalDocsTools internalDocsTools;
@Autowired
private DateTimeTools dateTimeTools;
@Autowired
private QueryMetricsTools queryMetricsTools;
@Autowired(required = false) // Mock 模式下才注册,所以设置为 optional,真实环境通过mcp配置注入
private QueryLogsTools queryLogsTools;
@Autowired
private ToolCallbackProvider tools;
@Value("${spring.ai.dashscope.api-key}")
private String dashScopeApiKey;
/**
* 创建 DashScope API 实例
*/
public DashScopeApi createDashScopeApi() {
return DashScopeApi.builder()
.apiKey(dashScopeApiKey)
.build();
}
/**
* 创建 ChatModel
* @param temperature 控制随机性 (0.0-1.0)
* @param maxToken 最大输出长度
* @param topP 核采样参数
*/
public DashScopeChatModel createChatModel(DashScopeApi dashScopeApi, double temperature, int maxToken, double topP) {
return DashScopeChatModel.builder()
.dashScopeApi(dashScopeApi)
.defaultOptions(DashScopeChatOptions.builder()
.withModel(DashScopeChatModel.DEFAULT_MODEL_NAME)
.withTemperature(temperature)
.withMaxToken(maxToken)
.withTopP(topP)
.build())
.build();
}
/**
* 创建标准对话 ChatModel(默认参数)
*/
public DashScopeChatModel createStandardChatModel(DashScopeApi dashScopeApi) {
return createChatModel(dashScopeApi, 0.7, 2000, 0.9);
}
/**
* 构建系统提示词(包含历史消息)
* @param history 历史消息列表
* @return 完整的系统提示词
*/
public String buildSystemPrompt(List<Map<String, String>> history) {
StringBuilder systemPromptBuilder = new StringBuilder();
// 基础系统提示
systemPromptBuilder.append("你是一个专业的智能助手,可以获取当前时间、查询天气信息、搜索内部文档知识库,以及查询 Prometheus 告警信息。\n");
systemPromptBuilder.append("当用户询问时间相关问题时,使用 getCurrentDateTime 工具。\n");
systemPromptBuilder.append("当用户需要查询公司内部文档、流程、最佳实践或技术指南时,使用 queryInternalDocs 工具。\n");
systemPromptBuilder.append("当用户需要查询 Prometheus 告警、监控指标或系统告警状态时,使用 queryPrometheusAlerts 工具。\n");
systemPromptBuilder.append("当用户需要查询腾讯云日志时,请调用腾讯云mcp服务查询,默认查询地域ap-guangzhou,查询时间范围为近一个月。\n\n");
// 添加历史消息
if (!history.isEmpty()) {
systemPromptBuilder.append("--- 对话历史 ---\n");
for (Map<String, String> msg : history) {
String role = msg.get("role");
String content = msg.get("content");
if ("user".equals(role)) {
systemPromptBuilder.append("用户: ").append(content).append("\n");
} else if ("assistant".equals(role)) {
systemPromptBuilder.append("助手: ").append(content).append("\n");
}
}
systemPromptBuilder.append("--- 对话历史结束 ---\n\n");
}
systemPromptBuilder.append("请基于以上对话历史,回答用户的新问题。");
return systemPromptBuilder.toString();
}
/**
* 动态构建方法工具数组
* 根据 cls.mock-enabled 决定是否包含 QueryLogsTools
*/
public Object[] buildMethodToolsArray() {
if (queryLogsTools != null) {
// Mock 模式:包含 QueryLogsTools
return new Object[]{dateTimeTools, internalDocsTools, queryMetricsTools, queryLogsTools};
} else {
// 真实模式:不包含 QueryLogsTools(由 MCP 提供日志查询功能)
return new Object[]{dateTimeTools, internalDocsTools, queryMetricsTools};
}
}
/**
* 获取工具回调列表,mcp服务提供的工具
*/
public ToolCallback[] getToolCallbacks() {
return tools.getToolCallbacks();
}
/**
* 记录可用工具列表:mcp服务提供的工具
*/
public void logAvailableTools() {
ToolCallback[] toolCallbacks = tools.getToolCallbacks();
logger.info("可用工具列表:");
for (ToolCallback toolCallback : toolCallbacks) {
logger.info(">>> {}", toolCallback.getToolDefinition().name());
}
}
/**
* 创建 ReactAgent
* @param chatModel 聊天模型
* @param systemPrompt 系统提示词
* @return 配置好的 ReactAgent
*/
public ReactAgent createReactAgent(DashScopeChatModel chatModel, String systemPrompt) {
return ReactAgent.builder()
.name("intelligent_assistant")
.model(chatModel)
.systemPrompt(systemPrompt)
.methodTools(buildMethodToolsArray())
.tools(getToolCallbacks())
.build();
}
/**
* 执行 ReactAgent 对话(非流式)
* @param agent ReactAgent 实例
* @param question 用户问题
* @return AI 回复
*/
public String executeChat(ReactAgent agent, String question) throws GraphRunnerException {
logger.info("执行 ReactAgent.call() - 自动处理工具调用");
var response = agent.call(question);
String answer = response.getText();
logger.info("ReactAgent 对话完成,答案长度: {}", answer.length());
return answer;
}
}
@@ -0,0 +1,229 @@
package org.example.service;
import org.example.config.DocumentChunkConfig;
import org.example.dto.DocumentChunk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 文档分片服务
* 负责将长文档切分为多个有语义完整性的小片段
*/
@Service
public class DocumentChunkService {
private static final Logger logger = LoggerFactory.getLogger(DocumentChunkService.class);
@Autowired
private DocumentChunkConfig chunkConfig;
/**
* 智能分片文档
* 优先按照标题、段落边界进行分割,保持语义完整性
*
* @param content 文档内容
* @param filePath 文件路径(用于日志)
* @return 文档分片列表
*/
public List<DocumentChunk> chunkDocument(String content, String filePath) {
List<DocumentChunk> chunks = new ArrayList<>();
if (content == null || content.trim().isEmpty()) {
logger.warn("文档内容为空: {}", filePath);
return chunks;
}
// 1. 首先尝试按标题分割(Markdown格式)
List<Section> sections = splitByHeadings(content);
// 2. 对每个章节进行进一步分片
int globalChunkIndex = 0;
for (Section section : sections) {
List<DocumentChunk> sectionChunks = chunkSection(section, globalChunkIndex);
chunks.addAll(sectionChunks);
globalChunkIndex += sectionChunks.size();
}
logger.info("文档分片完成: {} -> {} 个分片", filePath, chunks.size());
return chunks;
}
/**
* 按照 Markdown 标题分割文档
*/
private List<Section> splitByHeadings(String content) {
List<Section> sections = new ArrayList<>();
// 匹配 Markdown 标题:# 标题, ## 标题, ### 标题等
Pattern headingPattern = Pattern.compile("^(#{1,6})\\s+(.+)$", Pattern.MULTILINE);
Matcher matcher = headingPattern.matcher(content);
int lastEnd = 0;
String currentTitle = null;
while (matcher.find()) {
// 保存上一个章节
if (lastEnd < matcher.start()) {
String sectionContent = content.substring(lastEnd, matcher.start()).trim();
if (!sectionContent.isEmpty()) {
sections.add(new Section(currentTitle, sectionContent, lastEnd));
}
}
// 更新当前标题
currentTitle = matcher.group(2).trim();
lastEnd = matcher.start();
}
// 添加最后一个章节
if (lastEnd < content.length()) {
String sectionContent = content.substring(lastEnd).trim();
if (!sectionContent.isEmpty()) {
sections.add(new Section(currentTitle, sectionContent, lastEnd));
}
}
// 如果没有找到任何标题,将整个文档作为一个章节
if (sections.isEmpty()) {
sections.add(new Section(null, content, 0));
}
return sections;
}
/**
* 对单个章节进行分片
*/
private List<DocumentChunk> chunkSection(Section section, int startChunkIndex) {
List<DocumentChunk> chunks = new ArrayList<>();
String content = section.content;
String title = section.title;
// 如果章节内容小于最大尺寸,直接作为一个分片
if (content.length() <= chunkConfig.getMaxSize()) {
DocumentChunk chunk = new DocumentChunk(
content,
section.startIndex,
section.startIndex + content.length(),
startChunkIndex
);
chunk.setTitle(title);
chunks.add(chunk);
return chunks;
}
// 章节内容较长,需要进一步分片
// 优先在段落边界分割
List<String> paragraphs = splitByParagraphs(content);
StringBuilder currentChunk = new StringBuilder();
int currentStartIndex = section.startIndex;
int chunkIndex = startChunkIndex;
for (String paragraph : paragraphs) {
// 如果当前分片加上新段落超过最大尺寸
if (currentChunk.length() > 0 &&
currentChunk.length() + paragraph.length() > chunkConfig.getMaxSize()) {
// 保存当前分片
String chunkContent = currentChunk.toString().trim();
DocumentChunk chunk = new DocumentChunk(
chunkContent,
currentStartIndex,
currentStartIndex + chunkContent.length(),
chunkIndex++
);
chunk.setTitle(title);
chunks.add(chunk);
// 开始新分片,包含重叠部分
String overlap = getOverlapText(chunkContent);
currentChunk = new StringBuilder(overlap);
currentStartIndex = currentStartIndex + chunkContent.length() - overlap.length();
}
currentChunk.append(paragraph).append("\n\n");
}
// 保存最后一个分片
if (currentChunk.length() > 0) {
String chunkContent = currentChunk.toString().trim();
DocumentChunk chunk = new DocumentChunk(
chunkContent,
currentStartIndex,
currentStartIndex + chunkContent.length(),
chunkIndex
);
chunk.setTitle(title);
chunks.add(chunk);
}
return chunks;
}
/**
* 按段落分割文本
*/
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;
}
/**
* 获取重叠文本
* 从文本末尾提取指定长度的内容作为下一个分片的开头
*/
private String getOverlapText(String text) {
int overlapSize = Math.min(chunkConfig.getOverlap(), text.length());
if (overlapSize <= 0) {
return "";
}
// 从末尾提取重叠内容
String overlap = text.substring(text.length() - overlapSize);
// 尝试在句子边界截断(查找最后一个句号、问号、感叹号)
int lastSentenceEnd = Math.max(
overlap.lastIndexOf('。'),
Math.max(overlap.lastIndexOf('?'), overlap.lastIndexOf('!'))
);
if (lastSentenceEnd > overlapSize / 2) {
return overlap.substring(lastSentenceEnd + 1).trim();
}
return overlap.trim();
}
/**
* 章节数据类
*/
private static class Section {
String title;
String content;
int startIndex;
Section(String title, String content, int startIndex) {
this.title = title;
this.content = content;
this.startIndex = startIndex;
}
}
}
@@ -0,0 +1,233 @@
package org.example.service;
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.Constants;
import io.reactivex.Flowable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* RAG (Retrieval-Augmented Generation) 服务
* 结合向量检索和大语言模型生成答案
*/
@Service
public class RagService {
private static final Logger logger = LoggerFactory.getLogger(RagService.class);
@Autowired
private VectorSearchService vectorSearchService;
@Value("${dashscope.api.key}")
private String apiKey;
@Value("${rag.top-k:3}")
private int topK;
@Value("${rag.model:qwen3-30b-a3b-thinking-2507}")
private String model;
private Generation generation;
@PostConstruct
public void init() {
// 设置 API Key 和 Base URL
Constants.apiKey = apiKey;
Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1";
// 创建 Generation 实例
generation = new Generation();
logger.info("RAG 服务初始化完成,model: {}, topK: {}", model, topK);
}
/**
* 流式处理用户问题(不带历史消息)
*
* @param question 用户问题
* @param callback 流式回调接口
*/
public void queryStream(String question, StreamCallback callback) {
queryStream(question, new ArrayList<>(), callback);
}
/**
* 流式处理用户问题(带历史消息)
*
* @param question 用户问题
* @param history 历史消息列表,格式:[{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
* @param callback 流式回调接口
*/
public void queryStream(String question, List<Map<String, String>> history, StreamCallback callback) {
try {
logger.info("收到 RAG 流式查询: {}", question);
// 1. 从向量数据库检索相关文档
List<VectorSearchService.SearchResult> searchResults =
vectorSearchService.searchSimilarDocuments(question, topK);
// 发送检索结果
callback.onSearchResults(searchResults);
if (searchResults.isEmpty()) {
logger.warn("未找到相关文档");
callback.onComplete("抱歉,我在知识库中没有找到相关信息来回答您的问题。", "");
return;
}
// 2. 构建上下文和提示词
String context = buildContext(searchResults);
String prompt = buildPrompt(question, context);
// 3. 流式调用大语言模型(传入历史消息)
generateAnswerStream(prompt, history, callback);
} catch (Exception e) {
logger.error("RAG 流式查询失败", e);
callback.onError(e);
}
}
/**
* 构建上下文
*/
private String buildContext(List<VectorSearchService.SearchResult> searchResults) {
StringBuilder context = new StringBuilder();
for (int i = 0; i < searchResults.size(); i++) {
VectorSearchService.SearchResult result = searchResults.get(i);
context.append("【参考资料 ").append(i + 1).append("】\n");
context.append(result.getContent()).append("\n\n");
}
return context.toString();
}
/**
* 构建提示词
*/
private String buildPrompt(String question, String context) {
return String.format(
"你是一个专业的AI助手。请根据以下参考资料回答用户的问题。\n\n" +
"参考资料:\n%s\n" +
"用户问题:%s\n\n" +
"请基于上述参考资料给出准确、详细的回答。如果参考资料中没有相关信息,请明确说明。",
context, question
);
}
/**
* 生成答案(流式)
*
* @param prompt 当前问题的提示词
* @param history 历史消息列表
* @param callback 流式回调接口
*/
private void generateAnswerStream(String prompt, List<Map<String, String>> history, StreamCallback callback)
throws NoApiKeyException, ApiException, InputRequiredException {
// 构建消息列表:历史消息 + 当前问题
List<Message> messages = new ArrayList<>();
// 添加历史消息
for (Map<String, String> historyMsg : history) {
String role = historyMsg.get("role");
String content = historyMsg.get("content");
if ("user".equals(role)) {
messages.add(Message.builder()
.role(Role.USER.getValue())
.content(content)
.build());
} else if ("assistant".equals(role)) {
messages.add(Message.builder()
.role(Role.ASSISTANT.getValue())
.content(content)
.build());
}
}
// 添加当前用户问题
Message userMsg = Message.builder()
.role(Role.USER.getValue())
.content(prompt)
.build();
messages.add(userMsg);
logger.debug("发送给AI模型的消息数量: {}(包含 {} 条历史消息)",
messages.size(), history.size());
GenerationParam param = GenerationParam.builder()
.apiKey(apiKey)
.model(model)
.incrementalOutput(true)
.resultFormat("message")
.messages(messages)
.build();
logger.info("开始调用AI模型流式接口...");
Flowable<GenerationResult> result = generation.streamCall(param);
StringBuilder reasoningContent = new StringBuilder();
StringBuilder finalContent = new StringBuilder();
logger.info("开始接收AI模型流式响应...");
result.blockingForEach(message -> {
if (message.getOutput() != null &&
message.getOutput().getChoices() != null &&
!message.getOutput().getChoices().isEmpty()) {
// 获取消息内容
// 注意:qwen3-30b-a3b-thinking-2507 模型会在 content 中返回完整内容
// reasoning 部分可能需要通过特殊方式提取或者直接包含在 content 中
String content = message.getOutput().getChoices().get(0).getMessage().getContent();
if (content != null && !content.isEmpty()) {
logger.debug("收到AI模型内容块: {}", content);
// 对于 thinking 模型,content 可能包含思考过程和最终答案
// 这里我们将所有内容都作为答案返回
finalContent.append(content);
callback.onContentChunk(content);
logger.debug("已调用 onContentChunk 回调");
} else {
logger.debug("收到空内容块,跳过");
}
}
});
logger.info("AI模型流式响应完成,总内容长度: {}", finalContent.length());
callback.onComplete(finalContent.toString(), reasoningContent.toString());
logger.info("已调用 onComplete 回调");
}
/**
* 流式回调接口
*/
public interface StreamCallback {
void onSearchResults(List<VectorSearchService.SearchResult> results);
void onReasoningChunk(String chunk);
void onContentChunk(String chunk);
void onComplete(String fullContent, String fullReasoning);
void onError(Exception e);
}
}
@@ -0,0 +1,247 @@
package org.example.service;
import com.alibaba.dashscope.embeddings.TextEmbedding;
import com.alibaba.dashscope.embeddings.TextEmbeddingParam;
import com.alibaba.dashscope.embeddings.TextEmbeddingResult;
import com.alibaba.dashscope.embeddings.TextEmbeddingOutput;
import com.alibaba.dashscope.embeddings.TextEmbeddingResultItem;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.Constants;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 向量嵌入服务
* 使用阿里云 DashScope Text Embedding API
*/
@Service
public class VectorEmbeddingService {
private static final Logger logger = LoggerFactory.getLogger(VectorEmbeddingService.class);
@Value("${dashscope.api.key}")
private String apiKey;
@Value("${dashscope.embedding.model}")
private String model;
private TextEmbedding textEmbedding;
@PostConstruct
public void init() {
// 验证 API Key
if (apiKey == null || apiKey.trim().isEmpty() || apiKey.equals("your-api-key-here")) {
logger.error("API Key 未正确配置!当前值: {}", apiKey);
throw new IllegalStateException("请设置环境变量 DASHSCOPE_API_KEY 或在 application.yml 中配置正确的 API Key");
}
// 打印 API Key 前缀用于调试(不打印完整 Key 保证安全)
String maskedKey = apiKey.length() > 8 ?
apiKey.substring(0, 8) + "..." + apiKey.substring(apiKey.length() - 4) :
"***";
logger.info("API Key 已加载: {}", maskedKey);
// 设置全局 API Key(确保设置成功)
Constants.apiKey = apiKey;
// 验证 API Key 是否设置成功
if (Constants.apiKey == null || Constants.apiKey.isEmpty()) {
logger.error("Constants.apiKey 设置失败!");
throw new IllegalStateException("API Key 设置到 Constants 失败");
}
logger.info("Constants.apiKey 已设置: {}", Constants.apiKey.substring(0, Math.min(8, Constants.apiKey.length())) + "...");
// 创建 TextEmbedding 实例
textEmbedding = new TextEmbedding();
logger.info("阿里云 DashScope Embedding 服务初始化完成,模型: {}", model);
}
/**
* 生成向量嵌入
* 调用阿里云 DashScope Text Embedding API
*
* @param content 文本内容
* @return 向量嵌入(浮点数列表)
*/
public List<Float> generateEmbedding(String content) {
try {
if (content == null || content.trim().isEmpty()) {
logger.warn("内容为空,无法生成向量");
throw new IllegalArgumentException("内容不能为空");
}
logger.debug("开始生成向量嵌入, 内容长度: {} 字符", content.length());
// 确保 API Key 已设置(防止被其他地方覆盖)
if (Constants.apiKey == null || Constants.apiKey.isEmpty()) {
logger.warn("检测到 Constants.apiKey 为空,重新设置");
Constants.apiKey = apiKey;
}
logger.debug("调用 API 前 Constants.apiKey: {}",
Constants.apiKey != null ? Constants.apiKey.substring(0, Math.min(8, Constants.apiKey.length())) + "..." : "null");
// 构建请求参数
TextEmbeddingParam param = TextEmbeddingParam
.builder()
.model(model)
.texts(Collections.singletonList(content))
.build();
// 调用 API
TextEmbeddingResult result = textEmbedding.call(param);
// 检查结果
List<Float> floatEmbedding = getFloats(result);
logger.info("成功生成向量嵌入, 内容长度: {} 字符, 向量维度: {}",
content.length(), floatEmbedding.size());
return floatEmbedding;
} catch (NoApiKeyException e) {
logger.error("API Key 未设置或无效", e);
throw new RuntimeException("API Key 未设置,请配置 dashscope.api.key", e);
} catch (Exception e) {
logger.error("生成向量嵌入失败, 内容长度: {}", content != null ? content.length() : 0, e);
throw new RuntimeException("生成向量嵌入失败: " + e.getMessage(), e);
}
}
@NotNull
private static List<Float> getFloats(TextEmbeddingResult result) {
if (result == null || result.getOutput() == null || result.getOutput().getEmbeddings() == null) {
throw new RuntimeException("DashScope API 返回空结果");
}
TextEmbeddingOutput output = result.getOutput();
List<TextEmbeddingResultItem> embeddings = output.getEmbeddings();
if (embeddings.isEmpty()) {
throw new RuntimeException("DashScope API 返回空向量列表");
}
// 获取第一个文本的向量
List<Double> embeddingDoubles = embeddings.get(0).getEmbedding();
// 转换为 List<Float>
List<Float> floatEmbedding = new ArrayList<>(embeddingDoubles.size());
for (Double value : embeddingDoubles) {
floatEmbedding.add(value.floatValue());
}
return floatEmbedding;
}
/**
* 批量生成向量嵌入
*
* @param contents 文本内容列表
* @return 向量嵌入列表
*/
public List<List<Float>> generateEmbeddings(List<String> contents) {
try {
if (contents == null || contents.isEmpty()) {
logger.warn("内容列表为空,无法生成向量");
return Collections.emptyList();
}
logger.info("开始批量生成向量嵌入, 数量: {}", contents.size());
// 确保 API Key 已设置
if (Constants.apiKey == null || Constants.apiKey.isEmpty()) {
logger.warn("检测到 Constants.apiKey 为空,重新设置");
Constants.apiKey = apiKey;
}
// 构建请求参数 - 批量输入
TextEmbeddingParam param = TextEmbeddingParam
.builder()
.model(model)
.texts(contents)
.build();
// 调用 API
TextEmbeddingResult result = textEmbedding.call(param);
// 检查结果
if (result == null || result.getOutput() == null || result.getOutput().getEmbeddings() == null) {
throw new RuntimeException("批量 DashScope API 返回空结果");
}
List<TextEmbeddingResultItem> embeddingItems = result.getOutput().getEmbeddings();
if (embeddingItems.isEmpty()) {
throw new RuntimeException("批量 DashScope API 返回空向量列表");
}
// 转换结果
List<List<Float>> embeddings = new ArrayList<>();
for (TextEmbeddingResultItem item : embeddingItems) {
List<Double> embeddingDoubles = item.getEmbedding();
List<Float> embedding = new ArrayList<>(embeddingDoubles.size());
for (Double value : embeddingDoubles) {
embedding.add(value.floatValue());
}
embeddings.add(embedding);
}
logger.info("成功批量生成向量嵌入, 数量: {}, 维度: {}",
embeddings.size(),
embeddings.isEmpty() ? 0 : embeddings.get(0).size());
return embeddings;
} catch (NoApiKeyException e) {
logger.error("批量调用时 API Key 未设置或无效", e);
throw new RuntimeException("API Key 未设置,请配置 dashscope.api.key", e);
} catch (Exception e) {
logger.error("批量生成向量嵌入失败", e);
throw new RuntimeException("批量生成向量嵌入失败: " + e.getMessage(), e);
}
}
/**
* 生成查询向量
*
* @param query 查询文本
* @return 向量嵌入
*/
public List<Float> generateQueryVector(String query) {
return generateEmbedding(query);
}
/**
* 计算两个向量的余弦相似度
*
* @param vector1 向量1
* @param vector2 向量2
* @return 余弦相似度 [-1, 1]
*/
public float calculateCosineSimilarity(List<Float> vector1, List<Float> vector2) {
if (vector1.size() != vector2.size()) {
throw new IllegalArgumentException("向量维度不匹配");
}
float dotProduct = 0.0f;
float norm1 = 0.0f;
float norm2 = 0.0f;
for (int i = 0; i < vector1.size(); i++) {
dotProduct += vector1.get(i) * vector2.get(i);
norm1 += vector1.get(i) * vector1.get(i);
norm2 += vector2.get(i) * vector2.get(i);
}
return dotProduct / (float) (Math.sqrt(norm1) * Math.sqrt(norm2));
}
}
@@ -0,0 +1,351 @@
package org.example.service;
import io.milvus.client.MilvusServiceClient;
import io.milvus.grpc.MutationResult;
import io.milvus.param.R;
import io.milvus.param.RpcStatus;
import io.milvus.param.collection.LoadCollectionParam;
import io.milvus.param.dml.DeleteParam;
import io.milvus.param.dml.InsertParam;
import lombok.Getter;
import lombok.Setter;
import org.example.constant.MilvusConstants;
import org.example.dto.DocumentChunk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.*;
/**
* 向量索引服务
* 负责读取文件、生成向量、存储到 Milvus
*/
@Service
public class VectorIndexService {
private static final Logger logger = LoggerFactory.getLogger(VectorIndexService.class);
@Autowired
private MilvusServiceClient milvusClient;
@Autowired
private VectorEmbeddingService embeddingService;
@Autowired
private DocumentChunkService chunkService;
@Value("${file.upload.path}")
private String uploadPath;
/**
* 索引指定目录下的所有文件
*
* @param directoryPath 目录路径(可选,默认使用配置的上传目录)
* @return 索引结果 这里可以优化:定时重建目录下所有文件的索引
*/
public IndexingResult indexDirectory(String directoryPath) {
IndexingResult result = new IndexingResult();
result.setStartTime(LocalDateTime.now());
try {
// 使用指定目录或默认上传目录
String targetPath = (directoryPath != null && !directoryPath.trim().isEmpty())
? directoryPath : uploadPath;
Path dirPath = Paths.get(targetPath).normalize();
File directory = dirPath.toFile();
if (!directory.exists() || !directory.isDirectory()) {
throw new IllegalArgumentException("目录不存在或不是有效目录: " + targetPath);
}
result.setDirectoryPath(directory.getAbsolutePath());
// 获取所有支持的文件
File[] files = directory.listFiles((dir, name) ->
name.endsWith(".txt") || name.endsWith(".md")
);
if (files == null || files.length == 0) {
logger.warn("目录中没有找到支持的文件: {}", targetPath);
result.setTotalFiles(0);
result.setSuccess(true);
result.setEndTime(LocalDateTime.now());
return result;
}
result.setTotalFiles(files.length);
logger.info("开始索引目录: {}, 找到 {} 个文件", targetPath, files.length);
// 遍历并索引每个文件
for (File file : files) {
try {
indexSingleFile(file.getAbsolutePath());
result.incrementSuccessCount();
logger.info("✓ 文件索引成功: {}", file.getName());
} catch (Exception e) {
result.incrementFailCount();
result.addFailedFile(file.getAbsolutePath(), e.getMessage());
logger.error("✗ 文件索引失败: {}", file.getName(), e);
}
}
result.setSuccess(result.getFailCount() == 0);
result.setEndTime(LocalDateTime.now());
logger.info("目录索引完成: 总数={}, 成功={}, 失败={}",
result.getTotalFiles(), result.getSuccessCount(), result.getFailCount());
return result;
} catch (Exception e) {
logger.error("索引目录失败", e);
result.setSuccess(false);
result.setErrorMessage(e.getMessage());
result.setEndTime(LocalDateTime.now());
return result;
}
}
/**
* 索引单个文件
*
* @param filePath 文件路径
* @throws Exception 索引失败时抛出异常
*/
public void indexSingleFile(String filePath) throws Exception {
Path path = Paths.get(filePath).normalize();
File file = path.toFile();
if (!file.exists() || !file.isFile()) {
throw new IllegalArgumentException("文件不存在: " + filePath);
}
logger.info("开始索引文件: {}", path);
// 1. 读取文件内容
String content = Files.readString(path);
logger.info("读取文件: {}, 内容长度: {} 字符", path, content.length());
// 2. 删除该文件的旧数据(如果存在)
deleteExistingData(path.toString());
// 3. 文档分片
List<DocumentChunk> chunks = chunkService.chunkDocument(content, path.toString());
logger.info("文档分片完成: {} -> {} 个分片", filePath, chunks.size());
// 4. 为每个分片生成向量并插入 Milvus
for (int i = 0; i < chunks.size(); i++) {
DocumentChunk chunk = chunks.get(i);
try {
// 生成向量
List<Float> vector = embeddingService.generateEmbedding(chunk.getContent());
// 构建元数据(包含文件信息)
Map<String, Object> metadata = buildMetadata(path.toString(), chunk, chunks.size());
// 插入到 Milvus
insertToMilvus(chunk.getContent(), vector, metadata, chunk.getChunkIndex());
logger.info("✓ 分片 {}/{} 索引成功", i + 1, chunks.size());
} catch (Exception e) {
logger.error("✗ 分片 {}/{} 索引失败", i + 1, chunks.size(), e);
throw new RuntimeException("分片索引失败: " + e.getMessage(), e);
}
}
logger.info("文件索引完成: {}, 共 {} 个分片", filePath, chunks.size());
}
/**
* 删除文件的旧数据(根据 metadata._source)
*/
private void deleteExistingData(String filePath) {
try {
// 使用统一的路径分隔符(正斜杠)用于Milvus存储,避免表达式解析错误
// 将系统路径转换为统一格式
Path path = Paths.get(filePath).normalize();
String normalizedPath = path.toString().replace(File.separator, "/");
// 构建删除表达式:metadata["_source"] == "xxx"
String expr = String.format("metadata[\"_source\"] == \"%s\"", normalizedPath);
logger.info("准备删除旧数据,路径: {}, 表达式: {}", normalizedPath, expr);
// 确保 collection 已加载(删除操作需要集合已加载)
R<RpcStatus> loadResponse = milvusClient.loadCollection(
LoadCollectionParam.newBuilder()
.withCollectionName(MilvusConstants.MILVUS_COLLECTION_NAME)
.build()
);
// 状态码 65535 表示集合已经加载,这不是错误
if (loadResponse.getStatus() != 0 && loadResponse.getStatus() != 65535) {
logger.warn("加载 collection 失败: {}", loadResponse.getMessage());
return;
}
DeleteParam deleteParam = DeleteParam.newBuilder()
.withCollectionName(MilvusConstants.MILVUS_COLLECTION_NAME)
.withExpr(expr)
.build();
R<MutationResult> response = milvusClient.delete(deleteParam);
if (response.getStatus() != 0) {
logger.warn("删除旧数据时出现警告: {}", response.getMessage());
} else {
long deletedCount = response.getData().getDeleteCnt();
logger.info("✓ 已删除文件的旧数据: {}, 删除记录数: {}", normalizedPath, deletedCount);
}
} catch (Exception e) {
logger.warn("删除旧数据失败(可能是首次索引): {}", e.getMessage());
}
}
/**
* 构建元数据(包含文件信息)
*/
private Map<String, Object> buildMetadata(String filePath, DocumentChunk chunk, int totalChunks) {
Map<String, Object> metadata = new HashMap<>();
// 标准化路径:使用统一的路径分隔符(正斜杠)用于存储,确保跨平台一致性
Path path = Paths.get(filePath).normalize();
String normalizedPath = path.toString().replace(File.separator, "/");
// 文件信息
Path fileName = path.getFileName();
String fileNameStr = fileName != null ? fileName.toString() : "";
String extension = "";
int dotIndex = fileNameStr.lastIndexOf('.');
if (dotIndex > 0) {
extension = fileNameStr.substring(dotIndex);
}
metadata.put("_source", normalizedPath);
metadata.put("_extension", extension);
metadata.put("_file_name", fileNameStr);
// 分片信息
metadata.put("chunkIndex", chunk.getChunkIndex());
metadata.put("totalChunks", totalChunks);
// 标题信息
if (chunk.getTitle() != null && !chunk.getTitle().isEmpty()) {
metadata.put("title", chunk.getTitle());
}
return metadata;
}
/**
* 插入向量到 Milvus
*/
private void insertToMilvus(String content, List<Float> vector,
Map<String, Object> metadata, int chunkIndex) throws Exception {
try {
// 确保 collection 已加载
R<RpcStatus> loadResponse = milvusClient.loadCollection(
LoadCollectionParam.newBuilder()
.withCollectionName(MilvusConstants.MILVUS_COLLECTION_NAME)
.build()
);
if (loadResponse.getStatus() != 0 && loadResponse.getStatus() != 65535) {
throw new RuntimeException("加载 collection 失败: " + loadResponse.getMessage());
}
// 生成唯一 ID(使用 _source + 分片索引)
String source = (String) metadata.get("_source");
String id = UUID.nameUUIDFromBytes((source + "_" + chunkIndex).getBytes()).toString();
// 构建字段数据
List<InsertParam.Field> fields = new ArrayList<>();
// ID 字段
fields.add(new InsertParam.Field("id", Collections.singletonList(id)));
// content 字段
fields.add(new InsertParam.Field("content", Collections.singletonList(content)));
// vector 字段
fields.add(new InsertParam.Field("vector", Collections.singletonList(vector)));
// metadata 字段(JSON 对象)
com.google.gson.Gson gson = new com.google.gson.Gson();
com.google.gson.JsonObject metadataJson = gson.toJsonTree(metadata).getAsJsonObject();
fields.add(new InsertParam.Field("metadata", Collections.singletonList(metadataJson)));
// 构建插入参数
InsertParam insertParam = InsertParam.newBuilder()
.withCollectionName(MilvusConstants.MILVUS_COLLECTION_NAME)
.withFields(fields)
.build();
// 执行插入
R<MutationResult> insertResponse = milvusClient.insert(insertParam);
if (insertResponse.getStatus() != 0) {
throw new RuntimeException("插入向量失败: " + insertResponse.getMessage());
}
logger.debug("向量插入成功: id={}, source={}, chunk={}", id, source, chunkIndex);
} catch (Exception e) {
logger.error("插入向量到 Milvus 失败", e);
throw e;
}
}
/**
* 索引结果类
*/
@Getter
public static class IndexingResult {
@Setter
private boolean success;
@Setter
private String directoryPath;
@Setter
private int totalFiles;
private int successCount;
private int failCount;
@Setter
private LocalDateTime startTime;
@Setter
private LocalDateTime endTime;
@Setter
private String errorMessage;
private Map<String, String> failedFiles = new HashMap<>();
public void incrementSuccessCount() {
this.successCount++;
}
public void incrementFailCount() {
this.failCount++;
}
public long getDurationMs() {
if (startTime != null && endTime != null) {
return java.time.Duration.between(startTime, endTime).toMillis();
}
return 0;
}
public void addFailedFile(String filePath, String error) {
this.failedFiles.put(filePath, error);
}
}
}
@@ -0,0 +1,108 @@
package org.example.service;
import io.milvus.client.MilvusServiceClient;
import io.milvus.grpc.SearchResults;
import io.milvus.param.R;
import io.milvus.param.dml.SearchParam;
import io.milvus.response.SearchResultsWrapper;
import lombok.Getter;
import lombok.Setter;
import org.example.constant.MilvusConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 向量搜索服务
* 负责从 Milvus 中搜索相似向量
*/
@Service
public class VectorSearchService {
private static final Logger logger = LoggerFactory.getLogger(VectorSearchService.class);
@Autowired
private MilvusServiceClient milvusClient;
@Autowired
private VectorEmbeddingService embeddingService;
/**
* 搜索相似文档
*
* @param query 查询文本
* @param topK 返回最相似的K个结果
* @return 搜索结果列表
*/
public List<SearchResult> searchSimilarDocuments(String query, int topK) {
try {
logger.info("开始搜索相似文档, 查询: {}, topK: {}", query, topK);
// 1. 将查询文本向量化
List<Float> queryVector = embeddingService.generateQueryVector(query);
logger.debug("查询向量生成成功, 维度: {}", queryVector.size());
// 2. 构建搜索参数
SearchParam searchParam = SearchParam.newBuilder()
.withCollectionName(MilvusConstants.MILVUS_COLLECTION_NAME)
.withVectorFieldName("vector")
.withVectors(Collections.singletonList(queryVector))
.withTopK(topK)
.withMetricType(io.milvus.param.MetricType.L2)
.withOutFields(List.of("id", "content", "metadata"))
.withParams("{\"nprobe\":10}")
.build();
// 3. 执行搜索
R<SearchResults> searchResponse = milvusClient.search(searchParam);
if (searchResponse.getStatus() != 0) {
throw new RuntimeException("向量搜索失败: " + searchResponse.getMessage());
}
// 4. 解析搜索结果
SearchResultsWrapper wrapper = new SearchResultsWrapper(searchResponse.getData().getResults());
List<SearchResult> results = new ArrayList<>();
for (int i = 0; i < wrapper.getRowRecords(0).size(); i++) {
SearchResult result = new SearchResult();
result.setId((String) wrapper.getIDScore(0).get(i).get("id"));
result.setContent((String) wrapper.getFieldData("content", 0).get(i));
result.setScore(wrapper.getIDScore(0).get(i).getScore());
// 解析 metadata
Object metadataObj = wrapper.getFieldData("metadata", 0).get(i);
if (metadataObj != null) {
result.setMetadata(metadataObj.toString());
}
results.add(result);
}
logger.info("搜索完成, 找到 {} 个相似文档", results.size());
return results;
} catch (Exception e) {
logger.error("搜索相似文档失败", e);
throw new RuntimeException("搜索失败: " + e.getMessage(), e);
}
}
/**
* 搜索结果类
*/
@Setter
@Getter
public static class SearchResult {
private String id;
private String content;
private float score;
private String metadata;
}
}
@@ -0,0 +1,69 @@
package org.example.tool;
import io.milvus.client.MilvusServiceClient;
import io.milvus.param.ConnectParam;
import io.milvus.param.R;
import io.milvus.param.RpcStatus;
import io.milvus.param.collection.DropCollectionParam;
import io.milvus.param.collection.HasCollectionParam;
/**
* 删除 Milvus Collection 的工具类
* 用于重建 Collection 时清理旧数据
*/
public class DropCollection {
public static void main(String[] args) {
MilvusServiceClient client = null;
try {
// 连接到 Milvus
System.out.println("正在连接到 Milvus localhost:19530...");
client = new MilvusServiceClient(
ConnectParam.newBuilder()
.withHost("localhost")
.withPort(19530)
.build()
);
System.out.println("✓ 连接成功");
String collectionName = "biz";
// 检查 Collection 是否存在
R<Boolean> hasResponse = client.hasCollection(
HasCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build()
);
if (hasResponse.getData()) {
System.out.println("发现 Collection: " + collectionName);
System.out.println("正在删除...");
// 删除 Collection
R<RpcStatus> dropResponse = client.dropCollection(
DropCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build()
);
if (dropResponse.getStatus() == 0) {
System.out.println("✓ Collection 已成功删除");
System.out.println("\n请重启 Spring Boot 应用,它会自动创建新的 FloatVector Collection");
} else {
System.err.println("✗ 删除失败: " + dropResponse.getMessage());
}
} else {
System.out.println("Collection '" + collectionName + "' 不存在");
}
} catch (Exception e) {
System.err.println("错误: " + e.getMessage());
e.printStackTrace();
} finally {
if (client != null) {
client.close();
}
}
}
}
+78
View File
@@ -0,0 +1,78 @@
server:
port: 9900
servlet:
encoding:
charset: UTF-8
enabled: true
force: true
file:
upload:
path: ./uploads
allowed-extensions: txt,md
milvus:
host: localhost
port: 19530
username: ""
password: ""
database: default
timeout: 10000
# Spring AI Alibaba DashScope 配置
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY:your-api-key-here} # 从环境变量读取或使用默认值
chat:
options:
timeout: 180000 # 超时时间180秒(3分钟)
retry:
max-attempts: 3 # 最大重试次数
backoff:
initial-interval: 2000 # 初始重试间隔2秒
multiplier: 2 # 重试间隔倍数
max-interval: 10000 # 最大重试间隔10秒
# Spring AI MCP 客户端配置
# 如果使用mock数据,请注释这部分内容
mcp:
client:
enabled: true
name: tencent-mcp-server
version: 1.0.0
request-timeout: 60s
type: ASYNC
sse:
connections:
tencent-cls:
url: https://mcp-api.tencent-cloud.com
sse-endpoint: /sse/92XXXXXXXXb4 # 完整的SSE端点路径
# 阿里云 DashScope Embedding API 配置
dashscope:
api:
key: ${DASHSCOPE_API_KEY:your-api-key-here} # 从环境变量读取或使用默认值(用于自定义配置)
embedding:
model: text-embedding-v4 # 阿里云文本向量化模型
# 文档分片配置
document:
chunk:
max-size: 800 # 每个分片最大字符数
overlap: 100 # 分片之间的重叠字符数
# RAG 配置
rag:
top-k: 3 # 检索返回的最相似文档数量
model: "qwen3-max" # 大语言模型名称
# Prometheus 配置
prometheus:
base-url: http://localhost:9090
timeout: 10 # 超时时间(秒)
mock-enabled: false # 是否启用 Mock 模式(用于测试)
# CLS 云日志服务配置
cls:
mock-enabled: false # 是否启用 Mock 模式(用于测试,设为 true 返回与告警关联的模拟日志数据)
File diff suppressed because it is too large Load Diff
+132
View File
@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能OnCall助手</title>
<link rel="stylesheet" href="styles.css">
<!-- Markdown 渲染库 -->
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
<!-- 代码高亮库(可选) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github.min.css">
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/es/highlight.min.js"></script>
</head>
<body>
<div class="app-layout">
<!-- 左侧边栏 -->
<aside class="sidebar">
<div class="sidebar-header">
<h2 class="sidebar-title">智能OnCall助手</h2>
</div>
<div class="sidebar-content">
<button class="new-chat-btn" id="newChatBtn">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5V19M5 12H19" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<span>新建对话</span>
</button>
<div class="chat-history-section">
<div class="history-header">
<span>近期对话</span>
</div>
<div class="chat-history-list" id="chatHistoryList">
<!-- 历史对话列表将在这里动态生成 -->
</div>
</div>
</div>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<button class="ai-ops-top-btn" id="aiOpsSidebarBtn">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>AI Ops</span>
</button>
<div class="chat-container">
<div class="welcome-greeting" id="welcomeGreeting">
<p>你好!我是智能Oncall小助手</p>
</div>
<div class="chat-messages" id="chatMessages">
</div>
<div class="chat-input-container">
<div class="input-group-wrapper">
<div class="input-wrapper">
<input type="text" id="messageInput" placeholder="问问智能OnCall助手" maxlength="1000" class="message-input">
<div class="input-bottom-bar">
<div class="tools-btn-wrapper">
<button class="tools-btn" id="toolsBtn" title="更多选项">
<svg class="tools-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="1.5" fill="currentColor"/>
<circle cx="19" cy="12" r="1.5" fill="currentColor"/>
<circle cx="5" cy="12" r="1.5" fill="currentColor"/>
</svg>
</button>
<div class="tools-menu" id="toolsMenu">
<div class="tools-menu-item" id="uploadFileItem">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>上传文件</span>
</div>
</div>
</div>
<div class="right-actions">
<div class="mode-selector-wrapper">
<button class="mode-selector-btn" id="modeSelectorBtn">
<span id="currentModeText">快速</span>
<svg class="dropdown-arrow" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="mode-dropdown" id="modeDropdown">
<div class="dropdown-header">选择对话方式</div>
<div class="dropdown-item active" data-mode="quick">
<div class="dropdown-item-main">
<span>快速</span>
<span class="badge-new">新</span>
</div>
<div class="dropdown-item-sub">快速对话</div>
</div>
<div class="dropdown-item" data-mode="stream">
<div class="dropdown-item-main">
<span>流式</span>
</div>
<div class="dropdown-item-sub">流式对话</div>
</div>
</div>
</div>
<button class="send-btn-circle" id="sendButton" title="发送">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 2L11 13M22 2L15 22L11 13M22 2L2 9L11 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
</div>
</div>
<input type="file" id="fileInput" accept=".txt,.md,.markdown" style="display: none;">
</div>
</div>
</main>
</div>
<!-- 加载遮罩层 -->
<div id="loadingOverlay" class="loading-overlay">
<div class="loading-content">
<div class="loading-spinner"></div>
<div class="loading-text">智能运维分析中,请稍候...</div>
<div class="loading-subtext">后端正在处理,请耐心等待</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
services:
etcd:
container_name: milvus-etcd
image: quay.io/coreos/etcd:v3.5.18
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_SNAPSHOT_COUNT=50000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 30s
timeout: 20s
retries: 3
minio:
container_name: milvus-minio
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
ports:
- "9001:9001"
- "9000:9000"
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
command: minio server /minio_data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
standalone:
container_name: milvus-standalone
image: milvusdb/milvus:v2.5.10
command: ["milvus", "run", "standalone"]
security_opt:
- seccomp:unconfined
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
interval: 30s
start_period: 90s
timeout: 20s
retries: 3
ports:
- "19530:19530"
- "9091:9091"
depends_on:
- "etcd"
- "minio"
# 这是新增的 Attu 服务哦!
attu:
container_name: milvus-attu
image: zilliz/attu:v2.5
ports:
- "8000:3000" # 把本地的 8000 端口映射到容器的 3000 端口 (Attu 默认端口)
environment:
# MILVUS_URL 指向 Docker 网络里的 Milvus standalone 服务
MILVUS_URL: standalone:19530
depends_on:
- standalone # 确保 Milvus 启动后再启动 Attu
networks:
default:
name: milvus